opencode-anthropic-multi-account 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/index.js +2811 -0
  4. package/package.json +66 -0
package/dist/index.js ADDED
@@ -0,0 +1,2811 @@
1
+ // src/index.ts
2
+ import { AnthropicAuthPlugin as AnthropicAuthPlugin2 } from "opencode-anthropic-auth";
3
+ import { tool } from "@opencode-ai/plugin";
4
+
5
+ // ../multi-account-core/src/account-manager.ts
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ // ../multi-account-core/src/claims.ts
9
+ import { promises as fs2 } from "node:fs";
10
+ import { randomBytes as randomBytes2 } from "node:crypto";
11
+ import { dirname as dirname2, join as join3 } from "node:path";
12
+
13
+ // ../multi-account-core/src/utils.ts
14
+ import { join as join2 } from "node:path";
15
+ import { homedir as homedir2 } from "node:os";
16
+
17
+ // ../multi-account-core/src/config.ts
18
+ import { promises as fs } from "node:fs";
19
+ import { randomBytes } from "node:crypto";
20
+ import { dirname, join } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import * as v2 from "valibot";
23
+
24
+ // ../multi-account-core/src/types.ts
25
+ import * as v from "valibot";
26
+ var OAuthCredentialsSchema = v.object({
27
+ type: v.literal("oauth"),
28
+ refresh: v.string(),
29
+ access: v.string(),
30
+ expires: v.number()
31
+ });
32
+ var UsageLimitEntrySchema = v.object({
33
+ utilization: v.number(),
34
+ resets_at: v.nullable(v.string())
35
+ });
36
+ var UsageLimitsSchema = v.object({
37
+ five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
38
+ seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
39
+ seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
40
+ });
41
+ var CredentialRefreshPatchSchema = v.object({
42
+ accessToken: v.string(),
43
+ expiresAt: v.number(),
44
+ refreshToken: v.optional(v.string()),
45
+ uuid: v.optional(v.string()),
46
+ accountId: v.optional(v.string()),
47
+ email: v.optional(v.string())
48
+ });
49
+ var StoredAccountSchema = v.object({
50
+ uuid: v.optional(v.string()),
51
+ accountId: v.optional(v.string()),
52
+ label: v.optional(v.string()),
53
+ email: v.optional(v.string()),
54
+ planTier: v.optional(v.string(), ""),
55
+ refreshToken: v.string(),
56
+ accessToken: v.optional(v.string()),
57
+ expiresAt: v.optional(v.number()),
58
+ addedAt: v.number(),
59
+ lastUsed: v.number(),
60
+ enabled: v.optional(v.boolean(), true),
61
+ rateLimitResetAt: v.optional(v.number()),
62
+ cachedUsage: v.optional(UsageLimitsSchema),
63
+ cachedUsageAt: v.optional(v.number()),
64
+ consecutiveAuthFailures: v.optional(v.number(), 0),
65
+ isAuthDisabled: v.optional(v.boolean(), false),
66
+ authDisabledReason: v.optional(v.string())
67
+ });
68
+ var AccountStorageSchema = v.object({
69
+ version: v.literal(1),
70
+ accounts: v.optional(v.array(StoredAccountSchema), []),
71
+ activeAccountUuid: v.optional(v.string())
72
+ });
73
+ var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
74
+ var PluginConfigSchema = v.object({
75
+ account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
76
+ cross_process_claims: v.optional(v.boolean(), true),
77
+ soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
78
+ rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
79
+ default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
80
+ max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
81
+ token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
82
+ proactive_refresh: v.optional(v.boolean(), true),
83
+ proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
84
+ proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
85
+ quiet_mode: v.optional(v.boolean(), false),
86
+ debug: v.optional(v.boolean(), false)
87
+ });
88
+
89
+ // ../multi-account-core/src/config.ts
90
+ var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
91
+ var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
92
+ var configFilename = DEFAULT_CONFIG_FILENAME;
93
+ var cachedConfig = null;
94
+ var externalConfigGetter = null;
95
+ function getConfigDir() {
96
+ return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
97
+ }
98
+ function getConfigPath() {
99
+ return join(getConfigDir(), configFilename);
100
+ }
101
+ function parseConfig(raw) {
102
+ const result = v2.safeParse(PluginConfigSchema, raw);
103
+ return result.success ? result.output : DEFAULT_CONFIG;
104
+ }
105
+ function initCoreConfig(filename) {
106
+ configFilename = filename || DEFAULT_CONFIG_FILENAME;
107
+ cachedConfig = null;
108
+ }
109
+ async function loadConfig() {
110
+ if (cachedConfig) return cachedConfig;
111
+ const path = getConfigPath();
112
+ try {
113
+ const content = await fs.readFile(path, "utf-8");
114
+ cachedConfig = parseConfig(JSON.parse(content));
115
+ } catch {
116
+ cachedConfig = DEFAULT_CONFIG;
117
+ }
118
+ return cachedConfig;
119
+ }
120
+ function getConfig() {
121
+ if (cachedConfig) return cachedConfig;
122
+ if (externalConfigGetter && externalConfigGetter !== getConfig) {
123
+ try {
124
+ return parseConfig(externalConfigGetter());
125
+ } catch {
126
+ return DEFAULT_CONFIG;
127
+ }
128
+ }
129
+ return DEFAULT_CONFIG;
130
+ }
131
+ function setConfigGetter(getter) {
132
+ if (getter === getConfig) {
133
+ return;
134
+ }
135
+ externalConfigGetter = getter;
136
+ }
137
+ async function updateConfigField(key, value) {
138
+ const path = getConfigPath();
139
+ let existing = {};
140
+ try {
141
+ const content2 = await fs.readFile(path, "utf-8");
142
+ existing = JSON.parse(content2);
143
+ } catch {
144
+ }
145
+ existing[key] = value;
146
+ await fs.mkdir(dirname(path), { recursive: true });
147
+ const content = `${JSON.stringify(existing, null, 2)}
148
+ `;
149
+ const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
150
+ try {
151
+ await fs.writeFile(tempPath, content, "utf-8");
152
+ await fs.rename(tempPath, path);
153
+ } catch (error) {
154
+ try {
155
+ await fs.unlink(tempPath);
156
+ } catch {
157
+ }
158
+ throw error;
159
+ }
160
+ cachedConfig = null;
161
+ await loadConfig();
162
+ }
163
+
164
+ // ../multi-account-core/src/utils.ts
165
+ function getConfigDir2() {
166
+ return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
167
+ }
168
+ function getErrorCode(error) {
169
+ if (typeof error !== "object" || error === null || !("code" in error)) {
170
+ return void 0;
171
+ }
172
+ const code = error.code;
173
+ return typeof code === "string" ? code : void 0;
174
+ }
175
+ function formatWaitTime(ms) {
176
+ const totalSeconds = Math.ceil(ms / 1e3);
177
+ if (totalSeconds < 60) return `${totalSeconds}s`;
178
+ const days = Math.floor(totalSeconds / 86400);
179
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
180
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
181
+ const seconds = totalSeconds % 60;
182
+ const parts = [];
183
+ if (days > 0) parts.push(`${days}d`);
184
+ if (hours > 0) parts.push(`${hours}h`);
185
+ if (minutes > 0) parts.push(`${minutes}m`);
186
+ if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
187
+ return parts.join(" ") || "0s";
188
+ }
189
+ function getAccountLabel(account) {
190
+ if (account.label) return account.label;
191
+ if (account.email) return account.email;
192
+ if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
193
+ return `Account ${account.index + 1}`;
194
+ }
195
+ function sleep(ms) {
196
+ return new Promise((resolve) => setTimeout(resolve, ms));
197
+ }
198
+ async function showToast(client, message, variant) {
199
+ if (getConfig().quiet_mode) return;
200
+ try {
201
+ await client.tui.showToast({ body: { message, variant } });
202
+ } catch {
203
+ }
204
+ }
205
+ function debugLog(client, message, extra) {
206
+ if (!getConfig().debug) return;
207
+ client.app.log({
208
+ body: { service: "claude-multiauth", level: "debug", message, extra }
209
+ }).catch(() => {
210
+ });
211
+ }
212
+ function createMinimalClient() {
213
+ return {
214
+ auth: {
215
+ set: async () => {
216
+ }
217
+ },
218
+ tui: {
219
+ showToast: async () => {
220
+ }
221
+ },
222
+ app: {
223
+ log: async () => {
224
+ }
225
+ }
226
+ };
227
+ }
228
+
229
+ // ../multi-account-core/src/claims.ts
230
+ var CLAIMS_FILENAME = "multiauth-claims.json";
231
+ var CLAIM_EXPIRY_MS = 6e4;
232
+ function getClaimsPath() {
233
+ return join3(getConfigDir2(), CLAIMS_FILENAME);
234
+ }
235
+ function isClaimShape(value) {
236
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
237
+ const claim = value;
238
+ return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
239
+ }
240
+ function parseClaims(raw) {
241
+ let parsed;
242
+ try {
243
+ parsed = JSON.parse(raw);
244
+ } catch {
245
+ return {};
246
+ }
247
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
248
+ return {};
249
+ }
250
+ const claims = {};
251
+ for (const [accountId, claim] of Object.entries(parsed)) {
252
+ if (isClaimShape(claim)) {
253
+ claims[accountId] = claim;
254
+ }
255
+ }
256
+ return claims;
257
+ }
258
+ function isProcessAlive(pid) {
259
+ try {
260
+ process.kill(pid, 0);
261
+ return true;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+ function cleanClaims(claims, now) {
267
+ const cleaned = {};
268
+ let changed = false;
269
+ for (const [accountId, claim] of Object.entries(claims)) {
270
+ const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
271
+ const zombieClaim = !isProcessAlive(claim.pid);
272
+ if (expiredByTime || zombieClaim) {
273
+ changed = true;
274
+ continue;
275
+ }
276
+ cleaned[accountId] = claim;
277
+ }
278
+ return { cleaned, changed };
279
+ }
280
+ async function writeClaimsFile(claims) {
281
+ const path = getClaimsPath();
282
+ const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
283
+ await fs2.mkdir(dirname2(path), { recursive: true });
284
+ try {
285
+ await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
286
+ await fs2.rename(tempPath, path);
287
+ } catch (error) {
288
+ try {
289
+ await fs2.unlink(tempPath);
290
+ } catch {
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+ async function readClaims() {
296
+ try {
297
+ const data = await fs2.readFile(getClaimsPath(), "utf-8");
298
+ const parsed = parseClaims(data);
299
+ const now = Date.now();
300
+ const { cleaned, changed } = cleanClaims(parsed, now);
301
+ if (changed) {
302
+ try {
303
+ await writeClaimsFile(cleaned);
304
+ } catch {
305
+ }
306
+ }
307
+ return cleaned;
308
+ } catch {
309
+ return {};
310
+ }
311
+ }
312
+ async function writeClaim(accountId) {
313
+ const now = Date.now();
314
+ const claims = await readClaims();
315
+ const { cleaned } = cleanClaims(claims, now);
316
+ cleaned[accountId] = { pid: process.pid, at: now };
317
+ try {
318
+ await writeClaimsFile(cleaned);
319
+ } catch {
320
+ }
321
+ }
322
+ function isClaimedByOther(claims, accountId) {
323
+ if (!accountId) return false;
324
+ const claim = claims[accountId];
325
+ if (!claim) return false;
326
+ if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
327
+ if (!isProcessAlive(claim.pid)) return false;
328
+ return claim.pid !== process.pid;
329
+ }
330
+
331
+ // ../multi-account-core/src/account-manager.ts
332
+ var STARTUP_REFRESH_CONCURRENCY = 3;
333
+ var RECENT_429_COOLDOWN_MS = 3e4;
334
+ var HYBRID_SWITCH_MARGIN = 40;
335
+ function createAccountManagerForProvider(dependencies) {
336
+ const {
337
+ providerAuthId,
338
+ isTokenExpired: isTokenExpired2,
339
+ refreshToken: refreshToken2
340
+ } = dependencies;
341
+ return class AccountManager2 {
342
+ constructor(store) {
343
+ this.store = store;
344
+ }
345
+ cached = [];
346
+ activeAccountUuid;
347
+ client = null;
348
+ runtimeFactory = null;
349
+ roundRobinCursor = 0;
350
+ last429Map = /* @__PURE__ */ new Map();
351
+ static async create(store, currentAuth, client) {
352
+ const manager = new AccountManager2(store);
353
+ await manager.initialize(currentAuth, client);
354
+ return manager;
355
+ }
356
+ async initialize(currentAuth, client) {
357
+ if (client) this.client = client;
358
+ const storage = await this.store.load();
359
+ if (storage.accounts.length > 0) {
360
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
361
+ this.activeAccountUuid = storage.activeAccountUuid;
362
+ if (!this.getActiveAccount() && this.cached.length > 0) {
363
+ this.activeAccountUuid = this.cached[0].uuid;
364
+ }
365
+ return;
366
+ }
367
+ if (currentAuth.refresh) {
368
+ const newAccount = this.createNewAccount(currentAuth, Date.now());
369
+ await this.store.addAccount(newAccount);
370
+ await this.store.setActiveUuid(newAccount.uuid);
371
+ this.cached = [this.toManagedAccount(newAccount, 0)];
372
+ this.activeAccountUuid = newAccount.uuid;
373
+ }
374
+ }
375
+ async refresh() {
376
+ const storage = await this.store.load();
377
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
378
+ if (storage.activeAccountUuid) {
379
+ this.activeAccountUuid = storage.activeAccountUuid;
380
+ }
381
+ }
382
+ toManagedAccount(storedAccount, index) {
383
+ return {
384
+ index,
385
+ uuid: storedAccount.uuid,
386
+ accountId: storedAccount.accountId,
387
+ label: storedAccount.label,
388
+ email: storedAccount.email,
389
+ planTier: storedAccount.planTier,
390
+ refreshToken: storedAccount.refreshToken,
391
+ accessToken: storedAccount.accessToken,
392
+ expiresAt: storedAccount.expiresAt,
393
+ addedAt: storedAccount.addedAt,
394
+ lastUsed: storedAccount.lastUsed,
395
+ enabled: storedAccount.enabled,
396
+ rateLimitResetAt: storedAccount.rateLimitResetAt,
397
+ cachedUsage: storedAccount.cachedUsage,
398
+ cachedUsageAt: storedAccount.cachedUsageAt,
399
+ consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
400
+ isAuthDisabled: storedAccount.isAuthDisabled,
401
+ authDisabledReason: storedAccount.authDisabledReason,
402
+ last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
403
+ };
404
+ }
405
+ createNewAccount(auth, now) {
406
+ return {
407
+ uuid: randomUUID(),
408
+ refreshToken: auth.refresh,
409
+ accessToken: auth.access,
410
+ expiresAt: auth.expires,
411
+ addedAt: now,
412
+ lastUsed: now,
413
+ enabled: true,
414
+ planTier: "",
415
+ consecutiveAuthFailures: 0,
416
+ isAuthDisabled: false
417
+ };
418
+ }
419
+ getAccountCount() {
420
+ return this.getEligibleAccounts().length;
421
+ }
422
+ getAccounts() {
423
+ return [...this.cached];
424
+ }
425
+ getActiveAccount() {
426
+ if (this.activeAccountUuid) {
427
+ return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
428
+ }
429
+ return this.cached[0] ?? null;
430
+ }
431
+ setClient(client) {
432
+ this.client = client;
433
+ }
434
+ setRuntimeFactory(factory) {
435
+ this.runtimeFactory = factory;
436
+ }
437
+ getEligibleAccounts() {
438
+ return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
439
+ }
440
+ exceedsSoftQuota(account) {
441
+ const threshold = getConfig().soft_quota_threshold_percent;
442
+ if (threshold >= 100) return false;
443
+ const usage = account.cachedUsage;
444
+ if (!usage) return false;
445
+ const tiers = [usage.five_hour, usage.seven_day];
446
+ return tiers.some((tier) => tier != null && tier.utilization >= threshold);
447
+ }
448
+ hasAnyUsableAccount() {
449
+ return this.getEligibleAccounts().length > 0;
450
+ }
451
+ isRateLimited(account) {
452
+ if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
453
+ return true;
454
+ }
455
+ return this.isUsageExhausted(account);
456
+ }
457
+ isUsageExhausted(account) {
458
+ const usage = account.cachedUsage;
459
+ if (!usage) return false;
460
+ const now = Date.now();
461
+ const tiers = [usage.five_hour, usage.seven_day];
462
+ return tiers.some(
463
+ (tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
464
+ );
465
+ }
466
+ clearExpiredRateLimits() {
467
+ const now = Date.now();
468
+ for (const account of this.cached) {
469
+ if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
470
+ account.rateLimitResetAt = void 0;
471
+ }
472
+ }
473
+ }
474
+ getMinWaitTime() {
475
+ const eligible = this.getEligibleAccounts();
476
+ const available = eligible.filter((account) => !this.isRateLimited(account));
477
+ if (available.length > 0) return 0;
478
+ const now = Date.now();
479
+ const waits = [];
480
+ for (const account of eligible) {
481
+ if (account.rateLimitResetAt) {
482
+ const ms = account.rateLimitResetAt - now;
483
+ if (ms > 0) waits.push(ms);
484
+ }
485
+ const usageResetMs = this.getUsageResetMs(account);
486
+ if (usageResetMs !== null && usageResetMs > 0) {
487
+ waits.push(usageResetMs);
488
+ }
489
+ }
490
+ return waits.length > 0 ? Math.min(...waits) : 0;
491
+ }
492
+ getUsageResetMs(account) {
493
+ const usage = account.cachedUsage;
494
+ if (!usage) return null;
495
+ const now = Date.now();
496
+ const candidates = [];
497
+ const tiers = [usage.five_hour, usage.seven_day];
498
+ for (const tier of tiers) {
499
+ if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
500
+ const ms = Date.parse(tier.resets_at) - now;
501
+ if (ms > 0) candidates.push(ms);
502
+ }
503
+ }
504
+ return candidates.length > 0 ? Math.min(...candidates) : null;
505
+ }
506
+ async selectAccount() {
507
+ await this.refresh();
508
+ this.clearExpiredRateLimits();
509
+ const eligible = this.getEligibleAccounts();
510
+ if (eligible.length === 0) return null;
511
+ const config = getConfig();
512
+ const claims = config.cross_process_claims ? await readClaims() : {};
513
+ const strategy = config.account_selection_strategy;
514
+ let selected;
515
+ switch (strategy) {
516
+ case "round-robin":
517
+ selected = this.selectRoundRobin(eligible, claims);
518
+ break;
519
+ case "hybrid":
520
+ selected = this.selectHybrid(eligible, claims);
521
+ break;
522
+ case "sticky":
523
+ default:
524
+ selected = this.selectSticky(eligible, claims);
525
+ break;
526
+ }
527
+ if (selected?.uuid) {
528
+ this.activeAccountUuid = selected.uuid;
529
+ this.store.setActiveUuid(selected.uuid).catch(() => {
530
+ });
531
+ }
532
+ if (config.cross_process_claims && selected?.uuid) {
533
+ writeClaim(selected.uuid).catch(() => {
534
+ });
535
+ }
536
+ return selected;
537
+ }
538
+ isUsable(account) {
539
+ return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
540
+ }
541
+ isInRecentCooldown(account) {
542
+ if (!account.last429At) return false;
543
+ return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
544
+ }
545
+ fallbackNotRateLimited(eligible) {
546
+ const account = eligible.find((candidate) => !this.isRateLimited(candidate));
547
+ if (account) {
548
+ this.activateAccount(account);
549
+ return account;
550
+ }
551
+ return null;
552
+ }
553
+ selectSticky(eligible, claims) {
554
+ const current = this.getActiveAccount();
555
+ if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
556
+ this.activateAccount(current);
557
+ return current;
558
+ }
559
+ const unclaimed = eligible.find(
560
+ (account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
561
+ );
562
+ if (unclaimed) {
563
+ this.activateAccount(unclaimed);
564
+ return unclaimed;
565
+ }
566
+ const available = eligible.find((account) => this.isUsable(account));
567
+ if (available) {
568
+ this.activateAccount(available);
569
+ return available;
570
+ }
571
+ return this.fallbackNotRateLimited(eligible);
572
+ }
573
+ selectRoundRobin(eligible, claims) {
574
+ for (let i = 0; i < eligible.length; i++) {
575
+ const index = (this.roundRobinCursor + i) % eligible.length;
576
+ const account = eligible[index];
577
+ if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
578
+ this.roundRobinCursor = (index + 1) % eligible.length;
579
+ this.activateAccount(account);
580
+ return account;
581
+ }
582
+ }
583
+ for (let i = 0; i < eligible.length; i++) {
584
+ const index = (this.roundRobinCursor + i) % eligible.length;
585
+ const account = eligible[index];
586
+ if (this.isUsable(account)) {
587
+ this.roundRobinCursor = (index + 1) % eligible.length;
588
+ this.activateAccount(account);
589
+ return account;
590
+ }
591
+ }
592
+ return this.fallbackNotRateLimited(eligible);
593
+ }
594
+ selectHybrid(eligible, claims) {
595
+ const usable = eligible.filter((account) => this.isUsable(account));
596
+ const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
597
+ if (pool.length === 0) return null;
598
+ const activeUuid = this.activeAccountUuid;
599
+ let best = pool[0];
600
+ let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
601
+ for (let i = 1; i < pool.length; i++) {
602
+ const account = pool[i];
603
+ const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
604
+ if (score > bestScore) {
605
+ best = account;
606
+ bestScore = score;
607
+ }
608
+ }
609
+ const current = pool.find((account) => account.uuid === activeUuid);
610
+ if (current && current !== best) {
611
+ const currentScore = this.calculateHybridScore(current, true, claims);
612
+ const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
613
+ if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
614
+ this.activateAccount(current);
615
+ return current;
616
+ }
617
+ }
618
+ this.activateAccount(best);
619
+ return best;
620
+ }
621
+ calculateHybridScore(account, isActive, claims) {
622
+ const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
623
+ const usageScore = (100 - maxUtilization) / 100 * 450;
624
+ const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
625
+ const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
626
+ const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
627
+ const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
628
+ const stickinessBonus = isActive ? 120 : 0;
629
+ const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
630
+ return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
631
+ }
632
+ getMaxUtilization(account) {
633
+ const usage = account.cachedUsage;
634
+ if (!usage) return 65;
635
+ const tiers = [usage.five_hour, usage.seven_day];
636
+ const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
637
+ return utilizations.length > 0 ? Math.max(...utilizations) : 65;
638
+ }
639
+ activateAccount(account) {
640
+ this.activeAccountUuid = account.uuid;
641
+ account.lastUsed = Date.now();
642
+ }
643
+ async markRateLimited(uuid, backoffMs) {
644
+ const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
645
+ this.last429Map.set(uuid, Date.now());
646
+ await this.store.mutateAccount(uuid, (account) => {
647
+ account.rateLimitResetAt = Date.now() + effectiveBackoff;
648
+ });
649
+ }
650
+ async markRevoked(uuid) {
651
+ await this.store.mutateAccount(uuid, (account) => {
652
+ account.isAuthDisabled = true;
653
+ account.authDisabledReason = "OAuth token revoked (403)";
654
+ account.accessToken = void 0;
655
+ account.expiresAt = void 0;
656
+ });
657
+ this.runtimeFactory?.invalidate(uuid);
658
+ }
659
+ async markSuccess(uuid) {
660
+ this.last429Map.delete(uuid);
661
+ await this.store.mutateAccount(uuid, (account) => {
662
+ account.rateLimitResetAt = void 0;
663
+ account.consecutiveAuthFailures = 0;
664
+ account.lastUsed = Date.now();
665
+ });
666
+ }
667
+ syncToOpenCode(account) {
668
+ if (!this.client || !account.accessToken || !account.expiresAt) return;
669
+ this.client.auth.set({
670
+ path: { id: providerAuthId },
671
+ body: {
672
+ type: "oauth",
673
+ refresh: account.refreshToken,
674
+ access: account.accessToken,
675
+ expires: account.expiresAt
676
+ }
677
+ }).catch(() => {
678
+ });
679
+ }
680
+ async markAuthFailure(uuid, result) {
681
+ await this.store.mutateStorage((storage) => {
682
+ const account = storage.accounts.find((entry) => entry.uuid === uuid);
683
+ if (!account) return;
684
+ if (!result.ok && result.permanent) {
685
+ account.isAuthDisabled = true;
686
+ account.authDisabledReason = "Token permanently rejected (400/401/403)";
687
+ return;
688
+ }
689
+ account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
690
+ const maxFailures = getConfig().max_consecutive_auth_failures;
691
+ const usableCount = storage.accounts.filter(
692
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
693
+ ).length;
694
+ if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
695
+ account.isAuthDisabled = true;
696
+ account.authDisabledReason = `${maxFailures} consecutive auth failures`;
697
+ }
698
+ });
699
+ }
700
+ async applyUsageCache(uuid, usage) {
701
+ await this.store.mutateAccount(uuid, (account) => {
702
+ account.cachedUsage = usage;
703
+ account.cachedUsageAt = Date.now();
704
+ });
705
+ }
706
+ async applyProfileCache(uuid, profile) {
707
+ await this.store.mutateAccount(uuid, (account) => {
708
+ account.email = profile.email ?? account.email;
709
+ account.planTier = profile.planTier;
710
+ });
711
+ }
712
+ async ensureValidToken(uuid, client) {
713
+ const credentials = await this.store.readCredentials(uuid);
714
+ if (!credentials) return { ok: false, permanent: true };
715
+ if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
716
+ return {
717
+ ok: true,
718
+ patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
719
+ };
720
+ }
721
+ const result = await refreshToken2(credentials.refreshToken, uuid, client);
722
+ if (!result.ok) return result;
723
+ const updated = await this.store.mutateAccount(uuid, (account) => {
724
+ account.accessToken = result.patch.accessToken;
725
+ account.expiresAt = result.patch.expiresAt;
726
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
727
+ if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
728
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
729
+ if (result.patch.email) account.email = result.patch.email;
730
+ account.consecutiveAuthFailures = 0;
731
+ account.isAuthDisabled = false;
732
+ account.authDisabledReason = void 0;
733
+ });
734
+ if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
735
+ this.activeAccountUuid = result.patch.uuid;
736
+ this.store.setActiveUuid(result.patch.uuid).catch(() => {
737
+ });
738
+ }
739
+ if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
740
+ this.syncToOpenCode(updated);
741
+ }
742
+ return result;
743
+ }
744
+ async validateNonActiveTokens(client) {
745
+ await this.refresh();
746
+ const activeUuid = this.activeAccountUuid;
747
+ const eligible = this.cached.filter(
748
+ (account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
749
+ );
750
+ for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
751
+ const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
752
+ await Promise.all(
753
+ batch.map(async (account) => {
754
+ if (!account.uuid || !isTokenExpired2(account)) return;
755
+ const result = await this.ensureValidToken(account.uuid, client);
756
+ if (!result.ok) {
757
+ await this.markAuthFailure(account.uuid, result);
758
+ }
759
+ })
760
+ );
761
+ }
762
+ }
763
+ async removeAccount(index) {
764
+ const account = this.cached[index];
765
+ if (!account?.uuid) return false;
766
+ const removed = await this.store.removeAccount(account.uuid);
767
+ if (removed) {
768
+ await this.refresh();
769
+ }
770
+ return removed;
771
+ }
772
+ async clearAllAccounts() {
773
+ await this.store.clear();
774
+ this.cached = [];
775
+ this.activeAccountUuid = void 0;
776
+ }
777
+ async addAccount(auth) {
778
+ if (!auth.refresh) return;
779
+ const existing = this.cached.find((account) => account.refreshToken === auth.refresh);
780
+ if (existing) return;
781
+ const newAccount = this.createNewAccount(auth, Date.now());
782
+ await this.store.addAccount(newAccount);
783
+ this.activeAccountUuid = newAccount.uuid;
784
+ await this.store.setActiveUuid(newAccount.uuid);
785
+ await this.refresh();
786
+ }
787
+ async toggleEnabled(uuid) {
788
+ await this.store.mutateAccount(uuid, (account) => {
789
+ account.enabled = !(account.enabled ?? true);
790
+ if (account.enabled) {
791
+ account.isAuthDisabled = false;
792
+ account.authDisabledReason = void 0;
793
+ account.consecutiveAuthFailures = 0;
794
+ }
795
+ });
796
+ }
797
+ async replaceAccountCredentials(uuid, auth) {
798
+ const updated = await this.store.mutateAccount(uuid, (account) => {
799
+ account.refreshToken = auth.refresh;
800
+ account.accessToken = auth.access;
801
+ account.expiresAt = auth.expires;
802
+ account.lastUsed = Date.now();
803
+ account.enabled = true;
804
+ account.isAuthDisabled = false;
805
+ account.authDisabledReason = void 0;
806
+ account.consecutiveAuthFailures = 0;
807
+ account.rateLimitResetAt = void 0;
808
+ });
809
+ this.runtimeFactory?.invalidate(uuid);
810
+ if (updated && uuid === this.activeAccountUuid) {
811
+ this.syncToOpenCode(updated);
812
+ }
813
+ }
814
+ async retryAuth(uuid, client) {
815
+ await this.store.mutateAccount(uuid, (account) => {
816
+ account.consecutiveAuthFailures = 0;
817
+ account.isAuthDisabled = false;
818
+ account.authDisabledReason = void 0;
819
+ });
820
+ this.runtimeFactory?.invalidate(uuid);
821
+ const credentials = await this.store.readCredentials(uuid);
822
+ if (!credentials) return { ok: false, permanent: true };
823
+ const result = await refreshToken2(credentials.refreshToken, uuid, client);
824
+ if (result.ok) {
825
+ const updated = await this.store.mutateAccount(uuid, (account) => {
826
+ account.accessToken = result.patch.accessToken;
827
+ account.expiresAt = result.patch.expiresAt;
828
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
829
+ if (result.patch.uuid) account.uuid = result.patch.uuid;
830
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
831
+ if (result.patch.email) account.email = result.patch.email;
832
+ account.enabled = true;
833
+ account.consecutiveAuthFailures = 0;
834
+ });
835
+ this.runtimeFactory?.invalidate(uuid);
836
+ if (result.patch.uuid) {
837
+ this.runtimeFactory?.invalidate(result.patch.uuid);
838
+ }
839
+ const nextUuid = result.patch.uuid ?? uuid;
840
+ if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
841
+ this.activeAccountUuid = result.patch.uuid;
842
+ await this.store.setActiveUuid(result.patch.uuid);
843
+ }
844
+ if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
845
+ const freshCredentials = await this.store.readCredentials(nextUuid);
846
+ if (freshCredentials) {
847
+ this.syncToOpenCode({
848
+ refreshToken: freshCredentials.refreshToken,
849
+ accessToken: freshCredentials.accessToken,
850
+ expiresAt: freshCredentials.expiresAt
851
+ });
852
+ }
853
+ }
854
+ } else {
855
+ await this.markAuthFailure(uuid, result);
856
+ this.runtimeFactory?.invalidate(uuid);
857
+ }
858
+ return result;
859
+ }
860
+ };
861
+ }
862
+
863
+ // ../multi-account-core/src/account-store.ts
864
+ import { promises as fs4 } from "node:fs";
865
+ import { randomBytes as randomBytes3 } from "node:crypto";
866
+ import { dirname as dirname4, join as join5 } from "node:path";
867
+ import lockfile from "proper-lockfile";
868
+ import * as v4 from "valibot";
869
+
870
+ // ../multi-account-core/src/storage.ts
871
+ import { promises as fs3 } from "node:fs";
872
+ import { dirname as dirname3, join as join4 } from "node:path";
873
+ import * as v3 from "valibot";
874
+
875
+ // ../multi-account-core/src/constants.ts
876
+ var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
877
+ var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
878
+ function setAccountsFilename(filename) {
879
+ if (!filename) {
880
+ ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
881
+ return;
882
+ }
883
+ ACCOUNTS_FILENAME = filename;
884
+ }
885
+
886
+ // ../multi-account-core/src/storage.ts
887
+ function getStoragePath() {
888
+ return join4(getConfigDir2(), ACCOUNTS_FILENAME);
889
+ }
890
+ async function backupCorruptFile(targetPath, content) {
891
+ const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
892
+ await fs3.mkdir(dirname3(backupPath), { recursive: true });
893
+ await fs3.writeFile(backupPath, content, "utf-8");
894
+ }
895
+ async function readStorageFromDisk(targetPath, backupOnCorrupt) {
896
+ let content;
897
+ try {
898
+ content = await fs3.readFile(targetPath, "utf-8");
899
+ } catch (error) {
900
+ if (getErrorCode(error) === "ENOENT") {
901
+ return null;
902
+ }
903
+ throw error;
904
+ }
905
+ let parsed;
906
+ try {
907
+ parsed = JSON.parse(content);
908
+ } catch {
909
+ if (backupOnCorrupt) {
910
+ try {
911
+ await backupCorruptFile(targetPath, content);
912
+ } catch {
913
+ }
914
+ }
915
+ return null;
916
+ }
917
+ const validation = v3.safeParse(AccountStorageSchema, parsed);
918
+ if (!validation.success) {
919
+ if (backupOnCorrupt) {
920
+ try {
921
+ await backupCorruptFile(targetPath, content);
922
+ } catch {
923
+ }
924
+ }
925
+ return null;
926
+ }
927
+ return validation.output;
928
+ }
929
+ function deduplicateAccounts(accounts) {
930
+ const deduplicated = [];
931
+ const indexByUuid = /* @__PURE__ */ new Map();
932
+ for (const account of accounts) {
933
+ if (!account.uuid) {
934
+ deduplicated.push(account);
935
+ continue;
936
+ }
937
+ const existingIndex = indexByUuid.get(account.uuid);
938
+ if (existingIndex === void 0) {
939
+ indexByUuid.set(account.uuid, deduplicated.length);
940
+ deduplicated.push(account);
941
+ continue;
942
+ }
943
+ const existingAccount = deduplicated[existingIndex];
944
+ if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
945
+ deduplicated[existingIndex] = account;
946
+ }
947
+ }
948
+ return deduplicated;
949
+ }
950
+ async function loadAccounts() {
951
+ const storagePath = getStoragePath();
952
+ const storage = await readStorageFromDisk(storagePath, true);
953
+ if (!storage) {
954
+ return null;
955
+ }
956
+ return {
957
+ ...storage,
958
+ accounts: deduplicateAccounts(storage.accounts || [])
959
+ };
960
+ }
961
+
962
+ // ../multi-account-core/src/account-store.ts
963
+ var FILE_MODE = 384;
964
+ var LOCK_OPTIONS = {
965
+ stale: 1e4,
966
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
967
+ };
968
+ function getStoragePath2() {
969
+ return join5(getConfigDir2(), ACCOUNTS_FILENAME);
970
+ }
971
+ function createEmptyStorage() {
972
+ return { version: 1, accounts: [] };
973
+ }
974
+ function buildTempPath(targetPath) {
975
+ return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
976
+ }
977
+ async function writeAtomicText(targetPath, content) {
978
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
979
+ const tempPath = buildTempPath(targetPath);
980
+ try {
981
+ await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
982
+ await fs4.chmod(tempPath, FILE_MODE);
983
+ await fs4.rename(tempPath, targetPath);
984
+ await fs4.chmod(targetPath, FILE_MODE);
985
+ } catch (error) {
986
+ try {
987
+ await fs4.unlink(tempPath);
988
+ } catch {
989
+ }
990
+ throw error;
991
+ }
992
+ }
993
+ async function writeStorageAtomic(targetPath, storage) {
994
+ const validation = v4.safeParse(AccountStorageSchema, storage);
995
+ if (!validation.success) {
996
+ throw new Error("Invalid account storage payload");
997
+ }
998
+ await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
999
+ `);
1000
+ }
1001
+ async function ensureStorageFileExists(targetPath) {
1002
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
1003
+ const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
1004
+ `;
1005
+ try {
1006
+ await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
1007
+ } catch (error) {
1008
+ if (getErrorCode(error) !== "EEXIST") throw error;
1009
+ }
1010
+ }
1011
+ async function withFileLock(fn) {
1012
+ const storagePath = getStoragePath2();
1013
+ await ensureStorageFileExists(storagePath);
1014
+ let release = null;
1015
+ try {
1016
+ release = await lockfile.lock(storagePath, LOCK_OPTIONS);
1017
+ return await fn(storagePath);
1018
+ } finally {
1019
+ if (release) {
1020
+ try {
1021
+ await release();
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ var AccountStore = class {
1028
+ async load() {
1029
+ const storage = await loadAccounts();
1030
+ return storage ?? createEmptyStorage();
1031
+ }
1032
+ async readCredentials(uuid) {
1033
+ const storagePath = getStoragePath2();
1034
+ const storage = await readStorageFromDisk(storagePath, false);
1035
+ if (!storage) return null;
1036
+ const account = storage.accounts.find((a) => a.uuid === uuid);
1037
+ if (!account) return null;
1038
+ return {
1039
+ refreshToken: account.refreshToken,
1040
+ accessToken: account.accessToken,
1041
+ expiresAt: account.expiresAt,
1042
+ accountId: account.accountId
1043
+ };
1044
+ }
1045
+ async mutateAccount(uuid, fn) {
1046
+ return await withFileLock(async (storagePath) => {
1047
+ const current = await readStorageFromDisk(storagePath, false);
1048
+ if (!current) return null;
1049
+ const account = current.accounts.find((a) => a.uuid === uuid);
1050
+ if (!account) return null;
1051
+ fn(account);
1052
+ await writeStorageAtomic(storagePath, current);
1053
+ return { ...account };
1054
+ });
1055
+ }
1056
+ async mutateStorage(fn) {
1057
+ await withFileLock(async (storagePath) => {
1058
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1059
+ fn(current);
1060
+ await writeStorageAtomic(storagePath, current);
1061
+ });
1062
+ }
1063
+ async addAccount(account) {
1064
+ await withFileLock(async (storagePath) => {
1065
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1066
+ const exists = current.accounts.some(
1067
+ (a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
1068
+ );
1069
+ if (exists) return;
1070
+ current.accounts.push(account);
1071
+ await writeStorageAtomic(storagePath, current);
1072
+ });
1073
+ }
1074
+ async removeAccount(uuid) {
1075
+ return await withFileLock(async (storagePath) => {
1076
+ const current = await readStorageFromDisk(storagePath, false);
1077
+ if (!current) return false;
1078
+ const initialLength = current.accounts.length;
1079
+ current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
1080
+ if (current.accounts.length === initialLength) return false;
1081
+ if (current.activeAccountUuid === uuid) {
1082
+ current.activeAccountUuid = current.accounts[0]?.uuid;
1083
+ }
1084
+ await writeStorageAtomic(storagePath, current);
1085
+ return true;
1086
+ });
1087
+ }
1088
+ async setActiveUuid(uuid) {
1089
+ await this.mutateStorage((storage) => {
1090
+ storage.activeAccountUuid = uuid;
1091
+ });
1092
+ }
1093
+ async clear() {
1094
+ await withFileLock(async (storagePath) => {
1095
+ await writeStorageAtomic(storagePath, createEmptyStorage());
1096
+ });
1097
+ }
1098
+ };
1099
+
1100
+ // ../multi-account-core/src/executor.ts
1101
+ var MIN_MAX_RETRIES = 6;
1102
+ var RETRIES_PER_ACCOUNT = 3;
1103
+ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1104
+ var MAX_RESOLVE_ATTEMPTS = 10;
1105
+ var SERVER_RETRY_BASE_MS = 1e3;
1106
+ var SERVER_RETRY_MAX_MS = 4e3;
1107
+ var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1108
+ function createExecutorForProvider(providerName, dependencies) {
1109
+ const {
1110
+ handleRateLimitResponse: handleRateLimitResponse2,
1111
+ formatWaitTime: formatWaitTime2,
1112
+ sleep: sleep2,
1113
+ showToast: showToast2,
1114
+ getAccountLabel: getAccountLabel2
1115
+ } = dependencies;
1116
+ async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
1117
+ const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1118
+ let retries = 0;
1119
+ let previousAccountUuid;
1120
+ while (true) {
1121
+ if (++retries > maxRetries) {
1122
+ throw new Error(
1123
+ `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1124
+ );
1125
+ }
1126
+ await manager.refresh();
1127
+ const account = await resolveAccount(manager, client);
1128
+ const accountUuid = account.uuid;
1129
+ if (!accountUuid) continue;
1130
+ if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1131
+ void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1132
+ }
1133
+ previousAccountUuid = accountUuid;
1134
+ let runtime;
1135
+ let response;
1136
+ try {
1137
+ runtime = await runtimeFactory.getRuntime(accountUuid);
1138
+ response = await runtime.fetch(input, init);
1139
+ } catch (error) {
1140
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1141
+ continue;
1142
+ }
1143
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1144
+ continue;
1145
+ }
1146
+ if (response.status >= 500) {
1147
+ let serverResponse = response;
1148
+ let networkErrorDuringServerRetry = false;
1149
+ let authFailureDuringServerRetry = false;
1150
+ for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1151
+ const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1152
+ const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1153
+ await sleep2(jitteredBackoff);
1154
+ try {
1155
+ serverResponse = await runtime.fetch(input, init);
1156
+ } catch (error) {
1157
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1158
+ authFailureDuringServerRetry = true;
1159
+ break;
1160
+ }
1161
+ networkErrorDuringServerRetry = true;
1162
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1163
+ break;
1164
+ }
1165
+ if (serverResponse.status < 500) break;
1166
+ }
1167
+ if (authFailureDuringServerRetry) {
1168
+ continue;
1169
+ }
1170
+ if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1171
+ continue;
1172
+ }
1173
+ response = serverResponse;
1174
+ }
1175
+ if (response.status === 401) {
1176
+ runtimeFactory.invalidate(accountUuid);
1177
+ try {
1178
+ const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1179
+ const retryResponse = await retryRuntime.fetch(input, init);
1180
+ if (retryResponse.status !== 401) {
1181
+ await manager.markSuccess(accountUuid);
1182
+ return retryResponse;
1183
+ }
1184
+ } catch (error) {
1185
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1186
+ continue;
1187
+ }
1188
+ continue;
1189
+ }
1190
+ await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1191
+ await manager.refresh();
1192
+ if (!manager.hasAnyUsableAccount()) {
1193
+ void showToast2(client, "All accounts have auth failures.", "error");
1194
+ throw new Error(
1195
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1196
+ );
1197
+ }
1198
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1199
+ continue;
1200
+ }
1201
+ if (response.status === 403) {
1202
+ const revoked = await isRevokedTokenResponse(response);
1203
+ if (revoked) {
1204
+ await manager.markRevoked(accountUuid);
1205
+ await manager.refresh();
1206
+ void showToast2(
1207
+ client,
1208
+ `${getAccountLabel2(account)} disabled: OAuth token revoked.`,
1209
+ "error"
1210
+ );
1211
+ if (!manager.hasAnyUsableAccount()) {
1212
+ throw new Error(
1213
+ `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1214
+ );
1215
+ }
1216
+ continue;
1217
+ }
1218
+ }
1219
+ if (response.status === 429) {
1220
+ await handleRateLimitResponse2(manager, client, account, response);
1221
+ continue;
1222
+ }
1223
+ await manager.markSuccess(accountUuid);
1224
+ return response;
1225
+ }
1226
+ }
1227
+ async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1228
+ const refreshFailureStatus = getRefreshFailureStatus(error);
1229
+ if (refreshFailureStatus === void 0) return false;
1230
+ if (!account.uuid) return false;
1231
+ const accountUuid = account.uuid;
1232
+ runtimeFactory.invalidate(accountUuid);
1233
+ await manager.markAuthFailure(accountUuid, {
1234
+ ok: false,
1235
+ permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1236
+ });
1237
+ await manager.refresh();
1238
+ if (!manager.hasAnyUsableAccount()) {
1239
+ void showToast2(client, "All accounts have auth failures.", "error");
1240
+ throw new Error(
1241
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1242
+ );
1243
+ }
1244
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1245
+ return true;
1246
+ }
1247
+ async function resolveAccount(manager, client) {
1248
+ let attempts = 0;
1249
+ while (true) {
1250
+ if (++attempts > MAX_RESOLVE_ATTEMPTS) {
1251
+ throw new Error(
1252
+ `Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
1253
+ );
1254
+ }
1255
+ const account = await manager.selectAccount();
1256
+ if (account) return account;
1257
+ if (!manager.hasAnyUsableAccount()) {
1258
+ throw new Error(
1259
+ `All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
1260
+ );
1261
+ }
1262
+ const waitMs = manager.getMinWaitTime();
1263
+ if (waitMs <= 0) {
1264
+ throw new Error(
1265
+ `All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
1266
+ );
1267
+ }
1268
+ await showToast2(
1269
+ client,
1270
+ `All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
1271
+ "warning"
1272
+ );
1273
+ await sleep2(waitMs);
1274
+ }
1275
+ }
1276
+ return {
1277
+ executeWithAccountRotation: executeWithAccountRotation2
1278
+ };
1279
+ }
1280
+ function getRefreshFailureStatus(error) {
1281
+ if (!(error instanceof Error)) return void 0;
1282
+ const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1283
+ if (!matched) return void 0;
1284
+ const status = Number(matched[1]);
1285
+ return Number.isFinite(status) ? status : void 0;
1286
+ }
1287
+ async function isRevokedTokenResponse(response) {
1288
+ try {
1289
+ const cloned = response.clone();
1290
+ const body = await cloned.text();
1291
+ return body.includes("revoked");
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ }
1296
+
1297
+ // ../multi-account-core/src/proactive-refresh.ts
1298
+ var INITIAL_DELAY_MS = 5e3;
1299
+ function createProactiveRefreshQueueForProvider(dependencies) {
1300
+ const {
1301
+ getConfig: getConfig2,
1302
+ refreshToken: refreshToken2,
1303
+ isTokenExpired: isTokenExpired2,
1304
+ debugLog: debugLog2
1305
+ } = dependencies;
1306
+ return class ProactiveRefreshQueue {
1307
+ constructor(client, store, onInvalidate) {
1308
+ this.client = client;
1309
+ this.store = store;
1310
+ this.onInvalidate = onInvalidate;
1311
+ }
1312
+ timeoutHandle = null;
1313
+ runToken = 0;
1314
+ inFlight = null;
1315
+ start() {
1316
+ const config = getConfig2();
1317
+ if (!config.proactive_refresh) return;
1318
+ this.runToken++;
1319
+ this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1320
+ debugLog2(this.client, "Proactive refresh started", {
1321
+ intervalSeconds: config.proactive_refresh_interval_seconds,
1322
+ bufferSeconds: config.proactive_refresh_buffer_seconds
1323
+ });
1324
+ }
1325
+ async stop() {
1326
+ this.runToken++;
1327
+ if (this.timeoutHandle) {
1328
+ clearTimeout(this.timeoutHandle);
1329
+ this.timeoutHandle = null;
1330
+ }
1331
+ if (this.inFlight) {
1332
+ await this.inFlight;
1333
+ this.inFlight = null;
1334
+ }
1335
+ debugLog2(this.client, "Proactive refresh stopped");
1336
+ }
1337
+ scheduleNext(token, delayMs) {
1338
+ this.timeoutHandle = setTimeout(() => {
1339
+ if (token !== this.runToken) return;
1340
+ this.inFlight = this.runCheck(token).finally(() => {
1341
+ this.inFlight = null;
1342
+ });
1343
+ }, delayMs);
1344
+ }
1345
+ needsProactiveRefresh(account) {
1346
+ if (!account.accessToken || !account.expiresAt) return false;
1347
+ if (isTokenExpired2(account)) return false;
1348
+ const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
1349
+ return account.expiresAt <= Date.now() + bufferMs;
1350
+ }
1351
+ async runCheck(token) {
1352
+ try {
1353
+ const stored = await this.store.load();
1354
+ if (token !== this.runToken) return;
1355
+ const candidates = stored.accounts.filter(
1356
+ (a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
1357
+ );
1358
+ if (candidates.length === 0) return;
1359
+ debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
1360
+ for (const account of candidates) {
1361
+ if (token !== this.runToken) return;
1362
+ const credentials = await this.store.readCredentials(account.uuid);
1363
+ if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
1364
+ const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
1365
+ if (result.ok) {
1366
+ await this.store.mutateAccount(account.uuid, (target) => {
1367
+ target.accessToken = result.patch.accessToken;
1368
+ target.expiresAt = result.patch.expiresAt;
1369
+ if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
1370
+ if (result.patch.uuid) target.uuid = result.patch.uuid;
1371
+ if (result.patch.email) target.email = result.patch.email;
1372
+ if (result.patch.accountId) target.accountId = result.patch.accountId;
1373
+ target.consecutiveAuthFailures = 0;
1374
+ target.isAuthDisabled = false;
1375
+ target.authDisabledReason = void 0;
1376
+ });
1377
+ this.onInvalidate?.(account.uuid);
1378
+ } else {
1379
+ await this.persistFailure(account, result.permanent);
1380
+ }
1381
+ }
1382
+ } catch (error) {
1383
+ debugLog2(this.client, `Proactive refresh check error: ${error}`);
1384
+ } finally {
1385
+ if (token === this.runToken) {
1386
+ const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
1387
+ this.scheduleNext(token, intervalMs);
1388
+ }
1389
+ }
1390
+ }
1391
+ async persistFailure(account, permanent) {
1392
+ try {
1393
+ await this.store.mutateAccount(account.uuid, (target) => {
1394
+ if (permanent) {
1395
+ target.isAuthDisabled = true;
1396
+ target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1397
+ } else {
1398
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1399
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1400
+ if (target.consecutiveAuthFailures >= maxFailures) {
1401
+ target.isAuthDisabled = true;
1402
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1403
+ }
1404
+ }
1405
+ });
1406
+ } catch {
1407
+ debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1408
+ }
1409
+ }
1410
+ };
1411
+ }
1412
+
1413
+ // ../multi-account-core/src/rate-limit.ts
1414
+ var USAGE_FETCH_COOLDOWN_MS = 3e4;
1415
+ function createRateLimitHandlers(dependencies) {
1416
+ const {
1417
+ fetchUsage: fetchUsage2,
1418
+ getConfig: getConfig2,
1419
+ formatWaitTime: formatWaitTime2,
1420
+ getAccountLabel: getAccountLabel2,
1421
+ showToast: showToast2
1422
+ } = dependencies;
1423
+ function retryAfterMsFromResponse2(response) {
1424
+ const retryAfterMs = response.headers.get("retry-after-ms");
1425
+ if (retryAfterMs) {
1426
+ const parsed = parseInt(retryAfterMs, 10);
1427
+ if (!isNaN(parsed) && parsed > 0) return parsed;
1428
+ }
1429
+ const retryAfter = response.headers.get("retry-after");
1430
+ if (retryAfter) {
1431
+ const parsed = parseInt(retryAfter, 10);
1432
+ if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
1433
+ }
1434
+ return getConfig2().default_retry_after_ms;
1435
+ }
1436
+ function getResetMsFromUsage2(account) {
1437
+ const usage = account.cachedUsage;
1438
+ if (!usage) return null;
1439
+ const now = Date.now();
1440
+ const candidates = [];
1441
+ if (usage.five_hour?.resets_at) {
1442
+ const ms = Date.parse(usage.five_hour.resets_at) - now;
1443
+ if (ms > 0) candidates.push(ms);
1444
+ }
1445
+ if (usage.seven_day?.resets_at) {
1446
+ const ms = Date.parse(usage.seven_day.resets_at) - now;
1447
+ if (ms > 0) candidates.push(ms);
1448
+ }
1449
+ return candidates.length > 0 ? Math.min(...candidates) : null;
1450
+ }
1451
+ async function fetchUsageLimits2(accessToken, accountId) {
1452
+ if (!accessToken) return null;
1453
+ try {
1454
+ const result = await fetchUsage2(accessToken, accountId);
1455
+ return result.ok ? result.data : null;
1456
+ } catch {
1457
+ return null;
1458
+ }
1459
+ }
1460
+ async function handleRateLimitResponse2(manager, client, account, response) {
1461
+ if (!account.uuid) return;
1462
+ const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
1463
+ await manager.markRateLimited(account.uuid, resetMs);
1464
+ const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
1465
+ if (shouldFetchUsage) {
1466
+ const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
1467
+ if (usage) {
1468
+ await manager.applyUsageCache(account.uuid, usage);
1469
+ }
1470
+ }
1471
+ if (manager.getAccountCount() > 1) {
1472
+ void showToast2(
1473
+ client,
1474
+ `${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
1475
+ "warning"
1476
+ );
1477
+ }
1478
+ }
1479
+ return {
1480
+ retryAfterMsFromResponse: retryAfterMsFromResponse2,
1481
+ getResetMsFromUsage: getResetMsFromUsage2,
1482
+ fetchUsageLimits: fetchUsageLimits2,
1483
+ handleRateLimitResponse: handleRateLimitResponse2
1484
+ };
1485
+ }
1486
+
1487
+ // ../multi-account-core/src/auth-migration.ts
1488
+ import { promises as fs5 } from "node:fs";
1489
+ import { join as join6 } from "node:path";
1490
+ var AUTH_JSON_FILENAME = "auth.json";
1491
+ function isValidOAuthCredential(value) {
1492
+ if (typeof value !== "object" || value === null) return false;
1493
+ const candidate = value;
1494
+ return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
1495
+ }
1496
+ function resolveAuthJsonPath() {
1497
+ return join6(getConfigDir2(), AUTH_JSON_FILENAME);
1498
+ }
1499
+ async function readAuthJson() {
1500
+ const authPath = resolveAuthJsonPath();
1501
+ let content;
1502
+ try {
1503
+ content = await fs5.readFile(authPath, "utf-8");
1504
+ } catch {
1505
+ return null;
1506
+ }
1507
+ try {
1508
+ const parsed = JSON.parse(content);
1509
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1510
+ return null;
1511
+ }
1512
+ return parsed;
1513
+ } catch {
1514
+ return null;
1515
+ }
1516
+ }
1517
+ async function migrateFromAuthJson(providerKey, store) {
1518
+ const storage = await store.load();
1519
+ const hasExistingAccounts = storage.accounts.length > 0;
1520
+ if (hasExistingAccounts) return false;
1521
+ const authData = await readAuthJson();
1522
+ if (!authData) return false;
1523
+ const providerCredential = authData[providerKey];
1524
+ if (!isValidOAuthCredential(providerCredential)) return false;
1525
+ const now = Date.now();
1526
+ const newAccount = {
1527
+ uuid: crypto.randomUUID(),
1528
+ refreshToken: providerCredential.refresh,
1529
+ accessToken: providerCredential.access,
1530
+ expiresAt: providerCredential.expires,
1531
+ addedAt: now,
1532
+ lastUsed: now,
1533
+ enabled: true,
1534
+ planTier: "",
1535
+ consecutiveAuthFailures: 0,
1536
+ isAuthDisabled: false
1537
+ };
1538
+ await store.addAccount(newAccount);
1539
+ await store.setActiveUuid(newAccount.uuid);
1540
+ return true;
1541
+ }
1542
+
1543
+ // ../multi-account-core/src/ui/ansi.ts
1544
+ var ANSI = {
1545
+ hide: "\x1B[?25l",
1546
+ show: "\x1B[?25h",
1547
+ up: (n = 1) => `\x1B[${n}A`,
1548
+ down: (n = 1) => `\x1B[${n}B`,
1549
+ clearLine: "\x1B[2K",
1550
+ cyan: "\x1B[36m",
1551
+ green: "\x1B[32m",
1552
+ red: "\x1B[31m",
1553
+ yellow: "\x1B[33m",
1554
+ dim: "\x1B[2m",
1555
+ bold: "\x1B[1m",
1556
+ reset: "\x1B[0m"
1557
+ };
1558
+ function parseKey(data) {
1559
+ const s = data.toString();
1560
+ if (s === "\x1B[A" || s === "\x1BOA") return "up";
1561
+ if (s === "\x1B[B" || s === "\x1BOB") return "down";
1562
+ if (s === "\r" || s === "\n") return "enter";
1563
+ if (s === "") return "escape";
1564
+ if (s === "\x1B") return "escape-start";
1565
+ return null;
1566
+ }
1567
+ function isTTY() {
1568
+ return Boolean(process.stdin.isTTY);
1569
+ }
1570
+
1571
+ // ../multi-account-core/src/ui/select.ts
1572
+ var ESCAPE_TIMEOUT_MS = 50;
1573
+ var COLOR_MAP = {
1574
+ red: ANSI.red,
1575
+ green: ANSI.green,
1576
+ yellow: ANSI.yellow,
1577
+ cyan: ANSI.cyan
1578
+ };
1579
+ async function select(items, options) {
1580
+ if (!isTTY()) {
1581
+ throw new Error("Interactive select requires a TTY terminal");
1582
+ }
1583
+ const enabledItems = items.filter((i) => !i.disabled && !i.separator);
1584
+ if (enabledItems.length === 0) {
1585
+ throw new Error("All items disabled");
1586
+ }
1587
+ if (enabledItems.length === 1) {
1588
+ return enabledItems[0].value;
1589
+ }
1590
+ const { message, subtitle } = options;
1591
+ const { stdin, stdout } = process;
1592
+ let cursor = items.findIndex((i) => !i.disabled && !i.separator);
1593
+ if (cursor === -1) cursor = 0;
1594
+ let escapeTimeout = null;
1595
+ let isCleanedUp = false;
1596
+ let isFirstRender = true;
1597
+ const getTotalLines = () => {
1598
+ const subtitleLines = subtitle ? 3 : 0;
1599
+ return 1 + subtitleLines + items.length + 1 + 1;
1600
+ };
1601
+ const renderItemLabel = (item, isSelected) => {
1602
+ const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
1603
+ if (item.disabled) {
1604
+ return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
1605
+ }
1606
+ const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
1607
+ if (isSelected) {
1608
+ const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
1609
+ return `${label}${hintSuffix}`;
1610
+ }
1611
+ const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
1612
+ return `${dimLabel}${hintSuffix}`;
1613
+ };
1614
+ const render = () => {
1615
+ const totalLines = getTotalLines();
1616
+ if (!isFirstRender) {
1617
+ stdout.write(ANSI.up(totalLines) + "\r");
1618
+ }
1619
+ isFirstRender = false;
1620
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
1621
+ `);
1622
+ if (subtitle) {
1623
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1624
+ `);
1625
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
1626
+ `);
1627
+ stdout.write(`${ANSI.clearLine}
1628
+ `);
1629
+ }
1630
+ for (let i = 0; i < items.length; i++) {
1631
+ const item = items[i];
1632
+ if (!item) continue;
1633
+ if (item.separator) {
1634
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1635
+ `);
1636
+ continue;
1637
+ }
1638
+ const isSelected = i === cursor;
1639
+ const labelText = renderItemLabel(item, isSelected);
1640
+ const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
1641
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
1642
+ `);
1643
+ }
1644
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
1645
+ `);
1646
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
1647
+ `);
1648
+ };
1649
+ return new Promise((resolve) => {
1650
+ const wasRaw = stdin.isRaw ?? false;
1651
+ const cleanup = () => {
1652
+ if (isCleanedUp) return;
1653
+ isCleanedUp = true;
1654
+ if (escapeTimeout) {
1655
+ clearTimeout(escapeTimeout);
1656
+ escapeTimeout = null;
1657
+ }
1658
+ try {
1659
+ stdin.removeListener("data", onKey);
1660
+ stdin.setRawMode(wasRaw);
1661
+ stdin.pause();
1662
+ stdout.write(ANSI.show);
1663
+ } catch {
1664
+ }
1665
+ process.removeListener("SIGINT", onSignal);
1666
+ process.removeListener("SIGTERM", onSignal);
1667
+ };
1668
+ const onSignal = () => {
1669
+ cleanup();
1670
+ resolve(null);
1671
+ };
1672
+ const finishWithValue = (value) => {
1673
+ cleanup();
1674
+ resolve(value);
1675
+ };
1676
+ const findNextSelectable = (from, direction) => {
1677
+ if (items.length === 0) return from;
1678
+ let next = from;
1679
+ do {
1680
+ next = (next + direction + items.length) % items.length;
1681
+ } while (items[next]?.disabled || items[next]?.separator);
1682
+ return next;
1683
+ };
1684
+ const onKey = (data) => {
1685
+ if (escapeTimeout) {
1686
+ clearTimeout(escapeTimeout);
1687
+ escapeTimeout = null;
1688
+ }
1689
+ const action = parseKey(data);
1690
+ switch (action) {
1691
+ case "up":
1692
+ cursor = findNextSelectable(cursor, -1);
1693
+ render();
1694
+ return;
1695
+ case "down":
1696
+ cursor = findNextSelectable(cursor, 1);
1697
+ render();
1698
+ return;
1699
+ case "enter":
1700
+ finishWithValue(items[cursor]?.value ?? null);
1701
+ return;
1702
+ case "escape":
1703
+ finishWithValue(null);
1704
+ return;
1705
+ case "escape-start":
1706
+ escapeTimeout = setTimeout(() => {
1707
+ finishWithValue(null);
1708
+ }, ESCAPE_TIMEOUT_MS);
1709
+ return;
1710
+ default:
1711
+ return;
1712
+ }
1713
+ };
1714
+ process.once("SIGINT", onSignal);
1715
+ process.once("SIGTERM", onSignal);
1716
+ try {
1717
+ stdin.setRawMode(true);
1718
+ } catch {
1719
+ cleanup();
1720
+ resolve(null);
1721
+ return;
1722
+ }
1723
+ stdin.resume();
1724
+ stdout.write(ANSI.hide);
1725
+ render();
1726
+ stdin.on("data", onKey);
1727
+ });
1728
+ }
1729
+
1730
+ // ../multi-account-core/src/ui/confirm.ts
1731
+ async function confirm(message, defaultYes = false) {
1732
+ const items = defaultYes ? [
1733
+ { label: "Yes", value: true },
1734
+ { label: "No", value: false }
1735
+ ] : [
1736
+ { label: "No", value: false },
1737
+ { label: "Yes", value: true }
1738
+ ];
1739
+ const result = await select(items, { message });
1740
+ return result ?? false;
1741
+ }
1742
+
1743
+ // ../oauth-adapters/src/anthropic.ts
1744
+ var anthropicOAuthAdapter = {
1745
+ id: "anthropic",
1746
+ authProviderId: "anthropic",
1747
+ modelDisplayName: "Claude",
1748
+ statusToolName: "claude_multiauth_status",
1749
+ authMethodLabel: "Claude Pro/Max (Multi-Auth)",
1750
+ serviceLogName: "claude-multiauth",
1751
+ oauthClientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
1752
+ tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
1753
+ usageEndpoint: "https://api.anthropic.com/api/oauth/usage",
1754
+ profileEndpoint: "https://api.anthropic.com/api/oauth/profile",
1755
+ oauthBetaHeader: "oauth-2025-04-20",
1756
+ requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1757
+ cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1758
+ toolPrefix: "mcp_",
1759
+ accountStorageFilename: "anthropic-multi-account-accounts.json",
1760
+ transform: {
1761
+ rewriteOpenCodeBranding: true,
1762
+ addToolPrefix: true,
1763
+ stripToolPrefixInResponse: true,
1764
+ enableMessagesBetaQuery: true
1765
+ },
1766
+ planLabels: {
1767
+ max: "Claude Max",
1768
+ pro: "Claude Pro",
1769
+ free: "Free"
1770
+ },
1771
+ supported: true
1772
+ };
1773
+
1774
+ // src/constants.ts
1775
+ var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
1776
+ var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
1777
+ var ANTHROPIC_TOKEN_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.tokenEndpoint;
1778
+ var ANTHROPIC_USAGE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.usageEndpoint;
1779
+ var ANTHROPIC_PROFILE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.profileEndpoint;
1780
+ var ANTHROPIC_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader;
1781
+ var CLAUDE_CLI_USER_AGENT = ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
1782
+ var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
1783
+ var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
1784
+ var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
1785
+ var TOKEN_EXPIRY_BUFFER_MS = 6e4;
1786
+ var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
1787
+
1788
+ // src/token.ts
1789
+ import * as v6 from "valibot";
1790
+
1791
+ // src/types.ts
1792
+ import * as v5 from "valibot";
1793
+ var OAuthCredentialsSchema2 = v5.object({
1794
+ type: v5.literal("oauth"),
1795
+ refresh: v5.string(),
1796
+ access: v5.string(),
1797
+ expires: v5.number()
1798
+ });
1799
+ var UsageLimitEntrySchema2 = v5.object({
1800
+ utilization: v5.number(),
1801
+ resets_at: v5.nullable(v5.string())
1802
+ });
1803
+ var UsageLimitsSchema2 = v5.object({
1804
+ five_hour: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1805
+ seven_day: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1806
+ seven_day_sonnet: v5.optional(v5.nullable(UsageLimitEntrySchema2), null)
1807
+ });
1808
+ var CredentialRefreshPatchSchema2 = v5.object({
1809
+ accessToken: v5.string(),
1810
+ expiresAt: v5.number(),
1811
+ refreshToken: v5.optional(v5.string()),
1812
+ uuid: v5.optional(v5.string()),
1813
+ email: v5.optional(v5.string())
1814
+ });
1815
+ var StoredAccountSchema2 = v5.object({
1816
+ uuid: v5.optional(v5.string()),
1817
+ label: v5.optional(v5.string()),
1818
+ email: v5.optional(v5.string()),
1819
+ planTier: v5.optional(v5.string(), ""),
1820
+ refreshToken: v5.string(),
1821
+ accessToken: v5.optional(v5.string()),
1822
+ expiresAt: v5.optional(v5.number()),
1823
+ addedAt: v5.number(),
1824
+ lastUsed: v5.number(),
1825
+ enabled: v5.optional(v5.boolean(), true),
1826
+ rateLimitResetAt: v5.optional(v5.number()),
1827
+ cachedUsage: v5.optional(UsageLimitsSchema2),
1828
+ cachedUsageAt: v5.optional(v5.number()),
1829
+ consecutiveAuthFailures: v5.optional(v5.number(), 0),
1830
+ isAuthDisabled: v5.optional(v5.boolean(), false),
1831
+ authDisabledReason: v5.optional(v5.string())
1832
+ });
1833
+ var AccountStorageSchema2 = v5.object({
1834
+ version: v5.literal(1),
1835
+ accounts: v5.optional(v5.array(StoredAccountSchema2), []),
1836
+ activeAccountUuid: v5.optional(v5.string())
1837
+ });
1838
+ var TokenResponseSchema = v5.object({
1839
+ access_token: v5.string(),
1840
+ refresh_token: v5.optional(v5.string()),
1841
+ expires_in: v5.number(),
1842
+ account: v5.optional(v5.object({
1843
+ uuid: v5.optional(v5.string()),
1844
+ email_address: v5.optional(v5.string())
1845
+ }))
1846
+ });
1847
+ var AccountSelectionStrategySchema2 = v5.picklist(["sticky", "round-robin", "hybrid"]);
1848
+ var PluginConfigSchema2 = v5.object({
1849
+ /** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
1850
+ account_selection_strategy: v5.optional(AccountSelectionStrategySchema2, "sticky"),
1851
+ /** Use cross-process claim file to distribute parallel sessions across accounts */
1852
+ cross_process_claims: v5.optional(v5.boolean(), true),
1853
+ /** Skip account when any usage tier utilization >= this % (100 = disabled) */
1854
+ soft_quota_threshold_percent: v5.optional(v5.pipe(v5.number(), v5.minValue(0), v5.maxValue(100)), 100),
1855
+ /** Minimum backoff after rate limit (ms) */
1856
+ rate_limit_min_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
1857
+ /** Default retry-after when header is missing (ms) */
1858
+ default_retry_after_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 6e4),
1859
+ /** Consecutive auth failures before disabling account */
1860
+ max_consecutive_auth_failures: v5.optional(v5.pipe(v5.number(), v5.integer(), v5.minValue(1)), 3),
1861
+ /** Backoff after token refresh failure (ms) */
1862
+ token_failure_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
1863
+ /** Enable proactive background token refresh */
1864
+ proactive_refresh: v5.optional(v5.boolean(), true),
1865
+ /** Seconds before expiry to trigger proactive refresh (default 30 min) */
1866
+ proactive_refresh_buffer_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(60)), 1800),
1867
+ /** Interval between background refresh checks in seconds (default 5 min) */
1868
+ proactive_refresh_interval_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(30)), 300),
1869
+ /** Suppress toast notifications */
1870
+ quiet_mode: v5.optional(v5.boolean(), false),
1871
+ /** Enable debug logging */
1872
+ debug: v5.optional(v5.boolean(), false)
1873
+ });
1874
+
1875
+ // src/token.ts
1876
+ var PERMANENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1877
+ var refreshMutexByAccountId = /* @__PURE__ */ new Map();
1878
+ function isTokenExpired(account) {
1879
+ if (!account.accessToken || !account.expiresAt) return true;
1880
+ return account.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS;
1881
+ }
1882
+ async function refreshToken(currentRefreshToken, accountId, client) {
1883
+ if (!currentRefreshToken) return { ok: false, permanent: true };
1884
+ const inFlightRefresh = refreshMutexByAccountId.get(accountId);
1885
+ if (inFlightRefresh) return inFlightRefresh;
1886
+ const refreshPromise = (async () => {
1887
+ const controller = new AbortController();
1888
+ const timeout = setTimeout(() => controller.abort(), TOKEN_REFRESH_TIMEOUT_MS);
1889
+ try {
1890
+ const startTime = Date.now();
1891
+ const response = await fetch(ANTHROPIC_TOKEN_ENDPOINT, {
1892
+ method: "POST",
1893
+ headers: { "Content-Type": "application/json" },
1894
+ body: JSON.stringify({
1895
+ grant_type: "refresh_token",
1896
+ refresh_token: currentRefreshToken,
1897
+ client_id: ANTHROPIC_CLIENT_ID
1898
+ }),
1899
+ signal: controller.signal
1900
+ });
1901
+ if (!response.ok) {
1902
+ const isPermanent = PERMANENT_FAILURE_STATUSES.has(response.status);
1903
+ await client.app.log({
1904
+ body: {
1905
+ service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
1906
+ level: isPermanent ? "error" : "warn",
1907
+ message: `Token refresh failed: ${response.status}${isPermanent ? " (permanent)" : ""}`,
1908
+ extra: { accountId }
1909
+ }
1910
+ }).catch(() => {
1911
+ });
1912
+ return { ok: false, permanent: isPermanent };
1913
+ }
1914
+ const json = v6.parse(TokenResponseSchema, await response.json());
1915
+ const patch = {
1916
+ accessToken: json.access_token,
1917
+ expiresAt: startTime + json.expires_in * 1e3,
1918
+ refreshToken: json.refresh_token,
1919
+ uuid: json.account?.uuid,
1920
+ email: json.account?.email_address
1921
+ };
1922
+ return { ok: true, patch };
1923
+ } catch (error) {
1924
+ await client.app.log({
1925
+ body: {
1926
+ service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
1927
+ level: "warn",
1928
+ message: `Token refresh network error: ${error instanceof Error ? error.message : String(error)}`,
1929
+ extra: { accountId }
1930
+ }
1931
+ }).catch(() => {
1932
+ });
1933
+ return { ok: false, permanent: false };
1934
+ } finally {
1935
+ clearTimeout(timeout);
1936
+ refreshMutexByAccountId.delete(accountId);
1937
+ }
1938
+ })();
1939
+ refreshMutexByAccountId.set(accountId, refreshPromise);
1940
+ return refreshPromise;
1941
+ }
1942
+
1943
+ // src/account-manager.ts
1944
+ var AccountManager = createAccountManagerForProvider({
1945
+ providerAuthId: "anthropic",
1946
+ isTokenExpired,
1947
+ refreshToken
1948
+ });
1949
+
1950
+ // src/config.ts
1951
+ initCoreConfig("claude-multiauth.json");
1952
+
1953
+ // src/utils.ts
1954
+ setConfigGetter(getConfig);
1955
+
1956
+ // src/usage.ts
1957
+ import * as v7 from "valibot";
1958
+ var OAUTH_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.oauthBetaHeader;
1959
+ var ProfileResponseSchema = v7.object({
1960
+ account: v7.object({
1961
+ email: v7.optional(v7.string()),
1962
+ has_claude_pro: v7.optional(v7.boolean(), false),
1963
+ has_claude_max: v7.optional(v7.boolean(), false)
1964
+ })
1965
+ });
1966
+ async function fetchUsage(accessToken) {
1967
+ try {
1968
+ const response = await fetch(ANTHROPIC_USAGE_ENDPOINT, {
1969
+ method: "GET",
1970
+ headers: {
1971
+ Authorization: `Bearer ${accessToken}`,
1972
+ "anthropic-beta": OAUTH_BETA_HEADER
1973
+ }
1974
+ });
1975
+ if (!response.ok) {
1976
+ return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
1977
+ }
1978
+ const result = v7.safeParse(UsageLimitsSchema2, await response.json());
1979
+ if (!result.success) {
1980
+ return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
1981
+ }
1982
+ return { ok: true, data: result.output };
1983
+ } catch (error) {
1984
+ const message = error instanceof Error ? error.message : "Unknown error";
1985
+ return { ok: false, reason: message };
1986
+ }
1987
+ }
1988
+ async function fetchProfile(accessToken) {
1989
+ try {
1990
+ const response = await fetch(ANTHROPIC_PROFILE_ENDPOINT, {
1991
+ method: "GET",
1992
+ headers: {
1993
+ Authorization: `Bearer ${accessToken}`,
1994
+ "anthropic-beta": OAUTH_BETA_HEADER
1995
+ }
1996
+ });
1997
+ if (!response.ok) {
1998
+ return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
1999
+ }
2000
+ const result = v7.safeParse(ProfileResponseSchema, await response.json());
2001
+ if (!result.success) {
2002
+ return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2003
+ }
2004
+ const planTier = result.output.account.has_claude_max ? "max" : result.output.account.has_claude_pro ? "pro" : "free";
2005
+ return { ok: true, data: { email: result.output.account.email, planTier } };
2006
+ } catch (error) {
2007
+ const message = error instanceof Error ? error.message : "Unknown error";
2008
+ return { ok: false, reason: message };
2009
+ }
2010
+ }
2011
+ function formatTimeRemaining(resetAt) {
2012
+ if (!resetAt) return "unknown";
2013
+ const diffMs = new Date(resetAt).getTime() - Date.now();
2014
+ if (diffMs <= 0) return "0m";
2015
+ return formatWaitTime(diffMs);
2016
+ }
2017
+ function getUsageSummary(account) {
2018
+ if (!account.cachedUsage) return "no data";
2019
+ const parts = [];
2020
+ const { five_hour, seven_day } = account.cachedUsage;
2021
+ if (five_hour) {
2022
+ const reset = five_hour.resets_at ? ` (resets ${formatTimeRemaining(five_hour.resets_at)})` : "";
2023
+ parts.push(`5h: ${five_hour.utilization.toFixed(0)}%${reset}`);
2024
+ }
2025
+ if (seven_day) {
2026
+ const reset = seven_day.resets_at ? ` (resets ${formatTimeRemaining(seven_day.resets_at)})` : "";
2027
+ parts.push(`7d: ${seven_day.utilization.toFixed(0)}%${reset}`);
2028
+ }
2029
+ return parts.length > 0 ? parts.join(", ") : "no data";
2030
+ }
2031
+ function getPlanLabel(account) {
2032
+ if (!account.planTier || account.planTier === "free") return "";
2033
+ return PLAN_LABELS[account.planTier] ?? account.planTier.charAt(0).toUpperCase() + account.planTier.slice(1);
2034
+ }
2035
+
2036
+ // src/rate-limit.ts
2037
+ var {
2038
+ fetchUsageLimits,
2039
+ getResetMsFromUsage,
2040
+ handleRateLimitResponse,
2041
+ retryAfterMsFromResponse
2042
+ } = createRateLimitHandlers({
2043
+ fetchUsage: async (accessToken) => fetchUsage(accessToken),
2044
+ getConfig,
2045
+ formatWaitTime,
2046
+ getAccountLabel,
2047
+ showToast
2048
+ });
2049
+
2050
+ // src/executor.ts
2051
+ var { executeWithAccountRotation } = createExecutorForProvider("Anthropic", {
2052
+ handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
2053
+ manager,
2054
+ client,
2055
+ account,
2056
+ response
2057
+ ),
2058
+ formatWaitTime,
2059
+ sleep,
2060
+ showToast,
2061
+ getAccountLabel
2062
+ });
2063
+
2064
+ // src/ui/auth-menu.ts
2065
+ function formatRelativeTime(timestamp) {
2066
+ if (!timestamp) return "never";
2067
+ const days = Math.floor((Date.now() - timestamp) / 864e5);
2068
+ if (days === 0) return "today";
2069
+ if (days === 1) return "yesterday";
2070
+ if (days < 7) return `${days}d ago`;
2071
+ if (days < 30) return `${Math.floor(days / 7)}w ago`;
2072
+ return new Date(timestamp).toLocaleDateString();
2073
+ }
2074
+ function formatDate(timestamp) {
2075
+ if (!timestamp) return "unknown";
2076
+ return new Date(timestamp).toLocaleDateString();
2077
+ }
2078
+ function getAccountStatus(account) {
2079
+ if (account.isAuthDisabled) return "auth-disabled";
2080
+ if (!account.enabled) return "disabled";
2081
+ if (account.rateLimitResetAt && account.rateLimitResetAt > Date.now()) return "rate-limited";
2082
+ return "active";
2083
+ }
2084
+ var STATUS_BADGE = {
2085
+ "active": `${ANSI.green}[active]${ANSI.reset}`,
2086
+ "rate-limited": `${ANSI.yellow}[rate-limited]${ANSI.reset}`,
2087
+ "auth-disabled": `${ANSI.red}[auth-disabled]${ANSI.reset}`,
2088
+ "disabled": `${ANSI.red}[disabled]${ANSI.reset}`
2089
+ };
2090
+ function buildAccountMenuItem(account) {
2091
+ const label = getAccountLabel(account);
2092
+ const status = getAccountStatus(account);
2093
+ const badge = STATUS_BADGE[status];
2094
+ const fullLabel = `${label} ${badge}`;
2095
+ return {
2096
+ label: fullLabel,
2097
+ hint: account.lastUsed ? `used ${formatRelativeTime(account.lastUsed)}` : "",
2098
+ value: account,
2099
+ disabled: false
2100
+ };
2101
+ }
2102
+ async function showAuthMenu(accounts) {
2103
+ const items = [
2104
+ { label: "Add new account", value: { type: "add" }, color: "green" },
2105
+ { label: "Check quotas", value: { type: "check-quotas" }, color: "cyan" },
2106
+ { label: "Manage accounts", value: { type: "manage" } },
2107
+ { label: "Load balancing", value: { type: "load-balancing" } },
2108
+ { label: "", value: { type: "cancel" }, separator: true },
2109
+ { label: "Delete all accounts", value: { type: "delete-all" }, color: "red" }
2110
+ ];
2111
+ while (true) {
2112
+ const subtitle = `${accounts.length} account(s) registered`;
2113
+ const result = await select(items, {
2114
+ message: "Claude Multi-Auth",
2115
+ subtitle
2116
+ });
2117
+ if (!result) return { type: "cancel" };
2118
+ if (result.type === "delete-all") {
2119
+ const confirmed = await confirm("Delete ALL accounts? This cannot be undone.");
2120
+ if (!confirmed) continue;
2121
+ }
2122
+ return result;
2123
+ }
2124
+ }
2125
+ async function showManageAccounts(accounts) {
2126
+ const items = [
2127
+ { label: "Back", value: null },
2128
+ { label: "", value: null, separator: true },
2129
+ ...accounts.map(buildAccountMenuItem)
2130
+ ];
2131
+ const selected = await select(items, {
2132
+ message: "Manage Accounts",
2133
+ subtitle: "Select an account to manage"
2134
+ });
2135
+ if (!selected) return { action: "back" };
2136
+ return showAccountDetails(selected);
2137
+ }
2138
+ async function showAccountDetails(account) {
2139
+ const label = getAccountLabel(account);
2140
+ const status = getAccountStatus(account);
2141
+ const badge = STATUS_BADGE[status];
2142
+ console.log("");
2143
+ console.log(`${ANSI.bold}Account: ${label} ${badge}${ANSI.reset}`);
2144
+ console.log(`${ANSI.dim}Added: ${formatDate(account.addedAt)}${ANSI.reset}`);
2145
+ console.log(`${ANSI.dim}Last used: ${formatRelativeTime(account.lastUsed)}${ANSI.reset}`);
2146
+ if (account.isAuthDisabled) {
2147
+ console.log(`${ANSI.red}Auth disabled: ${account.authDisabledReason ?? "unknown"}${ANSI.reset}`);
2148
+ }
2149
+ console.log("");
2150
+ while (true) {
2151
+ const toggleLabel = account.enabled ? "Disable account" : "Enable account";
2152
+ const toggleColor = account.enabled ? "yellow" : "green";
2153
+ const items = [
2154
+ { label: "Back", value: "back" }
2155
+ ];
2156
+ items.push({ label: toggleLabel, value: "toggle", color: toggleColor });
2157
+ items.push({ label: "Re-authenticate", value: "retry-auth", color: "cyan" });
2158
+ items.push({ label: "Delete this account", value: "delete", color: "red" });
2159
+ const result = await select(items, {
2160
+ message: "Account options",
2161
+ subtitle: label
2162
+ });
2163
+ if (result === "delete") {
2164
+ const confirmed = await confirm(`Delete ${label}?`);
2165
+ if (!confirmed) continue;
2166
+ }
2167
+ return { action: result ?? "cancel", account };
2168
+ }
2169
+ }
2170
+ function getUsageColor(utilization) {
2171
+ if (utilization >= 90) return ANSI.red;
2172
+ if (utilization >= 60) return ANSI.yellow;
2173
+ return ANSI.green;
2174
+ }
2175
+ function createProgressBar(utilization, width = 20) {
2176
+ const filled = Math.round(utilization / 100 * width);
2177
+ const empty = width - filled;
2178
+ const color = getUsageColor(utilization);
2179
+ return `${color}${"\u2588".repeat(filled)}${ANSI.reset}${"\u2591".repeat(empty)} ${color}${Math.round(utilization)}% used${ANSI.reset}`;
2180
+ }
2181
+ function formatResetTime(resetAt) {
2182
+ if (!resetAt) return "";
2183
+ const date = new Date(resetAt);
2184
+ const now = /* @__PURE__ */ new Date();
2185
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
2186
+ const timeStr = date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit", hour12: true });
2187
+ const isSameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
2188
+ if (isSameDay) {
2189
+ return ` (resets ${timeStr}, ${tz})`;
2190
+ }
2191
+ const dateStr = date.toLocaleDateString(void 0, { month: "short", day: "numeric" });
2192
+ return ` (resets ${dateStr} ${timeStr}, ${tz})`;
2193
+ }
2194
+ function printUsageEntry(name, entry, isLast) {
2195
+ const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
2196
+ if (!entry) {
2197
+ console.log(` ${connector} ${name.padEnd(16)} no data`);
2198
+ return;
2199
+ }
2200
+ const bar = createProgressBar(entry.utilization);
2201
+ const reset = formatResetTime(entry.resets_at);
2202
+ console.log(` ${connector} ${name.padEnd(16)} ${bar}${reset}`);
2203
+ }
2204
+ function printQuotaReport(account, usage) {
2205
+ const label = getAccountLabel(account);
2206
+ const status = getAccountStatus(account);
2207
+ const badge = STATUS_BADGE[status];
2208
+ const planLabel = getPlanLabel(account) || "Free";
2209
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2210
+ console.log(` ${label} ${badge}`);
2211
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2212
+ if (account.email) {
2213
+ console.log(` \u{1F4E7} ${account.email}`);
2214
+ }
2215
+ console.log(` \u{1F4CB} ${planLabel}`);
2216
+ console.log(`
2217
+ \u2514\u2500 Claude Quota`);
2218
+ printUsageEntry("Current session", usage.five_hour, false);
2219
+ printUsageEntry("Current week", usage.seven_day, !usage.seven_day_sonnet);
2220
+ if (usage.seven_day_sonnet) {
2221
+ printUsageEntry("Sonnet only", usage.seven_day_sonnet, true);
2222
+ }
2223
+ console.log("");
2224
+ }
2225
+ var STRATEGY_DESCRIPTIONS = {
2226
+ "sticky": "Same account until rate-limited",
2227
+ "round-robin": "Rotate every request",
2228
+ "hybrid": "Score-based (usage + health)"
2229
+ };
2230
+ async function showStrategySelect(current) {
2231
+ const strategies = ["sticky", "round-robin", "hybrid"];
2232
+ const items = strategies.map((s) => ({
2233
+ label: `${s}${s === current ? " (current)" : ""}`,
2234
+ hint: STRATEGY_DESCRIPTIONS[s],
2235
+ value: s
2236
+ }));
2237
+ return select(items, {
2238
+ message: "Load Balancing Strategy",
2239
+ subtitle: `Current: ${current}`
2240
+ });
2241
+ }
2242
+ function printQuotaError(account, error) {
2243
+ const label = getAccountLabel(account);
2244
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2245
+ console.log(` ${label}`);
2246
+ if (account.email) {
2247
+ console.log(` \u{1F4E7} ${account.email}`);
2248
+ }
2249
+ console.log(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
2250
+ console.log(` ${ANSI.red}Error: ${error}${ANSI.reset}
2251
+ `);
2252
+ }
2253
+
2254
+ // src/account-store.ts
2255
+ setAccountsFilename(ACCOUNTS_FILENAME2);
2256
+
2257
+ // src/auth-handler.ts
2258
+ import { randomUUID as randomUUID2 } from "node:crypto";
2259
+ import { createInterface } from "node:readline";
2260
+ import { exec } from "node:child_process";
2261
+ function makeFailedFlowResult(message) {
2262
+ return {
2263
+ url: "",
2264
+ instructions: message,
2265
+ method: "auto",
2266
+ callback: async () => ({ type: "failed" })
2267
+ };
2268
+ }
2269
+ function delegateToOriginalAuth(originalAuth, manager, inputs) {
2270
+ const originalMethod = originalAuth.methods?.[0];
2271
+ if (!originalMethod?.authorize) {
2272
+ return Promise.resolve(makeFailedFlowResult("Original OAuth method not available"));
2273
+ }
2274
+ return originalMethod.authorize(inputs).then(
2275
+ (result) => wrapCallbackWithManagerSync(result, manager, originalAuth, inputs)
2276
+ );
2277
+ }
2278
+ function delegateReauthForAccount(originalAuth, manager, targetAccount, inputs) {
2279
+ const originalMethod = originalAuth.methods?.[0];
2280
+ if (!originalMethod?.authorize) {
2281
+ return Promise.resolve(makeFailedFlowResult("Original OAuth method not available"));
2282
+ }
2283
+ return originalMethod.authorize(inputs).then(
2284
+ (result) => wrapCallbackWithAccountReplace(result, manager, targetAccount)
2285
+ );
2286
+ }
2287
+ function wrapCallbackWithAccountReplace(result, manager, targetAccount) {
2288
+ const originalCallback = result.callback;
2289
+ return {
2290
+ ...result,
2291
+ callback: async function(code) {
2292
+ const callbackResult = await originalCallback(code);
2293
+ if (callbackResult?.type === "success" && callbackResult.refresh) {
2294
+ const auth = {
2295
+ type: "oauth",
2296
+ refresh: callbackResult.refresh,
2297
+ access: callbackResult.access,
2298
+ expires: callbackResult.expires
2299
+ };
2300
+ if (targetAccount.uuid) {
2301
+ await manager.replaceAccountCredentials(targetAccount.uuid, auth);
2302
+ }
2303
+ const label = getAccountLabel(targetAccount);
2304
+ console.log(`
2305
+ \u2705 ${label} re-authenticated successfully.
2306
+ `);
2307
+ }
2308
+ return callbackResult;
2309
+ }
2310
+ };
2311
+ }
2312
+ function wrapCallbackWithManagerSync(result, manager, originalAuth, inputs) {
2313
+ const originalCallback = result.callback;
2314
+ return {
2315
+ ...result,
2316
+ callback: async function(code) {
2317
+ const callbackResult = await originalCallback(code);
2318
+ if (callbackResult?.type === "success" && callbackResult.refresh) {
2319
+ const auth = {
2320
+ type: "oauth",
2321
+ refresh: callbackResult.refresh,
2322
+ access: callbackResult.access,
2323
+ expires: callbackResult.expires
2324
+ };
2325
+ if (manager) {
2326
+ const countBefore = manager.getAccounts().length;
2327
+ await manager.addAccount(auth);
2328
+ const countAfter = manager.getAccounts().length;
2329
+ if (countAfter > countBefore) {
2330
+ console.log(`
2331
+ \u2705 Account added to multi-auth pool (${countAfter} total).
2332
+ `);
2333
+ } else {
2334
+ console.log(`
2335
+ \u2139\uFE0F Account already exists in multi-auth pool (${countAfter} total).
2336
+ `);
2337
+ }
2338
+ if (originalAuth && inputs && isTTY()) {
2339
+ await addMoreAccountsLoop(manager, originalAuth, inputs);
2340
+ }
2341
+ } else {
2342
+ await persistFallback(auth);
2343
+ console.log(`
2344
+ \u2705 Account saved.
2345
+ `);
2346
+ }
2347
+ }
2348
+ return callbackResult;
2349
+ }
2350
+ };
2351
+ }
2352
+ function promptYesNo(message) {
2353
+ if (!isTTY()) return Promise.resolve(false);
2354
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2355
+ return new Promise((resolve) => {
2356
+ rl.question(message, (answer) => {
2357
+ rl.close();
2358
+ resolve(answer.trim().toLowerCase() === "y");
2359
+ });
2360
+ });
2361
+ }
2362
+ function openBrowser(url) {
2363
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2364
+ exec(`${cmd} ${JSON.stringify(url)}`);
2365
+ }
2366
+ async function addMoreAccountsLoop(manager, originalAuth, inputs) {
2367
+ const originalMethod = originalAuth.methods?.[0];
2368
+ if (!originalMethod?.authorize) return;
2369
+ while (true) {
2370
+ const currentCount = manager.getAccounts().length;
2371
+ const shouldAdd = await promptYesNo(`Add another account? (${currentCount} added) (y/n): `);
2372
+ if (!shouldAdd) break;
2373
+ let flow;
2374
+ try {
2375
+ flow = await originalMethod.authorize(inputs);
2376
+ } catch {
2377
+ console.log("\n\u274C Failed to start OAuth flow.\n");
2378
+ break;
2379
+ }
2380
+ if (flow.url) {
2381
+ openBrowser(flow.url);
2382
+ }
2383
+ let callbackResult;
2384
+ try {
2385
+ callbackResult = await flow.callback();
2386
+ } catch {
2387
+ console.log("\n\u274C Authentication failed.\n");
2388
+ break;
2389
+ }
2390
+ if (callbackResult?.type !== "success" || !("refresh" in callbackResult)) {
2391
+ console.log("\n\u274C Authentication failed.\n");
2392
+ break;
2393
+ }
2394
+ const auth = {
2395
+ type: "oauth",
2396
+ refresh: callbackResult.refresh,
2397
+ access: callbackResult.access,
2398
+ expires: callbackResult.expires
2399
+ };
2400
+ const countBefore = manager.getAccounts().length;
2401
+ await manager.addAccount(auth);
2402
+ const countAfter = manager.getAccounts().length;
2403
+ if (countAfter > countBefore) {
2404
+ console.log(`
2405
+ \u2705 Account added to multi-auth pool (${countAfter} total).
2406
+ `);
2407
+ } else {
2408
+ console.log(`
2409
+ \u2139\uFE0F Account already exists in multi-auth pool (${countAfter} total).
2410
+ `);
2411
+ }
2412
+ }
2413
+ }
2414
+ async function persistFallback(auth) {
2415
+ try {
2416
+ const store = new AccountStore();
2417
+ const now = Date.now();
2418
+ const account = {
2419
+ uuid: randomUUID2(),
2420
+ refreshToken: auth.refresh,
2421
+ accessToken: auth.access,
2422
+ expiresAt: auth.expires,
2423
+ addedAt: now,
2424
+ lastUsed: now,
2425
+ enabled: true,
2426
+ planTier: "",
2427
+ consecutiveAuthFailures: 0,
2428
+ isAuthDisabled: false
2429
+ };
2430
+ await store.addAccount(account);
2431
+ await store.setActiveUuid(account.uuid);
2432
+ } catch {
2433
+ }
2434
+ }
2435
+ async function handleAuthorize(originalAuth, manager, inputs, client) {
2436
+ if (!inputs || !isTTY()) {
2437
+ return delegateToOriginalAuth(originalAuth, manager, inputs);
2438
+ }
2439
+ const effectiveManager = manager ?? await loadManagerFromDisk(client);
2440
+ if (!effectiveManager || effectiveManager.getAccounts().length === 0) {
2441
+ return delegateToOriginalAuth(originalAuth, manager, inputs);
2442
+ }
2443
+ return runAccountManagementMenu(originalAuth, effectiveManager, inputs, client);
2444
+ }
2445
+ async function loadManagerFromDisk(client) {
2446
+ const store = new AccountStore();
2447
+ const stored = await store.load();
2448
+ if (stored.accounts.length === 0) return null;
2449
+ const emptyAuth = { type: "oauth", refresh: "", access: "", expires: 0 };
2450
+ const mgr = await AccountManager.create(store, emptyAuth, client);
2451
+ return mgr;
2452
+ }
2453
+ async function runAccountManagementMenu(originalAuth, manager, inputs, client) {
2454
+ while (true) {
2455
+ const allAccounts = manager.getAccounts();
2456
+ const menuAction = await showAuthMenu(allAccounts);
2457
+ switch (menuAction.type) {
2458
+ case "add":
2459
+ return delegateToOriginalAuth(originalAuth, manager, inputs);
2460
+ case "check-quotas":
2461
+ await handleCheckQuotas(manager, client);
2462
+ continue;
2463
+ case "manage": {
2464
+ const result = await showManageAccounts(allAccounts);
2465
+ if (result.action === "back" || result.action === "cancel") continue;
2466
+ const manageResult = await handleManageAction(manager, result.action, result.account, client);
2467
+ if (manageResult.triggerOAuth) {
2468
+ return delegateReauthForAccount(originalAuth, manager, manageResult.account, inputs);
2469
+ }
2470
+ continue;
2471
+ }
2472
+ case "load-balancing":
2473
+ await handleLoadBalancing();
2474
+ continue;
2475
+ case "delete-all":
2476
+ await manager.clearAllAccounts();
2477
+ console.log("\nAll accounts deleted.\n");
2478
+ return delegateToOriginalAuth(originalAuth, manager, inputs);
2479
+ case "cancel":
2480
+ return makeFailedFlowResult("Authentication cancelled");
2481
+ }
2482
+ }
2483
+ }
2484
+ async function handleCheckQuotas(manager, client) {
2485
+ await manager.refresh();
2486
+ const accounts = manager.getAccounts();
2487
+ const effectiveClient = client ?? createMinimalClient();
2488
+ if (client) manager.setClient(client);
2489
+ console.log(`
2490
+ \u{1F4CA} Checking quotas for ${accounts.length} account(s)...
2491
+ `);
2492
+ for (const account of accounts) {
2493
+ if (account.isAuthDisabled || !account.accessToken || isTokenExpired(account)) {
2494
+ if (!account.uuid) {
2495
+ printQuotaError(account, "Missing account UUID");
2496
+ continue;
2497
+ }
2498
+ const result = await manager.ensureValidToken(account.uuid, effectiveClient);
2499
+ if (!result.ok) {
2500
+ printQuotaError(account, account.isAuthDisabled ? `${account.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
2501
+ continue;
2502
+ }
2503
+ await manager.refresh();
2504
+ }
2505
+ const freshAccounts = manager.getAccounts();
2506
+ const freshAccount = freshAccounts.find((candidate) => candidate.uuid === account.uuid);
2507
+ if (!freshAccount?.accessToken) {
2508
+ printQuotaError(account, "No access token available");
2509
+ continue;
2510
+ }
2511
+ const usageResult = await fetchUsage(freshAccount.accessToken);
2512
+ if (!usageResult.ok) {
2513
+ printQuotaError(freshAccount, `Failed to fetch usage: ${usageResult.reason}`);
2514
+ continue;
2515
+ }
2516
+ if (freshAccount.uuid) {
2517
+ await manager.applyUsageCache(freshAccount.uuid, usageResult.data);
2518
+ }
2519
+ let reportAccount = freshAccount;
2520
+ const profileResult = await fetchProfile(freshAccount.accessToken);
2521
+ if (profileResult.ok) {
2522
+ if (freshAccount.uuid) {
2523
+ await manager.applyProfileCache(freshAccount.uuid, profileResult.data);
2524
+ }
2525
+ reportAccount = {
2526
+ ...freshAccount,
2527
+ email: profileResult.data.email ?? freshAccount.email,
2528
+ planTier: profileResult.data.planTier
2529
+ };
2530
+ }
2531
+ printQuotaReport(reportAccount, usageResult.data);
2532
+ }
2533
+ }
2534
+ async function handleLoadBalancing() {
2535
+ const current = getConfig().account_selection_strategy;
2536
+ const selected = await showStrategySelect(current);
2537
+ if (!selected || selected === current) return;
2538
+ await updateConfigField("account_selection_strategy", selected);
2539
+ console.log(`
2540
+ Load balancing strategy changed: ${current} \u2192 ${selected}
2541
+ `);
2542
+ }
2543
+ async function handleManageAction(manager, action, account, client) {
2544
+ if (!account) return { triggerOAuth: false };
2545
+ const label = getAccountLabel(account);
2546
+ switch (action) {
2547
+ case "toggle":
2548
+ if (!account.uuid) break;
2549
+ await manager.toggleEnabled(account.uuid);
2550
+ await manager.refresh();
2551
+ {
2552
+ const updated = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
2553
+ console.log(`
2554
+ ${label} ${updated?.enabled ? "enabled" : "disabled"}.
2555
+ `);
2556
+ }
2557
+ break;
2558
+ case "delete":
2559
+ if (!account.uuid) break;
2560
+ {
2561
+ const removed = await manager.removeAccount(account.index);
2562
+ console.log(removed ? "\nAccount deleted.\n" : "\nFailed to delete account.\n");
2563
+ }
2564
+ break;
2565
+ case "retry-auth": {
2566
+ if (!account.uuid) break;
2567
+ const effectiveClient = client ?? createMinimalClient();
2568
+ console.log(`
2569
+ Retrying authentication for ${label}...
2570
+ `);
2571
+ const result = await manager.retryAuth(account.uuid, effectiveClient);
2572
+ if (result.ok) {
2573
+ console.log(`\u2705 ${label} re-authenticated successfully.
2574
+ `);
2575
+ } else {
2576
+ console.log(`Token refresh failed \u2014 starting OAuth flow...
2577
+ `);
2578
+ return { triggerOAuth: true, account };
2579
+ }
2580
+ break;
2581
+ }
2582
+ }
2583
+ return { triggerOAuth: false };
2584
+ }
2585
+
2586
+ // src/proactive-refresh.ts
2587
+ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
2588
+ getConfig,
2589
+ isTokenExpired,
2590
+ refreshToken,
2591
+ debugLog
2592
+ });
2593
+
2594
+ // src/runtime-factory.ts
2595
+ import { AnthropicAuthPlugin } from "opencode-anthropic-auth";
2596
+ var AccountRuntimeFactory = class {
2597
+ constructor(pluginCtx, store, client, provider) {
2598
+ this.pluginCtx = pluginCtx;
2599
+ this.store = store;
2600
+ this.client = client;
2601
+ this.provider = provider;
2602
+ }
2603
+ runtimes = /* @__PURE__ */ new Map();
2604
+ initLocks = /* @__PURE__ */ new Map();
2605
+ async getRuntime(uuid) {
2606
+ const cached = this.runtimes.get(uuid);
2607
+ if (cached) return cached;
2608
+ const existing = this.initLocks.get(uuid);
2609
+ if (existing) return existing;
2610
+ const initPromise = this.createRuntime(uuid);
2611
+ this.initLocks.set(uuid, initPromise);
2612
+ try {
2613
+ const runtime = await initPromise;
2614
+ this.runtimes.set(uuid, runtime);
2615
+ return runtime;
2616
+ } finally {
2617
+ this.initLocks.delete(uuid);
2618
+ }
2619
+ }
2620
+ invalidate(uuid) {
2621
+ this.runtimes.delete(uuid);
2622
+ }
2623
+ invalidateAll() {
2624
+ this.runtimes.clear();
2625
+ }
2626
+ async createRuntime(uuid) {
2627
+ const scopedClient = this.createScopedClient(uuid);
2628
+ const scopedCtx = {
2629
+ ...this.pluginCtx,
2630
+ client: scopedClient
2631
+ };
2632
+ const hooks = await AnthropicAuthPlugin(scopedCtx);
2633
+ const auth = hooks.auth;
2634
+ if (!auth?.loader) {
2635
+ throw new Error(`Base plugin loader unavailable for account ${uuid}`);
2636
+ }
2637
+ const scopedGetAuth = this.createScopedGetAuth(uuid);
2638
+ const result = await auth.loader(scopedGetAuth, this.provider);
2639
+ if (!result?.fetch) {
2640
+ throw new Error(`Base plugin returned no fetch for account ${uuid}`);
2641
+ }
2642
+ debugLog(this.client, `Runtime created for account ${uuid.slice(0, 8)}`);
2643
+ return { fetch: result.fetch };
2644
+ }
2645
+ createScopedGetAuth(uuid) {
2646
+ const store = this.store;
2647
+ return async () => {
2648
+ const credentials = await store.readCredentials(uuid);
2649
+ if (!credentials) {
2650
+ return { type: "oauth", refresh: "", access: "", expires: 0 };
2651
+ }
2652
+ return {
2653
+ type: "oauth",
2654
+ refresh: credentials.refreshToken,
2655
+ access: credentials.accessToken ?? "",
2656
+ expires: credentials.expiresAt ?? 0
2657
+ };
2658
+ };
2659
+ }
2660
+ createScopedClient(uuid) {
2661
+ const store = this.store;
2662
+ const originalClient = this.client;
2663
+ return {
2664
+ auth: {
2665
+ async set(params) {
2666
+ const { body } = params;
2667
+ await store.mutateAccount(uuid, (account) => {
2668
+ account.accessToken = body.access;
2669
+ account.expiresAt = body.expires;
2670
+ if (body.refresh) account.refreshToken = body.refresh;
2671
+ account.consecutiveAuthFailures = 0;
2672
+ account.isAuthDisabled = false;
2673
+ account.authDisabledReason = void 0;
2674
+ });
2675
+ originalClient.auth.set(params).catch(() => {
2676
+ });
2677
+ }
2678
+ },
2679
+ tui: originalClient.tui,
2680
+ app: originalClient.app
2681
+ };
2682
+ }
2683
+ };
2684
+
2685
+ // src/index.ts
2686
+ var ClaudeMultiAuthPlugin = async (ctx) => {
2687
+ const { client } = ctx;
2688
+ await loadConfig();
2689
+ const originalHooks = await AnthropicAuthPlugin2(ctx);
2690
+ const originalAuth = originalHooks.auth;
2691
+ const store = new AccountStore();
2692
+ let manager = null;
2693
+ let runtimeFactory = null;
2694
+ let refreshQueue = null;
2695
+ return {
2696
+ "experimental.chat.system.transform": originalHooks["experimental.chat.system.transform"],
2697
+ tool: {
2698
+ [ANTHROPIC_OAUTH_ADAPTER.statusToolName]: tool({
2699
+ description: "Show status of all multi-auth accounts including rate limits and usage.",
2700
+ args: {},
2701
+ async execute(_args, _context) {
2702
+ if (!manager) {
2703
+ return "Multi-auth not initialized. No OAuth accounts detected.";
2704
+ }
2705
+ const accounts = manager.getAccounts();
2706
+ if (accounts.length === 0) {
2707
+ return "No accounts configured. Run `opencode auth login` to add an account.";
2708
+ }
2709
+ const lines = [
2710
+ `## ${ANTHROPIC_OAUTH_ADAPTER.modelDisplayName} Multi-Auth Status (${accounts.length} accounts)
2711
+ `
2712
+ ];
2713
+ for (const account of accounts) {
2714
+ const isActive = account.uuid === manager.getActiveAccount()?.uuid;
2715
+ const marker = isActive ? " **[ACTIVE]**" : "";
2716
+ const label = getAccountLabel(account);
2717
+ const usage = getUsageSummary(account);
2718
+ const planLabel = getPlanLabel(account);
2719
+ const planBadge = planLabel ? ` [${planLabel}]` : "";
2720
+ const statusParts = [];
2721
+ if (account.isAuthDisabled) statusParts.push(`AUTH DISABLED: ${account.authDisabledReason}`);
2722
+ else if (!account.enabled) statusParts.push("disabled");
2723
+ else statusParts.push("enabled");
2724
+ if (account.rateLimitResetAt && account.rateLimitResetAt > Date.now()) {
2725
+ const remaining = formatWaitTime(account.rateLimitResetAt - Date.now());
2726
+ statusParts.push(`RATE LIMITED (resets in ${remaining})`);
2727
+ }
2728
+ lines.push(
2729
+ `- **${label}**${planBadge}${marker}: ${statusParts.join(" | ")} | ${usage}`
2730
+ );
2731
+ }
2732
+ return lines.join("\n");
2733
+ }
2734
+ })
2735
+ },
2736
+ auth: {
2737
+ provider: ANTHROPIC_OAUTH_ADAPTER.authProviderId,
2738
+ methods: [
2739
+ {
2740
+ label: ANTHROPIC_OAUTH_ADAPTER.authMethodLabel,
2741
+ type: "oauth",
2742
+ async authorize() {
2743
+ const inputs = arguments.length > 0 ? arguments[0] : void 0;
2744
+ return handleAuthorize(originalAuth, manager, inputs, client);
2745
+ }
2746
+ },
2747
+ { type: "api", label: "Create an API Key" },
2748
+ { type: "api", label: "Manually enter API Key" }
2749
+ ],
2750
+ async loader(getAuth, provider) {
2751
+ const auth = await getAuth();
2752
+ if (auth.type !== "oauth") {
2753
+ return originalAuth.loader(getAuth, provider);
2754
+ }
2755
+ for (const model of Object.values(provider.models ?? {})) {
2756
+ if (model) {
2757
+ model.cost = { input: 0, output: 0, cache: { read: 0, write: 0 } };
2758
+ }
2759
+ }
2760
+ const credentials = auth;
2761
+ await migrateFromAuthJson("anthropic", store);
2762
+ manager = await AccountManager.create(store, credentials, client);
2763
+ runtimeFactory = new AccountRuntimeFactory(ctx, store, client, provider);
2764
+ manager.setRuntimeFactory(runtimeFactory);
2765
+ if (manager.getAccountCount() > 0) {
2766
+ const activeLabel = manager.getActiveAccount() ? getAccountLabel(manager.getActiveAccount()) : "none";
2767
+ void showToast(
2768
+ client,
2769
+ `Multi-Auth: ${manager.getAccountCount()} account(s) loaded. Active: ${activeLabel}`,
2770
+ "info"
2771
+ );
2772
+ await manager.validateNonActiveTokens(client);
2773
+ const disabledCount = manager.getAccounts().filter((a) => a.isAuthDisabled).length;
2774
+ if (disabledCount > 0) {
2775
+ void showToast(
2776
+ client,
2777
+ `${disabledCount} account(s) have auth failures.`,
2778
+ "warning"
2779
+ );
2780
+ }
2781
+ if (refreshQueue) {
2782
+ await refreshQueue.stop();
2783
+ }
2784
+ refreshQueue = new ProactiveRefreshQueue(
2785
+ client,
2786
+ store,
2787
+ (uuid) => runtimeFactory?.invalidate(uuid)
2788
+ );
2789
+ refreshQueue.start();
2790
+ }
2791
+ return {
2792
+ apiKey: "",
2793
+ async fetch(input, init) {
2794
+ if (!manager || !runtimeFactory) {
2795
+ return fetch(input, init);
2796
+ }
2797
+ if (manager.getAccountCount() === 0) {
2798
+ throw new Error(
2799
+ "No Anthropic accounts configured. Run `opencode auth login` to add an account."
2800
+ );
2801
+ }
2802
+ return executeWithAccountRotation(manager, runtimeFactory, client, input, init);
2803
+ }
2804
+ };
2805
+ }
2806
+ }
2807
+ };
2808
+ };
2809
+ export {
2810
+ ClaudeMultiAuthPlugin
2811
+ };