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.
- package/README.md +7 -5
- package/dist/bin.js +70 -19
- 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`
|
|
90
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
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[${
|
|
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
|
|
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) => {
|