claude-switcher-account 2.2.1 → 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 +16 -0
  2. package/dist/bin.js +336 -86
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,6 +86,20 @@ or `NO_COLOR` is set.
86
86
 
87
87
  Global flags: `--json` (on `list`), `--version`, `--help`.
88
88
 
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
+
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 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 `--`.
102
+
89
103
  ### Interactive menu
90
104
 
91
105
  Running `claudep` with no arguments (in a terminal) opens a menu of your saved
@@ -137,6 +151,8 @@ Configured in `~/.claude-profiles/config.json`:
137
151
  - **Linux/Windows**: canonical login file `~/.claude/.credentials.json`; saved
138
152
  accounts in `~/.claude-profiles/creds.json` (mode `0600`).
139
153
  - Account metadata: `~/.claude-profiles/accounts.json`.
154
+ - `CLAUDE_CONFIG_DIR` is not supported: only the default `~/.claude` login is
155
+ managed, and `claudep doctor` flags the variable when it is set.
140
156
 
141
157
  On macOS the first read of Claude's Keychain item may raise an "allow access"
142
158
  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 { stdout } = await execFileP("security", args);
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.exec(["add-generic-password", "-U", "-s", CLAUDE_SERVICE, "-a", this.osUsername, "-w", blob]);
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.exec(["add-generic-password", "-U", "-s", STORE_SERVICE, "-a", name, "-w", blob]);
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
- mkdirSync(dirname(path), { recursive: true });
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
- const parsed = JSON.parse(readFileSync(this.credsPath, "utf8"));
93
- return parsed && typeof parsed === "object" ? parsed : {};
146
+ parsed = JSON.parse(readFileSync(this.credsPath, "utf8"));
94
147
  } catch {
95
- return {};
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
- return this.readMap()[name] ?? "";
170
+ try {
171
+ return this.readMap()[name] ?? "";
172
+ } catch {
173
+ return "";
174
+ }
110
175
  }
111
176
  async writeSaved(name, blob) {
112
- const map = this.readMap();
113
- map[name] = blob;
114
- this.atomicWrite(this.credsPath, JSON.stringify(map, null, 2), 384);
177
+ await this.updateMap((map) => {
178
+ map[name] = blob;
179
+ return true;
180
+ });
115
181
  }
116
182
  async deleteSaved(name) {
117
- const map = this.readMap();
118
- if (!(name in map)) return;
119
- delete map[name];
120
- this.atomicWrite(this.credsPath, JSON.stringify(map, null, 2), 384);
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 mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync2, statSync, rmSync } from "fs";
138
- import { dirname as dirname2 } from "path";
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 { lastActive: parsed?.lastActive, accounts: Array.isArray(parsed?.accounts) ? parsed.accounts : [] };
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
- await this.acquireLock();
184
- try {
227
+ return withLock(this.lockPath, () => {
185
228
  const data = this.read();
186
229
  mutator(data);
187
- mkdirSync2(dirname2(this.path), { recursive: true });
230
+ mkdirSync3(dirname3(this.path), { recursive: true });
188
231
  const tmp = `${this.path}.tmp-${process.pid}`;
189
- writeFileSync2(tmp, JSON.stringify({ lastActive: data.lastActive, accounts: data.accounts }, null, 2));
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
- } finally {
193
- this.releaseLock();
194
- }
238
+ });
195
239
  }
196
240
  };
197
241
 
198
242
  // src/profileFile.ts
