claude-switcher-account 2.3.0 → 2.4.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 +7 -5
  2. package/dist/bin.js +70 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,17 +86,19 @@ or `NO_COLOR` is set.
86
86
 
87
87
  Global flags: `--json` (on `list`), `--version`, `--help`.
88
88
 
89
- `claudep list` and the menu show each account's plan usage, the same numbers as
90
- Claude Code's `/usage`:
89
+ `claudep list` shows each account's plan usage, the same numbers as Claude Code's
90
+ `/usage` (the interactive menu stays compact and skips it):
91
91
 
92
92
  ```
93
93
  5h: 40.0% Reset: 55m
94
94
  Weekly: 12.0% Weekly Reset: 5d 9hr 45m
95
95
  ```
96
96
 
97
- They are fetched live from Anthropic with each account's saved token. An account
98
- whose token has expired, or any fetch that fails, shows `--`; tokens are never
99
- refreshed by claudep.
97
+ They are fetched live from Anthropic with each account's token. When a saved
98
+ (inactive) account's access token has expired, claudep refreshes it the way
99
+ Claude Code does and saves the new credential in place of the old one; the active
100
+ login is left to Claude Code. An account that was logged out (no refresh token
101
+ left), or any fetch that fails, shows `--`.
100
102
 
101
103
  ### Interactive menu
102
104
 
package/dist/bin.js CHANGED
@@ -477,6 +477,15 @@ var AccountManager = class {
477
477
  return "";
478
478
  }
479
479
  }
