opencode-anthropic-multi-account 0.2.7 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,2188 +1,17 @@
1
1
  // src/index.ts
2
2
  import { tool } from "@opencode-ai/plugin";
3
+ import {
4
+ CascadeStateManager,
5
+ loadPoolChainConfig,
6
+ migrateFromAuthJson,
7
+ PoolManager
8
+ } from "opencode-multi-account-core";
3
9
 
4
- // ../multi-account-core/src/account-manager.ts
5
- import { randomUUID } from "node:crypto";
6
-
7
- // ../multi-account-core/src/claims.ts
8
- import { promises as fs2 } from "node:fs";
9
- import { randomBytes as randomBytes2 } from "node:crypto";
10
- import { dirname as dirname2, join as join3 } from "node:path";
11
-
12
- // ../multi-account-core/src/utils.ts
13
- import { join as join2 } from "node:path";
14
- import { homedir as homedir2 } from "node:os";
15
-
16
- // ../multi-account-core/src/config.ts
17
- import { promises as fs } from "node:fs";
18
- import { randomBytes } from "node:crypto";
19
- import { dirname, join } from "node:path";
20
- import { homedir } from "node:os";
21
- import * as v2 from "valibot";
22
-
23
- // ../multi-account-core/src/types.ts
24
- import * as v from "valibot";
25
- var OAuthCredentialsSchema = v.object({
26
- type: v.literal("oauth"),
27
- refresh: v.string(),
28
- access: v.string(),
29
- expires: v.number()
30
- });
31
- var UsageLimitEntrySchema = v.object({
32
- utilization: v.number(),
33
- resets_at: v.nullable(v.string())
34
- });
35
- var UsageLimitsSchema = v.object({
36
- five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
37
- seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
38
- seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
39
- });
40
- var CredentialRefreshPatchSchema = v.object({
41
- accessToken: v.string(),
42
- expiresAt: v.number(),
43
- refreshToken: v.optional(v.string()),
44
- uuid: v.optional(v.string()),
45
- accountId: v.optional(v.string()),
46
- email: v.optional(v.string())
47
- });
48
- var StoredAccountSchema = v.object({
49
- uuid: v.optional(v.string()),
50
- accountId: v.optional(v.string()),
51
- label: v.optional(v.string()),
52
- email: v.optional(v.string()),
53
- planTier: v.optional(v.string(), ""),
54
- refreshToken: v.string(),
55
- accessToken: v.optional(v.string()),
56
- expiresAt: v.optional(v.number()),
57
- addedAt: v.number(),
58
- lastUsed: v.number(),
59
- enabled: v.optional(v.boolean(), true),
60
- rateLimitResetAt: v.optional(v.number()),
61
- cachedUsage: v.optional(UsageLimitsSchema),
62
- cachedUsageAt: v.optional(v.number()),
63
- consecutiveAuthFailures: v.optional(v.number(), 0),
64
- isAuthDisabled: v.optional(v.boolean(), false),
65
- authDisabledReason: v.optional(v.string())
66
- });
67
- var AccountStorageSchema = v.object({
68
- version: v.literal(1),
69
- accounts: v.optional(v.array(StoredAccountSchema), []),
70
- activeAccountUuid: v.optional(v.string())
71
- });
72
- var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
73
- var PluginConfigSchema = v.object({
74
- account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
75
- cross_process_claims: v.optional(v.boolean(), true),
76
- soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
77
- rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
78
- default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
79
- max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
80
- token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
81
- proactive_refresh: v.optional(v.boolean(), true),
82
- proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
83
- proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
84
- quiet_mode: v.optional(v.boolean(), false),
85
- debug: v.optional(v.boolean(), false)
86
- });
87
- var TokenRefreshError = class _TokenRefreshError extends Error {
88
- status;
89
- permanent;
90
- constructor(permanent, status) {
91
- super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
92
- this.name = "TokenRefreshError";
93
- this.status = status;
94
- this.permanent = permanent;
95
- Object.setPrototypeOf(this, _TokenRefreshError.prototype);
96
- }
97
- };
98
- function isTokenRefreshError(error) {
99
- if (error instanceof TokenRefreshError) return true;
100
- if (!(error instanceof Error)) return false;
101
- const candidate = error;
102
- return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
103
- }
104
-
105
- // ../multi-account-core/src/config.ts
106
- var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
107
- var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
108
- var configFilename = DEFAULT_CONFIG_FILENAME;
109
- var cachedConfig = null;
110
- var externalConfigGetter = null;
111
- function getConfigDir() {
112
- return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
113
- }
114
- function getConfigPath() {
115
- return join(getConfigDir(), configFilename);
116
- }
117
- function parseConfig(raw) {
118
- const result = v2.safeParse(PluginConfigSchema, raw);
119
- return result.success ? result.output : DEFAULT_CONFIG;
120
- }
121
- function initCoreConfig(filename) {
122
- configFilename = filename || DEFAULT_CONFIG_FILENAME;
123
- cachedConfig = null;
124
- }
125
- async function loadConfig() {
126
- if (cachedConfig) return cachedConfig;
127
- const path = getConfigPath();
128
- try {
129
- const content = await fs.readFile(path, "utf-8");
130
- cachedConfig = parseConfig(JSON.parse(content));
131
- } catch {
132
- cachedConfig = DEFAULT_CONFIG;
133
- }
134
- return cachedConfig;
135
- }
136
- function getConfig() {
137
- if (cachedConfig) return cachedConfig;
138
- if (externalConfigGetter && externalConfigGetter !== getConfig) {
139
- try {
140
- return parseConfig(externalConfigGetter());
141
- } catch {
142
- return DEFAULT_CONFIG;
143
- }
144
- }
145
- return DEFAULT_CONFIG;
146
- }
147
- function setConfigGetter(getter) {
148
- if (getter === getConfig) {
149
- return;
150
- }
151
- externalConfigGetter = getter;
152
- }
153
- async function updateConfigField(key, value) {
154
- const path = getConfigPath();
155
- let existing = {};
156
- try {
157
- const content2 = await fs.readFile(path, "utf-8");
158
- existing = JSON.parse(content2);
159
- } catch {
160
- }
161
- existing[key] = value;
162
- await fs.mkdir(dirname(path), { recursive: true });
163
- const content = `${JSON.stringify(existing, null, 2)}
164
- `;
165
- const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
166
- try {
167
- await fs.writeFile(tempPath, content, "utf-8");
168
- await fs.rename(tempPath, path);
169
- } catch (error) {
170
- try {
171
- await fs.unlink(tempPath);
172
- } catch {
173
- }
174
- throw error;
175
- }
176
- cachedConfig = null;
177
- await loadConfig();
178
- }
179
-
180
- // ../multi-account-core/src/utils.ts
181
- function getConfigDir2() {
182
- return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
183
- }
184
- function getErrorCode(error) {
185
- if (typeof error !== "object" || error === null || !("code" in error)) {
186
- return void 0;
187
- }
188
- const code = error.code;
189
- return typeof code === "string" ? code : void 0;
190
- }
191
- function formatWaitTime(ms) {
192
- const totalSeconds = Math.ceil(ms / 1e3);
193
- if (totalSeconds < 60) return `${totalSeconds}s`;
194
- const days = Math.floor(totalSeconds / 86400);
195
- const hours = Math.floor(totalSeconds % 86400 / 3600);
196
- const minutes = Math.floor(totalSeconds % 3600 / 60);
197
- const seconds = totalSeconds % 60;
198
- const parts = [];
199
- if (days > 0) parts.push(`${days}d`);
200
- if (hours > 0) parts.push(`${hours}h`);
201
- if (minutes > 0) parts.push(`${minutes}m`);
202
- if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
203
- return parts.join(" ") || "0s";
204
- }
205
- function getAccountLabel(account) {
206
- if (account.label) return account.label;
207
- if (account.email) return account.email;
208
- if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
209
- return `Account ${account.index + 1}`;
210
- }
211
- function sleep(ms) {
212
- return new Promise((resolve) => setTimeout(resolve, ms));
213
- }
214
- async function showToast(client, message, variant) {
215
- if (getConfig().quiet_mode) return;
216
- try {
217
- await client.tui.showToast({ body: { message, variant } });
218
- } catch {
219
- }
220
- }
221
- function debugLog(client, message, extra) {
222
- if (!getConfig().debug) return;
223
- client.app.log({
224
- body: { service: "claude-multiauth", level: "debug", message, extra }
225
- }).catch(() => {
226
- });
227
- }
228
- function createMinimalClient() {
229
- return {
230
- auth: {
231
- set: async () => {
232
- }
233
- },
234
- tui: {
235
- showToast: async () => {
236
- }
237
- },
238
- app: {
239
- log: async () => {
240
- }
241
- }
242
- };
243
- }
244
- function getClearedOAuthBody() {
245
- return {
246
- type: "oauth",
247
- refresh: "",
248
- access: "",
249
- expires: 0
250
- };
251
- }
252
-
253
- // ../multi-account-core/src/claims.ts
254
- var CLAIMS_FILENAME = "multiauth-claims.json";
255
- var CLAIM_EXPIRY_MS = 6e4;
256
- function getClaimsPath() {
257
- return join3(getConfigDir2(), CLAIMS_FILENAME);
258
- }
259
- function isClaimShape(value) {
260
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
261
- const claim = value;
262
- return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
263
- }
264
- function parseClaims(raw) {
265
- let parsed;
266
- try {
267
- parsed = JSON.parse(raw);
268
- } catch {
269
- return {};
270
- }
271
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
272
- return {};
273
- }
274
- const claims = {};
275
- for (const [accountId, claim] of Object.entries(parsed)) {
276
- if (isClaimShape(claim)) {
277
- claims[accountId] = claim;
278
- }
279
- }
280
- return claims;
281
- }
282
- function isProcessAlive(pid) {
283
- try {
284
- process.kill(pid, 0);
285
- return true;
286
- } catch {
287
- return false;
288
- }
289
- }
290
- function cleanClaims(claims, now) {
291
- const cleaned = {};
292
- let changed = false;
293
- for (const [accountId, claim] of Object.entries(claims)) {
294
- const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
295
- const zombieClaim = !isProcessAlive(claim.pid);
296
- if (expiredByTime || zombieClaim) {
297
- changed = true;
298
- continue;
299
- }
300
- cleaned[accountId] = claim;
301
- }
302
- return { cleaned, changed };
303
- }
304
- async function writeClaimsFile(claims) {
305
- const path = getClaimsPath();
306
- const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
307
- await fs2.mkdir(dirname2(path), { recursive: true });
308
- try {
309
- await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
310
- await fs2.rename(tempPath, path);
311
- } catch (error) {
312
- try {
313
- await fs2.unlink(tempPath);
314
- } catch {
315
- }
316
- throw error;
317
- }
318
- }
319
- async function readClaims() {
320
- try {
321
- const data = await fs2.readFile(getClaimsPath(), "utf-8");
322
- const parsed = parseClaims(data);
323
- const now = Date.now();
324
- const { cleaned, changed } = cleanClaims(parsed, now);
325
- if (changed) {
326
- try {
327
- await writeClaimsFile(cleaned);
328
- } catch {
329
- }
330
- }
331
- return cleaned;
332
- } catch {
333
- return {};
334
- }
335
- }
336
- async function writeClaim(accountId) {
337
- const now = Date.now();
338
- const claims = await readClaims();
339
- const { cleaned } = cleanClaims(claims, now);
340
- cleaned[accountId] = { pid: process.pid, at: now };
341
- try {
342
- await writeClaimsFile(cleaned);
343
- } catch {
344
- }
345
- }
346
- function isClaimedByOther(claims, accountId) {
347
- if (!accountId) return false;
348
- const claim = claims[accountId];
349
- if (!claim) return false;
350
- if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
351
- if (!isProcessAlive(claim.pid)) return false;
352
- return claim.pid !== process.pid;
353
- }
354
-
355
- // ../multi-account-core/src/account-manager.ts
356
- var STARTUP_REFRESH_CONCURRENCY = 3;
357
- var RECENT_429_COOLDOWN_MS = 3e4;
358
- var HYBRID_SWITCH_MARGIN = 40;
359
- function createAccountManagerForProvider(dependencies) {
360
- const {
361
- providerAuthId,
362
- isTokenExpired: isTokenExpired2,
363
- refreshToken: refreshToken2
364
- } = dependencies;
365
- return class AccountManager2 {
366
- constructor(store) {
367
- this.store = store;
368
- }
369
- cached = [];
370
- activeAccountUuid;
371
- client = null;
372
- runtimeFactory = null;
373
- roundRobinCursor = 0;
374
- last429Map = /* @__PURE__ */ new Map();
375
- static async create(store, currentAuth, client) {
376
- const manager = new AccountManager2(store);
377
- await manager.initialize(currentAuth, client);
378
- return manager;
379
- }
380
- async initialize(currentAuth, client) {
381
- if (client) this.client = client;
382
- const storage = await this.store.load();
383
- if (storage.accounts.length > 0) {
384
- this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
385
- this.activeAccountUuid = storage.activeAccountUuid;
386
- if (!this.getActiveAccount() && this.cached.length > 0) {
387
- this.activeAccountUuid = this.cached[0].uuid;
388
- }
389
- return;
390
- }
391
- if (currentAuth.refresh) {
392
- const newAccount = this.createNewAccount(currentAuth, Date.now());
393
- await this.store.addAccount(newAccount);
394
- await this.store.setActiveUuid(newAccount.uuid);
395
- this.cached = [this.toManagedAccount(newAccount, 0)];
396
- this.activeAccountUuid = newAccount.uuid;
397
- }
398
- }
399
- async refresh() {
400
- const storage = await this.store.load();
401
- this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
402
- if (storage.activeAccountUuid) {
403
- this.activeAccountUuid = storage.activeAccountUuid;
404
- }
405
- }
406
- toManagedAccount(storedAccount, index) {
407
- return {
408
- index,
409
- uuid: storedAccount.uuid,
410
- accountId: storedAccount.accountId,
411
- label: storedAccount.label,
412
- email: storedAccount.email,
413
- planTier: storedAccount.planTier,
414
- refreshToken: storedAccount.refreshToken,
415
- accessToken: storedAccount.accessToken,
416
- expiresAt: storedAccount.expiresAt,
417
- addedAt: storedAccount.addedAt,
418
- lastUsed: storedAccount.lastUsed,
419
- enabled: storedAccount.enabled,
420
- rateLimitResetAt: storedAccount.rateLimitResetAt,
421
- cachedUsage: storedAccount.cachedUsage,
422
- cachedUsageAt: storedAccount.cachedUsageAt,
423
- consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
424
- isAuthDisabled: storedAccount.isAuthDisabled,
425
- authDisabledReason: storedAccount.authDisabledReason,
426
- last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
427
- };
428
- }
429
- createNewAccount(auth, now) {
430
- return {
431
- uuid: randomUUID(),
432
- refreshToken: auth.refresh,
433
- accessToken: auth.access,
434
- expiresAt: auth.expires,
435
- addedAt: now,
436
- lastUsed: now,
437
- enabled: true,
438
- planTier: "",
439
- consecutiveAuthFailures: 0,
440
- isAuthDisabled: false
441
- };
442
- }
443
- getAccountCount() {
444
- return this.getEligibleAccounts().length;
445
- }
446
- getAccounts() {
447
- return [...this.cached];
448
- }
449
- getActiveAccount() {
450
- if (this.activeAccountUuid) {
451
- return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
452
- }
453
- return this.cached[0] ?? null;
454
- }
455
- setClient(client) {
456
- this.client = client;
457
- }
458
- setRuntimeFactory(factory) {
459
- this.runtimeFactory = factory;
460
- }
461
- getEligibleAccounts() {
462
- return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
463
- }
464
- exceedsSoftQuota(account) {
465
- const threshold = getConfig().soft_quota_threshold_percent;
466
- if (threshold >= 100) return false;
467
- const usage = account.cachedUsage;
468
- if (!usage) return false;
469
- const tiers = [usage.five_hour, usage.seven_day];
470
- return tiers.some((tier) => tier != null && tier.utilization >= threshold);
471
- }
472
- hasAnyUsableAccount() {
473
- return this.getEligibleAccounts().length > 0;
474
- }
475
- isRateLimited(account) {
476
- if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
477
- return true;
478
- }
479
- return this.isUsageExhausted(account);
480
- }
481
- isUsageExhausted(account) {
482
- const usage = account.cachedUsage;
483
- if (!usage) return false;
484
- const now = Date.now();
485
- const tiers = [usage.five_hour, usage.seven_day];
486
- return tiers.some(
487
- (tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
488
- );
489
- }
490
- clearExpiredRateLimits() {
491
- const now = Date.now();
492
- for (const account of this.cached) {
493
- if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
494
- account.rateLimitResetAt = void 0;
495
- }
496
- }
497
- }
498
- getMinWaitTime() {
499
- const eligible = this.getEligibleAccounts();
500
- const available = eligible.filter((account) => !this.isRateLimited(account));
501
- if (available.length > 0) return 0;
502
- const now = Date.now();
503
- const waits = [];
504
- for (const account of eligible) {
505
- if (account.rateLimitResetAt) {
506
- const ms = account.rateLimitResetAt - now;
507
- if (ms > 0) waits.push(ms);
508
- }
509
- const usageResetMs = this.getUsageResetMs(account);
510
- if (usageResetMs !== null && usageResetMs > 0) {
511
- waits.push(usageResetMs);
512
- }
513
- }
514
- return waits.length > 0 ? Math.min(...waits) : 0;
515
- }
516
- getUsageResetMs(account) {
517
- const usage = account.cachedUsage;
518
- if (!usage) return null;
519
- const now = Date.now();
520
- const candidates = [];
521
- const tiers = [usage.five_hour, usage.seven_day];
522
- for (const tier of tiers) {
523
- if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
524
- const ms = Date.parse(tier.resets_at) - now;
525
- if (ms > 0) candidates.push(ms);
526
- }
527
- }
528
- return candidates.length > 0 ? Math.min(...candidates) : null;
529
- }
530
- async selectAccount() {
531
- await this.refresh();
532
- this.clearExpiredRateLimits();
533
- const eligible = this.getEligibleAccounts();
534
- if (eligible.length === 0) return null;
535
- const config = getConfig();
536
- const claims = config.cross_process_claims ? await readClaims() : {};
537
- const strategy = config.account_selection_strategy;
538
- let selected;
539
- switch (strategy) {
540
- case "round-robin":
541
- selected = this.selectRoundRobin(eligible, claims);
542
- break;
543
- case "hybrid":
544
- selected = this.selectHybrid(eligible, claims);
545
- break;
546
- case "sticky":
547
- default:
548
- selected = this.selectSticky(eligible, claims);
549
- break;
550
- }
551
- if (selected?.uuid) {
552
- this.activeAccountUuid = selected.uuid;
553
- this.store.setActiveUuid(selected.uuid).catch(() => {
554
- });
555
- }
556
- if (config.cross_process_claims && selected?.uuid) {
557
- writeClaim(selected.uuid).catch(() => {
558
- });
559
- }
560
- return selected;
561
- }
562
- isUsable(account) {
563
- return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
564
- }
565
- isInRecentCooldown(account) {
566
- if (!account.last429At) return false;
567
- return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
568
- }
569
- fallbackNotRateLimited(eligible) {
570
- const account = eligible.find((candidate) => !this.isRateLimited(candidate));
571
- if (account) {
572
- this.activateAccount(account);
573
- return account;
574
- }
575
- return null;
576
- }
577
- selectSticky(eligible, claims) {
578
- const current = this.getActiveAccount();
579
- if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
580
- this.activateAccount(current);
581
- return current;
582
- }
583
- const unclaimed = eligible.find(
584
- (account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
585
- );
586
- if (unclaimed) {
587
- this.activateAccount(unclaimed);
588
- return unclaimed;
589
- }
590
- const available = eligible.find((account) => this.isUsable(account));
591
- if (available) {
592
- this.activateAccount(available);
593
- return available;
594
- }
595
- return this.fallbackNotRateLimited(eligible);
596
- }
597
- selectRoundRobin(eligible, claims) {
598
- for (let i = 0; i < eligible.length; i++) {
599
- const index = (this.roundRobinCursor + i) % eligible.length;
600
- const account = eligible[index];
601
- if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
602
- this.roundRobinCursor = (index + 1) % eligible.length;
603
- this.activateAccount(account);
604
- return account;
605
- }
606
- }
607
- for (let i = 0; i < eligible.length; i++) {
608
- const index = (this.roundRobinCursor + i) % eligible.length;
609
- const account = eligible[index];
610
- if (this.isUsable(account)) {
611
- this.roundRobinCursor = (index + 1) % eligible.length;
612
- this.activateAccount(account);
613
- return account;
614
- }
615
- }
616
- return this.fallbackNotRateLimited(eligible);
617
- }
618
- selectHybrid(eligible, claims) {
619
- const usable = eligible.filter((account) => this.isUsable(account));
620
- const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
621
- if (pool.length === 0) return null;
622
- const activeUuid = this.activeAccountUuid;
623
- let best = pool[0];
624
- let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
625
- for (let i = 1; i < pool.length; i++) {
626
- const account = pool[i];
627
- const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
628
- if (score > bestScore) {
629
- best = account;
630
- bestScore = score;
631
- }
632
- }
633
- const current = pool.find((account) => account.uuid === activeUuid);
634
- if (current && current !== best) {
635
- const currentScore = this.calculateHybridScore(current, true, claims);
636
- const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
637
- if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
638
- this.activateAccount(current);
639
- return current;
640
- }
641
- }
642
- this.activateAccount(best);
643
- return best;
644
- }
645
- calculateHybridScore(account, isActive, claims) {
646
- const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
647
- const usageScore = (100 - maxUtilization) / 100 * 450;
648
- const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
649
- const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
650
- const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
651
- const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
652
- const stickinessBonus = isActive ? 120 : 0;
653
- const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
654
- return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
655
- }
656
- getMaxUtilization(account) {
657
- const usage = account.cachedUsage;
658
- if (!usage) return 65;
659
- const tiers = [usage.five_hour, usage.seven_day];
660
- const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
661
- return utilizations.length > 0 ? Math.max(...utilizations) : 65;
662
- }
663
- activateAccount(account) {
664
- this.activeAccountUuid = account.uuid;
665
- account.lastUsed = Date.now();
666
- }
667
- async markRateLimited(uuid, backoffMs) {
668
- const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
669
- this.last429Map.set(uuid, Date.now());
670
- await this.store.mutateAccount(uuid, (account) => {
671
- account.rateLimitResetAt = Date.now() + effectiveBackoff;
672
- });
673
- }
674
- async markRevoked(uuid) {
675
- await this.removeAccountByUuid(uuid);
676
- }
677
- async markSuccess(uuid) {
678
- this.last429Map.delete(uuid);
679
- await this.store.mutateAccount(uuid, (account) => {
680
- account.rateLimitResetAt = void 0;
681
- account.consecutiveAuthFailures = 0;
682
- account.lastUsed = Date.now();
683
- });
684
- }
685
- syncToOpenCode(account) {
686
- if (!this.client || !account.accessToken || !account.expiresAt) return;
687
- this.client.auth.set({
688
- path: { id: providerAuthId },
689
- body: {
690
- type: "oauth",
691
- refresh: account.refreshToken,
692
- access: account.accessToken,
693
- expires: account.expiresAt
694
- }
695
- }).catch(() => {
696
- });
697
- }
698
- async clearOpenCodeAuthIfNoAccountsRemain() {
699
- if (!this.client) return;
700
- const storage = await this.store.load();
701
- if (storage.accounts.length > 0) return;
702
- await this.client.auth.set({
703
- path: { id: providerAuthId },
704
- body: getClearedOAuthBody()
705
- }).catch(() => {
706
- });
707
- }
708
- async removeAccountByUuid(uuid) {
709
- const removed = await this.store.removeAccount(uuid);
710
- if (!removed) return;
711
- this.last429Map.delete(uuid);
712
- this.runtimeFactory?.invalidate(uuid);
713
- await this.refresh();
714
- await this.clearOpenCodeAuthIfNoAccountsRemain();
715
- }
716
- async markAuthFailure(uuid, result) {
717
- if (!result.ok && result.permanent) {
718
- await this.removeAccountByUuid(uuid);
719
- return;
720
- }
721
- await this.store.mutateStorage((storage) => {
722
- const account = storage.accounts.find((entry) => entry.uuid === uuid);
723
- if (!account) return;
724
- account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
725
- const maxFailures = getConfig().max_consecutive_auth_failures;
726
- const usableCount = storage.accounts.filter(
727
- (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
728
- ).length;
729
- if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
730
- account.isAuthDisabled = true;
731
- account.authDisabledReason = `${maxFailures} consecutive auth failures`;
732
- }
733
- });
734
- }
735
- async applyUsageCache(uuid, usage) {
736
- await this.store.mutateAccount(uuid, (account) => {
737
- const now = Date.now();
738
- const exhaustedTierResetTimes = [usage.five_hour, usage.seven_day].flatMap((tier) => {
739
- if (tier == null || tier.utilization < 100 || tier.resets_at == null) {
740
- return [];
741
- }
742
- return [Date.parse(tier.resets_at)];
743
- }).filter((resetAt) => Number.isFinite(resetAt) && resetAt > now);
744
- account.cachedUsage = usage;
745
- account.cachedUsageAt = Date.now();
746
- account.rateLimitResetAt = exhaustedTierResetTimes.length > 0 ? Math.min(...exhaustedTierResetTimes) : void 0;
747
- });
748
- }
749
- async applyProfileCache(uuid, profile) {
750
- await this.store.mutateAccount(uuid, (account) => {
751
- account.email = profile.email ?? account.email;
752
- account.planTier = profile.planTier;
753
- });
754
- }
755
- async ensureValidToken(uuid, client) {
756
- const credentials = await this.store.readCredentials(uuid);
757
- if (!credentials) return { ok: false, permanent: true };
758
- if (credentials.accessToken && credentials.expiresAt && !isTokenExpired2(credentials)) {
759
- return {
760
- ok: true,
761
- patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
762
- };
763
- }
764
- const result = await refreshToken2(credentials.refreshToken, uuid, client);
765
- if (!result.ok) return result;
766
- const updated = await this.store.mutateAccount(uuid, (account) => {
767
- account.accessToken = result.patch.accessToken;
768
- account.expiresAt = result.patch.expiresAt;
769
- if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
770
- if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
771
- if (result.patch.accountId) account.accountId = result.patch.accountId;
772
- if (result.patch.email) account.email = result.patch.email;
773
- account.consecutiveAuthFailures = 0;
774
- account.isAuthDisabled = false;
775
- account.authDisabledReason = void 0;
776
- });
777
- if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
778
- this.activeAccountUuid = result.patch.uuid;
779
- this.store.setActiveUuid(result.patch.uuid).catch(() => {
780
- });
781
- }
782
- if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
783
- this.syncToOpenCode(updated);
784
- }
785
- return result;
786
- }
787
- async validateNonActiveTokens(client) {
788
- await this.refresh();
789
- const activeUuid = this.activeAccountUuid;
790
- const eligible = this.cached.filter(
791
- (account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
792
- );
793
- for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
794
- const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
795
- await Promise.all(
796
- batch.map(async (account) => {
797
- if (!account.uuid || !isTokenExpired2(account)) return;
798
- const result = await this.ensureValidToken(account.uuid, client);
799
- if (!result.ok) {
800
- await this.markAuthFailure(account.uuid, result);
801
- }
802
- })
803
- );
804
- }
805
- }
806
- async removeAccount(index) {
807
- const account = this.cached[index];
808
- if (!account?.uuid) return false;
809
- const removed = await this.store.removeAccount(account.uuid);
810
- if (removed) {
811
- await this.refresh();
812
- }
813
- return removed;
814
- }
815
- async clearAllAccounts() {
816
- await this.store.clear();
817
- this.cached = [];
818
- this.activeAccountUuid = void 0;
819
- }
820
- async addAccount(auth, email) {
821
- if (!auth.refresh) return;
822
- const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
823
- if (existingByToken) return;
824
- if (email) {
825
- const existingByEmail = this.cached.find(
826
- (account) => account.email && account.email === email
827
- );
828
- if (existingByEmail?.uuid) {
829
- await this.replaceAccountCredentials(existingByEmail.uuid, auth);
830
- return;
831
- }
832
- }
833
- const newAccount = this.createNewAccount(auth, Date.now());
834
- if (email) newAccount.email = email;
835
- await this.store.addAccount(newAccount);
836
- this.activeAccountUuid = newAccount.uuid;
837
- await this.store.setActiveUuid(newAccount.uuid);
838
- await this.refresh();
839
- }
840
- async toggleEnabled(uuid) {
841
- await this.store.mutateAccount(uuid, (account) => {
842
- account.enabled = !(account.enabled ?? true);
843
- if (account.enabled) {
844
- account.isAuthDisabled = false;
845
- account.authDisabledReason = void 0;
846
- account.consecutiveAuthFailures = 0;
847
- }
848
- });
849
- }
850
- async replaceAccountCredentials(uuid, auth) {
851
- const updated = await this.store.mutateAccount(uuid, (account) => {
852
- account.refreshToken = auth.refresh;
853
- account.accessToken = auth.access;
854
- account.expiresAt = auth.expires;
855
- account.lastUsed = Date.now();
856
- account.enabled = true;
857
- account.isAuthDisabled = false;
858
- account.authDisabledReason = void 0;
859
- account.consecutiveAuthFailures = 0;
860
- account.rateLimitResetAt = void 0;
861
- });
862
- this.runtimeFactory?.invalidate(uuid);
863
- if (updated && uuid === this.activeAccountUuid) {
864
- this.syncToOpenCode(updated);
865
- }
866
- }
867
- async retryAuth(uuid, client) {
868
- await this.store.mutateAccount(uuid, (account) => {
869
- account.consecutiveAuthFailures = 0;
870
- account.isAuthDisabled = false;
871
- account.authDisabledReason = void 0;
872
- });
873
- this.runtimeFactory?.invalidate(uuid);
874
- const credentials = await this.store.readCredentials(uuid);
875
- if (!credentials) return { ok: false, permanent: true };
876
- const result = await refreshToken2(credentials.refreshToken, uuid, client);
877
- if (result.ok) {
878
- const updated = await this.store.mutateAccount(uuid, (account) => {
879
- account.accessToken = result.patch.accessToken;
880
- account.expiresAt = result.patch.expiresAt;
881
- if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
882
- if (result.patch.uuid) account.uuid = result.patch.uuid;
883
- if (result.patch.accountId) account.accountId = result.patch.accountId;
884
- if (result.patch.email) account.email = result.patch.email;
885
- account.enabled = true;
886
- account.consecutiveAuthFailures = 0;
887
- });
888
- this.runtimeFactory?.invalidate(uuid);
889
- if (result.patch.uuid) {
890
- this.runtimeFactory?.invalidate(result.patch.uuid);
891
- }
892
- const nextUuid = result.patch.uuid ?? uuid;
893
- if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
894
- this.activeAccountUuid = result.patch.uuid;
895
- await this.store.setActiveUuid(result.patch.uuid);
896
- }
897
- if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
898
- const freshCredentials = await this.store.readCredentials(nextUuid);
899
- if (freshCredentials) {
900
- this.syncToOpenCode({
901
- refreshToken: freshCredentials.refreshToken,
902
- accessToken: freshCredentials.accessToken,
903
- expiresAt: freshCredentials.expiresAt
904
- });
905
- }
906
- }
907
- } else {
908
- await this.markAuthFailure(uuid, result);
909
- this.runtimeFactory?.invalidate(uuid);
910
- }
911
- return result;
912
- }
913
- };
914
- }
915
-
916
- // ../multi-account-core/src/account-store.ts
917
- import { promises as fs4 } from "node:fs";
918
- import { randomBytes as randomBytes3 } from "node:crypto";
919
- import { dirname as dirname4, join as join5 } from "node:path";
920
- import lockfile from "proper-lockfile";
921
- import * as v4 from "valibot";
922
-
923
- // ../multi-account-core/src/storage.ts
924
- import { promises as fs3 } from "node:fs";
925
- import { dirname as dirname3, join as join4 } from "node:path";
926
- import * as v3 from "valibot";
927
-
928
- // ../multi-account-core/src/constants.ts
929
- var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
930
- var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
931
- function setAccountsFilename(filename) {
932
- if (!filename) {
933
- ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
934
- return;
935
- }
936
- ACCOUNTS_FILENAME = filename;
937
- }
938
-
939
- // ../multi-account-core/src/storage.ts
940
- function getStoragePath() {
941
- return join4(getConfigDir2(), ACCOUNTS_FILENAME);
942
- }
943
- async function backupCorruptFile(targetPath, content) {
944
- const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
945
- await fs3.mkdir(dirname3(backupPath), { recursive: true });
946
- await fs3.writeFile(backupPath, content, "utf-8");
947
- }
948
- async function readStorageFromDisk(targetPath, backupOnCorrupt) {
949
- let content;
950
- try {
951
- content = await fs3.readFile(targetPath, "utf-8");
952
- } catch (error) {
953
- if (getErrorCode(error) === "ENOENT") {
954
- return null;
955
- }
956
- throw error;
957
- }
958
- let parsed;
959
- try {
960
- parsed = JSON.parse(content);
961
- } catch {
962
- if (backupOnCorrupt) {
963
- try {
964
- await backupCorruptFile(targetPath, content);
965
- } catch {
966
- }
967
- }
968
- return null;
969
- }
970
- const validation = v3.safeParse(AccountStorageSchema, parsed);
971
- if (!validation.success) {
972
- if (backupOnCorrupt) {
973
- try {
974
- await backupCorruptFile(targetPath, content);
975
- } catch {
976
- }
977
- }
978
- return null;
979
- }
980
- return validation.output;
981
- }
982
- function deduplicateAccounts(accounts) {
983
- const deduplicated = [];
984
- const indexByUuid = /* @__PURE__ */ new Map();
985
- for (const account of accounts) {
986
- if (!account.uuid) {
987
- deduplicated.push(account);
988
- continue;
989
- }
990
- const existingIndex = indexByUuid.get(account.uuid);
991
- if (existingIndex === void 0) {
992
- indexByUuid.set(account.uuid, deduplicated.length);
993
- deduplicated.push(account);
994
- continue;
995
- }
996
- const existingAccount = deduplicated[existingIndex];
997
- if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
998
- deduplicated[existingIndex] = account;
999
- }
1000
- }
1001
- return deduplicated;
1002
- }
1003
- async function loadAccounts() {
1004
- const storagePath = getStoragePath();
1005
- const storage = await readStorageFromDisk(storagePath, true);
1006
- if (!storage) {
1007
- return null;
1008
- }
1009
- return {
1010
- ...storage,
1011
- accounts: deduplicateAccounts(storage.accounts || [])
1012
- };
1013
- }
1014
-
1015
- // ../multi-account-core/src/account-store.ts
1016
- var FILE_MODE = 384;
1017
- var LOCK_OPTIONS = {
1018
- stale: 1e4,
1019
- retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
1020
- };
1021
- function getStoragePath2() {
1022
- return join5(getConfigDir2(), ACCOUNTS_FILENAME);
1023
- }
1024
- function createEmptyStorage() {
1025
- return { version: 1, accounts: [] };
1026
- }
1027
- function buildTempPath(targetPath) {
1028
- return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
1029
- }
1030
- async function writeAtomicText(targetPath, content) {
1031
- await fs4.mkdir(dirname4(targetPath), { recursive: true });
1032
- const tempPath = buildTempPath(targetPath);
1033
- try {
1034
- await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
1035
- await fs4.chmod(tempPath, FILE_MODE);
1036
- await fs4.rename(tempPath, targetPath);
1037
- await fs4.chmod(targetPath, FILE_MODE);
1038
- } catch (error) {
1039
- try {
1040
- await fs4.unlink(tempPath);
1041
- } catch {
1042
- }
1043
- throw error;
1044
- }
1045
- }
1046
- async function writeStorageAtomic(targetPath, storage) {
1047
- const validation = v4.safeParse(AccountStorageSchema, storage);
1048
- if (!validation.success) {
1049
- throw new Error("Invalid account storage payload");
1050
- }
1051
- await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
1052
- `);
1053
- }
1054
- async function ensureStorageFileExists(targetPath) {
1055
- await fs4.mkdir(dirname4(targetPath), { recursive: true });
1056
- const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
1057
- `;
1058
- try {
1059
- await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
1060
- } catch (error) {
1061
- if (getErrorCode(error) !== "EEXIST") throw error;
1062
- }
1063
- }
1064
- async function withFileLock(fn) {
1065
- const storagePath = getStoragePath2();
1066
- await ensureStorageFileExists(storagePath);
1067
- let release = null;
1068
- try {
1069
- release = await lockfile.lock(storagePath, LOCK_OPTIONS);
1070
- return await fn(storagePath);
1071
- } finally {
1072
- if (release) {
1073
- try {
1074
- await release();
1075
- } catch {
1076
- }
1077
- }
1078
- }
1079
- }
1080
- var AccountStore = class {
1081
- async load() {
1082
- const storage = await loadAccounts();
1083
- return storage ?? createEmptyStorage();
1084
- }
1085
- async readCredentials(uuid) {
1086
- const storagePath = getStoragePath2();
1087
- const storage = await readStorageFromDisk(storagePath, false);
1088
- if (!storage) return null;
1089
- const account = storage.accounts.find((a) => a.uuid === uuid);
1090
- if (!account) return null;
1091
- return {
1092
- refreshToken: account.refreshToken,
1093
- accessToken: account.accessToken,
1094
- expiresAt: account.expiresAt,
1095
- accountId: account.accountId
1096
- };
1097
- }
1098
- async mutateAccount(uuid, fn) {
1099
- return await withFileLock(async (storagePath) => {
1100
- const current = await readStorageFromDisk(storagePath, false);
1101
- if (!current) return null;
1102
- const account = current.accounts.find((a) => a.uuid === uuid);
1103
- if (!account) return null;
1104
- fn(account);
1105
- await writeStorageAtomic(storagePath, current);
1106
- return { ...account };
1107
- });
1108
- }
1109
- async mutateStorage(fn) {
1110
- await withFileLock(async (storagePath) => {
1111
- const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1112
- fn(current);
1113
- await writeStorageAtomic(storagePath, current);
1114
- });
1115
- }
1116
- async addAccount(account) {
1117
- await withFileLock(async (storagePath) => {
1118
- const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1119
- const exists = current.accounts.some(
1120
- (a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
1121
- );
1122
- if (exists) return;
1123
- current.accounts.push(account);
1124
- await writeStorageAtomic(storagePath, current);
1125
- });
1126
- }
1127
- async removeAccount(uuid) {
1128
- return await withFileLock(async (storagePath) => {
1129
- const current = await readStorageFromDisk(storagePath, false);
1130
- if (!current) return false;
1131
- const initialLength = current.accounts.length;
1132
- current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
1133
- if (current.accounts.length === initialLength) return false;
1134
- if (current.activeAccountUuid === uuid) {
1135
- current.activeAccountUuid = current.accounts[0]?.uuid;
1136
- }
1137
- await writeStorageAtomic(storagePath, current);
1138
- return true;
1139
- });
1140
- }
1141
- async setActiveUuid(uuid) {
1142
- await this.mutateStorage((storage) => {
1143
- storage.activeAccountUuid = uuid;
1144
- });
1145
- }
1146
- async clear() {
1147
- await withFileLock(async (storagePath) => {
1148
- await writeStorageAtomic(storagePath, createEmptyStorage());
1149
- });
1150
- }
1151
- };
1152
-
1153
- // ../multi-account-core/src/executor.ts
1154
- var MIN_MAX_RETRIES = 6;
1155
- var RETRIES_PER_ACCOUNT = 3;
1156
- var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1157
- var MAX_RESOLVE_ATTEMPTS = 10;
1158
- var SERVER_RETRY_BASE_MS = 1e3;
1159
- var SERVER_RETRY_MAX_MS = 4e3;
1160
- function isAbortError(error) {
1161
- return error instanceof Error && error.name === "AbortError";
1162
- }
1163
- function createExecutorForProvider(providerName, dependencies) {
1164
- const {
1165
- handleRateLimitResponse: handleRateLimitResponse2,
1166
- formatWaitTime: formatWaitTime2,
1167
- sleep: sleep2,
1168
- showToast: showToast2,
1169
- getAccountLabel: getAccountLabel2
1170
- } = dependencies;
1171
- async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
1172
- const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1173
- let previousAccountUuid;
1174
- async function retryServerErrors(account, runtime) {
1175
- for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1176
- const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1177
- const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1178
- await sleep2(jitteredBackoff);
1179
- let retryResponse;
1180
- try {
1181
- retryResponse = await runtime.fetch(input, init);
1182
- } catch (error) {
1183
- if (isAbortError(error)) throw error;
1184
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1185
- return null;
1186
- }
1187
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1188
- return null;
1189
- }
1190
- if (retryResponse.status < 500) return retryResponse;
1191
- }
1192
- return null;
1193
- }
1194
- const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
1195
- if (response.status >= 500) {
1196
- const recovered = await retryServerErrors(account, runtime);
1197
- if (recovered === null) {
1198
- return { type: "retryOuter" };
1199
- }
1200
- response = recovered;
1201
- }
1202
- if (response.status === 401) {
1203
- if (allow401Retry) {
1204
- runtimeFactory.invalidate(accountUuid);
1205
- try {
1206
- const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1207
- const retryResponse = await retryRuntime.fetch(input, init);
1208
- return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
1209
- } catch (error) {
1210
- if (isAbortError(error)) throw error;
1211
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1212
- return { type: "retryOuter" };
1213
- }
1214
- return { type: "retryOuter" };
1215
- }
1216
- }
1217
- await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1218
- await manager.refresh();
1219
- if (!manager.hasAnyUsableAccount()) {
1220
- void showToast2(client, "All accounts have auth failures.", "error");
1221
- throw new Error(
1222
- `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1223
- );
1224
- }
1225
- void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1226
- return { type: "retryOuter" };
1227
- }
1228
- if (response.status === 403) {
1229
- const revoked = await isRevokedTokenResponse(response);
1230
- if (revoked) {
1231
- await manager.markRevoked(accountUuid);
1232
- await manager.refresh();
1233
- void showToast2(
1234
- client,
1235
- `${getAccountLabel2(account)} disabled: OAuth token revoked.`,
1236
- "error"
1237
- );
1238
- if (!manager.hasAnyUsableAccount()) {
1239
- throw new Error(
1240
- `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1241
- );
1242
- }
1243
- return { type: "retryOuter" };
1244
- }
1245
- if (from401RefreshRetry) {
1246
- return { type: "handled", response };
1247
- }
1248
- }
1249
- if (response.status === 429) {
1250
- await handleRateLimitResponse2(manager, client, account, response);
1251
- return { type: "handled" };
1252
- }
1253
- return { type: "success", response };
1254
- };
1255
- for (let retries = 1; retries <= maxRetries; retries++) {
1256
- await manager.refresh();
1257
- const account = await resolveAccount(manager, client);
1258
- const accountUuid = account.uuid;
1259
- if (!accountUuid) continue;
1260
- if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1261
- void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1262
- }
1263
- previousAccountUuid = accountUuid;
1264
- let runtime;
1265
- let response;
1266
- try {
1267
- runtime = await runtimeFactory.getRuntime(accountUuid);
1268
- response = await runtime.fetch(input, init);
1269
- } catch (error) {
1270
- if (isAbortError(error)) throw error;
1271
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1272
- continue;
1273
- }
1274
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1275
- continue;
1276
- }
1277
- const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
1278
- if (transition.type === "retryOuter" || transition.type === "handled") {
1279
- if (transition.type === "handled" && transition.response) {
1280
- return transition.response;
1281
- }
1282
- continue;
1283
- }
1284
- await manager.markSuccess(accountUuid);
1285
- return transition.response;
1286
- }
1287
- throw new Error(
1288
- `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1289
- );
1290
- }
1291
- async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1292
- if (!isTokenRefreshError(error)) return false;
1293
- if (!account.uuid) return false;
1294
- const accountUuid = account.uuid;
1295
- runtimeFactory.invalidate(accountUuid);
1296
- await manager.markAuthFailure(accountUuid, {
1297
- ok: false,
1298
- permanent: error.permanent
1299
- });
1300
- await manager.refresh();
1301
- if (!manager.hasAnyUsableAccount()) {
1302
- void showToast2(client, "All accounts have auth failures.", "error");
1303
- throw new Error(
1304
- `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1305
- );
1306
- }
1307
- void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1308
- return true;
1309
- }
1310
- async function resolveAccount(manager, client) {
1311
- let attempts = 0;
1312
- while (true) {
1313
- if (++attempts > MAX_RESOLVE_ATTEMPTS) {
1314
- throw new Error(
1315
- `Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
1316
- );
1317
- }
1318
- const account = await manager.selectAccount();
1319
- if (account) return account;
1320
- if (!manager.hasAnyUsableAccount()) {
1321
- throw new Error(
1322
- `All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
1323
- );
1324
- }
1325
- const waitMs = manager.getMinWaitTime();
1326
- if (waitMs <= 0) {
1327
- throw new Error(
1328
- `All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
1329
- );
1330
- }
1331
- await showToast2(
1332
- client,
1333
- `All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
1334
- "warning"
1335
- );
1336
- await sleep2(waitMs);
1337
- }
1338
- }
1339
- return {
1340
- executeWithAccountRotation: executeWithAccountRotation2
1341
- };
1342
- }
1343
- async function isRevokedTokenResponse(response) {
1344
- try {
1345
- const cloned = response.clone();
1346
- const body = await cloned.text();
1347
- return body.includes("revoked");
1348
- } catch {
1349
- return false;
1350
- }
1351
- }
1352
-
1353
- // ../multi-account-core/src/proactive-refresh.ts
1354
- var INITIAL_DELAY_MS = 5e3;
1355
- function createProactiveRefreshQueueForProvider(dependencies) {
1356
- const {
1357
- providerAuthId,
1358
- getConfig: getConfig2,
1359
- refreshToken: refreshToken2,
1360
- isTokenExpired: isTokenExpired2,
1361
- debugLog: debugLog2
1362
- } = dependencies;
1363
- return class ProactiveRefreshQueue {
1364
- constructor(client, store, onInvalidate) {
1365
- this.client = client;
1366
- this.store = store;
1367
- this.onInvalidate = onInvalidate;
1368
- }
1369
- timeoutHandle = null;
1370
- runToken = 0;
1371
- inFlight = null;
1372
- start() {
1373
- const config = getConfig2();
1374
- if (!config.proactive_refresh) return;
1375
- this.runToken++;
1376
- if (this.timeoutHandle) {
1377
- clearTimeout(this.timeoutHandle);
1378
- this.timeoutHandle = null;
1379
- }
1380
- this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1381
- debugLog2(this.client, "Proactive refresh started", {
1382
- intervalSeconds: config.proactive_refresh_interval_seconds,
1383
- bufferSeconds: config.proactive_refresh_buffer_seconds
1384
- });
1385
- }
1386
- async stop() {
1387
- this.runToken++;
1388
- if (this.timeoutHandle) {
1389
- clearTimeout(this.timeoutHandle);
1390
- this.timeoutHandle = null;
1391
- }
1392
- if (this.inFlight) {
1393
- await this.inFlight;
1394
- this.inFlight = null;
1395
- }
1396
- debugLog2(this.client, "Proactive refresh stopped");
1397
- }
1398
- scheduleNext(token, delayMs) {
1399
- this.timeoutHandle = setTimeout(() => {
1400
- if (token !== this.runToken) return;
1401
- this.inFlight = this.runCheck(token).finally(() => {
1402
- this.inFlight = null;
1403
- });
1404
- }, delayMs);
1405
- }
1406
- needsProactiveRefresh(account) {
1407
- if (!account.accessToken || !account.expiresAt) return false;
1408
- if (isTokenExpired2(account)) return false;
1409
- const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
1410
- return account.expiresAt <= Date.now() + bufferMs;
1411
- }
1412
- async runCheck(token) {
1413
- try {
1414
- const stored = await this.store.load();
1415
- if (token !== this.runToken) return;
1416
- const candidates = stored.accounts.filter(
1417
- (a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
1418
- );
1419
- if (candidates.length === 0) return;
1420
- debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
1421
- for (const account of candidates) {
1422
- if (token !== this.runToken) return;
1423
- const credentials = await this.store.readCredentials(account.uuid);
1424
- if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
1425
- const result = await refreshToken2(credentials.refreshToken, account.uuid, this.client);
1426
- if (result.ok) {
1427
- await this.store.mutateAccount(account.uuid, (target) => {
1428
- target.accessToken = result.patch.accessToken;
1429
- target.expiresAt = result.patch.expiresAt;
1430
- if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
1431
- if (result.patch.uuid) target.uuid = result.patch.uuid;
1432
- if (result.patch.email) target.email = result.patch.email;
1433
- if (result.patch.accountId) target.accountId = result.patch.accountId;
1434
- target.consecutiveAuthFailures = 0;
1435
- target.isAuthDisabled = false;
1436
- target.authDisabledReason = void 0;
1437
- });
1438
- this.onInvalidate?.(account.uuid);
1439
- } else {
1440
- await this.persistFailure(account, result.permanent);
1441
- }
1442
- }
1443
- } catch (error) {
1444
- debugLog2(this.client, `Proactive refresh check error: ${error}`);
1445
- } finally {
1446
- if (token === this.runToken) {
1447
- const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
1448
- this.scheduleNext(token, intervalMs);
1449
- }
1450
- }
1451
- }
1452
- async persistFailure(account, permanent) {
1453
- try {
1454
- const accountUuid = account.uuid;
1455
- if (!accountUuid) return;
1456
- if (permanent) {
1457
- const removed = await this.store.removeAccount(accountUuid);
1458
- if (!removed) return;
1459
- this.onInvalidate?.(accountUuid);
1460
- await this.clearOpenCodeAuthIfNoAccountsRemain();
1461
- return;
1462
- }
1463
- await this.store.mutateStorage((storage) => {
1464
- const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
1465
- if (!target) return;
1466
- target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1467
- const maxFailures = getConfig2().max_consecutive_auth_failures;
1468
- const usableCount = storage.accounts.filter(
1469
- (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
1470
- ).length;
1471
- if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
1472
- target.isAuthDisabled = true;
1473
- target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1474
- }
1475
- });
1476
- } catch {
1477
- debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1478
- }
1479
- }
1480
- async clearOpenCodeAuthIfNoAccountsRemain() {
1481
- const storage = await this.store.load();
1482
- if (storage.accounts.length > 0) return;
1483
- await this.client.auth.set({
1484
- path: { id: providerAuthId },
1485
- body: getClearedOAuthBody()
1486
- }).catch(() => {
1487
- });
1488
- }
1489
- };
1490
- }
1491
-
1492
- // ../multi-account-core/src/rate-limit.ts
1493
- var USAGE_FETCH_COOLDOWN_MS = 3e4;
1494
- function createRateLimitHandlers(dependencies) {
1495
- const {
1496
- fetchUsage: fetchUsage2,
1497
- getConfig: getConfig2,
1498
- formatWaitTime: formatWaitTime2,
1499
- getAccountLabel: getAccountLabel2,
1500
- showToast: showToast2
1501
- } = dependencies;
1502
- function retryAfterMsFromResponse2(response) {
1503
- const retryAfterMs = response.headers.get("retry-after-ms");
1504
- if (retryAfterMs) {
1505
- const parsed = parseInt(retryAfterMs, 10);
1506
- if (!isNaN(parsed) && parsed > 0) return parsed;
1507
- }
1508
- const retryAfter = response.headers.get("retry-after");
1509
- if (retryAfter) {
1510
- const parsed = parseInt(retryAfter, 10);
1511
- if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
1512
- }
1513
- return getConfig2().default_retry_after_ms;
1514
- }
1515
- function getResetMsFromUsage2(account) {
1516
- const usage = account.cachedUsage;
1517
- if (!usage) return null;
1518
- const now = Date.now();
1519
- const candidates = [];
1520
- if (usage.five_hour?.resets_at) {
1521
- const ms = Date.parse(usage.five_hour.resets_at) - now;
1522
- if (ms > 0) candidates.push(ms);
1523
- }
1524
- if (usage.seven_day?.resets_at) {
1525
- const ms = Date.parse(usage.seven_day.resets_at) - now;
1526
- if (ms > 0) candidates.push(ms);
1527
- }
1528
- return candidates.length > 0 ? Math.min(...candidates) : null;
1529
- }
1530
- async function fetchUsageLimits2(accessToken, accountId) {
1531
- if (!accessToken) return null;
1532
- try {
1533
- const result = await fetchUsage2(accessToken, accountId);
1534
- return result.ok ? result.data : null;
1535
- } catch {
1536
- return null;
1537
- }
1538
- }
1539
- async function handleRateLimitResponse2(manager, client, account, response) {
1540
- if (!account.uuid) return;
1541
- const resetMs = getResetMsFromUsage2(account) ?? retryAfterMsFromResponse2(response);
1542
- await manager.markRateLimited(account.uuid, resetMs);
1543
- const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
1544
- if (shouldFetchUsage) {
1545
- const usage = await fetchUsageLimits2(account.accessToken, account.accountId);
1546
- if (usage) {
1547
- await manager.applyUsageCache(account.uuid, usage);
1548
- }
1549
- }
1550
- if (manager.getAccountCount() > 1) {
1551
- void showToast2(
1552
- client,
1553
- `${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
1554
- "warning"
1555
- );
1556
- }
1557
- }
1558
- return {
1559
- retryAfterMsFromResponse: retryAfterMsFromResponse2,
1560
- getResetMsFromUsage: getResetMsFromUsage2,
1561
- fetchUsageLimits: fetchUsageLimits2,
1562
- handleRateLimitResponse: handleRateLimitResponse2
1563
- };
1564
- }
1565
-
1566
- // ../multi-account-core/src/auth-migration.ts
1567
- import { promises as fs5 } from "node:fs";
1568
- import { join as join6 } from "node:path";
1569
- var AUTH_JSON_FILENAME = "auth.json";
1570
- function isValidOAuthCredential(value) {
1571
- if (typeof value !== "object" || value === null) return false;
1572
- const candidate = value;
1573
- return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
1574
- }
1575
- function resolveAuthJsonPath() {
1576
- return join6(getConfigDir2(), AUTH_JSON_FILENAME);
1577
- }
1578
- async function readAuthJson() {
1579
- const authPath = resolveAuthJsonPath();
1580
- let content;
1581
- try {
1582
- content = await fs5.readFile(authPath, "utf-8");
1583
- } catch {
1584
- return null;
1585
- }
1586
- try {
1587
- const parsed = JSON.parse(content);
1588
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1589
- return null;
1590
- }
1591
- return parsed;
1592
- } catch {
1593
- return null;
1594
- }
1595
- }
1596
- async function migrateFromAuthJson(providerKey, store) {
1597
- const storage = await store.load();
1598
- const hasExistingAccounts = storage.accounts.length > 0;
1599
- if (hasExistingAccounts) return false;
1600
- const authData = await readAuthJson();
1601
- if (!authData) return false;
1602
- const providerCredential = authData[providerKey];
1603
- if (!isValidOAuthCredential(providerCredential)) return false;
1604
- const now = Date.now();
1605
- const newAccount = {
1606
- uuid: crypto.randomUUID(),
1607
- refreshToken: providerCredential.refresh,
1608
- accessToken: providerCredential.access,
1609
- expiresAt: providerCredential.expires,
1610
- addedAt: now,
1611
- lastUsed: now,
1612
- enabled: true,
1613
- planTier: "",
1614
- consecutiveAuthFailures: 0,
1615
- isAuthDisabled: false
1616
- };
1617
- await store.addAccount(newAccount);
1618
- await store.setActiveUuid(newAccount.uuid);
1619
- return true;
1620
- }
1621
-
1622
- // ../multi-account-core/src/ui/ansi.ts
1623
- var ANSI = {
1624
- hide: "\x1B[?25l",
1625
- show: "\x1B[?25h",
1626
- up: (n = 1) => `\x1B[${n}A`,
1627
- down: (n = 1) => `\x1B[${n}B`,
1628
- clearLine: "\x1B[2K",
1629
- cyan: "\x1B[36m",
1630
- green: "\x1B[32m",
1631
- red: "\x1B[31m",
1632
- yellow: "\x1B[33m",
1633
- dim: "\x1B[2m",
1634
- bold: "\x1B[1m",
1635
- reset: "\x1B[0m"
1636
- };
1637
- function parseKey(data) {
1638
- const s = data.toString();
1639
- if (s === "\x1B[A" || s === "\x1BOA") return "up";
1640
- if (s === "\x1B[B" || s === "\x1BOB") return "down";
1641
- if (s === "\r" || s === "\n") return "enter";
1642
- if (s === "") return "escape";
1643
- if (s === "\x1B") return "escape-start";
1644
- return null;
1645
- }
1646
- function isTTY() {
1647
- return Boolean(process.stdin.isTTY);
1648
- }
1649
-
1650
- // ../multi-account-core/src/ui/select.ts
1651
- var ESCAPE_TIMEOUT_MS = 50;
1652
- var COLOR_MAP = {
1653
- red: ANSI.red,
1654
- green: ANSI.green,
1655
- yellow: ANSI.yellow,
1656
- cyan: ANSI.cyan
1657
- };
1658
- async function select(items, options) {
1659
- if (!isTTY()) {
1660
- throw new Error("Interactive select requires a TTY terminal");
1661
- }
1662
- const enabledItems = items.filter((i) => !i.disabled && !i.separator);
1663
- if (enabledItems.length === 0) {
1664
- throw new Error("All items disabled");
1665
- }
1666
- if (enabledItems.length === 1) {
1667
- return enabledItems[0].value;
1668
- }
1669
- const { message, subtitle } = options;
1670
- const { stdin, stdout } = process;
1671
- let cursor = items.findIndex((i) => !i.disabled && !i.separator);
1672
- if (cursor === -1) cursor = 0;
1673
- let escapeTimeout = null;
1674
- let isCleanedUp = false;
1675
- let isFirstRender = true;
1676
- const getTotalLines = () => {
1677
- const subtitleLines = subtitle ? 3 : 0;
1678
- return 1 + subtitleLines + items.length + 1 + 1;
1679
- };
1680
- const renderItemLabel = (item, isSelected) => {
1681
- const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
1682
- if (item.disabled) {
1683
- return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
1684
- }
1685
- const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
1686
- if (isSelected) {
1687
- const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
1688
- return `${label}${hintSuffix}`;
1689
- }
1690
- const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
1691
- return `${dimLabel}${hintSuffix}`;
1692
- };
1693
- const render = () => {
1694
- const totalLines = getTotalLines();
1695
- if (!isFirstRender) {
1696
- stdout.write(ANSI.up(totalLines) + "\r");
1697
- }
1698
- isFirstRender = false;
1699
- stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
1700
- `);
1701
- if (subtitle) {
1702
- stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1703
- `);
1704
- stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
1705
- `);
1706
- stdout.write(`${ANSI.clearLine}
1707
- `);
1708
- }
1709
- for (let i = 0; i < items.length; i++) {
1710
- const item = items[i];
1711
- if (!item) continue;
1712
- if (item.separator) {
1713
- stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1714
- `);
1715
- continue;
1716
- }
1717
- const isSelected = i === cursor;
1718
- const labelText = renderItemLabel(item, isSelected);
1719
- const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
1720
- stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
1721
- `);
1722
- }
1723
- stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
1724
- `);
1725
- stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
1726
- `);
1727
- };
1728
- return new Promise((resolve) => {
1729
- const wasRaw = stdin.isRaw ?? false;
1730
- const cleanup = () => {
1731
- if (isCleanedUp) return;
1732
- isCleanedUp = true;
1733
- if (escapeTimeout) {
1734
- clearTimeout(escapeTimeout);
1735
- escapeTimeout = null;
1736
- }
1737
- try {
1738
- stdin.removeListener("data", onKey);
1739
- stdin.setRawMode(wasRaw);
1740
- stdin.pause();
1741
- stdout.write(ANSI.show);
1742
- } catch {
1743
- }
1744
- process.removeListener("SIGINT", onSignal);
1745
- process.removeListener("SIGTERM", onSignal);
1746
- };
1747
- const onSignal = () => {
1748
- cleanup();
1749
- resolve(null);
1750
- };
1751
- const finishWithValue = (value) => {
1752
- cleanup();
1753
- resolve(value);
1754
- };
1755
- const findNextSelectable = (from, direction) => {
1756
- if (items.length === 0) return from;
1757
- let next = from;
1758
- do {
1759
- next = (next + direction + items.length) % items.length;
1760
- } while (items[next]?.disabled || items[next]?.separator);
1761
- return next;
1762
- };
1763
- const onKey = (data) => {
1764
- if (escapeTimeout) {
1765
- clearTimeout(escapeTimeout);
1766
- escapeTimeout = null;
1767
- }
1768
- const action = parseKey(data);
1769
- switch (action) {
1770
- case "up":
1771
- cursor = findNextSelectable(cursor, -1);
1772
- render();
1773
- return;
1774
- case "down":
1775
- cursor = findNextSelectable(cursor, 1);
1776
- render();
1777
- return;
1778
- case "enter":
1779
- finishWithValue(items[cursor]?.value ?? null);
1780
- return;
1781
- case "escape":
1782
- finishWithValue(null);
1783
- return;
1784
- case "escape-start":
1785
- escapeTimeout = setTimeout(() => {
1786
- finishWithValue(null);
1787
- }, ESCAPE_TIMEOUT_MS);
1788
- return;
1789
- default:
1790
- return;
1791
- }
1792
- };
1793
- process.once("SIGINT", onSignal);
1794
- process.once("SIGTERM", onSignal);
1795
- try {
1796
- stdin.setRawMode(true);
1797
- } catch {
1798
- cleanup();
1799
- resolve(null);
1800
- return;
1801
- }
1802
- stdin.resume();
1803
- stdout.write(ANSI.hide);
1804
- render();
1805
- stdin.on("data", onKey);
1806
- });
1807
- }
1808
-
1809
- // ../multi-account-core/src/ui/confirm.ts
1810
- async function confirm(message, defaultYes = false) {
1811
- const items = defaultYes ? [
1812
- { label: "Yes", value: true },
1813
- { label: "No", value: false }
1814
- ] : [
1815
- { label: "No", value: false },
1816
- { label: "Yes", value: true }
1817
- ];
1818
- const result = await select(items, { message });
1819
- return result ?? false;
1820
- }
1821
-
1822
- // ../multi-account-core/src/adapters/anthropic.ts
1823
- var anthropicOAuthAdapter = {
1824
- id: "anthropic",
1825
- authProviderId: "anthropic",
1826
- modelDisplayName: "Claude",
1827
- statusToolName: "claude_multiauth_status",
1828
- authMethodLabel: "Claude Pro/Max (Multi-Auth)",
1829
- serviceLogName: "claude-multiauth",
1830
- oauthClientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
1831
- tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
1832
- usageEndpoint: "https://api.anthropic.com/api/oauth/usage",
1833
- profileEndpoint: "https://api.anthropic.com/api/oauth/profile",
1834
- oauthBetaHeader: "oauth-2025-04-20",
1835
- requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1836
- cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1837
- cliVersion: "2.1.80",
1838
- billingSalt: "59cf53e54c78",
1839
- toolPrefix: "mcp_",
1840
- accountStorageFilename: "anthropic-multi-account-accounts.json",
1841
- transform: {
1842
- rewriteOpenCodeBranding: true,
1843
- addToolPrefix: true,
1844
- stripToolPrefixInResponse: true,
1845
- enableMessagesBetaQuery: true
1846
- },
1847
- planLabels: {
1848
- max: "Claude Max",
1849
- pro: "Claude Pro",
1850
- free: "Free"
1851
- },
1852
- supported: true
1853
- };
1854
-
1855
- // ../multi-account-core/src/pool-types.ts
1856
- import * as v5 from "valibot";
1857
- var PoolConfigSchema = v5.object({
1858
- name: v5.string(),
1859
- baseProvider: v5.string(),
1860
- members: v5.array(v5.string()),
1861
- enabled: v5.boolean()
1862
- });
1863
- var ChainEntryConfigSchema = v5.object({
1864
- pool: v5.string(),
1865
- model: v5.optional(v5.string()),
1866
- enabled: v5.boolean()
1867
- });
1868
- var ChainConfigSchema = v5.object({
1869
- name: v5.string(),
1870
- entries: v5.array(ChainEntryConfigSchema),
1871
- enabled: v5.boolean()
1872
- });
1873
- var PoolChainConfigSchema = v5.object({
1874
- pools: v5.optional(v5.array(PoolConfigSchema), []),
1875
- chains: v5.optional(v5.array(ChainConfigSchema), [])
1876
- });
1877
-
1878
- // ../multi-account-core/src/pool-config-store.ts
1879
- import { promises as fs6 } from "node:fs";
1880
- import { dirname as dirname5, join as join7 } from "node:path";
1881
- import lockfile2 from "proper-lockfile";
1882
- import * as v6 from "valibot";
1883
- var POOL_CONFIG_FILENAME = "multiauth-pools.json";
1884
- function createEmptyConfig() {
1885
- return { pools: [], chains: [] };
1886
- }
1887
- function getGlobalConfigPath() {
1888
- return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
1889
- }
1890
- async function resolveConfigPath() {
1891
- const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
1892
- try {
1893
- await fs6.access(projectPath);
1894
- return projectPath;
1895
- } catch {
1896
- }
1897
- return getGlobalConfigPath();
1898
- }
1899
- function parsePoolChainConfig(content) {
1900
- let parsed;
1901
- try {
1902
- parsed = JSON.parse(content);
1903
- } catch {
1904
- return null;
1905
- }
1906
- const validation = v6.safeParse(PoolChainConfigSchema, parsed);
1907
- return validation.success ? validation.output : null;
1908
- }
1909
- async function loadPoolChainConfig() {
1910
- const path = await resolveConfigPath();
1911
- try {
1912
- const content = await fs6.readFile(path, "utf-8");
1913
- return parsePoolChainConfig(content) ?? createEmptyConfig();
1914
- } catch {
1915
- return createEmptyConfig();
1916
- }
1917
- }
1918
-
1919
- // ../multi-account-core/src/pool-manager.ts
1920
- var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
1921
- var PoolManager = class {
1922
- poolsByName = /* @__PURE__ */ new Map();
1923
- exhaustedUntilByAccount = /* @__PURE__ */ new Map();
1924
- exhaustedCooldownMs;
1925
- constructor(options) {
1926
- this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
1927
- }
1928
- loadPools(configs) {
1929
- this.poolsByName.clear();
1930
- for (const pool of configs) {
1931
- this.poolsByName.set(pool.name, pool);
1932
- }
1933
- }
1934
- getPoolForAccount(accountUuid) {
1935
- for (const pool of this.poolsByName.values()) {
1936
- if (!pool.enabled) continue;
1937
- if (pool.members.includes(accountUuid)) return pool;
1938
- }
1939
- return null;
1940
- }
1941
- getAvailableMembers(pool, accountManager) {
1942
- if (!pool.enabled) return [];
1943
- this.clearExpiredExhausted();
1944
- const accountsByUuid = /* @__PURE__ */ new Map();
1945
- for (const account of accountManager.getAccounts()) {
1946
- if (!account.uuid) continue;
1947
- accountsByUuid.set(account.uuid, account);
1948
- }
1949
- return pool.members.filter((accountUuid) => {
1950
- const account = accountsByUuid.get(accountUuid);
1951
- if (!account) return false;
1952
- if (!account.enabled || account.isAuthDisabled) return false;
1953
- if (this.isExhausted(accountUuid)) return false;
1954
- if (accountManager.isRateLimited(account)) return false;
1955
- return true;
1956
- });
1957
- }
1958
- markExhausted(accountUuid) {
1959
- this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
1960
- }
1961
- async getNextMember(pool, currentUuid, accountManager) {
1962
- const availableMembers = this.getAvailableMembers(pool, accountManager);
1963
- if (availableMembers.length === 0) return null;
1964
- const excluded = /* @__PURE__ */ new Set();
1965
- if (currentUuid) excluded.add(currentUuid);
1966
- const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
1967
- if (preferred) return preferred;
1968
- for (const candidate of availableMembers) {
1969
- if (candidate !== currentUuid) return candidate;
1970
- }
1971
- return null;
1972
- }
1973
- async buildFailoverPlan(currentAccount, config, accountManager, options) {
1974
- this.loadPools(config.pools ?? []);
1975
- if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
1976
- return { candidates: [], skips: [] };
1977
- }
1978
- const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
1979
- const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
1980
- const currentUuid = currentAccount?.uuid;
1981
- const candidates = [];
1982
- const skips = [];
1983
- const addedCandidateUuids = /* @__PURE__ */ new Set();
1984
- const appendPoolCandidates = async (poolName, source, chainIndex) => {
1985
- const pool = this.poolsByName.get(poolName);
1986
- if (!pool || !pool.enabled) {
1987
- skips.push({
1988
- type: "chain_disabled",
1989
- poolName,
1990
- reason: "Pool is missing or disabled"
1991
- });
1992
- return;
1993
- }
1994
- const available = this.getAvailableMembers(pool, accountManager);
1995
- if (available.length === 0) {
1996
- skips.push({
1997
- type: "pool_exhausted",
1998
- poolName,
1999
- reason: "No available members"
2000
- });
2001
- return;
2002
- }
2003
- const poolExclusions = /* @__PURE__ */ new Set();
2004
- if (currentUuid) poolExclusions.add(currentUuid);
2005
- while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
2006
- const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
2007
- if (!nextMember) break;
2008
- poolExclusions.add(nextMember);
2009
- if (attemptedAccounts.has(nextMember)) {
2010
- skips.push({
2011
- type: "account_attempted",
2012
- poolName,
2013
- reason: "Already attempted in this cascade",
2014
- detail: nextMember
2015
- });
2016
- continue;
2017
- }
2018
- if (addedCandidateUuids.has(nextMember)) continue;
2019
- candidates.push({
2020
- poolName,
2021
- accountUuid: nextMember,
2022
- source,
2023
- chainIndex
2024
- });
2025
- addedCandidateUuids.add(nextMember);
2026
- }
2027
- for (const memberUuid of available) {
2028
- if (poolExclusions.has(memberUuid)) continue;
2029
- if (attemptedAccounts.has(memberUuid)) {
2030
- skips.push({
2031
- type: "account_attempted",
2032
- poolName,
2033
- reason: "Already attempted in this cascade",
2034
- detail: memberUuid
2035
- });
2036
- continue;
2037
- }
2038
- if (addedCandidateUuids.has(memberUuid)) continue;
2039
- candidates.push({
2040
- poolName,
2041
- accountUuid: memberUuid,
2042
- source,
2043
- chainIndex
2044
- });
2045
- addedCandidateUuids.add(memberUuid);
2046
- }
2047
- };
2048
- if (currentUuid) {
2049
- const currentPool = this.getPoolForAccount(currentUuid);
2050
- if (currentPool) {
2051
- await appendPoolCandidates(currentPool.name, "pool");
2052
- }
2053
- }
2054
- let flattenedChainIndex = 0;
2055
- for (const chain of config.chains ?? []) {
2056
- if (!chain.enabled) {
2057
- for (let i = 0; i < chain.entries.length; i++) {
2058
- skips.push({
2059
- type: "chain_disabled",
2060
- poolName: chain.entries[i]?.pool ?? chain.name,
2061
- reason: `Chain '${chain.name}' is disabled`
2062
- });
2063
- flattenedChainIndex += 1;
2064
- }
2065
- continue;
2066
- }
2067
- for (const entry of chain.entries) {
2068
- if (visitedChainIndexes.has(flattenedChainIndex)) {
2069
- skips.push({
2070
- type: "chain_disabled",
2071
- poolName: entry.pool,
2072
- reason: "Chain entry already visited in this cascade",
2073
- detail: `${flattenedChainIndex}`
2074
- });
2075
- flattenedChainIndex += 1;
2076
- continue;
2077
- }
2078
- if (!entry.enabled) {
2079
- skips.push({
2080
- type: "chain_disabled",
2081
- poolName: entry.pool,
2082
- reason: "Chain entry is disabled",
2083
- detail: `${flattenedChainIndex}`
2084
- });
2085
- flattenedChainIndex += 1;
2086
- continue;
2087
- }
2088
- await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
2089
- flattenedChainIndex += 1;
2090
- }
2091
- }
2092
- return { candidates, skips };
2093
- }
2094
- isExhausted(accountUuid) {
2095
- const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
2096
- if (!exhaustedUntil) return false;
2097
- if (Date.now() >= exhaustedUntil) {
2098
- this.exhaustedUntilByAccount.delete(accountUuid);
2099
- return false;
2100
- }
2101
- return true;
2102
- }
2103
- clearExpiredExhausted() {
2104
- const now = Date.now();
2105
- for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
2106
- if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
2107
- }
2108
- }
2109
- async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
2110
- const availableSet = new Set(availableMembers);
2111
- const maxAttempts = Math.max(availableMembers.length * 2, 6);
2112
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
2113
- const selected = await accountManager.selectAccount();
2114
- if (!selected?.uuid) continue;
2115
- if (!availableSet.has(selected.uuid)) continue;
2116
- if (excludedMembers.has(selected.uuid)) continue;
2117
- return selected.uuid;
2118
- }
2119
- for (const memberUuid of availableMembers) {
2120
- if (!excludedMembers.has(memberUuid)) return memberUuid;
2121
- }
2122
- return null;
2123
- }
2124
- };
2125
-
2126
- // ../multi-account-core/src/cascade-state.ts
2127
- function createCascadeState(prompt, currentAccountUuid) {
2128
- const attemptedAccounts = /* @__PURE__ */ new Set();
2129
- if (currentAccountUuid) {
2130
- attemptedAccounts.add(currentAccountUuid);
2131
- }
2132
- return {
2133
- prompt,
2134
- attemptedAccounts,
2135
- visitedChainIndexes: /* @__PURE__ */ new Set()
2136
- };
2137
- }
2138
- var CascadeStateManager = class {
2139
- suppressNextStartTurn = false;
2140
- cascadeState = null;
2141
- startTurn(prompt, currentAccountUuid) {
2142
- if (this.suppressNextStartTurn) {
2143
- this.suppressNextStartTurn = false;
2144
- return this.ensureCascadeState(prompt, currentAccountUuid);
2145
- }
2146
- const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
2147
- if (shouldReset) {
2148
- this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2149
- return this.cascadeState;
2150
- }
2151
- return this.ensureCascadeState(prompt, currentAccountUuid);
2152
- }
2153
- ensureCascadeState(prompt, currentAccountUuid) {
2154
- if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
2155
- this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2156
- return this.cascadeState;
2157
- }
2158
- if (currentAccountUuid) {
2159
- this.cascadeState.attemptedAccounts.add(currentAccountUuid);
2160
- }
2161
- return this.cascadeState;
2162
- }
2163
- markAttempted(accountUuid) {
2164
- if (!this.cascadeState) return;
2165
- this.cascadeState.attemptedAccounts.add(accountUuid);
2166
- }
2167
- markVisitedChainIndex(index) {
2168
- if (!this.cascadeState) return;
2169
- this.cascadeState.visitedChainIndexes.add(index);
2170
- }
2171
- clearCascadeState() {
2172
- this.cascadeState = null;
2173
- this.suppressNextStartTurn = false;
2174
- }
2175
- getSnapshot() {
2176
- if (!this.cascadeState) return null;
2177
- return {
2178
- prompt: this.cascadeState.prompt,
2179
- attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
2180
- visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
2181
- };
2182
- }
2183
- };
10
+ // src/account-manager.ts
11
+ import { createAccountManagerForProvider } from "opencode-multi-account-core";
2184
12
 
