claude-switcher-account 2.2.0 → 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 +17 -5
  2. package/dist/bin.js +289 -98
  3. package/package.json +1 -9
package/README.md CHANGED
@@ -30,13 +30,11 @@ npm i -g claude-switcher-account
30
30
 
31
31
  (The npm package is `claude-switcher-account`; the command it installs is `claudep`.)
32
32
 
33
- From source or git (the `prepare` script builds automatically):
33
+ From source (the `prepare` script builds automatically):
34
34
 
35
35
  ```bash
36
- npm i -g git+https://github.com/khainguyenhm-itr/claude-switcher-account.git
37
- # or, from a local clone:
38
- git clone https://github.com/khainguyenhm-itr/claude-switcher-account.git
39
- cd claude-switcher-account && npm i -g .
36
+ # from a local clone of the sources:
37
+ npm i -g .
40
38
  ```
41
39
 
42
40
  Verify with `claudep doctor`. On macOS the first run may raise a Keychain
@@ -88,6 +86,18 @@ or `NO_COLOR` is set.
88
86
 
89
87
  Global flags: `--json` (on `list`), `--version`, `--help`.
90
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
+
91
101
  ### Interactive menu
92
102
 
93
103
  Running `claudep` with no arguments (in a terminal) opens a menu of your saved
@@ -139,6 +149,8 @@ Configured in `~/.claude-profiles/config.json`:
139
149
  - **Linux/Windows**: canonical login file `~/.claude/.credentials.json`; saved
140
150
  accounts in `~/.claude-profiles/creds.json` (mode `0600`).
141
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.
142
154
 
143
155
  On macOS the first read of Claude's Keychain item may raise an "allow access"
144
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.0",
843
+ version: "2.3.0",
682
844
  description: "Switch between multiple Claude Code accounts from your terminal.",
683
845
  keywords: [
684
846
  "claude",
@@ -696,14 +858,6 @@ var package_default = {
696
858
  "cli",
697
859
  "developer-tools"
698
860
  ],
699
- homepage: "https://github.com/khainguyenhm-itr/claude-switcher-account#readme",
700
- repository: {
701
- type: "git",
702
- url: "git+https://github.com/khainguyenhm-itr/claude-switcher-account.git"
703
- },
704
- bugs: {
705
- url: "https://github.com/khainguyenhm-itr/claude-switcher-account/issues"
706
- },
707
861
  license: "MIT",
708
862
  author: "khainguyenhm-itr",
709
863
  type: "module",
@@ -778,7 +932,7 @@ import { createInterface } from "readline/promises";
778
932
  function pad2(s, width) {
779
933
  return s.length >= width ? s : s + " ".repeat(width - s.length);
780
934
  }
781
- function buildMenuChoices(accounts) {
935
+ function buildMenuChoices(accounts, usage, nowMs = Date.now()) {
782
936
  const ordered = orderAccounts(accounts);
783
937
  if (!ordered.length) return [];
784
938
  const emailW = Math.max(...ordered.map((a) => a.email.length));
@@ -788,7 +942,8 @@ function buildMenuChoices(accounts) {
788
942
  const org = a.organizationName ? ` ${a.organizationName}` : "";
789
943
  return {
790
944
  label: `${i + 1} ${mark} ${pad2(a.email, emailW)}${org} ${when}`,
791
- action: { type: "switch", name: a.name }
945
+ action: { type: "switch", name: a.name },
946
+ ...usage && { detail: formatUsageLines(usage[a.name] ?? null, nowMs) }
792
947
  };
793
948
  });
794
949
  }
@@ -802,7 +957,8 @@ async function dispatchMenu(manager, action, io) {
802
957
  case "switch":
803
958
  try {
804
959
  await manager.switchTo(action.name);
805
- io.out(`${SYMBOLS.ok} Now using ${action.name}`);
960
+ io.out(`${SYMBOLS.ok} Now using ${action.name}
961
+ restart running claude sessions to apply`);
806
962
  } catch (e) {
807
963
  io.out(`${SYMBOLS.err} ${e.message}`);
808
964
  }
@@ -840,7 +996,11 @@ async function runMenu(manager, io) {
840
996
  } catch {
841
997
  }
842
998
  const accounts = await manager.listAccounts();
843
- 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
+ );
844
1004
  if (!choice) return;
845
1005
  await dispatchMenu(manager, choice.action, io);
846
1006
  }