480
+ /** Replace an idle account's saved credential with a refreshed one. Unlike refreshStoredCredential it
481
+ * leaves savedAt and the profile alone: only the tokens changed. */
482
+ async saveRefreshedCredential(name, blob) {
483
+ await this.store.writeSaved(name, blob);
484
+ await this.meta.update((d) => {
485
+ const a = d.accounts.find((x) => x.name === name);
486
+ if (a) a.fingerprint = fingerprint(blob);
487
+ });
488
+ }
480
489
  current() {
481
490
  const label = this.profile.peekLabel();
482
491
  const accounts = this.meta.read().accounts;
@@ -610,9 +619,45 @@ function createManager(env) {
610
619
  return new AccountManager({ store, meta, profile, config });
611
620
  }
612
621
 
622
+ // src/oauth.ts
623
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
624
+ var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
625
+ var TIMEOUT_MS = 5e3;
626
+ async function refreshCredential(blob, nowMs, fetchFn = fetch) {
627
+ try {
628
+ const parsed = JSON.parse(blob);
629
+ const oauth = parsed?.claudeAiOauth;
630
+ if (typeof oauth?.refreshToken !== "string" || !oauth.refreshToken) return null;
631
+ const res = await fetchFn(TOKEN_URL, {
632
+ method: "POST",
633
+ headers: { "Content-Type": "application/json" },
634
+ body: JSON.stringify({ grant_type: "refresh_token", refresh_token: oauth.refreshToken, client_id: CLIENT_ID }),
635
+ signal: AbortSignal.timeout(TIMEOUT_MS)
636
+ });
637
+ if (!res.ok) return null;
638
+ const t = await res.json();
639
+ if (typeof t?.access_token !== "string") return null;
640
+ const next = {
641
+ ...parsed,
642
+ claudeAiOauth: {
643
+ ...oauth,
644
+ accessToken: t.access_token,
645
+ refreshToken: typeof t.refresh_token === "string" ? t.refresh_token : oauth.refreshToken,
646
+ expiresAt: typeof t.expires_in === "number" ? nowMs + t.expires_in * 1e3 : oauth.expiresAt,
647
+ ...typeof t.refresh_token_expires_in === "number" && {
648
+ refreshTokenExpiresAt: nowMs + t.refresh_token_expires_in * 1e3
649
+ }
650
+ }
651
+ };
652
+ return { blob: JSON.stringify(next), accessToken: t.access_token };
653
+ } catch {
654
+ return null;
655
+ }
656
+ }
657
+
613
658
  // src/usage.ts
614
659
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
615
- var TIMEOUT_MS = 3e3;
660
+ var TIMEOUT_MS2 = 3e3;
616
661
  function accessTokenOf(blob, nowMs) {
617
662
  try {
618
663
  const o = JSON.parse(blob)?.claudeAiOauth;
@@ -638,7 +683,7 @@ async function fetchUsage(token, fetchFn = fetch) {
638
683
  try {
639
684
  const res = await fetchFn(USAGE_URL, {
640
685
  headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
641
- signal: AbortSignal.timeout(TIMEOUT_MS)
686
+ signal: AbortSignal.timeout(TIMEOUT_MS2)
642
687
  });
643
688
  if (!res.ok) return null;
644
689
  return parseUsage(await res.json());
@@ -646,10 +691,25 @@ async function fetchUsage(token, fetchFn = fetch) {
646
691
  return null;
647
692
  }
648
693
  }
649
- async function loadUsage(mgr, views, nowMs, fetchFn = fetchUsage) {
694
+ async function liveToken(mgr, v, nowMs, refresh) {
695
+ const blob = await mgr.credentialOf(v);
696
+ const token = accessTokenOf(blob, nowMs);
697
+ if (token || v.active || !blob) return token;
698
+ const r = await refresh(blob, nowMs);
699
+ if (!r) return null;
700
+ try {
701
+ await mgr.saveRefreshedCredential(v.name, r.blob);
702
+ } catch {
703
+ return null;
704
+ }
705
+ return r.accessToken;
706
+ }
707
+ async function loadUsage(mgr, views, nowMs, deps = {}) {
708
+ const fetchFn = deps.fetchUsage ?? fetchUsage;
709
+ const refresh = deps.refresh ?? refreshCredential;
650
710
  const entries = await Promise.all(
651
711
  views.map(async (v) => {
652
- const token = accessTokenOf(await mgr.credentialOf(v), nowMs);
712
+ const token = await liveToken(mgr, v, nowMs, refresh);
653
713
  return [v.name, token ? await fetchFn(token) : null];
654
714
  })
655
715
  );
@@ -840,7 +900,7 @@ function buildDoctorDeps(env) {
840
900
  // package.json
841
901
  var package_default = {
842
902
  name: "claude-switcher-account",
843
- version: "2.3.0",
903
+ version: "2.4.0",
844
904
  description: "Switch between multiple Claude Code accounts from your terminal.",
845
905
  keywords: [
846
906
  "claude",
@@ -932,7 +992,7 @@ import { createInterface } from "readline/promises";
932
992
  function pad2(s, width) {
933
993
  return s.length >= width ? s : s + " ".repeat(width - s.length);
934
994
  }
935
- function buildMenuChoices(accounts, usage, nowMs = Date.now()) {
995
+ function buildMenuChoices(accounts) {
936
996
  const ordered = orderAccounts(accounts);
937
997
  if (!ordered.length) return [];
938
998
  const emailW = Math.max(...ordered.map((a) => a.email.length));
@@ -942,8 +1002,7 @@ function buildMenuChoices(accounts, usage, nowMs = Date.now()) {
942
1002
  const org = a.organizationName ? ` ${a.organizationName}` : "";
943
1003
  return {
944
1004
  label: `${i + 1} ${mark} ${pad2(a.email, emailW)}${org} ${when}`,
945
- action: { type: "switch", name: a.name },
946
- ...usage && { detail: formatUsageLines(usage[a.name] ?? null, nowMs) }
1005
+ action: { type: "switch", name: a.name }
947
1006
  };
948
1007
  });
949
1008
  }
@@ -996,11 +1055,7 @@ async function runMenu(manager, io) {
996
1055
  } catch {
997
1056
  }
998
1057
  const accounts = await manager.listAccounts();
999
- const usage = io.usage ? await io.usage(accounts) : void 0;
1000
- const choice = await io.pick(
1001
- `claudep \xB7 ${accounts.length} account${accounts.length === 1 ? "" : "s"}`,
1002
- buildMenuChoices(accounts, usage)
1003
- );
1058
+ const choice = await io.pick(`claudep \xB7 ${accounts.length} account${accounts.length === 1 ? "" : "s"}`, buildMenuChoices(accounts));
1004
1059
  if (!choice) return;
1005
1060
  await dispatchMenu(manager, choice.action, io);
1006
1061
  }
@@ -1025,15 +1080,12 @@ function interactiveSelect(title, choices) {
1025
1080
  ${colors.dim(` ${DIVIDER}`)}
1026
1081
  `);
1027
1082
  if (!choices.length) stdout.write(colors.dim(" (no saved accounts yet)\n"));
1028
- const rowLines = choices.reduce((n, c) => n + 1 + (c.detail?.length ?? 0), 0);
1029
1083
  const render = (first) => {
1030
- if (!first) stdout.write(`\x1B[${rowLines + chrome}A`);
1084
+ if (!first) stdout.write(`\x1B[${choices.length + chrome}A`);
1031
1085
  for (let i = 0; i < choices.length; i++) {
1032
1086
  const active = i === selected;
1033
1087
  const line = active ? `${colors.cyan("\u276F")} ${choices[i].label}` : ` ${choices[i].label}`;
1034
1088
  stdout.write(`\x1B[2K\r${line}
1035
- `);
1036
- for (const d of choices[i].detail ?? []) stdout.write(`\x1B[2K\r${colors.dim(` ${d}`)}
1037
1089
  `);
1038
1090
  }
1039
1091
  stdout.write(`\x1B[2K\r${colors.dim(` ${DIVIDER}`)}
@@ -1094,7 +1146,6 @@ async function runInteractiveMenu(manager, out) {
1094
1146
  return a;
1095
1147
  },
1096
1148
  doctor: async () => (await runDoctor(buildDoctorDeps())).report,
1097
- usage: (accounts) => loadUsage(manager, accounts, Date.now()),
1098
1149
  out
1099
1150
  };
1100
1151
  await runMenu(manager, io);
@@ -1372,7 +1423,7 @@ function buildProgram(deps = {}) {
1372
1423
  }
1373
1424
  const cur = mgr.current();
1374
1425
  const external = !cur.saved && cur.label ? cur.label.email : void 0;
1375
- const usage = await loadUsage(mgr, views, now(), deps.fetchUsage ?? fetchUsage);
1426
+ const usage = await loadUsage(mgr, views, now(), { fetchUsage: deps.fetchUsage, refresh: deps.refreshCredential });
1376
1427
  out(formatList(views, now(), colors, { external, usage }));
1377
1428
  });
1378
1429
  program2.command("switch <target>").alias("use").description("Make an account the active login (by name or number)").action(async (target) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-switcher-account",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Switch between multiple Claude Code accounts from your terminal.",
5
5
  "keywords": [
6
6
  "claude",