agent-control-plane 0.1.1 → 0.1.3

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 (184) hide show
  1. package/README.md +6 -0
  2. package/npm/bin/agent-control-plane.js +149 -5
  3. package/package.json +2 -3
  4. package/tools/vendor/codex-quota/README.md +8 -16
  5. package/tools/vendor/codex-quota/lib/claude-accounts.js +2 -68
  6. package/tools/vendor/codex-quota/lib/claude-usage.js +76 -667
  7. package/tools/vendor/codex-quota/lib/display.js +6 -14
  8. package/tools/vendor/codex-quota/lib/handlers.js +32 -117
  9. package/tools/vendor/codex-quota/lib/sync.js +9 -14
  10. package/tools/tests/test-agent-control-plane-npm-cli.sh +0 -280
  11. package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +0 -56
  12. package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -89
  13. package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +0 -82
  14. package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +0 -90
  15. package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +0 -73
  16. package/tools/tests/test-agent-project-claude-session-wrapper.sh +0 -103
  17. package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +0 -90
  18. package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +0 -90
  19. package/tools/tests/test-agent-project-codex-live-thread-persist.sh +0 -76
  20. package/tools/tests/test-agent-project-codex-recovery.sh +0 -731
  21. package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -105
  22. package/tools/tests/test-agent-project-codex-session-wrapper.sh +0 -97
  23. package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +0 -81
  24. package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -109
  25. package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +0 -89
  26. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +0 -113
  27. package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +0 -135
  28. package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +0 -179
  29. package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +0 -119
  30. package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +0 -91
  31. package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +0 -117
  32. package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +0 -148
  33. package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +0 -146
  34. package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +0 -145
  35. package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +0 -147
  36. package/tools/tests/test-agent-project-reconcile-failure-reason.sh +0 -456
  37. package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +0 -96
  38. package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +0 -90
  39. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +0 -212
  40. package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +0 -207
  41. package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +0 -101
  42. package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +0 -113
  43. package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +0 -117
  44. package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +0 -55
  45. package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +0 -125
  46. package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +0 -118
  47. package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +0 -144
  48. package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +0 -145
  49. package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +0 -139
  50. package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +0 -242
  51. package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +0 -142
  52. package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +0 -106
  53. package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +0 -66
  54. package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +0 -129
  55. package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +0 -69
  56. package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +0 -43
  57. package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +0 -46
  58. package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +0 -79
  59. package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +0 -56
  60. package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +0 -72
  61. package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +0 -125
  62. package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +0 -178
  63. package/tools/tests/test-codex-quota-wrapper.sh +0 -37
  64. package/tools/tests/test-contribution-docs.sh +0 -18
  65. package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +0 -343
  66. package/tools/tests/test-create-follow-up-issue.sh +0 -73
  67. package/tools/tests/test-dashboard-launchd-bootstrap.sh +0 -55
  68. package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +0 -30
  69. package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +0 -48
  70. package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +0 -85
  71. package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +0 -60
  72. package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +0 -64
  73. package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +0 -77
  74. package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +0 -52
  75. package/tools/tests/test-flow-resident-reap-stale-controllers.sh +0 -63
  76. package/tools/tests/test-flow-resolve-codex-quota-tools.sh +0 -104
  77. package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +0 -27
  78. package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +0 -79
  79. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +0 -115
  80. package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +0 -117
  81. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +0 -96
  82. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +0 -96
  83. package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +0 -96
  84. package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +0 -170
  85. package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +0 -201
  86. package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +0 -201
  87. package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +0 -160
  88. package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +0 -133
  89. package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +0 -157
  90. package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +0 -181
  91. package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +0 -160
  92. package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +0 -134
  93. package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +0 -162
  94. package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +0 -136
  95. package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +0 -139
  96. package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +0 -119
  97. package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +0 -140
  98. package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +0 -142
  99. package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +0 -141
  100. package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +0 -65
  101. package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +0 -179
  102. package/tools/tests/test-install-dashboard-launchd.sh +0 -78
  103. package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +0 -87
  104. package/tools/tests/test-install-project-launchd.sh +0 -110
  105. package/tools/tests/test-issue-local-workspace-install-policy.sh +0 -81
  106. package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +0 -70
  107. package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +0 -36
  108. package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +0 -47
  109. package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +0 -132
  110. package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +0 -64
  111. package/tools/tests/test-package-funding-metadata.sh +0 -21
  112. package/tools/tests/test-package-public-metadata.sh +0 -62
  113. package/tools/tests/test-placeholder-worker-adapters.sh +0 -38
  114. package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +0 -110
  115. package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +0 -70
  116. package/tools/tests/test-pr-risk-fix-label-semantics.sh +0 -114
  117. package/tools/tests/test-pr-risk-local-first-no-checks.sh +0 -70
  118. package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +0 -67
  119. package/tools/tests/test-profile-activate.sh +0 -33
  120. package/tools/tests/test-profile-adopt-allow-missing-repo.sh +0 -68
  121. package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +0 -61
  122. package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +0 -90
  123. package/tools/tests/test-profile-smoke-collision.sh +0 -44
  124. package/tools/tests/test-profile-smoke-invalid-claude-config.sh +0 -31
  125. package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +0 -68
  126. package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +0 -36
  127. package/tools/tests/test-profile-smoke.sh +0 -45
  128. package/tools/tests/test-project-init-force-and-skip-sync.sh +0 -61
  129. package/tools/tests/test-project-init-repo-slug-mismatch.sh +0 -29
  130. package/tools/tests/test-project-init.sh +0 -66
  131. package/tools/tests/test-project-launchd-bootstrap.sh +0 -66
  132. package/tools/tests/test-project-remove.sh +0 -150
  133. package/tools/tests/test-project-runtime-supervisor.sh +0 -47
  134. package/tools/tests/test-project-runtimectl-launchd.sh +0 -115
  135. package/tools/tests/test-project-runtimectl-missing-profile.sh +0 -54
  136. package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +0 -108
  137. package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +0 -95
  138. package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +0 -59
  139. package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +0 -85
  140. package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +0 -78
  141. package/tools/tests/test-project-runtimectl.sh +0 -212
  142. package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +0 -39
  143. package/tools/tests/test-provider-cooldown-state.sh +0 -59
  144. package/tools/tests/test-public-repo-docs.sh +0 -160
  145. package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +0 -75
  146. package/tools/tests/test-render-dashboard-snapshot.sh +0 -149
  147. package/tools/tests/test-render-flow-config-demo-profile.sh +0 -36
  148. package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +0 -81
  149. package/tools/tests/test-render-flow-config.sh +0 -52
  150. package/tools/tests/test-run-codex-task-claude-routing.sh +0 -125
  151. package/tools/tests/test-run-codex-task-codex-resident-routing.sh +0 -108
  152. package/tools/tests/test-run-codex-task-kilo-routing.sh +0 -98
  153. package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +0 -117
  154. package/tools/tests/test-run-codex-task-openclaw-routing.sh +0 -113
  155. package/tools/tests/test-run-codex-task-opencode-routing.sh +0 -98
  156. package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +0 -146
  157. package/tools/tests/test-scaffold-profile.sh +0 -108
  158. package/tools/tests/test-serve-dashboard.sh +0 -93
  159. package/tools/tests/test-start-issue-worker-blocked-context.sh +0 -129
  160. package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +0 -189
  161. package/tools/tests/test-start-issue-worker-local-install-routing.sh +0 -157
  162. package/tools/tests/test-start-issue-worker-profile-template-routing.sh +0 -149
  163. package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +0 -212
  164. package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +0 -219
  165. package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +0 -155
  166. package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +0 -199
  167. package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +0 -275
  168. package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +0 -185
  169. package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +0 -152
  170. package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +0 -186
  171. package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +0 -212
  172. package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +0 -148
  173. package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +0 -194
  174. package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +0 -198
  175. package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +0 -145
  176. package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +0 -67
  177. package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +0 -70
  178. package/tools/tests/test-sync-recurring-issue-checklist.sh +0 -95
  179. package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +0 -66
  180. package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +0 -47
  181. package/tools/tests/test-test-smoke.sh +0 -86
  182. package/tools/tests/test-uninstall-project-launchd.sh +0 -37
  183. package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +0 -49
  184. package/tools/tests/test-workflow-catalog.sh +0 -43