@@ -865,12 +1025,15 @@ function interactiveSelect(title, choices) {
865
1025
  ${colors.dim(` ${DIVIDER}`)}
866
1026
  `);
867
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);
868
1029
  const render = (first) => {
869
- if (!first) stdout.write(`\x1B[${choices.length + chrome}A`);
1030
+ if (!first) stdout.write(`\x1B[${rowLines + chrome}A`);
870
1031
  for (let i = 0; i < choices.length; i++) {
871
1032
  const active = i === selected;
872
1033
  const line = active ? `${colors.cyan("\u276F")} ${choices[i].label}` : ` ${choices[i].label}`;
873
1034
  stdout.write(`\x1B[2K\r${line}
1035
+ `);
1036
+ for (const d of choices[i].detail ?? []) stdout.write(`\x1B[2K\r${colors.dim(` ${d}`)}
874
1037
  `);
875
1038
  }
876
1039
  stdout.write(`\x1B[2K\r${colors.dim(` ${DIVIDER}`)}
@@ -930,13 +1093,8 @@ async function runInteractiveMenu(manager, out) {
930
1093
  rl.close();
931
1094
  return a;
932
1095
  },
933
- confirm: async (q) => {
934
- const rl = createInterface({ input: process.stdin, output: process.stdout });
935
- const a = (await rl.question(`${q} [y/N] `)).trim().toLowerCase();
936
- rl.close();
937
- return a === "y" || a === "yes";
938
- },
939
1096
  doctor: async () => (await runDoctor(buildDoctorDeps())).report,
1097
+ usage: (accounts) => loadUsage(manager, accounts, Date.now()),
940
1098
  out
941
1099
  };
942
1100
  await runMenu(manager, io);
@@ -966,9 +1124,23 @@ function startDaemon(manager, opts = {}) {
966
1124
  const interval = opts.intervalMs ?? 1e3;
967
1125
  const log = opts.log ?? ((s) => console.log(s));
968
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
+ };
969
1141
  log(`[claudep] watching ${path} every ${interval}ms \u2014 instant capture on login/switch/logout`);
970
- void reconcileAndLog(manager, log);
971
- return subscribe(path, interval, () => void reconcileAndLog(manager, log));
1142
+ void run();
1143
+ return subscribe(path, interval, () => void run());
972
1144
  }
973
1145
  function defaultWatch(path, intervalMs, onChange) {
974
1146
  const listener = () => onChange();
@@ -979,9 +1151,9 @@ function defaultWatch(path, intervalMs, onChange) {
979
1151
  // src/autostart.ts
980
1152
  import { execFile as execFile2 } from "child_process";
981
1153
  import { promisify as promisify2 } from "util";
982
- 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";
983
1155
  import { homedir as homedir4 } from "os";
984
- import { dirname as dirname4, join as join5 } from "path";
1156
+ import { dirname as dirname5, join as join5 } from "path";
985
1157
  var execFileP2 = promisify2(execFile2);
986
1158
  var LABEL = "com.claude-switcher-account.daemon";
987
1159
  var WIN_TASK = "claude-switcher-account";
@@ -992,6 +1164,8 @@ var LEGACY_UNIT = "claude-login-switcher.service";
992
1164
  var defaultExec2 = async (file, args) => {
993
1165
  await execFileP2(file, args);
994
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, "$$$$")}"`;
995
1169
  function launchdPlist(p) {
996
1170
  return `<?xml version="1.0" encoding="UTF-8"?>
997
1171
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -1000,15 +1174,15 @@ function launchdPlist(p) {
1000
1174
  <key>Label</key><string>${LABEL}</string>
1001
1175
  <key>ProgramArguments</key>
1002
1176
  <array>
1003
- <string>${p.nodePath}</string>
1004
- <string>${p.binPath}</string>
1177
+ <string>${xml(p.nodePath)}</string>
1178
+ <string>${xml(p.binPath)}</string>
1005
1179
  <string>daemon</string>
1006
1180
  <string>run</string>
1007
1181
  </array>
1008
1182
  <key>RunAtLoad</key><true/>
1009
1183
  <key>KeepAlive</key><true/>
1010
- <key>StandardOutPath</key><string>${p.logPath}</string>
1011
- <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>
1012
1186
  </dict>
1013
1187
  </plist>
1014
1188
  `;
@@ -1018,7 +1192,7 @@ function systemdUnit(p) {
1018
1192
  Description=claude-switcher-account instant login watcher
1019
1193
 
1020
1194
  [Service]
1021
- ExecStart=${p.nodePath} ${p.binPath} daemon run
1195
+ ExecStart=${systemdArg(p.nodePath)} ${systemdArg(p.binPath)} daemon run
1022
1196
  Restart=always
1023
1197
  StandardOutput=append:${p.logPath}
1024
1198
  StandardError=append:${p.logPath}
@@ -1048,7 +1222,7 @@ function autostartInstalled(platform, home = homedir4()) {
1048
1222
  return false;
1049
1223
  }
1050
1224
  function writeFile(path, contents) {
1051
- mkdirSync4(dirname4(path), { recursive: true });
1225
+ mkdirSync5(dirname5(path), { recursive: true });
1052
1226
  writeFileSync4(path, contents);
1053
1227
  }
1054
1228
  async function removeLegacyAutostart(platform, home = homedir4(), exec = defaultExec2) {
@@ -1085,7 +1259,14 @@ async function installAutostart(platform, paths, home = homedir4(), exec = defau
1085
1259
  return `systemd user service installed at ${unit}`;
1086
1260
  }
1087
1261
  if (platform === "win32") {
1088
- 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
+ }
1089
1270
  return `Task Scheduler task '${WIN_TASK}' created (runs at logon)`;
1090
1271
  }
1091
1272
  throw new Error(`Autostart not supported on ${platform}.`);
@@ -1118,6 +1299,11 @@ async function defaultConfirm(question) {
1118
1299
  rl.close();
1119
1300
  return answer === "y" || answer === "yes";
1120
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
+ }
1121
1307
  function buildProgram(deps = {}) {
1122
1308
  const out = deps.out ?? ((s) => console.log(s));
1123
1309
  const now = deps.now ?? (() => Date.now());
@@ -1186,7 +1372,8 @@ function buildProgram(deps = {}) {
1186
1372
  }
1187
1373
  const cur = mgr.current();
1188
1374
  const external = !cur.saved && cur.label ? cur.label.email : void 0;
1189
- 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 }));
1190
1377
  });
1191
1378
  program2.command("switch <target>").alias("use").description("Make an account the active login (by name or number)").action(async (target) => {
1192
1379
  const mgr = manager();
@@ -1202,6 +1389,11 @@ function buildProgram(deps = {}) {
1202
1389
  process.exitCode = 1;
1203
1390
  return;
1204
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
+ }
1205
1397
  if (!opts.yes) {
1206
1398
  const yes = await confirm(`Remove ${name}? This deletes its stored credential.`);
1207
1399
  if (!yes) {
@@ -1272,6 +1464,5 @@ var program = buildProgram();
1272
1464
  try {
1273
1465
  await program.parseAsync(process.argv);
1274
1466
  } catch (err) {
1275
- const code = err.exitCode ?? 1;
1276
- process.exit(code);
1467
+ process.exit(handleFatal(err, (s) => console.error(s)));
1277
1468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-switcher-account",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Switch between multiple Claude Code accounts from your terminal.",
5
5
  "keywords": [
6
6
  "claude",
@@ -18,14 +18,6 @@
18
18
  "cli",
19
19
  "developer-tools"
20
20
  ],
21
- "homepage": "https://github.com/khainguyenhm-itr/claude-switcher-account#readme",
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/khainguyenhm-itr/claude-switcher-account.git"
25
- },
26
- "bugs": {
27
- "url": "https://github.com/khainguyenhm-itr/claude-switcher-account/issues"
28
- },
29
21
  "license": "MIT",
30
22
  "author": "khainguyenhm-itr",
31
23
  "type": "module",