2185
13
  // src/constants.ts
14
+ import { anthropicOAuthAdapter } from "opencode-multi-account-core";
2186
15
  var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
2187
16
  var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
2188
17
  var ANTHROPIC_TOKEN_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.tokenEndpoint;
@@ -2191,17 +20,17 @@ var ANTHROPIC_PROFILE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.profileEndpoint;
2191
20
  var ANTHROPIC_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader;
2192
21
  var CLAUDE_CLI_USER_AGENT = ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
2193
22
  var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
2194
- var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
23
+ var ACCOUNTS_FILENAME = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
2195
24
  var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
2196
25
  var TOKEN_EXPIRY_BUFFER_MS = 6e4;
2197
26
  var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
2198
27
 
2199
28
  // src/pi-ai-adapter.ts
2200
- import { AsyncLocalStorage } from "node:async_hooks";
29
+ import { AsyncLocalStorage } from "async_hooks";
2201
30
  import * as piAiOauth from "@mariozechner/pi-ai/oauth";
2202
31
 
2203
32
  // src/token-node-request.ts
2204
- import * as childProcess from "node:child_process";
33
+ import * as childProcess from "child_process";
2205
34
  function buildNodeTokenRequestScript() {
2206
35
  return `
2207
36
  const https = require("node:https");
@@ -2289,106 +118,126 @@ async function runNodeTokenRequest(options) {
2289
118
  return await nodeTokenRequestRunner(options);
2290
119
  }
2291
120
 
121
+ // src/utils.ts
122
+ import { setConfigGetter } from "opencode-multi-account-core";
123
+
2292
124
  // src/config.ts
125
+ import {
126
+ getConfig,
127
+ initCoreConfig,
128
+ loadConfig,
129
+ resetConfigCache,
130
+ updateConfigField
131
+ } from "opencode-multi-account-core";
2293
132
  initCoreConfig("claude-multiauth.json");
2294
133
 
2295
134
  // src/utils.ts
135
+ import {
136
+ createMinimalClient,
137
+ debugLog,
138
+ formatWaitTime,
139
+ getAccountLabel,
140
+ getConfigDir,
141
+ getErrorCode,
142
+ showToast,
143
+ sleep
144
+ } from "opencode-multi-account-core";
2296
145
  setConfigGetter(getConfig);
2297
146
 
2298
147
  // src/usage.ts
2299
- import * as v8 from "valibot";
148
+ import * as v2 from "valibot";
2300
149
 
2301
150
  // src/types.ts
2302
- import * as v7 from "valibot";
2303
- var OAuthCredentialsSchema2 = v7.object({
2304
- type: v7.literal("oauth"),
2305
- refresh: v7.string(),
2306
- access: v7.string(),
2307
- expires: v7.number()
151
+ import * as v from "valibot";
152
+ var OAuthCredentialsSchema = v.object({
153
+ type: v.literal("oauth"),
154
+ refresh: v.string(),
155
+ access: v.string(),
156
+ expires: v.number()
2308
157
  });
2309
- var UsageLimitEntrySchema2 = v7.object({
2310
- utilization: v7.number(),
2311
- resets_at: v7.nullable(v7.string())
158
+ var UsageLimitEntrySchema = v.object({
159
+ utilization: v.number(),
160
+ resets_at: v.nullable(v.string())
2312
161
  });
2313
- var UsageLimitsSchema2 = v7.object({
2314
- five_hour: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2315
- seven_day: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2316
- seven_day_sonnet: v7.optional(v7.nullable(UsageLimitEntrySchema2), null)
162
+ var UsageLimitsSchema = v.object({
163
+ five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
164
+ seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
165
+ seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
2317
166
  });
2318
- var CredentialRefreshPatchSchema2 = v7.object({
2319
- accessToken: v7.string(),
2320
- expiresAt: v7.number(),
2321
- refreshToken: v7.optional(v7.string()),
2322
- uuid: v7.optional(v7.string()),
2323
- email: v7.optional(v7.string())
167
+ var CredentialRefreshPatchSchema = v.object({
168
+ accessToken: v.string(),
169
+ expiresAt: v.number(),
170
+ refreshToken: v.optional(v.string()),
171
+ uuid: v.optional(v.string()),
172
+ email: v.optional(v.string())
2324
173
  });
2325
- var StoredAccountSchema2 = v7.object({
2326
- uuid: v7.optional(v7.string()),
2327
- label: v7.optional(v7.string()),
2328
- email: v7.optional(v7.string()),
2329
- planTier: v7.optional(v7.string(), ""),
2330
- refreshToken: v7.string(),
2331
- accessToken: v7.optional(v7.string()),
2332
- expiresAt: v7.optional(v7.number()),
2333
- addedAt: v7.number(),
2334
- lastUsed: v7.number(),
2335
- enabled: v7.optional(v7.boolean(), true),
2336
- rateLimitResetAt: v7.optional(v7.number()),
2337
- cachedUsage: v7.optional(UsageLimitsSchema2),
2338
- cachedUsageAt: v7.optional(v7.number()),
2339
- consecutiveAuthFailures: v7.optional(v7.number(), 0),
2340
- isAuthDisabled: v7.optional(v7.boolean(), false),
2341
- authDisabledReason: v7.optional(v7.string())
174
+ var StoredAccountSchema = v.object({
175
+ uuid: v.optional(v.string()),
176
+ label: v.optional(v.string()),
177
+ email: v.optional(v.string()),
178
+ planTier: v.optional(v.string(), ""),
179
+ refreshToken: v.string(),
180
+ accessToken: v.optional(v.string()),
181
+ expiresAt: v.optional(v.number()),
182
+ addedAt: v.number(),
183
+ lastUsed: v.number(),
184
+ enabled: v.optional(v.boolean(), true),
185
+ rateLimitResetAt: v.optional(v.number()),
186
+ cachedUsage: v.optional(UsageLimitsSchema),
187
+ cachedUsageAt: v.optional(v.number()),
188
+ consecutiveAuthFailures: v.optional(v.number(), 0),
189
+ isAuthDisabled: v.optional(v.boolean(), false),
190
+ authDisabledReason: v.optional(v.string())
2342
191
  });
2343
- var AccountStorageSchema2 = v7.object({
2344
- version: v7.literal(1),
2345
- accounts: v7.optional(v7.array(StoredAccountSchema2), []),
2346
- activeAccountUuid: v7.optional(v7.string())
192
+ var AccountStorageSchema = v.object({
193
+ version: v.literal(1),
194
+ accounts: v.optional(v.array(StoredAccountSchema), []),
195
+ activeAccountUuid: v.optional(v.string())
2347
196
  });
2348
- var TokenResponseSchema = v7.object({
2349
- access_token: v7.string(),
2350
- refresh_token: v7.optional(v7.string()),
2351
- expires_in: v7.number(),
2352
- account: v7.optional(v7.object({
2353
- uuid: v7.optional(v7.string()),
2354
- email_address: v7.optional(v7.string())
197
+ var TokenResponseSchema = v.object({
198
+ access_token: v.string(),
199
+ refresh_token: v.optional(v.string()),
200
+ expires_in: v.number(),
201
+ account: v.optional(v.object({
202
+ uuid: v.optional(v.string()),
203
+ email_address: v.optional(v.string())
2355
204
  }))
2356
205
  });
2357
- var AccountSelectionStrategySchema2 = v7.picklist(["sticky", "round-robin", "hybrid"]);
2358
- var PluginConfigSchema2 = v7.object({
206
+ var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
207
+ var PluginConfigSchema = v.object({
2359
208
  /** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
2360
- account_selection_strategy: v7.optional(AccountSelectionStrategySchema2, "sticky"),
209
+ account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
2361
210
  /** Use cross-process claim file to distribute parallel sessions across accounts */
2362
- cross_process_claims: v7.optional(v7.boolean(), true),
211
+ cross_process_claims: v.optional(v.boolean(), true),
2363
212
  /** Skip account when any usage tier utilization >= this % (100 = disabled) */
2364
- soft_quota_threshold_percent: v7.optional(v7.pipe(v7.number(), v7.minValue(0), v7.maxValue(100)), 100),
213
+ soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
2365
214
  /** Minimum backoff after rate limit (ms) */
2366
- rate_limit_min_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
215
+ rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
2367
216
  /** Default retry-after when header is missing (ms) */
2368
- default_retry_after_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 6e4),
217
+ default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
2369
218
  /** Consecutive auth failures before disabling account */
2370
- max_consecutive_auth_failures: v7.optional(v7.pipe(v7.number(), v7.integer(), v7.minValue(1)), 3),
219
+ max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
2371
220
  /** Backoff after token refresh failure (ms) */
2372
- token_failure_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
221
+ token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
2373
222
  /** Enable proactive background token refresh */
2374
- proactive_refresh: v7.optional(v7.boolean(), true),
223
+ proactive_refresh: v.optional(v.boolean(), true),
2375
224
  /** Seconds before expiry to trigger proactive refresh (default 30 min) */
2376
- proactive_refresh_buffer_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(60)), 1800),
225
+ proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
2377
226
  /** Interval between background refresh checks in seconds (default 5 min) */
2378
- proactive_refresh_interval_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(30)), 300),
227
+ proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
2379
228
  /** Suppress toast notifications */
2380
- quiet_mode: v7.optional(v7.boolean(), false),
229
+ quiet_mode: v.optional(v.boolean(), false),
2381
230
  /** Enable debug logging */
2382
- debug: v7.optional(v7.boolean(), false)
231
+ debug: v.optional(v.boolean(), false)
2383
232
  });
2384
233
 
2385
234
  // src/usage.ts
2386
235
  var OAUTH_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.oauthBetaHeader;
2387
- var ProfileResponseSchema = v8.object({
2388
- account: v8.object({
2389
- email: v8.optional(v8.string()),
2390
- has_claude_pro: v8.optional(v8.boolean(), false),
2391
- has_claude_max: v8.optional(v8.boolean(), false)
236
+ var ProfileResponseSchema = v2.object({
237
+ account: v2.object({
238
+ email: v2.optional(v2.string()),
239
+ has_claude_pro: v2.optional(v2.boolean(), false),
240
+ has_claude_max: v2.optional(v2.boolean(), false)
2392
241
  })
2393
242
  });
2394
243
  async function fetchUsage(accessToken) {
@@ -2403,7 +252,7 @@ async function fetchUsage(accessToken) {
2403
252
  if (!response.ok) {
2404
253
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2405
254
  }
2406
- const result = v8.safeParse(UsageLimitsSchema2, await response.json());
255
+ const result = v2.safeParse(UsageLimitsSchema, await response.json());
2407
256
  if (!result.success) {
2408
257
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2409
258
  }
@@ -2425,7 +274,7 @@ async function fetchProfile(accessToken) {
2425
274
  if (!response.ok) {
2426
275
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2427
276
  }
2428
- const result = v8.safeParse(ProfileResponseSchema, await response.json());
277
+ const result = v2.safeParse(ProfileResponseSchema, await response.json());
2429
278
  if (!result.success) {
2430
279
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2431
280
  }
@@ -2675,7 +524,11 @@ var AccountManager = createAccountManagerForProvider({
2675
524
  refreshToken
2676
525
  });
2677
526
 
527
+ // src/executor.ts
528
+ import { createExecutorForProvider as createExecutorForProvider2, getClearedOAuthBody } from "opencode-multi-account-core";
529
+
2678
530
  // src/rate-limit.ts
531
+ import { createRateLimitHandlers } from "opencode-multi-account-core";
2679
532
  var {
2680
533
  fetchUsageLimits,
2681
534
  getResetMsFromUsage,
@@ -2690,6 +543,7 @@ var {
2690
543
  });
2691
544
 
2692
545
  // src/pool-chain-executor.ts
546
+ import { createExecutorForProvider } from "opencode-multi-account-core";
2693
547
  function buildCascadePrompt(input, init) {
2694
548
  if (typeof init?.body === "string" && init.body.length > 0) {
2695
549
  return init.body;
@@ -2774,7 +628,7 @@ async function executeWithPoolChainRotation(manager, runtimeFactory, poolManager
2774
628
  }
2775
629
 
2776
630
  // src/executor.ts
2777
- var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider("Anthropic", {
631
+ var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider2("Anthropic", {
2778
632
  handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
2779
633
  manager,
2780
634
  client,
@@ -2799,8 +653,8 @@ async function clearAuthIfNoUsableAccount(manager, client) {
2799
653
  }).catch(() => {
2800
654
  });
2801
655
  }
2802
- function hasPoolChainEntries(config) {
2803
- return (config.pools?.length ?? 0) > 0 || (config.chains?.length ?? 0) > 0;
656
+ function hasPoolChainEntries(config2) {
657
+ return (config2.pools?.length ?? 0) > 0 || (config2.chains?.length ?? 0) > 0;
2804
658
  }
2805
659
  async function executeWithAccountRotation(manager, runtimeFactory, client, input, init, options) {
2806
660
  try {
@@ -2825,6 +679,21 @@ async function executeWithAccountRotation(manager, runtimeFactory, client, input
2825
679
  }
2826
680
  }
2827
681
 
682
+ // src/ui/ansi.ts
683
+ import {
684
+ ANSI,
685
+ isTTY,
686
+ parseKey
687
+ } from "opencode-multi-account-core";
688
+
689
+ // src/ui/select.ts
690
+ import {
691
+ select
692
+ } from "opencode-multi-account-core";
693
+
694
+ // src/ui/confirm.ts
695
+ import { confirm } from "opencode-multi-account-core";
696
+
2828
697
  // src/ui/auth-menu.ts
2829
698
  function formatRelativeTime(timestamp) {
2830
699
  if (!timestamp) return "never";
@@ -3030,12 +899,16 @@ function printQuotaError(account, error) {
3030
899
  }
3031
900
 
3032
901
  // src/account-store.ts
3033
- setAccountsFilename(ACCOUNTS_FILENAME2);
902
+ import {
903
+ AccountStore,
904
+ setAccountsFilename
905
+ } from "opencode-multi-account-core";
906
+ setAccountsFilename(ACCOUNTS_FILENAME);
3034
907
 
3035
908
  // src/auth-handler.ts
3036
- import { randomUUID as randomUUID2 } from "node:crypto";
3037
- import { createInterface } from "node:readline";
3038
- import { exec } from "node:child_process";
909
+ import { randomUUID } from "crypto";
910
+ import { createInterface } from "readline";
911
+ import { exec } from "child_process";
3039
912
  function makeFailedFlowResult(message) {
3040
913
  return {
3041
914
  url: "",
@@ -3175,7 +1048,7 @@ async function persistFallback(auth) {
3175
1048
  const store = new AccountStore();
3176
1049
  const now = Date.now();
3177
1050
  const account = {
3178
- uuid: randomUUID2(),
1051
+ uuid: randomUUID(),
3179
1052
  refreshToken: auth.refresh,
3180
1053
  accessToken: auth.access,
3181
1054
  expiresAt: auth.expires,
@@ -3352,7 +1225,122 @@ Retrying authentication for ${label}...
3352
1225
  }
3353
1226
 
3354
1227
  // src/request-transform.ts
3355
- import { createHash } from "node:crypto";
1228
+ import { createHash } from "crypto";
1229
+
1230
+ // src/model-config.ts
1231
+ function splitBetaFlags(value) {
1232
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
1233
+ }
1234
+ var config = {
1235
+ ccVersion: ANTHROPIC_OAUTH_ADAPTER.cliVersion,
1236
+ baseBetas: splitBetaFlags(ANTHROPIC_OAUTH_ADAPTER.requestBetaHeader),
1237
+ longContextBetas: ["context-1m-2025-08-07", "interleaved-thinking-2025-05-14"],
1238
+ modelOverrides: {
1239
+ "4-6": {
1240
+ add: ["effort-2025-11-24"]
1241
+ }
1242
+ }
1243
+ };
1244
+ function getCliVersion() {
1245
+ return process.env.ANTHROPIC_CLI_VERSION ?? config.ccVersion;
1246
+ }
1247
+ function getUserAgent() {
1248
+ if (process.env.ANTHROPIC_USER_AGENT) {
1249
+ return process.env.ANTHROPIC_USER_AGENT;
1250
+ }
1251
+ if (process.env.ANTHROPIC_CLI_VERSION) {
1252
+ return `claude-cli/${getCliVersion()} (external, cli)`;
1253
+ }
1254
+ return ANTHROPIC_OAUTH_ADAPTER.cliUserAgent;
1255
+ }
1256
+ function getRequiredBetas() {
1257
+ return splitBetaFlags(process.env.ANTHROPIC_BETA_FLAGS ?? config.baseBetas.join(","));
1258
+ }
1259
+ function getModelOverride(modelId) {
1260
+ const lowerModelId = modelId.toLowerCase();
1261
+ for (const [pattern, override] of Object.entries(config.modelOverrides)) {
1262
+ if (lowerModelId.includes(pattern)) {
1263
+ return override;
1264
+ }
1265
+ }
1266
+ return null;
1267
+ }
1268
+
1269
+ // src/betas.ts
1270
+ var LONG_CONTEXT_BETAS = config.longContextBetas;
1271
+ var excludedBetas = /* @__PURE__ */ new Map();
1272
+ var lastBetaFlagsEnv = process.env.ANTHROPIC_BETA_FLAGS;
1273
+ var lastModelId;
1274
+ function getExcludedBetas(modelId) {
1275
+ const currentBetaFlags = process.env.ANTHROPIC_BETA_FLAGS;
1276
+ if (currentBetaFlags !== lastBetaFlagsEnv) {
1277
+ excludedBetas.clear();
1278
+ lastBetaFlagsEnv = currentBetaFlags;
1279
+ }
1280
+ if (lastModelId !== void 0 && lastModelId !== modelId) {
1281
+ excludedBetas.clear();
1282
+ }
1283
+ lastModelId = modelId;
1284
+ return excludedBetas.get(modelId) ?? /* @__PURE__ */ new Set();
1285
+ }
1286
+ function addExcludedBeta(modelId, beta) {
1287
+ const nextExcludedBetas = excludedBetas.get(modelId) ?? /* @__PURE__ */ new Set();
1288
+ nextExcludedBetas.add(beta);
1289
+ excludedBetas.set(modelId, nextExcludedBetas);
1290
+ }
1291
+ function isLongContextError(responseBody) {
1292
+ return responseBody.includes("Extra usage is required for long context requests") || responseBody.includes("long context beta is not yet available");
1293
+ }
1294
+ function getNextBetaToExclude(modelId) {
1295
+ const excluded = getExcludedBetas(modelId);
1296
+ for (const beta of LONG_CONTEXT_BETAS) {
1297
+ if (!excluded.has(beta)) {
1298
+ return beta;
1299
+ }
1300
+ }
1301
+ return null;
1302
+ }
1303
+ function supports1mContext(modelId) {
1304
+ const lowerModelId = modelId.toLowerCase();
1305
+ if (!lowerModelId.includes("opus") && !lowerModelId.includes("sonnet")) {
1306
+ return false;
1307
+ }
1308
+ const versionMatch = lowerModelId.match(/(opus|sonnet)-(\d+)-(\d+)/);
1309
+ if (!versionMatch) {
1310
+ return false;
1311
+ }
1312
+ const major = Number.parseInt(versionMatch[2] ?? "0", 10);
1313
+ const minor = Number.parseInt(versionMatch[3] ?? "0", 10);
1314
+ const effectiveMinor = minor > 99 ? 0 : minor;
1315
+ return major > 4 || major === 4 && effectiveMinor >= 6;
1316
+ }
1317
+ function getModelBetas(modelId, excluded) {
1318
+ const betas = [...getRequiredBetas()];
1319
+ const longContextBeta = config.longContextBetas[0];
1320
+ if (longContextBeta && process.env.ANTHROPIC_ENABLE_1M_CONTEXT === "true" && supports1mContext(modelId)) {
1321
+ betas.push(longContextBeta);
1322
+ }
1323
+ const override = getModelOverride(modelId);
1324
+ if (override?.exclude) {
1325
+ for (const excludedBeta of override.exclude) {
1326
+ const index = betas.indexOf(excludedBeta);
1327
+ if (index !== -1) {
1328
+ betas.splice(index, 1);
1329
+ }
1330
+ }
1331
+ }
1332
+ if (override?.add) {
1333
+ for (const addedBeta of override.add) {
1334
+ if (!betas.includes(addedBeta)) {
1335
+ betas.push(addedBeta);
1336
+ }
1337
+ }
1338
+ }
1339
+ if (!excluded || excluded.size === 0) {
1340
+ return betas;
1341
+ }
1342
+ return betas.filter((beta) => !excluded.has(beta));
1343
+ }
3356
1344
 
3357
1345
  // src/anthropic-prompt.ts
3358
1346
  var SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
@@ -3565,7 +1553,7 @@ function processCompleteLines(buffer) {
3565
1553
  `;
3566
1554
  return { output, remaining };
3567
1555
  }