@@ -1,31 +1,19 @@
1
1
  /**
2
- * Claude usage API fetch (session + OAuth).
2
+ * Claude usage API fetch (OAuth only).
3
3
  * Depends on: lib/constants.js, lib/paths.js, lib/claude-accounts.js, lib/claude-tokens.js
4
4
  */
5
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";
6
+ import { existsSync, readFileSync } from "node:fs";
11
7
  import {
12
- CLAUDE_CREDENTIALS_PATH,
13
8
  CLAUDE_MULTI_ACCOUNT_PATHS,
14
- CLAUDE_API_BASE,
15
- CLAUDE_ORIGIN,
16
- CLAUDE_ORGS_URL,
17
- CLAUDE_ACCOUNT_URL,
18
9
  CLAUDE_TIMEOUT_MS,
19
- CLAUDE_USER_AGENT,
20
10
  CLAUDE_OAUTH_USAGE_URL,
21
11
  CLAUDE_OAUTH_VERSION,
22
12
  CLAUDE_OAUTH_BETA,
23
13
  } from "./constants.js";
24
14
  import { getOpencodeAuthPath } from "./paths.js";
25
15
  import {
26
- findClaudeSessionKey,
27
16
  loadClaudeAccountsFromFile,
28
- loadClaudeSessionFromCredentials,
29
17
  loadClaudeOAuthToken,
30
18
  } from "./claude-accounts.js";
