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.
- package/README.md +6 -0
- package/npm/bin/agent-control-plane.js +149 -5
- package/package.json +2 -3
- package/tools/vendor/codex-quota/README.md +8 -16
- package/tools/vendor/codex-quota/lib/claude-accounts.js +2 -68
- package/tools/vendor/codex-quota/lib/claude-usage.js +76 -667
- package/tools/vendor/codex-quota/lib/display.js +6 -14
- package/tools/vendor/codex-quota/lib/handlers.js +32 -117
- package/tools/vendor/codex-quota/lib/sync.js +9 -14
- package/tools/tests/test-agent-control-plane-npm-cli.sh +0 -280
- package/tools/tests/test-agent-github-update-labels-falls-back-to-repository-id.sh +0 -56
- package/tools/tests/test-agent-project-claude-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -89
- package/tools/tests/test-agent-project-claude-session-wrapper-does-not-retry-provider-quota.sh +0 -82
- package/tools/tests/test-agent-project-claude-session-wrapper-retries-transient-failures.sh +0 -90
- package/tools/tests/test-agent-project-claude-session-wrapper-times-out.sh +0 -73
- package/tools/tests/test-agent-project-claude-session-wrapper.sh +0 -103
- package/tools/tests/test-agent-project-cleanup-session-orphan-fallback.sh +0 -90
- package/tools/tests/test-agent-project-cleanup-session-skip-worktree-cleanup.sh +0 -90
- package/tools/tests/test-agent-project-codex-live-thread-persist.sh +0 -76
- package/tools/tests/test-agent-project-codex-recovery.sh +0 -731
- package/tools/tests/test-agent-project-codex-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -105
- package/tools/tests/test-agent-project-codex-session-wrapper.sh +0 -97
- package/tools/tests/test-agent-project-open-pr-worktree-config-prefix.sh +0 -81
- package/tools/tests/test-agent-project-openclaw-session-wrapper-clears-stale-sandbox-artifacts.sh +0 -109
- package/tools/tests/test-agent-project-openclaw-session-wrapper-infers-blocked-result-contract.sh +0 -89
- package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-literal-env-artifacts.sh +0 -113
- package/tools/tests/test-agent-project-openclaw-session-wrapper-recovers-version-mismatch.sh +0 -135
- package/tools/tests/test-agent-project-openclaw-session-wrapper-resident.sh +0 -179
- package/tools/tests/test-agent-project-openclaw-session-wrapper-reuses-existing-agent-after-add-race.sh +0 -119
- package/tools/tests/test-agent-project-openclaw-session-wrapper-terminates-rate-limit-hang.sh +0 -91
- package/tools/tests/test-agent-project-openclaw-session-wrapper.sh +0 -117
- package/tools/tests/test-agent-project-publish-issue-pr-prunes-stale-worktree-entry.sh +0 -148
- package/tools/tests/test-agent-project-publish-issue-pr-reads-archived-session.sh +0 -146
- package/tools/tests/test-agent-project-publish-issue-pr-recovers-final-head.sh +0 -145
- package/tools/tests/test-agent-project-publish-issue-pr-reuses-existing-worktree.sh +0 -147
- package/tools/tests/test-agent-project-reconcile-failure-reason.sh +0 -456
- package/tools/tests/test-agent-project-reconcile-issue-archived-session-fallback.sh +0 -96
- package/tools/tests/test-agent-project-reconcile-issue-before-blocked.sh +0 -90
- package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery-uses-recovered-worktree.sh +0 -212
- package/tools/tests/test-agent-project-reconcile-issue-host-verification-recovery.sh +0 -207
- package/tools/tests/test-agent-project-reconcile-issue-provider-quota-schedules-provider-cooldown.sh +0 -101
- package/tools/tests/test-agent-project-reconcile-issue-session-backfills-lane-metadata-from-worker-key.sh +0 -113
- package/tools/tests/test-agent-project-reconcile-issue-session-clears-stale-failed-summary.sh +0 -117
- package/tools/tests/test-agent-project-reconcile-issue-session-initializes-shared-agent-home.sh +0 -55
- package/tools/tests/test-agent-project-reconcile-issue-session-normalizes-runner-state.sh +0 -125
- package/tools/tests/test-agent-project-reconcile-issue-session-records-invalid-contract-summary.sh +0 -118
- package/tools/tests/test-agent-project-reconcile-issue-session-skips-duplicate-blocked-comment.sh +0 -144
- package/tools/tests/test-agent-project-reconcile-issue-session-standardizes-no-commits-blocker.sh +0 -145
- package/tools/tests/test-agent-project-reconcile-issue-session-synthesizes-blocked-comment.sh +0 -139
- package/tools/tests/test-agent-project-reconcile-pr-blocked-host-recovery.sh +0 -242
- package/tools/tests/test-agent-project-reconcile-pr-guard-blocked-no-commit.sh +0 -142
- package/tools/tests/test-agent-project-reconcile-pr-provider-quota-schedules-provider-cooldown.sh +0 -106
- package/tools/tests/test-agent-project-reconcile-pr-session-initializes-shared-agent-home.sh +0 -66
- package/tools/tests/test-agent-project-reconcile-pr-updated-branch-noop.sh +0 -129
- package/tools/tests/test-audit-agent-worktrees-active-launch-skips-git-inspection.sh +0 -69
- package/tools/tests/test-audit-agent-worktrees-broken-worktree.sh +0 -43
- package/tools/tests/test-audit-agent-worktrees-pending-launch-owner.sh +0 -46
- package/tools/tests/test-audit-agent-worktrees-unreconciled-owner.sh +0 -79
- package/tools/tests/test-audit-issue-routing-managed-branch-globs.sh +0 -56
- package/tools/tests/test-branch-verification-guard-generated-artifacts.sh +0 -72
- package/tools/tests/test-branch-verification-guard-targeted-coverage.sh +0 -125
- package/tools/tests/test-codex-quota-manager-failure-driven-rotation.sh +0 -178
- package/tools/tests/test-codex-quota-wrapper.sh +0 -37
- package/tools/tests/test-contribution-docs.sh +0 -18
- package/tools/tests/test-control-plane-dashboard-runtime-smoke.sh +0 -343
- package/tools/tests/test-create-follow-up-issue.sh +0 -73
- package/tools/tests/test-dashboard-launchd-bootstrap.sh +0 -55
- package/tools/tests/test-flow-export-execution-env-exports-repo-id.sh +0 -30
- package/tools/tests/test-flow-export-github-cli-auth-env-prefers-git-credential.sh +0 -48
- package/tools/tests/test-flow-github-api-repo-fallback-preserves-input.sh +0 -85
- package/tools/tests/test-flow-github-api-repo-prefers-explicit-repository-id.sh +0 -60
- package/tools/tests/test-flow-github-issue-list-falls-back-to-repository-id.sh +0 -64
- package/tools/tests/test-flow-github-pr-list-falls-back-to-repository-id.sh +0 -77
- package/tools/tests/test-flow-resident-can-reuse-does-not-leak-metadata.sh +0 -52
- package/tools/tests/test-flow-resident-reap-stale-controllers.sh +0 -63
- package/tools/tests/test-flow-resolve-codex-quota-tools.sh +0 -104
- package/tools/tests/test-flow-runtime-doctor-profile-selection.sh +0 -27
- package/tools/tests/test-heartbeat-codex-pr-linked-issue-exclusion.sh +0 -79
- package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-idle-controller.sh +0 -115
- package/tools/tests/test-heartbeat-hooks-enqueue-resident-issue-for-live-lane-controller.sh +0 -117
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-claude.sh +0 -96
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop-codex.sh +0 -96
- package/tools/tests/test-heartbeat-hooks-start-resident-issue-loop.sh +0 -96
- package/tools/tests/test-heartbeat-loop-auth-wait-does-not-consume-capacity.sh +0 -170
- package/tools/tests/test-heartbeat-loop-blocked-recovery-lane.sh +0 -201
- package/tools/tests/test-heartbeat-loop-blocked-recovery-vs-pr-reservation.sh +0 -201
- package/tools/tests/test-heartbeat-loop-idle-resident-controller-does-not-block-launches.sh +0 -160
- package/tools/tests/test-heartbeat-loop-pr-launch-dedup.sh +0 -133
- package/tools/tests/test-heartbeat-loop-provider-cooldown-suppresses-launches.sh +0 -157
- package/tools/tests/test-heartbeat-loop-reaps-stale-resident-controller.sh +0 -181
- package/tools/tests/test-heartbeat-loop-waiting-provider-resident-controller-does-not-block-launches.sh +0 -160
- package/tools/tests/test-heartbeat-ready-issues-blocked-recovery.sh +0 -134
- package/tools/tests/test-heartbeat-safe-auto-dynamic-concurrency.sh +0 -162
- package/tools/tests/test-heartbeat-safe-auto-no-tmux-sessions.sh +0 -136
- package/tools/tests/test-heartbeat-safe-auto-openclaw-skips-codex-quota.sh +0 -139
- package/tools/tests/test-heartbeat-safe-auto-quota-health-signal.sh +0 -119
- package/tools/tests/test-heartbeat-safe-auto-stale-shared-loop-pid-does-not-skip.sh +0 -140
- package/tools/tests/test-heartbeat-safe-auto-static-capacity-without-quota-cache.sh +0 -142
- package/tools/tests/test-heartbeat-safe-auto-zero-healthy-pools.sh +0 -141
- package/tools/tests/test-heartbeat-sync-issue-labels-empty-schedule.sh +0 -65
- package/tools/tests/test-heartbeat-sync-open-agent-prs-terminal-clears-running.sh +0 -179
- package/tools/tests/test-install-dashboard-launchd.sh +0 -78
- package/tools/tests/test-install-project-launchd-adds-tool-paths.sh +0 -87
- package/tools/tests/test-install-project-launchd.sh +0 -110
- package/tools/tests/test-issue-local-workspace-install-policy.sh +0 -81
- package/tools/tests/test-issue-publish-scope-guard-docs-signal.sh +0 -70
- package/tools/tests/test-issue-reconcile-hooks-success-clears-blocked.sh +0 -36
- package/tools/tests/test-kick-scheduler-requires-explicit-profile.sh +0 -47
- package/tools/tests/test-label-follow-up-issues-falls-back-to-repository-id.sh +0 -132
- package/tools/tests/test-manual-operator-entrypoints-require-explicit-profile.sh +0 -64
- package/tools/tests/test-package-funding-metadata.sh +0 -21
- package/tools/tests/test-package-public-metadata.sh +0 -62
- package/tools/tests/test-placeholder-worker-adapters.sh +0 -38
- package/tools/tests/test-pr-reconcile-hooks-refreshes-recurring-issue-checklist.sh +0 -110
- package/tools/tests/test-pr-risk-cohesive-mobile-locale-scope.sh +0 -70
- package/tools/tests/test-pr-risk-fix-label-semantics.sh +0 -114
- package/tools/tests/test-pr-risk-local-first-no-checks.sh +0 -70
- package/tools/tests/test-prepare-worktree-simple-repo-baseline.sh +0 -67
- package/tools/tests/test-profile-activate.sh +0 -33
- package/tools/tests/test-profile-adopt-allow-missing-repo.sh +0 -68
- package/tools/tests/test-profile-adopt-skip-workspace-sync-missing-file.sh +0 -61
- package/tools/tests/test-profile-adopt-syncs-anchor-and-workspace.sh +0 -90
- package/tools/tests/test-profile-smoke-collision.sh +0 -44
- package/tools/tests/test-profile-smoke-invalid-claude-config.sh +0 -31
- package/tools/tests/test-profile-smoke-invalid-provider-pool.sh +0 -68
- package/tools/tests/test-profile-smoke-repo-slug-mismatch.sh +0 -36
- package/tools/tests/test-profile-smoke.sh +0 -45
- package/tools/tests/test-project-init-force-and-skip-sync.sh +0 -61
- package/tools/tests/test-project-init-repo-slug-mismatch.sh +0 -29
- package/tools/tests/test-project-init.sh +0 -66
- package/tools/tests/test-project-launchd-bootstrap.sh +0 -66
- package/tools/tests/test-project-remove.sh +0 -150
- package/tools/tests/test-project-runtime-supervisor.sh +0 -47
- package/tools/tests/test-project-runtimectl-launchd.sh +0 -115
- package/tools/tests/test-project-runtimectl-missing-profile.sh +0 -54
- package/tools/tests/test-project-runtimectl-start-falls-back-to-bootstrap.sh +0 -108
- package/tools/tests/test-project-runtimectl-status-reports-supervisor-as-heartbeat-parent.sh +0 -95
- package/tools/tests/test-project-runtimectl-status-supervisor-running.sh +0 -59
- package/tools/tests/test-project-runtimectl-stop-cancels-pending-kick.sh +0 -85
- package/tools/tests/test-project-runtimectl-stop-clears-running-labels.sh +0 -78
- package/tools/tests/test-project-runtimectl.sh +0 -212
- package/tools/tests/test-provider-cooldown-state-prefers-runtime-worker-context.sh +0 -39
- package/tools/tests/test-provider-cooldown-state.sh +0 -59
- package/tools/tests/test-public-repo-docs.sh +0 -160
- package/tools/tests/test-reconcile-pr-worker-acp-config-routing.sh +0 -75
- package/tools/tests/test-render-dashboard-snapshot.sh +0 -149
- package/tools/tests/test-render-flow-config-demo-profile.sh +0 -36
- package/tools/tests/test-render-flow-config-provider-pool-fallback.sh +0 -81
- package/tools/tests/test-render-flow-config.sh +0 -52
- package/tools/tests/test-run-codex-task-claude-routing.sh +0 -125
- package/tools/tests/test-run-codex-task-codex-resident-routing.sh +0 -108
- package/tools/tests/test-run-codex-task-kilo-routing.sh +0 -98
- package/tools/tests/test-run-codex-task-openclaw-resident-routing.sh +0 -117
- package/tools/tests/test-run-codex-task-openclaw-routing.sh +0 -113
- package/tools/tests/test-run-codex-task-opencode-routing.sh +0 -98
- package/tools/tests/test-run-codex-task-provider-pool-fallback-routing.sh +0 -146
- package/tools/tests/test-scaffold-profile.sh +0 -108
- package/tools/tests/test-serve-dashboard.sh +0 -93
- package/tools/tests/test-start-issue-worker-blocked-context.sh +0 -129
- package/tools/tests/test-start-issue-worker-blocks-complete-recurring-checklist.sh +0 -189
- package/tools/tests/test-start-issue-worker-local-install-routing.sh +0 -157
- package/tools/tests/test-start-issue-worker-profile-template-routing.sh +0 -149
- package/tools/tests/test-start-issue-worker-recurring-resident-reuse-codex.sh +0 -212
- package/tools/tests/test-start-issue-worker-recurring-resident-reuse.sh +0 -219
- package/tools/tests/test-start-issue-worker-renders-verification-snippet.sh +0 -155
- package/tools/tests/test-start-issue-worker-resident-reuse-falls-back-to-new-worktree.sh +0 -199
- package/tools/tests/test-start-pr-fix-worker-host-blocker-context.sh +0 -275
- package/tools/tests/test-start-resident-issue-loop-adopts-next-recurring-issue.sh +0 -185
- package/tools/tests/test-start-resident-issue-loop-clears-pending-while-waiting-due.sh +0 -152
- package/tools/tests/test-start-resident-issue-loop-consumes-queued-lease.sh +0 -186
- package/tools/tests/test-start-resident-issue-loop-fails-over-provider-pool.sh +0 -212
- package/tools/tests/test-start-resident-issue-loop-immediate-cycles.sh +0 -148
- package/tools/tests/test-start-resident-issue-loop-waits-for-provider.sh +0 -194
- package/tools/tests/test-start-resident-issue-loop-waits-for-terminal-reconcile-status.sh +0 -198
- package/tools/tests/test-start-resident-issue-loop-yields-to-live-lane-controller.sh +0 -145
- package/tools/tests/test-sync-pr-labels-fix-lane-uses-repair-queued.sh +0 -67
- package/tools/tests/test-sync-recurring-issue-checklist-backfills-workflow-complete-blocker.sh +0 -70
- package/tools/tests/test-sync-recurring-issue-checklist.sh +0 -95
- package/tools/tests/test-sync-shared-agent-home-local-source-root.sh +0 -66
- package/tools/tests/test-sync-shared-agent-home-preserves-unrelated-workflow-catalog-skill.sh +0 -47
- package/tools/tests/test-test-smoke.sh +0 -86
- package/tools/tests/test-uninstall-project-launchd.sh +0 -37
- package/tools/tests/test-update-github-labels-prefers-sibling-helper.sh +0 -49
- package/tools/tests/test-workflow-catalog.sh +0 -43
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude usage API fetch (
|
|
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
|
|
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 /
|
|
31
|
+
return /invalid authorization|http 401|http 403/i.test(String(error));
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
export function loadClaudeOAuthFromClaudeCode() {
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
140
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
|
770
|
-
source: credentials
|
|
771
|
-
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
|
-
|
|
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
|
|
810
|
-
if (!
|
|
811
|
-
const credentials = loadClaudeSessionFromCredentials();
|
|
315
|
+
const oauth = loadClaudeOAuthToken();
|
|
316
|
+
if (!oauth.token) {
|
|
812
317
|
return {
|
|
813
318
|
success: false,
|
|
814
|
-
source:
|
|
815
|
-
error:
|
|
319
|
+
source: oauth.source,
|
|
320
|
+
error: oauth.error ?? "Claude OAuth token required",
|
|
816
321
|
};
|
|
817
322
|
}
|
|
818
323
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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:
|
|
925
|
-
source:
|
|
926
|
-
|
|
334
|
+
success: true,
|
|
335
|
+
source: oauth.source,
|
|
336
|
+
usage: result.data,
|
|
927
337
|
};
|
|
928
338
|
}
|
|
929
|
-
|