@efoo/ccprofile 0.3.0 → 0.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.
@@ -4,7 +4,32 @@ import { parseArgs } from "node:util";
4
4
  import { assertDarwin } from "../lib/keychain.js";
5
5
  import { chromeUserAgent, chromeUserDataDir, parseProfiles, readSessionCookies, safeStorageKey, } from "../lib/chrome.js";
6
6
  import { fetchAccountUsage, NOT_SIGNED_IN, } from "../lib/claudeai.js";
7
- import { bold, dim, fail, red, table, warn, yellow } from "../lib/format.js";
7
+ import { loadConfig } from "../lib/config.js";
8
+ import { bold, cyan, dim, fail, red, table, warn, yellow } from "../lib/format.js";
9
+ /**
10
+ * Maps a claude.ai account email to the ccprofile name registered for it, so a
11
+ * signed-in Chrome account that matches a stored profile is labelled with that
12
+ * profile's name. Matching is case-insensitive.
13
+ */
14
+ function profileNamesByEmail() {
15
+ const byEmail = new Map();
16
+ try {
17
+ for (const [name, entry] of Object.entries(loadConfig().profiles)) {
18
+ if (entry.email)
19
+ byEmail.set(entry.email.toLowerCase(), name);
20
+ }
21
+ }
22
+ catch {
23
+ // A malformed/unsupported config.json shouldn't sink the usage table;
24
+ // profile labelling is auxiliary, so degrade to unlabelled accounts.
25
+ }
26
+ return byEmail;
27
+ }
28
+ function matchedProfileName(email, byEmail) {
29
+ if (email === null)
30
+ return null;
31
+ return byEmail.get(email.toLowerCase()) ?? null;
32
+ }
8
33
  export async function usageCommand(argv) {
9
34
  const { values } = parseArgs({
10
35
  args: argv,
@@ -32,11 +57,12 @@ export async function usageCommand(argv) {
32
57
  : startSpinner(`querying claude.ai for ${profiles.length} Chrome profile(s)…`);
33
58
  const results = await Promise.all(profiles.map((profile) => loadUsage(profile, userDataDir, key, userAgent)));
34
59
  spinner.stop();
60
+ const byEmail = profileNamesByEmail();
35
61
  if (values.json) {
36
- console.log(JSON.stringify(results.map(toJson), null, 2));
62
+ console.log(JSON.stringify(results.map((r) => toJson(r, byEmail)), null, 2));
37
63
  return hasRealFailure(results) ? 1 : 0;
38
64
  }
39
- return render(results);
65
+ return render(results, byEmail);
40
66
  }
41
67
  /**
42
68
  * A profile that was never signed in is expected and not a failure; anything
@@ -57,19 +83,12 @@ async function loadUsage(profile, userDataDir, key, userAgent) {
57
83
  return { profile, usage: { ok: false, status: 0, detail } };
58
84
  }
59
85
  }
60
- function render(results) {
61
- const header = ["ACCOUNT", "CHROME", "5-HOUR", "WEEK · ALL", "FABLE · WEEK"].map(bold);
62
- const rows = [header];
86
+ function render(results, byEmail) {
87
+ const signedIn = [];
63
88
  const problems = [];
64
89
  for (const { profile, usage } of results) {
65
90
  if (usage.ok) {
66
- rows.push([
67
- usage.email ?? dim("(unknown)"),
68
- dim(profile.name),
69
- windowCell(usage.report.session),
70
- windowCell(usage.report.weeklyAll),
71
- windowCell(usage.report.fable),
72
- ]);
91
+ signedIn.push({ profile, usage });
73
92
  }
74
93
  else if (usage.detail !== NOT_SIGNED_IN) {
75
94
  // A missing session is expected for stray Chrome profiles; only real
@@ -77,6 +96,19 @@ function render(results) {
77
96
  problems.push(warn(`${profile.name}: ${usage.detail}`));
78
97
  }
79
98
  }
99
+ const header = ["PROFILE", "ACCOUNT", "CHROME", "5-HOUR", "WEEK · ALL", "FABLE · WEEK"].map(bold);
100
+ const rows = [header];
101
+ for (const { profile, usage } of sortByWeeklyReset(signedIn)) {
102
+ const name = matchedProfileName(usage.email, byEmail);
103
+ rows.push([
104
+ name === null ? dim("-") : cyan(name),
105
+ usage.email ?? dim("(unknown)"),
106
+ dim(profile.name),
107
+ windowCell(usage.report.session),
108
+ windowCell(usage.report.weeklyAll),
109
+ windowCell(usage.report.fable),
110
+ ]);
111
+ }
80
112
  if (rows.length === 1 && problems.length === 0) {
81
113
  console.log(dim("No claude.ai sessions found in Chrome. Sign in at https://claude.ai and retry."));
82
114
  return 0;
@@ -87,6 +119,15 @@ function render(results) {
87
119
  console.log(problem);
88
120
  return problems.length > 0 ? 1 : 0;
89
121
  }
122
+ /**
123
+ * Orders accounts by how soon their weekly (all-models) limit resets — the
124
+ * nearest reset first, the furthest last. Accounts with no weekly window sort
125
+ * to the end so a resolved figure never sits below a blank one.
126
+ */
127
+ function sortByWeeklyReset(results) {
128
+ const resetKey = (r) => r.usage.report.weeklyAll?.resetsAt?.getTime() ?? Number.MAX_SAFE_INTEGER;
129
+ return [...results].sort((a, b) => resetKey(a) - resetKey(b));
130
+ }
90
131
  function windowCell(window) {
91
132
  if (window === null)
92
133
  return dim("-");
@@ -111,12 +152,14 @@ function formatReset(date) {
111
152
  const md = `${date.getMonth() + 1}/${date.getDate()}`.padStart(5, " ");
112
153
  return `${md} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
113
154
  }
114
- function toJson(result) {
155
+ function toJson(result, byEmail) {
115
156
  const { profile, usage } = result;
157
+ const email = usage.ok ? usage.email : null;
116
158
  return {
159
+ profile: matchedProfileName(email, byEmail),
117
160
  chromeProfile: profile.name,
118
161
  chromeDir: profile.dir,
119
- email: usage.ok ? usage.email : null,
162
+ email,
120
163
  error: usage.ok ? null : usage.detail,
121
164
  usage: usage.ok ? serializeReport(usage.report) : null,
122
165
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efoo/ccprofile",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Per-directory Claude Code account routing via ANTHROPIC_AUTH_TOKEN, direnv, and macOS Keychain",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.4",