oc-auth-switcher 0.1.2 → 0.2.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/README.md CHANGED
@@ -8,7 +8,7 @@ This plugin registers as the Anthropic auth provider for OpenCode. It uses [`@ex
8
8
 
9
9
  1. **Multi-account rotation** — maintains a pool of OAuth accounts and selects the best one on each request
10
10
  2. **Real-time metric capture** — reads Anthropic's rate-limit response headers on every API call (no manual pinging needed)
11
- 3. **Automatic failover** — when any utilization metric exceeds the threshold (default 90%), the next request automatically uses a different account
11
+ 3. **Automatic failover** — when any utilization metric exceeds the threshold (default 95%), the next request automatically uses a different account
12
12
 
13
13
  ## Setup
14
14
 
@@ -58,8 +58,8 @@ oc-auth-switcher <command> [options]
58
58
  ## Configuration
59
59
 
60
60
  ```bash
61
- # Set uniform threshold (default: 90%)
62
- oc-auth-switcher config --threshold 0.90
61
+ # Set uniform threshold (default: 95%)
62
+ oc-auth-switcher config --threshold 0.95
63
63
 
64
64
  # Set per-metric thresholds (5h, 7d, 7d-sonnet)
65
65
  oc-auth-switcher config --thresholds 90,80,70
package/dist/cli.js CHANGED
@@ -11,10 +11,27 @@ import os from "os";
11
11
  var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
12
12
  var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
13
13
  var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
14
- var DEFAULT_THRESHOLD = 0.9;
14
+ var DEFAULT_THRESHOLD = 0.95;
15
15
  var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
16
16
  var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
17
17
 
18
+ // node_modules/@ex-machina/opencode-anthropic-auth/dist/constants.js
19
+ var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
20
+ var AUTHORIZE_URLS = {
21
+ console: "https://platform.claude.com/oauth/authorize",
22
+ max: "https://claude.ai/oauth/authorize"
23
+ };
24
+ var CODE_CALLBACK_URL = "https://platform.claude.com/oauth/code/callback";
25
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
26
+ var OAUTH_SCOPES = [
27
+ "org:create_api_key",
28
+ "user:profile",
29
+ "user:inference",
30
+ "user:sessions:claude_code",
31
+ "user:mcp_servers",
32
+ "user:file_upload"
33
+ ];
34
+
18
35
  // src/accounts.ts
19
36
  function normalizeAccount(raw) {
20
37
  const name = raw.name || "unnamed";
@@ -86,6 +103,49 @@ function removeAccount(name) {
86
103
  saveAccounts(data);
87
104
  return data;
88
105
  }
106
+ function updateAccountTokens(name, access, refresh, expires) {
107
+ const data = loadAccounts();
108
+ const account = data.accounts.find((a) => a.name === name);
109
+ if (account) {
110
+ account.access = access;
111
+ account.refresh = refresh;
112
+ account.expires = expires;
113
+ saveAccounts(data);
114
+ }
115
+ }
116
+ async function refreshAccountToken(refreshTokenValue) {
117
+ try {
118
+ const response = await fetch(TOKEN_URL, {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ Accept: "application/json, text/plain, */*",
123
+ "User-Agent": "axios/1.13.6"
124
+ },
125
+ body: JSON.stringify({
126
+ grant_type: "refresh_token",
127
+ refresh_token: refreshTokenValue,
128
+ client_id: CLIENT_ID
129
+ })
130
+ });
131
+ if (!response.ok) {
132
+ const body = await response.text().catch(() => "");
133
+ return { ok: false, error: `HTTP ${response.status}: ${body}` };
134
+ }
135
+ const json = await response.json();
136
+ return {
137
+ ok: true,
138
+ access: json.access_token,
139
+ refresh: json.refresh_token,
140
+ expires: Date.now() + json.expires_in * 1000
141
+ };
142
+ } catch (err) {
143
+ return {
144
+ ok: false,
145
+ error: err instanceof Error ? err.message : String(err)
146
+ };
147
+ }
148
+ }
89
149
 
90
150
  // src/state.ts
91
151
  var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
@@ -167,23 +227,6 @@ function clearAuthFailure(state, accountName) {
167
227
  delete state.authFailures[accountName];
168
228
  }
169
229
 
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
230
  // node_modules/@ex-machina/opencode-anthropic-auth/dist/pkce.js
188
231
  function base64UrlEncode(bytes) {
189
232
  let bin = "";
@@ -294,6 +337,7 @@ async function exchange(input, verifier, redirectUri, expectedState) {
294
337
  }
295
338
 
296
339
  // src/cli.ts
340
+ import { spawn } from "child_process";
297
341
  var RESET = "\x1B[0m";
298
342
  var BOLD = "\x1B[1m";
299
343
  var DIM = "\x1B[2m";
@@ -315,6 +359,36 @@ function progressBar(value, threshold, width = 30) {
315
359
  const label = `${(value * 100).toFixed(1)}%`;
316
360
  return `${bar} ${color}${label}${RESET}`;
317
361
  }
362
+ function tryCopy(cmd, args, text) {
363
+ return new Promise((resolve) => {
364
+ try {
365
+ const child = spawn(cmd, args);
366
+ child.on("error", () => resolve(false));
367
+ child.on("close", (code) => resolve(code === 0));
368
+ child.stdin.on("error", () => resolve(false));
369
+ child.stdin.end(text);
370
+ } catch {
371
+ resolve(false);
372
+ }
373
+ });
374
+ }
375
+ async function copyToClipboard(text) {
376
+ switch (process.platform) {
377
+ case "darwin":
378
+ return tryCopy("pbcopy", [], text);
379
+ case "win32":
380
+ return tryCopy("clip", [], text);
381
+ default: {
382
+ if (await tryCopy("wl-copy", [], text))
383
+ return true;
384
+ if (await tryCopy("xclip", ["-selection", "clipboard"], text))
385
+ return true;
386
+ if (await tryCopy("xsel", ["--clipboard", "--input"], text))
387
+ return true;
388
+ return false;
389
+ }
390
+ }
391
+ }
318
392
  async function prompt(question) {
319
393
  process.stdout.write(question);
320
394
  const buf = [];
@@ -349,6 +423,10 @@ ${BOLD}Starting OAuth flow for account: ${CYAN}${name}${RESET}
349
423
  console.log(`${BOLD}Open this URL in your browser:${RESET}
350
424
  `);
351
425
  console.log(` ${BLUE}${authResult.url}${RESET}
426
+ `);
427
+ const copied = await copyToClipboard(authResult.url);
428
+ if (copied)
429
+ console.log(`${DIM}(URL copied to clipboard)${RESET}
352
430
  `);
353
431
  console.log(`${DIM}After authorizing, paste the callback URL or code below:${RESET}
354
432
  `);
@@ -406,6 +484,10 @@ ${BOLD}Re-authenticating account: ${CYAN}${name}${RESET}
406
484
  console.log(`${BOLD}Open this URL in your browser:${RESET}
407
485
  `);
408
486
  console.log(` ${BLUE}${authResult.url}${RESET}
487
+ `);
488
+ const copied = await copyToClipboard(authResult.url);
489
+ if (copied)
490
+ console.log(`${DIM}(URL copied to clipboard)${RESET}
409
491
  `);
410
492
  const callbackInput = await prompt(`${CYAN}Callback: ${RESET}`);
411
493
  const exchangeResult = await exchange(callbackInput, authResult.verifier, authResult.redirectUri, authResult.state);
@@ -424,11 +506,27 @@ ${RED}Re-authentication failed${RESET}`);
424
506
  console.log(`
425
507
  ${GREEN}Account "${name}" re-authenticated successfully.${RESET}`);
426
508
  }
427
- function cmdUsage(args) {
509
+ async function refreshExpiredAccounts(data, state) {
510
+ for (const account of data.accounts) {
511
+ if (account.access && account.expires > Date.now())
512
+ continue;
513
+ const result = await refreshAccountToken(account.refresh);
514
+ if (result.ok && result.access && result.refresh && result.expires) {
515
+ account.access = result.access;
516
+ account.refresh = result.refresh;
517
+ account.expires = result.expires;
518
+ updateAccountTokens(account.name, result.access, result.refresh, result.expires);
519
+ clearAuthFailure(state, account.name);
520
+ }
521
+ }
522
+ }
523
+ async function cmdUsage(args) {
428
524
  const watch = args.includes("--watch") || args.includes("-w");
429
- const showUsage = () => {
525
+ const showUsage = async () => {
430
526
  const data = loadAccounts();
431
527
  const state = loadState();
528
+ await refreshExpiredAccounts(data, state);
529
+ saveState(state);
432
530
  resolveStaleMetrics(state);
433
531
  ensureAccountsInState(state, data.accounts.map((a) => a.name));
434
532
  const thresholds = getThresholds(state.config);
@@ -470,11 +568,19 @@ ${BOLD}${CYAN}=== Auth Switcher Usage Dashboard ===${RESET}
470
568
  console.log();
471
569
  }
472
570
  };
473
- showUsage();
571
+ await showUsage();
474
572
  if (watch) {
475
573
  console.log(`${DIM}Refreshing every 5 seconds. Press Ctrl+C to stop.${RESET}
476
574
  `);
477
- setInterval(showUsage, 5000);
575
+ let running = false;
576
+ setInterval(() => {
577
+ if (running)
578
+ return;
579
+ running = true;
580
+ showUsage().catch((err) => console.error(`${RED}Refresh error:${RESET}`, err)).finally(() => {
581
+ running = false;
582
+ });
583
+ }, 5000);
478
584
  new Promise(() => {});
479
585
  }
480
586
  }
@@ -654,7 +760,7 @@ async function main() {
654
760
  break;
655
761
  case "usage":
656
762
  case "u":
657
- cmdUsage(commandArgs);
763
+ await cmdUsage(commandArgs);
658
764
  break;
659
765
  case "config":
660
766
  case "c":
package/dist/index.js CHANGED
@@ -413,7 +413,7 @@ import os from "node:os";
413
413
  var configDir = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode");
414
414
  var ACCOUNTS_FILE = path.join(configDir, "auth-switcher-accounts.json");
415
415
  var STATE_FILE = path.join(configDir, "auth-switcher-state.json");
416
- var DEFAULT_THRESHOLD = 0.9;
416
+ var DEFAULT_THRESHOLD = 0.95;
417
417
  var DEFAULT_CHECK_INTERVAL = 60 * 60 * 1000;
418
418
  var AUTH_FAILURE_COOLDOWN = 10 * 60 * 1000;
419
419
 
@@ -481,6 +481,39 @@ function updateAccountTokens(name, access, refresh, expires) {
481
481
  saveAccounts(data);
482
482
  }
483
483
  }
484
+ async function refreshAccountToken(refreshTokenValue) {
485
+ try {
486
+ const response = await fetch(TOKEN_URL, {
487
+ method: "POST",
488
+ headers: {
489
+ "Content-Type": "application/json",
490
+ Accept: "application/json, text/plain, */*",
491
+ "User-Agent": "axios/1.13.6"
492
+ },
493
+ body: JSON.stringify({
494
+ grant_type: "refresh_token",
495
+ refresh_token: refreshTokenValue,
496
+ client_id: CLIENT_ID
497
+ })
498
+ });
499
+ if (!response.ok) {
500
+ const body = await response.text().catch(() => "");
501
+ return { ok: false, error: `HTTP ${response.status}: ${body}` };
502
+ }
503
+ const json = await response.json();
504
+ return {
505
+ ok: true,
506
+ access: json.access_token,
507
+ refresh: json.refresh_token,
508
+ expires: Date.now() + json.expires_in * 1000
509
+ };
510
+ } catch (err) {
511
+ return {
512
+ ok: false,
513
+ error: err instanceof Error ? err.message : String(err)
514
+ };
515
+ }
516
+ }
484
517
 
485
518
  // src/state.ts
486
519
  var EMPTY_METRIC = { utilization: 0, reset: 0, status: "" };
@@ -782,39 +815,6 @@ function clearAuthFailure(state, accountName) {
782
815
  }
783
816
 
784
817
  // src/index.ts
785
- async function refreshAccountToken(refreshTokenValue) {
786
- try {
787
- const response = await fetch(TOKEN_URL, {
788
- method: "POST",
789
- headers: {
790
- "Content-Type": "application/json",
791
- Accept: "application/json, text/plain, */*",
792
- "User-Agent": "axios/1.13.6"
793
- },
794
- body: JSON.stringify({
795
- grant_type: "refresh_token",
796
- refresh_token: refreshTokenValue,
797
- client_id: CLIENT_ID
798
- })
799
- });
800
- if (!response.ok) {
801
- const body = await response.text().catch(() => "");
802
- return { ok: false, error: `HTTP ${response.status}: ${body}` };
803
- }
804
- const json = await response.json();
805
- return {
806
- ok: true,
807
- access: json.access_token,
808
- refresh: json.refresh_token,
809
- expires: Date.now() + json.expires_in * 1000
810
- };
811
- } catch (err) {
812
- return {
813
- ok: false,
814
- error: err instanceof Error ? err.message : String(err)
815
- };
816
- }
817
- }
818
818
  var AuthSwitcherPlugin = async ({ client }) => {
819
819
  return {
820
820
  auth: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-auth-switcher",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode auth plugin for multi-account Anthropic Claude Max rotation with automatic failover.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",