agent-control-plane 0.1.0
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/LICENSE +21 -0
- package/README.md +589 -0
- package/SKILL.md +149 -0
- package/assets/workflow-catalog.json +57 -0
- package/bin/audit-issue-routing.sh +74 -0
- package/bin/issue-resource-class.sh +58 -0
- package/bin/label-follow-up-issues.sh +114 -0
- package/bin/pr-risk.sh +532 -0
- package/bin/sync-pr-labels.sh +112 -0
- package/hooks/heartbeat-hooks.sh +573 -0
- package/hooks/issue-reconcile-hooks.sh +217 -0
- package/hooks/pr-reconcile-hooks.sh +225 -0
- package/npm/bin/agent-control-plane.js +1984 -0
- package/npm/public-bin/agent-control-plane +3 -0
- package/package.json +61 -0
- package/tools/bin/agent-cleanup-worktree +247 -0
- package/tools/bin/agent-github-update-labels +66 -0
- package/tools/bin/agent-init-worktree +216 -0
- package/tools/bin/agent-project-archive-run +52 -0
- package/tools/bin/agent-project-capture-worker +46 -0
- package/tools/bin/agent-project-catch-up-merged-prs +137 -0
- package/tools/bin/agent-project-cleanup-session +244 -0
- package/tools/bin/agent-project-detached-launch +107 -0
- package/tools/bin/agent-project-heartbeat-loop +2347 -0
- package/tools/bin/agent-project-open-issue-worktree +89 -0
- package/tools/bin/agent-project-open-pr-worktree +80 -0
- package/tools/bin/agent-project-publish-issue-pr +349 -0
- package/tools/bin/agent-project-reconcile-issue-session +1128 -0
- package/tools/bin/agent-project-reconcile-pr-session +1005 -0
- package/tools/bin/agent-project-retry-state +147 -0
- package/tools/bin/agent-project-run-claude-session +657 -0
- package/tools/bin/agent-project-run-codex-resilient +718 -0
- package/tools/bin/agent-project-run-codex-session +316 -0
- package/tools/bin/agent-project-run-kilo-session +27 -0
- package/tools/bin/agent-project-run-openclaw-session +984 -0
- package/tools/bin/agent-project-run-opencode-session +27 -0
- package/tools/bin/agent-project-sync-anchor-repo +128 -0
- package/tools/bin/agent-project-worker-status +143 -0
- package/tools/bin/audit-agent-worktrees.sh +310 -0
- package/tools/bin/audit-issue-routing.sh +11 -0
- package/tools/bin/audit-retained-layout.sh +58 -0
- package/tools/bin/audit-retained-overlap.sh +135 -0
- package/tools/bin/audit-retained-worktrees.sh +228 -0
- package/tools/bin/branch-verification-guard.sh +351 -0
- package/tools/bin/capture-worker.sh +18 -0
- package/tools/bin/check-skill-contracts.sh +324 -0
- package/tools/bin/cleanup-worktree.sh +44 -0
- package/tools/bin/codex-quota +31 -0
- package/tools/bin/create-follow-up-issue.sh +114 -0
- package/tools/bin/dashboard-launchd-bootstrap.sh +38 -0
- package/tools/bin/flow-config-lib.sh +2127 -0
- package/tools/bin/flow-resident-worker-lib.sh +683 -0
- package/tools/bin/flow-runtime-doctor.sh +97 -0
- package/tools/bin/flow-shell-lib.sh +266 -0
- package/tools/bin/heartbeat-recovery-preflight.sh +106 -0
- package/tools/bin/heartbeat-safe-auto.sh +551 -0
- package/tools/bin/install-dashboard-launchd.sh +152 -0
- package/tools/bin/install-project-launchd.sh +219 -0
- package/tools/bin/issue-publish-scope-guard.sh +242 -0
- package/tools/bin/issue-requires-local-workspace-install.sh +31 -0
- package/tools/bin/issue-resource-class.sh +12 -0
- package/tools/bin/kick-scheduler.sh +75 -0
- package/tools/bin/label-follow-up-issues.sh +14 -0
- package/tools/bin/new-pr-worktree.sh +50 -0
- package/tools/bin/new-worktree.sh +49 -0
- package/tools/bin/pr-risk.sh +12 -0
- package/tools/bin/prepare-worktree.sh +140 -0
- package/tools/bin/profile-activate.sh +109 -0
- package/tools/bin/profile-adopt.sh +219 -0
- package/tools/bin/profile-smoke.sh +461 -0
- package/tools/bin/project-init.sh +189 -0
- package/tools/bin/project-launchd-bootstrap.sh +54 -0
- package/tools/bin/project-remove.sh +155 -0
- package/tools/bin/project-runtime-supervisor.sh +56 -0
- package/tools/bin/project-runtimectl.sh +586 -0
- package/tools/bin/provider-cooldown-state.sh +166 -0
- package/tools/bin/publish-issue-worker.sh +31 -0
- package/tools/bin/reconcile-issue-worker.sh +34 -0
- package/tools/bin/reconcile-pr-worker.sh +34 -0
- package/tools/bin/record-verification.sh +71 -0
- package/tools/bin/render-architecture-infographics.sh +110 -0
- package/tools/bin/render-dashboard-demo-media.sh +333 -0
- package/tools/bin/render-dashboard-snapshot.py +16 -0
- package/tools/bin/render-flow-config.sh +86 -0
- package/tools/bin/retry-state.sh +31 -0
- package/tools/bin/reuse-issue-worktree.sh +75 -0
- package/tools/bin/run-codex-bypass.sh +3 -0
- package/tools/bin/run-codex-safe.sh +3 -0
- package/tools/bin/run-codex-task.sh +231 -0
- package/tools/bin/scaffold-profile.sh +374 -0
- package/tools/bin/serve-dashboard.sh +5 -0
- package/tools/bin/split-retained-slice.sh +124 -0
- package/tools/bin/start-issue-worker.sh +796 -0
- package/tools/bin/start-pr-fix-worker.sh +458 -0
- package/tools/bin/start-pr-merge-repair-worker.sh +8 -0
- package/tools/bin/start-pr-review-worker.sh +227 -0
- package/tools/bin/start-resident-issue-loop.sh +908 -0
- package/tools/bin/sync-agent-repo.sh +52 -0
- package/tools/bin/sync-dependency-baseline.sh +247 -0
- package/tools/bin/sync-pr-labels.sh +12 -0
- package/tools/bin/sync-recurring-issue-checklist.sh +274 -0
- package/tools/bin/sync-shared-agent-home.sh +214 -0
- package/tools/bin/sync-vscode-workspace.sh +157 -0
- package/tools/bin/test-smoke.sh +63 -0
- package/tools/bin/uninstall-project-launchd.sh +55 -0
- package/tools/bin/update-github-labels.sh +14 -0
- package/tools/bin/worker-status.sh +19 -0
- package/tools/bin/workflow-catalog.sh +77 -0
- package/tools/dashboard/app.js +286 -0
- package/tools/dashboard/dashboard_snapshot.py +466 -0
- package/tools/dashboard/index.html +41 -0
- package/tools/dashboard/server.py +64 -0
- package/tools/dashboard/styles.css +351 -0
- package/tools/templates/issue-prompt-template.md +109 -0
- package/tools/templates/pr-fix-template.md +120 -0
- package/tools/templates/pr-merge-repair-template.md +91 -0
- package/tools/templates/pr-review-template.md +62 -0
- package/tools/templates/scheduled-issue-prompt-template.md +62 -0
- package/tools/tests/test-agent-control-plane-npm-cli.sh +279 -0
- package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +56 -0
- package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +89 -0
- package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +82 -0
- package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +90 -0
- package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +73 -0
- package/tools/tests/test-agent-project-claude-session-wrapper.sh +103 -0
- package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +90 -0
- package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +90 -0
- package/tools/tests/test-agent-project-codex-live-thread-persist.sh +76 -0
- package/tools/tests/test-agent-project-codex-recovery.sh +731 -0
- package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +105 -0
- package/tools/tests/test-agent-project-codex-session-wrapper.sh +97 -0
- package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +81 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +109 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +89 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +113 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +135 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +179 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +119 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +91 -0
- package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +117 -0
- package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +148 -0
- package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +146 -0
- package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +145 -0
- package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +147 -0
- package/tools/tests/test-agent-project-reconcile-failure-reason.sh +456 -0
- package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +96 -0
- package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +90 -0
- package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +212 -0
- package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +207 -0
- package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +101 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +113 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +117 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +55 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +125 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +118 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +144 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +145 -0
- package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +139 -0
- package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +242 -0
- package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +142 -0
- package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +106 -0
- package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +66 -0
- package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +129 -0
- package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +69 -0
- package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +43 -0
- package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +46 -0
- package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +79 -0
- package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +56 -0
- package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +72 -0
- package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +125 -0
- package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +178 -0
- package/tools/tests/test-codex-quota-wrapper.sh +37 -0
- package/tools/tests/test-contribution-docs.sh +18 -0
- package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +343 -0
- package/tools/tests/test-create-follow-up-issue.sh +73 -0
- package/tools/tests/test-dashboard-launchd-bootstrap.sh +55 -0
- package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +30 -0
- package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +48 -0
- package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +85 -0
- package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +60 -0
- package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +64 -0
- package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +77 -0
- package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +52 -0
- package/tools/tests/test-flow-resident-reap-stale-controllers.sh +63 -0
- package/tools/tests/test-flow-resolve-codex-quota-tools.sh +104 -0
- package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +27 -0
- package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +79 -0
- package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +115 -0
- package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +117 -0
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +96 -0
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +96 -0
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +96 -0
- package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +170 -0
- package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +201 -0
- package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +201 -0
- package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +160 -0
- package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +133 -0
- package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +157 -0
- package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +181 -0
- package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +160 -0
- package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +134 -0
- package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +162 -0
- package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +136 -0
- package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +139 -0
- package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +119 -0
- package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +140 -0
- package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +142 -0
- package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +141 -0
- package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +65 -0
- package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +179 -0
- package/tools/tests/test-install-dashboard-launchd.sh +78 -0
- package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +87 -0
- package/tools/tests/test-install-project-launchd.sh +110 -0
- package/tools/tests/test-issue-local-workspace-install-policy.sh +81 -0
- package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +70 -0
- package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +36 -0
- package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +47 -0
- package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +132 -0
- package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +64 -0
- package/tools/tests/test-package-funding-metadata.sh +21 -0
- package/tools/tests/test-package-public-metadata.sh +62 -0
- package/tools/tests/test-placeholder-worker-adapters.sh +38 -0
- package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +110 -0
- package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +70 -0
- package/tools/tests/test-pr-risk-fix-label-semantics.sh +114 -0
- package/tools/tests/test-pr-risk-local-first-no-checks.sh +70 -0
- package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +67 -0
- package/tools/tests/test-profile-activate.sh +33 -0
- package/tools/tests/test-profile-adopt-allow-missing-repo.sh +68 -0
- package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +61 -0
- package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +90 -0
- package/tools/tests/test-profile-smoke-collision.sh +44 -0
- package/tools/tests/test-profile-smoke-invalid-claude-config.sh +31 -0
- package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +68 -0
- package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +36 -0
- package/tools/tests/test-profile-smoke.sh +45 -0
- package/tools/tests/test-project-init-force-and-skip-sync.sh +61 -0
- package/tools/tests/test-project-init-repo-slug-mismatch.sh +29 -0
- package/tools/tests/test-project-init.sh +66 -0
- package/tools/tests/test-project-launchd-bootstrap.sh +66 -0
- package/tools/tests/test-project-remove.sh +150 -0
- package/tools/tests/test-project-runtime-supervisor.sh +47 -0
- package/tools/tests/test-project-runtimectl-launchd.sh +115 -0
- package/tools/tests/test-project-runtimectl-missing-profile.sh +54 -0
- package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +108 -0
- package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +95 -0
- package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +59 -0
- package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +85 -0
- package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +78 -0
- package/tools/tests/test-project-runtimectl.sh +212 -0
- package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +39 -0
- package/tools/tests/test-provider-cooldown-state.sh +59 -0
- package/tools/tests/test-public-repo-docs.sh +159 -0
- package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +75 -0
- package/tools/tests/test-render-dashboard-snapshot.sh +149 -0
- package/tools/tests/test-render-flow-config-demo-profile.sh +36 -0
- package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +81 -0
- package/tools/tests/test-render-flow-config.sh +52 -0
- package/tools/tests/test-run-codex-task-claude-routing.sh +125 -0
- package/tools/tests/test-run-codex-task-codex-resident-routing.sh +108 -0
- package/tools/tests/test-run-codex-task-kilo-routing.sh +98 -0
- package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +117 -0
- package/tools/tests/test-run-codex-task-openclaw-routing.sh +113 -0
- package/tools/tests/test-run-codex-task-opencode-routing.sh +98 -0
- package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +146 -0
- package/tools/tests/test-scaffold-profile.sh +108 -0
- package/tools/tests/test-serve-dashboard.sh +93 -0
- package/tools/tests/test-start-issue-worker-blocked-context.sh +129 -0
- package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +189 -0
- package/tools/tests/test-start-issue-worker-local-install-routing.sh +157 -0
- package/tools/tests/test-start-issue-worker-profile-template-routing.sh +149 -0
- package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +212 -0
- package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +219 -0
- package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +155 -0
- package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +199 -0
- package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +275 -0
- package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +185 -0
- package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +152 -0
- package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +186 -0
- package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +212 -0
- package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +148 -0
- package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +194 -0
- package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +198 -0
- package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +145 -0
- package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +67 -0
- package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +70 -0
- package/tools/tests/test-sync-recurring-issue-checklist.sh +95 -0
- package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +66 -0
- package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +47 -0
- package/tools/tests/test-test-smoke.sh +86 -0
- package/tools/tests/test-uninstall-project-launchd.sh +37 -0
- package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +49 -0
- package/tools/tests/test-workflow-catalog.sh +43 -0
- package/tools/vendor/codex-quota/LICENSE +21 -0
- package/tools/vendor/codex-quota/README.md +459 -0
- package/tools/vendor/codex-quota/codex-quota.js +261 -0
- package/tools/vendor/codex-quota/lib/claude-accounts.js +226 -0
- package/tools/vendor/codex-quota/lib/claude-oauth.js +174 -0
- package/tools/vendor/codex-quota/lib/claude-tokens.js +471 -0
- package/tools/vendor/codex-quota/lib/claude-usage.js +929 -0
- package/tools/vendor/codex-quota/lib/codex-accounts.js +205 -0
- package/tools/vendor/codex-quota/lib/codex-tokens.js +326 -0
- package/tools/vendor/codex-quota/lib/codex-usage.js +32 -0
- package/tools/vendor/codex-quota/lib/color.js +72 -0
- package/tools/vendor/codex-quota/lib/constants.js +57 -0
- package/tools/vendor/codex-quota/lib/container.js +143 -0
- package/tools/vendor/codex-quota/lib/display.js +1111 -0
- package/tools/vendor/codex-quota/lib/fs.js +63 -0
- package/tools/vendor/codex-quota/lib/handlers.js +2060 -0
- package/tools/vendor/codex-quota/lib/jwt.js +33 -0
- package/tools/vendor/codex-quota/lib/oauth.js +486 -0
- package/tools/vendor/codex-quota/lib/paths.js +34 -0
- package/tools/vendor/codex-quota/lib/prompts.js +44 -0
- package/tools/vendor/codex-quota/lib/sync.js +1438 -0
- package/tools/vendor/codex-quota/lib/token-match.js +96 -0
- package/tools/vendor/codex-quota-manager/scripts/auto-switch.sh +500 -0
- package/tools/vendor/codex-quota-manager/scripts/batch-add.sh +123 -0
|
@@ -0,0 +1,2060 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subcommand handlers (add, switch, sync, list, remove, quota, etc.)
|
|
3
|
+
* Depends on: most other modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
MULTI_ACCOUNT_PATHS,
|
|
10
|
+
CLAUDE_CREDENTIALS_PATH,
|
|
11
|
+
CLAUDE_MULTI_ACCOUNT_PATHS,
|
|
12
|
+
PRIMARY_CMD,
|
|
13
|
+
} from "./constants.js";
|
|
14
|
+
import { GREEN, RED, YELLOW, colorize } from "./color.js";
|
|
15
|
+
import { getPackageVersion } from "./color.js";
|
|
16
|
+
import {
|
|
17
|
+
shortenPath,
|
|
18
|
+
drawBox,
|
|
19
|
+
buildAccountUsageLines,
|
|
20
|
+
buildClaudeUsageLines,
|
|
21
|
+
formatExpiryStatus,
|
|
22
|
+
printHelp,
|
|
23
|
+
printHelpCodex,
|
|
24
|
+
printHelpClaude,
|
|
25
|
+
printHelpAdd,
|
|
26
|
+
printHelpCodexReauth,
|
|
27
|
+
printHelpSwitch,
|
|
28
|
+
printHelpCodexSync,
|
|
29
|
+
printHelpList,
|
|
30
|
+
printHelpRemove,
|
|
31
|
+
printHelpQuota,
|
|
32
|
+
printHelpClaudeAdd,
|
|
33
|
+
printHelpClaudeReauth,
|
|
34
|
+
printHelpClaudeSwitch,
|
|
35
|
+
printHelpClaudeSync,
|
|
36
|
+
printHelpClaudeList,
|
|
37
|
+
printHelpClaudeRemove,
|
|
38
|
+
printHelpClaudeQuota,
|
|
39
|
+
} from "./display.js";
|
|
40
|
+
import {
|
|
41
|
+
generatePKCE,
|
|
42
|
+
generateState,
|
|
43
|
+
buildAuthUrl,
|
|
44
|
+
checkPortAvailable,
|
|
45
|
+
openBrowser,
|
|
46
|
+
startCallbackServer,
|
|
47
|
+
exchangeCodeForTokens,
|
|
48
|
+
} from "./oauth.js";
|
|
49
|
+
import {
|
|
50
|
+
buildClaudeAuthUrl,
|
|
51
|
+
parseClaudeCodeState,
|
|
52
|
+
exchangeClaudeCodeForTokens,
|
|
53
|
+
handleClaudeOAuthFlow,
|
|
54
|
+
} from "./claude-oauth.js";
|
|
55
|
+
import {
|
|
56
|
+
loadAccountsFromEnv,
|
|
57
|
+
loadAccountsFromFile,
|
|
58
|
+
loadAccountFromCodexCli,
|
|
59
|
+
loadAllAccounts,
|
|
60
|
+
loadAllAccountsNoDedup,
|
|
61
|
+
findAccountByLabel,
|
|
62
|
+
getAllLabels,
|
|
63
|
+
isValidAccount,
|
|
64
|
+
readCodexActiveStoreContainer,
|
|
65
|
+
getCodexActiveLabelInfo,
|
|
66
|
+
} from "./codex-accounts.js";
|
|
67
|
+
import {
|
|
68
|
+
loadClaudeAccounts,
|
|
69
|
+
loadClaudeAccountsFromFile,
|
|
70
|
+
findClaudeAccountByLabel,
|
|
71
|
+
getClaudeLabels,
|
|
72
|
+
getClaudeActiveLabelInfo,
|
|
73
|
+
readClaudeActiveStoreContainer,
|
|
74
|
+
findClaudeSessionKey,
|
|
75
|
+
} from "./claude-accounts.js";
|
|
76
|
+
import {
|
|
77
|
+
updateOpencodeAuth,
|
|
78
|
+
updatePiAuth,
|
|
79
|
+
persistOpenAiOAuthTokens,
|
|
80
|
+
ensureFreshToken,
|
|
81
|
+
} from "./codex-tokens.js";
|
|
82
|
+
import {
|
|
83
|
+
updateClaudeCredentials,
|
|
84
|
+
updateOpencodeClaudeAuth,
|
|
85
|
+
updatePiClaudeAuth,
|
|
86
|
+
persistClaudeOAuthTokens,
|
|
87
|
+
ensureFreshClaudeOAuthToken,
|
|
88
|
+
} from "./claude-tokens.js";
|
|
89
|
+
import { fetchUsage } from "./codex-usage.js";
|
|
90
|
+
import {
|
|
91
|
+
loadClaudeOAuthFromClaudeCode,
|
|
92
|
+
loadClaudeOAuthFromOpenCode,
|
|
93
|
+
loadClaudeOAuthFromEnv,
|
|
94
|
+
loadAllClaudeOAuthAccounts,
|
|
95
|
+
fetchClaudeOAuthUsage,
|
|
96
|
+
fetchClaudeOAuthUsageForAccount,
|
|
97
|
+
fetchClaudeUsage,
|
|
98
|
+
deduplicateClaudeOAuthAccounts,
|
|
99
|
+
deduplicateClaudeResultsByUsage,
|
|
100
|
+
} from "./claude-usage.js";
|
|
101
|
+
import { readMultiAccountContainer, writeMultiAccountContainer, mapContainerAccounts } from "./container.js";
|
|
102
|
+
import { writeFileAtomic } from "./fs.js";
|
|
103
|
+
import { getOpencodeAuthPath, getCodexCliAuthPath, getPiAuthPath } from "./paths.js";
|
|
104
|
+
import { extractAccountId, extractProfile } from "./jwt.js";
|
|
105
|
+
import { promptConfirm, promptInput } from "./prompts.js";
|
|
106
|
+
import {
|
|
107
|
+
detectCodexDivergence,
|
|
108
|
+
detectClaudeDivergence,
|
|
109
|
+
setCodexActiveLabel,
|
|
110
|
+
setClaudeActiveLabel,
|
|
111
|
+
getActiveAccountId,
|
|
112
|
+
getActiveAccountInfo,
|
|
113
|
+
findFresherOpenAiOAuthStore,
|
|
114
|
+
findFresherClaudeOAuthStore,
|
|
115
|
+
findClaudeOAuthRecoveryStore,
|
|
116
|
+
findCodexAccountByLabelInFiles,
|
|
117
|
+
clearCodexQuotaLabelForRemovedAccount,
|
|
118
|
+
maybeImportClaudeOauthStores,
|
|
119
|
+
getActiveClaudeAccountFromStore,
|
|
120
|
+
handleCodexSync,
|
|
121
|
+
handleClaudeSync,
|
|
122
|
+
} from "./sync.js";
|
|
123
|
+
|
|
124
|
+
// Handlers extracted from codex-quota.js
|
|
125
|
+
export async function handleAdd(args, flags) {
|
|
126
|
+
// Extract optional label from args (can be overridden after auth)
|
|
127
|
+
let label = args[0] || null;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// 1. Check if port is available before starting
|
|
131
|
+
const portAvailable = await checkPortAvailable(1455);
|
|
132
|
+
if (!portAvailable) {
|
|
133
|
+
throw new Error(`Port 1455 is in use. Close other ${PRIMARY_CMD} instances and retry.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Generate PKCE code verifier and challenge
|
|
137
|
+
const { verifier, challenge } = generatePKCE();
|
|
138
|
+
|
|
139
|
+
// 3. Generate random state for CSRF protection
|
|
140
|
+
const state = generateState();
|
|
141
|
+
|
|
142
|
+
// 4. Build authorization URL
|
|
143
|
+
const authUrl = buildAuthUrl(challenge, state);
|
|
144
|
+
|
|
145
|
+
// 5. Print starting message
|
|
146
|
+
console.log("Starting OAuth authentication...");
|
|
147
|
+
|
|
148
|
+
// 6. Start callback server (in background)
|
|
149
|
+
const callbackPromise = startCallbackServer(state);
|
|
150
|
+
|
|
151
|
+
// 7. Open browser or print URL
|
|
152
|
+
openBrowser(authUrl, { noBrowser: flags.noBrowser });
|
|
153
|
+
|
|
154
|
+
// 8. Wait for callback with auth code
|
|
155
|
+
console.log("Waiting for browser authentication...");
|
|
156
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
157
|
+
|
|
158
|
+
// 9. Verify state matches (already done in startCallbackServer, but double-check)
|
|
159
|
+
if (returnedState !== state) {
|
|
160
|
+
throw new Error("State mismatch. Possible CSRF attack.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 10. Exchange code for tokens
|
|
164
|
+
console.log("Exchanging code for tokens...");
|
|
165
|
+
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
166
|
+
|
|
167
|
+
// 11. Derive label from email if not provided
|
|
168
|
+
if (!label && tokens.email) {
|
|
169
|
+
// Use email prefix as suggested label (e.g., "john" from "john@example.com")
|
|
170
|
+
label = tokens.email.split("@")[0].toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
171
|
+
}
|
|
172
|
+
if (!label) {
|
|
173
|
+
// Fallback to generic label with timestamp
|
|
174
|
+
label = `account-${Date.now()}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 12. Check for duplicate labels
|
|
178
|
+
const existingLabels = getAllLabels();
|
|
179
|
+
if (existingLabels.includes(label)) {
|
|
180
|
+
throw new Error(`Label "${label}" already exists. Use a different label or remove the existing one.\nExisting labels: ${existingLabels.join(", ")}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 13. Validate label format (alphanumeric with hyphens/underscores)
|
|
184
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(label)) {
|
|
185
|
+
throw new Error(`Invalid label "${label}". Use only letters, numbers, hyphens, and underscores.`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 14. Create new account object
|
|
189
|
+
const newAccount = {
|
|
190
|
+
label: label,
|
|
191
|
+
accountId: tokens.accountId,
|
|
192
|
+
access: tokens.accessToken,
|
|
193
|
+
refresh: tokens.refreshToken,
|
|
194
|
+
idToken: tokens.idToken,
|
|
195
|
+
expires: tokens.expires,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// 15. Determine target file and save
|
|
199
|
+
const targetPath = MULTI_ACCOUNT_PATHS[0]; // ~/.codex-accounts.json
|
|
200
|
+
const container = readMultiAccountContainer(targetPath);
|
|
201
|
+
const accounts = [...container.accounts, newAccount];
|
|
202
|
+
writeMultiAccountContainer(targetPath, container, accounts, {}, { mode: 0o600 });
|
|
203
|
+
|
|
204
|
+
// 16. Print success message (human-readable OR JSON, not both)
|
|
205
|
+
if (flags.json) {
|
|
206
|
+
console.log(JSON.stringify({
|
|
207
|
+
success: true,
|
|
208
|
+
label: label,
|
|
209
|
+
email: tokens.email,
|
|
210
|
+
accountId: tokens.accountId,
|
|
211
|
+
source: targetPath,
|
|
212
|
+
}, null, 2));
|
|
213
|
+
} else {
|
|
214
|
+
const emailDisplay = tokens.email ? ` <${tokens.email}>` : "";
|
|
215
|
+
const lines = [
|
|
216
|
+
colorize(`Added account ${label}${emailDisplay}`, GREEN),
|
|
217
|
+
"",
|
|
218
|
+
`Saved to: ${shortenPath(targetPath)}`,
|
|
219
|
+
"",
|
|
220
|
+
`Run 'cq codex switch ${label}' to activate this account`,
|
|
221
|
+
];
|
|
222
|
+
const boxLines = drawBox(lines);
|
|
223
|
+
console.log(boxLines.join("\n"));
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
// Handle specific error types with user-friendly messages (JSON OR human-readable, not both)
|
|
227
|
+
if (flags.json) {
|
|
228
|
+
console.log(JSON.stringify({
|
|
229
|
+
success: false,
|
|
230
|
+
error: error.message,
|
|
231
|
+
}, null, 2));
|
|
232
|
+
} else if (error.message.includes("Port 1455")) {
|
|
233
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
234
|
+
} else if (error.message.includes("timed out")) {
|
|
235
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
236
|
+
} else if (error.message.includes("cancelled")) {
|
|
237
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
238
|
+
} else if (error.message.includes("State mismatch")) {
|
|
239
|
+
console.error(colorize("Error: State mismatch. Possible CSRF attack.", RED));
|
|
240
|
+
} else if (error.message.includes("Token exchange failed")) {
|
|
241
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
242
|
+
} else if (error.message.includes("OAuth error")) {
|
|
243
|
+
console.error(colorize(`Error: Authentication was denied or cancelled.`, RED));
|
|
244
|
+
} else {
|
|
245
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handle reauth subcommand - re-authenticate an existing Codex account via OAuth browser flow
|
|
254
|
+
* This updates the existing account's tokens without changing the label
|
|
255
|
+
* @param {string[]} args - Non-flag arguments (label is required)
|
|
256
|
+
* @param {{ json: boolean, noBrowser: boolean }} flags - Parsed flags
|
|
257
|
+
*/
|
|
258
|
+
export async function handleCodexReauth(args, flags) {
|
|
259
|
+
const label = args[0];
|
|
260
|
+
if (!label) {
|
|
261
|
+
if (flags.json) {
|
|
262
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
263
|
+
} else {
|
|
264
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} codex reauth <label>`, RED));
|
|
265
|
+
console.error("Re-authenticates an existing account via OAuth browser flow.");
|
|
266
|
+
}
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
// 1. Find existing account by label
|
|
272
|
+
const existingAccount = findAccountByLabel(label);
|
|
273
|
+
if (!existingAccount) {
|
|
274
|
+
const allLabels = getAllLabels();
|
|
275
|
+
if (flags.json) {
|
|
276
|
+
console.log(JSON.stringify({
|
|
277
|
+
success: false,
|
|
278
|
+
error: `Account "${label}" not found`,
|
|
279
|
+
availableLabels: allLabels,
|
|
280
|
+
}, null, 2));
|
|
281
|
+
} else if (allLabels.length === 0) {
|
|
282
|
+
console.error(colorize(`Account "${label}" not found. No accounts configured.`, RED));
|
|
283
|
+
console.error(`Run '${PRIMARY_CMD} codex add' to add an account.`);
|
|
284
|
+
} else {
|
|
285
|
+
console.error(colorize(`Account "${label}" not found.`, RED));
|
|
286
|
+
console.error(`Available: ${allLabels.join(", ")}`);
|
|
287
|
+
}
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const source = existingAccount.source;
|
|
292
|
+
|
|
293
|
+
// 2. Check if account can be re-authenticated (must be in a multi-account file)
|
|
294
|
+
if (source === "env") {
|
|
295
|
+
if (flags.json) {
|
|
296
|
+
console.log(JSON.stringify({
|
|
297
|
+
success: false,
|
|
298
|
+
error: "Cannot re-authenticate account from CODEX_ACCOUNTS env var. Modify the env var directly.",
|
|
299
|
+
}, null, 2));
|
|
300
|
+
} else {
|
|
301
|
+
console.error(colorize("Cannot re-authenticate account from CODEX_ACCOUNTS env var.", RED));
|
|
302
|
+
console.error("Modify the env var directly to update this account.");
|
|
303
|
+
}
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 3. Check if port is available before starting
|
|
308
|
+
const portAvailable = await checkPortAvailable(1455);
|
|
309
|
+
if (!portAvailable) {
|
|
310
|
+
throw new Error(`Port 1455 is in use. Close other ${PRIMARY_CMD} instances and retry.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 4. Generate PKCE code verifier and challenge
|
|
314
|
+
const { verifier, challenge } = generatePKCE();
|
|
315
|
+
|
|
316
|
+
// 5. Generate random state for CSRF protection
|
|
317
|
+
const state = generateState();
|
|
318
|
+
|
|
319
|
+
// 6. Build authorization URL
|
|
320
|
+
const authUrl = buildAuthUrl(challenge, state);
|
|
321
|
+
|
|
322
|
+
// 7. Print starting message
|
|
323
|
+
console.log(`Re-authenticating account "${label}"...`);
|
|
324
|
+
|
|
325
|
+
// 8. Start callback server (in background)
|
|
326
|
+
const callbackPromise = startCallbackServer(state);
|
|
327
|
+
|
|
328
|
+
// 9. Open browser or print URL
|
|
329
|
+
openBrowser(authUrl, { noBrowser: flags.noBrowser });
|
|
330
|
+
|
|
331
|
+
// 10. Wait for callback with auth code
|
|
332
|
+
console.log("Waiting for browser authentication...");
|
|
333
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
334
|
+
|
|
335
|
+
// 11. Verify state matches
|
|
336
|
+
if (returnedState !== state) {
|
|
337
|
+
throw new Error("State mismatch. Possible CSRF attack.");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 12. Exchange code for tokens
|
|
341
|
+
console.log("Exchanging code for tokens...");
|
|
342
|
+
const tokens = await exchangeCodeForTokens(code, verifier);
|
|
343
|
+
|
|
344
|
+
// 13. Update the account entry in the source file
|
|
345
|
+
const container = readMultiAccountContainer(source);
|
|
346
|
+
if (container.rootType === "invalid") {
|
|
347
|
+
throw new Error(`Failed to parse ${source}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const updatedAccounts = container.accounts.map(entry => {
|
|
351
|
+
if (!entry || typeof entry !== "object" || entry.label !== label) {
|
|
352
|
+
return entry;
|
|
353
|
+
}
|
|
354
|
+
// Preserve any extra fields from the existing entry
|
|
355
|
+
return {
|
|
356
|
+
...entry,
|
|
357
|
+
accountId: tokens.accountId,
|
|
358
|
+
access: tokens.accessToken,
|
|
359
|
+
refresh: tokens.refreshToken,
|
|
360
|
+
idToken: tokens.idToken,
|
|
361
|
+
expires: tokens.expires,
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
writeMultiAccountContainer(source, container, updatedAccounts, {}, { mode: 0o600 });
|
|
366
|
+
|
|
367
|
+
// 14. Update CLI auth files if this account is active
|
|
368
|
+
const activeInfo = getCodexActiveLabelInfo();
|
|
369
|
+
if (activeInfo.activeLabel === label) {
|
|
370
|
+
// This is the active account - sync to CLI auth files
|
|
371
|
+
const updatedAccount = {
|
|
372
|
+
label,
|
|
373
|
+
accountId: tokens.accountId,
|
|
374
|
+
access: tokens.accessToken,
|
|
375
|
+
refresh: tokens.refreshToken,
|
|
376
|
+
idToken: tokens.idToken,
|
|
377
|
+
expires: tokens.expires,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Update Codex CLI auth.json
|
|
381
|
+
const codexAuthPath = getCodexCliAuthPath();
|
|
382
|
+
let existingAuth = {};
|
|
383
|
+
if (existsSync(codexAuthPath)) {
|
|
384
|
+
try {
|
|
385
|
+
const raw = readFileSync(codexAuthPath, "utf-8");
|
|
386
|
+
existingAuth = JSON.parse(raw);
|
|
387
|
+
} catch {
|
|
388
|
+
existingAuth = {};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const codexTokens = {
|
|
393
|
+
access_token: tokens.accessToken,
|
|
394
|
+
refresh_token: tokens.refreshToken,
|
|
395
|
+
account_id: tokens.accountId,
|
|
396
|
+
expires_at: Math.floor(tokens.expires / 1000),
|
|
397
|
+
};
|
|
398
|
+
if (tokens.idToken) {
|
|
399
|
+
codexTokens.id_token = tokens.idToken;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const newAuth = {
|
|
403
|
+
...(existingAuth.OPENAI_API_KEY !== undefined ? { OPENAI_API_KEY: existingAuth.OPENAI_API_KEY } : {}),
|
|
404
|
+
tokens: codexTokens,
|
|
405
|
+
last_refresh: new Date().toISOString(),
|
|
406
|
+
codex_quota_label: label,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const codexDir = dirname(codexAuthPath);
|
|
410
|
+
if (!existsSync(codexDir)) {
|
|
411
|
+
mkdirSync(codexDir, { recursive: true });
|
|
412
|
+
}
|
|
413
|
+
writeFileAtomic(codexAuthPath, JSON.stringify(newAuth, null, 2) + "\n", { mode: 0o600 });
|
|
414
|
+
|
|
415
|
+
// Update OpenCode and pi auth files
|
|
416
|
+
updateOpencodeAuth(updatedAccount);
|
|
417
|
+
updatePiAuth(updatedAccount);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 15. Print success message
|
|
421
|
+
if (flags.json) {
|
|
422
|
+
console.log(JSON.stringify({
|
|
423
|
+
success: true,
|
|
424
|
+
label,
|
|
425
|
+
email: tokens.email,
|
|
426
|
+
accountId: tokens.accountId,
|
|
427
|
+
source,
|
|
428
|
+
}, null, 2));
|
|
429
|
+
} else {
|
|
430
|
+
const emailDisplay = tokens.email ? ` <${tokens.email}>` : "";
|
|
431
|
+
const lines = [
|
|
432
|
+
colorize(`Re-authenticated account ${label}${emailDisplay}`, GREEN),
|
|
433
|
+
"",
|
|
434
|
+
`Updated: ${shortenPath(source)}`,
|
|
435
|
+
];
|
|
436
|
+
if (activeInfo.activeLabel === label) {
|
|
437
|
+
lines.push("");
|
|
438
|
+
lines.push("CLI auth files also updated (active account)");
|
|
439
|
+
}
|
|
440
|
+
const boxLines = drawBox(lines);
|
|
441
|
+
console.log(boxLines.join("\n"));
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (flags.json) {
|
|
445
|
+
console.log(JSON.stringify({
|
|
446
|
+
success: false,
|
|
447
|
+
error: error.message,
|
|
448
|
+
}, null, 2));
|
|
449
|
+
} else {
|
|
450
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
451
|
+
}
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Handle switch subcommand - switch active account for Codex CLI/OpenCode/pi auth files
|
|
458
|
+
* @param {string[]} args - Non-flag arguments (label is required)
|
|
459
|
+
* @param {{ json: boolean }} flags - Parsed flags
|
|
460
|
+
*/
|
|
461
|
+
export async function handleSwitch(args, flags) {
|
|
462
|
+
// 1. Extract required label
|
|
463
|
+
const label = args[0];
|
|
464
|
+
if (!label) {
|
|
465
|
+
if (flags.json) {
|
|
466
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
467
|
+
} else {
|
|
468
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} codex switch <label>`, RED));
|
|
469
|
+
console.error("Switches the active account in ~/.codex/auth.json");
|
|
470
|
+
}
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
// 2. Find account by label from all sources
|
|
476
|
+
const account = findAccountByLabel(label);
|
|
477
|
+
if (!account) {
|
|
478
|
+
const allLabels = getAllLabels();
|
|
479
|
+
if (flags.json) {
|
|
480
|
+
console.log(JSON.stringify({
|
|
481
|
+
success: false,
|
|
482
|
+
error: `Account "${label}" not found`,
|
|
483
|
+
availableLabels: allLabels,
|
|
484
|
+
}, null, 2));
|
|
485
|
+
} else if (allLabels.length === 0) {
|
|
486
|
+
console.error(colorize(`Account "${label}" not found. No accounts configured.`, RED));
|
|
487
|
+
console.error(`Run '${PRIMARY_CMD} codex add' to add an account via OAuth.`);
|
|
488
|
+
} else {
|
|
489
|
+
console.error(colorize(`Account "${label}" not found.`, RED));
|
|
490
|
+
console.error(`Available: ${allLabels.join(", ")}`);
|
|
491
|
+
}
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 3. Refresh token if needed (create a temporary array for ensureFreshToken)
|
|
496
|
+
const accountsForRefresh = [account];
|
|
497
|
+
const tokenOk = await ensureFreshToken(account, accountsForRefresh);
|
|
498
|
+
if (!tokenOk) {
|
|
499
|
+
if (flags.json) {
|
|
500
|
+
console.log(JSON.stringify({
|
|
501
|
+
success: false,
|
|
502
|
+
error: `Failed to refresh token for "${label}". Re-authentication may be required.`,
|
|
503
|
+
}, null, 2));
|
|
504
|
+
} else {
|
|
505
|
+
console.error(colorize(`Error: Failed to refresh token for "${label}". Re-authentication may be required.`, RED));
|
|
506
|
+
console.error(`Run '${PRIMARY_CMD} codex add' to re-authenticate this account.`);
|
|
507
|
+
}
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 4. Update activeLabel in the source-of-truth multi-account file
|
|
512
|
+
// Always set activeLabel regardless of account source - the label tracking
|
|
513
|
+
// should work even for accounts loaded from env or single-account files
|
|
514
|
+
let activeLabelPath = null;
|
|
515
|
+
let activeLabelError = null;
|
|
516
|
+
try {
|
|
517
|
+
const activeUpdate = setCodexActiveLabel(label);
|
|
518
|
+
activeLabelPath = activeUpdate.path;
|
|
519
|
+
} catch (err) {
|
|
520
|
+
activeLabelError = err?.message ?? String(err);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 5. Read existing ~/.codex/auth.json to preserve OPENAI_API_KEY
|
|
524
|
+
let existingAuth = {};
|
|
525
|
+
const codexAuthPath = getCodexCliAuthPath();
|
|
526
|
+
if (existsSync(codexAuthPath)) {
|
|
527
|
+
try {
|
|
528
|
+
const raw = readFileSync(codexAuthPath, "utf-8");
|
|
529
|
+
existingAuth = JSON.parse(raw);
|
|
530
|
+
} catch {
|
|
531
|
+
// If corrupted, start fresh
|
|
532
|
+
existingAuth = {};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 6. Build new auth.json structure (matching Codex CLI format)
|
|
537
|
+
const tokens = {
|
|
538
|
+
access_token: account.access,
|
|
539
|
+
refresh_token: account.refresh,
|
|
540
|
+
account_id: account.accountId,
|
|
541
|
+
expires_at: Math.floor(account.expires / 1000), // Convert ms to seconds
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Only include id_token if it exists (Codex CLI rejects null)
|
|
545
|
+
if (account.idToken) {
|
|
546
|
+
tokens.id_token = account.idToken;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const newAuth = {
|
|
550
|
+
// Preserve existing OPENAI_API_KEY if present
|
|
551
|
+
...(existingAuth.OPENAI_API_KEY !== undefined ? { OPENAI_API_KEY: existingAuth.OPENAI_API_KEY } : {}),
|
|
552
|
+
tokens,
|
|
553
|
+
last_refresh: new Date().toISOString(),
|
|
554
|
+
// Track which managed account we switched to (for detecting native login divergence)
|
|
555
|
+
codex_quota_label: label,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// 7. Create ~/.codex directory if needed
|
|
559
|
+
const codexDir = dirname(codexAuthPath);
|
|
560
|
+
if (!existsSync(codexDir)) {
|
|
561
|
+
mkdirSync(codexDir, { recursive: true });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// 8. Write auth.json atomically (temp file + rename) with 0600 permissions
|
|
565
|
+
writeFileAtomic(codexAuthPath, JSON.stringify(newAuth, null, 2) + "\n", { mode: 0o600 });
|
|
566
|
+
|
|
567
|
+
// 9. Update OpenCode auth.json if present
|
|
568
|
+
const opencodeUpdate = updateOpencodeAuth(account);
|
|
569
|
+
if (opencodeUpdate.error && !flags.json) {
|
|
570
|
+
console.error(colorize(`Warning: ${opencodeUpdate.error}`, YELLOW));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 10. Update pi auth.json if present
|
|
574
|
+
const piUpdate = updatePiAuth(account);
|
|
575
|
+
if (piUpdate.error && !flags.json) {
|
|
576
|
+
console.error(colorize(`Warning: ${piUpdate.error}`, YELLOW));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 11. Get profile info for display
|
|
580
|
+
const profile = extractProfile(account.access);
|
|
581
|
+
|
|
582
|
+
// 12. Print confirmation (JSON OR human-readable, not both)
|
|
583
|
+
if (flags.json) {
|
|
584
|
+
const output = {
|
|
585
|
+
success: true,
|
|
586
|
+
label: label,
|
|
587
|
+
email: profile.email,
|
|
588
|
+
accountId: account.accountId,
|
|
589
|
+
authPath: codexAuthPath,
|
|
590
|
+
};
|
|
591
|
+
if (activeLabelPath) {
|
|
592
|
+
output.activeLabelPath = activeLabelPath;
|
|
593
|
+
}
|
|
594
|
+
if (activeLabelError) {
|
|
595
|
+
output.activeLabelError = activeLabelError;
|
|
596
|
+
}
|
|
597
|
+
if (opencodeUpdate.updated) {
|
|
598
|
+
output.opencodeAuthPath = opencodeUpdate.path;
|
|
599
|
+
} else if (opencodeUpdate.error) {
|
|
600
|
+
output.opencodeAuthError = opencodeUpdate.error;
|
|
601
|
+
}
|
|
602
|
+
if (piUpdate.updated) {
|
|
603
|
+
output.piAuthPath = piUpdate.path;
|
|
604
|
+
} else if (piUpdate.error) {
|
|
605
|
+
output.piAuthError = piUpdate.error;
|
|
606
|
+
}
|
|
607
|
+
console.log(JSON.stringify(output, null, 2));
|
|
608
|
+
} else {
|
|
609
|
+
if (activeLabelError) {
|
|
610
|
+
console.error(colorize(`Warning: Failed to update activeLabel: ${activeLabelError}`, YELLOW));
|
|
611
|
+
}
|
|
612
|
+
const emailDisplay = profile.email ? ` <${profile.email}>` : "";
|
|
613
|
+
const planDisplay = profile.planType ? ` (${profile.planType})` : "";
|
|
614
|
+
const lines = [
|
|
615
|
+
colorize(`Switched to ${label}${emailDisplay}${planDisplay}`, GREEN),
|
|
616
|
+
"",
|
|
617
|
+
`Codex CLI: ${shortenPath(codexAuthPath)}`,
|
|
618
|
+
];
|
|
619
|
+
if (activeLabelPath) {
|
|
620
|
+
lines.push(`Active label: ${shortenPath(activeLabelPath)}`);
|
|
621
|
+
}
|
|
622
|
+
if (opencodeUpdate.updated) {
|
|
623
|
+
lines.push(`OpenCode: ${shortenPath(opencodeUpdate.path)}`);
|
|
624
|
+
}
|
|
625
|
+
if (piUpdate.updated) {
|
|
626
|
+
lines.push(`pi: ${shortenPath(piUpdate.path)}`);
|
|
627
|
+
}
|
|
628
|
+
const boxLines = drawBox(lines);
|
|
629
|
+
console.log(boxLines.join("\n"));
|
|
630
|
+
}
|
|
631
|
+
} catch (error) {
|
|
632
|
+
if (flags.json) {
|
|
633
|
+
console.log(JSON.stringify({
|
|
634
|
+
success: false,
|
|
635
|
+
error: error.message,
|
|
636
|
+
}, null, 2));
|
|
637
|
+
} else {
|
|
638
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Handle sync subcommand - bi-directional sync for activeLabel account
|
|
647
|
+
* 1. Pull: if a CLI store has the same refresh token but newer access/expires, pull it back
|
|
648
|
+
* 2. Push: write the (now freshest) account tokens to all CLI auth files
|
|
649
|
+
* @param {string[]} args - Non-flag arguments (unused)
|
|
650
|
+
* @param {{ json: boolean, dryRun?: boolean }} flags - Parsed flags
|
|
651
|
+
*/
|
|
652
|
+
export async function handleList(flags) {
|
|
653
|
+
const codexDivergence = flags.local ? null : detectCodexDivergence({ allowMigration: false });
|
|
654
|
+
const activeLabel = codexDivergence?.activeLabel ?? null;
|
|
655
|
+
const accounts = loadAllAccounts(activeLabel, { local: flags.local });
|
|
656
|
+
|
|
657
|
+
// Handle zero accounts case
|
|
658
|
+
if (!accounts.length) {
|
|
659
|
+
if (flags.json) {
|
|
660
|
+
console.log(JSON.stringify({ accounts: [] }, null, 2));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
console.log("No accounts found.");
|
|
664
|
+
console.log("\nSearched:");
|
|
665
|
+
console.log(" - CODEX_ACCOUNTS env var");
|
|
666
|
+
for (const p of MULTI_ACCOUNT_PATHS) {
|
|
667
|
+
console.log(` - ${p}`);
|
|
668
|
+
}
|
|
669
|
+
console.log(` - ${getCodexCliAuthPath()}`);
|
|
670
|
+
console.log(`\nRun '${PRIMARY_CMD} codex add' to add an account via OAuth.`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const activeAccountId = codexDivergence?.activeAccount?.accountId ?? null;
|
|
675
|
+
const cliAccountId = codexDivergence?.cliAccountId ?? null;
|
|
676
|
+
const cliLabel = codexDivergence?.cliLabel ?? null;
|
|
677
|
+
const divergenceDetected = codexDivergence?.diverged ?? false;
|
|
678
|
+
const nativeAccountId = cliAccountId && (!activeAccountId || cliAccountId !== activeAccountId)
|
|
679
|
+
? cliAccountId
|
|
680
|
+
: null;
|
|
681
|
+
|
|
682
|
+
// Build account details for each account
|
|
683
|
+
const accountDetails = accounts.map(account => {
|
|
684
|
+
const profile = extractProfile(account.access);
|
|
685
|
+
const expiry = formatExpiryStatus(account.expires);
|
|
686
|
+
|
|
687
|
+
const isActive = activeLabel !== null && account.label === activeLabel;
|
|
688
|
+
const isNativeActive = !isActive && nativeAccountId !== null && account.accountId === nativeAccountId;
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
label: account.label,
|
|
692
|
+
email: profile.email,
|
|
693
|
+
accountId: account.accountId,
|
|
694
|
+
planType: profile.planType,
|
|
695
|
+
expires: account.expires,
|
|
696
|
+
expiryStatus: expiry.status,
|
|
697
|
+
expiryDisplay: expiry.display,
|
|
698
|
+
source: account.source,
|
|
699
|
+
isActive,
|
|
700
|
+
isNativeActive,
|
|
701
|
+
};
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// JSON output
|
|
705
|
+
if (flags.json) {
|
|
706
|
+
const output = {
|
|
707
|
+
accounts: accountDetails,
|
|
708
|
+
activeInfo: {
|
|
709
|
+
activeLabel,
|
|
710
|
+
activeAccountId,
|
|
711
|
+
activeStorePath: codexDivergence?.activeStorePath ?? null,
|
|
712
|
+
cliAccountId,
|
|
713
|
+
cliLabel,
|
|
714
|
+
divergence: divergenceDetected,
|
|
715
|
+
migrated: codexDivergence?.migrated ?? false,
|
|
716
|
+
local: flags.local ?? false,
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
console.log(JSON.stringify(output, null, 2));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (divergenceDetected) {
|
|
724
|
+
const activeLabelDisplay = activeLabel ?? "(none)";
|
|
725
|
+
const activeIdDisplay = activeAccountId ?? "(unknown)";
|
|
726
|
+
const cliLabelDisplay = cliLabel ?? "(unknown)";
|
|
727
|
+
const cliIdDisplay = cliAccountId ?? "(unknown)";
|
|
728
|
+
console.error(colorize("Warning: CLI auth diverged from activeLabel", YELLOW));
|
|
729
|
+
console.error(` Active: ${activeLabelDisplay} (${activeIdDisplay})`);
|
|
730
|
+
console.error(` CLI: ${cliLabelDisplay} (${cliIdDisplay})`);
|
|
731
|
+
console.error("");
|
|
732
|
+
console.error(`Run '${PRIMARY_CMD} codex sync' to push active account to CLI.`);
|
|
733
|
+
console.error("");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Human-readable output with box styling
|
|
737
|
+
const lines = [];
|
|
738
|
+
if (accounts.length) {
|
|
739
|
+
lines.push(`Accounts (${accounts.length} total)`);
|
|
740
|
+
lines.push("");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
for (let i = 0; i < accountDetails.length; i++) {
|
|
744
|
+
const detail = accountDetails[i];
|
|
745
|
+
|
|
746
|
+
// Active indicator:
|
|
747
|
+
// * = active account set by codex-quota
|
|
748
|
+
// ~ = native login (not set by us, but currently active in auth.json)
|
|
749
|
+
// = inactive
|
|
750
|
+
let activeMarker = " ";
|
|
751
|
+
let statusText = "";
|
|
752
|
+
if (detail.isActive) {
|
|
753
|
+
activeMarker = "*";
|
|
754
|
+
statusText = " [active]";
|
|
755
|
+
} else if (detail.isNativeActive) {
|
|
756
|
+
activeMarker = "~";
|
|
757
|
+
statusText = " [native]";
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Label and email with plan
|
|
761
|
+
const emailDisplay = detail.email ? ` <${detail.email}>` : "";
|
|
762
|
+
const planDisplay = detail.planType ? ` (${detail.planType})` : "";
|
|
763
|
+
lines.push(`${activeMarker} ${detail.label}${emailDisplay}${planDisplay}${statusText}`);
|
|
764
|
+
|
|
765
|
+
// Details line with expiry and source
|
|
766
|
+
const expiryColor = detail.expiryStatus === "expired" ? "Expired" :
|
|
767
|
+
detail.expiryStatus === "expiring" ? detail.expiryDisplay :
|
|
768
|
+
`Expires: ${detail.expiryDisplay}`;
|
|
769
|
+
lines.push(` ${expiryColor} | ${shortenPath(detail.source)}`);
|
|
770
|
+
|
|
771
|
+
// Add spacing between accounts (but not after the last one)
|
|
772
|
+
if (i < accountDetails.length - 1) {
|
|
773
|
+
lines.push("");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Legend - show appropriate legend based on what markers are present
|
|
778
|
+
const hasActive = accountDetails.some(a => a.isActive);
|
|
779
|
+
const hasNativeActive = accountDetails.some(a => a.isNativeActive);
|
|
780
|
+
|
|
781
|
+
if (hasActive || hasNativeActive) {
|
|
782
|
+
lines.push("");
|
|
783
|
+
if (hasActive) {
|
|
784
|
+
lines.push("* = active (from activeLabel)");
|
|
785
|
+
}
|
|
786
|
+
if (hasNativeActive) {
|
|
787
|
+
lines.push(`~ = CLI auth (run '${PRIMARY_CMD} codex sync' to realign)`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (lines.length) {
|
|
792
|
+
const boxLines = drawBox(lines);
|
|
793
|
+
console.log(boxLines.join("\n"));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Handle Claude list subcommand - list Claude credentials
|
|
800
|
+
* @param {{ json: boolean, local?: boolean }} flags - Parsed flags
|
|
801
|
+
*/
|
|
802
|
+
export async function handleClaudeList(flags) {
|
|
803
|
+
if (!flags.local) {
|
|
804
|
+
const importResult = await maybeImportClaudeOauthStores({ json: flags.json });
|
|
805
|
+
if (importResult.warnings.length && !flags.json) {
|
|
806
|
+
for (const warning of importResult.warnings) {
|
|
807
|
+
console.error(colorize(`Warning: ${warning}`, YELLOW));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const divergence = flags.local ? null : detectClaudeDivergence();
|
|
812
|
+
const activeLabel = divergence?.activeLabel ?? null;
|
|
813
|
+
const claudeAccounts = loadClaudeAccounts();
|
|
814
|
+
|
|
815
|
+
if (!claudeAccounts.length) {
|
|
816
|
+
if (flags.json) {
|
|
817
|
+
console.log(JSON.stringify({ accounts: [] }, null, 2));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
console.log("No Claude accounts found.");
|
|
821
|
+
console.log("\nSearched:");
|
|
822
|
+
console.log(" - CLAUDE_ACCOUNTS env var");
|
|
823
|
+
for (const p of CLAUDE_MULTI_ACCOUNT_PATHS) {
|
|
824
|
+
console.log(` - ${p}`);
|
|
825
|
+
}
|
|
826
|
+
console.log(`\nRun '${PRIMARY_CMD} claude add' to add a Claude credential.`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (flags.json) {
|
|
831
|
+
const output = {
|
|
832
|
+
accounts: claudeAccounts.map(account => ({
|
|
833
|
+
label: account.label,
|
|
834
|
+
source: account.source,
|
|
835
|
+
hasSessionKey: Boolean(account.sessionKey ?? findClaudeSessionKey(account.cookies)),
|
|
836
|
+
hasOauthToken: Boolean(account.oauthToken),
|
|
837
|
+
orgId: account.orgId ?? null,
|
|
838
|
+
isActive: activeLabel !== null && account.label === activeLabel,
|
|
839
|
+
})),
|
|
840
|
+
activeInfo: {
|
|
841
|
+
activeLabel,
|
|
842
|
+
activeStorePath: divergence?.activeStorePath ?? null,
|
|
843
|
+
divergence: divergence?.diverged ?? false,
|
|
844
|
+
skipped: divergence?.skipped ?? false,
|
|
845
|
+
skipReason: divergence?.skipReason ?? null,
|
|
846
|
+
local: flags.local ?? false,
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
console.log(JSON.stringify(output, null, 2));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (divergence?.diverged) {
|
|
854
|
+
const divergedStores = divergence.stores
|
|
855
|
+
.filter(store => store.considered && store.matches === false)
|
|
856
|
+
.map(store => store.name);
|
|
857
|
+
const storeDisplay = divergedStores.length ? divergedStores.join(", ") : "one or more stores";
|
|
858
|
+
console.error(colorize(`Warning: Claude auth diverged from activeLabel (${activeLabel})`, YELLOW));
|
|
859
|
+
console.error(` Diverged stores: ${storeDisplay}`);
|
|
860
|
+
console.error("");
|
|
861
|
+
console.error(`Run '${PRIMARY_CMD} claude sync' to push active account to CLI.`);
|
|
862
|
+
console.error("");
|
|
863
|
+
} else if (divergence?.skipped && divergence.skipReason === "active-account-not-oauth" && activeLabel) {
|
|
864
|
+
console.error("Note: Active Claude account has no OAuth tokens; skipping divergence check.");
|
|
865
|
+
console.error("");
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const claudeLines = [];
|
|
869
|
+
claudeLines.push(`Claude Accounts (${claudeAccounts.length} total)`);
|
|
870
|
+
claudeLines.push("");
|
|
871
|
+
for (let i = 0; i < claudeAccounts.length; i++) {
|
|
872
|
+
const account = claudeAccounts[i];
|
|
873
|
+
const isActive = activeLabel !== null && account.label === activeLabel;
|
|
874
|
+
const marker = isActive ? "*" : " ";
|
|
875
|
+
const statusText = isActive ? " [active]" : "";
|
|
876
|
+
const authParts = [];
|
|
877
|
+
if (account.sessionKey ?? findClaudeSessionKey(account.cookies)) {
|
|
878
|
+
authParts.push("sessionKey");
|
|
879
|
+
}
|
|
880
|
+
if (account.oauthToken) {
|
|
881
|
+
authParts.push("oauthToken");
|
|
882
|
+
}
|
|
883
|
+
const authDisplay = authParts.length ? authParts.join("+") : "unknown";
|
|
884
|
+
claudeLines.push(`${marker} ${account.label}${statusText}`);
|
|
885
|
+
claudeLines.push(` Auth: ${authDisplay} | ${shortenPath(account.source)}`);
|
|
886
|
+
if (i < claudeAccounts.length - 1) {
|
|
887
|
+
claudeLines.push("");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (activeLabel !== null) {
|
|
891
|
+
claudeLines.push("");
|
|
892
|
+
claudeLines.push("* = active (from activeLabel)");
|
|
893
|
+
}
|
|
894
|
+
const claudeBox = drawBox(claudeLines);
|
|
895
|
+
console.log(claudeBox.join("\n"));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Prompt for confirmation using readline
|
|
900
|
+
* @param {string} message - Message to display
|
|
901
|
+
* @returns {Promise<boolean>} True if user confirms (y/Y), false otherwise
|
|
902
|
+
*/
|
|
903
|
+
export async function handleRemove(args, flags) {
|
|
904
|
+
const label = args[0];
|
|
905
|
+
if (!label) {
|
|
906
|
+
if (flags.json) {
|
|
907
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
908
|
+
} else {
|
|
909
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} codex remove <label>`, RED));
|
|
910
|
+
console.error("Removes an account from the multi-account file.");
|
|
911
|
+
}
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Find the account
|
|
916
|
+
const account = findAccountByLabel(label);
|
|
917
|
+
if (!account) {
|
|
918
|
+
const availableLabels = getAllLabels();
|
|
919
|
+
if (flags.json) {
|
|
920
|
+
console.log(JSON.stringify({
|
|
921
|
+
success: false,
|
|
922
|
+
error: `Account "${label}" not found`,
|
|
923
|
+
availableLabels
|
|
924
|
+
}, null, 2));
|
|
925
|
+
} else {
|
|
926
|
+
console.error(colorize(`Account "${label}" not found.`, RED));
|
|
927
|
+
if (availableLabels.length) {
|
|
928
|
+
console.error(`Available labels: ${availableLabels.join(", ")}`);
|
|
929
|
+
} else {
|
|
930
|
+
console.error("No accounts configured.");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const source = account.source;
|
|
937
|
+
|
|
938
|
+
// Check source type
|
|
939
|
+
if (source === "env") {
|
|
940
|
+
if (flags.json) {
|
|
941
|
+
console.log(JSON.stringify({
|
|
942
|
+
success: false,
|
|
943
|
+
error: "Cannot remove account from CODEX_ACCOUNTS env var. Modify the env var directly."
|
|
944
|
+
}, null, 2));
|
|
945
|
+
} else {
|
|
946
|
+
console.error(colorize("Cannot remove account from CODEX_ACCOUNTS env var.", RED));
|
|
947
|
+
console.error("Modify the env var directly to remove this account.");
|
|
948
|
+
}
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Handle Codex CLI auth.json (single account file)
|
|
953
|
+
const codexAuthPath = getCodexCliAuthPath();
|
|
954
|
+
if (source === codexAuthPath) {
|
|
955
|
+
if (!flags.json) {
|
|
956
|
+
console.log(colorize("Warning: This will clear your Codex CLI authentication.", YELLOW));
|
|
957
|
+
console.log(`You will need to re-authenticate using 'codex auth' or '${PRIMARY_CMD} codex add'.`);
|
|
958
|
+
const confirmed = await promptConfirm("Continue?");
|
|
959
|
+
if (!confirmed) {
|
|
960
|
+
console.log("Cancelled.");
|
|
961
|
+
process.exit(0);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Delete the auth.json file
|
|
966
|
+
try {
|
|
967
|
+
unlinkSync(codexAuthPath);
|
|
968
|
+
if (flags.json) {
|
|
969
|
+
console.log(JSON.stringify({
|
|
970
|
+
success: true,
|
|
971
|
+
label,
|
|
972
|
+
source: shortenPath(codexAuthPath),
|
|
973
|
+
message: "Codex CLI auth cleared"
|
|
974
|
+
}, null, 2));
|
|
975
|
+
} else {
|
|
976
|
+
const lines = [
|
|
977
|
+
colorize(`Removed account ${label}`, GREEN),
|
|
978
|
+
"",
|
|
979
|
+
`Deleted: ${shortenPath(codexAuthPath)}`,
|
|
980
|
+
];
|
|
981
|
+
console.log(drawBox(lines).join("\n"));
|
|
982
|
+
}
|
|
983
|
+
} catch (err) {
|
|
984
|
+
if (flags.json) {
|
|
985
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
986
|
+
} else {
|
|
987
|
+
console.error(colorize(`Error removing auth file: ${err.message}`, RED));
|
|
988
|
+
}
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const removedWasActive = detectCodexDivergence().activeLabel === label;
|
|
995
|
+
let activeLabelCleared = false;
|
|
996
|
+
let activeLabelClearError = null;
|
|
997
|
+
let codexQuotaLabelCleared = false;
|
|
998
|
+
let codexQuotaClearError = null;
|
|
999
|
+
|
|
1000
|
+
// Handle multi-account files
|
|
1001
|
+
// Count accounts in the same source file
|
|
1002
|
+
const allAccounts = loadAllAccountsNoDedup();
|
|
1003
|
+
const accountsInSameFile = allAccounts.filter(a => a.source === source);
|
|
1004
|
+
|
|
1005
|
+
if (accountsInSameFile.length === 1) {
|
|
1006
|
+
if (!flags.json) {
|
|
1007
|
+
console.log(colorize("Warning: This is the only account in this file.", YELLOW));
|
|
1008
|
+
console.log(`The file will be deleted: ${shortenPath(source)}`);
|
|
1009
|
+
const confirmed = await promptConfirm("Continue?");
|
|
1010
|
+
if (!confirmed) {
|
|
1011
|
+
console.log("Cancelled.");
|
|
1012
|
+
process.exit(0);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Read the file container directly (to preserve any extra root fields)
|
|
1018
|
+
const container = readMultiAccountContainer(source);
|
|
1019
|
+
if (container.rootType === "invalid") {
|
|
1020
|
+
if (flags.json) {
|
|
1021
|
+
console.log(JSON.stringify({ success: false, error: `Failed to parse ${source}` }, null, 2));
|
|
1022
|
+
} else {
|
|
1023
|
+
console.error(colorize(`Error reading ${source}`, RED));
|
|
1024
|
+
}
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
const existingAccounts = container.accounts;
|
|
1028
|
+
|
|
1029
|
+
// Filter out the account with matching label
|
|
1030
|
+
const updatedAccounts = existingAccounts.filter(a => a.label !== label);
|
|
1031
|
+
|
|
1032
|
+
if (updatedAccounts.length === existingAccounts.length) {
|
|
1033
|
+
// This shouldn't happen if findAccountByLabel worked, but handle it gracefully
|
|
1034
|
+
if (flags.json) {
|
|
1035
|
+
console.log(JSON.stringify({ success: false, error: `Account "${label}" not found in ${source}` }, null, 2));
|
|
1036
|
+
} else {
|
|
1037
|
+
console.error(colorize(`Account "${label}" not found in ${shortenPath(source)}`, RED));
|
|
1038
|
+
}
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Write back or delete
|
|
1043
|
+
try {
|
|
1044
|
+
const fileDeleted = updatedAccounts.length === 0;
|
|
1045
|
+
if (fileDeleted) {
|
|
1046
|
+
// No accounts left - delete the file
|
|
1047
|
+
unlinkSync(source);
|
|
1048
|
+
} else {
|
|
1049
|
+
// Write updated accounts atomically
|
|
1050
|
+
writeMultiAccountContainer(source, container, updatedAccounts, {}, { mode: 0o600 });
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (removedWasActive) {
|
|
1054
|
+
try {
|
|
1055
|
+
const cleared = setCodexActiveLabel(null);
|
|
1056
|
+
activeLabelCleared = cleared.updated;
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
activeLabelClearError = err?.message ?? String(err);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
const cleared = clearCodexQuotaLabelForRemovedAccount(account);
|
|
1064
|
+
codexQuotaLabelCleared = cleared.updated;
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
codexQuotaClearError = err?.message ?? String(err);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (flags.json) {
|
|
1070
|
+
const output = {
|
|
1071
|
+
success: true,
|
|
1072
|
+
label,
|
|
1073
|
+
source: shortenPath(source),
|
|
1074
|
+
};
|
|
1075
|
+
if (fileDeleted) {
|
|
1076
|
+
output.message = "File deleted (no accounts remaining)";
|
|
1077
|
+
} else {
|
|
1078
|
+
output.remainingAccounts = updatedAccounts.length;
|
|
1079
|
+
}
|
|
1080
|
+
if (removedWasActive) {
|
|
1081
|
+
output.activeLabelCleared = activeLabelCleared;
|
|
1082
|
+
}
|
|
1083
|
+
if (activeLabelClearError) {
|
|
1084
|
+
output.activeLabelError = activeLabelClearError;
|
|
1085
|
+
}
|
|
1086
|
+
if (codexQuotaLabelCleared) {
|
|
1087
|
+
output.codexQuotaLabelCleared = true;
|
|
1088
|
+
}
|
|
1089
|
+
if (codexQuotaClearError) {
|
|
1090
|
+
output.codexQuotaLabelError = codexQuotaClearError;
|
|
1091
|
+
}
|
|
1092
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (activeLabelClearError) {
|
|
1097
|
+
console.error(colorize(`Warning: Failed to clear activeLabel: ${activeLabelClearError}`, YELLOW));
|
|
1098
|
+
}
|
|
1099
|
+
if (codexQuotaClearError) {
|
|
1100
|
+
console.error(colorize(`Warning: Failed to clear codex_quota_label: ${codexQuotaClearError}`, YELLOW));
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (fileDeleted) {
|
|
1104
|
+
const lines = [
|
|
1105
|
+
colorize(`Removed account ${label}`, GREEN),
|
|
1106
|
+
"",
|
|
1107
|
+
`Deleted: ${shortenPath(source)} (no accounts remaining)`,
|
|
1108
|
+
];
|
|
1109
|
+
console.log(drawBox(lines).join("\n"));
|
|
1110
|
+
} else {
|
|
1111
|
+
const lines = [
|
|
1112
|
+
colorize(`Removed account ${label}`, GREEN),
|
|
1113
|
+
"",
|
|
1114
|
+
`Updated: ${shortenPath(source)} (${updatedAccounts.length} account(s) remaining)`,
|
|
1115
|
+
];
|
|
1116
|
+
console.log(drawBox(lines).join("\n"));
|
|
1117
|
+
}
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
if (flags.json) {
|
|
1120
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
1121
|
+
} else {
|
|
1122
|
+
console.error(colorize(`Error writing ${shortenPath(source)}: ${err.message}`, RED));
|
|
1123
|
+
}
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Handle Claude remove subcommand - remove a Claude account from storage
|
|
1130
|
+
* @param {string[]} args - Non-flag arguments (label is required)
|
|
1131
|
+
* @param {{ json: boolean }} flags - Parsed flags
|
|
1132
|
+
*/
|
|
1133
|
+
export async function handleClaudeRemove(args, flags) {
|
|
1134
|
+
const label = args[0];
|
|
1135
|
+
if (!label) {
|
|
1136
|
+
if (flags.json) {
|
|
1137
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
1138
|
+
} else {
|
|
1139
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} claude remove <label>`, RED));
|
|
1140
|
+
console.error("Removes a Claude credential from the multi-account file.");
|
|
1141
|
+
}
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const account = findClaudeAccountByLabel(label);
|
|
1146
|
+
if (!account) {
|
|
1147
|
+
const availableLabels = getClaudeLabels();
|
|
1148
|
+
if (flags.json) {
|
|
1149
|
+
console.log(JSON.stringify({
|
|
1150
|
+
success: false,
|
|
1151
|
+
error: `Claude account "${label}" not found`,
|
|
1152
|
+
availableLabels,
|
|
1153
|
+
}, null, 2));
|
|
1154
|
+
} else {
|
|
1155
|
+
console.error(colorize(`Claude account "${label}" not found.`, RED));
|
|
1156
|
+
if (availableLabels.length) {
|
|
1157
|
+
console.error(`Available labels: ${availableLabels.join(", ")}`);
|
|
1158
|
+
} else {
|
|
1159
|
+
console.error("No Claude accounts configured.");
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (account.source === "env") {
|
|
1166
|
+
if (flags.json) {
|
|
1167
|
+
console.log(JSON.stringify({
|
|
1168
|
+
success: false,
|
|
1169
|
+
error: "Cannot remove account from CLAUDE_ACCOUNTS env var. Modify the env var directly.",
|
|
1170
|
+
}, null, 2));
|
|
1171
|
+
} else {
|
|
1172
|
+
console.error(colorize("Cannot remove account from CLAUDE_ACCOUNTS env var.", RED));
|
|
1173
|
+
console.error("Modify the env var directly to remove this account.");
|
|
1174
|
+
}
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const source = account.source;
|
|
1179
|
+
if (!CLAUDE_MULTI_ACCOUNT_PATHS.includes(source)) {
|
|
1180
|
+
if (flags.json) {
|
|
1181
|
+
console.log(JSON.stringify({
|
|
1182
|
+
success: false,
|
|
1183
|
+
error: `Cannot remove Claude account from ${source}. Remove it from the owning tool instead.`,
|
|
1184
|
+
}, null, 2));
|
|
1185
|
+
} else {
|
|
1186
|
+
console.error(colorize(`Cannot remove Claude account from ${shortenPath(source)}.`, RED));
|
|
1187
|
+
console.error("Remove it from the owning tool instead.");
|
|
1188
|
+
}
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const removedWasActive = getClaudeActiveLabelInfo().activeLabel === label;
|
|
1193
|
+
let activeLabelCleared = false;
|
|
1194
|
+
let activeLabelClearError = null;
|
|
1195
|
+
|
|
1196
|
+
const container = readMultiAccountContainer(source);
|
|
1197
|
+
if (container.rootType === "invalid") {
|
|
1198
|
+
if (flags.json) {
|
|
1199
|
+
console.log(JSON.stringify({ success: false, error: `Failed to parse ${source}` }, null, 2));
|
|
1200
|
+
} else {
|
|
1201
|
+
console.error(colorize(`Error reading ${shortenPath(source)}`, RED));
|
|
1202
|
+
}
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const existingAccounts = container.accounts;
|
|
1206
|
+
|
|
1207
|
+
const updatedAccounts = existingAccounts.filter(a => a.label !== label);
|
|
1208
|
+
if (updatedAccounts.length === existingAccounts.length) {
|
|
1209
|
+
if (flags.json) {
|
|
1210
|
+
console.log(JSON.stringify({ success: false, error: `Claude account "${label}" not found in ${source}` }, null, 2));
|
|
1211
|
+
} else {
|
|
1212
|
+
console.error(colorize(`Claude account "${label}" not found in ${shortenPath(source)}`, RED));
|
|
1213
|
+
}
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (updatedAccounts.length === 0 && !flags.json) {
|
|
1218
|
+
console.log(colorize("Warning: This is the only Claude account in this file.", YELLOW));
|
|
1219
|
+
console.log(`The file will be deleted: ${shortenPath(source)}`);
|
|
1220
|
+
const confirmed = await promptConfirm("Continue?");
|
|
1221
|
+
if (!confirmed) {
|
|
1222
|
+
console.log("Cancelled.");
|
|
1223
|
+
process.exit(0);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
const fileDeleted = updatedAccounts.length === 0;
|
|
1229
|
+
if (fileDeleted) {
|
|
1230
|
+
unlinkSync(source);
|
|
1231
|
+
} else {
|
|
1232
|
+
writeMultiAccountContainer(source, container, updatedAccounts, {}, { mode: 0o600 });
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (removedWasActive) {
|
|
1236
|
+
try {
|
|
1237
|
+
const cleared = setClaudeActiveLabel(null);
|
|
1238
|
+
activeLabelCleared = cleared.updated;
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
activeLabelClearError = err?.message ?? String(err);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (flags.json) {
|
|
1245
|
+
const output = {
|
|
1246
|
+
success: true,
|
|
1247
|
+
label,
|
|
1248
|
+
source: shortenPath(source),
|
|
1249
|
+
};
|
|
1250
|
+
if (fileDeleted) {
|
|
1251
|
+
output.message = "File deleted (no accounts remaining)";
|
|
1252
|
+
} else {
|
|
1253
|
+
output.remainingAccounts = updatedAccounts.length;
|
|
1254
|
+
}
|
|
1255
|
+
if (removedWasActive) {
|
|
1256
|
+
output.activeLabelCleared = activeLabelCleared;
|
|
1257
|
+
}
|
|
1258
|
+
if (activeLabelClearError) {
|
|
1259
|
+
output.activeLabelError = activeLabelClearError;
|
|
1260
|
+
}
|
|
1261
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (activeLabelClearError) {
|
|
1266
|
+
console.error(colorize(`Warning: Failed to clear activeLabel: ${activeLabelClearError}`, YELLOW));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (fileDeleted) {
|
|
1270
|
+
const lines = [
|
|
1271
|
+
colorize(`Removed Claude account ${label}`, GREEN),
|
|
1272
|
+
"",
|
|
1273
|
+
`Deleted: ${shortenPath(source)} (no accounts remaining)`,
|
|
1274
|
+
];
|
|
1275
|
+
console.log(drawBox(lines).join("\n"));
|
|
1276
|
+
} else {
|
|
1277
|
+
const lines = [
|
|
1278
|
+
colorize(`Removed Claude account ${label}`, GREEN),
|
|
1279
|
+
"",
|
|
1280
|
+
`Updated: ${shortenPath(source)} (${updatedAccounts.length} account(s) remaining)`,
|
|
1281
|
+
];
|
|
1282
|
+
console.log(drawBox(lines).join("\n"));
|
|
1283
|
+
}
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
if (flags.json) {
|
|
1286
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
1287
|
+
} else {
|
|
1288
|
+
console.error(colorize(`Error writing ${shortenPath(source)}: ${err.message}`, RED));
|
|
1289
|
+
}
|
|
1290
|
+
process.exit(1);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Handle Claude switch subcommand - switch Claude Code/OpenCode/pi credentials
|
|
1296
|
+
* @param {string[]} args - Non-flag arguments (label is required)
|
|
1297
|
+
* @param {{ json: boolean }} flags - Parsed flags
|
|
1298
|
+
*/
|
|
1299
|
+
export async function handleClaudeSwitch(args, flags) {
|
|
1300
|
+
const label = args[0];
|
|
1301
|
+
if (!label) {
|
|
1302
|
+
if (flags.json) {
|
|
1303
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
1304
|
+
} else {
|
|
1305
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} claude switch <label>`, RED));
|
|
1306
|
+
console.error("Switches Claude credentials in Claude Code, OpenCode, and pi.");
|
|
1307
|
+
}
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const account = findClaudeAccountByLabel(label);
|
|
1312
|
+
if (!account) {
|
|
1313
|
+
const availableLabels = getClaudeLabels();
|
|
1314
|
+
if (flags.json) {
|
|
1315
|
+
console.log(JSON.stringify({
|
|
1316
|
+
success: false,
|
|
1317
|
+
error: `Claude account "${label}" not found`,
|
|
1318
|
+
availableLabels,
|
|
1319
|
+
}, null, 2));
|
|
1320
|
+
} else {
|
|
1321
|
+
console.error(colorize(`Claude account "${label}" not found.`, RED));
|
|
1322
|
+
if (availableLabels.length) {
|
|
1323
|
+
console.error(`Available: ${availableLabels.join(", ")}`);
|
|
1324
|
+
} else {
|
|
1325
|
+
console.error(`Run '${PRIMARY_CMD} claude add' to add a Claude credential.`);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!account.oauthToken) {
|
|
1332
|
+
const message = "Claude switch requires an OAuth token. Re-add with --oauth or provide an oauthToken.";
|
|
1333
|
+
if (flags.json) {
|
|
1334
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
1335
|
+
} else {
|
|
1336
|
+
console.error(colorize(`Error: ${message}`, RED));
|
|
1337
|
+
}
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
let activeLabelPath = null;
|
|
1342
|
+
let activeLabelError = null;
|
|
1343
|
+
if (CLAUDE_MULTI_ACCOUNT_PATHS.includes(account.source)) {
|
|
1344
|
+
try {
|
|
1345
|
+
const activeUpdate = setClaudeActiveLabel(label);
|
|
1346
|
+
activeLabelPath = activeUpdate.path;
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
activeLabelError = err?.message ?? String(err);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const credentialsUpdate = updateClaudeCredentials(account);
|
|
1353
|
+
if (credentialsUpdate.error) {
|
|
1354
|
+
if (flags.json) {
|
|
1355
|
+
console.log(JSON.stringify({ success: false, error: credentialsUpdate.error }, null, 2));
|
|
1356
|
+
} else {
|
|
1357
|
+
console.error(colorize(`Error: ${credentialsUpdate.error}`, RED));
|
|
1358
|
+
}
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const opencodeUpdate = updateOpencodeClaudeAuth(account);
|
|
1363
|
+
if (opencodeUpdate.error && !flags.json) {
|
|
1364
|
+
console.error(colorize(`Warning: ${opencodeUpdate.error}`, YELLOW));
|
|
1365
|
+
}
|
|
1366
|
+
const piUpdate = updatePiClaudeAuth(account);
|
|
1367
|
+
if (piUpdate.error && !flags.json) {
|
|
1368
|
+
console.error(colorize(`Warning: ${piUpdate.error}`, YELLOW));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (flags.json) {
|
|
1372
|
+
const output = {
|
|
1373
|
+
success: true,
|
|
1374
|
+
label,
|
|
1375
|
+
claudeCredentialsPath: credentialsUpdate.path,
|
|
1376
|
+
};
|
|
1377
|
+
if (activeLabelPath) {
|
|
1378
|
+
output.activeLabelPath = activeLabelPath;
|
|
1379
|
+
}
|
|
1380
|
+
if (activeLabelError) {
|
|
1381
|
+
output.activeLabelError = activeLabelError;
|
|
1382
|
+
}
|
|
1383
|
+
if (opencodeUpdate.updated) {
|
|
1384
|
+
output.opencodeAuthPath = opencodeUpdate.path;
|
|
1385
|
+
} else if (opencodeUpdate.error) {
|
|
1386
|
+
output.opencodeAuthError = opencodeUpdate.error;
|
|
1387
|
+
}
|
|
1388
|
+
if (piUpdate.updated) {
|
|
1389
|
+
output.piAuthPath = piUpdate.path;
|
|
1390
|
+
} else if (piUpdate.error) {
|
|
1391
|
+
output.piAuthError = piUpdate.error;
|
|
1392
|
+
}
|
|
1393
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (activeLabelError) {
|
|
1398
|
+
console.error(colorize(`Warning: Failed to update activeLabel: ${activeLabelError}`, YELLOW));
|
|
1399
|
+
}
|
|
1400
|
+
const lines = [
|
|
1401
|
+
colorize(`Switched Claude credentials to ${label}`, GREEN),
|
|
1402
|
+
"",
|
|
1403
|
+
`Claude Code: ${shortenPath(credentialsUpdate.path)}`,
|
|
1404
|
+
];
|
|
1405
|
+
if (activeLabelPath) {
|
|
1406
|
+
lines.push(`Active label: ${shortenPath(activeLabelPath)}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (opencodeUpdate.updated) {
|
|
1409
|
+
lines.push(`OpenCode: ${shortenPath(opencodeUpdate.path)}`);
|
|
1410
|
+
}
|
|
1411
|
+
if (piUpdate.updated) {
|
|
1412
|
+
lines.push(`pi: ${shortenPath(piUpdate.path)}`);
|
|
1413
|
+
}
|
|
1414
|
+
console.log(drawBox(lines).join("\n"));
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Handle Claude sync subcommand - bi-directional sync for activeLabel account
|
|
1419
|
+
* 1. Pull: if a CLI store has the same refresh token but newer access/expires, pull it back
|
|
1420
|
+
* 2. Push: write the (now freshest) account tokens to all CLI auth files
|
|
1421
|
+
* @param {string[]} args - Non-flag arguments (unused)
|
|
1422
|
+
* @param {{ json: boolean, dryRun?: boolean }} flags - Parsed flags
|
|
1423
|
+
*/
|
|
1424
|
+
export async function handleClaudeAdd(args, flags) {
|
|
1425
|
+
let label = args[0] || null;
|
|
1426
|
+
try {
|
|
1427
|
+
// Check for conflicting flags
|
|
1428
|
+
if (flags.oauth && flags.manual) {
|
|
1429
|
+
throw new Error("Cannot use both --oauth and --manual flags. Choose one authentication method.");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const existingAccounts = loadClaudeAccounts();
|
|
1433
|
+
const existingLabels = new Set(existingAccounts.map(a => a.label));
|
|
1434
|
+
|
|
1435
|
+
// Prompt for label if not provided
|
|
1436
|
+
if (!label) {
|
|
1437
|
+
label = (await promptInput("Label (e.g., work, personal): ")).trim();
|
|
1438
|
+
}
|
|
1439
|
+
if (!label) {
|
|
1440
|
+
throw new Error("Label is required");
|
|
1441
|
+
}
|
|
1442
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(label)) {
|
|
1443
|
+
throw new Error(`Invalid label "${label}". Use only letters, numbers, hyphens, and underscores.`);
|
|
1444
|
+
}
|
|
1445
|
+
if (existingLabels.has(label)) {
|
|
1446
|
+
throw new Error(`Label "${label}" already exists. Choose a different label.`);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Determine authentication method
|
|
1450
|
+
let useOAuth = flags.oauth;
|
|
1451
|
+
if (!flags.oauth && !flags.manual) {
|
|
1452
|
+
// Prompt for choice
|
|
1453
|
+
console.log("\nChoose authentication method:");
|
|
1454
|
+
console.log(" [1] OAuth (recommended) - Authenticate via browser");
|
|
1455
|
+
console.log(" [2] Manual - Paste sessionKey/token directly\n");
|
|
1456
|
+
const choice = (await promptInput("Enter choice (1 or 2): ")).trim();
|
|
1457
|
+
useOAuth = choice === "1";
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
let newAccount;
|
|
1461
|
+
let viaMethod;
|
|
1462
|
+
|
|
1463
|
+
if (useOAuth) {
|
|
1464
|
+
// OAuth browser flow
|
|
1465
|
+
const tokens = await handleClaudeOAuthFlow({ noBrowser: flags.noBrowser });
|
|
1466
|
+
newAccount = {
|
|
1467
|
+
label,
|
|
1468
|
+
sessionKey: null,
|
|
1469
|
+
oauthToken: tokens.accessToken,
|
|
1470
|
+
oauthRefreshToken: tokens.refreshToken,
|
|
1471
|
+
oauthExpiresAt: tokens.expiresAt,
|
|
1472
|
+
oauthScopes: tokens.scopes,
|
|
1473
|
+
cfClearance: null,
|
|
1474
|
+
orgId: null,
|
|
1475
|
+
};
|
|
1476
|
+
viaMethod = "via OAuth";
|
|
1477
|
+
} else {
|
|
1478
|
+
// Manual entry flow
|
|
1479
|
+
console.log("\nPaste your Claude sessionKey or OAuth token.");
|
|
1480
|
+
const sessionKeyInput = await promptInput("sessionKey (sk-ant-...): ", { allowEmpty: true });
|
|
1481
|
+
const oauthTokenInput = await promptInput("oauthToken (optional): ", { allowEmpty: true });
|
|
1482
|
+
const cfClearanceInput = await promptInput("cfClearance (optional): ", { allowEmpty: true });
|
|
1483
|
+
const orgIdInput = await promptInput("orgId (optional): ", { allowEmpty: true });
|
|
1484
|
+
|
|
1485
|
+
let parsedInput = null;
|
|
1486
|
+
if (sessionKeyInput && sessionKeyInput.trim().startsWith("{")) {
|
|
1487
|
+
try {
|
|
1488
|
+
parsedInput = JSON.parse(sessionKeyInput);
|
|
1489
|
+
} catch {
|
|
1490
|
+
parsedInput = null;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const sessionKey = findClaudeSessionKey(parsedInput ?? sessionKeyInput) ?? null;
|
|
1495
|
+
const oauthToken = oauthTokenInput?.trim()
|
|
1496
|
+
|| parsedInput?.claudeAiOauth?.accessToken
|
|
1497
|
+
|| parsedInput?.claude_ai_oauth?.accessToken
|
|
1498
|
+
|| parsedInput?.accessToken
|
|
1499
|
+
|| parsedInput?.access_token
|
|
1500
|
+
|| null;
|
|
1501
|
+
const cfClearance = cfClearanceInput?.trim() || null;
|
|
1502
|
+
const orgId = orgIdInput?.trim() || null;
|
|
1503
|
+
|
|
1504
|
+
if (!sessionKey && !oauthToken) {
|
|
1505
|
+
throw new Error("Provide at least a sessionKey or an OAuth token.");
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
newAccount = {
|
|
1509
|
+
label,
|
|
1510
|
+
sessionKey,
|
|
1511
|
+
oauthToken,
|
|
1512
|
+
cfClearance,
|
|
1513
|
+
orgId,
|
|
1514
|
+
};
|
|
1515
|
+
viaMethod = "";
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const { path: targetPath, container } = readClaudeActiveStoreContainer();
|
|
1519
|
+
const accounts = [...container.accounts, newAccount];
|
|
1520
|
+
writeMultiAccountContainer(targetPath, container, accounts, {}, { mode: 0o600 });
|
|
1521
|
+
|
|
1522
|
+
if (flags.json) {
|
|
1523
|
+
console.log(JSON.stringify({
|
|
1524
|
+
success: true,
|
|
1525
|
+
label,
|
|
1526
|
+
method: useOAuth ? "oauth" : "manual",
|
|
1527
|
+
source: targetPath,
|
|
1528
|
+
}, null, 2));
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
const credentialText = viaMethod ? `Added Claude credential ${label} (${viaMethod})` : `Added Claude credential ${label}`;
|
|
1533
|
+
const lines = [
|
|
1534
|
+
colorize(credentialText, GREEN),
|
|
1535
|
+
"",
|
|
1536
|
+
`Saved to: ${shortenPath(targetPath)}`,
|
|
1537
|
+
"",
|
|
1538
|
+
`Run '${PRIMARY_CMD} claude quota' to check Claude usage`,
|
|
1539
|
+
];
|
|
1540
|
+
console.log(drawBox(lines).join("\n"));
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (flags.json) {
|
|
1543
|
+
console.log(JSON.stringify({
|
|
1544
|
+
success: false,
|
|
1545
|
+
error: error.message,
|
|
1546
|
+
}, null, 2));
|
|
1547
|
+
} else {
|
|
1548
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
1549
|
+
}
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Handle Claude reauth subcommand - re-authenticate an existing Claude account via OAuth browser flow
|
|
1556
|
+
* This updates the existing account's tokens without changing the label
|
|
1557
|
+
* @param {string[]} args - Non-flag arguments (label is required)
|
|
1558
|
+
* @param {{ json: boolean, noBrowser: boolean }} flags - Parsed flags
|
|
1559
|
+
*/
|
|
1560
|
+
export async function handleClaudeReauth(args, flags) {
|
|
1561
|
+
const label = args[0];
|
|
1562
|
+
if (!label) {
|
|
1563
|
+
if (flags.json) {
|
|
1564
|
+
console.log(JSON.stringify({ success: false, error: "Missing required label argument" }, null, 2));
|
|
1565
|
+
} else {
|
|
1566
|
+
console.error(colorize(`Usage: ${PRIMARY_CMD} claude reauth <label>`, RED));
|
|
1567
|
+
console.error("Re-authenticates an existing Claude account via OAuth browser flow.");
|
|
1568
|
+
}
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
try {
|
|
1573
|
+
// 1. Find existing account by label
|
|
1574
|
+
const existingAccount = findClaudeAccountByLabel(label);
|
|
1575
|
+
if (!existingAccount) {
|
|
1576
|
+
const availableLabels = getClaudeLabels();
|
|
1577
|
+
if (flags.json) {
|
|
1578
|
+
console.log(JSON.stringify({
|
|
1579
|
+
success: false,
|
|
1580
|
+
error: `Claude account "${label}" not found`,
|
|
1581
|
+
availableLabels,
|
|
1582
|
+
}, null, 2));
|
|
1583
|
+
} else if (availableLabels.length === 0) {
|
|
1584
|
+
console.error(colorize(`Claude account "${label}" not found. No accounts configured.`, RED));
|
|
1585
|
+
console.error(`Run '${PRIMARY_CMD} claude add' to add an account.`);
|
|
1586
|
+
} else {
|
|
1587
|
+
console.error(colorize(`Claude account "${label}" not found.`, RED));
|
|
1588
|
+
console.error(`Available: ${availableLabels.join(", ")}`);
|
|
1589
|
+
}
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const source = existingAccount.source;
|
|
1594
|
+
|
|
1595
|
+
// 2. Check if account can be re-authenticated (must be in a multi-account file)
|
|
1596
|
+
if (source === "env") {
|
|
1597
|
+
if (flags.json) {
|
|
1598
|
+
console.log(JSON.stringify({
|
|
1599
|
+
success: false,
|
|
1600
|
+
error: "Cannot re-authenticate account from CLAUDE_ACCOUNTS env var. Modify the env var directly.",
|
|
1601
|
+
}, null, 2));
|
|
1602
|
+
} else {
|
|
1603
|
+
console.error(colorize("Cannot re-authenticate account from CLAUDE_ACCOUNTS env var.", RED));
|
|
1604
|
+
console.error("Modify the env var directly to update this account.");
|
|
1605
|
+
}
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (!CLAUDE_MULTI_ACCOUNT_PATHS.includes(source)) {
|
|
1610
|
+
if (flags.json) {
|
|
1611
|
+
console.log(JSON.stringify({
|
|
1612
|
+
success: false,
|
|
1613
|
+
error: `Cannot re-authenticate account from ${source}. Use the owning tool to re-authenticate.`,
|
|
1614
|
+
}, null, 2));
|
|
1615
|
+
} else {
|
|
1616
|
+
console.error(colorize(`Cannot re-authenticate account from ${shortenPath(source)}.`, RED));
|
|
1617
|
+
console.error("Use the owning tool to re-authenticate this account.");
|
|
1618
|
+
}
|
|
1619
|
+
process.exit(1);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// 3. Run OAuth flow
|
|
1623
|
+
console.log(`Re-authenticating Claude account "${label}"...`);
|
|
1624
|
+
const tokens = await handleClaudeOAuthFlow({ noBrowser: flags.noBrowser });
|
|
1625
|
+
|
|
1626
|
+
// 4. Update the account entry in the source file
|
|
1627
|
+
const container = readMultiAccountContainer(source);
|
|
1628
|
+
if (container.rootType === "invalid") {
|
|
1629
|
+
throw new Error(`Failed to parse ${source}`);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const updatedAccounts = container.accounts.map(entry => {
|
|
1633
|
+
if (!entry || typeof entry !== "object" || entry.label !== label) {
|
|
1634
|
+
return entry;
|
|
1635
|
+
}
|
|
1636
|
+
// Preserve any extra fields from the existing entry
|
|
1637
|
+
return {
|
|
1638
|
+
...entry,
|
|
1639
|
+
oauthToken: tokens.accessToken,
|
|
1640
|
+
oauthRefreshToken: tokens.refreshToken,
|
|
1641
|
+
oauthExpiresAt: tokens.expiresAt,
|
|
1642
|
+
oauthScopes: tokens.scopes,
|
|
1643
|
+
};
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
writeMultiAccountContainer(source, container, updatedAccounts, {}, { mode: 0o600 });
|
|
1647
|
+
|
|
1648
|
+
// 5. Update CLI auth files if this account is active
|
|
1649
|
+
const activeInfo = getClaudeActiveLabelInfo();
|
|
1650
|
+
if (activeInfo.activeLabel === label) {
|
|
1651
|
+
// This is the active account - sync to CLI auth files
|
|
1652
|
+
const updatedAccount = {
|
|
1653
|
+
oauthToken: tokens.accessToken,
|
|
1654
|
+
oauthRefreshToken: tokens.refreshToken,
|
|
1655
|
+
oauthExpiresAt: tokens.expiresAt,
|
|
1656
|
+
oauthScopes: tokens.scopes,
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
updateClaudeCredentials(updatedAccount);
|
|
1660
|
+
updateOpencodeClaudeAuth(updatedAccount);
|
|
1661
|
+
updatePiClaudeAuth(updatedAccount);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// 6. Print success message
|
|
1665
|
+
if (flags.json) {
|
|
1666
|
+
console.log(JSON.stringify({
|
|
1667
|
+
success: true,
|
|
1668
|
+
label,
|
|
1669
|
+
source,
|
|
1670
|
+
}, null, 2));
|
|
1671
|
+
} else {
|
|
1672
|
+
const lines = [
|
|
1673
|
+
colorize(`Re-authenticated Claude account ${label}`, GREEN),
|
|
1674
|
+
"",
|
|
1675
|
+
`Updated: ${shortenPath(source)}`,
|
|
1676
|
+
];
|
|
1677
|
+
if (activeInfo.activeLabel === label) {
|
|
1678
|
+
lines.push("");
|
|
1679
|
+
lines.push("CLI auth files also updated (active account)");
|
|
1680
|
+
}
|
|
1681
|
+
const boxLines = drawBox(lines);
|
|
1682
|
+
console.log(boxLines.join("\n"));
|
|
1683
|
+
}
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
if (flags.json) {
|
|
1686
|
+
console.log(JSON.stringify({
|
|
1687
|
+
success: false,
|
|
1688
|
+
error: error.message,
|
|
1689
|
+
}, null, 2));
|
|
1690
|
+
} else {
|
|
1691
|
+
console.error(colorize(`Error: ${error.message}`, RED));
|
|
1692
|
+
}
|
|
1693
|
+
process.exit(1);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Handle Codex subcommand entrypoint
|
|
1699
|
+
* @param {string[]} args - Codex subcommand args
|
|
1700
|
+
* @param {{ json: boolean, noBrowser: boolean, noColor: boolean }} flags - Parsed flags
|
|
1701
|
+
*/
|
|
1702
|
+
export async function handleCodex(args, flags) {
|
|
1703
|
+
const subcommand = args[0];
|
|
1704
|
+
const subArgs = args.slice(1);
|
|
1705
|
+
|
|
1706
|
+
if (!subcommand) {
|
|
1707
|
+
printHelpCodex();
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
switch (subcommand) {
|
|
1712
|
+
case "quota":
|
|
1713
|
+
await handleQuota(subArgs, flags, "codex");
|
|
1714
|
+
break;
|
|
1715
|
+
case "add":
|
|
1716
|
+
await handleAdd(subArgs, flags);
|
|
1717
|
+
break;
|
|
1718
|
+
case "reauth":
|
|
1719
|
+
await handleCodexReauth(subArgs, flags);
|
|
1720
|
+
break;
|
|
1721
|
+
case "switch":
|
|
1722
|
+
await handleSwitch(subArgs, flags);
|
|
1723
|
+
break;
|
|
1724
|
+
case "sync":
|
|
1725
|
+
await handleCodexSync(subArgs, flags);
|
|
1726
|
+
break;
|
|
1727
|
+
case "list":
|
|
1728
|
+
await handleList(flags);
|
|
1729
|
+
break;
|
|
1730
|
+
case "remove":
|
|
1731
|
+
await handleRemove(subArgs, flags);
|
|
1732
|
+
break;
|
|
1733
|
+
case "help":
|
|
1734
|
+
printHelpCodex();
|
|
1735
|
+
break;
|
|
1736
|
+
default:
|
|
1737
|
+
printHelpCodex();
|
|
1738
|
+
process.exit(1);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Handle Claude subcommand entrypoint
|
|
1744
|
+
* @param {string[]} args - Claude subcommand args
|
|
1745
|
+
* @param {{ json: boolean, noBrowser: boolean, oauth: boolean, manual: boolean }} flags - Parsed flags
|
|
1746
|
+
*/
|
|
1747
|
+
export async function handleClaude(args, flags) {
|
|
1748
|
+
const subcommand = args[0];
|
|
1749
|
+
const subArgs = args.slice(1);
|
|
1750
|
+
|
|
1751
|
+
if (!subcommand) {
|
|
1752
|
+
printHelpClaude();
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
switch (subcommand) {
|
|
1757
|
+
case "quota":
|
|
1758
|
+
await handleQuota(subArgs, flags, "claude");
|
|
1759
|
+
break;
|
|
1760
|
+
case "add":
|
|
1761
|
+
await handleClaudeAdd(subArgs, flags);
|
|
1762
|
+
break;
|
|
1763
|
+
case "reauth":
|
|
1764
|
+
await handleClaudeReauth(subArgs, flags);
|
|
1765
|
+
break;
|
|
1766
|
+
case "list":
|
|
1767
|
+
await handleClaudeList(flags);
|
|
1768
|
+
break;
|
|
1769
|
+
case "switch":
|
|
1770
|
+
await handleClaudeSwitch(subArgs, flags);
|
|
1771
|
+
break;
|
|
1772
|
+
case "sync":
|
|
1773
|
+
await handleClaudeSync(subArgs, flags);
|
|
1774
|
+
break;
|
|
1775
|
+
case "remove":
|
|
1776
|
+
await handleClaudeRemove(subArgs, flags);
|
|
1777
|
+
break;
|
|
1778
|
+
case "help":
|
|
1779
|
+
printHelpClaude();
|
|
1780
|
+
break;
|
|
1781
|
+
default:
|
|
1782
|
+
printHelpClaude();
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Handle quota subcommand (default behavior)
|
|
1789
|
+
* By default, shows both Codex and Claude accounts
|
|
1790
|
+
* @param {string[]} args - Non-flag arguments (e.g., label filter)
|
|
1791
|
+
* @param {{ json: boolean, local?: boolean }} flags - Parsed flags
|
|
1792
|
+
* @param {"all" | "codex" | "claude"} scope - Which accounts to show
|
|
1793
|
+
*/
|
|
1794
|
+
export async function handleQuota(args, flags, scope = "all") {
|
|
1795
|
+
const labelFilter = args[0];
|
|
1796
|
+
const localMode = Boolean(flags.local);
|
|
1797
|
+
|
|
1798
|
+
// Determine which account types to show:
|
|
1799
|
+
// - scope "all": show both (default)
|
|
1800
|
+
// - scope "codex": show only Codex
|
|
1801
|
+
// - scope "claude": show only Claude
|
|
1802
|
+
const showCodex = scope === "all" || scope === "codex";
|
|
1803
|
+
const showClaude = scope === "all" || scope === "claude";
|
|
1804
|
+
|
|
1805
|
+
const codexDivergence = showCodex && !localMode ? detectCodexDivergence({ allowMigration: false }) : null;
|
|
1806
|
+
const codexActiveLabel = codexDivergence?.activeLabel ?? null;
|
|
1807
|
+
const allAccounts = showCodex ? loadAllAccounts(codexActiveLabel, { local: localMode }) : [];
|
|
1808
|
+
const hasOpenAiAccounts = allAccounts.length > 0;
|
|
1809
|
+
const claudeDivergence = showClaude && !localMode ? detectClaudeDivergence() : null;
|
|
1810
|
+
|
|
1811
|
+
// Check if we have any accounts to show
|
|
1812
|
+
if (!hasOpenAiAccounts && !showClaude) {
|
|
1813
|
+
if (flags.json) {
|
|
1814
|
+
console.log(JSON.stringify({
|
|
1815
|
+
success: false,
|
|
1816
|
+
error: "No Codex accounts found",
|
|
1817
|
+
searchedLocations: [
|
|
1818
|
+
"CODEX_ACCOUNTS env var",
|
|
1819
|
+
...MULTI_ACCOUNT_PATHS,
|
|
1820
|
+
getCodexCliAuthPath(),
|
|
1821
|
+
],
|
|
1822
|
+
}, null, 2));
|
|
1823
|
+
} else {
|
|
1824
|
+
console.error(colorize("No Codex accounts found.", RED));
|
|
1825
|
+
console.error("\nSearched:");
|
|
1826
|
+
console.error(" - CODEX_ACCOUNTS env var");
|
|
1827
|
+
for (const p of MULTI_ACCOUNT_PATHS) {
|
|
1828
|
+
console.error(` - ${p}`);
|
|
1829
|
+
}
|
|
1830
|
+
console.error(` - ${getCodexCliAuthPath()}`);
|
|
1831
|
+
console.error(`\nRun '${PRIMARY_CMD} codex add' to add an account.`);
|
|
1832
|
+
}
|
|
1833
|
+
process.exit(1);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
let accounts = [];
|
|
1837
|
+
if (hasOpenAiAccounts && showCodex) {
|
|
1838
|
+
accounts = labelFilter
|
|
1839
|
+
? allAccounts.filter(a => a.label === labelFilter)
|
|
1840
|
+
: allAccounts;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (labelFilter && showCodex && !accounts.length && hasOpenAiAccounts) {
|
|
1844
|
+
if (flags.json) {
|
|
1845
|
+
console.log(JSON.stringify({
|
|
1846
|
+
success: false,
|
|
1847
|
+
error: `Account "${labelFilter}" not found`,
|
|
1848
|
+
availableLabels: allAccounts.map(a => a.label),
|
|
1849
|
+
}, null, 2));
|
|
1850
|
+
} else {
|
|
1851
|
+
console.error(colorize(`Account "${labelFilter}" not found.`, RED));
|
|
1852
|
+
console.error("Available:", allAccounts.map(a => a.label).join(", "));
|
|
1853
|
+
}
|
|
1854
|
+
process.exit(1);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
const results = [];
|
|
1858
|
+
|
|
1859
|
+
for (const account of accounts) {
|
|
1860
|
+
const tokenOk = await ensureFreshToken(account, allAccounts);
|
|
1861
|
+
if (!tokenOk) {
|
|
1862
|
+
results.push({ account, usage: { error: "Token refresh failed - re-auth required" } });
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
const usage = await fetchUsage(account);
|
|
1866
|
+
results.push({ account, usage });
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
let claudeResults = null;
|
|
1870
|
+
if (showClaude) {
|
|
1871
|
+
if (!localMode) {
|
|
1872
|
+
const importResult = await maybeImportClaudeOauthStores({ json: flags.json });
|
|
1873
|
+
if (importResult.warnings.length && !flags.json) {
|
|
1874
|
+
for (const warning of importResult.warnings) {
|
|
1875
|
+
console.error(colorize(`Warning: ${warning}`, YELLOW));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
const wantsClaudeLabel = scope === "claude" && Boolean(labelFilter);
|
|
1880
|
+
const oauthAccounts = loadAllClaudeOAuthAccounts({ local: localMode });
|
|
1881
|
+
const filteredOauthAccounts = wantsClaudeLabel
|
|
1882
|
+
? oauthAccounts.filter(account => account.label === labelFilter)
|
|
1883
|
+
: oauthAccounts;
|
|
1884
|
+
|
|
1885
|
+
if (filteredOauthAccounts.length) {
|
|
1886
|
+
const rawResults = await Promise.all(
|
|
1887
|
+
filteredOauthAccounts.map(account => fetchClaudeOAuthUsageForAccount(account))
|
|
1888
|
+
);
|
|
1889
|
+
claudeResults = deduplicateClaudeResultsByUsage(rawResults);
|
|
1890
|
+
} else {
|
|
1891
|
+
const claudeAccounts = loadClaudeAccounts();
|
|
1892
|
+
const filteredClaudeAccounts = wantsClaudeLabel
|
|
1893
|
+
? claudeAccounts.filter(account => account.label === labelFilter)
|
|
1894
|
+
: claudeAccounts;
|
|
1895
|
+
|
|
1896
|
+
if (filteredClaudeAccounts.length) {
|
|
1897
|
+
const rawResults = await Promise.all(
|
|
1898
|
+
filteredClaudeAccounts.map(account => fetchClaudeUsageForCredentials(account))
|
|
1899
|
+
);
|
|
1900
|
+
claudeResults = deduplicateClaudeResultsByUsage(rawResults);
|
|
1901
|
+
} else if (wantsClaudeLabel) {
|
|
1902
|
+
const availableLabels = new Set([
|
|
1903
|
+
...oauthAccounts.map(account => account.label),
|
|
1904
|
+
...claudeAccounts.map(account => account.label),
|
|
1905
|
+
]);
|
|
1906
|
+
const labelList = Array.from(availableLabels);
|
|
1907
|
+
if (flags.json) {
|
|
1908
|
+
console.log(JSON.stringify({
|
|
1909
|
+
success: false,
|
|
1910
|
+
error: `Claude account "${labelFilter}" not found`,
|
|
1911
|
+
availableLabels: labelList,
|
|
1912
|
+
}, null, 2));
|
|
1913
|
+
} else {
|
|
1914
|
+
console.error(colorize(`Claude account "${labelFilter}" not found.`, RED));
|
|
1915
|
+
if (labelList.length) {
|
|
1916
|
+
console.error(`Available: ${labelList.join(", ")}`);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
} else {
|
|
1921
|
+
const legacyResult = await fetchClaudeUsage();
|
|
1922
|
+
if (legacyResult.success || legacyResult.usage) {
|
|
1923
|
+
claudeResults = [legacyResult];
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Check if we have anything to show
|
|
1930
|
+
const hasCodexResults = results.length > 0;
|
|
1931
|
+
const hasClaudeResults = claudeResults && claudeResults.length > 0;
|
|
1932
|
+
|
|
1933
|
+
if (!hasCodexResults && !hasClaudeResults) {
|
|
1934
|
+
if (flags.json) {
|
|
1935
|
+
console.log(JSON.stringify({
|
|
1936
|
+
success: false,
|
|
1937
|
+
error: "No accounts found",
|
|
1938
|
+
}, null, 2));
|
|
1939
|
+
} else {
|
|
1940
|
+
console.error(colorize("No accounts found.", RED));
|
|
1941
|
+
const codexMessage = `Run '${PRIMARY_CMD} codex add' to add a Codex account.`;
|
|
1942
|
+
const claudeMessage = `Run '${PRIMARY_CMD} claude add' to add a Claude account.`;
|
|
1943
|
+
if (scope === "codex") {
|
|
1944
|
+
console.error(`\n${codexMessage}`);
|
|
1945
|
+
} else if (scope === "claude") {
|
|
1946
|
+
console.error(`\n${claudeMessage}`);
|
|
1947
|
+
} else {
|
|
1948
|
+
console.error(`\n${codexMessage}`);
|
|
1949
|
+
console.error(claudeMessage);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
process.exit(1);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if (flags.json) {
|
|
1956
|
+
const openaiOutput = results.map(({ account, usage }) => {
|
|
1957
|
+
const profile = extractProfile(account.access);
|
|
1958
|
+
return {
|
|
1959
|
+
label: account.label,
|
|
1960
|
+
email: profile.email,
|
|
1961
|
+
accountId: account.accountId,
|
|
1962
|
+
planType: profile.planType,
|
|
1963
|
+
usage,
|
|
1964
|
+
source: account.source,
|
|
1965
|
+
};
|
|
1966
|
+
});
|
|
1967
|
+
const codexDivergenceInfo = codexDivergence
|
|
1968
|
+
? {
|
|
1969
|
+
activeLabel: codexDivergence.activeLabel ?? null,
|
|
1970
|
+
activeAccountId: codexDivergence.activeAccount?.accountId ?? null,
|
|
1971
|
+
activeStorePath: codexDivergence.activeStorePath,
|
|
1972
|
+
cliAccountId: codexDivergence.cliAccountId ?? null,
|
|
1973
|
+
cliLabel: codexDivergence.cliLabel ?? null,
|
|
1974
|
+
diverged: codexDivergence.diverged,
|
|
1975
|
+
migrated: codexDivergence.migrated,
|
|
1976
|
+
}
|
|
1977
|
+
: null;
|
|
1978
|
+
const claudeDivergenceInfo = claudeDivergence
|
|
1979
|
+
? {
|
|
1980
|
+
activeLabel: claudeDivergence.activeLabel ?? null,
|
|
1981
|
+
activeStorePath: claudeDivergence.activeStorePath,
|
|
1982
|
+
diverged: claudeDivergence.diverged,
|
|
1983
|
+
skipped: claudeDivergence.skipped,
|
|
1984
|
+
skipReason: claudeDivergence.skipReason,
|
|
1985
|
+
stores: claudeDivergence.stores,
|
|
1986
|
+
}
|
|
1987
|
+
: null;
|
|
1988
|
+
const openaiOutputWithDivergence = codexDivergenceInfo
|
|
1989
|
+
? openaiOutput.map(item => ({ ...item, divergence: codexDivergenceInfo }))
|
|
1990
|
+
: openaiOutput;
|
|
1991
|
+
const claudeOutputWithDivergence = claudeDivergenceInfo
|
|
1992
|
+
? (claudeResults ?? []).map(item => (
|
|
1993
|
+
item && typeof item === "object"
|
|
1994
|
+
? { ...item, divergence: claudeDivergenceInfo }
|
|
1995
|
+
: item
|
|
1996
|
+
))
|
|
1997
|
+
: claudeResults ?? [];
|
|
1998
|
+
// Always output both fields when showing both, or just the relevant one
|
|
1999
|
+
if (showCodex && showClaude) {
|
|
2000
|
+
const payload = {
|
|
2001
|
+
codex: openaiOutputWithDivergence,
|
|
2002
|
+
claude: claudeOutputWithDivergence,
|
|
2003
|
+
};
|
|
2004
|
+
payload.divergence = {
|
|
2005
|
+
codex: codexDivergenceInfo,
|
|
2006
|
+
claude: claudeDivergenceInfo,
|
|
2007
|
+
};
|
|
2008
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2009
|
+
} else if (showClaude) {
|
|
2010
|
+
console.log(JSON.stringify(claudeOutputWithDivergence, null, 2));
|
|
2011
|
+
} else {
|
|
2012
|
+
console.log(JSON.stringify(openaiOutputWithDivergence, null, 2));
|
|
2013
|
+
}
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (showCodex && codexDivergence?.diverged) {
|
|
2018
|
+
const activeLabelDisplay = codexDivergence.activeLabel ?? "(none)";
|
|
2019
|
+
const activeIdDisplay = codexDivergence.activeAccount?.accountId ?? "(unknown)";
|
|
2020
|
+
const cliLabelDisplay = codexDivergence.cliLabel ?? "(unknown)";
|
|
2021
|
+
const cliIdDisplay = codexDivergence.cliAccountId ?? "(unknown)";
|
|
2022
|
+
console.error(colorize("Warning: CLI auth diverged from activeLabel", YELLOW));
|
|
2023
|
+
console.error(` Active: ${activeLabelDisplay} (${activeIdDisplay})`);
|
|
2024
|
+
console.error(` CLI: ${cliLabelDisplay} (${cliIdDisplay})`);
|
|
2025
|
+
console.error("");
|
|
2026
|
+
console.error(`Run '${PRIMARY_CMD} codex sync' to push active account to CLI.`);
|
|
2027
|
+
console.error("");
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (showClaude && claudeDivergence?.diverged) {
|
|
2031
|
+
const activeLabelDisplay = claudeDivergence.activeLabel ?? "(none)";
|
|
2032
|
+
const divergedStores = claudeDivergence.stores
|
|
2033
|
+
.filter(store => store.considered && store.matches === false)
|
|
2034
|
+
.map(store => store.name);
|
|
2035
|
+
const storeDisplay = divergedStores.length ? divergedStores.join(", ") : "one or more stores";
|
|
2036
|
+
console.error(colorize(`Warning: Claude auth diverged from activeLabel (${activeLabelDisplay})`, YELLOW));
|
|
2037
|
+
console.error(` Diverged stores: ${storeDisplay}`);
|
|
2038
|
+
console.error("");
|
|
2039
|
+
console.error(`Run '${PRIMARY_CMD} claude sync' to push active account to CLI.`);
|
|
2040
|
+
console.error("");
|
|
2041
|
+
} else if (showClaude && claudeDivergence?.skipped && claudeDivergence.skipReason === "active-account-not-oauth" && claudeDivergence.activeLabel) {
|
|
2042
|
+
console.error("Note: Active Claude account has no OAuth tokens; skipping divergence check.");
|
|
2043
|
+
console.error("");
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
for (const { account, usage } of results) {
|
|
2047
|
+
const lines = buildAccountUsageLines(account, usage);
|
|
2048
|
+
const boxLines = drawBox(lines);
|
|
2049
|
+
console.log(boxLines.join("\n"));
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (claudeResults) {
|
|
2053
|
+
for (const result of claudeResults) {
|
|
2054
|
+
const lines = buildClaudeUsageLines(result);
|
|
2055
|
+
const boxLines = drawBox(lines);
|
|
2056
|
+
console.log(boxLines.join("\n"));
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|