tokenmaxxing 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ claude # use claude as always; each session gets its own accoun
66
66
  | `tokenmaxxing init --codex` | same for codex: import login, install codex supervisor + Stop hook |
67
67
  | `tokenmaxxing add` | register an additional account (isolated login, harvested once into its own store) |
68
68
  | `tokenmaxxing add --codex` | register an additional codex account (isolated login) |
69
- | `tokenmaxxing auth [--codex] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every needs-reauth account one by one; `--codex` does the same for the codex pool |
69
+ | `tokenmaxxing auth [--codex] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every account that is flagged or has no usable credential in its store, one by one; `--codex` does the same for the codex pool |
70
70
  | `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start); the claude pool has no manual switch, each session is placed at launch |
71
71
  | `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
72
72
  | `tokenmaxxing config` | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.40.0",
3
+ "version": "1.41.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/auth.ts CHANGED
@@ -18,14 +18,13 @@ const AuthPlanSchema = z.discriminatedUnion("kind", [
18
18
  ]);
19
19
  export type AuthPlan = z.infer<typeof AuthPlanSchema>;
20
20
 
21
- export function planAuth(input: { p: Provider; accounts: Account[]; argv: string[] }): AuthPlan {
21
+ export function planAuth(input: { p: Provider; accounts: Account[]; argv: string[]; needsAuth: Set<string> }): AuthPlan {
22
22
  const all = input.argv.includes("--all");
23
23
  const rest = input.argv.filter((a) => a !== "--all");
24
24
  if ((all && rest.length > 0) || rest.length > 1) return { kind: "usage" };
25
25
  if (input.accounts.length === 0) return { kind: "error", message: `no accounts in the pool - run \`tokenmaxxing init${input.p.flag}\` first` };
26
26
  if (all) {
27
- const flagged = input.accounts.filter((a) => a.needsReauth === true);
28
- return { kind: "targets", ids: flagged.map((a) => a.id) };
27
+ return { kind: "targets", ids: input.accounts.filter((a) => input.needsAuth.has(a.id)).map((a) => a.id) };
29
28
  }
30
29
  const selector = rest[0];
31
30
  if (selector !== undefined) {
@@ -36,18 +35,19 @@ export function planAuth(input: { p: Provider; accounts: Account[]; argv: string
36
35
  return { kind: "pick" };
37
36
  }
38
37
 
39
- export function pickerOrder(accounts: Account[]): Account[] {
40
- const [flagged, healthy] = partition(accounts, (a) => a.needsReauth === true);
38
+ export function pickerOrder(accounts: Account[], needsAuth: Set<string>): Account[] {
39
+ const [flagged, healthy] = partition(accounts, (a) => needsAuth.has(a.id));
41
40
  return [...flagged, ...healthy];
42
41
  }
43
42
 
44
- function askWhichAccount(idx: AccountsIndex): Account | null {
45
- const ordered = pickerOrder(idx.accounts);
43
+ function askWhichAccount(idx: AccountsIndex, needsAuth: Set<string>): Account | null {
44
+ const ordered = pickerOrder(idx.accounts, needsAuth);
46
45
  console.log("which account do you want to reauthenticate?");
47
46
  for (const [i, a] of ordered.entries()) {
48
47
  const flags: string[] = [];
49
48
  if (a.id === idx.activeId) flags.push(c.green("active"));
50
49
  if (a.needsReauth) flags.push(c.red("needs-reauth"));
50
+ else if (needsAuth.has(a.id)) flags.push(c.red("no-credential"));
51
51
  const labelNote = a.email != null && a.label !== a.email ? ` (${a.label})` : "";
52
52
  const tag = flags.length ? ` ${flags.join(" ")}` : "";
53
53
  console.log(` ${i + 1}. ${c.bold(a.email ?? a.label)}${labelNote}${tag}`);
@@ -107,7 +107,11 @@ async function reauthOne(p: Provider, target: Account): Promise<boolean> {
107
107
 
108
108
  export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
109
109
  const idx = loadAccounts(p.pool);
110
- const plan = planAuth({ p, accounts: idx.accounts, argv });
110
+ const needsAuth = new Set<string>();
111
+ for (const a of idx.accounts) {
112
+ if (a.needsReauth === true || !(await p.storeUsable(a))) needsAuth.add(a.id);
113
+ }
114
+ const plan = planAuth({ p, accounts: idx.accounts, argv, needsAuth });
111
115
  if (plan.kind === "usage") {
112
116
  console.error(AUTH_USAGE);
113
117
  return 2;
@@ -119,7 +123,7 @@ export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
119
123
 
120
124
  const targets: Account[] = [];
121
125
  if (plan.kind === "pick") {
122
- const picked = askWhichAccount(idx);
126
+ const picked = askWhichAccount(idx, needsAuth);
123
127
  if (!picked) return 1;
124
128
  targets.push(picked);
125
129
  } else {
@@ -128,7 +132,7 @@ export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
128
132
  if (account) targets.push(account);
129
133
  }
130
134
  if (targets.length === 0) {
131
- console.log(`${c.green("✓")} no account needs reauth`);
135
+ console.log(`${c.green("✓")} every account has a usable credential`);
132
136
  return 0;
133
137
  }
134
138
  }
package/src/lib/claude.ts CHANGED
@@ -108,6 +108,15 @@ async function prepareMove(target: Account): Promise<void> {
108
108
  log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
109
109
  }
110
110
 
111
+ async function storeUsable(a: Account): Promise<boolean> {
112
+ try {
113
+ const creds = await readStore(a.id);
114
+ return creds != null && !isDeadCredential(creds);
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
111
120
  export function pickSeat(now: number): Account | null {
112
121
  const cfg = loadConfig();
113
122
  const idx = loadAccounts(claudePool);
@@ -277,6 +286,7 @@ export const claude: Provider = {
277
286
  swap: prepareMove,
278
287
  classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" : e instanceof StoreUnusableError ? "skip" : "fatal"),
279
288
  removeCredentials,
289
+ storeUsable,
280
290
  login,
281
291
  importLive,
282
292
  preflight: () => pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() }),
package/src/lib/codex.ts CHANGED
@@ -334,6 +334,13 @@ export const codex: Provider = {
334
334
  swap,
335
335
  classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : "fatal"),
336
336
  removeCredentials: async (a) => deleteParkedCodexAuth({ credFile: codexCredItemFor(a.id) }),
337
+ storeUsable: async (a) => {
338
+ try {
339
+ return readParkedCodexAuth({ credFile: codexCredItemFor(a.id) }) != null;
340
+ } catch {
341
+ return false;
342
+ }
343
+ },
337
344
  login,
338
345
  importLive,
339
346
  preflight,
@@ -23,6 +23,7 @@ export type Provider = {
23
23
  swap(target: Account): Promise<void>;
24
24
  classifySwapError(e: unknown): SwapFailure;
25
25
  removeCredentials(account: Account): Promise<void>;
26
+ storeUsable(account: Account): Promise<boolean>;
26
27
  login(): Promise<Harvest | null>;
27
28
  importLive(): Promise<Harvest | null>;
28
29
  preflight(): void;
package/src/main.ts CHANGED
@@ -42,7 +42,7 @@ function printHelp(): void {
42
42
  ${c.cyan("tokenmaxxing init --codex")} same for codex: import login, install codex supervisor + Stop hook (trust it via /hooks)
43
43
  ${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
44
44
  ${c.cyan("tokenmaxxing add --codex")} register an additional codex account (isolated login)
45
- ${c.cyan("tokenmaxxing auth")} [--codex] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every needs-reauth account, one by one)
45
+ ${c.cyan("tokenmaxxing auth")} [--codex] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
46
46
  ${c.cyan("tokenmaxxing switch --codex")} [sel] switch the codex pool (takes effect on next codex start)
47
47
  ${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
48
48
  ${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)