tokenmaxxing 1.169.0 → 1.171.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
@@ -40,7 +40,7 @@ claude # use claude as always; each session gets its own accoun
40
40
  | `tokenmaxxing init --codex` | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
41
41
  | `tokenmaxxing init --pi` | verify and pin the real `pi` and install the `pi` supervisor, which runs pi sessions on pooled Claude and ChatGPT accounts that have a pi login |
42
42
  | `tokenmaxxing init --grok` | pool the first grok login, status-only: no supervisor, no switching |
43
- | `tokenmaxxing init --muse` | pool the first Muse Code login, status-only: no supervisor, no switching, no usage read |
43
+ | `tokenmaxxing init --muse` | pool the first Muse Code login, status-only: no supervisor, no switching |
44
44
  | `tokenmaxxing add [--codex \| --grok \| --muse]` | register an additional account in that pool (isolated login, harvested once into its own store) |
45
45
  | `tokenmaxxing auth [--codex \| --grok \| --muse] [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 |
46
46
  | `tokenmaxxing auth --pi [--codex] [sel \| --all]` | log a pooled Claude account (or a codex account with `--codex`) into pi, isolated, and keep that login as the account's pi credential after an identity check; `--all` walks every account without a pi login |
@@ -126,7 +126,7 @@ pi keeps its own credential and refreshes it itself, so each pooled account gets
126
126
  - **Keychain (macOS).** The first keychain access for a new store happens inside the interactive `init`, `add`, or `auth` run, where a prompt can be answered, not inside a headless hook.
127
127
  - **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
128
128
  - **The status-only grok pool does not switch.** grok `status` shows the weekly subscription credit window, and no grok session moves.
129
- - **The status-only Muse Code pool does not switch and shows no usage.** Muse Code exposes no read-only quota endpoint, so every Muse Code account stays unmeasured.
129
+ - **The status-only Muse Code pool does not switch, and shows no usage before the first model request of a window.** Its usage read is the key mint, which reports no usage until a model request opens the 5-hour window.
130
130
 
131
131
  ## How it's built
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.169.0",
3
+ "version": "1.171.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/lib/codex.ts CHANGED
@@ -41,10 +41,16 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
41
41
  if (usage.planType != null) account.tier = usage.planType;
42
42
  }
43
43
 
44
- type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean };
44
+ type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean; rejectedToken?: string };
45
45
  type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | CodexReadFailure;
46
46
 
47
- async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
47
+ const REJECTED_REFRESH_BACKOFF_MS = 60 * 60 * 1000;
48
+
49
+ function afterRejection(rejected: CodexReadFailure, refreshFailure: CodexReadFailure): CodexReadFailure {
50
+ return { ...refreshFailure, reason: `${rejected.reason}; refresh skipped: ${refreshFailure.reason}` };
51
+ }
52
+
53
+ async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string, rejectedToken?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
48
54
  let auth: CodexAuthJson | null;
49
55
  try {
50
56
  auth = readCodexStoreAuth(account.id);
@@ -52,7 +58,11 @@ async function freshCodexAuth(account: Account, now: number, refresh: boolean, h
52
58
  return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
53
59
  }
54
60
  if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
55
- if (!isCodexAccessExpiring({ auth, now })) return { ok: true, auth };
61
+ const expiring = isCodexAccessExpiring({ auth, now });
62
+ if (auth.tokens.access_token !== rejectedToken && !expiring) return { ok: true, auth };
63
+ if (!expiring && auth.last_refresh != null && now - Date.parse(auth.last_refresh) < REJECTED_REFRESH_BACKOFF_MS) {
64
+ return { ok: false, reason: "the store was refreshed within the last hour", deadGrant: false };
65
+ }
56
66
  if (!refresh) return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false, expiring: true };
57
67
  if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
58
68
  return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
@@ -68,18 +78,26 @@ async function freshCodexAuth(account: Account, now: number, refresh: boolean, h
68
78
  return { ok: true, auth };
69
79
  }
70
80
 
71
- async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
72
- const fresh = await freshCodexAuth(account, now, refresh, holder);
81
+ async function readCodexUsageOnce(account: Account, now: number, refresh: boolean, holder?: string, rejectedToken?: string): Promise<CodexReadOutcome> {
82
+ const fresh = await freshCodexAuth(account, now, refresh, holder, rejectedToken);
73
83
  if (!fresh.ok) return fresh;
74
84
  try {
75
85
  const at = Date.now();
76
86
  return { ok: true, usage: await fetchCodexUsage({ auth: fresh.auth, at }), at };
77
87
  } catch (e) {
78
- if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
88
+ if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false, rejectedToken: e.status === 401 ? fresh.auth.tokens.access_token : undefined };
79
89
  throw e;
80
90
  }
81
91
  }
82
92
 
93
+ async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
94
+ const outcome = await readCodexUsageOnce(account, now, refresh, holder);
95
+ if (outcome.ok || outcome.rejectedToken === undefined || !refresh) return outcome;
96
+ const { rejectedToken } = outcome;
97
+ const fresh = await withLock(codexPool.lockFile, () => freshCodexAuth(account, now, true, holder, rejectedToken));
98
+ return fresh.ok ? readCodexUsageOnce(account, now, false) : afterRejection(outcome, fresh);
99
+ }
100
+
83
101
  export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean; holder?: string }): Promise<Observation | null> {
84
102
  if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
85
103
  const outcome = await readCodexUsage(account, now, opts.refresh, opts.holder);
@@ -112,17 +130,21 @@ async function samplePool(accounts: Account[], now: number): Promise<Map<string,
112
130
  reports.set(account.id, { ok: false, reason: outcome.reason });
113
131
  }
114
132
  };
115
- const expiring: Account[] = [];
133
+ const expiring: { account: Account; rejected?: CodexReadFailure }[] = [];
116
134
  await Promise.all(
117
135
  accounts.map(async (account) => {
118
136
  const outcome = await readCodexUsage(account, now, false);
119
- if (!outcome.ok && outcome.expiring === true) expiring.push(account);
137
+ if (!outcome.ok && (outcome.expiring === true || outcome.rejectedToken !== undefined)) expiring.push({ account, rejected: outcome.rejectedToken === undefined ? undefined : outcome });
120
138
  else record(account, outcome);
121
139
  }),
122
140
  );
123
141
  if (expiring.length > 0) {
124
- const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async (account) => ({ account, fresh: await freshCodexAuth(account, now, true) }))));
125
- await Promise.all(rotated.map(async ({ account, fresh }) => record(account, fresh.ok ? await readCodexUsage(account, now, false) : fresh)));
142
+ const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async ({ account, rejected }) => ({ account, rejected, fresh: await freshCodexAuth(account, now, true, undefined, rejected?.rejectedToken) }))));
143
+ await Promise.all(
144
+ rotated.map(async ({ account, rejected, fresh }) =>
145
+ record(account, fresh.ok ? await readCodexUsage(account, now, false) : rejected ? afterRejection(rejected, fresh) : fresh),
146
+ ),
147
+ );
126
148
  }
127
149
  return reports;
128
150
  }
@@ -11,9 +11,11 @@ export const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
11
11
  const USAGE_URL = env("TOKENMAXXING_CODEX_USAGE_URL", CODEX_USAGE_URL);
12
12
 
13
13
  export class CodexUsageReadError extends Error {
14
- constructor(detail: string) {
14
+ readonly status: number | null;
15
+ constructor(detail: string, status: number | null = null) {
15
16
  super(`codex usage read failed: ${detail}`);
16
17
  this.name = "CodexUsageReadError";
18
+ this.status = status;
17
19
  }
18
20
  }
19
21
 
@@ -73,7 +75,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
73
75
  }
74
76
  const text = await res.text();
75
77
  if (!res.ok) {
76
- throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}`);
78
+ throw new CodexUsageReadError(`HTTP ${res.status}: ${safeErrorDetail({ text })}`, res.status);
77
79
  }
