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.
Files changed (3) hide show
  1. package/README.md +14 -0
  2. package/dist/bin.js +289 -90
  3. 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 { 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,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 (known.fingerprint !== fpNow) await this.refreshStoredCredential(known.name, blob, fpNow);
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 rows = ordered.map((v, i) => {
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
- return ` ${idx} ${mark} ${email} ${org} ${when}`;
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.2.1",
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 choice = await io.pick(`claudep \xB7 ${accounts.length} account${accounts.length === 1 ? "" : "s"}`, buildMenuChoices(accounts));
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[${choices.length + chrome}A`);
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 reconcileAndLog(manager, log);
963
- return subscribe(path, interval, () => void reconcileAndLog(manager, log));
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 mkdirSync4, rmSync as rmSync2, existsSync as existsSync3 } from "fs";
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 dirname4, join as join5 } from "path";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- mkdirSync4(dirname4(path), { recursive: true });
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
- await exec("schtasks", windowsCreateArgs(paths));
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
- out(formatList(views, now(), colors, { external }));
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
- const code = err.exitCode ?? 1;
1268
- process.exit(code);
1467
+ process.exit(handleFatal(err, (s) => console.error(s)));
1269
1468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-switcher-account",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Switch between multiple Claude Code accounts from your terminal.",
5
5
  "keywords": [
6
6
  "claude",