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,929 @@
1
+ /**
2
+ * Claude usage API fetch (session + OAuth).
3
+ * Depends on: lib/constants.js, lib/paths.js, lib/claude-accounts.js, lib/claude-tokens.js
4
+ */
5
+
6
+ import { existsSync, readFileSync, copyFileSync, unlinkSync } from "node:fs";
7
+ import { spawnSync } from "node:child_process";
8
+ import { randomBytes, pbkdf2Sync, createDecipheriv } from "node:crypto";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import {
12
+ CLAUDE_CREDENTIALS_PATH,
13
+ CLAUDE_MULTI_ACCOUNT_PATHS,
14
+ CLAUDE_API_BASE,
15
+ CLAUDE_ORIGIN,
16
+ CLAUDE_ORGS_URL,
17
+ CLAUDE_ACCOUNT_URL,
18
+ CLAUDE_TIMEOUT_MS,
19
+ CLAUDE_USER_AGENT,
20
+ CLAUDE_OAUTH_USAGE_URL,
21
+ CLAUDE_OAUTH_VERSION,
22
+ CLAUDE_OAUTH_BETA,
23
+ } from "./constants.js";
24
+ import { getOpencodeAuthPath } from "./paths.js";
25
+ import {
26
+ findClaudeSessionKey,
27
+ loadClaudeAccountsFromFile,
28
+ loadClaudeSessionFromCredentials,
29
+ loadClaudeOAuthToken,
30
+ } from "./claude-accounts.js";
31
+ import { ensureFreshClaudeOAuthToken } from "./claude-tokens.js";
32
+
33
+ export function normalizeClaudeOrgId(orgId) {
34
+ if (!orgId || typeof orgId !== "string") return orgId;
35
+ if (/^[0-9a-f-]{36}$/i.test(orgId)) {
36
+ return orgId.replace(/-/g, "");
37
+ }
38
+ return orgId;
39
+ }
40
+
41
+ export function isClaudeAuthError(error) {
42
+ if (!error) return false;
43
+ return /account_session_invalid|invalid authorization|http 401|http 403/i.test(String(error));
44
+ }
45
+
46
+ export function loadClaudeOAuthFromClaudeCode() {
47
+ const credentialsPath = process.env.CLAUDE_CREDENTIALS_PATH || CLAUDE_CREDENTIALS_PATH;
48
+ if (!existsSync(credentialsPath)) return [];
49
+
50
+ try {
51
+ const raw = readFileSync(credentialsPath, "utf-8");
52
+ const parsed = JSON.parse(raw);
53
+ const oauth = parsed?.claudeAiOauth ?? parsed?.claude_ai_oauth;
54
+
55
+ if (!oauth?.accessToken) return [];
56
+
57
+ // Check if token has user:profile scope (required for usage API)
58
+ const scopes = oauth.scopes ?? [];
59
+ if (!scopes.includes("user:profile")) {
60
+ return [];
61
+ }
62
+
63
+ return [{
64
+ label: "claude-code",
65
+ accessToken: oauth.accessToken,
66
+ refreshToken: oauth.refreshToken,
67
+ expiresAt: oauth.expiresAt,
68
+ subscriptionType: oauth.subscriptionType,
69
+ rateLimitTier: oauth.rateLimitTier,
70
+ scopes,
71
+ source: credentialsPath,
72
+ }];
73
+ } catch {
74
+ return [];
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Load Claude OAuth account from OpenCode auth.json
80
+ * @returns {Array<{ label: string, accessToken: string, refreshToken?: string, expiresAt?: number, source: string }>}
81
+ */
82
+ export function loadClaudeOAuthFromOpenCode() {
83
+ const authPath = getOpencodeAuthPath();
84
+ if (!existsSync(authPath)) return [];
85
+
86
+ try {
87
+ const raw = readFileSync(authPath, "utf-8");
88
+ const parsed = JSON.parse(raw);
89
+ const anthropic = parsed?.anthropic;
90
+
91
+ if (!anthropic?.access) return [];
92
+
93
+ return [{
94
+ label: "opencode",
95
+ accessToken: anthropic.access,
96
+ refreshToken: anthropic.refresh,
97
+ expiresAt: anthropic.expires,
98
+ source: authPath,
99
+ }];
100
+ } catch {
101
+ return [];
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Load Claude OAuth accounts from environment variable
107
+ * Format: JSON array with { label, accessToken, refreshToken?, ... }
108
+ * @returns {Array<{ label: string, accessToken: string, ... }>}
109
+ */
110
+ export function loadClaudeOAuthFromEnv() {
111
+ const envAccounts = process.env.CLAUDE_OAUTH_ACCOUNTS;
112
+ if (!envAccounts) return [];
113
+
114
+ try {
115
+ const parsed = JSON.parse(envAccounts);
116
+ const accounts = Array.isArray(parsed) ? parsed : parsed?.accounts ?? [];
117
+ return accounts
118
+ .filter(a => a?.label && a?.accessToken)
119
+ .map(a => ({ ...a, source: "env:CLAUDE_OAUTH_ACCOUNTS" }));
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Deduplicate Claude OAuth accounts by refresh token
127
+ * This handles the case where the same Claude account is sourced from multiple files
128
+ * (e.g., claude-code and opencode both storing the same credentials)
129
+ *
130
+ * We use refreshToken because:
131
+ * - Access tokens change on refresh, but refresh tokens stay constant
132
+ * - Two entries with same refresh token are the same underlying account
133
+ * @param {Array<{accessToken: string, refreshToken?: string, ...}>} accounts - Array of accounts
134
+ * @returns {Array<{accessToken: string, ...}>} Deduplicated accounts
135
+ */
136
+ export function deduplicateClaudeOAuthAccounts(accounts) {
137
+ const seenTokens = new Set();
138
+ return accounts.filter(account => {
139
+ if (!account.accessToken) return true; // Keep accounts without token (shouldn't happen)
140
+ // Use refresh token if available (stays constant), otherwise fall back to access token
141
+ const tokenKey = account.refreshToken
142
+ ? account.refreshToken.substring(0, 50)
143
+ : account.accessToken.substring(0, 50);
144
+ if (seenTokens.has(tokenKey)) return false;
145
+ seenTokens.add(tokenKey);
146
+ return true;
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Deduplicate Claude usage results by comparing usage fingerprints
152
+ * This catches cases where the same account has different OAuth tokens
153
+ * (e.g., claude-code and opencode both logged into the same Claude account)
154
+ *
155
+ * We consider two results identical if they have the same utilization values.
156
+ * Reset times are NOT included since they can differ by milliseconds between calls.
157
+ *
158
+ * @param {Array<{usage: object, ...}>} results - Array of fetched usage results
159
+ * @returns {Array<{usage: object, ...}>} Deduplicated results
160
+ */
161
+ export function deduplicateClaudeResultsByUsage(results) {
162
+ const seen = new Set();
163
+ return results.filter(result => {
164
+ if (!result.success || !result.usage) return true; // Keep errors/failures
165
+
166
+ // Create a fingerprint from utilization values only (not reset times)
167
+ const usage = result.usage;
168
+ const fiveHour = usage.five_hour?.utilization ?? "null";
169
+ const sevenDay = usage.seven_day?.utilization ?? "null";
170
+ const sevenDayOpus = usage.seven_day_opus?.utilization ?? "null";
171
+ const sevenDaySonnet = usage.seven_day_sonnet?.utilization ?? "null";
172
+
173
+ // Fingerprint: all utilization values concatenated
174
+ // Same account will have identical utilization regardless of which OAuth token is used
175
+ const fingerprint = `${fiveHour}|${sevenDay}|${sevenDayOpus}|${sevenDaySonnet}`;
176
+
177
+ if (seen.has(fingerprint)) return false;
178
+ seen.add(fingerprint);
179
+ return true;
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Load all Claude OAuth accounts from all sources
185
+ * Sources (in priority order):
186
+ * 1. CLAUDE_OAUTH_ACCOUNTS env var
187
+ * 2. ~/.claude-accounts.json (accounts with oauthToken field)
188
+ * 3. ~/.claude/.credentials.json (Claude Code) [skipped when local=true]
189
+ * 4. ~/.local/share/opencode/auth.json (OpenCode) [skipped when local=true]
190
+ * Deduplicates by accessToken to prevent showing same account twice
191
+ * @param {{ local?: boolean }} [options] - When local=true, skip harness auth files
192
+ * @returns {Array<{ label: string, accessToken: string, refreshToken?: string, expiresAt?: number, subscriptionType?: string, rateLimitTier?: string, source: string }>}
193
+ */
194
+ export function loadAllClaudeOAuthAccounts(options = {}) {
195
+ const all = [];
196
+ const seenLabels = new Set();
197
+
198
+ // 1. Environment variable
199
+ for (const account of loadClaudeOAuthFromEnv()) {
200
+ if (!seenLabels.has(account.label)) {
201
+ seenLabels.add(account.label);
202
+ all.push(account);
203
+ }
204
+ }
205
+
206
+ // 2. Multi-account file (accounts with oauthToken)
207
+ for (const path of CLAUDE_MULTI_ACCOUNT_PATHS) {
208
+ const accounts = loadClaudeAccountsFromFile(path);
209
+ for (const account of accounts) {
210
+ if (account.oauthToken && !seenLabels.has(account.label)) {
211
+ seenLabels.add(account.label);
212
+ all.push({
213
+ label: account.label,
214
+ accessToken: account.oauthToken,
215
+ // Pass through new OAuth metadata fields (optional, may be null for legacy accounts)
216
+ refreshToken: account.oauthRefreshToken || null,
217
+ expiresAt: account.oauthExpiresAt || null,
218
+ scopes: account.oauthScopes || null,
219
+ source: account.source,
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ // 3. Claude Code credentials (skip in local mode)
226
+ if (!options.local) {
227
+ for (const account of loadClaudeOAuthFromClaudeCode()) {
228
+ if (!seenLabels.has(account.label)) {
229
+ seenLabels.add(account.label);
230
+ all.push(account);
231
+ }
232
+ }
233
+ }
234
+
235
+ // 4. OpenCode credentials (skip in local mode)
236
+ if (!options.local) {
237
+ for (const account of loadClaudeOAuthFromOpenCode()) {
238
+ if (!seenLabels.has(account.label)) {
239
+ seenLabels.add(account.label);
240
+ all.push(account);
241
+ }
242
+ }
243
+ }
244
+
245
+ // 5. Deduplicate by accessToken (same account from multiple sources with different labels)
246
+ return deduplicateClaudeOAuthAccounts(all);
247
+ }
248
+
249
+ /**
250
+ * Fetch Claude usage via OAuth API (new official endpoint)
251
+ * Endpoint: GET https://api.anthropic.com/api/oauth/usage
252
+ * Required headers:
253
+ * - Authorization: Bearer <access_token>
254
+ * - anthropic-version: 2023-06-01
255
+ * - anthropic-beta: oauth-2025-04-20
256
+ * @param {string} accessToken - OAuth access token with user:profile scope
257
+ * @returns {Promise<{ success: boolean, data?: object, error?: string }>}
258
+ */
259
+ export async function fetchClaudeOAuthUsage(accessToken) {
260
+ const controller = new AbortController();
261
+ const timeout = setTimeout(() => controller.abort(), CLAUDE_TIMEOUT_MS);
262
+
263
+ try {
264
+ const res = await fetch(CLAUDE_OAUTH_USAGE_URL, {
265
+ method: "GET",
266
+ headers: {
267
+ Authorization: `Bearer ${accessToken}`,
268
+ "anthropic-version": CLAUDE_OAUTH_VERSION,
269
+ "anthropic-beta": CLAUDE_OAUTH_BETA,
270
+ },
271
+ signal: controller.signal,
272
+ });
273
+
274
+ if (!res.ok) {
275
+ const body = await res.text().catch(() => "");
276
+ return {
277
+ success: false,
278
+ error: `HTTP ${res.status}: ${body.slice(0, 200) || res.statusText}`,
279
+ };
280
+ }
281
+
282
+ const data = await res.json();
283
+ return { success: true, data };
284
+ } catch (err) {
285
+ const message = err.name === "AbortError" ? "Request timed out" : err.message;
286
+ return { success: false, error: message };
287
+ } finally {
288
+ clearTimeout(timeout);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Fetch usage for a Claude OAuth account
294
+ * @param {{ label: string, accessToken: string, ... }} account - OAuth account
295
+ * @returns {Promise<{ success: boolean, label: string, source: string, usage?: object, ... }>}
296
+ */
297
+ export async function fetchClaudeOAuthUsageForAccount(account) {
298
+ const refreshed = await ensureFreshClaudeOAuthToken(account);
299
+ if (!refreshed) {
300
+ const message = account.refreshToken
301
+ ? "OAuth token expired and refresh failed - run 'claude /login'"
302
+ : "OAuth token expired - refresh token missing, run 'claude /login'";
303
+ return {
304
+ success: false,
305
+ label: account.label,
306
+ source: account.source,
307
+ error: message,
308
+ subscriptionType: account.subscriptionType,
309
+ rateLimitTier: account.rateLimitTier,
310
+ };
311
+ }
312
+
313
+ const result = await fetchClaudeOAuthUsage(account.accessToken);
314
+
315
+ if (!result.success) {
316
+ return {
317
+ success: false,
318
+ label: account.label,
319
+ source: account.source,
320
+ error: result.error,
321
+ subscriptionType: account.subscriptionType,
322
+ rateLimitTier: account.rateLimitTier,
323
+ };
324
+ }
325
+
326
+ return {
327
+ success: true,
328
+ label: account.label,
329
+ source: account.source,
330
+ usage: result.data,
331
+ subscriptionType: account.subscriptionType,
332
+ rateLimitTier: account.rateLimitTier,
333
+ };
334
+ }
335
+
336
+ export function getChromeSafeStoragePassword() {
337
+ const candidates = ["chromium", "chrome", "google-chrome", "google-chrome-canary"];
338
+ for (const app of candidates) {
339
+ try {
340
+ const result = spawnSync("secret-tool", ["lookup", "application", app], {
341
+ encoding: "utf-8",
342
+ });
343
+ if (result.status === 0) {
344
+ const value = (result.stdout || "").trim();
345
+ if (value) return value;
346
+ }
347
+ } catch {
348
+ // ignore
349
+ }
350
+ }
351
+ return "peanuts";
352
+ }
353
+
354
+ export function decryptChromeCookie(encryptedValue, password) {
355
+ if (!encryptedValue || encryptedValue.length < 4) return null;
356
+ const prefix = encryptedValue.slice(0, 3).toString("utf-8");
357
+ if (prefix !== "v10" && prefix !== "v11") {
358
+ try {
359
+ return encryptedValue.toString("utf-8");
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+
365
+ try {
366
+ const ciphertext = encryptedValue.slice(3);
367
+ const key = pbkdf2Sync(password, "saltysalt", 1, 16, "sha1");
368
+ const iv = Buffer.alloc(16, " ");
369
+ const decipher = createDecipheriv("aes-128-cbc", key, iv);
370
+ let decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
371
+ const pad = decrypted[decrypted.length - 1];
372
+ if (pad > 0 && pad <= 16) {
373
+ decrypted = decrypted.slice(0, -pad);
374
+ }
375
+ return decrypted.toString("utf-8");
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+
381
+ export function stripNonPrintable(value) {
382
+ if (!value) return value;
383
+ return value.replace(/^[^\x20-\x7E]+/, "").replace(/[^\x20-\x7E]+$/, "");
384
+ }
385
+
386
+ export function extractClaudeCookieValue(value, name = null) {
387
+ const cleaned = stripNonPrintable(value);
388
+ if (!cleaned) return null;
389
+ const asciiOnly = cleaned.replace(/[^\x20-\x7E]/g, "");
390
+ if (!asciiOnly) return null;
391
+ if (name === "sessionKey") {
392
+ const match = asciiOnly.match(/sk-ant-[a-z0-9_-]+/i);
393
+ return match ? match[0] : null;
394
+ }
395
+ if (name === "cf_clearance") {
396
+ const match = asciiOnly.match(/[A-Za-z0-9._-]{20,}/);
397
+ return match ? match[0] : null;
398
+ }
399
+ if (name === "lastActiveOrg") {
400
+ const match = asciiOnly.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
401
+ return match ? match[0] : null;
402
+ }
403
+ return asciiOnly;
404
+ }
405
+
406
+ export function readClaudeCookiesFromDb(cookiePath) {
407
+ const tempPath = join(tmpdir(), `cq-claude-cookies-${randomBytes(6).toString("hex")}.db`);
408
+ try {
409
+ copyFileSync(cookiePath, tempPath);
410
+ const query = [
411
+ "select name, value, hex(encrypted_value)",
412
+ "from cookies",
413
+ "where host_key like '%claude.ai%'",
414
+ ";",
415
+ ].join(" ");
416
+ const result = spawnSync("sqlite3", ["-readonly", "-separator", "\t", tempPath, query], {
417
+ encoding: "utf-8",
418
+ });
419
+ if (result.status !== 0) {
420
+ return { error: result.stderr?.trim() || "Failed to read cookie DB" };
421
+ }
422
+ const lines = (result.stdout || "").trim().split("\n").filter(Boolean);
423
+ if (!lines.length) {
424
+ return { error: "No Claude cookies found in DB" };
425
+ }
426
+
427
+ const password = getChromeSafeStoragePassword();
428
+ const cookies = {};
429
+
430
+ for (const line of lines) {
431
+ const [name, plainValue, hexValue] = line.split("\t");
432
+ if (!name) continue;
433
+ if (plainValue) {
434
+ const value = extractClaudeCookieValue(plainValue, name);
435
+ if (value) cookies[name] = value;
436
+ continue;
437
+ }
438
+ if (hexValue) {
439
+ const buffer = Buffer.from(hexValue, "hex");
440
+ const decrypted = decryptChromeCookie(buffer, password);
441
+ const value = extractClaudeCookieValue(decrypted, name);
442
+ if (value) cookies[name] = value;
443
+ }
444
+ }
445
+
446
+ return {
447
+ sessionKey: cookies.sessionKey ?? null,
448
+ cfClearance: cookies.cf_clearance ?? null,
449
+ cookies,
450
+ };
451
+ } catch (err) {
452
+ return { error: err?.message ?? String(err) };
453
+ } finally {
454
+ try {
455
+ unlinkSync(tempPath);
456
+ } catch {
457
+ // ignore
458
+ }
459
+ }
460
+ }
461
+
462
+ export function loadClaudeCookieCandidates() {
463
+ const overridePath = process.env.CLAUDE_COOKIE_DB_PATH;
464
+ const candidates = overridePath
465
+ ? [overridePath]
466
+ : [
467
+ join(homedir(), ".config", "chromium", "Default", "Cookies"),
468
+ join(homedir(), ".config", "google-chrome", "Default", "Cookies"),
469
+ join(homedir(), ".config", "google-chrome-canary", "Default", "Cookies"),
470
+ join(homedir(), ".config", "google-chrome-for-testing", "Default", "Cookies"),
471
+ ];
472
+
473
+ const sessions = [];
474
+
475
+ for (const cookiePath of candidates) {
476
+ if (!existsSync(cookiePath)) continue;
477
+ const result = readClaudeCookiesFromDb(cookiePath);
478
+ if (result.sessionKey) {
479
+ sessions.push({
480
+ sessionKey: result.sessionKey,
481
+ cfClearance: result.cfClearance ?? null,
482
+ cookies: result.cookies ?? null,
483
+ source: cookiePath,
484
+ });
485
+ }
486
+ }
487
+
488
+ return sessions;
489
+ }
490
+
491
+ export function loadClaudeSessionCandidates() {
492
+ const sessions = [];
493
+ const cookieSessions = loadClaudeCookieCandidates();
494
+ const oauth = loadClaudeOAuthToken();
495
+ for (const session of cookieSessions) {
496
+ sessions.push({
497
+ ...session,
498
+ oauthToken: oauth.token ?? null,
499
+ });
500
+ }
501
+
502
+ const credentialsSession = loadClaudeSessionFromCredentials();
503
+ if (credentialsSession.sessionKey) {
504
+ sessions.push({
505
+ ...credentialsSession,
506
+ oauthToken: oauth.token ?? credentialsSession.sessionKey,
507
+ });
508
+ }
509
+
510
+ return sessions;
511
+ }
512
+
513
+ export function buildClaudeHeaders(sessionKey, cfClearance, bearerToken, mode, cookies) {
514
+ const headers = {
515
+ accept: "application/json, text/plain, */*",
516
+ "accept-language": "en-US,en;q=0.9",
517
+ "cache-control": "no-cache",
518
+ pragma: "no-cache",
519
+ origin: CLAUDE_ORIGIN,
520
+ referer: `${CLAUDE_ORIGIN}/`,
521
+ "user-agent": CLAUDE_USER_AGENT,
522
+ "sec-fetch-dest": "empty",
523
+ "sec-fetch-mode": "cors",
524
+ "sec-fetch-site": "same-origin",
525
+ "x-requested-with": "XMLHttpRequest",
526
+ };
527
+ if (mode.includes("cookie")) {
528
+ if (!sessionKey && !(cookies && typeof cookies === "object")) {
529
+ return headers;
530
+ }
531
+ let parts = [];
532
+ if (cookies && typeof cookies === "object") {
533
+ parts = Object.entries(cookies)
534
+ .filter(([, value]) => typeof value === "string" && value.length)
535
+ .map(([name, value]) => `${name}=${value}`);
536
+ } else {
537
+ parts = [`sessionKey=${sessionKey}`];
538
+ if (cfClearance) {
539
+ parts.push(`cf_clearance=${cfClearance}`);
540
+ }
541
+ }
542
+ headers.Cookie = parts.join("; ");
543
+ }
544
+ if (mode.includes("bearer")) {
545
+ if (bearerToken) {
546
+ headers.Authorization = `Bearer ${bearerToken}`;
547
+ }
548
+ }
549
+ return headers;
550
+ }
551
+
552
+ export async function fetchClaudeJson(url, sessionKey, cfClearance, oauthToken, cookies) {
553
+ const controller = new AbortController();
554
+ const timeout = setTimeout(() => controller.abort(), CLAUDE_TIMEOUT_MS);
555
+
556
+ try {
557
+ const attempts = [];
558
+ const hasCookie = Boolean(sessionKey || (cookies && typeof cookies === "object"));
559
+ const hasSessionBearer = Boolean(sessionKey);
560
+ const hasOauthBearer = Boolean(oauthToken);
561
+
562
+ if (hasCookie) attempts.push({ mode: "cookie", bearer: null });
563
+ if (hasSessionBearer) attempts.push({ mode: "bearer", bearer: sessionKey });
564
+ if (hasOauthBearer) attempts.push({ mode: "bearer", bearer: oauthToken });
565
+ if (hasCookie && hasSessionBearer) attempts.push({ mode: "cookie+bearer", bearer: sessionKey });
566
+ if (hasCookie && hasOauthBearer) attempts.push({ mode: "cookie+bearer", bearer: oauthToken });
567
+ let lastError = null;
568
+
569
+ for (const attempt of attempts) {
570
+ const res = await fetch(url, {
571
+ method: "GET",
572
+ headers: buildClaudeHeaders(
573
+ sessionKey,
574
+ cfClearance,
575
+ attempt.bearer,
576
+ attempt.mode,
577
+ cookies
578
+ ),
579
+ signal: controller.signal,
580
+ });
581
+ if (res.ok) {
582
+ const text = await res.text();
583
+ if (!text) {
584
+ return { data: null };
585
+ }
586
+ try {
587
+ return { data: JSON.parse(text) };
588
+ } catch {
589
+ return { error: "Invalid JSON response" };
590
+ }
591
+ }
592
+
593
+ let detail = "";
594
+ try {
595
+ const text = await res.text();
596
+ if (text) {
597
+ detail = text.trim().slice(0, 200);
598
+ }
599
+ } catch {
600
+ // ignore body parse errors
601
+ }
602
+ const error = {
603
+ status: res.status,
604
+ error: detail ? `HTTP ${res.status}: ${detail}` : `HTTP ${res.status}`,
605
+ };
606
+ lastError = error;
607
+ if (res.status !== 401 && res.status !== 403) {
608
+ return error;
609
+ }
610
+ }
611
+
612
+ return lastError ?? { error: "HTTP 403" };
613
+ } catch (err) {
614
+ const message = err?.name === "AbortError" ? "Request timed out" : err?.message ?? String(err);
615
+ return { error: message };
616
+ } finally {
617
+ clearTimeout(timeout);
618
+ }
619
+ }
620
+
621
+ export function extractClaudeOrgId(payload) {
622
+ if (!payload) return null;
623
+ if (typeof payload === "string") return payload;
624
+
625
+ const isUuidLike = (value) => {
626
+ if (typeof value !== "string") return false;
627
+ if (/^[0-9a-f]{32}$/i.test(value)) return true;
628
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
629
+ return true;
630
+ }
631
+ return false;
632
+ };
633
+
634
+ const searchUuid = (root) => {
635
+ const stack = [root];
636
+ const seen = new Set();
637
+ while (stack.length) {
638
+ const current = stack.pop();
639
+ if (!current || typeof current !== "object") continue;
640
+ if (seen.has(current)) continue;
641
+ seen.add(current);
642
+ if (Array.isArray(current)) {
643
+ for (const item of current) {
644
+ if (isUuidLike(item)) return item;
645
+ if (item && typeof item === "object") stack.push(item);
646
+ }
647
+ } else {
648
+ for (const value of Object.values(current)) {
649
+ if (isUuidLike(value)) return value;
650
+ if (value && typeof value === "object") stack.push(value);
651
+ }
652
+ }
653
+ }
654
+ return null;
655
+ };
656
+
657
+ const uuidCandidate = searchUuid(payload);
658
+ if (uuidCandidate) return uuidCandidate;
659
+
660
+ const direct = payload.id ?? payload.uuid ?? payload.organizationId ?? payload.orgId ?? payload.org_id;
661
+ if (direct) return direct;
662
+ if (payload.current_organization_uuid) return payload.current_organization_uuid;
663
+
664
+ const orgs = Array.isArray(payload)
665
+ ? payload
666
+ : payload.organizations ?? payload.orgs ?? payload.items ?? payload.data;
667
+
668
+ if (!Array.isArray(orgs) || orgs.length === 0) return null;
669
+ const first = orgs[0];
670
+ if (typeof first === "string") return first;
671
+ return first?.id ?? first?.uuid ?? first?.organizationId ?? first?.orgId ?? first?.org_id ?? null;
672
+ }
673
+
674
+ export async function fetchClaudeUsageForCredentials(credentials) {
675
+ const sessionKey = credentials.sessionKey ?? findClaudeSessionKey(credentials.cookies);
676
+ const oauthToken = credentials.oauthToken ?? null;
677
+ const cfClearance = credentials.cfClearance ?? credentials.cookies?.cf_clearance ?? credentials.cookies?.cfClearance ?? null;
678
+ const cookies = credentials.cookies ?? null;
679
+
680
+ if (!sessionKey && !oauthToken && !cookies) {
681
+ return {
682
+ success: false,
683
+ label: credentials.label ?? null,
684
+ source: credentials.source ?? null,
685
+ error: "Missing Claude session key or OAuth token",
686
+ };
687
+ }
688
+
689
+ let lastAuthError = null;
690
+ const tryUsageForOrg = async (orgId) => {
691
+ const normalizedOrgId = normalizeClaudeOrgId(orgId);
692
+ const usageUrl = `${CLAUDE_API_BASE}/organizations/${normalizedOrgId}/usage`;
693
+ const overageUrl = `${CLAUDE_API_BASE}/organizations/${normalizedOrgId}/overage_spend_limit`;
694
+
695
+ const [usageResponse, overageResponse, accountResponse] = await Promise.all([
696
+ fetchClaudeJson(
697
+ usageUrl,
698
+ sessionKey,
699
+ cfClearance,
700
+ oauthToken,
701
+ cookies
702
+ ),
703
+ fetchClaudeJson(
704
+ overageUrl,
705
+ sessionKey,
706
+ cfClearance,
707
+ oauthToken,
708
+ cookies
709
+ ),
710
+ fetchClaudeJson(
711
+ CLAUDE_ACCOUNT_URL,
712
+ sessionKey,
713
+ cfClearance,
714
+ oauthToken,
715
+ cookies
716
+ ),
717
+ ]);
718
+
719
+ const errors = {};
720
+ if (usageResponse.error) errors.usage = usageResponse.error;
721
+ if (overageResponse.error) errors.overage = overageResponse.error;
722
+ if (accountResponse.error) errors.account = accountResponse.error;
723
+
724
+ return { usageResponse, overageResponse, accountResponse, errors, orgId };
725
+ };
726
+
727
+ const cookieOrg = credentials.cookies?.lastActiveOrg;
728
+ const configuredOrg = credentials.orgId ?? null;
729
+
730
+ if (configuredOrg || cookieOrg) {
731
+ const orgAttempt = await tryUsageForOrg(configuredOrg ?? cookieOrg);
732
+ const authErrors = Object.values(orgAttempt.errors).some(isClaudeAuthError);
733
+ if (!authErrors) {
734
+ return {
735
+ success: true,
736
+ label: credentials.label ?? null,
737
+ source: credentials.source ?? null,
738
+ orgId: orgAttempt.orgId,
739
+ usage: orgAttempt.usageResponse.data ?? null,
740
+ overage: orgAttempt.overageResponse.data ?? null,
741
+ account: orgAttempt.accountResponse.data ?? null,
742
+ errors: Object.keys(orgAttempt.errors).length ? orgAttempt.errors : null,
743
+ };
744
+ }
745
+ lastAuthError = orgAttempt.errors.usage || orgAttempt.errors.overage || lastAuthError;
746
+ }
747
+
748
+ const orgsResponse = await fetchClaudeJson(
749
+ CLAUDE_ORGS_URL,
750
+ sessionKey,
751
+ cfClearance,
752
+ oauthToken,
753
+ cookies
754
+ );
755
+ if (orgsResponse.error) {
756
+ const errorText = String(orgsResponse.error);
757
+ const isAuthError = /account_session_invalid|invalid authorization|http 401|http 403/i.test(errorText);
758
+ if (isAuthError) {
759
+ lastAuthError = orgsResponse.error;
760
+ return {
761
+ success: false,
762
+ label: credentials.label ?? null,
763
+ source: credentials.source ?? null,
764
+ error: `Organizations request failed: ${lastAuthError}`,
765
+ };
766
+ }
767
+ return {
768
+ success: false,
769
+ label: credentials.label ?? null,
770
+ source: credentials.source ?? null,
771
+ error: `Organizations request failed: ${orgsResponse.error}`,
772
+ };
773
+ }
774
+
775
+ const orgId = extractClaudeOrgId(orgsResponse.data);
776
+ if (!orgId) {
777
+ return {
778
+ success: false,
779
+ label: credentials.label ?? null,
780
+ source: credentials.source ?? null,
781
+ error: "No Claude organization ID found",
782
+ };
783
+ }
784
+
785
+ const orgAttempt = await tryUsageForOrg(orgId);
786
+ const authErrors = Object.values(orgAttempt.errors).some(isClaudeAuthError);
787
+ if (!authErrors) {
788
+ return {
789
+ success: true,
790
+ label: credentials.label ?? null,
791
+ source: credentials.source ?? null,
792
+ orgId,
793
+ usage: orgAttempt.usageResponse.data ?? null,
794
+ overage: orgAttempt.overageResponse.data ?? null,
795
+ account: orgAttempt.accountResponse.data ?? null,
796
+ errors: Object.keys(orgAttempt.errors).length ? orgAttempt.errors : null,
797
+ };
798
+ }
799
+
800
+ return {
801
+ success: false,
802
+ label: credentials.label ?? null,
803
+ source: credentials.source ?? null,
804
+ error: `Organizations request failed: ${lastAuthError || "Invalid authorization"}`,
805
+ };
806
+ }
807
+
808
+ export async function fetchClaudeUsage() {
809
+ const candidates = loadClaudeSessionCandidates();
810
+ if (!candidates.length) {
811
+ const credentials = loadClaudeSessionFromCredentials();
812
+ return {
813
+ success: false,
814
+ source: credentials.source,
815
+ error: credentials.error ?? "Missing Claude session key",
816
+ };
817
+ }
818
+
819
+ let lastAuthError = null;
820
+
821
+ for (const credentials of candidates) {
822
+ const tryUsageForOrg = async (orgId) => {
823
+ const normalizedOrgId = normalizeClaudeOrgId(orgId);
824
+ const usageUrl = `${CLAUDE_API_BASE}/organizations/${normalizedOrgId}/usage`;
825
+ const overageUrl = `${CLAUDE_API_BASE}/organizations/${normalizedOrgId}/overage_spend_limit`;
826
+
827
+ const [usageResponse, overageResponse, accountResponse] = await Promise.all([
828
+ fetchClaudeJson(
829
+ usageUrl,
830
+ credentials.sessionKey,
831
+ credentials.cfClearance,
832
+ credentials.oauthToken,
833
+ credentials.cookies
834
+ ),
835
+ fetchClaudeJson(
836
+ overageUrl,
837
+ credentials.sessionKey,
838
+ credentials.cfClearance,
839
+ credentials.oauthToken,
840
+ credentials.cookies
841
+ ),
842
+ fetchClaudeJson(
843
+ CLAUDE_ACCOUNT_URL,
844
+ credentials.sessionKey,
845
+ credentials.cfClearance,
846
+ credentials.oauthToken,
847
+ credentials.cookies
848
+ ),
849
+ ]);
850
+
851
+ const errors = {};
852
+ if (usageResponse.error) errors.usage = usageResponse.error;
853
+ if (overageResponse.error) errors.overage = overageResponse.error;
854
+ if (accountResponse.error) errors.account = accountResponse.error;
855
+
856
+ return { usageResponse, overageResponse, accountResponse, errors, orgId };
857
+ };
858
+
859
+ const cookieOrg = credentials.cookies?.lastActiveOrg;
860
+ if (cookieOrg) {
861
+ const cookieAttempt = await tryUsageForOrg(cookieOrg);
862
+ const authErrors = Object.values(cookieAttempt.errors).some(isClaudeAuthError);
863
+ if (!authErrors) {
864
+ return {
865
+ success: true,
866
+ source: credentials.source,
867
+ orgId: cookieAttempt.orgId,
868
+ usage: cookieAttempt.usageResponse.data ?? null,
869
+ overage: cookieAttempt.overageResponse.data ?? null,
870
+ account: cookieAttempt.accountResponse.data ?? null,
871
+ errors: Object.keys(cookieAttempt.errors).length ? cookieAttempt.errors : null,
872
+ };
873
+ }
874
+ lastAuthError = cookieAttempt.errors.usage || cookieAttempt.errors.overage || lastAuthError;
875
+ }
876
+
877
+ const orgsResponse = await fetchClaudeJson(
878
+ CLAUDE_ORGS_URL,
879
+ credentials.sessionKey,
880
+ credentials.cfClearance,
881
+ credentials.oauthToken,
882
+ credentials.cookies
883
+ );
884
+ if (orgsResponse.error) {
885
+ const errorText = String(orgsResponse.error);
886
+ const isAuthError = /account_session_invalid|invalid authorization|http 401|http 403/i.test(errorText);
887
+ if (isAuthError) {
888
+ lastAuthError = orgsResponse.error;
889
+ continue;
890
+ }
891
+ return {
892
+ success: false,
893
+ source: credentials.source,
894
+ error: `Organizations request failed: ${orgsResponse.error}`,
895
+ };
896
+ }
897
+
898
+ const orgId = extractClaudeOrgId(orgsResponse.data);
899
+ if (!orgId) {
900
+ return {
901
+ success: false,
902
+ source: credentials.source,
903
+ error: "No Claude organization ID found",
904
+ };
905
+ }
906
+
907
+ const orgAttempt = await tryUsageForOrg(orgId);
908
+ const authErrors = Object.values(orgAttempt.errors).some(isClaudeAuthError);
909
+ if (!authErrors) {
910
+ return {
911
+ success: true,
912
+ source: credentials.source,
913
+ orgId,
914
+ usage: orgAttempt.usageResponse.data ?? null,
915
+ overage: orgAttempt.overageResponse.data ?? null,
916
+ account: orgAttempt.accountResponse.data ?? null,
917
+ errors: Object.keys(orgAttempt.errors).length ? orgAttempt.errors : null,
918
+ };
919
+ }
920
+ lastAuthError = orgAttempt.errors.usage || orgAttempt.errors.overage || lastAuthError;
921
+ }
922
+
923
+ return {
924
+ success: false,
925
+ source: candidates[0]?.source ?? null,
926
+ error: `Organizations request failed: ${lastAuthError || "Invalid authorization"}`,
927
+ };
928
+ }
929
+