199
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3 } from "fs";
200
- import { dirname as dirname3 } from "path";
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
- mkdirSync3(dirname3(this.path), { recursive: true });
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) await this.store.writeSaved(newName, 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,23 @@ 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
+ }
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
+ }
406
489
  current() {
407
490
  const label = this.profile.peekLabel();
408
491
  const accounts = this.meta.read().accounts;
@@ -492,16 +575,30 @@ var AccountManager = class {
492
575
  const accounts = this.meta.read().accounts;
493
576
  const known = this.matchByIdentity(accounts, this.profile.currentIdentity()) ?? accounts.find((a) => a.fingerprint === fpNow);
494
577
  if (known) {
578
+ const nowMs = this.clock();
579
+ let confirmed = false;
495
580
  await this.meta.update((d) => {
496
581
  d.lastActive = known.name;
582
+ if (known.fingerprint === fpNow) {
583
+ d.pendingRefresh = void 0;
584
+ return;
585
+ }
586
+ const p = d.pendingRefresh;
587
+ if (p?.name !== known.name || p.fingerprint !== fpNow) {
588
+ d.pendingRefresh = { name: known.name, fingerprint: fpNow, since: nowMs };
589
+ } else if (nowMs - p.since >= REFRESH_CONFIRM_MS) {
590
+ d.pendingRefresh = void 0;
591
+ confirmed = true;
592
+ }
497
593
  });
498
- if (known.fingerprint !== fpNow) await this.refreshStoredCredential(known.name, blob, fpNow);
594
+ if (confirmed) await this.refreshStoredCredential(known.name, blob, fpNow);
499
595
  return nothing;
500
596
  }
501
597
  const saved = await this.autoSaveIfNewLogin();
502
598
  if (saved) {
503
599
  await this.meta.update((d) => {
504
600
  d.lastActive = saved.name;
601
+ d.pendingRefresh = void 0;
505
602
  });
506
603
  }
507
604
  return { autoSaved: saved, removed: null, switchedTo: null };
@@ -522,6 +619,119 @@ function createManager(env) {
522
619
  return new AccountManager({ store, meta, profile, config });
523
620
  }
524
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
+
658
+ // src/usage.ts
659
+ var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
660
+ var TIMEOUT_MS2 = 3e3;
661
+ function accessTokenOf(blob, nowMs) {
662
+ try {
663
+ const o = JSON.parse(blob)?.claudeAiOauth;
664
+ if (typeof o?.accessToken !== "string") return null;
665
+ if (typeof o.expiresAt === "number" && o.expiresAt <= nowMs) return null;
666
+ return o.accessToken;
667
+ } catch {
668
+ return null;
669
+ }
670
+ }
671
+ function parseWindow(w) {
672
+ const o = w;
673
+ if (typeof o?.utilization !== "number") return null;
674
+ const t = typeof o.resets_at === "string" ? Date.parse(o.resets_at) : NaN;
675
+ return { pct: o.utilization, resetsAt: Number.isNaN(t) ? null : t };
676
+ }
677
+ function parseUsage(json) {
678
+ if (!json || typeof json !== "object") return null;
679
+ const o = json;
680
+ return { fiveHour: parseWindow(o.five_hour), weekly: parseWindow(o.seven_day) };
681
+ }
682
+ async function fetchUsage(token, fetchFn = fetch) {
683
+ try {
684
+ const res = await fetchFn(USAGE_URL, {
685
+ headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
686
+ signal: AbortSignal.timeout(TIMEOUT_MS2)
687
+ });
688
+ if (!res.ok) return null;
689
+ return parseUsage(await res.json());
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
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;
710
+ const entries = await Promise.all(
711
+ views.map(async (v) => {
712
+ const token = await liveToken(mgr, v, nowMs, refresh);
713
+ return [v.name, token ? await fetchFn(token) : null];
714
+ })
715
+ );
716
+ return Object.fromEntries(entries);
717
+ }
718
+ function formatDuration(ms) {
719
+ const totalMin = Math.floor(Math.max(0, ms) / 6e4);
720
+ const d = Math.floor(totalMin / 1440);
721
+ const h = Math.floor(totalMin % 1440 / 60);
722
+ const m = totalMin % 60;
723
+ const parts = [d && `${d}d`, h && `${h}hr`, m && `${m}m`].filter(Boolean);
724
+ return parts.length ? parts.join(" ") : "<1m";
725
+ }
726
+ function formatUsageLines(u, nowMs) {
727
+ const pct = (w) => w ? `${w.pct.toFixed(1)}%` : "--";
728
+ const reset = (w) => w?.resetsAt != null ? formatDuration(w.resetsAt - nowMs) : "--";
729
+ return [
730
+ `5h: ${pct(u?.fiveHour)} Reset: ${reset(u?.fiveHour)}`,
731
+ `Weekly: ${pct(u?.weekly)} Weekly Reset: ${reset(u?.weekly)}`
732
+ ];
733
+ }
734
+
525
735
  // src/format.ts
526
736
  var SYMBOLS = { active: "\u25CF", ok: "\u2713", err: "\u2717", warn: "!", off: "\u25CB" };
527
737
  function formatRelative(iso, nowMs) {
@@ -562,13 +772,16 @@ function formatList(views, nowMs, colors, opts = {}) {
562
772
  const idxW = String(ordered.length).length;
563
773
  const emailW = Math.max(...ordered.map((v) => v.email.length));
564
774
  const orgW = Math.min(ORG_MAX, Math.max(...ordered.map((v) => (v.organizationName ?? "\u2014").length)));
565
- const rows = ordered.map((v, i) => {
775
+ const usageIndent = " ".repeat(2 + idxW + 4);
776
+ const rows = ordered.flatMap((v, i) => {
566
777
  const idx = colors.dim(pad(String(i + 1), idxW));
567
778
  const mark = v.active ? colors.green(SYMBOLS.active) : " ";
568
779
  const email = v.active ? colors.green(pad(v.email, emailW)) : pad(v.email, emailW);
569
780
  const org = colors.dim(pad(truncate(v.organizationName ?? "\u2014", ORG_MAX), orgW));
570
781
  const when = v.active ? colors.green("active") : colors.dim(formatRelative(v.savedAt, nowMs));
571
- return ` ${idx} ${mark} ${email} ${org} ${when}`;
782
+ const row = ` ${idx} ${mark} ${email} ${org} ${when}`;
783
+ if (!opts.usage) return [row];
784
+ return [row, ...formatUsageLines(opts.usage[v.name] ?? null, nowMs).map((l) => colors.dim(usageIndent + l))];
572
785
  });
573
786
  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
787
  if (opts.external) {
@@ -608,7 +821,15 @@ async function runDoctor(deps, colors = makeColors(false)) {
608
821
  label: "credential backend",
609
822
  detail: deps.store.isAvailable() ? "available" : "unavailable",
610
823
  hint: "This OS/backend is not supported yet."
611
- }
824
+ },
825
+ ...deps.claudeConfigDir ? [
826
+ {
827
+ ok: false,
828
+ label: "CLAUDE_CONFIG_DIR",
829
+ detail: `${deps.claudeConfigDir} (not supported)`,
830
+ hint: "claudep only manages the default ~/.claude login; unset CLAUDE_CONFIG_DIR to use it."
831
+ }
832
+ ] : []
612
833
  ]
613
834
  });
614
835
  const blob = await deps.store.readCanonical().catch(() => "");
@@ -671,14 +892,15 @@ function buildDoctorDeps(env) {
671
892
  profile: new ProfileFile({ path: join3(home, ".claude.json") }),
672
893
  meta: new MetadataStore({ path: join3(home, ".claude-profiles", "accounts.json") }),
673
894
  config: new ConfigStore({ path: join3(home, ".claude-profiles", "config.json") }).load(),
674
- platform
895
+ platform,
896
+ claudeConfigDir: process.env.CLAUDE_CONFIG_DIR || void 0
675
897
  };
676
898
  }
677
899
 
678
900
  // package.json
679
901
  var package_default = {
680
902
  name: "claude-switcher-account",
681
- version: "2.2.1",
903
+ version: "2.4.0",
682
904
  description: "Switch between multiple Claude Code accounts from your terminal.",
683
905
  keywords: [
684
906
  "claude",
@@ -794,7 +1016,8 @@ async function dispatchMenu(manager, action, io) {
794
1016
  case "switch":
795
1017
  try {
796
1018
  await manager.switchTo(action.name);
797
- io.out(`${SYMBOLS.ok} Now using ${action.name}`);
1019
+ io.out(`${SYMBOLS.ok} Now using ${action.name}
1020
+ restart running claude sessions to apply`);
798
1021
  } catch (e) {
799
1022
  io.out(`${SYMBOLS.err} ${e.message}`);
800
1023
  }
@@ -922,12 +1145,6 @@ async function runInteractiveMenu(manager, out) {
922
1145
  rl.close();
923
1146
  return a;
924
1147
  },
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
1148
  doctor: async () => (await runDoctor(buildDoctorDeps())).report,
932
1149
  out
933
1150
  };
@@ -958,9 +1175,23 @@ function startDaemon(manager, opts = {}) {
958
1175
  const interval = opts.intervalMs ?? 1e3;
959
1176
  const log = opts.log ?? ((s) => console.log(s));
960
1177
  const subscribe = opts.watch ?? defaultWatch;
1178
+ let running = false;
1179
+ let again = false;
1180
+ const run = async () => {
1181
+ if (running) {
1182
+ again = true;
1183
+ return;
1184
+ }
1185
+ running = true;
1186
+ do {
1187
+ again = false;
1188
+ await reconcileAndLog(manager, log);
1189
+ } while (again);
1190
+ running = false;
1191
+ };
961
1192
  log(`[claudep] watching ${path} every ${interval}ms \u2014 instant capture on login/switch/logout`);
962
- void reconcileAndLog(manager, log);
963
- return subscribe(path, interval, () => void reconcileAndLog(manager, log));
1193
+ void run();
1194
+ return subscribe(path, interval, () => void run());
964
1195
  }
965
1196
  function defaultWatch(path, intervalMs, onChange) {
966
1197
  const listener = () => onChange();
@@ -971,9 +1202,9 @@ function defaultWatch(path, intervalMs, onChange) {
971
1202
  // src/autostart.ts
972
1203
  import { execFile as execFile2 } from "child_process";
973
1204
  import { promisify as promisify2 } from "util";
974
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, rmSync as rmSync2, existsSync as existsSync3 } from "fs";
1205
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync as rmSync2, existsSync as existsSync3 } from "fs";
975
1206
  import { homedir as homedir4 } from "os";
976
- import { dirname as dirname4, join as join5 } from "path";
1207
+ import { dirname as dirname5, join as join5 } from "path";
977
1208
  var execFileP2 = promisify2(execFile2);
978
1209
  var LABEL = "com.claude-switcher-account.daemon";
979
1210
  var WIN_TASK = "claude-switcher-account";
@@ -984,6 +1215,8 @@ var LEGACY_UNIT = "claude-login-switcher.service";
984
1215
  var defaultExec2 = async (file, args) => {
985
1216
  await execFileP2(file, args);
986
1217
  };
1218
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1219
+ var systemdArg = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, "$$$$")}"`;
987
1220
  function launchdPlist(p) {
988
1221
  return `<?xml version="1.0" encoding="UTF-8"?>
989
1222
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -992,15 +1225,15 @@ function launchdPlist(p) {
992
1225
  <key>Label</key><string>${LABEL}</string>
993
1226
  <key>ProgramArguments</key>
994
1227
  <array>
995
- <string>${p.nodePath}</string>
996
- <string>${p.binPath}</string>
1228
+ <string>${xml(p.nodePath)}</string>
1229
+ <string>${xml(p.binPath)}</string>
997
1230
  <string>daemon</string>
998
1231
  <string>run</string>
999
1232
  </array>
1000
1233
  <key>RunAtLoad</key><true/>
1001
1234
  <key>KeepAlive</key><true/>
1002
- <key>StandardOutPath</key><string>${p.logPath}</string>
1003
- <key>StandardErrorPath</key><string>${p.logPath}</string>
1235
+ <key>StandardOutPath</key><string>${xml(p.logPath)}</string>
1236
+ <key>StandardErrorPath</key><string>${xml(p.logPath)}</string>
1004
1237
  </dict>
1005
1238
  </plist>
1006
1239
  `;
@@ -1010,7 +1243,7 @@ function systemdUnit(p) {
1010
1243
  Description=claude-switcher-account instant login watcher
1011
1244
 
1012
1245
  [Service]
1013
- ExecStart=${p.nodePath} ${p.binPath} daemon run
1246
+ ExecStart=${systemdArg(p.nodePath)} ${systemdArg(p.binPath)} daemon run
1014
1247
  Restart=always
1015
1248
  StandardOutput=append:${p.logPath}
1016
1249
  StandardError=append:${p.logPath}
@@ -1040,7 +1273,7 @@ function autostartInstalled(platform, home = homedir4()) {
1040
1273
  return false;
1041
1274
  }
1042
1275
  function writeFile(path, contents) {
1043
- mkdirSync4(dirname4(path), { recursive: true });
1276
+ mkdirSync5(dirname5(path), { recursive: true });
1044
1277
  writeFileSync4(path, contents);
1045
1278
  }
1046
1279
  async function removeLegacyAutostart(platform, home = homedir4(), exec = defaultExec2) {
@@ -1077,7 +1310,14 @@ async function installAutostart(platform, paths, home = homedir4(), exec = defau
1077
1310
  return `systemd user service installed at ${unit}`;
1078
1311
  }
1079
1312
  if (platform === "win32") {
1080
- await exec("schtasks", windowsCreateArgs(paths));
1313
+ try {
1314
+ await exec("schtasks", windowsCreateArgs(paths));
1315
+ } catch (e) {
1316
+ throw new Error(
1317
+ `${e.message.trim()}
1318
+ Creating a logon task can require an Administrator terminal \u2014 rerun \`claudep daemon install\` from one.`
1319
+ );
1320
+ }
1081
1321
  return `Task Scheduler task '${WIN_TASK}' created (runs at logon)`;
1082
1322
  }
1083
1323
  throw new Error(`Autostart not supported on ${platform}.`);
@@ -1110,6 +1350,11 @@ async function defaultConfirm(question) {
1110
1350
  rl.close();
1111
1351
  return answer === "y" || answer === "yes";
1112
1352
  }
1353
+ function handleFatal(err, errOut) {
1354
+ if (err instanceof CommanderError) return err.exitCode;
1355
+ errOut(` ${SYMBOLS.err} ${err instanceof Error ? err.message : String(err)}`);
1356
+ return 1;
1357
+ }
1113
1358
  function buildProgram(deps = {}) {
1114
1359
  const out = deps.out ?? ((s) => console.log(s));
1115
1360
  const now = deps.now ?? (() => Date.now());
@@ -1178,7 +1423,8 @@ function buildProgram(deps = {}) {
1178
1423
  }
1179
1424
  const cur = mgr.current();
1180
1425
  const external = !cur.saved && cur.label ? cur.label.email : void 0;
1181
- out(formatList(views, now(), colors, { external }));
1426
+ const usage = await loadUsage(mgr, views, now(), { fetchUsage: deps.fetchUsage, refresh: deps.refreshCredential });
1427
+ out(formatList(views, now(), colors, { external, usage }));
1182
1428
  });
1183
1429
  program2.command("switch <target>").alias("use").description("Make an account the active login (by name or number)").action(async (target) => {
1184
1430
  const mgr = manager();
@@ -1194,6 +1440,11 @@ function buildProgram(deps = {}) {
1194
1440
  process.exitCode = 1;
1195
1441
  return;
1196
1442
  }
1443
+ if (!(await mgr.listAccounts()).some((a) => a.name === name)) {
1444
+ out(err(`Saved account '${name}' not found.`) + "\n" + note(listHint));
1445
+ process.exitCode = 1;
1446
+ return;
1447
+ }
1197
1448
  if (!opts.yes) {
1198
1449
  const yes = await confirm(`Remove ${name}? This deletes its stored credential.`);
1199
1450
  if (!yes) {
@@ -1264,6 +1515,5 @@ var program = buildProgram();
1264
1515
  try {
1265
1516
  await program.parseAsync(process.argv);
1266
1517
  } catch (err) {
1267
- const code = err.exitCode ?? 1;
1268
- process.exit(code);
1518
+ process.exit(handleFatal(err, (s) => console.error(s)));
1269
1519
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-switcher-account",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
4
4
  "description": "Switch between multiple Claude Code accounts from your terminal.",
5
5
  "keywords": [
6
6
  "claude",