3568
- function buildRequestHeaders(input, init, accessToken) {
1556
+ function buildRequestHeaders(input, init, accessToken, modelId = "unknown", excludedBetas2) {
3569
1557
  const headers = new Headers();
3570
1558
  if (input instanceof Request) {
3571
1559
  input.headers.forEach((value, key) => {
@@ -3588,13 +1576,14 @@ function buildRequestHeaders(input, init, accessToken) {
3588
1576
  }
3589
1577
  }
3590
1578
  const incomingBetas = (headers.get("anthropic-beta") || "").split(",").map((b) => b.trim()).filter(Boolean);
1579
+ const modelBetas = getModelBetas(modelId, excludedBetas2);
3591
1580
  const mergedBetas = [.../* @__PURE__ */ new Set([
3592
- ...ANTHROPIC_BETA_HEADER.split(","),
1581
+ ...modelBetas,
3593
1582
  ...incomingBetas
3594
1583
  ])].join(",");
3595
1584
  headers.set("authorization", `Bearer ${accessToken}`);
3596
1585
  headers.set("anthropic-beta", mergedBetas);
3597
- headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
1586
+ headers.set("user-agent", getUserAgent());
3598
1587
  headers.set("anthropic-dangerous-direct-browser-access", "true");
3599
1588
  headers.set("x-app", "cli");
3600
1589
  headers.delete("x-api-key");
@@ -3639,6 +1628,17 @@ function transformRequestBody(body) {
3639
1628
  return body;
3640
1629
  }
3641
1630
  }
1631
+ function extractModelIdFromBody(body) {
1632
+ if (typeof body !== "string") {
1633
+ return "unknown";
1634
+ }
1635
+ try {
1636
+ const parsed = JSON.parse(body);
1637
+ return parsed.model ?? "unknown";
1638
+ } catch {
1639
+ return "unknown";
1640
+ }
1641
+ }
3642
1642
  function transformRequestUrl(input) {
3643
1643
  let url = null;
3644
1644
  try {
@@ -3705,6 +1705,7 @@ function createResponseStreamTransform(response) {
3705
1705
  }
3706
1706
 
3707
1707
  // src/proactive-refresh.ts
1708
+ import { createProactiveRefreshQueueForProvider } from "opencode-multi-account-core";
3708
1709
  var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
3709
1710
  providerAuthId: "anthropic",
3710
1711
  getConfig,
@@ -3714,12 +1715,15 @@ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
3714
1715
  });
3715
1716
 
3716
1717
  // src/runtime-factory.ts
1718
+ import { TokenRefreshError } from "opencode-multi-account-core";
3717
1719
  var TOKEN_REFRESH_PERMANENT_FAILURE_STATUS = 401;
3718
1720
  var AccountRuntimeFactory = class {
3719
1721
  constructor(store, client) {
3720
1722
  this.store = store;
3721
1723
  this.client = client;
3722
1724
  }
1725
+ store;
1726
+ client;
3723
1727
  runtimes = /* @__PURE__ */ new Map();
3724
1728
  initLocks = /* @__PURE__ */ new Map();
3725
1729
  async getRuntime(uuid) {
@@ -3775,13 +1779,41 @@ var AccountRuntimeFactory = class {
3775
1779
  }
3776
1780
  async executeTransformedFetch(input, init, accessToken) {
3777
1781
  const transformedInput = transformRequestUrl(input);
3778
- const headers = buildRequestHeaders(transformedInput, init, accessToken);
1782
+ const modelId = extractModelIdFromBody(init?.body);
1783
+ const excludedBetas2 = getExcludedBetas(modelId);
1784
+ const headers = buildRequestHeaders(transformedInput, init, accessToken, modelId, excludedBetas2);
3779
1785
  const transformedBody = typeof init?.body === "string" ? transformRequestBody(init.body) : init?.body;
3780
- const response = await fetch(transformedInput, {
1786
+ let response = await fetch(transformedInput, {
3781
1787
  ...init,
3782
1788
  headers,
3783
1789
  body: transformedBody
3784
1790
  });
1791
+ for (let attempt = 0; attempt < LONG_CONTEXT_BETAS.length; attempt += 1) {
1792
+ if (response.status !== 400 && response.status !== 429) {
1793
+ break;
1794
+ }
1795
+ const responseBody = await response.clone().text();
1796
+ if (!isLongContextError(responseBody)) {
1797
+ break;
1798
+ }
1799
+ const betaToExclude = getNextBetaToExclude(modelId);
1800
+ if (!betaToExclude) {
1801
+ break;
1802
+ }
1803
+ addExcludedBeta(modelId, betaToExclude);
1804
+ const retryHeaders = buildRequestHeaders(
1805
+ transformedInput,
1806
+ init,
1807
+ accessToken,
1808
+ modelId,
1809
+ getExcludedBetas(modelId)
1810
+ );
1811
+ response = await fetch(transformedInput, {
1812
+ ...init,
1813
+ headers: retryHeaders,
1814
+ body: transformedBody
1815
+ });
1816
+ }
3785
1817
  return createResponseStreamTransform(response);
3786
1818
  }
3787
1819
  async createRuntime(uuid) {
@@ -3806,7 +1838,91 @@ var AccountRuntimeFactory = class {
3806
1838
  }
3807
1839
  };
3808
1840
 
1841
+ // src/bootstrap-auth.ts
1842
+ import { promises as fs } from "fs";
1843
+ import { join } from "path";
1844
+ import { getConfigDir as getConfigDir2 } from "opencode-multi-account-core";
1845
+ var AUTH_JSON_FILENAME = "auth.json";
1846
+ function hasCompleteOAuthCredential(account) {
1847
+ return typeof account.refreshToken === "string" && account.refreshToken.length > 0 && typeof account.accessToken === "string" && account.accessToken.length > 0 && typeof account.expiresAt === "number" && Number.isFinite(account.expiresAt);
1848
+ }
1849
+ function selectBootstrapAccount(accounts, activeAccountUuid) {
1850
+ const completeAccounts = accounts.filter(hasCompleteOAuthCredential);
1851
+ if (completeAccounts.length === 0) {
1852
+ return null;
1853
+ }
1854
+ const activeAccount = activeAccountUuid ? completeAccounts.find((account) => account.uuid === activeAccountUuid) : void 0;
1855
+ if (activeAccount) {
1856
+ return activeAccount;
1857
+ }
1858
+ const firstUsableAccount = completeAccounts.find(
1859
+ (account) => account.enabled !== false && account.isAuthDisabled !== true
1860
+ );
1861
+ return firstUsableAccount ?? completeAccounts[0] ?? null;
1862
+ }
1863
+ async function readCurrentAuth(providerId) {
1864
+ const authPath = join(getConfigDir2(), AUTH_JSON_FILENAME);
1865
+ let raw;
1866
+ try {
1867
+ raw = await fs.readFile(authPath, "utf-8");
1868
+ } catch {
1869
+ return null;
1870
+ }
1871
+ try {
1872
+ const parsed = JSON.parse(raw);
1873
+ const providerAuth = parsed[providerId];
1874
+ if (providerAuth?.type !== "oauth" || typeof providerAuth.refresh !== "string" || typeof providerAuth.access !== "string" || typeof providerAuth.expires !== "number") {
1875
+ return null;
1876
+ }
1877
+ return {
1878
+ type: "oauth",
1879
+ refresh: providerAuth.refresh,
1880
+ access: providerAuth.access,
1881
+ expires: providerAuth.expires
1882
+ };
1883
+ } catch {
1884
+ return null;
1885
+ }
1886
+ }
1887
+ function shouldSyncBootstrapAuth(currentAuth, nextAuth) {
1888
+ if (!currentAuth) {
1889
+ return true;
1890
+ }
1891
+ if (currentAuth.refresh === nextAuth.refresh && currentAuth.access === nextAuth.access && currentAuth.expires === nextAuth.expires) {
1892
+ return false;
1893
+ }
1894
+ return currentAuth.expires < nextAuth.expires;
1895
+ }
1896
+ async function syncBootstrapAuth(client, store) {
1897
+ const storage = await store.load();
1898
+ const bootstrapAccount = selectBootstrapAccount(storage.accounts, storage.activeAccountUuid);
1899
+ if (!bootstrapAccount) {
1900
+ return false;
1901
+ }
1902
+ const nextAuth = {
1903
+ type: "oauth",
1904
+ refresh: bootstrapAccount.refreshToken,
1905
+ access: bootstrapAccount.accessToken,
1906
+ expires: bootstrapAccount.expiresAt
1907
+ };
1908
+ const currentAuth = await readCurrentAuth(ANTHROPIC_OAUTH_ADAPTER.authProviderId);
1909
+ if (!shouldSyncBootstrapAuth(currentAuth, nextAuth)) {
1910
+ return false;
1911
+ }
1912
+ await client.auth.set({
1913
+ path: { id: ANTHROPIC_OAUTH_ADAPTER.authProviderId },
1914
+ body: nextAuth
1915
+ });
1916
+ return true;
1917
+ }
1918
+
3809
1919
  // src/index.ts
1920
+ var EMPTY_OAUTH_CREDENTIALS = {
1921
+ type: "oauth",
1922
+ refresh: "",
1923
+ access: "",
1924
+ expires: 0
1925
+ };
3810
1926
  function extractFirstUserText(input) {
3811
1927
  try {
3812
1928
  const raw = input;
@@ -3839,12 +1955,65 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3839
1955
  const { client } = ctx;
3840
1956
  await loadConfig();
3841
1957
  const store = new AccountStore();
1958
+ await syncBootstrapAuth(client, store).catch(() => {
1959
+ });
3842
1960
  let manager = null;
3843
1961
  let runtimeFactory = null;
3844
1962
  let refreshQueue = null;
3845
1963
  let poolManager = null;
3846
1964
  let cascadeStateManager = null;
3847
1965
  let poolChainConfig = { pools: [], chains: [] };
1966
+ async function ensureExecutionInfrastructure() {
1967
+ runtimeFactory ??= new AccountRuntimeFactory(store, client);
1968
+ poolChainConfig = await loadPoolChainConfig();
1969
+ poolManager ??= new PoolManager();
1970
+ poolManager.loadPools(poolChainConfig.pools);
1971
+ cascadeStateManager ??= new CascadeStateManager();
1972
+ if (manager) {
1973
+ manager.setRuntimeFactory(runtimeFactory);
1974
+ manager.setClient(client);
1975
+ }
1976
+ }
1977
+ async function startRefreshQueueIfNeeded() {
1978
+ if (!manager || manager.getAccountCount() === 0) {
1979
+ return;
1980
+ }
1981
+ await ensureExecutionInfrastructure();
1982
+ if (refreshQueue) {
1983
+ return;
1984
+ }
1985
+ refreshQueue = new ProactiveRefreshQueue(
1986
+ client,
1987
+ store,
1988
+ (uuid) => {
1989
+ runtimeFactory?.invalidate(uuid);
1990
+ void manager?.refresh();
1991
+ }
1992
+ );
1993
+ refreshQueue.start();
1994
+ }
1995
+ async function initializeManagerFromStore() {
1996
+ if (manager) {
1997
+ return manager.getAccountCount() > 0;
1998
+ }
1999
+ const storage = await store.load();
2000
+ if (storage.accounts.length === 0) {
2001
+ return false;
2002
+ }
2003
+ manager = await AccountManager.create(store, EMPTY_OAUTH_CREDENTIALS, client);
2004
+ await ensureExecutionInfrastructure();
2005
+ await startRefreshQueueIfNeeded();
2006
+ return manager.getAccountCount() > 0;
2007
+ }
2008
+ async function initializeManagerFromAuth(credentials) {
2009
+ if (!manager) {
2010
+ manager = await AccountManager.create(store, credentials, client);
2011
+ }
2012
+ await ensureExecutionInfrastructure();
2013
+ await startRefreshQueueIfNeeded();
2014
+ }
2015
+ await initializeManagerFromStore().catch(() => {
2016
+ });
3848
2017
  return {
3849
2018
  "experimental.chat.system.transform": (input, output) => {
3850
2019
  injectSystemPrompt(output);
@@ -3932,22 +2101,21 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3932
2101
  }
3933
2102
  const credentials = auth;
3934
2103
  await migrateFromAuthJson("anthropic", store);
3935
- manager = await AccountManager.create(store, credentials, client);
3936
- runtimeFactory = new AccountRuntimeFactory(store, client);
3937
- manager.setRuntimeFactory(runtimeFactory);
3938
- poolChainConfig = await loadPoolChainConfig();
3939
- poolManager = new PoolManager();
3940
- poolManager.loadPools(poolChainConfig.pools);
3941
- cascadeStateManager = new CascadeStateManager();
3942
- if (manager.getAccountCount() > 0) {
3943
- const activeLabel = manager.getActiveAccount() ? getAccountLabel(manager.getActiveAccount()) : "none";
2104
+ await initializeManagerFromAuth(credentials);
2105
+ const initializedManager = manager;
2106
+ if (!initializedManager) {
2107
+ return { apiKey: "", fetch };
2108
+ }
2109
+ if (initializedManager.getAccountCount() > 0) {
2110
+ const activeAccount = initializedManager.getActiveAccount();
2111
+ const activeLabel = activeAccount ? getAccountLabel(activeAccount) : "none";
3944
2112
  void showToast(
3945
2113
  client,
3946
- `Multi-Auth: ${manager.getAccountCount()} account(s) loaded. Active: ${activeLabel}`,
2114
+ `Multi-Auth: ${initializedManager.getAccountCount()} account(s) loaded. Active: ${activeLabel}`,
3947
2115
  "info"
3948
2116
  );
3949
- await manager.validateNonActiveTokens(client);
3950
- const disabledCount = manager.getAccounts().filter((a) => a.isAuthDisabled).length;
2117
+ await initializedManager.validateNonActiveTokens(client);
2118
+ const disabledCount = initializedManager.getAccounts().filter((a) => a.isAuthDisabled).length;
3951
2119
  if (disabledCount > 0) {
3952
2120
  void showToast(
3953
2121
  client,
@@ -3955,32 +2123,20 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3955
2123
  "warning"
3956
2124
  );
3957
2125
  }
3958
- if (refreshQueue) {
3959
- await refreshQueue.stop();
3960
- }
3961
- refreshQueue = new ProactiveRefreshQueue(
3962
- client,
3963
- store,
3964
- (uuid) => {
3965
- runtimeFactory?.invalidate(uuid);
3966
- void manager?.refresh();
3967
- }
3968
- );
3969
- refreshQueue.start();
3970
2126
  }
3971
2127
  return {
3972
2128
  apiKey: "",
3973
2129
  "chat.headers": async (input, output) => {
3974
2130
  if (input.provider?.info?.id !== ANTHROPIC_OAUTH_ADAPTER.authProviderId) return;
3975
- output.headers["user-agent"] = CLAUDE_CLI_USER_AGENT;
2131
+ output.headers["user-agent"] = getUserAgent();
3976
2132
  output.headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER;
3977
2133
  output.headers["x-app"] = "cli";
3978
2134
  },
3979
2135
  async fetch(input, init) {
3980
- if (!manager || !runtimeFactory) {
2136
+ if (!initializedManager || !runtimeFactory) {
3981
2137
  return fetch(input, init);
3982
2138
  }
3983
- if (manager.getAccountCount() === 0) {
2139
+ if (initializedManager.getAccountCount() === 0) {
3984
2140
  throw new Error(
3985
2141
  "No Anthropic accounts configured. Run `opencode auth login` to add an account."
3986
2142
  );
@@ -3991,7 +2147,7 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3991
2147
  cascadeStateManager = new CascadeStateManager();
3992
2148
  }
3993
2149
  return executeWithAccountRotation(
3994
- manager,
2150
+ initializedManager,
3995
2151
  runtimeFactory,
3996
2152
  client,
3997
2153
  input,
@@ -4011,3 +2167,4 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
4011
2167
  export {
4012
2168
  ClaudeMultiAuthPlugin
4013
2169
  };
2170
+ //# sourceMappingURL=index.js.map