opencode-multi-account-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +57 -0
  2. package/dist/index.js +1798 -0
  3. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,1798 @@
1
+ // src/account-manager.ts
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ // src/claims.ts
5
+ import { promises as fs2 } from "node:fs";
6
+ import { randomBytes as randomBytes2 } from "node:crypto";
7
+ import { dirname as dirname2, join as join3 } from "node:path";
8
+
9
+ // src/utils.ts
10
+ import { join as join2 } from "node:path";
11
+ import { homedir as homedir2 } from "node:os";
12
+
13
+ // src/config.ts
14
+ import { promises as fs } from "node:fs";
15
+ import { randomBytes } from "node:crypto";
16
+ import { dirname, join } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import * as v2 from "valibot";
19
+
20
+ // src/types.ts
21
+ import * as v from "valibot";
22
+ var OAuthCredentialsSchema = v.object({
23
+ type: v.literal("oauth"),
24
+ refresh: v.string(),
25
+ access: v.string(),
26
+ expires: v.number()
27
+ });
28
+ var UsageLimitEntrySchema = v.object({
29
+ utilization: v.number(),
30
+ resets_at: v.nullable(v.string())
31
+ });
32
+ var UsageLimitsSchema = v.object({
33
+ five_hour: v.optional(v.nullable(UsageLimitEntrySchema), null),
34
+ seven_day: v.optional(v.nullable(UsageLimitEntrySchema), null),
35
+ seven_day_sonnet: v.optional(v.nullable(UsageLimitEntrySchema), null)
36
+ });
37
+ var CredentialRefreshPatchSchema = v.object({
38
+ accessToken: v.string(),
39
+ expiresAt: v.number(),
40
+ refreshToken: v.optional(v.string()),
41
+ uuid: v.optional(v.string()),
42
+ accountId: v.optional(v.string()),
43
+ email: v.optional(v.string())
44
+ });
45
+ var StoredAccountSchema = v.object({
46
+ uuid: v.optional(v.string()),
47
+ accountId: v.optional(v.string()),
48
+ label: v.optional(v.string()),
49
+ email: v.optional(v.string()),
50
+ planTier: v.optional(v.string(), ""),
51
+ refreshToken: v.string(),
52
+ accessToken: v.optional(v.string()),
53
+ expiresAt: v.optional(v.number()),
54
+ addedAt: v.number(),
55
+ lastUsed: v.number(),
56
+ enabled: v.optional(v.boolean(), true),
57
+ rateLimitResetAt: v.optional(v.number()),
58
+ cachedUsage: v.optional(UsageLimitsSchema),
59
+ cachedUsageAt: v.optional(v.number()),
60
+ consecutiveAuthFailures: v.optional(v.number(), 0),
61
+ isAuthDisabled: v.optional(v.boolean(), false),
62
+ authDisabledReason: v.optional(v.string())
63
+ });
64
+ var AccountStorageSchema = v.object({
65
+ version: v.literal(1),
66
+ accounts: v.optional(v.array(StoredAccountSchema), []),
67
+ activeAccountUuid: v.optional(v.string())
68
+ });
69
+ var AccountSelectionStrategySchema = v.picklist(["sticky", "round-robin", "hybrid"]);
70
+ var PluginConfigSchema = v.object({
71
+ account_selection_strategy: v.optional(AccountSelectionStrategySchema, "sticky"),
72
+ cross_process_claims: v.optional(v.boolean(), true),
73
+ soft_quota_threshold_percent: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100)), 100),
74
+ rate_limit_min_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
75
+ default_retry_after_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 6e4),
76
+ max_consecutive_auth_failures: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1)), 3),
77
+ token_failure_backoff_ms: v.optional(v.pipe(v.number(), v.minValue(0)), 3e4),
78
+ proactive_refresh: v.optional(v.boolean(), true),
79
+ proactive_refresh_buffer_seconds: v.optional(v.pipe(v.number(), v.minValue(60)), 1800),
80
+ proactive_refresh_interval_seconds: v.optional(v.pipe(v.number(), v.minValue(30)), 300),
81
+ quiet_mode: v.optional(v.boolean(), false),
82
+ debug: v.optional(v.boolean(), false)
83
+ });
84
+
85
+ // src/config.ts
86
+ var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
87
+ var DEFAULT_CONFIG = v2.parse(PluginConfigSchema, {});
88
+ var configFilename = DEFAULT_CONFIG_FILENAME;
89
+ var cachedConfig = null;
90
+ var externalConfigGetter = null;
91
+ function getConfigDir() {
92
+ return process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
93
+ }
94
+ function getConfigPath() {
95
+ return join(getConfigDir(), configFilename);
96
+ }
97
+ function parseConfig(raw) {
98
+ const result = v2.safeParse(PluginConfigSchema, raw);
99
+ return result.success ? result.output : DEFAULT_CONFIG;
100
+ }
101
+ function initCoreConfig(filename) {
102
+ configFilename = filename || DEFAULT_CONFIG_FILENAME;
103
+ cachedConfig = null;
104
+ }
105
+ async function loadConfig() {
106
+ if (cachedConfig) return cachedConfig;
107
+ const path = getConfigPath();
108
+ try {
109
+ const content = await fs.readFile(path, "utf-8");
110
+ cachedConfig = parseConfig(JSON.parse(content));
111
+ } catch {
112
+ cachedConfig = DEFAULT_CONFIG;
113
+ }
114
+ return cachedConfig;
115
+ }
116
+ function getConfig() {
117
+ if (cachedConfig) return cachedConfig;
118
+ if (externalConfigGetter && externalConfigGetter !== getConfig) {
119
+ try {
120
+ return parseConfig(externalConfigGetter());
121
+ } catch {
122
+ return DEFAULT_CONFIG;
123
+ }
124
+ }
125
+ return DEFAULT_CONFIG;
126
+ }
127
+ function resetConfigCache() {
128
+ cachedConfig = null;
129
+ }
130
+ function setConfigGetter(getter) {
131
+ if (getter === getConfig) {
132
+ return;
133
+ }
134
+ externalConfigGetter = getter;
135
+ }
136
+ async function updateConfigField(key, value) {
137
+ const path = getConfigPath();
138
+ let existing = {};
139
+ try {
140
+ const content2 = await fs.readFile(path, "utf-8");
141
+ existing = JSON.parse(content2);
142
+ } catch {
143
+ }
144
+ existing[key] = value;
145
+ await fs.mkdir(dirname(path), { recursive: true });
146
+ const content = `${JSON.stringify(existing, null, 2)}
147
+ `;
148
+ const tempPath = `${path}.${randomBytes(8).toString("hex")}.tmp`;
149
+ try {
150
+ await fs.writeFile(tempPath, content, "utf-8");
151
+ await fs.rename(tempPath, path);
152
+ } catch (error) {
153
+ try {
154
+ await fs.unlink(tempPath);
155
+ } catch {
156
+ }
157
+ throw error;
158
+ }
159
+ cachedConfig = null;
160
+ await loadConfig();
161
+ }
162
+
163
+ // src/utils.ts
164
+ function getConfigDir2() {
165
+ return process.env.OPENCODE_CONFIG_DIR || join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "opencode");
166
+ }
167
+ function getErrorCode(error) {
168
+ if (typeof error !== "object" || error === null || !("code" in error)) {
169
+ return void 0;
170
+ }
171
+ const code = error.code;
172
+ return typeof code === "string" ? code : void 0;
173
+ }
174
+ function formatWaitTime(ms) {
175
+ const totalSeconds = Math.ceil(ms / 1e3);
176
+ if (totalSeconds < 60) return `${totalSeconds}s`;
177
+ const days = Math.floor(totalSeconds / 86400);
178
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
179
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
180
+ const seconds = totalSeconds % 60;
181
+ const parts = [];
182
+ if (days > 0) parts.push(`${days}d`);
183
+ if (hours > 0) parts.push(`${hours}h`);
184
+ if (minutes > 0) parts.push(`${minutes}m`);
185
+ if (seconds > 0 && days === 0) parts.push(`${seconds}s`);
186
+ return parts.join(" ") || "0s";
187
+ }
188
+ function getAccountLabel(account) {
189
+ if (account.label) return account.label;
190
+ if (account.email) return account.email;
191
+ if (account.uuid) return `Account (${account.uuid.slice(0, 8)})`;
192
+ return `Account ${account.index + 1}`;
193
+ }
194
+ function sleep(ms) {
195
+ return new Promise((resolve) => setTimeout(resolve, ms));
196
+ }
197
+ async function showToast(client, message, variant) {
198
+ if (getConfig().quiet_mode) return;
199
+ try {
200
+ await client.tui.showToast({ body: { message, variant } });
201
+ } catch {
202
+ }
203
+ }
204
+ function debugLog(client, message, extra) {
205
+ if (!getConfig().debug) return;
206
+ client.app.log({
207
+ body: { service: "claude-multiauth", level: "debug", message, extra }
208
+ }).catch(() => {
209
+ });
210
+ }
211
+ function createMinimalClient() {
212
+ return {
213
+ auth: {
214
+ set: async () => {
215
+ }
216
+ },
217
+ tui: {
218
+ showToast: async () => {
219
+ }
220
+ },
221
+ app: {
222
+ log: async () => {
223
+ }
224
+ }
225
+ };
226
+ }
227
+
228
+ // src/claims.ts
229
+ var CLAIMS_FILENAME = "multiauth-claims.json";
230
+ var CLAIM_EXPIRY_MS = 6e4;
231
+ function getClaimsPath() {
232
+ return join3(getConfigDir2(), CLAIMS_FILENAME);
233
+ }
234
+ function isClaimShape(value) {
235
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
236
+ const claim = value;
237
+ return typeof claim.pid === "number" && Number.isInteger(claim.pid) && claim.pid > 0 && typeof claim.at === "number" && Number.isFinite(claim.at);
238
+ }
239
+ function parseClaims(raw) {
240
+ let parsed;
241
+ try {
242
+ parsed = JSON.parse(raw);
243
+ } catch {
244
+ return {};
245
+ }
246
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
247
+ return {};
248
+ }
249
+ const claims = {};
250
+ for (const [accountId, claim] of Object.entries(parsed)) {
251
+ if (isClaimShape(claim)) {
252
+ claims[accountId] = claim;
253
+ }
254
+ }
255
+ return claims;
256
+ }
257
+ function isProcessAlive(pid) {
258
+ try {
259
+ process.kill(pid, 0);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ function cleanClaims(claims, now) {
266
+ const cleaned = {};
267
+ let changed = false;
268
+ for (const [accountId, claim] of Object.entries(claims)) {
269
+ const expiredByTime = now - claim.at > CLAIM_EXPIRY_MS;
270
+ const zombieClaim = !isProcessAlive(claim.pid);
271
+ if (expiredByTime || zombieClaim) {
272
+ changed = true;
273
+ continue;
274
+ }
275
+ cleaned[accountId] = claim;
276
+ }
277
+ return { cleaned, changed };
278
+ }
279
+ async function writeClaimsFile(claims) {
280
+ const path = getClaimsPath();
281
+ const tempPath = `${path}.${randomBytes2(6).toString("hex")}.tmp`;
282
+ await fs2.mkdir(dirname2(path), { recursive: true });
283
+ try {
284
+ await fs2.writeFile(tempPath, JSON.stringify(claims, null, 2), { encoding: "utf-8", mode: 384 });
285
+ await fs2.rename(tempPath, path);
286
+ } catch (error) {
287
+ try {
288
+ await fs2.unlink(tempPath);
289
+ } catch {
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+ async function readClaims() {
295
+ try {
296
+ const data = await fs2.readFile(getClaimsPath(), "utf-8");
297
+ const parsed = parseClaims(data);
298
+ const now = Date.now();
299
+ const { cleaned, changed } = cleanClaims(parsed, now);
300
+ if (changed) {
301
+ try {
302
+ await writeClaimsFile(cleaned);
303
+ } catch {
304
+ }
305
+ }
306
+ return cleaned;
307
+ } catch {
308
+ return {};
309
+ }
310
+ }
311
+ async function writeClaim(accountId) {
312
+ const now = Date.now();
313
+ const claims = await readClaims();
314
+ const { cleaned } = cleanClaims(claims, now);
315
+ cleaned[accountId] = { pid: process.pid, at: now };
316
+ try {
317
+ await writeClaimsFile(cleaned);
318
+ } catch {
319
+ }
320
+ }
321
+ async function releaseClaim(accountId) {
322
+ const now = Date.now();
323
+ const claims = await readClaims();
324
+ const { cleaned } = cleanClaims(claims, now);
325
+ const currentClaim = cleaned[accountId];
326
+ if (!currentClaim || currentClaim.pid !== process.pid) {
327
+ return;
328
+ }
329
+ delete cleaned[accountId];
330
+ try {
331
+ await writeClaimsFile(cleaned);
332
+ } catch {
333
+ }
334
+ }
335
+ function isClaimedByOther(claims, accountId) {
336
+ if (!accountId) return false;
337
+ const claim = claims[accountId];
338
+ if (!claim) return false;
339
+ if (Date.now() - claim.at > CLAIM_EXPIRY_MS) return false;
340
+ if (!isProcessAlive(claim.pid)) return false;
341
+ return claim.pid !== process.pid;
342
+ }
343
+
344
+ // src/account-manager.ts
345
+ var STARTUP_REFRESH_CONCURRENCY = 3;
346
+ var RECENT_429_COOLDOWN_MS = 3e4;
347
+ var HYBRID_SWITCH_MARGIN = 40;
348
+ function createAccountManagerForProvider(dependencies) {
349
+ const {
350
+ providerAuthId,
351
+ isTokenExpired,
352
+ refreshToken
353
+ } = dependencies;
354
+ return class AccountManager {
355
+ constructor(store) {
356
+ this.store = store;
357
+ }
358
+ cached = [];
359
+ activeAccountUuid;
360
+ client = null;
361
+ runtimeFactory = null;
362
+ roundRobinCursor = 0;
363
+ last429Map = /* @__PURE__ */ new Map();
364
+ static async create(store, currentAuth, client) {
365
+ const manager = new AccountManager(store);
366
+ await manager.initialize(currentAuth, client);
367
+ return manager;
368
+ }
369
+ async initialize(currentAuth, client) {
370
+ if (client) this.client = client;
371
+ const storage = await this.store.load();
372
+ if (storage.accounts.length > 0) {
373
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
374
+ this.activeAccountUuid = storage.activeAccountUuid;
375
+ if (!this.getActiveAccount() && this.cached.length > 0) {
376
+ this.activeAccountUuid = this.cached[0].uuid;
377
+ }
378
+ return;
379
+ }
380
+ if (currentAuth.refresh) {
381
+ const newAccount = this.createNewAccount(currentAuth, Date.now());
382
+ await this.store.addAccount(newAccount);
383
+ await this.store.setActiveUuid(newAccount.uuid);
384
+ this.cached = [this.toManagedAccount(newAccount, 0)];
385
+ this.activeAccountUuid = newAccount.uuid;
386
+ }
387
+ }
388
+ async refresh() {
389
+ const storage = await this.store.load();
390
+ this.cached = storage.accounts.map((account, index) => this.toManagedAccount(account, index));
391
+ if (storage.activeAccountUuid) {
392
+ this.activeAccountUuid = storage.activeAccountUuid;
393
+ }
394
+ }
395
+ toManagedAccount(storedAccount, index) {
396
+ return {
397
+ index,
398
+ uuid: storedAccount.uuid,
399
+ accountId: storedAccount.accountId,
400
+ label: storedAccount.label,
401
+ email: storedAccount.email,
402
+ planTier: storedAccount.planTier,
403
+ refreshToken: storedAccount.refreshToken,
404
+ accessToken: storedAccount.accessToken,
405
+ expiresAt: storedAccount.expiresAt,
406
+ addedAt: storedAccount.addedAt,
407
+ lastUsed: storedAccount.lastUsed,
408
+ enabled: storedAccount.enabled,
409
+ rateLimitResetAt: storedAccount.rateLimitResetAt,
410
+ cachedUsage: storedAccount.cachedUsage,
411
+ cachedUsageAt: storedAccount.cachedUsageAt,
412
+ consecutiveAuthFailures: storedAccount.consecutiveAuthFailures,
413
+ isAuthDisabled: storedAccount.isAuthDisabled,
414
+ authDisabledReason: storedAccount.authDisabledReason,
415
+ last429At: storedAccount.uuid ? this.last429Map.get(storedAccount.uuid) : void 0
416
+ };
417
+ }
418
+ createNewAccount(auth, now) {
419
+ return {
420
+ uuid: randomUUID(),
421
+ refreshToken: auth.refresh,
422
+ accessToken: auth.access,
423
+ expiresAt: auth.expires,
424
+ addedAt: now,
425
+ lastUsed: now,
426
+ enabled: true,
427
+ planTier: "",
428
+ consecutiveAuthFailures: 0,
429
+ isAuthDisabled: false
430
+ };
431
+ }
432
+ getAccountCount() {
433
+ return this.getEligibleAccounts().length;
434
+ }
435
+ getAccounts() {
436
+ return [...this.cached];
437
+ }
438
+ getActiveAccount() {
439
+ if (this.activeAccountUuid) {
440
+ return this.cached.find((account) => account.uuid === this.activeAccountUuid) ?? null;
441
+ }
442
+ return this.cached[0] ?? null;
443
+ }
444
+ setClient(client) {
445
+ this.client = client;
446
+ }
447
+ setRuntimeFactory(factory) {
448
+ this.runtimeFactory = factory;
449
+ }
450
+ getEligibleAccounts() {
451
+ return this.cached.filter((account) => account.uuid && account.enabled && !account.isAuthDisabled);
452
+ }
453
+ exceedsSoftQuota(account) {
454
+ const threshold = getConfig().soft_quota_threshold_percent;
455
+ if (threshold >= 100) return false;
456
+ const usage = account.cachedUsage;
457
+ if (!usage) return false;
458
+ const tiers = [usage.five_hour, usage.seven_day];
459
+ return tiers.some((tier) => tier != null && tier.utilization >= threshold);
460
+ }
461
+ hasAnyUsableAccount() {
462
+ return this.getEligibleAccounts().length > 0;
463
+ }
464
+ isRateLimited(account) {
465
+ if (account.rateLimitResetAt && Date.now() < account.rateLimitResetAt) {
466
+ return true;
467
+ }
468
+ return this.isUsageExhausted(account);
469
+ }
470
+ isUsageExhausted(account) {
471
+ const usage = account.cachedUsage;
472
+ if (!usage) return false;
473
+ const now = Date.now();
474
+ const tiers = [usage.five_hour, usage.seven_day];
475
+ return tiers.some(
476
+ (tier) => tier != null && tier.utilization >= 100 && tier.resets_at != null && Date.parse(tier.resets_at) > now
477
+ );
478
+ }
479
+ clearExpiredRateLimits() {
480
+ const now = Date.now();
481
+ for (const account of this.cached) {
482
+ if (account.rateLimitResetAt && now >= account.rateLimitResetAt) {
483
+ account.rateLimitResetAt = void 0;
484
+ }
485
+ }
486
+ }
487
+ getMinWaitTime() {
488
+ const eligible = this.getEligibleAccounts();
489
+ const available = eligible.filter((account) => !this.isRateLimited(account));
490
+ if (available.length > 0) return 0;
491
+ const now = Date.now();
492
+ const waits = [];
493
+ for (const account of eligible) {
494
+ if (account.rateLimitResetAt) {
495
+ const ms = account.rateLimitResetAt - now;
496
+ if (ms > 0) waits.push(ms);
497
+ }
498
+ const usageResetMs = this.getUsageResetMs(account);
499
+ if (usageResetMs !== null && usageResetMs > 0) {
500
+ waits.push(usageResetMs);
501
+ }
502
+ }
503
+ return waits.length > 0 ? Math.min(...waits) : 0;
504
+ }
505
+ getUsageResetMs(account) {
506
+ const usage = account.cachedUsage;
507
+ if (!usage) return null;
508
+ const now = Date.now();
509
+ const candidates = [];
510
+ const tiers = [usage.five_hour, usage.seven_day];
511
+ for (const tier of tiers) {
512
+ if (tier != null && tier.utilization >= 100 && tier.resets_at != null) {
513
+ const ms = Date.parse(tier.resets_at) - now;
514
+ if (ms > 0) candidates.push(ms);
515
+ }
516
+ }
517
+ return candidates.length > 0 ? Math.min(...candidates) : null;
518
+ }
519
+ async selectAccount() {
520
+ await this.refresh();
521
+ this.clearExpiredRateLimits();
522
+ const eligible = this.getEligibleAccounts();
523
+ if (eligible.length === 0) return null;
524
+ const config = getConfig();
525
+ const claims = config.cross_process_claims ? await readClaims() : {};
526
+ const strategy = config.account_selection_strategy;
527
+ let selected;
528
+ switch (strategy) {
529
+ case "round-robin":
530
+ selected = this.selectRoundRobin(eligible, claims);
531
+ break;
532
+ case "hybrid":
533
+ selected = this.selectHybrid(eligible, claims);
534
+ break;
535
+ case "sticky":
536
+ default:
537
+ selected = this.selectSticky(eligible, claims);
538
+ break;
539
+ }
540
+ if (selected?.uuid) {
541
+ this.activeAccountUuid = selected.uuid;
542
+ this.store.setActiveUuid(selected.uuid).catch(() => {
543
+ });
544
+ }
545
+ if (config.cross_process_claims && selected?.uuid) {
546
+ writeClaim(selected.uuid).catch(() => {
547
+ });
548
+ }
549
+ return selected;
550
+ }
551
+ isUsable(account) {
552
+ return !this.isRateLimited(account) && !this.isInRecentCooldown(account) && !this.exceedsSoftQuota(account);
553
+ }
554
+ isInRecentCooldown(account) {
555
+ if (!account.last429At) return false;
556
+ return Date.now() - account.last429At < RECENT_429_COOLDOWN_MS;
557
+ }
558
+ fallbackNotRateLimited(eligible) {
559
+ const account = eligible.find((candidate) => !this.isRateLimited(candidate));
560
+ if (account) {
561
+ this.activateAccount(account);
562
+ return account;
563
+ }
564
+ return null;
565
+ }
566
+ selectSticky(eligible, claims) {
567
+ const current = this.getActiveAccount();
568
+ if (current?.enabled && !current.isAuthDisabled && this.isUsable(current)) {
569
+ this.activateAccount(current);
570
+ return current;
571
+ }
572
+ const unclaimed = eligible.find(
573
+ (account) => this.isUsable(account) && !isClaimedByOther(claims, account.uuid)
574
+ );
575
+ if (unclaimed) {
576
+ this.activateAccount(unclaimed);
577
+ return unclaimed;
578
+ }
579
+ const available = eligible.find((account) => this.isUsable(account));
580
+ if (available) {
581
+ this.activateAccount(available);
582
+ return available;
583
+ }
584
+ return this.fallbackNotRateLimited(eligible);
585
+ }
586
+ selectRoundRobin(eligible, claims) {
587
+ for (let i = 0; i < eligible.length; i++) {
588
+ const index = (this.roundRobinCursor + i) % eligible.length;
589
+ const account = eligible[index];
590
+ if (this.isUsable(account) && !isClaimedByOther(claims, account.uuid)) {
591
+ this.roundRobinCursor = (index + 1) % eligible.length;
592
+ this.activateAccount(account);
593
+ return account;
594
+ }
595
+ }
596
+ for (let i = 0; i < eligible.length; i++) {
597
+ const index = (this.roundRobinCursor + i) % eligible.length;
598
+ const account = eligible[index];
599
+ if (this.isUsable(account)) {
600
+ this.roundRobinCursor = (index + 1) % eligible.length;
601
+ this.activateAccount(account);
602
+ return account;
603
+ }
604
+ }
605
+ return this.fallbackNotRateLimited(eligible);
606
+ }
607
+ selectHybrid(eligible, claims) {
608
+ const usable = eligible.filter((account) => this.isUsable(account));
609
+ const pool = usable.length > 0 ? usable : eligible.filter((account) => !this.isRateLimited(account));
610
+ if (pool.length === 0) return null;
611
+ const activeUuid = this.activeAccountUuid;
612
+ let best = pool[0];
613
+ let bestScore = this.calculateHybridScore(best, best.uuid === activeUuid, claims);
614
+ for (let i = 1; i < pool.length; i++) {
615
+ const account = pool[i];
616
+ const score = this.calculateHybridScore(account, account.uuid === activeUuid, claims);
617
+ if (score > bestScore) {
618
+ best = account;
619
+ bestScore = score;
620
+ }
621
+ }
622
+ const current = pool.find((account) => account.uuid === activeUuid);
623
+ if (current && current !== best) {
624
+ const currentScore = this.calculateHybridScore(current, true, claims);
625
+ const bestWithoutStickiness = this.calculateHybridScore(best, false, claims);
626
+ if (bestWithoutStickiness <= currentScore + HYBRID_SWITCH_MARGIN) {
627
+ this.activateAccount(current);
628
+ return current;
629
+ }
630
+ }
631
+ this.activateAccount(best);
632
+ return best;
633
+ }
634
+ calculateHybridScore(account, isActive, claims) {
635
+ const maxUtilization = Math.min(100, Math.max(0, this.getMaxUtilization(account)));
636
+ const usageScore = (100 - maxUtilization) / 100 * 450;
637
+ const maxFailures = Math.max(1, getConfig().max_consecutive_auth_failures);
638
+ const healthScore = Math.max(0, (maxFailures - account.consecutiveAuthFailures) / maxFailures * 250);
639
+ const secondsSinceUsed = (Date.now() - account.lastUsed) / 1e3;
640
+ const freshnessScore = Math.min(secondsSinceUsed, 900) / 900 * 60;
641
+ const stickinessBonus = isActive ? 120 : 0;
642
+ const claimPenalty = isClaimedByOther(claims, account.uuid) ? -200 : 0;
643
+ return usageScore + healthScore + freshnessScore + stickinessBonus + claimPenalty;
644
+ }
645
+ getMaxUtilization(account) {
646
+ const usage = account.cachedUsage;
647
+ if (!usage) return 65;
648
+ const tiers = [usage.five_hour, usage.seven_day];
649
+ const utilizations = tiers.filter((tier) => tier != null).map((tier) => tier.utilization);
650
+ return utilizations.length > 0 ? Math.max(...utilizations) : 65;
651
+ }
652
+ activateAccount(account) {
653
+ this.activeAccountUuid = account.uuid;
654
+ account.lastUsed = Date.now();
655
+ }
656
+ async markRateLimited(uuid, backoffMs) {
657
+ const effectiveBackoff = backoffMs ?? getConfig().rate_limit_min_backoff_ms;
658
+ this.last429Map.set(uuid, Date.now());
659
+ await this.store.mutateAccount(uuid, (account) => {
660
+ account.rateLimitResetAt = Date.now() + effectiveBackoff;
661
+ });
662
+ }
663
+ async markRevoked(uuid) {
664
+ await this.store.mutateAccount(uuid, (account) => {
665
+ account.isAuthDisabled = true;
666
+ account.authDisabledReason = "OAuth token revoked (403)";
667
+ account.accessToken = void 0;
668
+ account.expiresAt = void 0;
669
+ });
670
+ this.runtimeFactory?.invalidate(uuid);
671
+ }
672
+ async markSuccess(uuid) {
673
+ this.last429Map.delete(uuid);
674
+ await this.store.mutateAccount(uuid, (account) => {
675
+ account.rateLimitResetAt = void 0;
676
+ account.consecutiveAuthFailures = 0;
677
+ account.lastUsed = Date.now();
678
+ });
679
+ }
680
+ syncToOpenCode(account) {
681
+ if (!this.client || !account.accessToken || !account.expiresAt) return;
682
+ this.client.auth.set({
683
+ path: { id: providerAuthId },
684
+ body: {
685
+ type: "oauth",
686
+ refresh: account.refreshToken,
687
+ access: account.accessToken,
688
+ expires: account.expiresAt
689
+ }
690
+ }).catch(() => {
691
+ });
692
+ }
693
+ async markAuthFailure(uuid, result) {
694
+ await this.store.mutateStorage((storage) => {
695
+ const account = storage.accounts.find((entry) => entry.uuid === uuid);
696
+ if (!account) return;
697
+ if (!result.ok && result.permanent) {
698
+ account.isAuthDisabled = true;
699
+ account.authDisabledReason = "Token permanently rejected (400/401/403)";
700
+ return;
701
+ }
702
+ account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
703
+ const maxFailures = getConfig().max_consecutive_auth_failures;
704
+ const usableCount = storage.accounts.filter(
705
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== uuid
706
+ ).length;
707
+ if (account.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
708
+ account.isAuthDisabled = true;
709
+ account.authDisabledReason = `${maxFailures} consecutive auth failures`;
710
+ }
711
+ });
712
+ }
713
+ async applyUsageCache(uuid, usage) {
714
+ await this.store.mutateAccount(uuid, (account) => {
715
+ account.cachedUsage = usage;
716
+ account.cachedUsageAt = Date.now();
717
+ });
718
+ }
719
+ async applyProfileCache(uuid, profile) {
720
+ await this.store.mutateAccount(uuid, (account) => {
721
+ account.email = profile.email ?? account.email;
722
+ account.planTier = profile.planTier;
723
+ });
724
+ }
725
+ async ensureValidToken(uuid, client) {
726
+ const credentials = await this.store.readCredentials(uuid);
727
+ if (!credentials) return { ok: false, permanent: true };
728
+ if (credentials.accessToken && credentials.expiresAt && !isTokenExpired(credentials)) {
729
+ return {
730
+ ok: true,
731
+ patch: { accessToken: credentials.accessToken, expiresAt: credentials.expiresAt }
732
+ };
733
+ }
734
+ const result = await refreshToken(credentials.refreshToken, uuid, client);
735
+ if (!result.ok) return result;
736
+ const updated = await this.store.mutateAccount(uuid, (account) => {
737
+ account.accessToken = result.patch.accessToken;
738
+ account.expiresAt = result.patch.expiresAt;
739
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
740
+ if (result.patch.uuid && result.patch.uuid !== uuid) account.uuid = result.patch.uuid;
741
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
742
+ if (result.patch.email) account.email = result.patch.email;
743
+ account.consecutiveAuthFailures = 0;
744
+ account.isAuthDisabled = false;
745
+ account.authDisabledReason = void 0;
746
+ });
747
+ if (result.patch.uuid && result.patch.uuid !== uuid && this.activeAccountUuid === uuid) {
748
+ this.activeAccountUuid = result.patch.uuid;
749
+ this.store.setActiveUuid(result.patch.uuid).catch(() => {
750
+ });
751
+ }
752
+ if (updated && (uuid === this.activeAccountUuid || updated.uuid === this.activeAccountUuid)) {
753
+ this.syncToOpenCode(updated);
754
+ }
755
+ return result;
756
+ }
757
+ async validateNonActiveTokens(client) {
758
+ await this.refresh();
759
+ const activeUuid = this.activeAccountUuid;
760
+ const eligible = this.cached.filter(
761
+ (account) => account.enabled && !account.isAuthDisabled && account.uuid && account.uuid !== activeUuid
762
+ );
763
+ for (let i = 0; i < eligible.length; i += STARTUP_REFRESH_CONCURRENCY) {
764
+ const batch = eligible.slice(i, i + STARTUP_REFRESH_CONCURRENCY);
765
+ await Promise.all(
766
+ batch.map(async (account) => {
767
+ if (!account.uuid || !isTokenExpired(account)) return;
768
+ const result = await this.ensureValidToken(account.uuid, client);
769
+ if (!result.ok) {
770
+ await this.markAuthFailure(account.uuid, result);
771
+ }
772
+ })
773
+ );
774
+ }
775
+ }
776
+ async removeAccount(index) {
777
+ const account = this.cached[index];
778
+ if (!account?.uuid) return false;
779
+ const removed = await this.store.removeAccount(account.uuid);
780
+ if (removed) {
781
+ await this.refresh();
782
+ }
783
+ return removed;
784
+ }
785
+ async clearAllAccounts() {
786
+ await this.store.clear();
787
+ this.cached = [];
788
+ this.activeAccountUuid = void 0;
789
+ }
790
+ async addAccount(auth) {
791
+ if (!auth.refresh) return;
792
+ const existing = this.cached.find((account) => account.refreshToken === auth.refresh);
793
+ if (existing) return;
794
+ const newAccount = this.createNewAccount(auth, Date.now());
795
+ await this.store.addAccount(newAccount);
796
+ this.activeAccountUuid = newAccount.uuid;
797
+ await this.store.setActiveUuid(newAccount.uuid);
798
+ await this.refresh();
799
+ }
800
+ async toggleEnabled(uuid) {
801
+ await this.store.mutateAccount(uuid, (account) => {
802
+ account.enabled = !(account.enabled ?? true);
803
+ if (account.enabled) {
804
+ account.isAuthDisabled = false;
805
+ account.authDisabledReason = void 0;
806
+ account.consecutiveAuthFailures = 0;
807
+ }
808
+ });
809
+ }
810
+ async replaceAccountCredentials(uuid, auth) {
811
+ const updated = await this.store.mutateAccount(uuid, (account) => {
812
+ account.refreshToken = auth.refresh;
813
+ account.accessToken = auth.access;
814
+ account.expiresAt = auth.expires;
815
+ account.lastUsed = Date.now();
816
+ account.enabled = true;
817
+ account.isAuthDisabled = false;
818
+ account.authDisabledReason = void 0;
819
+ account.consecutiveAuthFailures = 0;
820
+ account.rateLimitResetAt = void 0;
821
+ });
822
+ this.runtimeFactory?.invalidate(uuid);
823
+ if (updated && uuid === this.activeAccountUuid) {
824
+ this.syncToOpenCode(updated);
825
+ }
826
+ }
827
+ async retryAuth(uuid, client) {
828
+ await this.store.mutateAccount(uuid, (account) => {
829
+ account.consecutiveAuthFailures = 0;
830
+ account.isAuthDisabled = false;
831
+ account.authDisabledReason = void 0;
832
+ });
833
+ this.runtimeFactory?.invalidate(uuid);
834
+ const credentials = await this.store.readCredentials(uuid);
835
+ if (!credentials) return { ok: false, permanent: true };
836
+ const result = await refreshToken(credentials.refreshToken, uuid, client);
837
+ if (result.ok) {
838
+ const updated = await this.store.mutateAccount(uuid, (account) => {
839
+ account.accessToken = result.patch.accessToken;
840
+ account.expiresAt = result.patch.expiresAt;
841
+ if (result.patch.refreshToken) account.refreshToken = result.patch.refreshToken;
842
+ if (result.patch.uuid) account.uuid = result.patch.uuid;
843
+ if (result.patch.accountId) account.accountId = result.patch.accountId;
844
+ if (result.patch.email) account.email = result.patch.email;
845
+ account.enabled = true;
846
+ account.consecutiveAuthFailures = 0;
847
+ });
848
+ this.runtimeFactory?.invalidate(uuid);
849
+ if (result.patch.uuid) {
850
+ this.runtimeFactory?.invalidate(result.patch.uuid);
851
+ }
852
+ const nextUuid = result.patch.uuid ?? uuid;
853
+ if (this.activeAccountUuid === uuid && result.patch.uuid && result.patch.uuid !== uuid) {
854
+ this.activeAccountUuid = result.patch.uuid;
855
+ await this.store.setActiveUuid(result.patch.uuid);
856
+ }
857
+ if (updated && (uuid === this.activeAccountUuid || nextUuid === this.activeAccountUuid)) {
858
+ const freshCredentials = await this.store.readCredentials(nextUuid);
859
+ if (freshCredentials) {
860
+ this.syncToOpenCode({
861
+ refreshToken: freshCredentials.refreshToken,
862
+ accessToken: freshCredentials.accessToken,
863
+ expiresAt: freshCredentials.expiresAt
864
+ });
865
+ }
866
+ }
867
+ } else {
868
+ await this.markAuthFailure(uuid, result);
869
+ this.runtimeFactory?.invalidate(uuid);
870
+ }
871
+ return result;
872
+ }
873
+ };
874
+ }
875
+
876
+ // src/account-store.ts
877
+ import { promises as fs4 } from "node:fs";
878
+ import { randomBytes as randomBytes3 } from "node:crypto";
879
+ import { dirname as dirname4, join as join5 } from "node:path";
880
+ import lockfile from "proper-lockfile";
881
+ import * as v4 from "valibot";
882
+
883
+ // src/storage.ts
884
+ import { promises as fs3 } from "node:fs";
885
+ import { dirname as dirname3, join as join4 } from "node:path";
886
+ import * as v3 from "valibot";
887
+
888
+ // src/constants.ts
889
+ var DEFAULT_ACCOUNTS_FILENAME = "multiauth-accounts.json";
890
+ var ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
891
+ function setAccountsFilename(filename) {
892
+ if (!filename) {
893
+ ACCOUNTS_FILENAME = DEFAULT_ACCOUNTS_FILENAME;
894
+ return;
895
+ }
896
+ ACCOUNTS_FILENAME = filename;
897
+ }
898
+
899
+ // src/storage.ts
900
+ function getStoragePath() {
901
+ return join4(getConfigDir2(), ACCOUNTS_FILENAME);
902
+ }
903
+ async function backupCorruptFile(targetPath, content) {
904
+ const backupPath = `${targetPath}.corrupt.${Date.now()}.bak`;
905
+ await fs3.mkdir(dirname3(backupPath), { recursive: true });
906
+ await fs3.writeFile(backupPath, content, "utf-8");
907
+ }
908
+ async function readStorageFromDisk(targetPath, backupOnCorrupt) {
909
+ let content;
910
+ try {
911
+ content = await fs3.readFile(targetPath, "utf-8");
912
+ } catch (error) {
913
+ if (getErrorCode(error) === "ENOENT") {
914
+ return null;
915
+ }
916
+ throw error;
917
+ }
918
+ let parsed;
919
+ try {
920
+ parsed = JSON.parse(content);
921
+ } catch {
922
+ if (backupOnCorrupt) {
923
+ try {
924
+ await backupCorruptFile(targetPath, content);
925
+ } catch {
926
+ }
927
+ }
928
+ return null;
929
+ }
930
+ const validation = v3.safeParse(AccountStorageSchema, parsed);
931
+ if (!validation.success) {
932
+ if (backupOnCorrupt) {
933
+ try {
934
+ await backupCorruptFile(targetPath, content);
935
+ } catch {
936
+ }
937
+ }
938
+ return null;
939
+ }
940
+ return validation.output;
941
+ }
942
+ function deduplicateAccounts(accounts) {
943
+ const deduplicated = [];
944
+ const indexByUuid = /* @__PURE__ */ new Map();
945
+ for (const account of accounts) {
946
+ if (!account.uuid) {
947
+ deduplicated.push(account);
948
+ continue;
949
+ }
950
+ const existingIndex = indexByUuid.get(account.uuid);
951
+ if (existingIndex === void 0) {
952
+ indexByUuid.set(account.uuid, deduplicated.length);
953
+ deduplicated.push(account);
954
+ continue;
955
+ }
956
+ const existingAccount = deduplicated[existingIndex];
957
+ if (!existingAccount || account.lastUsed >= existingAccount.lastUsed) {
958
+ deduplicated[existingIndex] = account;
959
+ }
960
+ }
961
+ return deduplicated;
962
+ }
963
+ async function loadAccounts() {
964
+ const storagePath = getStoragePath();
965
+ const storage = await readStorageFromDisk(storagePath, true);
966
+ if (!storage) {
967
+ return null;
968
+ }
969
+ return {
970
+ ...storage,
971
+ accounts: deduplicateAccounts(storage.accounts || [])
972
+ };
973
+ }
974
+
975
+ // src/account-store.ts
976
+ var FILE_MODE = 384;
977
+ var LOCK_OPTIONS = {
978
+ stale: 1e4,
979
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
980
+ };
981
+ function getStoragePath2() {
982
+ return join5(getConfigDir2(), ACCOUNTS_FILENAME);
983
+ }
984
+ function createEmptyStorage() {
985
+ return { version: 1, accounts: [] };
986
+ }
987
+ function buildTempPath(targetPath) {
988
+ return `${targetPath}.${randomBytes3(8).toString("hex")}.tmp`;
989
+ }
990
+ async function writeAtomicText(targetPath, content) {
991
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
992
+ const tempPath = buildTempPath(targetPath);
993
+ try {
994
+ await fs4.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE });
995
+ await fs4.chmod(tempPath, FILE_MODE);
996
+ await fs4.rename(tempPath, targetPath);
997
+ await fs4.chmod(targetPath, FILE_MODE);
998
+ } catch (error) {
999
+ try {
1000
+ await fs4.unlink(tempPath);
1001
+ } catch {
1002
+ }
1003
+ throw error;
1004
+ }
1005
+ }
1006
+ async function writeStorageAtomic(targetPath, storage) {
1007
+ const validation = v4.safeParse(AccountStorageSchema, storage);
1008
+ if (!validation.success) {
1009
+ throw new Error("Invalid account storage payload");
1010
+ }
1011
+ await writeAtomicText(targetPath, `${JSON.stringify(validation.output, null, 2)}
1012
+ `);
1013
+ }
1014
+ async function ensureStorageFileExists(targetPath) {
1015
+ await fs4.mkdir(dirname4(targetPath), { recursive: true });
1016
+ const emptyContent = `${JSON.stringify(createEmptyStorage(), null, 2)}
1017
+ `;
1018
+ try {
1019
+ await fs4.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE });
1020
+ } catch (error) {
1021
+ if (getErrorCode(error) !== "EEXIST") throw error;
1022
+ }
1023
+ }
1024
+ async function withFileLock(fn) {
1025
+ const storagePath = getStoragePath2();
1026
+ await ensureStorageFileExists(storagePath);
1027
+ let release = null;
1028
+ try {
1029
+ release = await lockfile.lock(storagePath, LOCK_OPTIONS);
1030
+ return await fn(storagePath);
1031
+ } finally {
1032
+ if (release) {
1033
+ try {
1034
+ await release();
1035
+ } catch {
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ var AccountStore = class {
1041
+ async load() {
1042
+ const storage = await loadAccounts();
1043
+ return storage ?? createEmptyStorage();
1044
+ }
1045
+ async readCredentials(uuid) {
1046
+ const storagePath = getStoragePath2();
1047
+ const storage = await readStorageFromDisk(storagePath, false);
1048
+ if (!storage) return null;
1049
+ const account = storage.accounts.find((a) => a.uuid === uuid);
1050
+ if (!account) return null;
1051
+ return {
1052
+ refreshToken: account.refreshToken,
1053
+ accessToken: account.accessToken,
1054
+ expiresAt: account.expiresAt,
1055
+ accountId: account.accountId
1056
+ };
1057
+ }
1058
+ async mutateAccount(uuid, fn) {
1059
+ return await withFileLock(async (storagePath) => {
1060
+ const current = await readStorageFromDisk(storagePath, false);
1061
+ if (!current) return null;
1062
+ const account = current.accounts.find((a) => a.uuid === uuid);
1063
+ if (!account) return null;
1064
+ fn(account);
1065
+ await writeStorageAtomic(storagePath, current);
1066
+ return { ...account };
1067
+ });
1068
+ }
1069
+ async mutateStorage(fn) {
1070
+ await withFileLock(async (storagePath) => {
1071
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1072
+ fn(current);
1073
+ await writeStorageAtomic(storagePath, current);
1074
+ });
1075
+ }
1076
+ async addAccount(account) {
1077
+ await withFileLock(async (storagePath) => {
1078
+ const current = await readStorageFromDisk(storagePath, false) ?? createEmptyStorage();
1079
+ const exists = current.accounts.some(
1080
+ (a) => a.uuid === account.uuid || a.refreshToken === account.refreshToken
1081
+ );
1082
+ if (exists) return;
1083
+ current.accounts.push(account);
1084
+ await writeStorageAtomic(storagePath, current);
1085
+ });
1086
+ }
1087
+ async removeAccount(uuid) {
1088
+ return await withFileLock(async (storagePath) => {
1089
+ const current = await readStorageFromDisk(storagePath, false);
1090
+ if (!current) return false;
1091
+ const initialLength = current.accounts.length;
1092
+ current.accounts = current.accounts.filter((a) => a.uuid !== uuid);
1093
+ if (current.accounts.length === initialLength) return false;
1094
+ if (current.activeAccountUuid === uuid) {
1095
+ current.activeAccountUuid = current.accounts[0]?.uuid;
1096
+ }
1097
+ await writeStorageAtomic(storagePath, current);
1098
+ return true;
1099
+ });
1100
+ }
1101
+ async setActiveUuid(uuid) {
1102
+ await this.mutateStorage((storage) => {
1103
+ storage.activeAccountUuid = uuid;
1104
+ });
1105
+ }
1106
+ async clear() {
1107
+ await withFileLock(async (storagePath) => {
1108
+ await writeStorageAtomic(storagePath, createEmptyStorage());
1109
+ });
1110
+ }
1111
+ };
1112
+
1113
+ // src/executor.ts
1114
+ var MIN_MAX_RETRIES = 6;
1115
+ var RETRIES_PER_ACCOUNT = 3;
1116
+ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1117
+ var MAX_RESOLVE_ATTEMPTS = 10;
1118
+ var SERVER_RETRY_BASE_MS = 1e3;
1119
+ var SERVER_RETRY_MAX_MS = 4e3;
1120
+ var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1121
+ function createExecutorForProvider(providerName, dependencies) {
1122
+ const {
1123
+ handleRateLimitResponse,
1124
+ formatWaitTime: formatWaitTime2,
1125
+ sleep: sleep2,
1126
+ showToast: showToast2,
1127
+ getAccountLabel: getAccountLabel2
1128
+ } = dependencies;
1129
+ async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
1130
+ const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1131
+ let retries = 0;
1132
+ let previousAccountUuid;
1133
+ while (true) {
1134
+ if (++retries > maxRetries) {
1135
+ throw new Error(
1136
+ `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1137
+ );
1138
+ }
1139
+ await manager.refresh();
1140
+ const account = await resolveAccount(manager, client);
1141
+ const accountUuid = account.uuid;
1142
+ if (!accountUuid) continue;
1143
+ if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1144
+ void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1145
+ }
1146
+ previousAccountUuid = accountUuid;
1147
+ let runtime;
1148
+ let response;
1149
+ try {
1150
+ runtime = await runtimeFactory.getRuntime(accountUuid);
1151
+ response = await runtime.fetch(input, init);
1152
+ } catch (error) {
1153
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1154
+ continue;
1155
+ }
1156
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1157
+ continue;
1158
+ }
1159
+ if (response.status >= 500) {
1160
+ let serverResponse = response;
1161
+ let networkErrorDuringServerRetry = false;
1162
+ let authFailureDuringServerRetry = false;
1163
+ for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1164
+ const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1165
+ const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1166
+ await sleep2(jitteredBackoff);
1167
+ try {
1168
+ serverResponse = await runtime.fetch(input, init);
1169
+ } catch (error) {
1170
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1171
+ authFailureDuringServerRetry = true;
1172
+ break;
1173
+ }
1174
+ networkErrorDuringServerRetry = true;
1175
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1176
+ break;
1177
+ }
1178
+ if (serverResponse.status < 500) break;
1179
+ }
1180
+ if (authFailureDuringServerRetry) {
1181
+ continue;
1182
+ }
1183
+ if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1184
+ continue;
1185
+ }
1186
+ response = serverResponse;
1187
+ }
1188
+ if (response.status === 401) {
1189
+ runtimeFactory.invalidate(accountUuid);
1190
+ try {
1191
+ const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1192
+ const retryResponse = await retryRuntime.fetch(input, init);
1193
+ if (retryResponse.status !== 401) {
1194
+ await manager.markSuccess(accountUuid);
1195
+ return retryResponse;
1196
+ }
1197
+ } catch (error) {
1198
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1199
+ continue;
1200
+ }
1201
+ continue;
1202
+ }
1203
+ await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1204
+ await manager.refresh();
1205
+ if (!manager.hasAnyUsableAccount()) {
1206
+ void showToast2(client, "All accounts have auth failures.", "error");
1207
+ throw new Error(
1208
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1209
+ );
1210
+ }
1211
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1212
+ continue;
1213
+ }
1214
+ if (response.status === 403) {
1215
+ const revoked = await isRevokedTokenResponse(response);
1216
+ if (revoked) {
1217
+ await manager.markRevoked(accountUuid);
1218
+ await manager.refresh();
1219
+ void showToast2(
1220
+ client,
1221
+ `${getAccountLabel2(account)} disabled: OAuth token revoked.`,
1222
+ "error"
1223
+ );
1224
+ if (!manager.hasAnyUsableAccount()) {
1225
+ throw new Error(
1226
+ `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1227
+ );
1228
+ }
1229
+ continue;
1230
+ }
1231
+ }
1232
+ if (response.status === 429) {
1233
+ await handleRateLimitResponse(manager, client, account, response);
1234
+ continue;
1235
+ }
1236
+ await manager.markSuccess(accountUuid);
1237
+ return response;
1238
+ }
1239
+ }
1240
+ async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1241
+ const refreshFailureStatus = getRefreshFailureStatus(error);
1242
+ if (refreshFailureStatus === void 0) return false;
1243
+ if (!account.uuid) return false;
1244
+ const accountUuid = account.uuid;
1245
+ runtimeFactory.invalidate(accountUuid);
1246
+ await manager.markAuthFailure(accountUuid, {
1247
+ ok: false,
1248
+ permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1249
+ });
1250
+ await manager.refresh();
1251
+ if (!manager.hasAnyUsableAccount()) {
1252
+ void showToast2(client, "All accounts have auth failures.", "error");
1253
+ throw new Error(
1254
+ `All ${providerName} accounts have authentication failures. Re-authenticate with \`opencode auth login\`.`
1255
+ );
1256
+ }
1257
+ void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1258
+ return true;
1259
+ }
1260
+ async function resolveAccount(manager, client) {
1261
+ let attempts = 0;
1262
+ while (true) {
1263
+ if (++attempts > MAX_RESOLVE_ATTEMPTS) {
1264
+ throw new Error(
1265
+ `Failed to resolve an available account after ${MAX_RESOLVE_ATTEMPTS} attempts. All accounts may be rate-limited or disabled.`
1266
+ );
1267
+ }
1268
+ const account = await manager.selectAccount();
1269
+ if (account) return account;
1270
+ if (!manager.hasAnyUsableAccount()) {
1271
+ throw new Error(
1272
+ `All ${providerName} accounts are disabled. Re-authenticate with \`opencode auth login\`.`
1273
+ );
1274
+ }
1275
+ const waitMs = manager.getMinWaitTime();
1276
+ if (waitMs <= 0) {
1277
+ throw new Error(
1278
+ `All ${providerName} accounts are rate-limited. Add more accounts with \`opencode auth login\` or wait.`
1279
+ );
1280
+ }
1281
+ await showToast2(
1282
+ client,
1283
+ `All ${manager.getAccountCount()} account(s) rate-limited. Waiting ${formatWaitTime2(waitMs)}...`,
1284
+ "warning"
1285
+ );
1286
+ await sleep2(waitMs);
1287
+ }
1288
+ }
1289
+ return {
1290
+ executeWithAccountRotation
1291
+ };
1292
+ }
1293
+ function getRefreshFailureStatus(error) {
1294
+ if (!(error instanceof Error)) return void 0;
1295
+ const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1296
+ if (!matched) return void 0;
1297
+ const status = Number(matched[1]);
1298
+ return Number.isFinite(status) ? status : void 0;
1299
+ }
1300
+ async function isRevokedTokenResponse(response) {
1301
+ try {
1302
+ const cloned = response.clone();
1303
+ const body = await cloned.text();
1304
+ return body.includes("revoked");
1305
+ } catch {
1306
+ return false;
1307
+ }
1308
+ }
1309
+
1310
+ // src/proactive-refresh.ts
1311
+ var INITIAL_DELAY_MS = 5e3;
1312
+ function createProactiveRefreshQueueForProvider(dependencies) {
1313
+ const {
1314
+ getConfig: getConfig2,
1315
+ refreshToken,
1316
+ isTokenExpired,
1317
+ debugLog: debugLog2
1318
+ } = dependencies;
1319
+ return class ProactiveRefreshQueue {
1320
+ constructor(client, store, onInvalidate) {
1321
+ this.client = client;
1322
+ this.store = store;
1323
+ this.onInvalidate = onInvalidate;
1324
+ }
1325
+ timeoutHandle = null;
1326
+ runToken = 0;
1327
+ inFlight = null;
1328
+ start() {
1329
+ const config = getConfig2();
1330
+ if (!config.proactive_refresh) return;
1331
+ this.runToken++;
1332
+ this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1333
+ debugLog2(this.client, "Proactive refresh started", {
1334
+ intervalSeconds: config.proactive_refresh_interval_seconds,
1335
+ bufferSeconds: config.proactive_refresh_buffer_seconds
1336
+ });
1337
+ }
1338
+ async stop() {
1339
+ this.runToken++;
1340
+ if (this.timeoutHandle) {
1341
+ clearTimeout(this.timeoutHandle);
1342
+ this.timeoutHandle = null;
1343
+ }
1344
+ if (this.inFlight) {
1345
+ await this.inFlight;
1346
+ this.inFlight = null;
1347
+ }
1348
+ debugLog2(this.client, "Proactive refresh stopped");
1349
+ }
1350
+ scheduleNext(token, delayMs) {
1351
+ this.timeoutHandle = setTimeout(() => {
1352
+ if (token !== this.runToken) return;
1353
+ this.inFlight = this.runCheck(token).finally(() => {
1354
+ this.inFlight = null;
1355
+ });
1356
+ }, delayMs);
1357
+ }
1358
+ needsProactiveRefresh(account) {
1359
+ if (!account.accessToken || !account.expiresAt) return false;
1360
+ if (isTokenExpired(account)) return false;
1361
+ const bufferMs = getConfig2().proactive_refresh_buffer_seconds * 1e3;
1362
+ return account.expiresAt <= Date.now() + bufferMs;
1363
+ }
1364
+ async runCheck(token) {
1365
+ try {
1366
+ const stored = await this.store.load();
1367
+ if (token !== this.runToken) return;
1368
+ const candidates = stored.accounts.filter(
1369
+ (a) => a.enabled !== false && !a.isAuthDisabled && a.uuid && this.needsProactiveRefresh(a)
1370
+ );
1371
+ if (candidates.length === 0) return;
1372
+ debugLog2(this.client, `Proactive refresh: ${candidates.length} account(s) approaching expiry`);
1373
+ for (const account of candidates) {
1374
+ if (token !== this.runToken) return;
1375
+ const credentials = await this.store.readCredentials(account.uuid);
1376
+ if (!credentials || !this.needsProactiveRefresh(credentials)) continue;
1377
+ const result = await refreshToken(credentials.refreshToken, account.uuid, this.client);
1378
+ if (result.ok) {
1379
+ await this.store.mutateAccount(account.uuid, (target) => {
1380
+ target.accessToken = result.patch.accessToken;
1381
+ target.expiresAt = result.patch.expiresAt;
1382
+ if (result.patch.refreshToken) target.refreshToken = result.patch.refreshToken;
1383
+ if (result.patch.uuid) target.uuid = result.patch.uuid;
1384
+ if (result.patch.email) target.email = result.patch.email;
1385
+ if (result.patch.accountId) target.accountId = result.patch.accountId;
1386
+ target.consecutiveAuthFailures = 0;
1387
+ target.isAuthDisabled = false;
1388
+ target.authDisabledReason = void 0;
1389
+ });
1390
+ this.onInvalidate?.(account.uuid);
1391
+ } else {
1392
+ await this.persistFailure(account, result.permanent);
1393
+ }
1394
+ }
1395
+ } catch (error) {
1396
+ debugLog2(this.client, `Proactive refresh check error: ${error}`);
1397
+ } finally {
1398
+ if (token === this.runToken) {
1399
+ const intervalMs = getConfig2().proactive_refresh_interval_seconds * 1e3;
1400
+ this.scheduleNext(token, intervalMs);
1401
+ }
1402
+ }
1403
+ }
1404
+ async persistFailure(account, permanent) {
1405
+ try {
1406
+ await this.store.mutateAccount(account.uuid, (target) => {
1407
+ if (permanent) {
1408
+ target.isAuthDisabled = true;
1409
+ target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1410
+ } else {
1411
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1412
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1413
+ if (target.consecutiveAuthFailures >= maxFailures) {
1414
+ target.isAuthDisabled = true;
1415
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1416
+ }
1417
+ }
1418
+ });
1419
+ } catch {
1420
+ debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1421
+ }
1422
+ }
1423
+ };
1424
+ }
1425
+
1426
+ // src/rate-limit.ts
1427
+ var USAGE_FETCH_COOLDOWN_MS = 3e4;
1428
+ function createRateLimitHandlers(dependencies) {
1429
+ const {
1430
+ fetchUsage,
1431
+ getConfig: getConfig2,
1432
+ formatWaitTime: formatWaitTime2,
1433
+ getAccountLabel: getAccountLabel2,
1434
+ showToast: showToast2
1435
+ } = dependencies;
1436
+ function retryAfterMsFromResponse(response) {
1437
+ const retryAfterMs = response.headers.get("retry-after-ms");
1438
+ if (retryAfterMs) {
1439
+ const parsed = parseInt(retryAfterMs, 10);
1440
+ if (!isNaN(parsed) && parsed > 0) return parsed;
1441
+ }
1442
+ const retryAfter = response.headers.get("retry-after");
1443
+ if (retryAfter) {
1444
+ const parsed = parseInt(retryAfter, 10);
1445
+ if (!isNaN(parsed) && parsed > 0) return parsed * 1e3;
1446
+ }
1447
+ return getConfig2().default_retry_after_ms;
1448
+ }
1449
+ function getResetMsFromUsage(account) {
1450
+ const usage = account.cachedUsage;
1451
+ if (!usage) return null;
1452
+ const now = Date.now();
1453
+ const candidates = [];
1454
+ if (usage.five_hour?.resets_at) {
1455
+ const ms = Date.parse(usage.five_hour.resets_at) - now;
1456
+ if (ms > 0) candidates.push(ms);
1457
+ }
1458
+ if (usage.seven_day?.resets_at) {
1459
+ const ms = Date.parse(usage.seven_day.resets_at) - now;
1460
+ if (ms > 0) candidates.push(ms);
1461
+ }
1462
+ return candidates.length > 0 ? Math.min(...candidates) : null;
1463
+ }
1464
+ async function fetchUsageLimits(accessToken, accountId) {
1465
+ if (!accessToken) return null;
1466
+ try {
1467
+ const result = await fetchUsage(accessToken, accountId);
1468
+ return result.ok ? result.data : null;
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+ async function handleRateLimitResponse(manager, client, account, response) {
1474
+ if (!account.uuid) return;
1475
+ const resetMs = getResetMsFromUsage(account) ?? retryAfterMsFromResponse(response);
1476
+ await manager.markRateLimited(account.uuid, resetMs);
1477
+ const shouldFetchUsage = account.accessToken && (!account.cachedUsageAt || Date.now() - account.cachedUsageAt > USAGE_FETCH_COOLDOWN_MS);
1478
+ if (shouldFetchUsage) {
1479
+ const usage = await fetchUsageLimits(account.accessToken, account.accountId);
1480
+ if (usage) {
1481
+ await manager.applyUsageCache(account.uuid, usage);
1482
+ }
1483
+ }
1484
+ if (manager.getAccountCount() > 1) {
1485
+ void showToast2(
1486
+ client,
1487
+ `${getAccountLabel2(account)} rate-limited (resets in ${formatWaitTime2(resetMs)}). Switching...`,
1488
+ "warning"
1489
+ );
1490
+ }
1491
+ }
1492
+ return {
1493
+ retryAfterMsFromResponse,
1494
+ getResetMsFromUsage,
1495
+ fetchUsageLimits,
1496
+ handleRateLimitResponse
1497
+ };
1498
+ }
1499
+
1500
+ // src/auth-migration.ts
1501
+ import { promises as fs5 } from "node:fs";
1502
+ import { join as join6 } from "node:path";
1503
+ var AUTH_JSON_FILENAME = "auth.json";
1504
+ function isValidOAuthCredential(value) {
1505
+ if (typeof value !== "object" || value === null) return false;
1506
+ const candidate = value;
1507
+ return candidate.type === "oauth" && typeof candidate.refresh === "string" && candidate.refresh.length > 0;
1508
+ }
1509
+ function resolveAuthJsonPath() {
1510
+ return join6(getConfigDir2(), AUTH_JSON_FILENAME);
1511
+ }
1512
+ async function readAuthJson() {
1513
+ const authPath = resolveAuthJsonPath();
1514
+ let content;
1515
+ try {
1516
+ content = await fs5.readFile(authPath, "utf-8");
1517
+ } catch {
1518
+ return null;
1519
+ }
1520
+ try {
1521
+ const parsed = JSON.parse(content);
1522
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1523
+ return null;
1524
+ }
1525
+ return parsed;
1526
+ } catch {
1527
+ return null;
1528
+ }
1529
+ }
1530
+ async function migrateFromAuthJson(providerKey, store) {
1531
+ const storage = await store.load();
1532
+ const hasExistingAccounts = storage.accounts.length > 0;
1533
+ if (hasExistingAccounts) return false;
1534
+ const authData = await readAuthJson();
1535
+ if (!authData) return false;
1536
+ const providerCredential = authData[providerKey];
1537
+ if (!isValidOAuthCredential(providerCredential)) return false;
1538
+ const now = Date.now();
1539
+ const newAccount = {
1540
+ uuid: crypto.randomUUID(),
1541
+ refreshToken: providerCredential.refresh,
1542
+ accessToken: providerCredential.access,
1543
+ expiresAt: providerCredential.expires,
1544
+ addedAt: now,
1545
+ lastUsed: now,
1546
+ enabled: true,
1547
+ planTier: "",
1548
+ consecutiveAuthFailures: 0,
1549
+ isAuthDisabled: false
1550
+ };
1551
+ await store.addAccount(newAccount);
1552
+ await store.setActiveUuid(newAccount.uuid);
1553
+ return true;
1554
+ }
1555
+
1556
+ // src/ui/ansi.ts
1557
+ var ANSI = {
1558
+ hide: "\x1B[?25l",
1559
+ show: "\x1B[?25h",
1560
+ up: (n = 1) => `\x1B[${n}A`,
1561
+ down: (n = 1) => `\x1B[${n}B`,
1562
+ clearLine: "\x1B[2K",
1563
+ cyan: "\x1B[36m",
1564
+ green: "\x1B[32m",
1565
+ red: "\x1B[31m",
1566
+ yellow: "\x1B[33m",
1567
+ dim: "\x1B[2m",
1568
+ bold: "\x1B[1m",
1569
+ reset: "\x1B[0m"
1570
+ };
1571
+ function parseKey(data) {
1572
+ const s = data.toString();
1573
+ if (s === "\x1B[A" || s === "\x1BOA") return "up";
1574
+ if (s === "\x1B[B" || s === "\x1BOB") return "down";
1575
+ if (s === "\r" || s === "\n") return "enter";
1576
+ if (s === "") return "escape";
1577
+ if (s === "\x1B") return "escape-start";
1578
+ return null;
1579
+ }
1580
+ function isTTY() {
1581
+ return Boolean(process.stdin.isTTY);
1582
+ }
1583
+
1584
+ // src/ui/select.ts
1585
+ var ESCAPE_TIMEOUT_MS = 50;
1586
+ var COLOR_MAP = {
1587
+ red: ANSI.red,
1588
+ green: ANSI.green,
1589
+ yellow: ANSI.yellow,
1590
+ cyan: ANSI.cyan
1591
+ };
1592
+ async function select(items, options) {
1593
+ if (!isTTY()) {
1594
+ throw new Error("Interactive select requires a TTY terminal");
1595
+ }
1596
+ const enabledItems = items.filter((i) => !i.disabled && !i.separator);
1597
+ if (enabledItems.length === 0) {
1598
+ throw new Error("All items disabled");
1599
+ }
1600
+ if (enabledItems.length === 1) {
1601
+ return enabledItems[0].value;
1602
+ }
1603
+ const { message, subtitle } = options;
1604
+ const { stdin, stdout } = process;
1605
+ let cursor = items.findIndex((i) => !i.disabled && !i.separator);
1606
+ if (cursor === -1) cursor = 0;
1607
+ let escapeTimeout = null;
1608
+ let isCleanedUp = false;
1609
+ let isFirstRender = true;
1610
+ const getTotalLines = () => {
1611
+ const subtitleLines = subtitle ? 3 : 0;
1612
+ return 1 + subtitleLines + items.length + 1 + 1;
1613
+ };
1614
+ const renderItemLabel = (item, isSelected) => {
1615
+ const colorCode = item.color ? COLOR_MAP[item.color] ?? "" : "";
1616
+ if (item.disabled) {
1617
+ return `${ANSI.dim}${item.label} (unavailable)${ANSI.reset}`;
1618
+ }
1619
+ const hintSuffix = item.hint ? ` ${ANSI.dim}${item.hint}${ANSI.reset}` : "";
1620
+ if (isSelected) {
1621
+ const label = colorCode ? `${colorCode}${item.label}${ANSI.reset}` : item.label;
1622
+ return `${label}${hintSuffix}`;
1623
+ }
1624
+ const dimLabel = colorCode ? `${ANSI.dim}${colorCode}${item.label}${ANSI.reset}` : `${ANSI.dim}${item.label}${ANSI.reset}`;
1625
+ return `${dimLabel}${hintSuffix}`;
1626
+ };
1627
+ const render = () => {
1628
+ const totalLines = getTotalLines();
1629
+ if (!isFirstRender) {
1630
+ stdout.write(ANSI.up(totalLines) + "\r");
1631
+ }
1632
+ isFirstRender = false;
1633
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u250C ${ANSI.reset}${message}
1634
+ `);
1635
+ if (subtitle) {
1636
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1637
+ `);
1638
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u25C6${ANSI.reset} ${subtitle}
1639
+ `);
1640
+ stdout.write(`${ANSI.clearLine}
1641
+ `);
1642
+ }
1643
+ for (let i = 0; i < items.length; i++) {
1644
+ const item = items[i];
1645
+ if (!item) continue;
1646
+ if (item.separator) {
1647
+ stdout.write(`${ANSI.clearLine}${ANSI.dim}\u2502${ANSI.reset}
1648
+ `);
1649
+ continue;
1650
+ }
1651
+ const isSelected = i === cursor;
1652
+ const labelText = renderItemLabel(item, isSelected);
1653
+ const bullet = isSelected ? `${ANSI.green}\u25CF${ANSI.reset}` : `${ANSI.dim}\u25CB${ANSI.reset}`;
1654
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${bullet} ${labelText}
1655
+ `);
1656
+ }
1657
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2502${ANSI.reset} ${ANSI.dim}\u2191/\u2193 to select \u2022 Enter: confirm${ANSI.reset}
1658
+ `);
1659
+ stdout.write(`${ANSI.clearLine}${ANSI.cyan}\u2514${ANSI.reset}
1660
+ `);
1661
+ };
1662
+ return new Promise((resolve) => {
1663
+ const wasRaw = stdin.isRaw ?? false;
1664
+ const cleanup = () => {
1665
+ if (isCleanedUp) return;
1666
+ isCleanedUp = true;
1667
+ if (escapeTimeout) {
1668
+ clearTimeout(escapeTimeout);
1669
+ escapeTimeout = null;
1670
+ }
1671
+ try {
1672
+ stdin.removeListener("data", onKey);
1673
+ stdin.setRawMode(wasRaw);
1674
+ stdin.pause();
1675
+ stdout.write(ANSI.show);
1676
+ } catch {
1677
+ }
1678
+ process.removeListener("SIGINT", onSignal);
1679
+ process.removeListener("SIGTERM", onSignal);
1680
+ };
1681
+ const onSignal = () => {
1682
+ cleanup();
1683
+ resolve(null);
1684
+ };
1685
+ const finishWithValue = (value) => {
1686
+ cleanup();
1687
+ resolve(value);
1688
+ };
1689
+ const findNextSelectable = (from, direction) => {
1690
+ if (items.length === 0) return from;
1691
+ let next = from;
1692
+ do {
1693
+ next = (next + direction + items.length) % items.length;
1694
+ } while (items[next]?.disabled || items[next]?.separator);
1695
+ return next;
1696
+ };
1697
+ const onKey = (data) => {
1698
+ if (escapeTimeout) {
1699
+ clearTimeout(escapeTimeout);
1700
+ escapeTimeout = null;
1701
+ }
1702
+ const action = parseKey(data);
1703
+ switch (action) {
1704
+ case "up":
1705
+ cursor = findNextSelectable(cursor, -1);
1706
+ render();
1707
+ return;
1708
+ case "down":
1709
+ cursor = findNextSelectable(cursor, 1);
1710
+ render();
1711
+ return;
1712
+ case "enter":
1713
+ finishWithValue(items[cursor]?.value ?? null);
1714
+ return;
1715
+ case "escape":
1716
+ finishWithValue(null);
1717
+ return;
1718
+ case "escape-start":
1719
+ escapeTimeout = setTimeout(() => {
1720
+ finishWithValue(null);
1721
+ }, ESCAPE_TIMEOUT_MS);
1722
+ return;
1723
+ default:
1724
+ return;
1725
+ }
1726
+ };
1727
+ process.once("SIGINT", onSignal);
1728
+ process.once("SIGTERM", onSignal);
1729
+ try {
1730
+ stdin.setRawMode(true);
1731
+ } catch {
1732
+ cleanup();
1733
+ resolve(null);
1734
+ return;
1735
+ }
1736
+ stdin.resume();
1737
+ stdout.write(ANSI.hide);
1738
+ render();
1739
+ stdin.on("data", onKey);
1740
+ });
1741
+ }
1742
+
1743
+ // src/ui/confirm.ts
1744
+ async function confirm(message, defaultYes = false) {
1745
+ const items = defaultYes ? [
1746
+ { label: "Yes", value: true },
1747
+ { label: "No", value: false }
1748
+ ] : [
1749
+ { label: "No", value: false },
1750
+ { label: "Yes", value: true }
1751
+ ];
1752
+ const result = await select(items, { message });
1753
+ return result ?? false;
1754
+ }
1755
+ export {
1756
+ ACCOUNTS_FILENAME,
1757
+ ANSI,
1758
+ AccountSelectionStrategySchema,
1759
+ AccountStorageSchema,
1760
+ AccountStore,
1761
+ CredentialRefreshPatchSchema,
1762
+ OAuthCredentialsSchema,
1763
+ PluginConfigSchema,
1764
+ StoredAccountSchema,
1765
+ UsageLimitEntrySchema,
1766
+ UsageLimitsSchema,
1767
+ confirm,
1768
+ createAccountManagerForProvider,
1769
+ createExecutorForProvider,
1770
+ createMinimalClient,
1771
+ createProactiveRefreshQueueForProvider,
1772
+ createRateLimitHandlers,
1773
+ debugLog,
1774
+ deduplicateAccounts,
1775
+ formatWaitTime,
1776
+ getAccountLabel,
1777
+ getConfig,
1778
+ getConfigDir2 as getConfigDir,
1779
+ getErrorCode,
1780
+ initCoreConfig,
1781
+ isClaimedByOther,
1782
+ isTTY,
1783
+ loadAccounts,
1784
+ loadConfig,
1785
+ migrateFromAuthJson,
1786
+ parseKey,
1787
+ readClaims,
1788
+ readStorageFromDisk,
1789
+ releaseClaim,
1790
+ resetConfigCache,
1791
+ select,
1792
+ setAccountsFilename,
1793
+ setConfigGetter,
1794
+ showToast,
1795
+ sleep,
1796
+ updateConfigField,
1797
+ writeClaim
1798
+ };