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.
Files changed (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +589 -0
  3. package/SKILL.md +149 -0
  4. package/assets/workflow-catalog.json +57 -0
  5. package/bin/audit-issue-routing.sh +74 -0
  6. package/bin/issue-resource-class.sh +58 -0
  7. package/bin/label-follow-up-issues.sh +114 -0
  8. package/bin/pr-risk.sh +532 -0
  9. package/bin/sync-pr-labels.sh +112 -0
  10. package/hooks/heartbeat-hooks.sh +573 -0
  11. package/hooks/issue-reconcile-hooks.sh +217 -0
  12. package/hooks/pr-reconcile-hooks.sh +225 -0
  13. package/npm/bin/agent-control-plane.js +1984 -0
  14. package/npm/public-bin/agent-control-plane +3 -0
  15. package/package.json +61 -0
  16. package/tools/bin/agent-cleanup-worktree +247 -0
  17. package/tools/bin/agent-github-update-labels +66 -0
  18. package/tools/bin/agent-init-worktree +216 -0
  19. package/tools/bin/agent-project-archive-run +52 -0
  20. package/tools/bin/agent-project-capture-worker +46 -0
  21. package/tools/bin/agent-project-catch-up-merged-prs +137 -0
  22. package/tools/bin/agent-project-cleanup-session +244 -0
  23. package/tools/bin/agent-project-detached-launch +107 -0
  24. package/tools/bin/agent-project-heartbeat-loop +2347 -0
  25. package/tools/bin/agent-project-open-issue-worktree +89 -0
  26. package/tools/bin/agent-project-open-pr-worktree +80 -0
  27. package/tools/bin/agent-project-publish-issue-pr +349 -0
  28. package/tools/bin/agent-project-reconcile-issue-session +1128 -0
  29. package/tools/bin/agent-project-reconcile-pr-session +1005 -0
  30. package/tools/bin/agent-project-retry-state +147 -0
  31. package/tools/bin/agent-project-run-claude-session +657 -0
  32. package/tools/bin/agent-project-run-codex-resilient +718 -0
  33. package/tools/bin/agent-project-run-codex-session +316 -0
  34. package/tools/bin/agent-project-run-kilo-session +27 -0
  35. package/tools/bin/agent-project-run-openclaw-session +984 -0
  36. package/tools/bin/agent-project-run-opencode-session +27 -0
  37. package/tools/bin/agent-project-sync-anchor-repo +128 -0
  38. package/tools/bin/agent-project-worker-status +143 -0
  39. package/tools/bin/audit-agent-worktrees.sh +310 -0
  40. package/tools/bin/audit-issue-routing.sh +11 -0
  41. package/tools/bin/audit-retained-layout.sh +58 -0
  42. package/tools/bin/audit-retained-overlap.sh +135 -0
  43. package/tools/bin/audit-retained-worktrees.sh +228 -0
  44. package/tools/bin/branch-verification-guard.sh +351 -0
  45. package/tools/bin/capture-worker.sh +18 -0
  46. package/tools/bin/check-skill-contracts.sh +324 -0
  47. package/tools/bin/cleanup-worktree.sh +44 -0
  48. package/tools/bin/codex-quota +31 -0
  49. package/tools/bin/create-follow-up-issue.sh +114 -0
  50. package/tools/bin/dashboard-launchd-bootstrap.sh +38 -0
  51. package/tools/bin/flow-config-lib.sh +2127 -0
  52. package/tools/bin/flow-resident-worker-lib.sh +683 -0
  53. package/tools/bin/flow-runtime-doctor.sh +97 -0
  54. package/tools/bin/flow-shell-lib.sh +266 -0
  55. package/tools/bin/heartbeat-recovery-preflight.sh +106 -0
  56. package/tools/bin/heartbeat-safe-auto.sh +551 -0
  57. package/tools/bin/install-dashboard-launchd.sh +152 -0
  58. package/tools/bin/install-project-launchd.sh +219 -0
  59. package/tools/bin/issue-publish-scope-guard.sh +242 -0
  60. package/tools/bin/issue-requires-local-workspace-install.sh +31 -0
  61. package/tools/bin/issue-resource-class.sh +12 -0
  62. package/tools/bin/kick-scheduler.sh +75 -0
  63. package/tools/bin/label-follow-up-issues.sh +14 -0
  64. package/tools/bin/new-pr-worktree.sh +50 -0
  65. package/tools/bin/new-worktree.sh +49 -0
  66. package/tools/bin/pr-risk.sh +12 -0
  67. package/tools/bin/prepare-worktree.sh +140 -0
  68. package/tools/bin/profile-activate.sh +109 -0
  69. package/tools/bin/profile-adopt.sh +219 -0
  70. package/tools/bin/profile-smoke.sh +461 -0
  71. package/tools/bin/project-init.sh +189 -0
  72. package/tools/bin/project-launchd-bootstrap.sh +54 -0
  73. package/tools/bin/project-remove.sh +155 -0
  74. package/tools/bin/project-runtime-supervisor.sh +56 -0
  75. package/tools/bin/project-runtimectl.sh +586 -0
  76. package/tools/bin/provider-cooldown-state.sh +166 -0
  77. package/tools/bin/publish-issue-worker.sh +31 -0
  78. package/tools/bin/reconcile-issue-worker.sh +34 -0
  79. package/tools/bin/reconcile-pr-worker.sh +34 -0
  80. package/tools/bin/record-verification.sh +71 -0
  81. package/tools/bin/render-architecture-infographics.sh +110 -0
  82. package/tools/bin/render-dashboard-demo-media.sh +333 -0
  83. package/tools/bin/render-dashboard-snapshot.py +16 -0
  84. package/tools/bin/render-flow-config.sh +86 -0
  85. package/tools/bin/retry-state.sh +31 -0
  86. package/tools/bin/reuse-issue-worktree.sh +75 -0
  87. package/tools/bin/run-codex-bypass.sh +3 -0
  88. package/tools/bin/run-codex-safe.sh +3 -0
  89. package/tools/bin/run-codex-task.sh +231 -0
  90. package/tools/bin/scaffold-profile.sh +374 -0
  91. package/tools/bin/serve-dashboard.sh +5 -0
  92. package/tools/bin/split-retained-slice.sh +124 -0
  93. package/tools/bin/start-issue-worker.sh +796 -0
  94. package/tools/bin/start-pr-fix-worker.sh +458 -0
  95. package/tools/bin/start-pr-merge-repair-worker.sh +8 -0
  96. package/tools/bin/start-pr-review-worker.sh +227 -0
  97. package/tools/bin/start-resident-issue-loop.sh +908 -0
  98. package/tools/bin/sync-agent-repo.sh +52 -0
  99. package/tools/bin/sync-dependency-baseline.sh +247 -0
  100. package/tools/bin/sync-pr-labels.sh +12 -0
  101. package/tools/bin/sync-recurring-issue-checklist.sh +274 -0
  102. package/tools/bin/sync-shared-agent-home.sh +214 -0
  103. package/tools/bin/sync-vscode-workspace.sh +157 -0
  104. package/tools/bin/test-smoke.sh +63 -0
  105. package/tools/bin/uninstall-project-launchd.sh +55 -0
  106. package/tools/bin/update-github-labels.sh +14 -0
  107. package/tools/bin/worker-status.sh +19 -0
  108. package/tools/bin/workflow-catalog.sh +77 -0
  109. package/tools/dashboard/app.js +286 -0
  110. package/tools/dashboard/dashboard_snapshot.py +466 -0
  111. package/tools/dashboard/index.html +41 -0
  112. package/tools/dashboard/server.py +64 -0
  113. package/tools/dashboard/styles.css +351 -0
  114. package/tools/templates/issue-prompt-template.md +109 -0
  115. package/tools/templates/pr-fix-template.md +120 -0
  116. package/tools/templates/pr-merge-repair-template.md +91 -0
  117. package/tools/templates/pr-review-template.md +62 -0
  118. package/tools/templates/scheduled-issue-prompt-template.md +62 -0
  119. package/tools/tests/test-agent-control-plane-npm-cli.sh +279 -0
  120. package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +56 -0
  121. package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +89 -0
  122. package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +82 -0
  123. package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +90 -0
  124. package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +73 -0
  125. package/tools/tests/test-agent-project-claude-session-wrapper.sh +103 -0
  126. package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +90 -0
  127. package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +90 -0
  128. package/tools/tests/test-agent-project-codex-live-thread-persist.sh +76 -0
  129. package/tools/tests/test-agent-project-codex-recovery.sh +731 -0
  130. package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +105 -0
  131. package/tools/tests/test-agent-project-codex-session-wrapper.sh +97 -0
  132. package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +81 -0
  133. package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +109 -0
  134. package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +89 -0
  135. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +113 -0
  136. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +135 -0
  137. package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +179 -0
  138. package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +119 -0
  139. package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +91 -0
  140. package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +117 -0
  141. package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +148 -0
  142. package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +146 -0
  143. package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +145 -0
  144. package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +147 -0
  145. package/tools/tests/test-agent-project-reconcile-failure-reason.sh +456 -0
  146. package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +96 -0
  147. package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +90 -0
  148. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +212 -0
  149. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +207 -0
  150. package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +101 -0
  151. package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +113 -0
  152. package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +117 -0
  153. package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +55 -0
  154. package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +125 -0
  155. package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +118 -0
  156. package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +144 -0
  157. package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +145 -0
  158. package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +139 -0
  159. package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +242 -0
  160. package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +142 -0
  161. package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +106 -0
  162. package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +66 -0
  163. package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +129 -0
  164. package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +69 -0
  165. package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +43 -0
  166. package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +46 -0
  167. package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +79 -0
  168. package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +56 -0
  169. package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +72 -0
  170. package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +125 -0
  171. package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +178 -0
  172. package/tools/tests/test-codex-quota-wrapper.sh +37 -0
  173. package/tools/tests/test-contribution-docs.sh +18 -0
  174. package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +343 -0
  175. package/tools/tests/test-create-follow-up-issue.sh +73 -0
  176. package/tools/tests/test-dashboard-launchd-bootstrap.sh +55 -0
  177. package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +30 -0
  178. package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +48 -0
  179. package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +85 -0
  180. package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +60 -0
  181. package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +64 -0
  182. package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +77 -0
  183. package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +52 -0
  184. package/tools/tests/test-flow-resident-reap-stale-controllers.sh +63 -0
  185. package/tools/tests/test-flow-resolve-codex-quota-tools.sh +104 -0
  186. package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +27 -0
  187. package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +79 -0
  188. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +115 -0
  189. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +117 -0
  190. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +96 -0
  191. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +96 -0
  192. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +96 -0
  193. package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +170 -0
  194. package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +201 -0
  195. package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +201 -0
  196. package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +160 -0
  197. package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +133 -0
  198. package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +157 -0
  199. package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +181 -0
  200. package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +160 -0
  201. package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +134 -0
  202. package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +162 -0
  203. package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +136 -0
  204. package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +139 -0
  205. package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +119 -0
  206. package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +140 -0
  207. package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +142 -0
  208. package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +141 -0
  209. package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +65 -0
  210. package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +179 -0
  211. package/tools/tests/test-install-dashboard-launchd.sh +78 -0
  212. package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +87 -0
  213. package/tools/tests/test-install-project-launchd.sh +110 -0
  214. package/tools/tests/test-issue-local-workspace-install-policy.sh +81 -0
  215. package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +70 -0
  216. package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +36 -0
  217. package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +47 -0
  218. package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +132 -0
  219. package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +64 -0
  220. package/tools/tests/test-package-funding-metadata.sh +21 -0
  221. package/tools/tests/test-package-public-metadata.sh +62 -0
  222. package/tools/tests/test-placeholder-worker-adapters.sh +38 -0
  223. package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +110 -0
  224. package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +70 -0
  225. package/tools/tests/test-pr-risk-fix-label-semantics.sh +114 -0
  226. package/tools/tests/test-pr-risk-local-first-no-checks.sh +70 -0
  227. package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +67 -0
  228. package/tools/tests/test-profile-activate.sh +33 -0
  229. package/tools/tests/test-profile-adopt-allow-missing-repo.sh +68 -0
  230. package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +61 -0
  231. package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +90 -0
  232. package/tools/tests/test-profile-smoke-collision.sh +44 -0
  233. package/tools/tests/test-profile-smoke-invalid-claude-config.sh +31 -0
  234. package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +68 -0
  235. package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +36 -0
  236. package/tools/tests/test-profile-smoke.sh +45 -0
  237. package/tools/tests/test-project-init-force-and-skip-sync.sh +61 -0
  238. package/tools/tests/test-project-init-repo-slug-mismatch.sh +29 -0
  239. package/tools/tests/test-project-init.sh +66 -0
  240. package/tools/tests/test-project-launchd-bootstrap.sh +66 -0
  241. package/tools/tests/test-project-remove.sh +150 -0
  242. package/tools/tests/test-project-runtime-supervisor.sh +47 -0
  243. package/tools/tests/test-project-runtimectl-launchd.sh +115 -0
  244. package/tools/tests/test-project-runtimectl-missing-profile.sh +54 -0
  245. package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +108 -0
  246. package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +95 -0
  247. package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +59 -0
  248. package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +85 -0
  249. package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +78 -0
  250. package/tools/tests/test-project-runtimectl.sh +212 -0
  251. package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +39 -0
  252. package/tools/tests/test-provider-cooldown-state.sh +59 -0
  253. package/tools/tests/test-public-repo-docs.sh +159 -0
  254. package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +75 -0
  255. package/tools/tests/test-render-dashboard-snapshot.sh +149 -0
  256. package/tools/tests/test-render-flow-config-demo-profile.sh +36 -0
  257. package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +81 -0
  258. package/tools/tests/test-render-flow-config.sh +52 -0
  259. package/tools/tests/test-run-codex-task-claude-routing.sh +125 -0
  260. package/tools/tests/test-run-codex-task-codex-resident-routing.sh +108 -0
  261. package/tools/tests/test-run-codex-task-kilo-routing.sh +98 -0
  262. package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +117 -0
  263. package/tools/tests/test-run-codex-task-openclaw-routing.sh +113 -0
  264. package/tools/tests/test-run-codex-task-opencode-routing.sh +98 -0
  265. package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +146 -0
  266. package/tools/tests/test-scaffold-profile.sh +108 -0
  267. package/tools/tests/test-serve-dashboard.sh +93 -0
  268. package/tools/tests/test-start-issue-worker-blocked-context.sh +129 -0
  269. package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +189 -0
  270. package/tools/tests/test-start-issue-worker-local-install-routing.sh +157 -0
  271. package/tools/tests/test-start-issue-worker-profile-template-routing.sh +149 -0
  272. package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +212 -0
  273. package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +219 -0
  274. package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +155 -0
  275. package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +199 -0
  276. package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +275 -0
  277. package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +185 -0
  278. package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +152 -0
  279. package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +186 -0
  280. package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +212 -0
  281. package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +148 -0
  282. package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +194 -0
  283. package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +198 -0
  284. package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +145 -0
  285. package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +67 -0
  286. package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +70 -0
  287. package/tools/tests/test-sync-recurring-issue-checklist.sh +95 -0
  288. package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +66 -0
  289. package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +47 -0
  290. package/tools/tests/test-test-smoke.sh +86 -0
  291. package/tools/tests/test-uninstall-project-launchd.sh +37 -0
  292. package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +49 -0
  293. package/tools/tests/test-workflow-catalog.sh +43 -0
  294. package/tools/vendor/codex-quota/LICENSE +21 -0
  295. package/tools/vendor/codex-quota/README.md +459 -0
  296. package/tools/vendor/codex-quota/codex-quota.js +261 -0
  297. package/tools/vendor/codex-quota/lib/claude-accounts.js +226 -0
  298. package/tools/vendor/codex-quota/lib/claude-oauth.js +174 -0
  299. package/tools/vendor/codex-quota/lib/claude-tokens.js +471 -0
  300. package/tools/vendor/codex-quota/lib/claude-usage.js +929 -0
  301. package/tools/vendor/codex-quota/lib/codex-accounts.js +205 -0
  302. package/tools/vendor/codex-quota/lib/codex-tokens.js +326 -0
  303. package/tools/vendor/codex-quota/lib/codex-usage.js +32 -0
  304. package/tools/vendor/codex-quota/lib/color.js +72 -0
  305. package/tools/vendor/codex-quota/lib/constants.js +57 -0
  306. package/tools/vendor/codex-quota/lib/container.js +143 -0
  307. package/tools/vendor/codex-quota/lib/display.js +1111 -0
  308. package/tools/vendor/codex-quota/lib/fs.js +63 -0
  309. package/tools/vendor/codex-quota/lib/handlers.js +2060 -0
  310. package/tools/vendor/codex-quota/lib/jwt.js +33 -0
  311. package/tools/vendor/codex-quota/lib/oauth.js +486 -0
  312. package/tools/vendor/codex-quota/lib/paths.js +34 -0
  313. package/tools/vendor/codex-quota/lib/prompts.js +44 -0
  314. package/tools/vendor/codex-quota/lib/sync.js +1438 -0
  315. package/tools/vendor/codex-quota/lib/token-match.js +96 -0
  316. package/tools/vendor/codex-quota-manager/scripts/auto-switch.sh +500 -0
  317. 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;