78
80
  const parsed = WireUsageSchema.safeParse(JsonTextSchema.safeParse(text).data);
79
81
  if (!parsed.success) {
package/src/lib/muse.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { writeFileAtomic } from "./atomic.ts";
3
- import { museAuthJsonFor, musePaths, musePool, museStoreDirFor } from "./paths.ts";
3
+ import { http, safeErrorDetail } from "./http.ts";
4
+ import { errorMessage } from "./log.ts";
5
+ import { env, museAuthJsonFor, musePaths, musePool, museStoreDirFor } from "./paths.ts";
4
6
  import type { SampleReport } from "./provider.ts";
5
7
  import { readJsonFile, type Harvest } from "./state.ts";
6
8
  import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
7
- import { ErrnoSchema, type Account } from "./types.ts";
9
+ import { EpochSecondsSchema, ErrnoSchema, JsonTextSchema, type Account } from "./types.ts";
8
10
  import { c } from "../cli/render.ts";
9
11
 
10
12
  const MuseProviderSchema = z.looseObject({
11
13
  access_token: z.string().optional(),
12
- api_key: z.string().optional(),
13
14
  user_email: z.string().nullish(),
14
15
  });
15
16
 
@@ -48,13 +49,75 @@ function readAuth(path: string): AuthEntry[] {
48
49
  writeFileAtomic(museAuthJsonFor(id), JSON.stringify({ ...file, providers: { [key]: value } }, null, 2), 0o600);
49
50
  },
50
51
  };
