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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex account loading, dedup, active-label resolution.
|
|
3
|
+
* Depends on: lib/constants.js, lib/jwt.js, lib/container.js, lib/paths.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { MULTI_ACCOUNT_PATHS } from "./constants.js";
|
|
8
|
+
import { extractAccountId, extractProfile } from "./jwt.js";
|
|
9
|
+
import { readMultiAccountContainer } from "./container.js";
|
|
10
|
+
import { getCodexCliAuthPath } from "./paths.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load accounts from CODEX_ACCOUNTS environment variable
|
|
14
|
+
* @returns {Array<{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string}>}
|
|
15
|
+
*/
|
|
16
|
+
export function loadAccountsFromEnv() {
|
|
17
|
+
const envAccounts = process.env.CODEX_ACCOUNTS;
|
|
18
|
+
if (!envAccounts) return [];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(envAccounts);
|
|
22
|
+
const accounts = Array.isArray(parsed) ? parsed : parsed?.accounts ?? [];
|
|
23
|
+
return accounts
|
|
24
|
+
.filter(isValidAccount)
|
|
25
|
+
.map(a => ({ ...a, source: "env" }));
|
|
26
|
+
} catch {
|
|
27
|
+
console.error("Warning: CODEX_ACCOUNTS env var is not valid JSON");
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load accounts from a multi-account JSON file
|
|
34
|
+
* @param {string} filePath - Path to the JSON file
|
|
35
|
+
* @returns {Array<{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string}>}
|
|
36
|
+
*/
|
|
37
|
+
export function loadAccountsFromFile(filePath) {
|
|
38
|
+
const container = readMultiAccountContainer(filePath);
|
|
39
|
+
if (!container.exists) return [];
|
|
40
|
+
return container.accounts
|
|
41
|
+
.filter(isValidAccount)
|
|
42
|
+
.map(a => ({ ...a, source: filePath }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load account from Codex CLI auth.json (single account format)
|
|
47
|
+
* Returns array with single account for consistency with other loaders
|
|
48
|
+
* @returns {Array<{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string}>}
|
|
49
|
+
*/
|
|
50
|
+
export function loadAccountFromCodexCli() {
|
|
51
|
+
const codexAuthPath = getCodexCliAuthPath();
|
|
52
|
+
if (!existsSync(codexAuthPath)) return [];
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const raw = readFileSync(codexAuthPath, "utf-8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
const tokens = parsed?.tokens;
|
|
58
|
+
|
|
59
|
+
if (!tokens?.access_token || !tokens?.refresh_token) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const accountId = typeof tokens.account_id === "string" && tokens.account_id
|
|
64
|
+
? tokens.account_id
|
|
65
|
+
: extractAccountId(tokens.access_token);
|
|
66
|
+
if (!accountId) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [{
|
|
71
|
+
label: "codex-cli",
|
|
72
|
+
accountId,
|
|
73
|
+
access: tokens.access_token,
|
|
74
|
+
refresh: tokens.refresh_token,
|
|
75
|
+
expires: tokens.expires_at ? tokens.expires_at * 1000 : Date.now() - 1000,
|
|
76
|
+
source: codexAuthPath,
|
|
77
|
+
}];
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isValidAccount(a) {
|
|
84
|
+
return a?.label && a?.accountId && a?.access && a?.refresh;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Deduplicate accounts by email+accountId (from JWT token), keeping the first occurrence.
|
|
89
|
+
* Same user on different team accounts (different accountId) will be kept separately
|
|
90
|
+
* because each team has its own quota.
|
|
91
|
+
* Optionally prefer a specific label so the active account remains visible.
|
|
92
|
+
* @param {Array<{access: string, accountId?: string, label?: string}>} accounts
|
|
93
|
+
* @param {{ preferredLabel?: string | null }} [options]
|
|
94
|
+
* @returns {Array<{access: string, accountId?: string, label?: string}>}
|
|
95
|
+
*/
|
|
96
|
+
export function deduplicateAccountsByEmail(accounts, options = {}) {
|
|
97
|
+
const preferredLabel = options.preferredLabel ?? null;
|
|
98
|
+
let preferredKey = null;
|
|
99
|
+
if (preferredLabel) {
|
|
100
|
+
const preferredAccount = accounts.find(account => account.label === preferredLabel);
|
|
101
|
+
if (preferredAccount?.access) {
|
|
102
|
+
const email = extractProfile(preferredAccount.access)?.email ?? null;
|
|
103
|
+
if (email) {
|
|
104
|
+
preferredKey = `${email}|${preferredAccount.accountId ?? ""}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
return accounts.filter(account => {
|
|
111
|
+
if (!account.access) return true;
|
|
112
|
+
const profile = extractProfile(account.access);
|
|
113
|
+
const email = profile?.email;
|
|
114
|
+
if (!email) return true;
|
|
115
|
+
const key = `${email}|${account.accountId ?? ""}`;
|
|
116
|
+
if (preferredKey && key === preferredKey) {
|
|
117
|
+
return account.label === preferredLabel;
|
|
118
|
+
}
|
|
119
|
+
if (seen.has(key)) return false;
|
|
120
|
+
seen.add(key);
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the multi-account file that stores activeLabel for Codex.
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
export function resolveCodexActiveStorePath() {
|
|
130
|
+
for (const path of MULTI_ACCOUNT_PATHS) {
|
|
131
|
+
if (existsSync(path)) return path;
|
|
132
|
+
}
|
|
133
|
+
return MULTI_ACCOUNT_PATHS[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Read the active-label store container for Codex.
|
|
138
|
+
* @returns {{ path: string, container: ReturnType<typeof readMultiAccountContainer> }}
|
|
139
|
+
*/
|
|
140
|
+
export function readCodexActiveStoreContainer() {
|
|
141
|
+
const path = resolveCodexActiveStorePath();
|
|
142
|
+
const container = readMultiAccountContainer(path);
|
|
143
|
+
return { path, container };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the activeLabel stored for Codex (if any).
|
|
148
|
+
* @returns {{ activeLabel: string | null, path: string, schemaVersion: number }}
|
|
149
|
+
*/
|
|
150
|
+
export function getCodexActiveLabelInfo() {
|
|
151
|
+
const { path, container } = readCodexActiveStoreContainer();
|
|
152
|
+
return {
|
|
153
|
+
activeLabel: container.activeLabel ?? null,
|
|
154
|
+
path,
|
|
155
|
+
schemaVersion: container.schemaVersion ?? 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Load ALL accounts from ALL sources without deduplication by email.
|
|
161
|
+
* @param {{ local?: boolean }} [options] - When local=true, skip harness auth files
|
|
162
|
+
* @returns {Array<{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string}>}
|
|
163
|
+
*/
|
|
164
|
+
export function loadAllAccountsNoDedup(options = {}) {
|
|
165
|
+
const all = [];
|
|
166
|
+
all.push(...loadAccountsFromEnv());
|
|
167
|
+
for (const path of MULTI_ACCOUNT_PATHS) {
|
|
168
|
+
all.push(...loadAccountsFromFile(path));
|
|
169
|
+
}
|
|
170
|
+
if (all.length === 0 && !options.local) {
|
|
171
|
+
all.push(...loadAccountFromCodexCli());
|
|
172
|
+
}
|
|
173
|
+
return all;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Load ALL accounts from ALL sources (env, file paths, codex-cli)
|
|
178
|
+
* Deduplicates by email to prevent showing same user twice
|
|
179
|
+
* @param {string | null} [preferredLabel]
|
|
180
|
+
* @param {{ local?: boolean }} [options]
|
|
181
|
+
* @returns {Array<{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string}>}
|
|
182
|
+
*/
|
|
183
|
+
export function loadAllAccounts(preferredLabel = null, options = {}) {
|
|
184
|
+
const all = loadAllAccountsNoDedup(options);
|
|
185
|
+
return deduplicateAccountsByEmail(all, { preferredLabel });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Find an account by label from all sources
|
|
190
|
+
* @param {string} label
|
|
191
|
+
* @returns {{label: string, accountId: string, access: string, refresh: string, expires?: number, source: string} | null}
|
|
192
|
+
*/
|
|
193
|
+
export function findAccountByLabel(label) {
|
|
194
|
+
const accounts = loadAllAccountsNoDedup();
|
|
195
|
+
return accounts.find(a => a.label === label) ?? null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all labels from all account sources
|
|
200
|
+
* @returns {string[]}
|
|
201
|
+
*/
|
|
202
|
+
export function getAllLabels() {
|
|
203
|
+
const accounts = loadAllAccountsNoDedup();
|
|
204
|
+
return [...new Set(accounts.map(a => a.label))];
|
|
205
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI token refresh and multi-store persistence.
|
|
3
|
+
* Depends on: lib/constants.js, lib/paths.js, lib/jwt.js, lib/token-match.js, lib/container.js, lib/fs.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { TOKEN_URL, CLIENT_ID, MULTI_ACCOUNT_PATHS, OPENAI_OAUTH_REFRESH_BUFFER_MS } from "./constants.js";
|
|
8
|
+
import { getOpencodeAuthPath, getCodexCliAuthPath, getPiAuthPath } from "./paths.js";
|
|
9
|
+
import { extractAccountId } from "./jwt.js";
|
|
10
|
+
import { isOauthTokenMatch, normalizeEntryTokens, updateEntryTokens, OPENAI_TOKEN_FIELDS } from "./token-match.js";
|
|
11
|
+
import { readMultiAccountContainer, writeMultiAccountContainer, mapContainerAccounts } from "./container.js";
|
|
12
|
+
import { writeFileAtomic } from "./fs.js";
|
|
13
|
+
|
|
14
|
+
// Keep the original function names as internal helpers for backward compat
|
|
15
|
+
function isOpenAiOauthTokenMatch(params) {
|
|
16
|
+
return isOauthTokenMatch(params);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeOpenAiOauthEntryTokens(entry) {
|
|
20
|
+
return normalizeEntryTokens(entry, OPENAI_TOKEN_FIELDS);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function updateOpenAiOauthEntry(entry, account) {
|
|
24
|
+
const fields = {
|
|
25
|
+
access: account.access,
|
|
26
|
+
refresh: account.refresh,
|
|
27
|
+
expires: account.expires ?? null,
|
|
28
|
+
accountId: account.accountId,
|
|
29
|
+
};
|
|
30
|
+
// Only include idToken when truthy (matches original behavior)
|
|
31
|
+
if (account.idToken) {
|
|
32
|
+
fields.idToken = account.idToken;
|
|
33
|
+
}
|
|
34
|
+
return updateEntryTokens(entry, fields, OPENAI_TOKEN_FIELDS);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update OpenCode auth.json with new OpenAI OAuth tokens
|
|
39
|
+
* @param {{ access: string, refresh: string, expires?: number, accountId: string }} account
|
|
40
|
+
* @returns {{ updated: boolean, path: string, error?: string, skipped?: boolean }}
|
|
41
|
+
*/
|
|
42
|
+
export function updateOpencodeAuth(account) {
|
|
43
|
+
const authPath = getOpencodeAuthPath();
|
|
44
|
+
if (!existsSync(authPath)) {
|
|
45
|
+
return { updated: false, path: authPath, skipped: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let existingAuth = {};
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(authPath, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
+
return { updated: false, path: authPath, error: "Invalid OpenCode auth.json format" };
|
|
54
|
+
}
|
|
55
|
+
existingAuth = parsed;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const message = err?.message ?? String(err);
|
|
58
|
+
return { updated: false, path: authPath, error: `Failed to read OpenCode auth.json: ${message}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const openaiEntry = existingAuth.openai;
|
|
62
|
+
const openaiAuth = openaiEntry && typeof openaiEntry === "object" ? openaiEntry : {};
|
|
63
|
+
const expires = account.expires ?? Date.now() - 1000;
|
|
64
|
+
const updatedAuth = {
|
|
65
|
+
...existingAuth,
|
|
66
|
+
openai: {
|
|
67
|
+
...openaiAuth,
|
|
68
|
+
type: "oauth",
|
|
69
|
+
access: account.access,
|
|
70
|
+
refresh: account.refresh,
|
|
71
|
+
expires: expires,
|
|
72
|
+
accountId: account.accountId,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
writeFileAtomic(authPath, JSON.stringify(updatedAuth, null, 2) + "\n", { mode: 0o600 });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const message = err?.message ?? String(err);
|
|
80
|
+
return { updated: false, path: authPath, error: `Failed to write OpenCode auth.json: ${message}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { updated: true, path: authPath };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Update pi auth.json with new OpenAI Codex OAuth tokens
|
|
88
|
+
* @param {{ access: string, refresh: string, expires?: number, accountId: string }} account
|
|
89
|
+
* @returns {{ updated: boolean, path: string, error?: string, skipped?: boolean }}
|
|
90
|
+
*/
|
|
91
|
+
export function updatePiAuth(account) {
|
|
92
|
+
const authPath = getPiAuthPath();
|
|
93
|
+
if (!existsSync(authPath)) {
|
|
94
|
+
return { updated: false, path: authPath, skipped: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let existingAuth = {};
|
|
98
|
+
try {
|
|
99
|
+
const raw = readFileSync(authPath, "utf-8");
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
102
|
+
return { updated: false, path: authPath, error: "Invalid pi auth.json format" };
|
|
103
|
+
}
|
|
104
|
+
existingAuth = parsed;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err?.message ?? String(err);
|
|
107
|
+
return { updated: false, path: authPath, error: `Failed to read pi auth.json: ${message}` };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const codexEntry = existingAuth["openai-codex"];
|
|
111
|
+
const codexAuth = codexEntry && typeof codexEntry === "object" ? codexEntry : {};
|
|
112
|
+
const expires = account.expires ?? Date.now() - 1000;
|
|
113
|
+
const updatedAuth = {
|
|
114
|
+
...existingAuth,
|
|
115
|
+
"openai-codex": {
|
|
116
|
+
...codexAuth,
|
|
117
|
+
type: "oauth",
|
|
118
|
+
access: account.access,
|
|
119
|
+
refresh: account.refresh,
|
|
120
|
+
expires: expires,
|
|
121
|
+
accountId: account.accountId,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
writeFileAtomic(authPath, JSON.stringify(updatedAuth, null, 2) + "\n", { mode: 0o600 });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
const message = err?.message ?? String(err);
|
|
129
|
+
return { updated: false, path: authPath, error: `Failed to write pi auth.json: ${message}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { updated: true, path: authPath };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Persist refreshed OpenAI OAuth tokens to all known stores that match.
|
|
137
|
+
* @param {{ label: string, access: string, refresh: string, expires?: number, accountId: string, idToken?: string, source?: string }} account
|
|
138
|
+
* @param {{ previousAccessToken?: string | null, previousRefreshToken?: string | null }} previousTokens
|
|
139
|
+
* @returns {{ updatedPaths: string[], errors: string[] }}
|
|
140
|
+
*/
|
|
141
|
+
export function persistOpenAiOAuthTokens(account, previousTokens = {}) {
|
|
142
|
+
const updatedPaths = [];
|
|
143
|
+
const errors = [];
|
|
144
|
+
const previousAccess = previousTokens.previousAccessToken ?? null;
|
|
145
|
+
const previousRefresh = previousTokens.previousRefreshToken ?? null;
|
|
146
|
+
|
|
147
|
+
if (account.source?.startsWith("env")) {
|
|
148
|
+
return { updatedPaths, errors };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const codexAuthPath = getCodexCliAuthPath();
|
|
152
|
+
if (existsSync(codexAuthPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const raw = readFileSync(codexAuthPath, "utf-8");
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
const tokens = parsed?.tokens;
|
|
157
|
+
if (!tokens || typeof tokens !== "object" || Array.isArray(tokens)) {
|
|
158
|
+
errors.push(`Invalid Codex auth.json format at ${codexAuthPath}`);
|
|
159
|
+
} else {
|
|
160
|
+
const storedAccess = tokens.access_token ?? null;
|
|
161
|
+
const storedRefresh = tokens.refresh_token ?? null;
|
|
162
|
+
if (isOpenAiOauthTokenMatch({
|
|
163
|
+
storedAccess,
|
|
164
|
+
storedRefresh,
|
|
165
|
+
previousAccess,
|
|
166
|
+
previousRefresh,
|
|
167
|
+
label: account.label,
|
|
168
|
+
storedLabel: parsed?.codex_quota_label ?? null,
|
|
169
|
+
})) {
|
|
170
|
+
const updatedTokens = {
|
|
171
|
+
...tokens,
|
|
172
|
+
access_token: account.access,
|
|
173
|
+
refresh_token: account.refresh,
|
|
174
|
+
account_id: account.accountId,
|
|
175
|
+
};
|
|
176
|
+
if (account.expires) {
|
|
177
|
+
updatedTokens.expires_at = Math.floor(account.expires / 1000);
|
|
178
|
+
}
|
|
179
|
+
if (account.idToken) {
|
|
180
|
+
updatedTokens.id_token = account.idToken;
|
|
181
|
+
}
|
|
182
|
+
const updatedPayload = { ...parsed, tokens: updatedTokens };
|
|
183
|
+
writeFileAtomic(codexAuthPath, JSON.stringify(updatedPayload, null, 2) + "\n", { mode: 0o600 });
|
|
184
|
+
updatedPaths.push(codexAuthPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const message = err?.message ?? String(err);
|
|
189
|
+
errors.push(`Failed to update ${codexAuthPath}: ${message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const opencodePath = getOpencodeAuthPath();
|
|
194
|
+
if (existsSync(opencodePath)) {
|
|
195
|
+
try {
|
|
196
|
+
const raw = readFileSync(opencodePath, "utf-8");
|
|
197
|
+
const parsed = JSON.parse(raw);
|
|
198
|
+
const openai = parsed?.openai ?? null;
|
|
199
|
+
const storedAccess = openai?.access ?? null;
|
|
200
|
+
const storedRefresh = openai?.refresh ?? null;
|
|
201
|
+
if (isOpenAiOauthTokenMatch({
|
|
202
|
+
storedAccess,
|
|
203
|
+
storedRefresh,
|
|
204
|
+
previousAccess,
|
|
205
|
+
previousRefresh,
|
|
206
|
+
label: account.label,
|
|
207
|
+
storedLabel: "opencode",
|
|
208
|
+
})) {
|
|
209
|
+
const result = updateOpencodeAuth(account);
|
|
210
|
+
if (result.updated) updatedPaths.push(result.path);
|
|
211
|
+
if (result.error) errors.push(result.error);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore parse errors, handled by updateOpencodeAuth
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const piPath = getPiAuthPath();
|
|
219
|
+
if (existsSync(piPath)) {
|
|
220
|
+
try {
|
|
221
|
+
const raw = readFileSync(piPath, "utf-8");
|
|
222
|
+
const parsed = JSON.parse(raw);
|
|
223
|
+
const codex = parsed?.["openai-codex"] ?? null;
|
|
224
|
+
const storedAccess = codex?.access ?? null;
|
|
225
|
+
const storedRefresh = codex?.refresh ?? null;
|
|
226
|
+
if (isOpenAiOauthTokenMatch({
|
|
227
|
+
storedAccess,
|
|
228
|
+
storedRefresh,
|
|
229
|
+
previousAccess,
|
|
230
|
+
previousRefresh,
|
|
231
|
+
label: account.label,
|
|
232
|
+
storedLabel: "pi",
|
|
233
|
+
})) {
|
|
234
|
+
const result = updatePiAuth(account);
|
|
235
|
+
if (result.updated) updatedPaths.push(result.path);
|
|
236
|
+
if (result.error) errors.push(result.error);
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// ignore parse errors, handled by updatePiAuth
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const path of MULTI_ACCOUNT_PATHS) {
|
|
244
|
+
if (!existsSync(path)) continue;
|
|
245
|
+
try {
|
|
246
|
+
const container = readMultiAccountContainer(path);
|
|
247
|
+
if (container.rootType === "invalid") {
|
|
248
|
+
errors.push(`Failed to parse ${path}`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const mapped = mapContainerAccounts(container, (entry) => {
|
|
252
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
253
|
+
const stored = normalizeOpenAiOauthEntryTokens(entry);
|
|
254
|
+
const matches = isOpenAiOauthTokenMatch({
|
|
255
|
+
storedAccess: stored.access,
|
|
256
|
+
storedRefresh: stored.refresh,
|
|
257
|
+
previousAccess,
|
|
258
|
+
previousRefresh,
|
|
259
|
+
label: account.label,
|
|
260
|
+
storedLabel: entry?.label ?? null,
|
|
261
|
+
});
|
|
262
|
+
if (!matches) return entry;
|
|
263
|
+
return updateOpenAiOauthEntry({ ...entry }, account);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (mapped.updated) {
|
|
267
|
+
writeMultiAccountContainer(path, container, mapped.accounts, {}, { mode: 0o600 });
|
|
268
|
+
updatedPaths.push(path);
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const message = err?.message ?? String(err);
|
|
272
|
+
errors.push(`Failed to update ${path}: ${message}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { updatedPaths, errors };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function refreshToken(refreshTokenValue) {
|
|
280
|
+
const res = await fetch(TOKEN_URL, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
283
|
+
body: new URLSearchParams({
|
|
284
|
+
grant_type: "refresh_token",
|
|
285
|
+
refresh_token: refreshTokenValue,
|
|
286
|
+
client_id: CLIENT_ID,
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
if (!res.ok) return null;
|
|
290
|
+
const json = await res.json();
|
|
291
|
+
if (!json?.access_token || !json?.refresh_token || typeof json?.expires_in !== "number") {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
access: json.access_token,
|
|
296
|
+
refresh: json.refresh_token,
|
|
297
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function isOpenAiOauthTokenExpiring(expires) {
|
|
302
|
+
if (!expires) return true;
|
|
303
|
+
return expires <= Date.now() + OPENAI_OAUTH_REFRESH_BUFFER_MS;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function ensureFreshToken(account, allAccounts) {
|
|
307
|
+
if (!isOpenAiOauthTokenExpiring(account.expires)) return true;
|
|
308
|
+
const previousAccessToken = account.access;
|
|
309
|
+
const previousRefreshToken = account.refresh;
|
|
310
|
+
const refreshed = await refreshToken(account.refresh);
|
|
311
|
+
if (!refreshed) return false;
|
|
312
|
+
|
|
313
|
+
// Update accountId from new token (in case it changed)
|
|
314
|
+
const newAccountId = extractAccountId(refreshed.access);
|
|
315
|
+
if (newAccountId) account.accountId = newAccountId;
|
|
316
|
+
|
|
317
|
+
account.access = refreshed.access;
|
|
318
|
+
account.refresh = refreshed.refresh;
|
|
319
|
+
account.expires = refreshed.expires;
|
|
320
|
+
account.updatedAt = Date.now();
|
|
321
|
+
persistOpenAiOAuthTokens(account, {
|
|
322
|
+
previousAccessToken,
|
|
323
|
+
previousRefreshToken,
|
|
324
|
+
});
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex usage API fetch.
|
|
3
|
+
* Depends on: lib/constants.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { USAGE_URL } from "./constants.js";
|
|
7
|
+
|
|
8
|
+
export async function fetchUsage(account) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(USAGE_URL, {
|
|
14
|
+
method: "GET",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${account.access}`,
|
|
17
|
+
accept: "application/json",
|
|
18
|
+
"chatgpt-account-id": account.accountId,
|
|
19
|
+
originator: "codex_cli_rs",
|
|
20
|
+
},
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) {
|
|
24
|
+
return { error: `HTTP ${res.status}` };
|
|
25
|
+
}
|
|
26
|
+
return await res.json();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { error: e.message };
|
|
29
|
+
} finally {
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal color output helpers.
|
|
3
|
+
* Zero internal dependencies — only uses Node.js built-ins.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { PACKAGE_JSON_PATH } from "./constants.js";
|
|
8
|
+
|
|
9
|
+
// ANSI color codes
|
|
10
|
+
export const GREEN = "\x1b[32m";
|
|
11
|
+
export const RED = "\x1b[31m";
|
|
12
|
+
export const YELLOW = "\x1b[33m";
|
|
13
|
+
export const RESET = "\x1b[0m";
|
|
14
|
+
|
|
15
|
+
// Global flag set by main() based on CLI args
|
|
16
|
+
let noColorFlag = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Set the noColorFlag value (for testing purposes)
|
|
20
|
+
* @param {boolean} value - Whether to disable colors
|
|
21
|
+
*/
|
|
22
|
+
export function setNoColorFlag(value) {
|
|
23
|
+
noColorFlag = value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if terminal supports colors
|
|
28
|
+
* Respects NO_COLOR env var (https://no-color.org/) and --no-color flag
|
|
29
|
+
* @returns {boolean} true if colors should be used
|
|
30
|
+
*/
|
|
31
|
+
export function supportsColor() {
|
|
32
|
+
// Respect --no-color CLI flag
|
|
33
|
+
if (noColorFlag) return false;
|
|
34
|
+
// Respect NO_COLOR env var (any non-empty value disables color)
|
|
35
|
+
if (process.env.NO_COLOR) return false;
|
|
36
|
+
// Check if stdout is a TTY (not piped/redirected)
|
|
37
|
+
if (!process.stdout.isTTY) return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Apply color to text if terminal supports it
|
|
43
|
+
* @param {string} text - Text to colorize
|
|
44
|
+
* @param {string} color - ANSI color code (GREEN, RED, YELLOW)
|
|
45
|
+
* @returns {string} Colorized text or plain text if colors disabled
|
|
46
|
+
*/
|
|
47
|
+
export function colorize(text, color) {
|
|
48
|
+
if (!supportsColor()) return text;
|
|
49
|
+
return `${color}${text}${RESET}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Output data as formatted JSON to stdout
|
|
54
|
+
* Standardizes JSON output across all handlers with 2-space indent
|
|
55
|
+
* @param {any} data - Data to serialize and output
|
|
56
|
+
*/
|
|
57
|
+
export function outputJson(data) {
|
|
58
|
+
console.log(JSON.stringify(data, null, 2));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the CLI version from package.json
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function getPackageVersion() {
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf-8"));
|
|
68
|
+
return pkg.version || "unknown";
|
|
69
|
+
} catch {
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All configuration constants for codex-quota.
|
|
3
|
+
* Zero internal dependencies — only uses Node.js built-ins.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
// OAuth config (matches OpenAI Codex CLI)
|
|
11
|
+
export const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
12
|
+
export const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
13
|
+
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
14
|
+
export const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
15
|
+
export const SCOPE = "openid profile email offline_access";
|
|
16
|
+
export const OAUTH_TIMEOUT_MS = 120000; // 2 minutes
|
|
17
|
+
export const OPENAI_OAUTH_REFRESH_BUFFER_MS = 60 * 1000;
|
|
18
|
+
export const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
19
|
+
export const JWT_CLAIM = "https://api.openai.com/auth";
|
|
20
|
+
export const JWT_PROFILE = "https://api.openai.com/profile";
|
|
21
|
+
export const CLAUDE_CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
|
|
22
|
+
export const CLAUDE_MULTI_ACCOUNT_PATHS = [
|
|
23
|
+
join(homedir(), ".claude-accounts.json"),
|
|
24
|
+
];
|
|
25
|
+
export const CLAUDE_API_BASE = "https://claude.ai/api";
|
|
26
|
+
export const CLAUDE_ORIGIN = "https://claude.ai";
|
|
27
|
+
export const CLAUDE_ORGS_URL = `${CLAUDE_API_BASE}/organizations`;
|
|
28
|
+
export const CLAUDE_ACCOUNT_URL = `${CLAUDE_API_BASE}/account`;
|
|
29
|
+
export const CLAUDE_TIMEOUT_MS = 15000;
|
|
30
|
+
export const CLAUDE_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
31
|
+
|
|
32
|
+
// Claude OAuth API configuration (new official endpoint)
|
|
33
|
+
export const CLAUDE_OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
34
|
+
export const CLAUDE_OAUTH_VERSION = "2023-06-01";
|
|
35
|
+
export const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
|
|
36
|
+
export const CLAUDE_OAUTH_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
// Claude OAuth browser flow configuration
|
|
39
|
+
export const CLAUDE_OAUTH_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
40
|
+
export const CLAUDE_OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
|
|
41
|
+
export const CLAUDE_OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
|
|
42
|
+
export const CLAUDE_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
43
|
+
export const CLAUDE_OAUTH_SCOPES = "org:create_api_key user:profile user:inference";
|
|
44
|
+
|
|
45
|
+
// CLI command names
|
|
46
|
+
export const PRIMARY_CMD = "codex-quota";
|
|
47
|
+
export const PACKAGE_JSON_PATH = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
48
|
+
|
|
49
|
+
export const MULTI_ACCOUNT_PATHS = [
|
|
50
|
+
join(homedir(), ".codex-accounts.json"),
|
|
51
|
+
join(homedir(), ".opencode", "openai-codex-auth-accounts.json"),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export const CODEX_CLI_AUTH_PATH = join(homedir(), ".codex", "auth.json");
|
|
55
|
+
export const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
56
|
+
export const DEFAULT_XDG_DATA_HOME = join(homedir(), ".local", "share");
|
|
57
|
+
export const MULTI_ACCOUNT_SCHEMA_VERSION = 1;
|