oc-auth-switcher 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.
package/dist/cli.js ADDED
@@ -0,0 +1,689 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/accounts.ts
5
+ import fs from "fs";
6
+ import path2 from "path";
7
+
8
+ // src/constants.ts
9
+ import path from "path";
10
+ import os from "os";
11
+ var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
12
+ var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
13
+ var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
14
+ var DEFAULT_THRESHOLD = 0.9;
15
+ var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
16
+ var AUTH_FAILURE_COOLDOWN = 60 * 60 * 1000;
17
+
18
+ // src/accounts.ts
19
+ function normalizeAccount(raw) {
20
+ const name = raw.name || "unnamed";
21
+ const access = raw.access || raw.accessToken || "";
22
+ const refresh = raw.refresh || raw.refreshToken || "";
23
+ let expires = 0;
24
+ const rawExpires = raw.expires ?? raw.expiresAt;
25
+ if (typeof rawExpires === "number") {
26
+ expires = rawExpires;
27
+ } else if (typeof rawExpires === "string") {
28
+ const parsed = Date.parse(rawExpires);
29
+ if (!isNaN(parsed))
30
+ expires = parsed;
31
+ }
32
+ return { name, access, refresh, expires };
33
+ }
34
+ function ensureDir(filePath) {
35
+ const dir = path2.dirname(filePath);
36
+ if (!fs.existsSync(dir)) {
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ }
39
+ }
40
+ function safeReadJSON(filePath, fallback) {
41
+ for (const p of [filePath, filePath + ".bak"]) {
42
+ try {
43
+ const raw = fs.readFileSync(p, "utf-8");
44
+ return JSON.parse(raw);
45
+ } catch {}
46
+ }
47
+ return fallback;
48
+ }
49
+ function safeWriteJSON(filePath, data) {
50
+ ensureDir(filePath);
51
+ const content = JSON.stringify(data, null, 2);
52
+ const tmpPath = filePath + ".tmp";
53
+ const bakPath = filePath + ".bak";
54
+ if (fs.existsSync(filePath)) {
55
+ try {
56
+ fs.copyFileSync(filePath, bakPath);
57
+ } catch {}
58
+ }
59
+ fs.writeFileSync(tmpPath, content, { mode: 384 });
60
+ fs.renameSync(tmpPath, filePath);
61
+ }
62
+ function loadAccounts() {
63
+ const raw = safeReadJSON(ACCOUNTS_FILE, {
64
+ accounts: []
65
+ });
66
+ const accounts = (raw.accounts || []).map((a) => normalizeAccount(a));
67
+ return { accounts };
68
+ }
69
+ function saveAccounts(data) {
70
+ safeWriteJSON(ACCOUNTS_FILE, data);
71
+ }
72
+ function addAccount(account) {
73
+ const data = loadAccounts();
74
+ const idx = data.accounts.findIndex((a) => a.name === account.name);
75
+ if (idx >= 0) {
76
+ data.accounts[idx] = account;
77
+ } else {
78
+ data.accounts.push(account);
79
+ }
80
+ saveAccounts(data);
81
+ return data;
82
+ }
83
+ function removeAccount(name) {
84
+ const data = loadAccounts();
85
+ data.accounts = data.accounts.filter((a) => a.name !== name);
86
+ saveAccounts(data);
87
+ return data;
88
+ }
89
+
90
+ // src/state.ts
91
+ var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
92
+ var EMPTY_USAGE = {
93
+ session5h: { ...EMPTY_METRIC },
94
+ weekly7d: { ...EMPTY_METRIC },
95
+ weekly7dSonnet: { ...EMPTY_METRIC }
96
+ };
97
+ function defaultState() {
98
+ return {
99
+ currentAccount: null,
100
+ lastRotationCheck: 0,
101
+ requestCount: 0,
102
+ config: {
103
+ threshold: DEFAULT_THRESHOLD,
104
+ checkInterval: DEFAULT_CHECK_INTERVAL
105
+ },
106
+ usage: {},
107
+ authFailures: {}
108
+ };
109
+ }
110
+ function loadState() {
111
+ const raw = safeReadJSON(STATE_FILE, {});
112
+ const defaults = defaultState();
113
+ return {
114
+ currentAccount: raw.currentAccount ?? defaults.currentAccount,
115
+ lastRotationCheck: raw.lastRotationCheck ?? defaults.lastRotationCheck,
116
+ requestCount: raw.requestCount ?? defaults.requestCount,
117
+ config: {
118
+ threshold: raw.config?.threshold ?? defaults.config.threshold,
119
+ checkInterval: raw.config?.checkInterval ?? defaults.config.checkInterval
120
+ },
121
+ usage: raw.usage ?? defaults.usage,
122
+ authFailures: raw.authFailures ?? defaults.authFailures
123
+ };
124
+ }
125
+ function saveState(state) {
126
+ safeWriteJSON(STATE_FILE, state);
127
+ }
128
+ function getThresholds(config) {
129
+ if (typeof config.threshold === "number") {
130
+ return {
131
+ session5h: config.threshold,
132
+ weekly7d: config.threshold,
133
+ weekly7dSonnet: config.threshold
134
+ };
135
+ }
136
+ return config.threshold;
137
+ }
138
+ function resolveStaleMetrics(state) {
139
+ const now = Date.now() / 1000;
140
+ for (const accountName of Object.keys(state.usage)) {
141
+ const usage = state.usage[accountName];
142
+ const metrics = ["session5h", "weekly7d", "weekly7dSonnet"];
143
+ for (const key of metrics) {
144
+ const metric = usage[key];
145
+ if (metric.reset > 0 && metric.reset <= now) {
146
+ metric.utilization = 0;
147
+ metric.reset = 0;
148
+ metric.status = "";
149
+ }
150
+ }
151
+ }
152
+ }
153
+ function ensureAccountsInState(state, accountNames) {
154
+ for (const name of accountNames) {
155
+ if (!state.usage[name]) {
156
+ state.usage[name] = {
157
+ session5h: { ...EMPTY_METRIC },
158
+ weekly7d: { ...EMPTY_METRIC },
159
+ weekly7dSonnet: { ...EMPTY_METRIC }
160
+ };
161
+ }
162
+ }
163
+ }
164
+
165
+ // src/rotation.ts
166
+ function clearAuthFailure(state, accountName) {
167
+ delete state.authFailures[accountName];
168
+ }
169
+
170
+ // node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
171
+ var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
172
+ var AUTHORIZE_URLS = {
173
+ console: "https://platform.claude.com/oauth/authorize",
174
+ max: "https://claude.ai/oauth/authorize"
175
+ };
176
+ var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
177
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
178
+ var OAUTH_SCOPES = [
179
+ "org:create_api_key",
180
+ "user:profile",
181
+ "user:inference",
182
+ "user:sessions:claude_code",
183
+ "user:mcp_servers",
184
+ "user:file_upload"
185
+ ];
186
+
187
+ // node_modules/@ex-machina/opencode-anthropic-auth/dist/pkce.js
188
+ function base64UrlEncode(bytes) {
189
+ let bin = "";
190
+ for (const byte of bytes)
191
+ bin += String.fromCharCode(byte);
192
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
193
+ }
194
+ async function generatePKCE() {
195
+ const buf = new Uint8Array(64);
196
+ crypto.getRandomValues(buf);
197
+ const verifier = base64UrlEncode(buf);
198
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
199
+ return {
200
+ verifier,
201
+ challenge: base64UrlEncode(new Uint8Array(digest)),
202
+ method: "S256"
203
+ };
204
+ }
205
+
206
+ // node_modules/@ex-machina/opencode-anthropic-auth/dist/auth.js
207
+ function generateState() {
208
+ return crypto.randomUUID().replace(/-/g, "");
209
+ }
210
+ function parseCallbackInput(input) {
211
+ const trimmed = input.trim();
212
+ try {
213
+ const url = new URL(trimmed);
214
+ const code2 = url.searchParams.get("code");
215
+ const state2 = url.searchParams.get("state");
216
+ if (code2 && state2) {
217
+ return { code: code2, state: state2 };
218
+ }
219
+ } catch {}
220
+ const hashSplits = trimmed.split("#");
221
+ if (hashSplits.length === 2 && hashSplits[0] && hashSplits[1]) {
222
+ return { code: hashSplits[0], state: hashSplits[1] };
223
+ }
224
+ const params = new URLSearchParams(trimmed);
225
+ const code = params.get("code");
226
+ const state = params.get("state");
227
+ if (code && state) {
228
+ return { code, state };
229
+ }
230
+ return null;
231
+ }
232
+ async function exchangeCode(callback, verifier, redirectUri) {
233
+ const result = await fetch(TOKEN_URL, {
234
+ method: "POST",
235
+ headers: {
236
+ "Content-Type": "application/json",
237
+ Accept: "application/json, text/plain, */*",
238
+ "User-Agent": "axios/1.13.6"
239
+ },
240
+ body: JSON.stringify({
241
+ code: callback.code,
242
+ state: callback.state,
243
+ grant_type: "authorization_code",
244
+ client_id: CLIENT_ID,
245
+ redirect_uri: redirectUri,
246
+ code_verifier: verifier
247
+ })
248
+ });
249
+ if (!result.ok) {
250
+ return {
251
+ type: "failed"
252
+ };
253
+ }
254
+ const json = await result.json();
255
+ return {
256
+ type: "success",
257
+ refresh: json.refresh_token,
258
+ access: json.access_token,
259
+ expires: Date.now() + json.expires_in * 1000
260
+ };
261
+ }
262
+ async function authorize(mode) {
263
+ const pkce = await generatePKCE();
264
+ const state = generateState();
265
+ const url = new URL(AUTHORIZE_URLS[mode], import.meta.url);
266
+ url.searchParams.set("code", "true");
267
+ url.searchParams.set("client_id", CLIENT_ID);
268
+ url.searchParams.set("response_type", "code");
269
+ url.searchParams.set("redirect_uri", CODE_CALLBACK_URL);
270
+ url.searchParams.set("scope", OAUTH_SCOPES.join(" "));
271
+ url.searchParams.set("code_challenge", pkce.challenge);
272
+ url.searchParams.set("code_challenge_method", "S256");
273
+ url.searchParams.set("state", state);
274
+ return {
275
+ url: url.toString(),
276
+ redirectUri: CODE_CALLBACK_URL,
277
+ state,
278
+ verifier: pkce.verifier
279
+ };
280
+ }
281
+ async function exchange(input, verifier, redirectUri, expectedState) {
282
+ const callback = parseCallbackInput(input);
283
+ if (!callback) {
284
+ return {
285
+ type: "failed"
286
+ };
287
+ }
288
+ if (expectedState && callback.state !== expectedState) {
289
+ return {
290
+ type: "failed"
291
+ };
292
+ }
293
+ return exchangeCode(callback, verifier, redirectUri);
294
+ }
295
+
296
+ // src/cli.ts
297
+ var RESET = "\x1B[0m";
298
+ var BOLD = "\x1B[1m";
299
+ var DIM = "\x1B[2m";
300
+ var RED = "\x1B[31m";
301
+ var GREEN = "\x1B[32m";
302
+ var YELLOW = "\x1B[33m";
303
+ var BLUE = "\x1B[34m";
304
+ var CYAN = "\x1B[36m";
305
+ function progressBar(value, threshold, width = 30) {
306
+ const pct = Math.min(value, 1);
307
+ const filled = Math.round(pct * width);
308
+ const empty = width - filled;
309
+ let color = GREEN;
310
+ if (value >= threshold)
311
+ color = RED;
312
+ else if (value >= threshold * 0.8)
313
+ color = YELLOW;
314
+ const bar = color + "\u2588".repeat(filled) + DIM + "\u2591".repeat(empty) + RESET;
315
+ const label = `${(value * 100).toFixed(1)}%`;
316
+ return `${bar} ${color}${label}${RESET}`;
317
+ }
318
+ async function prompt(question) {
319
+ process.stdout.write(question);
320
+ const buf = [];
321
+ return new Promise((resolve) => {
322
+ process.stdin.resume();
323
+ process.stdin.setEncoding("utf-8");
324
+ const onData = (chunk) => {
325
+ const str = chunk.toString();
326
+ if (str.includes(`
327
+ `)) {
328
+ process.stdin.removeListener("data", onData);
329
+ process.stdin.pause();
330
+ buf.push(Buffer.from(str));
331
+ resolve(Buffer.concat(buf).toString().trim());
332
+ } else {
333
+ buf.push(Buffer.from(str));
334
+ }
335
+ };
336
+ process.stdin.on("data", onData);
337
+ });
338
+ }
339
+ async function cmdAdd(args) {
340
+ const name = args[0] || await prompt(`${CYAN}Account name: ${RESET}`);
341
+ if (!name) {
342
+ console.error(`${RED}Account name is required${RESET}`);
343
+ process.exit(1);
344
+ }
345
+ console.log(`
346
+ ${BOLD}Starting OAuth flow for account: ${CYAN}${name}${RESET}
347
+ `);
348
+ const authResult = await authorize("max");
349
+ console.log(`${BOLD}Open this URL in your browser:${RESET}
350
+ `);
351
+ console.log(` ${BLUE}${authResult.url}${RESET}
352
+ `);
353
+ console.log(`${DIM}After authorizing, paste the callback URL or code below:${RESET}
354
+ `);
355
+ const callbackInput = await prompt(`${CYAN}Callback: ${RESET}`);
356
+ const exchangeResult = await exchange(callbackInput, authResult.verifier, authResult.redirectUri, authResult.state);
357
+ if (exchangeResult.type === "failed") {
358
+ console.error(`
359
+ ${RED}Authentication failed${RESET}`);
360
+ process.exit(1);
361
+ }
362
+ addAccount({
363
+ name,
364
+ access: exchangeResult.access,
365
+ refresh: exchangeResult.refresh,
366
+ expires: exchangeResult.expires
367
+ });
368
+ console.log(`
369
+ ${GREEN}Account "${name}" added successfully.${RESET}`);
370
+ console.log(`${DIM}Stored in: ${ACCOUNTS_FILE}${RESET}`);
371
+ const data = loadAccounts();
372
+ if (data.accounts.length === 1) {
373
+ const state = loadState();
374
+ state.currentAccount = name;
375
+ saveState(state);
376
+ console.log(`
377
+ ${YELLOW}This is the only account \u2014 set as active.${RESET}`);
378
+ }
379
+ }
380
+ async function cmdReauth(args) {
381
+ const data = loadAccounts();
382
+ if (data.accounts.length === 0) {
383
+ console.error(`${RED}No accounts configured.${RESET}`);
384
+ process.exit(1);
385
+ }
386
+ const name = args[0];
387
+ if (!name) {
388
+ console.log(`
389
+ ${BOLD}Available accounts:${RESET}`);
390
+ for (const a of data.accounts) {
391
+ console.log(` - ${a.name}`);
392
+ }
393
+ console.error(`
394
+ ${RED}Usage: oc-auth-switcher reauth <account-name>${RESET}`);
395
+ process.exit(1);
396
+ }
397
+ const account = data.accounts.find((a) => a.name === name);
398
+ if (!account) {
399
+ console.error(`${RED}Account "${name}" not found${RESET}`);
400
+ process.exit(1);
401
+ }
402
+ console.log(`
403
+ ${BOLD}Re-authenticating account: ${CYAN}${name}${RESET}
404
+ `);
405
+ const authResult = await authorize("max");
406
+ console.log(`${BOLD}Open this URL in your browser:${RESET}
407
+ `);
408
+ console.log(` ${BLUE}${authResult.url}${RESET}
409
+ `);
410
+ const callbackInput = await prompt(`${CYAN}Callback: ${RESET}`);
411
+ const exchangeResult = await exchange(callbackInput, authResult.verifier, authResult.redirectUri, authResult.state);
412
+ if (exchangeResult.type === "failed") {
413
+ console.error(`
414
+ ${RED}Re-authentication failed${RESET}`);
415
+ process.exit(1);
416
+ }
417
+ account.access = exchangeResult.access;
418
+ account.refresh = exchangeResult.refresh;
419
+ account.expires = exchangeResult.expires;
420
+ saveAccounts(data);
421
+ const state = loadState();
422
+ clearAuthFailure(state, name);
423
+ saveState(state);
424
+ console.log(`
425
+ ${GREEN}Account "${name}" re-authenticated successfully.${RESET}`);
426
+ }
427
+ function cmdUsage(args) {
428
+ const watch = args.includes("--watch") || args.includes("-w");
429
+ const showUsage = () => {
430
+ const data = loadAccounts();
431
+ const state = loadState();
432
+ resolveStaleMetrics(state);
433
+ ensureAccountsInState(state, data.accounts.map((a) => a.name));
434
+ const thresholds = getThresholds(state.config);
435
+ if (watch) {
436
+ process.stdout.write("\x1B[2J\x1B[H");
437
+ }
438
+ console.log(`
439
+ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
440
+ `);
441
+ console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
442
+ console.log(` Total accounts: ${data.accounts.length}`);
443
+ console.log(` Thresholds: 5h=${(thresholds.session5h * 100).toFixed(0)}% 7d=${(thresholds.weekly7d * 100).toFixed(0)}% 7d-sonnet=${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
444
+ console.log();
445
+ if (data.accounts.length === 0) {
446
+ console.log(` ${DIM}No accounts configured. Run 'oc-auth-switcher add' to add one.${RESET}
447
+ `);
448
+ return;
449
+ }
450
+ for (const account of data.accounts) {
451
+ const usage = state.usage[account.name];
452
+ const isActive = account.name === state.currentAccount;
453
+ const isCooling = !!state.authFailures[account.name] && state.authFailures[account.name] > Date.now();
454
+ const tag = isActive ? `${GREEN} [ACTIVE]${RESET}` : isCooling ? `${RED} [COOLDOWN]${RESET}` : "";
455
+ console.log(` ${BOLD}${account.name}${RESET}${tag}`);
456
+ if (usage) {
457
+ console.log(` 5h session: ${progressBar(usage.session5h.utilization, thresholds.session5h)}`);
458
+ console.log(` 7d weekly: ${progressBar(usage.weekly7d.utilization, thresholds.weekly7d)}`);
459
+ console.log(` 7d sonnet: ${progressBar(usage.weekly7dSonnet.utilization, thresholds.weekly7dSonnet)}`);
460
+ if (usage.timestamp) {
461
+ console.log(` ${DIM}Last updated: ${usage.timestamp}${RESET}`);
462
+ }
463
+ } else {
464
+ console.log(` ${DIM}No usage data \u2014 metrics update automatically on each API request${RESET}`);
465
+ }
466
+ const tokenExpiry = new Date(account.expires);
467
+ const isExpired = account.expires <= Date.now();
468
+ const tokenStatus = isExpired ? `${RED}EXPIRED${RESET}` : `${GREEN}valid until ${tokenExpiry.toLocaleString()}${RESET}`;
469
+ console.log(` Token: ${tokenStatus}`);
470
+ console.log();
471
+ }
472
+ };
473
+ showUsage();
474
+ if (watch) {
475
+ console.log(`${DIM}Refreshing every 5 seconds. Press Ctrl+C to stop.${RESET}
476
+ `);
477
+ setInterval(showUsage, 5000);
478
+ new Promise(() => {});
479
+ }
480
+ }
481
+ function cmdConfig(args) {
482
+ const state = loadState();
483
+ if (args.length === 0) {
484
+ const thresholds = getThresholds(state.config);
485
+ console.log(`
486
+ ${BOLD}${CYAN}=== Auth Switcher Configuration ===${RESET}
487
+ `);
488
+ console.log(` Threshold (5h): ${(thresholds.session5h * 100).toFixed(0)}%`);
489
+ console.log(` Threshold (7d): ${(thresholds.weekly7d * 100).toFixed(0)}%`);
490
+ console.log(` Threshold (7d sonnet): ${(thresholds.weekly7dSonnet * 100).toFixed(0)}%`);
491
+ console.log(` Check interval: ${state.config.checkInterval / 60000} min`);
492
+ console.log();
493
+ console.log(`${DIM} Config file: ${STATE_FILE}${RESET}`);
494
+ console.log();
495
+ return;
496
+ }
497
+ for (let i = 0;i < args.length; i++) {
498
+ const arg = args[i];
499
+ if (arg === "--threshold" && args[i + 1]) {
500
+ const val = parseFloat(args[++i]);
501
+ if (isNaN(val) || val < 0 || val > 1) {
502
+ console.error(`${RED}Threshold must be between 0 and 1 (e.g., 0.90)${RESET}`);
503
+ process.exit(1);
504
+ }
505
+ state.config.threshold = val;
506
+ console.log(`${GREEN}Set uniform threshold to ${(val * 100).toFixed(0)}%${RESET}`);
507
+ } else if (arg === "--thresholds" && args[i + 1]) {
508
+ const parts = args[++i].split(",").map((s) => parseFloat(s.trim()));
509
+ if (parts.length !== 3 || parts.some(isNaN)) {
510
+ console.error(`${RED}--thresholds requires 3 comma-separated values (e.g., 90,80,70)${RESET}`);
511
+ process.exit(1);
512
+ }
513
+ const normalized = parts.map((v) => v > 1 ? v / 100 : v);
514
+ state.config.threshold = {
515
+ session5h: normalized[0],
516
+ weekly7d: normalized[1],
517
+ weekly7dSonnet: normalized[2]
518
+ };
519
+ console.log(`${GREEN}Set per-metric thresholds: 5h=${(normalized[0] * 100).toFixed(0)}% 7d=${(normalized[1] * 100).toFixed(0)}% 7d-sonnet=${(normalized[2] * 100).toFixed(0)}%${RESET}`);
520
+ } else if (arg === "--interval" && args[i + 1]) {
521
+ const minutes = parseInt(args[++i], 10);
522
+ if (isNaN(minutes) || minutes < 1) {
523
+ console.error(`${RED}Interval must be a positive number of minutes${RESET}`);
524
+ process.exit(1);
525
+ }
526
+ state.config.checkInterval = minutes * 60 * 1000;
527
+ console.log(`${GREEN}Set check interval to ${minutes} minutes${RESET}`);
528
+ } else if (arg === "--reset") {
529
+ state.config.threshold = DEFAULT_THRESHOLD;
530
+ state.config.checkInterval = DEFAULT_CHECK_INTERVAL;
531
+ console.log(`${GREEN}Reset to defaults: threshold=${(DEFAULT_THRESHOLD * 100).toFixed(0)}% interval=${DEFAULT_CHECK_INTERVAL / 60000}min${RESET}`);
532
+ }
533
+ }
534
+ saveState(state);
535
+ }
536
+ function cmdSwitch(args) {
537
+ const data = loadAccounts();
538
+ if (data.accounts.length === 0) {
539
+ console.error(`${RED}No accounts configured.${RESET}`);
540
+ process.exit(1);
541
+ }
542
+ const name = args[0];
543
+ if (!name) {
544
+ console.log(`
545
+ ${BOLD}Available accounts:${RESET}`);
546
+ const state2 = loadState();
547
+ for (const a of data.accounts) {
548
+ const tag = a.name === state2.currentAccount ? ` ${GREEN}[ACTIVE]${RESET}` : "";
549
+ console.log(` - ${a.name}${tag}`);
550
+ }
551
+ console.error(`
552
+ ${RED}Usage: oc-auth-switcher switch <account-name>${RESET}`);
553
+ process.exit(1);
554
+ }
555
+ const account = data.accounts.find((a) => a.name === name);
556
+ if (!account) {
557
+ console.error(`${RED}Account "${name}" not found${RESET}`);
558
+ process.exit(1);
559
+ }
560
+ console.log(`${CYAN}Switching to account: ${name}...${RESET}`);
561
+ const state = loadState();
562
+ state.currentAccount = name;
563
+ state.lastRotationCheck = Date.now();
564
+ saveState(state);
565
+ console.log(`${GREEN}Switched to "${name}". Will take effect on the next API request.${RESET}`);
566
+ }
567
+ function cmdStatus() {
568
+ const data = loadAccounts();
569
+ const state = loadState();
570
+ resolveStaleMetrics(state);
571
+ console.log(`
572
+ ${BOLD}${CYAN}=== Auth Switcher Status ===${RESET}
573
+ `);
574
+ console.log(` Active account: ${BOLD}${state.currentAccount || "(none)"}${RESET}`);
575
+ console.log(` Total accounts: ${data.accounts.length}`);
576
+ console.log(` Request count: ${state.requestCount}`);
577
+ console.log(` Last rotation: ${state.lastRotationCheck ? new Date(state.lastRotationCheck).toLocaleString() : "never"}`);
578
+ console.log();
579
+ const failures = Object.entries(state.authFailures).filter(([_, until]) => until > Date.now());
580
+ if (failures.length > 0) {
581
+ console.log(` ${RED}Auth cooldowns:${RESET}`);
582
+ for (const [name, until] of failures) {
583
+ const remaining = Math.ceil((until - Date.now()) / 60000);
584
+ console.log(` - ${name}: ${remaining} min remaining`);
585
+ }
586
+ console.log();
587
+ }
588
+ console.log(` ${DIM}Accounts file: ${ACCOUNTS_FILE}${RESET}`);
589
+ console.log(` ${DIM}State file: ${STATE_FILE}${RESET}`);
590
+ console.log();
591
+ }
592
+ function cmdRemove(args) {
593
+ const name = args[0];
594
+ if (!name) {
595
+ console.error(`${RED}Usage: oc-auth-switcher remove <account-name>${RESET}`);
596
+ process.exit(1);
597
+ }
598
+ const data = loadAccounts();
599
+ const found = data.accounts.find((a) => a.name === name);
600
+ if (!found) {
601
+ console.error(`${RED}Account "${name}" not found${RESET}`);
602
+ process.exit(1);
603
+ }
604
+ removeAccount(name);
605
+ console.log(`${GREEN}Account "${name}" removed.${RESET}`);
606
+ const state = loadState();
607
+ if (state.currentAccount === name) {
608
+ state.currentAccount = null;
609
+ saveState(state);
610
+ console.log(`${YELLOW}This was the active account. Rotation will pick a new one automatically.${RESET}`);
611
+ }
612
+ }
613
+ function showHelp() {
614
+ console.log(`
615
+ ${BOLD}oc-auth-switcher${RESET} \u2014 Multi-account Anthropic Claude Max rotation plugin
616
+
617
+ ${BOLD}USAGE:${RESET}
618
+ oc-auth-switcher <command> [options]
619
+
620
+ ${BOLD}COMMANDS:${RESET}
621
+ ${CYAN}add${RESET} [name] Add a new Anthropic account via OAuth
622
+ ${CYAN}reauth${RESET} <name> Re-authenticate an existing account
623
+ ${CYAN}usage${RESET} [--watch] Show usage dashboard with utilization metrics
624
+ ${CYAN}config${RESET} [options] View or modify threshold/interval configuration
625
+ ${CYAN}switch${RESET} <name> Manually switch to a specific account
626
+ ${CYAN}status${RESET} Show current active account and rotation state
627
+ ${CYAN}remove${RESET} <name> Remove an account from the pool
628
+
629
+ ${BOLD}CONFIG OPTIONS:${RESET}
630
+ --threshold <0-1> Set uniform threshold (e.g., 0.90)
631
+ --thresholds <a,b,c> Set per-metric thresholds (5h,7d,7d-sonnet)
632
+ --interval <minutes> Set primary recovery check interval
633
+ --reset Reset to defaults
634
+
635
+ ${BOLD}EXAMPLES:${RESET}
636
+ oc-auth-switcher add primary
637
+ oc-auth-switcher add fallback-1
638
+ oc-auth-switcher usage --watch
639
+ oc-auth-switcher config --threshold 0.90
640
+ oc-auth-switcher switch fallback-1
641
+ `);
642
+ }
643
+ async function main() {
644
+ const args = process.argv.slice(2);
645
+ const command = args[0];
646
+ const commandArgs = args.slice(1);
647
+ switch (command) {
648
+ case "add":
649
+ case "a":
650
+ await cmdAdd(commandArgs);
651
+ break;
652
+ case "reauth":
653
+ await cmdReauth(commandArgs);
654
+ break;
655
+ case "usage":
656
+ case "u":
657
+ cmdUsage(commandArgs);
658
+ break;
659
+ case "config":
660
+ case "c":
661
+ cmdConfig(commandArgs);
662
+ break;
663
+ case "switch":
664
+ case "s":
665
+ cmdSwitch(commandArgs);
666
+ break;
667
+ case "status":
668
+ cmdStatus();
669
+ break;
670
+ case "remove":
671
+ case "rm":
672
+ cmdRemove(commandArgs);
673
+ break;
674
+ case "help":
675
+ case "--help":
676
+ case "-h":
677
+ case undefined:
678
+ showHelp();
679
+ break;
680
+ default:
681
+ console.error(`${RED}Unknown command: ${command}${RESET}`);
682
+ showHelp();
683
+ process.exit(1);
684
+ }
685
+ }
686
+ main().catch((err) => {
687
+ console.error(`${RED}Fatal error:${RESET}`, err);
688
+ process.exit(1);
689
+ });