51
- out.push({ id, usable: (parsed.data.access_token ?? "") !== "" || (parsed.data.api_key ?? "") !== "", harvest });
52
+ out.push({ id, usable: (parsed.data.access_token ?? "") !== "", harvest });
52
53
  }
53
54
  return out;
54
55
  }
55
56
 
56
- async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
57
- return new Map(accounts.map((a) => [a.id, { ok: false, reason: "Muse Code exposes no read-only quota endpoint" }]));
57
+ const SubscriptionWindowSchema = z.looseObject({ used_percent: z.number(), resets_at: EpochSecondsSchema });
58
+
59
+ const MintSchema = z.looseObject({
60
+ subs_usage: z
61
+ .looseObject({
62
+ window: SubscriptionWindowSchema.extend({ window_duration_mins: z.number() }),
63
+ weekly: SubscriptionWindowSchema,
64
+ })
65
+ .nullable(),
66
+ });
67
+
68
+ const WEEK_S = 7 * 86400;
69
+
70
+ function usageUrl(): string {
71
+ return env("TOKENMAXXING_MUSE_USAGE_URL", "https://api.meta.ai/muse-code/key");
72
+ }
73
+
74
+ function storedAccessToken(id: string): string | null {
75
+ const file = readFile(museAuthJsonFor(id));
76
+ if (file == null) return null;
77
+ for (const value of Object.values(file.providers)) {
78
+ const token = MuseProviderSchema.safeParse(value).data?.access_token ?? "";
79
+ if (token !== "") return token;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ async function readMuseUsage(account: Account, now: number): Promise<SampleReport> {
85
+ let token: string | null;
86
+ try {
87
+ token = storedAccessToken(account.id);
88
+ } catch (e) {
89
+ return { ok: false, reason: `this account's store is unreadable (${errorMessage(e)}) - run \`tokenmaxxing auth --muse\`` };
90
+ }
91
+ if (token == null) return { ok: false, reason: "no access_token in this account's store - run `tokenmaxxing auth --muse`" };
92
+ let res: Response;
93
+ let text: string;
94
+ try {
95
+ res = await http.post(usageUrl(), { headers: { Authorization: `Bearer ${token}` }, json: {} });
96
+ text = await res.text();
97
+ } catch (e) {
98
+ return { ok: false, reason: `Muse Code usage read failed: endpoint unreachable: ${errorMessage(e)}` };
99
+ }
100
+ if (res.status === 401 || res.status === 403) {
101
+ account.needsReauth = true;
102
+ return { ok: false, reason: `Muse Code rejected the stored login (HTTP ${res.status}) - run \`tokenmaxxing auth --muse\`` };
103
+ }
104
+ if (!res.ok) return { ok: false, reason: `Muse Code usage read failed: HTTP ${res.status}: ${safeErrorDetail({ text })}` };
105
+ const parsed = MintSchema.safeParse(JsonTextSchema.safeParse(text).data);
106
+ if (!parsed.success) return { ok: false, reason: "Muse Code usage read failed: endpoint returned an unexpected body shape (withheld)" };
107
+ if (parsed.data.subs_usage == null) return { ok: false, reason: "Muse Code reports no usage until a model request opens the 5-hour window" };
108
+ const { window, weekly } = parsed.data.subs_usage;
109
+ account.windows = [
110
+ { name: null, usedPercentage: window.used_percent, resetsAt: window.resets_at, windowSeconds: window.window_duration_mins * 60, sampledAt: now },
111
+ { name: null, usedPercentage: weekly.used_percent, resetsAt: weekly.resets_at, windowSeconds: WEEK_S, sampledAt: now },
112
+ ];
113
+ account.lastUsageAt = now;
114
+ return { ok: true, source: "probe" };
115
+ }
116
+
117
+ async function samplePool(accounts: Account[], now: number): Promise<Map<string, SampleReport>> {
118
+ const reports = new Map<string, SampleReport>();
119
+ await Promise.all(accounts.map(async (account) => reports.set(account.id, await readMuseUsage(account, now))));
120
+ return reports;
58
121
  }
59
122
 
60
123
  export const muse = statusOnlyProvider({