31
19
  import { ensureFreshClaudeOAuthToken } from "./claude-tokens.js";
@@ -40,39 +28,46 @@ export function normalizeClaudeOrgId(orgId) {
40
28
 
41
29
  export function isClaudeAuthError(error) {
42
30
  if (!error) return false;
43
- return /account_session_invalid|invalid authorization|http 401|http 403/i.test(String(error));
31
+ return /invalid authorization|http 401|http 403/i.test(String(error));
44
32
  }
45
33
 
46
34
  export function loadClaudeOAuthFromClaudeCode() {
47
- const credentialsPath = process.env.CLAUDE_CREDENTIALS_PATH || CLAUDE_CREDENTIALS_PATH;
48
- if (!existsSync(credentialsPath)) return [];
35
+ const credentials = loadClaudeOAuthToken();
36
+ if (!credentials.token) return [];
37
+
38
+ let scopes = [];
39
+ let refreshToken = null;
40
+ let expiresAt = null;
41
+ let subscriptionType = null;
42
+ let rateLimitTier = null;
49
43
 
50
44
  try {
51
- const raw = readFileSync(credentialsPath, "utf-8");
45
+ const raw = readFileSync(credentials.source, "utf-8");
52
46
  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
- }];
47
+ const oauth = parsed?.claudeAiOauth ?? parsed?.claude_ai_oauth ?? null;
48
+ scopes = Array.isArray(oauth?.scopes) ? oauth.scopes : [];
49
+ refreshToken = oauth?.refreshToken ?? null;
50
+ expiresAt = oauth?.expiresAt ?? null;
51
+ subscriptionType = oauth?.subscriptionType ?? null;
52
+ rateLimitTier = oauth?.rateLimitTier ?? null;
73
53
  } catch {
54
+ // Ignore parse failures and return the token-only shape.
55
+ }
56
+
57
+ if (!scopes.includes("user:profile")) {
74
58
  return [];
75
59
  }
60
+
61
+ return [{
62
+ label: "claude-code",
63
+ accessToken: credentials.token,
64
+ refreshToken,
65
+ expiresAt,
66
+ subscriptionType,
67
+ rateLimitTier,
68
+ scopes,
69
+ source: credentials.source,
70
+ }];
76
71
  }
