claude-switcher-account 2.2.1 → 2.3.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 +14 -0
- package/dist/bin.js +289 -90
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,18 @@ 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`:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
5h: 40.0% Reset: 55m
|
|
94
|
+
Weekly: 12.0% Weekly Reset: 5d 9hr 45m
|
|
95
|
+
```
|
|
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.
|
|
100
|
+
|
|
89
101
|
### Interactive menu
|
|
90
102
|
|
|
91
103
|
Running `claudep` with no arguments (in a terminal) opens a menu of your saved
|
|
@@ -137,6 +149,8 @@ Configured in `~/.claude-profiles/config.json`:
|
|
|
137
149
|
- **Linux/Windows**: canonical login file `~/.claude/.credentials.json`; saved
|
|
138
150
|
accounts in `~/.claude-profiles/creds.json` (mode `0600`).
|
|
139
151
|
- Account metadata: `~/.claude-profiles/accounts.json`.
|
|
152
|
+
- `CLAUDE_CONFIG_DIR` is not supported: only the default `~/.claude` login is
|
|
153
|
+
managed, and `claudep doctor` flags the variable when it is set.
|
|
140
154
|
|
|
141
155
|
On macOS the first read of Claude's Keychain item may raise an "allow access"
|
|
142
156
|
dialog — click Allow. Run `claudep doctor` if anything looks off.
|
package/dist/bin.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, CommanderError } from "commander";
|
|
5
5
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6
6
|
import { homedir as homedir5 } from "os";
|
|
7
7
|
import { join as join6 } from "path";
|
|
@@ -19,10 +19,14 @@ import { promisify } from "util";
|
|
|
19
19
|
var execFileP = promisify(execFile);
|
|
20
20
|
var CLAUDE_SERVICE = "Claude Code-credentials";
|
|
21
21
|
var STORE_SERVICE = "claude-profiles-accounts";
|
|
22
|
-
var defaultExec = async (args) => {
|
|
23
|
-
const
|
|
22
|
+
var defaultExec = async (args, input) => {
|
|
23
|
+
const p = execFileP("security", args);
|
|
24
|
+
p.child.stdin?.end(input ?? "");
|
|
25
|
+
const { stdout } = await p;
|
|
24
26
|
return stdout;
|
|
25
27
|
};
|
|
28
|
+
var INTERACTIVE_MAX_LINE = 4e3;
|
|
29
|
+
var quote = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
26
30
|
var MacKeychainStore = class {
|
|
27
31
|
exec;
|
|
28
32
|
osUsername;
|
|
@@ -43,8 +47,18 @@ var MacKeychainStore = class {
|
|
|
43
47
|
return "";
|
|
44
48
|
}
|
|
45
49
|
}
|
|
50
|
+
/** Upsert a secret, keeping it off the argv (visible to `ps`) whenever it fits on one `security -i` line. */
|
|
51
|
+
async addPassword(service, account, blob) {
|
|
52
|
+
const line = `add-generic-password -U -s ${quote(service)} -a ${quote(account)} -w ${quote(blob)}
|
|
53
|
+
`;
|
|
54
|
+
if (!/[\r\n]/.test(blob) && Buffer.byteLength(line) <= INTERACTIVE_MAX_LINE) {
|
|
55
|
+
await this.exec(["-i"], line);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await this.exec(["add-generic-password", "-U", "-s", service, "-a", account, "-w", blob]);
|
|
59
|
+
}
|
|
46
60
|
async writeCanonical(blob) {
|
|
47
|
-
await this.
|
|
61
|
+
await this.addPassword(CLAUDE_SERVICE, this.osUsername, blob);
|
|
48
62
|
}
|
|
49
63
|
async readSaved(name) {
|
|
50
64
|
try {
|
|
@@ -55,7 +69,7 @@ var MacKeychainStore = class {
|
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
async writeSaved(name, blob) {
|
|
58
|
-
await this.
|
|
72
|
+
await this.addPassword(STORE_SERVICE, name, blob);
|
|
59
73
|
}
|
|
60
74
|
async deleteSaved(name) {
|
|
61
75
|
try {
|
|
@@ -66,8 +80,46 @@ var MacKeychainStore = class {
|
|
|
66
80
|
};
|
|
67
81
|
|
|
68
82
|
// src/stores/fileStore.ts
|
|
69
|
-
import { mkdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "fs";
|
|
83
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync, renameSync, existsSync } from "fs";
|
|
84
|
+
import { dirname as dirname2 } from "path";
|
|
85
|
+
|
|
86
|
+
// src/lock.ts
|
|
87
|
+
import { mkdirSync, statSync, rmSync } from "fs";
|
|
70
88
|
import { dirname } from "path";
|
|
89
|
+
var LOCK_STALE_MS = 1e4;
|
|
90
|
+
var LOCK_RETRY_MS = 25;
|
|
91
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
92
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
93
|
+
async function acquireLock(lockPath) {
|
|
94
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
95
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
96
|
+
for (; ; ) {
|
|
97
|
+
try {
|
|
98
|
+
mkdirSync(lockPath);
|
|
99
|
+
return;
|
|
100
|
+
} catch {
|
|
101
|
+
try {
|
|
102
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
103
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
if (Date.now() > deadline) throw new Error(`Timed out acquiring lock ${lockPath}`);
|
|
109
|
+
await sleep(LOCK_RETRY_MS);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function withLock(lockPath, fn) {
|
|
114
|
+
await acquireLock(lockPath);
|
|
115
|
+
try {
|
|
116
|
+
return await fn();
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/stores/fileStore.ts
|
|
71
123
|
var FileStore = class {
|
|
72
124
|
canonicalPath;
|
|
73
125
|
credsPath;
|
|
@@ -81,19 +133,28 @@ var FileStore = class {
|
|
|
81
133
|
return this.platform === "linux" || this.platform === "win32";
|
|
82
134
|
}
|
|
83
135
|
atomicWrite(path, data, mode) {
|
|
84
|
-
|
|
136
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
85
137
|
const tmp = `${path}.tmp-${process.pid}`;
|
|
86
138
|
writeFileSync(tmp, data, mode !== void 0 ? { mode } : void 0);
|
|
87
139
|
renameSync(tmp, path);
|
|
88
140
|
}
|
|
141
|
+
/** Throws if the file exists but can't be parsed, so a write never replaces it with an empty map. */
|
|
89
142
|
readMap() {
|
|
90
143
|
if (!existsSync(this.credsPath)) return {};
|
|
144
|
+
let parsed;
|
|
91
145
|
try {
|
|
92
|
-
|
|
93
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
146
|
+
parsed = JSON.parse(readFileSync(this.credsPath, "utf8"));
|
|
94
147
|
} catch {
|
|
95
|
-
|
|
148
|
+
parsed = null;
|
|
96
149
|
}
|
|
150
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`Saved creds file ${this.credsPath} is unreadable.`);
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
updateMap(mutate) {
|
|
154
|
+
return withLock(`${this.credsPath}.lock`, () => {
|
|
155
|
+
const map = this.readMap();
|
|
156
|
+
if (mutate(map)) this.atomicWrite(this.credsPath, JSON.stringify(map, null, 2), 384);
|
|
157
|
+
});
|
|
97
158
|
}
|
|
98
159
|
async readCanonical() {
|
|
99
160
|
try {
|
|
@@ -106,18 +167,24 @@ var FileStore = class {
|
|
|
106
167
|
this.atomicWrite(this.canonicalPath, blob, 384);
|
|
107
168
|
}
|
|
108
169
|
async readSaved(name) {
|
|
109
|
-
|
|
170
|
+
try {
|
|
171
|
+
return this.readMap()[name] ?? "";
|
|
172
|
+
} catch {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
110
175
|
}
|
|
111
176
|
async writeSaved(name, blob) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
177
|
+
await this.updateMap((map) => {
|
|
178
|
+
map[name] = blob;
|
|
179
|
+
return true;
|
|
180
|
+
});
|
|
115
181
|
}
|
|
116
182
|
async deleteSaved(name) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
183
|
+
await this.updateMap((map) => {
|
|
184
|
+
if (!(name in map)) return false;
|
|
185
|
+
delete map[name];
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
121
188
|
}
|
|
122
189
|
};
|
|
123
190
|
|
|
@@ -134,12 +201,8 @@ function createCredentialStore(opts) {
|
|
|
134
201
|
}
|
|
135
202
|
|
|
136
203
|
// src/metadataStore.ts
|
|
137
|
-
import { mkdirSync as
|
|
138
|
-
import { dirname as
|
|
139
|
-
var LOCK_STALE_MS = 1e4;
|
|
140
|
-
var LOCK_RETRY_MS = 25;
|
|
141
|
-
var LOCK_TIMEOUT_MS = 5e3;
|
|
142
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
204
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync2 } from "fs";
|
|
205
|
+
import { dirname as dirname3 } from "path";
|
|
143
206
|
var MetadataStore = class {
|
|
144
207
|
path;
|
|
145
208
|
lockPath;
|
|
@@ -151,53 +214,34 @@ var MetadataStore = class {
|
|
|
151
214
|
if (!existsSync2(this.path)) return { accounts: [] };
|
|
152
215
|
try {
|
|
153
216
|
const parsed = JSON.parse(readFileSync2(this.path, "utf8"));
|
|
154
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
lastActive: parsed?.lastActive,
|
|
219
|
+
pendingRefresh: parsed?.pendingRefresh,
|
|
220
|
+
accounts: Array.isArray(parsed?.accounts) ? parsed.accounts : []
|
|
221
|
+
};
|
|
155
222
|
} catch {
|
|
156
223
|
return { accounts: [] };
|
|
157
224
|
}
|
|
158
225
|
}
|
|
159
|
-
async acquireLock() {
|
|
160
|
-
mkdirSync2(dirname2(this.lockPath), { recursive: true });
|
|
161
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
162
|
-
for (; ; ) {
|
|
163
|
-
try {
|
|
164
|
-
mkdirSync2(this.lockPath);
|
|
165
|
-
return;
|
|
166
|
-
} catch {
|
|
167
|
-
try {
|
|
168
|
-
if (Date.now() - statSync(this.lockPath).mtimeMs > LOCK_STALE_MS) {
|
|
169
|
-
rmSync(this.lockPath, { recursive: true, force: true });
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
if (Date.now() > deadline) throw new Error(`Timed out acquiring lock ${this.lockPath}`);
|
|
175
|
-
await sleep(LOCK_RETRY_MS);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
releaseLock() {
|
|
180
|
-
rmSync(this.lockPath, { recursive: true, force: true });
|
|
181
|
-
}
|
|
182
226
|
async update(mutator) {
|
|
183
|
-
|
|
184
|
-
try {
|
|
227
|
+
return withLock(this.lockPath, () => {
|
|
185
228
|
const data = this.read();
|
|
186
229
|
mutator(data);
|
|
187
|
-
|
|
230
|
+
mkdirSync3(dirname3(this.path), { recursive: true });
|
|
188
231
|
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
189
|
-
writeFileSync2(
|
|
232
|
+
writeFileSync2(
|
|
233
|
+
tmp,
|
|
234
|
+
JSON.stringify({ lastActive: data.lastActive, pendingRefresh: data.pendingRefresh, accounts: data.accounts }, null, 2)
|
|
235
|
+
);
|
|
190
236
|
renameSync2(tmp, this.path);
|
|
191
237
|
return data;
|
|
192
|
-
}
|
|
193
|
-
this.releaseLock();
|
|
194
|
-
}
|
|
238
|
+
});
|
|
195
239
|
}
|
|
196
240
|
};
|
|
197
241
|
|
|
198
242
|
// src/profileFile.ts
|
|
199
|
-
import { mkdirSync as
|
|
200
|
-
import { dirname as
|
|
243
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "fs";
|
|
244
|
+
import { dirname as dirname4 } from "path";
|
|
201
245
|
var ProfileFile = class {
|
|
202
246
|
path;
|
|
203
247
|
constructor(opts) {
|
|
@@ -241,7 +285,7 @@ var ProfileFile = class {
|
|
|
241
285
|
if (snapshot) d.oauthAccount = snapshot;
|
|
242
286
|
else delete d.oauthAccount;
|
|
243
287
|
try {
|
|
244
|
-
|
|
288
|
+
mkdirSync4(dirname4(this.path), { recursive: true });
|
|
245
289
|
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
246
290
|
writeFileSync3(tmp, JSON.stringify(d, null, 2));
|
|
247
291
|
renameSync3(tmp, this.path);
|
|
@@ -285,6 +329,7 @@ function orderAccounts(views) {
|
|
|
285
329
|
}
|
|
286
330
|
|
|
287
331
|
// src/accountManager.ts
|
|
332
|
+
var REFRESH_CONFIRM_MS = 1e4;
|
|
288
333
|
function toView(a, active) {
|
|
289
334
|
return {
|
|
290
335
|
name: a.name,
|
|
@@ -301,12 +346,14 @@ var AccountManager = class {
|
|
|
301
346
|
profile;
|
|
302
347
|
config;
|
|
303
348
|
now;
|
|
349
|
+
clock;
|
|
304
350
|
constructor(deps) {
|
|
305
351
|
this.store = deps.store;
|
|
306
352
|
this.meta = deps.meta;
|
|
307
353
|
this.profile = deps.profile;
|
|
308
354
|
this.config = deps.config;
|
|
309
355
|
this.now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
356
|
+
this.clock = deps.clock ?? Date.now;
|
|
310
357
|
}
|
|
311
358
|
matchByIdentity(accounts, id) {
|
|
312
359
|
if (id.uuid) {
|
|
@@ -365,7 +412,25 @@ var AccountManager = class {
|
|
|
365
412
|
const ordered = orderAccounts(await this.listAccounts());
|
|
366
413
|
return ordered[n - 1]?.name ?? null;
|
|
367
414
|
}
|
|
415
|
+
/** Before the login is replaced, copy a token the active account rotated since its last save — reconcile
|
|
416
|
+
* may still be waiting out REFRESH_CONFIRM_MS on it. Skipped when the credential is another saved
|
|
417
|
+
* account's, i.e. a login-over whose ~/.claude.json write hasn't landed. */
|
|
418
|
+
async keepActiveCredential() {
|
|
419
|
+
let blob = "";
|
|
420
|
+
try {
|
|
421
|
+
blob = await this.store.readCanonical();
|
|
422
|
+
} catch {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!blob) return;
|
|
426
|
+
const fp = fingerprint(blob);
|
|
427
|
+
const accounts = this.meta.read().accounts;
|
|
428
|
+
if (accounts.some((a) => a.fingerprint === fp)) return;
|
|
429
|
+
const active = this.matchByIdentity(accounts, this.profile.currentIdentity());
|
|
430
|
+
if (active) await this.refreshStoredCredential(active.name, blob, fp);
|
|
431
|
+
}
|
|
368
432
|
async switchTo(name) {
|
|
433
|
+
await this.keepActiveCredential();
|
|
369
434
|
const blob = (await this.store.readSaved(name)).trim();
|
|
370
435
|
if (!blob) throw new Error(`Saved account '${name}' not found.`);
|
|
371
436
|
await this.store.writeCanonical(blob);
|
|
@@ -385,7 +450,8 @@ var AccountManager = class {
|
|
|
385
450
|
if (!entry) throw new Error(`Saved account '${oldName}' not found.`);
|
|
386
451
|
if (data.accounts.some((a) => a.name === newName)) throw new Error(`Account '${newName}' already exists.`);
|
|
387
452
|
const blob = await this.store.readSaved(oldName);
|
|
388
|
-
if (blob)
|
|
453
|
+
if (!blob) throw new Error(`Saved account '${oldName}' has no stored credential.`);
|
|
454
|
+
await this.store.writeSaved(newName, blob);
|
|
389
455
|
await this.store.deleteSaved(oldName);
|
|
390
456
|
await this.meta.update((d) => {
|
|
391
457
|
d.accounts = d.accounts.map((a) => a.name === oldName ? { ...a, name: newName } : a);
|
|
@@ -403,6 +469,14 @@ var AccountManager = class {
|
|
|
403
469
|
}
|
|
404
470
|
return accounts.map((a) => toView(a, byId ? a.name === byId.name : activeFp !== null && a.fingerprint === activeFp));
|
|
405
471
|
}
|
|
472
|
+
/** The account's credential: the live login for the active one (its saved copy may be older), else the saved one. */
|
|
473
|
+
async credentialOf(view) {
|
|
474
|
+
try {
|
|
475
|
+
return view.active ? await this.store.readCanonical() : await this.store.readSaved(view.name);
|
|
476
|
+
} catch {
|
|
477
|
+
return "";
|
|
478
|
+
}
|
|
479
|
+
}
|
|
406
480
|
current() {
|
|
407
481
|
const label = this.profile.peekLabel();
|
|
408
482
|
const accounts = this.meta.read().accounts;
|
|
@@ -492,16 +566,30 @@ var AccountManager = class {
|
|
|
492
566
|
const accounts = this.meta.read().accounts;
|
|
493
567
|
const known = this.matchByIdentity(accounts, this.profile.currentIdentity()) ?? accounts.find((a) => a.fingerprint === fpNow);
|
|
494
568
|
if (known) {
|
|
569
|
+
const nowMs = this.clock();
|
|
570
|
+
let confirmed = false;
|
|
495
571
|
await this.meta.update((d) => {
|
|
496
572
|
d.lastActive = known.name;
|
|
573
|
+
if (known.fingerprint === fpNow) {
|
|
574
|
+
d.pendingRefresh = void 0;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const p = d.pendingRefresh;
|
|
578
|
+
if (p?.name !== known.name || p.fingerprint !== fpNow) {
|
|
579
|
+
d.pendingRefresh = { name: known.name, fingerprint: fpNow, since: nowMs };
|
|
580
|
+
} else if (nowMs - p.since >= REFRESH_CONFIRM_MS) {
|
|
581
|
+
d.pendingRefresh = void 0;
|
|
582
|
+
confirmed = true;
|
|
583
|
+
}
|
|
497
584
|
});
|
|
498
|
-
if (
|
|
585
|
+
if (confirmed) await this.refreshStoredCredential(known.name, blob, fpNow);
|
|
499
586
|
return nothing;
|
|
500
587
|
}
|
|
501
588
|
const saved = await this.autoSaveIfNewLogin();
|
|
502
589
|
if (saved) {
|
|
503
590
|
await this.meta.update((d) => {
|
|
504
591
|
d.lastActive = saved.name;
|
|
592
|
+
d.pendingRefresh = void 0;
|
|
505
593
|
});
|
|
506
594
|
}
|
|
507
595
|
return { autoSaved: saved, removed: null, switchedTo: null };
|
|
@@ -522,6 +610,68 @@ function createManager(env) {
|
|
|
522
610
|
return new AccountManager({ store, meta, profile, config });
|
|
523
611
|
}
|
|
524
612
|
|
|
613
|
+
// src/usage.ts
|
|
614
|
+
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
615
|
+
var TIMEOUT_MS = 3e3;
|
|
616
|
+
function accessTokenOf(blob, nowMs) {
|
|
617
|
+
try {
|
|
618
|
+
const o = JSON.parse(blob)?.claudeAiOauth;
|
|
619
|
+
if (typeof o?.accessToken !== "string") return null;
|
|
620
|
+
if (typeof o.expiresAt === "number" && o.expiresAt <= nowMs) return null;
|
|
621
|
+
return o.accessToken;
|
|
622
|
+
} catch {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function parseWindow(w) {
|
|
627
|
+
const o = w;
|
|
628
|
+
if (typeof o?.utilization !== "number") return null;
|
|
629
|
+
const t = typeof o.resets_at === "string" ? Date.parse(o.resets_at) : NaN;
|
|
630
|
+
return { pct: o.utilization, resetsAt: Number.isNaN(t) ? null : t };
|
|
631
|
+
}
|
|
632
|
+
function parseUsage(json) {
|
|
633
|
+
if (!json || typeof json !== "object") return null;
|
|
634
|
+
const o = json;
|
|
635
|
+
return { fiveHour: parseWindow(o.five_hour), weekly: parseWindow(o.seven_day) };
|
|
636
|
+
}
|
|
637
|
+
async function fetchUsage(token, fetchFn = fetch) {
|
|
638
|
+
try {
|
|
639
|
+
const res = await fetchFn(USAGE_URL, {
|
|
640
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
641
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
642
|
+
});
|
|
643
|
+
if (!res.ok) return null;
|
|
644
|
+
return parseUsage(await res.json());
|
|
645
|
+
} catch {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function loadUsage(mgr, views, nowMs, fetchFn = fetchUsage) {
|
|
650
|
+
const entries = await Promise.all(
|
|
651
|
+
views.map(async (v) => {
|
|
652
|
+
const token = accessTokenOf(await mgr.credentialOf(v), nowMs);
|
|
653
|
+
return [v.name, token ? await fetchFn(token) : null];
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
return Object.fromEntries(entries);
|
|
657
|
+
}
|
|
658
|
+
function formatDuration(ms) {
|
|
659
|
+
const totalMin = Math.floor(Math.max(0, ms) / 6e4);
|
|
660
|
+
const d = Math.floor(totalMin / 1440);
|
|
661
|
+
const h = Math.floor(totalMin % 1440 / 60);
|
|
662
|
+
const m = totalMin % 60;
|
|
663
|
+
const parts = [d && `${d}d`, h && `${h}hr`, m && `${m}m`].filter(Boolean);
|
|
664
|
+
return parts.length ? parts.join(" ") : "<1m";
|
|
665
|
+
}
|
|
666
|
+
function formatUsageLines(u, nowMs) {
|
|
667
|
+
const pct = (w) => w ? `${w.pct.toFixed(1)}%` : "--";
|
|
668
|
+
const reset = (w) => w?.resetsAt != null ? formatDuration(w.resetsAt - nowMs) : "--";
|
|
669
|
+
return [
|
|
670
|
+
`5h: ${pct(u?.fiveHour)} Reset: ${reset(u?.fiveHour)}`,
|
|
671
|
+
`Weekly: ${pct(u?.weekly)} Weekly Reset: ${reset(u?.weekly)}`
|
|
672
|
+
];
|
|
673
|
+
}
|
|
674
|
+
|
|
525
675
|
// src/format.ts
|
|
526
676
|
var SYMBOLS = { active: "\u25CF", ok: "\u2713", err: "\u2717", warn: "!", off: "\u25CB" };
|
|
527
677
|
function formatRelative(iso, nowMs) {
|
|
@@ -562,13 +712,16 @@ function formatList(views, nowMs, colors, opts = {}) {
|
|
|
562
712
|
const idxW = String(ordered.length).length;
|
|
563
713
|
const emailW = Math.max(...ordered.map((v) => v.email.length));
|
|
564
714
|
const orgW = Math.min(ORG_MAX, Math.max(...ordered.map((v) => (v.organizationName ?? "\u2014").length)));
|
|
565
|
-
const
|
|
715
|
+
const usageIndent = " ".repeat(2 + idxW + 4);
|
|
716
|
+
const rows = ordered.flatMap((v, i) => {
|
|
566
717
|
const idx = colors.dim(pad(String(i + 1), idxW));
|
|
567
718
|
const mark = v.active ? colors.green(SYMBOLS.active) : " ";
|
|
568
719
|
const email = v.active ? colors.green(pad(v.email, emailW)) : pad(v.email, emailW);
|
|
569
720
|
const org = colors.dim(pad(truncate(v.organizationName ?? "\u2014", ORG_MAX), orgW));
|
|
570
721
|
const when = v.active ? colors.green("active") : colors.dim(formatRelative(v.savedAt, nowMs));
|
|
571
|
-
|
|
722
|
+
const row = ` ${idx} ${mark} ${email} ${org} ${when}`;
|
|
723
|
+
if (!opts.usage) return [row];
|
|
724
|
+
return [row, ...formatUsageLines(opts.usage[v.name] ?? null, nowMs).map((l) => colors.dim(usageIndent + l))];
|
|
572
725
|
});
|
|
573
726
|
const lines = [header, colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"), ...rows, ""];
|
|
574
727
|
if (opts.external) {
|
|
@@ -608,7 +761,15 @@ async function runDoctor(deps, colors = makeColors(false)) {
|
|
|
608
761
|
label: "credential backend",
|
|
609
762
|
detail: deps.store.isAvailable() ? "available" : "unavailable",
|
|
610
763
|
hint: "This OS/backend is not supported yet."
|
|
611
|
-
}
|
|
764
|
+
},
|
|
765
|
+
...deps.claudeConfigDir ? [
|
|
766
|
+
{
|
|
767
|
+
ok: false,
|
|
768
|
+
label: "CLAUDE_CONFIG_DIR",
|
|
769
|
+
detail: `${deps.claudeConfigDir} (not supported)`,
|
|
770
|
+
hint: "claudep only manages the default ~/.claude login; unset CLAUDE_CONFIG_DIR to use it."
|
|
771
|
+
}
|
|
772
|
+
] : []
|
|
612
773
|
]
|
|
613
774
|
});
|
|
614
775
|
const blob = await deps.store.readCanonical().catch(() => "");
|
|
@@ -671,14 +832,15 @@ function buildDoctorDeps(env) {
|
|
|
671
832
|
profile: new ProfileFile({ path: join3(home, ".claude.json") }),
|
|
672
833
|
meta: new MetadataStore({ path: join3(home, ".claude-profiles", "accounts.json") }),
|
|
673
834
|
config: new ConfigStore({ path: join3(home, ".claude-profiles", "config.json") }).load(),
|
|
674
|
-
platform
|
|
835
|
+
platform,
|
|
836
|
+
claudeConfigDir: process.env.CLAUDE_CONFIG_DIR || void 0
|
|
675
837
|
};
|
|
676
838
|
}
|
|
677
839
|
|
|
678
840
|
// package.json
|
|
679
841
|
var package_default = {
|
|
680
842
|
name: "claude-switcher-account",
|
|
681
|
-
version: "2.
|
|
843
|
+
version: "2.3.0",
|
|
682
844
|
description: "Switch between multiple Claude Code accounts from your terminal.",
|
|
683
845
|
keywords: [
|
|
684
846
|
"claude",
|
|
@@ -770,7 +932,7 @@ import { createInterface } from "readline/promises";
|
|
|
770
932
|
function pad2(s, width) {
|
|
771
933
|
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
772
934
|
}
|
|
773
|
-
function buildMenuChoices(accounts) {
|
|
935
|
+
function buildMenuChoices(accounts, usage, nowMs = Date.now()) {
|
|
774
936
|
const ordered = orderAccounts(accounts);
|
|
775
937
|
if (!ordered.length) return [];
|
|
776
938
|
const emailW = Math.max(...ordered.map((a) => a.email.length));
|
|
@@ -780,7 +942,8 @@ function buildMenuChoices(accounts) {
|
|
|
780
942
|
const org = a.organizationName ? ` ${a.organizationName}` : "";
|
|
781
943
|
return {
|
|
782
944
|
label: `${i + 1} ${mark} ${pad2(a.email, emailW)}${org} ${when}`,
|
|
783
|
-
action: { type: "switch", name: a.name }
|
|
945
|
+
action: { type: "switch", name: a.name },
|
|
946
|
+
...usage && { detail: formatUsageLines(usage[a.name] ?? null, nowMs) }
|
|
784
947
|
};
|
|
785
948
|
});
|
|
786
949
|
}
|
|
@@ -794,7 +957,8 @@ async function dispatchMenu(manager, action, io) {
|
|
|
794
957
|
case "switch":
|
|
795
958
|
try {
|
|
796
959
|
await manager.switchTo(action.name);
|
|
797
|
-
io.out(`${SYMBOLS.ok} Now using ${action.name}
|
|
960
|
+
io.out(`${SYMBOLS.ok} Now using ${action.name}
|
|
961
|
+
restart running claude sessions to apply`);
|
|
798
962
|
} catch (e) {
|
|
799
963
|
io.out(`${SYMBOLS.err} ${e.message}`);
|
|
800
964
|
}
|
|
@@ -832,7 +996,11 @@ async function runMenu(manager, io) {
|
|
|
832
996
|
} catch {
|
|
833
997
|
}
|
|
834
998
|
const accounts = await manager.listAccounts();
|
|
835
|
-
const
|
|
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
|
+
);
|
|
836
1004
|
if (!choice) return;
|
|
837
1005
|
await dispatchMenu(manager, choice.action, io);
|
|
838
1006
|
}
|
|
@@ -857,12 +1025,15 @@ function interactiveSelect(title, choices) {
|
|
|
857
1025
|
${colors.dim(` ${DIVIDER}`)}
|
|
858
1026
|
`);
|
|
859
1027
|
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);
|
|
860
1029
|
const render = (first) => {
|
|
861
|
-
if (!first) stdout.write(`\x1B[${
|
|
1030
|
+
if (!first) stdout.write(`\x1B[${rowLines + chrome}A`);
|
|
862
1031
|
for (let i = 0; i < choices.length; i++) {
|
|
863
1032
|
const active = i === selected;
|
|
864
1033
|
const line = active ? `${colors.cyan("\u276F")} ${choices[i].label}` : ` ${choices[i].label}`;
|
|
865
1034
|
stdout.write(`\x1B[2K\r${line}
|
|
1035
|
+
`);
|
|
1036
|
+
for (const d of choices[i].detail ?? []) stdout.write(`\x1B[2K\r${colors.dim(` ${d}`)}
|
|
866
1037
|
`);
|
|
867
1038
|
}
|
|
868
1039
|
stdout.write(`\x1B[2K\r${colors.dim(` ${DIVIDER}`)}
|
|
@@ -922,13 +1093,8 @@ async function runInteractiveMenu(manager, out) {
|
|
|
922
1093
|
rl.close();
|
|
923
1094
|
return a;
|
|
924
1095
|
},
|
|
925
|
-
confirm: async (q) => {
|
|
926
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
927
|
-
const a = (await rl.question(`${q} [y/N] `)).trim().toLowerCase();
|
|
928
|
-
rl.close();
|
|
929
|
-
return a === "y" || a === "yes";
|
|
930
|
-
},
|
|
931
1096
|
doctor: async () => (await runDoctor(buildDoctorDeps())).report,
|
|
1097
|
+
usage: (accounts) => loadUsage(manager, accounts, Date.now()),
|
|
932
1098
|
out
|
|
933
1099
|
};
|
|
934
1100
|
await runMenu(manager, io);
|
|
@@ -958,9 +1124,23 @@ function startDaemon(manager, opts = {}) {
|
|
|
958
1124
|
const interval = opts.intervalMs ?? 1e3;
|
|
959
1125
|
const log = opts.log ?? ((s) => console.log(s));
|
|
960
1126
|
const subscribe = opts.watch ?? defaultWatch;
|
|
1127
|
+
let running = false;
|
|
1128
|
+
let again = false;
|
|
1129
|
+
const run = async () => {
|
|
1130
|
+
if (running) {
|
|
1131
|
+
again = true;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
running = true;
|
|
1135
|
+
do {
|
|
1136
|
+
again = false;
|
|
1137
|
+
await reconcileAndLog(manager, log);
|
|
1138
|
+
} while (again);
|
|
1139
|
+
running = false;
|
|
1140
|
+
};
|
|
961
1141
|
log(`[claudep] watching ${path} every ${interval}ms \u2014 instant capture on login/switch/logout`);
|
|
962
|
-
void
|
|
963
|
-
return subscribe(path, interval, () => void
|
|
1142
|
+
void run();
|
|
1143
|
+
return subscribe(path, interval, () => void run());
|
|
964
1144
|
}
|
|
965
1145
|
function defaultWatch(path, intervalMs, onChange) {
|
|
966
1146
|
const listener = () => onChange();
|
|
@@ -971,9 +1151,9 @@ function defaultWatch(path, intervalMs, onChange) {
|
|
|
971
1151
|
// src/autostart.ts
|
|
972
1152
|
import { execFile as execFile2 } from "child_process";
|
|
973
1153
|
import { promisify as promisify2 } from "util";
|
|
974
|
-
import { writeFileSync as writeFileSync4, mkdirSync as
|
|
1154
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync as rmSync2, existsSync as existsSync3 } from "fs";
|
|
975
1155
|
import { homedir as homedir4 } from "os";
|
|
976
|
-
import { dirname as
|
|
1156
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
977
1157
|
var execFileP2 = promisify2(execFile2);
|
|
978
1158
|
var LABEL = "com.claude-switcher-account.daemon";
|
|
979
1159
|
var WIN_TASK = "claude-switcher-account";
|
|
@@ -984,6 +1164,8 @@ var LEGACY_UNIT = "claude-login-switcher.service";
|
|
|
984
1164
|
var defaultExec2 = async (file, args) => {
|
|
985
1165
|
await execFileP2(file, args);
|
|
986
1166
|
};
|
|
1167
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1168
|
+
var systemdArg = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, "$$$$")}"`;
|
|
987
1169
|
function launchdPlist(p) {
|
|
988
1170
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
989
1171
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -992,15 +1174,15 @@ function launchdPlist(p) {
|
|
|
992
1174
|
<key>Label</key><string>${LABEL}</string>
|
|
993
1175
|
<key>ProgramArguments</key>
|
|
994
1176
|
<array>
|
|
995
|
-
<string>${p.nodePath}</string>
|
|
996
|
-
<string>${p.binPath}</string>
|
|
1177
|
+
<string>${xml(p.nodePath)}</string>
|
|
1178
|
+
<string>${xml(p.binPath)}</string>
|
|
997
1179
|
<string>daemon</string>
|
|
998
1180
|
<string>run</string>
|
|
999
1181
|
</array>
|
|
1000
1182
|
<key>RunAtLoad</key><true/>
|
|
1001
1183
|
<key>KeepAlive</key><true/>
|
|
1002
|
-
<key>StandardOutPath</key><string>${p.logPath}</string>
|
|
1003
|
-
<key>StandardErrorPath</key><string>${p.logPath}</string>
|
|
1184
|
+
<key>StandardOutPath</key><string>${xml(p.logPath)}</string>
|
|
1185
|
+
<key>StandardErrorPath</key><string>${xml(p.logPath)}</string>
|
|
1004
1186
|
</dict>
|
|
1005
1187
|
</plist>
|
|
1006
1188
|
`;
|
|
@@ -1010,7 +1192,7 @@ function systemdUnit(p) {
|
|
|
1010
1192
|
Description=claude-switcher-account instant login watcher
|
|
1011
1193
|
|
|
1012
1194
|
[Service]
|
|
1013
|
-
ExecStart=${p.nodePath} ${p.binPath} daemon run
|
|
1195
|
+
ExecStart=${systemdArg(p.nodePath)} ${systemdArg(p.binPath)} daemon run
|
|
1014
1196
|
Restart=always
|
|
1015
1197
|
StandardOutput=append:${p.logPath}
|
|
1016
1198
|
StandardError=append:${p.logPath}
|
|
@@ -1040,7 +1222,7 @@ function autostartInstalled(platform, home = homedir4()) {
|
|
|
1040
1222
|
return false;
|
|
1041
1223
|
}
|
|
1042
1224
|
function writeFile(path, contents) {
|
|
1043
|
-
|
|
1225
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1044
1226
|
writeFileSync4(path, contents);
|
|
1045
1227
|
}
|
|
1046
1228
|
async function removeLegacyAutostart(platform, home = homedir4(), exec = defaultExec2) {
|
|
@@ -1077,7 +1259,14 @@ async function installAutostart(platform, paths, home = homedir4(), exec = defau
|
|
|
1077
1259
|
return `systemd user service installed at ${unit}`;
|
|
1078
1260
|
}
|
|
1079
1261
|
if (platform === "win32") {
|
|
1080
|
-
|
|
1262
|
+
try {
|
|
1263
|
+
await exec("schtasks", windowsCreateArgs(paths));
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`${e.message.trim()}
|
|
1267
|
+
Creating a logon task can require an Administrator terminal \u2014 rerun \`claudep daemon install\` from one.`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1081
1270
|
return `Task Scheduler task '${WIN_TASK}' created (runs at logon)`;
|
|
1082
1271
|
}
|
|
1083
1272
|
throw new Error(`Autostart not supported on ${platform}.`);
|
|
@@ -1110,6 +1299,11 @@ async function defaultConfirm(question) {
|
|
|
1110
1299
|
rl.close();
|
|
1111
1300
|
return answer === "y" || answer === "yes";
|
|
1112
1301
|
}
|
|
1302
|
+
function handleFatal(err, errOut) {
|
|
1303
|
+
if (err instanceof CommanderError) return err.exitCode;
|
|
1304
|
+
errOut(` ${SYMBOLS.err} ${err instanceof Error ? err.message : String(err)}`);
|
|
1305
|
+
return 1;
|
|
1306
|
+
}
|
|
1113
1307
|
function buildProgram(deps = {}) {
|
|
1114
1308
|
const out = deps.out ?? ((s) => console.log(s));
|
|
1115
1309
|
const now = deps.now ?? (() => Date.now());
|
|
@@ -1178,7 +1372,8 @@ function buildProgram(deps = {}) {
|
|
|
1178
1372
|
}
|
|
1179
1373
|
const cur = mgr.current();
|
|
1180
1374
|
const external = !cur.saved && cur.label ? cur.label.email : void 0;
|
|
1181
|
-
|
|
1375
|
+
const usage = await loadUsage(mgr, views, now(), deps.fetchUsage ?? fetchUsage);
|
|
1376
|
+
out(formatList(views, now(), colors, { external, usage }));
|
|
1182
1377
|
});
|
|
1183
1378
|
program2.command("switch <target>").alias("use").description("Make an account the active login (by name or number)").action(async (target) => {
|
|
1184
1379
|
const mgr = manager();
|
|
@@ -1194,6 +1389,11 @@ function buildProgram(deps = {}) {
|
|
|
1194
1389
|
process.exitCode = 1;
|
|
1195
1390
|
return;
|
|
1196
1391
|
}
|
|
1392
|
+
if (!(await mgr.listAccounts()).some((a) => a.name === name)) {
|
|
1393
|
+
out(err(`Saved account '${name}' not found.`) + "\n" + note(listHint));
|
|
1394
|
+
process.exitCode = 1;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1197
1397
|
if (!opts.yes) {
|
|
1198
1398
|
const yes = await confirm(`Remove ${name}? This deletes its stored credential.`);
|
|
1199
1399
|
if (!yes) {
|
|
@@ -1264,6 +1464,5 @@ var program = buildProgram();
|
|
|
1264
1464
|
try {
|
|
1265
1465
|
await program.parseAsync(process.argv);
|
|
1266
1466
|
} catch (err) {
|
|
1267
|
-
|
|
1268
|
-
process.exit(code);
|
|
1467
|
+
process.exit(handleFatal(err, (s) => console.error(s)));
|
|
1269
1468
|
}
|