77
72
 
78
73
  /**
@@ -124,21 +119,13 @@ export function loadClaudeOAuthFromEnv() {
124
119
 
125
120
  /**
126
121
  * 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
122
+ * This handles the case where the same Claude account is sourced from multiple files.
135
123
  */
136
124
  export function deduplicateClaudeOAuthAccounts(accounts) {
137
125
  const seenTokens = new Set();
138
126
  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
127
+ if (!account.accessToken) return true;
128
+ const tokenKey = account.refreshToken
142
129
  ? account.refreshToken.substring(0, 50)
143
130
  : account.accessToken.substring(0, 50);
144
131
  if (seenTokens.has(tokenKey)) return false;
@@ -148,32 +135,20 @@ export function deduplicateClaudeOAuthAccounts(accounts) {
148
135
  }
149
136
 
150
137
  /**
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
138
+ * Deduplicate Claude usage results by comparing usage fingerprints.
160
139
  */
161
140
  export function deduplicateClaudeResultsByUsage(results) {
162
141
  const seen = new Set();
163
142
  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)
143
+ if (!result.success || !result.usage) return true;
144
+
167
145
  const usage = result.usage;
168
146
  const fiveHour = usage.five_hour?.utilization ?? "null";
169
147
  const sevenDay = usage.seven_day?.utilization ?? "null";
170
148
  const sevenDayOpus = usage.seven_day_opus?.utilization ?? "null";
171
149
  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
150
  const fingerprint = `${fiveHour}|${sevenDay}|${sevenDayOpus}|${sevenDaySonnet}`;
176
-
151
+
177
152
  if (seen.has(fingerprint)) return false;
178
153
  seen.add(fingerprint);
179
154
  return true;
@@ -181,21 +156,12 @@ export function deduplicateClaudeResultsByUsage(results) {
181
156
  }
182
157
 
183
158
  /**
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 }>}
159
+ * Load all Claude OAuth accounts from all supported sources.
193
160
  */
194
161
  export function loadAllClaudeOAuthAccounts(options = {}) {
195
162
  const all = [];
196
163
  const seenLabels = new Set();
197
164
 
198
- // 1. Environment variable
199
165
  for (const account of loadClaudeOAuthFromEnv()) {
200
166
  if (!seenLabels.has(account.label)) {
201
167
  seenLabels.add(account.label);
@@ -203,7 +169,6 @@ export function loadAllClaudeOAuthAccounts(options = {}) {
203
169
  }
204
170
  }
205
171
 
206
- // 2. Multi-account file (accounts with oauthToken)
207
172
  for (const path of CLAUDE_MULTI_ACCOUNT_PATHS) {
208
173
  const accounts = loadClaudeAccountsFromFile(path);
209
174
  for (const account of accounts) {
@@ -212,7 +177,6 @@ export function loadAllClaudeOAuthAccounts(options = {}) {
212
177
  all.push({
213
178
  label: account.label,
214
179
  accessToken: account.oauthToken,
215
- // Pass through new OAuth metadata fields (optional, may be null for legacy accounts)
216
180
  refreshToken: account.oauthRefreshToken || null,
217
181
  expiresAt: account.oauthExpiresAt || null,
218
182
  scopes: account.oauthScopes || null,
@@ -222,7 +186,6 @@ export function loadAllClaudeOAuthAccounts(options = {}) {
222
186
  }
223
187
  }
224
188
 
225
- // 3. Claude Code credentials (skip in local mode)
226
189
  if (!options.local) {
227
190
  for (const account of loadClaudeOAuthFromClaudeCode()) {
228
191
  if (!seenLabels.has(account.label)) {
@@ -230,10 +193,6 @@ export function loadAllClaudeOAuthAccounts(options = {}) {
230
193
  all.push(account);
231
194
  }
232
195
  }
233
- }
234
-
235
- // 4. OpenCode credentials (skip in local mode)
236
- if (!options.local) {
237
196
  for (const account of loadClaudeOAuthFromOpenCode()) {
238
197
  if (!seenLabels.has(account.label)) {
239
198
  seenLabels.add(account.label);
@@ -242,19 +201,11 @@ export function loadAllClaudeOAuthAccounts(options = {}) {
242
201
  }
243
202
  }
244
203
 
245
- // 5. Deduplicate by accessToken (same account from multiple sources with different labels)
246
204
  return deduplicateClaudeOAuthAccounts(all);
247
205
  }
248
206
 
249
207
  /**
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 }>}
208
+ * Fetch Claude usage via OAuth API.
258
209
  */
259
210
  export async function fetchClaudeOAuthUsage(accessToken) {
260
211
  const controller = new AbortController();
@@ -290,9 +241,7 @@ export async function fetchClaudeOAuthUsage(accessToken) {
290
241
  }
291
242
 
292
243
  /**
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, ... }>}
244
+ * Fetch usage for a Claude OAuth account.
296
245
  */
297
246
  export async function fetchClaudeOAuthUsageForAccount(account) {
298
247
  const refreshed = await ensureFreshClaudeOAuthToken(account);
@@ -311,7 +260,6 @@ export async function fetchClaudeOAuthUsageForAccount(account) {
311
260
  }
312
261
 
313
262
  const result = await fetchClaudeOAuthUsage(account.accessToken);
314
-
315
263
  if (!result.success) {
316
264
  return {
317
265
  success: false,
@@ -333,597 +281,58 @@ export async function fetchClaudeOAuthUsageForAccount(account) {
333
281
  };
334
282
  }
335
283
 
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
-
284
+ /**
285
+ * Backward-compatible wrapper for stored Claude credentials.
286
+ * Only OAuth-backed credentials are supported.
287
+ */
674
288
  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
- }
289
+ if (!credentials?.oauthToken) {
767
290
  return {
768
291
  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,
292
+ label: credentials?.label ?? null,
293
+ source: credentials?.source ?? null,
294
+ error: "Claude OAuth token required",
797
295
  };
798
296
  }
799
297
 
800
- return {
801
- success: false,
298
+ return fetchClaudeOAuthUsageForAccount({
802
299
  label: credentials.label ?? null,
803
300
  source: credentials.source ?? null,
804
- error: `Organizations request failed: ${lastAuthError || "Invalid authorization"}`,
805
- };
301
+ accessToken: credentials.oauthToken,
302
+ refreshToken: credentials.oauthRefreshToken ?? null,
303
+ expiresAt: credentials.oauthExpiresAt ?? null,
304
+ scopes: credentials.oauthScopes ?? null,
305
+ subscriptionType: credentials.subscriptionType,
306
+ rateLimitTier: credentials.rateLimitTier,
307
+ });
806
308
  }
807
309
 
310
+ /**
311
+ * Backward-compatible wrapper for Claude Code credentials.
312
+ * Only OAuth-backed credentials are supported.
313
+ */
808
314
  export async function fetchClaudeUsage() {
809
- const candidates = loadClaudeSessionCandidates();
810
- if (!candidates.length) {
811
- const credentials = loadClaudeSessionFromCredentials();
315
+ const oauth = loadClaudeOAuthToken();
316
+ if (!oauth.token) {
812
317
  return {
813
318
  success: false,
814
- source: credentials.source,
815
- error: credentials.error ?? "Missing Claude session key",
319
+ source: oauth.source,
320
+ error: oauth.error ?? "Claude OAuth token required",
816
321
  };
817
322
  }
818
323
 
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 };
324
+ const result = await fetchClaudeOAuthUsage(oauth.token);
325
+ if (!result.success) {
326
+ return {
327
+ success: false,
328
+ source: oauth.source,
329
+ error: result.error,
857
330
  };
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
331
  }
922
332
 
923
333
  return {
924
- success: false,
925
- source: candidates[0]?.source ?? null,
926
- error: `Organizations request failed: ${lastAuthError || "Invalid authorization"}`,
334
+ success: true,
335
+ source: oauth.source,
336
+ usage: result.data,
927
337
  };
928
338
  }
929
-