tokenmaxxing 1.34.0 → 1.35.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 (46) hide show
  1. package/DESIGN.md +3 -5
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +23 -40
  5. package/src/cli/auth.ts +38 -52
  6. package/src/cli/check.ts +2 -1
  7. package/src/cli/doctor.ts +13 -13
  8. package/src/cli/init.ts +29 -126
  9. package/src/cli/rename.ts +11 -45
  10. package/src/cli/render.ts +0 -7
  11. package/src/cli/rm.ts +24 -39
  12. package/src/cli/setuptoken.ts +16 -16
  13. package/src/cli/status.ts +95 -238
  14. package/src/cli/switch.ts +54 -46
  15. package/src/entries/codexstophook.ts +22 -16
  16. package/src/entries/codexsupervisor.ts +3 -3
  17. package/src/entries/sessionstart.ts +3 -2
  18. package/src/entries/statusline.ts +15 -13
  19. package/src/entries/stopfailurehook.ts +7 -5
  20. package/src/entries/stophook.ts +3 -2
  21. package/src/lib/claude.ts +320 -0
  22. package/src/lib/codex.ts +353 -0
  23. package/src/lib/codexauth.ts +6 -0
  24. package/src/lib/codexpresence.ts +0 -10
  25. package/src/lib/codexusage.ts +14 -32
  26. package/src/lib/decide.ts +86 -174
  27. package/src/lib/oauth.ts +7 -0
  28. package/src/lib/paths.ts +14 -6
  29. package/src/lib/picker.ts +56 -37
  30. package/src/lib/provider.ts +34 -0
  31. package/src/lib/sample.ts +33 -33
  32. package/src/lib/state.ts +49 -16
  33. package/src/lib/swap.ts +41 -38
  34. package/src/lib/types.ts +17 -51
  35. package/src/lib/usage.ts +17 -4
  36. package/src/main.ts +20 -20
  37. package/src/cli/codexadd.ts +0 -89
  38. package/src/cli/codexinit.ts +0 -120
  39. package/src/cli/codexrm.ts +0 -36
  40. package/src/cli/codexswitch.ts +0 -113
  41. package/src/cli/onboard.ts +0 -92
  42. package/src/lib/codexdecide.ts +0 -157
  43. package/src/lib/codexpick.ts +0 -80
  44. package/src/lib/codexsample.ts +0 -48
  45. package/src/lib/codexstate.ts +0 -22
  46. package/src/lib/codexswap.ts +0 -70
@@ -1,18 +1,17 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { z } from "zod";
4
- import { codexPaths } from "../lib/paths.ts";
4
+ import { codexPaths, codexPool } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
6
  import { writeFileAtomic } from "../lib/atomic.ts";
7
- import { evaluateAndMaybeSwapCodex } from "../lib/codexdecide.ts";
8
- import { isCodexExhausted } from "../lib/codexpick.ts";
7
+ import { codex, codexPickCtx, reconcileSiblings } from "../lib/codex.ts";
8
+ import { liveCodexAccountId } from "../lib/codexauth.ts";
9
+ import { evaluateAndMaybeSwap } from "../lib/decide.ts";
10
+ import { isExhausted } from "../lib/picker.ts";
9
11
  import { livingCodexPresences } from "../lib/codexpresence.ts";
10
- import { liveCodexAccountId } from "../lib/codexsample.ts";
11
- import { loadCodexAccounts } from "../lib/codexstate.ts";
12
- import { loadConfig } from "../lib/state.ts";
13
- import { thresholdBars } from "../lib/picker.ts";
12
+ import { loadAccounts } from "../lib/state.ts";
14
13
  import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
15
- import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, type CodexAccount } from "../lib/types.ts";
14
+ import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema } from "../lib/types.ts";
16
15
  import { log } from "../lib/log.ts";
17
16
 
18
17
  const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
@@ -20,7 +19,7 @@ const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
20
19
  async function promoteReconcile(input: { supervisorId: string; sessionId: string | null }): Promise<boolean> {
21
20
  const markerPath = join(codexPaths.reconcileDir, input.supervisorId);
22
21
  if (!existsSync(markerPath)) return false;
23
- return withLock(codexPaths.lockFile, async () => promoteReconcileLocked(input));
22
+ return withLock(codexPool.lockFile, async () => promoteReconcileLocked(input));
24
23
  }
25
24
 
26
25
  function promoteReconcileLocked(input: { supervisorId: string; sessionId: string | null }): boolean {
@@ -51,12 +50,8 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
51
50
  return false;
52
51
  }
53
52
  const now = Date.now();
54
- const bars = thresholdBars(loadConfig());
55
- const index = loadCodexAccounts();
56
- const unusable = (account: CodexAccount): boolean =>
57
- account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
58
- const liveAccount = index.accounts.find((a) => a.accountId === liveId);
59
- if (!liveAccount || unusable(liveAccount)) {
53
+ const liveAccount = loadAccounts(codexPool).accounts.find((a) => a.id === liveId);
54
+ if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) {
60
55
  rmSync(markerPath, { force: true });
61
56
  log("codexstop.reconcile_blocked_target", {});
62
57
  return false;
@@ -75,6 +70,14 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
75
70
  return true;
76
71
  }
77
72
 
73
+ async function sweepSiblings(now: number): Promise<void> {
74
+ try {
75
+ await withLock(codexPool.lockFile, () => reconcileSiblings(now));
76
+ } catch (e) {
77
+ log("codexstop.resweep_failed", { err: e instanceof Error ? e.message : String(e) });
78
+ }
79
+ }
80
+
78
81
  async function readStdin(): Promise<string> {
79
82
  const chunks: Uint8Array[] = [];
80
83
  for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
@@ -98,8 +101,11 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
98
101
  return;
99
102
  }
100
103
  if (await promoteReconcile({ supervisorId, sessionId })) return;
101
- const decision = await evaluateAndMaybeSwapCodex({});
104
+ const now = Date.now();
105
+ await withLock(codexPool.lockFile, () => reconcileSiblings(now));
106
+ const decision = await evaluateAndMaybeSwap(codex, now);
102
107
  if (decision.swapped && decision.account) {
108
+ await sweepSiblings(now);
103
109
  mkdirSync(codexPaths.respawnDir, { recursive: true });
104
110
  const payload = CodexRespawnMarkerSchema.parse({
105
111
  account: decision.account.label,
@@ -1,12 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
- import { codexPaths, paths } from "../lib/paths.ts";
4
+ import { codexPaths, codexPool, paths } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
6
  import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
7
7
  import { resolveRealCodex } from "../lib/codexbin.ts";
8
8
  import { clearCodexPresence, writeCodexPresence } from "../lib/codexpresence.ts";
9
- import { liveCodexAccountId } from "../lib/codexsample.ts";
9
+ import { liveCodexAccountId } from "../lib/codexauth.ts";
10
10
  import { saveTermios, restoreTermios } from "../lib/tty.ts";
11
11
  import { CodexRespawnMarkerSchema } from "../lib/types.ts";
12
12
  import { log } from "../lib/log.ts";
@@ -95,7 +95,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
95
95
  if (existsSync(marker)) rmSync(marker, { force: true });
96
96
  log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, args: launchArgs.join(" ") });
97
97
 
98
- const child = await withLock(codexPaths.lockFile, async () => {
98
+ const child = await withLock(codexPool.lockFile, async () => {
99
99
  const spawnAccountId = liveCodexAccountId();
100
100
  const spawned = Bun.spawn([real, ...launchArgs], {
101
101
  stdin: "inherit",
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { claude } from "../lib/claude.ts";
2
3
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
3
4
  import { log } from "../lib/log.ts";
4
5
 
@@ -21,9 +22,9 @@ export async function runSessionStart(): Promise<number> {
21
22
  const source = parsed.success ? parsed.data.source : undefined;
22
23
 
23
24
  try {
24
- const decision = await evaluateAndMaybeSwap();
25
+ const decision = await evaluateAndMaybeSwap(claude);
25
26
  if (decision.swapped && decision.account) {
26
- log("sessionstart.swapped", { source, account: decision.account.accountUuid.slice(0, 8) });
27
+ log("sessionstart.swapped", { source, account: decision.account.id.slice(0, 8) });
27
28
  }
28
29
  } catch (e) {
29
30
  log("sessionstart.error", { err: e instanceof Error ? e.message : String(e) });
@@ -1,16 +1,17 @@
1
1
  import { sortBy } from "es-toolkit";
2
2
  import { z } from "zod";
3
3
  import { readOAuthAccount } from "../lib/claudejson.ts";
4
+ import { claudePool } from "../lib/paths.ts";
4
5
  import { loadAccounts, loadConfig, loadLastSwapAt, writeUsage } from "../lib/state.ts";
5
6
  import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
6
- import { earliestReset, weeklyExpiry } from "../lib/picker.ts";
7
+ import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
7
8
  import { worktreeName } from "../lib/worktree.ts";
8
9
  import { makeColors, makeUsagePaint } from "../cli/render.ts";
9
10
  import { fmtResetShort } from "../lib/usage.ts";
10
11
  import {
11
12
  AccountsIndexSchema,
12
13
  StatusLineStdinSchema,
13
- UsageWindowSchema,
14
+ WindowSchema,
14
15
  type Account,
15
16
  type UsageState,
16
17
  type UsageWindow,
@@ -20,7 +21,7 @@ const ADOPTION_GRACE_MS = 45_000;
20
21
 
21
22
  const RenderCtxSchema = z.object({
22
23
  accounts: AccountsIndexSchema,
23
- perModel: z.record(z.string(), UsageWindowSchema),
24
+ perModel: z.record(z.string(), WindowSchema),
24
25
  switchModels: z.array(z.string()),
25
26
  worktree: z.string().nullable(),
26
27
  liveAccount: z.string().nullable(),
@@ -78,11 +79,11 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
78
79
  windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
79
80
  }
80
81
  const seatUuid =
81
- (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.accountUuid === ctx.liveAccount) ? ctx.liveAccount : null) ??
82
- ctx.accounts.activeAccountUuid;
82
+ (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.id === ctx.liveAccount) ? ctx.liveAccount : null) ??
83
+ ctx.accounts.activeId;
83
84
  const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
84
85
  const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
85
- const seat = ctx.accounts.accounts.find((a) => a.accountUuid === seatUuid);
86
+ const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
86
87
  if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
87
88
  const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
88
89
  const active =
@@ -93,20 +94,20 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
93
94
  : "";
94
95
 
95
96
  const parked = sortBy(
96
- ctx.accounts.accounts.filter((a) => a.accountUuid !== seatUuid),
97
+ ctx.accounts.accounts.filter((a) => a.id !== seatUuid),
97
98
  [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
98
99
  );
99
100
  const poolSeg = (a: Account): string => {
100
101
  const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
101
102
  if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
102
- const week = a.lastUsage?.sevenDay;
103
+ const week = weeklyWindow(a);
103
104
  if (week == null) return `${marker} ?`;
104
105
  const weekUsed = used(week);
105
106
  if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
106
107
 
107
108
  const parts: string[] = [];
108
- for (const [name, w] of Object.entries(a.lastUsage?.perModel ?? {})) {
109
- if (used(w) > weekUsed) parts.push(seg(initial(name), w, null));
109
+ for (const w of limitWindows(a)) {
110
+ if (used(w) > weekUsed) parts.push(seg(initial(w.name ?? ""), w, null));
110
111
  }
111
112
  const expiry = weeklyExpiry(a, ctx.now);
112
113
  parts.push(seg("", week, Number.isFinite(expiry) ? expiry : null));
@@ -129,7 +130,7 @@ export async function runStatusline(): Promise<number> {
129
130
  try {
130
131
  account = readOAuthAccount()?.accountUuid ?? null;
131
132
  const windows = obj == null ? null : parseStatusLineStdin(obj);
132
- const lastSwapAt = loadLastSwapAt();
133
+ const lastSwapAt = loadLastSwapAt(claudePool);
133
134
  if (windows && (lastSwapAt == null || now - lastSwapAt >= ADOPTION_GRACE_MS)) {
134
135
  const state: UsageState = { fiveHour: windows.fiveHour, sevenDay: windows.sevenDay, account, ts: now, model: parseStatusLineModel(obj) };
135
136
  writeUsage(state);
@@ -140,13 +141,14 @@ export async function runStatusline(): Promise<number> {
140
141
  let line: string;
141
142
  try {
142
143
  const cfg = loadConfig();
143
- const accounts = loadAccounts();
144
+ const accounts = loadAccounts(claudePool);
144
145
  const stdin = StatusLineStdinSchema.safeParse(obj);
145
146
  const dir = stdin.success ? (stdin.data.workspace?.current_dir ?? stdin.data.workspace?.project_dir ?? null) : null;
146
147
  const colorterm = z.string().optional().parse(process.env.COLORTERM);
148
+ const live = accounts.accounts.find((a) => a.id === account);
147
149
  const ctx: RenderCtx = {
148
150
  accounts,
149
- perModel: accounts.accounts.find((a) => a.accountUuid === account)?.lastUsage?.perModel ?? {},
151
+ perModel: Object.fromEntries((live ? limitWindows(live) : []).map((w) => [w.name ?? "", w])),
150
152
  switchModels: cfg.policy.switchModels,
151
153
  worktree: dir == null ? null : worktreeName(dir),
152
154
  liveAccount: account,
@@ -2,9 +2,11 @@ import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { paths } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
+ import { claude } from "../lib/claude.ts";
5
6
  import { readOAuthAccount } from "../lib/claudejson.ts";
6
7
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
7
8
  import { withLock } from "../lib/lock.ts";
9
+ import { claudePool } from "../lib/paths.ts";
8
10
  import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
9
11
  import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
10
12
  import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
@@ -29,7 +31,7 @@ async function readStdin(): Promise<string> {
29
31
  export async function runStopFailureHook(): Promise<number> {
30
32
  if (process.env.TOKENMAXXING_PROBE) return 0;
31
33
 
32
- const entry = await withLock(paths.lockFile, () => ({ swapClock: loadLastSwapAt(), account: readOAuthAccount()?.accountUuid ?? null }));
34
+ const entry = await withLock(claudePool.lockFile, () => ({ swapClock: loadLastSwapAt(claudePool), account: readOAuthAccount()?.accountUuid ?? null }));
33
35
  const account = entry.account;
34
36
  const now = Date.now();
35
37
  const raw = await readStdin();
@@ -44,7 +46,7 @@ export async function runStopFailureHook(): Promise<number> {
44
46
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
45
47
 
46
48
  try {
47
- const lastSwapAt = loadLastSwapAt();
49
+ const lastSwapAt = loadLastSwapAt(claudePool);
48
50
  if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
49
51
  log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
50
52
  return 0;
@@ -68,7 +70,7 @@ export async function runStopFailureHook(): Promise<number> {
68
70
  });
69
71
  }
70
72
 
71
- const decision = await evaluateAndMaybeSwap(now, canPause && enforced != null, enforced);
73
+ const decision = await evaluateAndMaybeSwap(claude, now, canPause && enforced != null, enforced);
72
74
  if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
73
75
  const marker = join(paths.respawnDir, pinnedSid);
74
76
  const payload = RespawnMarkerSchema.parse({
@@ -79,9 +81,9 @@ export async function runStopFailureHook(): Promise<number> {
79
81
  ...(launchedAt != null ? { launchedAt } : {}),
80
82
  });
81
83
  writeFileAtomic(marker, JSON.stringify(payload));
82
- log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.accountUuid.slice(0, 8), waitUntil: payload.waitUntil });
84
+ log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: payload.waitUntil });
83
85
  } else {
84
- log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
86
+ log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
85
87
  }
86
88
  } catch (e) {
87
89
  log("stopfailure.error", { err: e instanceof Error ? e.message : String(e) });
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { paths } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
+ import { claude } from "../lib/claude.ts";
5
6
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
6
7
  import { RespawnMarkerSchema } from "../lib/types.ts";
7
8
  import { log } from "../lib/log.ts";
@@ -24,9 +25,9 @@ export async function runStopHook(): Promise<number> {
24
25
 
25
26
  try {
26
27
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
27
- const decision = await evaluateAndMaybeSwap(Date.now(), canPause);
28
+ const decision = await evaluateAndMaybeSwap(claude, Date.now(), canPause);
28
29
  if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
29
- log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
30
+ log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
30
31
  if (decision.waitUntil !== undefined && canPause && pinnedSid) {
31
32
  const marker = join(paths.respawnDir, pinnedSid);
32
33
  const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
@@ -0,0 +1,320 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { readItem, writeItem, deleteItem, liveTarget, parkedTarget, isolatedTarget, claudeAiOauthOnly, mergeIntoLive } from "./credstore.ts";
5
+ import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
6
+ import { withClaudeRefreshLock } from "./claudelock.ts";
7
+ import { isApiKeyMode, readOAuthAccount } from "./claudejson.ts";
8
+ import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
9
+ import { withLock } from "./lock.ts";
10
+ import { log } from "./log.ts";
11
+ import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isAccessTokenExpiring, isDeadCredential, refreshCredential, InvalidGrantError } from "./oauth.ts";
12
+ import { claudePool, credItemFor, paths } from "./paths.ts";
13
+ import type { Observation, Provider, SampleReport } from "./provider.ts";
14
+ import { ensureLiveTokenFresh, probeActiveUsage, probeParkedUsage } from "./sample.ts";
15
+ import { loadAccounts, loadUsage, loadUsageSnapshot, pinBinOverride, saveAccounts, writeUsage, type Harvest } from "./state.ts";
16
+ import { isSkippableSwapError, performSwap } from "./swap.ts";
17
+ import { loadSetupTokens, saveSetupTokens } from "./setuptokens.ts";
18
+ import { saveTermios, restoreTermios } from "./tty.ts";
19
+ import { CRED_ENV_OVERRIDES, familyTokens, gatedFamilies, mergeWindows, probeUsage, windowsOf } from "./usage.ts";
20
+ import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config, type UsageState } from "./types.ts";
21
+ import { c } from "../cli/render.ts";
22
+
23
+ function teeObservation(account: Account, snap: { state: UsageState; at: number } | null): Observation | null {
24
+ if (!snap || snap.state.account !== account.id) return null;
25
+ if (account.lastUsageAt != null && snap.at < account.lastUsageAt) return { windows: account.windows, at: account.lastUsageAt };
26
+ const at = snap.state.sampledAt ?? snap.at;
27
+ const aggregate = windowsOf({ fiveHour: snap.state.fiveHour, sevenDay: snap.state.sevenDay, perModel: {} }, at);
28
+ return { windows: mergeWindows(aggregate, account.windows), at };
29
+ }
30
+
31
+ async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
32
+ let snap = loadUsageSnapshot();
33
+ const ttl = cfg.policy.usagePollTtlMs;
34
+ const probeAttempted = account.lastProbeAt != null && now - account.lastProbeAt <= ttl;
35
+ const fresh = snap != null && snap.state.account === account.id && now - snap.at <= ttl;
36
+ const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
37
+ if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
38
+ const startedAt = Date.now();
39
+ const full = await probeUsage();
40
+ const ts = Date.now();
41
+ if (readOAuthAccount()?.accountUuid === account.id) {
42
+ if (full) {
43
+ const teed = loadUsageSnapshot();
44
+ if (teed && teed.state.account === account.id && ts - teed.at <= ttl) {
45
+ snap = teed;
46
+ } else {
47
+ const state: UsageState = { fiveHour: full.fiveHour, sevenDay: full.sevenDay, account: account.id, ts, model: null };
48
+ writeUsage(state);
49
+ snap = { state, at: ts };
50
+ }
51
+ const expected = gatedFamilies(snap.state.model, cfg.policy.switchModels);
52
+ const rows = Object.keys(full.perModel);
53
+ if (expected.length > 0 && !expected.some((f) => rows.some((k) => familyTokens(k).includes(f)))) {
54
+ log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
55
+ }
56
+ }
57
+ await withLock(claudePool.lockFile, () => {
58
+ const idx = loadAccounts(claudePool);
59
+ const a = idx.accounts.find((x) => x.id === account.id);
60
+ if (!a) return;
61
+ a.lastProbeAt = startedAt;
62
+ if (full) {
63
+ const probed = windowsOf(full, startedAt);
64
+ const aggregate = a.windows.some((w) => w.name == null) ? a.windows.filter((w) => w.name == null) : probed.filter((w) => w.name == null);
65
+ if (a.lastUsageAt == null) a.lastUsageAt = startedAt;
66
+ a.windows = mergeWindows([...aggregate, ...probed.filter((w) => w.name != null)], a.windows);
67
+ }
68
+ saveAccounts(claudePool, idx);
69
+ });
70
+ }
71
+ }
72
+ const current = loadAccounts(claudePool).accounts.find((a) => a.id === account.id) ?? account;
73
+ return teeObservation(current, snap);
74
+ }
75
+
76
+ async function samplePool(accounts: Account[], liveId: string | null): Promise<Map<string, SampleReport>> {
77
+ const reports = new Map<string, SampleReport>();
78
+ const tee = loadUsageSnapshot();
79
+ const probeOne = async (a: Account): Promise<void> => {
80
+ const isActive = liveId != null && liveId === a.id;
81
+ const teeCurrent = tee != null && (a.lastUsageAt == null || tee.at >= a.lastUsageAt);
82
+ if (isActive && tee && teeCurrent && tee.state.account === a.id) {
83
+ const at = tee.state.sampledAt ?? tee.at;
84
+ a.lastUsageAt = at;
85
+ a.windows = mergeWindows(windowsOf({ fiveHour: tee.state.fiveHour, sevenDay: tee.state.sevenDay, perModel: {} }, at), a.windows);
86
+ reports.set(a.id, { ok: true, source: "statusline" });
87
+ return;
88
+ }
89
+ const outcome = isActive ? await probeActiveUsage(a) : await probeParkedUsage(a);
90
+ if (!outcome.ok) {
91
+ reports.set(a.id, { ok: false, reason: outcome.reason });
92
+ return;
93
+ }
94
+ const at = Date.now();
95
+ a.lastUsageAt = at;
96
+ a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
97
+ reports.set(a.id, { ok: true, source: "probe" });
98
+ };
99
+ const active = accounts.find((a) => liveId != null && liveId === a.id) ?? null;
100
+ if (active) await probeOne(active);
101
+ try {
102
+ await ensureLiveTokenFresh();
103
+ } catch {
104
+ }
105
+ await Promise.all(accounts.filter((a) => a !== active).map(probeOne));
106
+ return reports;
107
+ }
108
+
109
+ async function liveOwner(): Promise<string | null> {
110
+ const live = await readItem(liveTarget());
111
+ if (live == null) return null;
112
+ const creds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
113
+ return (await fetchTokenIdentity(creds.accessToken)).accountUuid;
114
+ }
115
+
116
+ async function removeCredentials(a: Account): Promise<void> {
117
+ const setupTokens = loadSetupTokens();
118
+ const item = credItemFor(a.id);
119
+ await deleteItem(parkedTarget(item));
120
+ for (const sampleDir of [join(paths.sampleDir, item), join(paths.sampleDir, `${item}-tick`)]) {
121
+ await deleteItem(isolatedTarget(sampleDir));
122
+ rmSync(sampleDir, { recursive: true, force: true });
123
+ }
124
+ if (setupTokens.tokens.some((t) => t.accountUuid === a.id)) {
125
+ saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.id) });
126
+ }
127
+ }
128
+
129
+ function identityReady(cjPath: string): boolean {
130
+ if (!existsSync(cjPath)) return false;
131
+ try {
132
+ const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
133
+ return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ async function login(): Promise<Harvest | null> {
140
+ const onboardDir = paths.onboardDir;
141
+ rmSync(onboardDir, { recursive: true, force: true });
142
+ mkdirSync(onboardDir, { recursive: true });
143
+ const iso = isolatedTarget(onboardDir);
144
+ await deleteItem(iso);
145
+ const cjPath = join(onboardDir, ".claude.json");
146
+ const real = resolveRealClaude();
147
+
148
+ const savedTermios = saveTermios();
149
+ const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
150
+ for (const key of CRED_ENV_OVERRIDES) delete env[key];
151
+ const p = Bun.spawn([real], {
152
+ stdin: "inherit",
153
+ stdout: "inherit",
154
+ stderr: "inherit",
155
+ env,
156
+ });
157
+
158
+ try {
159
+ let exited = false;
160
+ const onExit = p.exited.then(() => { exited = true; });
161
+ while (!exited) {
162
+ await Bun.sleep(400);
163
+ if (identityReady(cjPath) && (await readItem(iso))) {
164
+ p.kill();
165
+ break;
166
+ }
167
+ }
168
+ await p.exited;
169
+ await onExit;
170
+ restoreTermios(savedTermios);
171
+
172
+ const blobRaw = await readItem(iso);
173
+ if (!blobRaw || !identityReady(cjPath)) {
174
+ console.error(c.red("no login detected in the isolated session - nothing changed."));
175
+ return null;
176
+ }
177
+
178
+ let blob, oauthAccount;
179
+ try {
180
+ blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
181
+ oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
182
+ } catch {
183
+ console.error(c.red("could not parse the onboarded account's credential/identity."));
184
+ return null;
185
+ }
186
+
187
+ console.log(c.dim("sampling usage..."));
188
+ const sampled = await probeUsage(onboardDir);
189
+ if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
190
+ const at = Date.now();
191
+ const id = oauthAccount.accountUuid;
192
+ return {
193
+ id,
194
+ email: oauthAccount.emailAddress,
195
+ tier: claudeTierLabel(blob.claudeAiOauth),
196
+ oauthAccount,
197
+ sample: sampled ? { windows: windowsOf(sampled, at), at } : null,
198
+ park: () => writeItem(parkedTarget(credItemFor(id)), claudeAiOauthOnly(blobRaw)),
199
+ };
200
+ } finally {
201
+ if (p.exitCode === null) {
202
+ p.kill();
203
+ await p.exited;
204
+ }
205
+ restoreTermios(savedTermios);
206
+ await deleteItem(iso);
207
+ rmSync(onboardDir, { recursive: true, force: true });
208
+ }
209
+ }
210
+
211
+ async function importLive(): Promise<Harvest | null> {
212
+ if (isApiKeyMode()) {
213
+ console.error(c.yellow("tokenmaxxing pools subscription accounts, but you're authed via API key / apiKeyHelper."));
214
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} with a Pro/Max account first, then re-run ${c.cyan("tokenmaxxing init")}.`);
215
+ return null;
216
+ }
217
+
218
+ const oauthAccount = readOAuthAccount();
219
+ const liveRaw = await readItem(liveTarget());
220
+ if (!oauthAccount || !liveRaw) {
221
+ console.error(c.red("no active Claude subscription login found (missing oauthAccount or credential)."));
222
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
223
+ return null;
224
+ }
225
+
226
+ let blob;
227
+ try {
228
+ blob = CredentialBlobSchema.parse(JSON.parse(liveRaw));
229
+ } catch {
230
+ console.error(c.red("the live credential is not a recognizable Claude OAuth blob."));
231
+ return null;
232
+ }
233
+
234
+ let creds = blob.claudeAiOauth;
235
+ if (isDeadCredential(creds)) {
236
+ console.error(c.red("the live credential was cleared after a failed refresh."));
237
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
238
+ return null;
239
+ }
240
+ if (isAccessTokenExpiring(creds)) {
241
+ await withClaudeRefreshLock(async (lock) => {
242
+ const raw2 = await readItem(liveTarget());
243
+ if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
244
+ const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
245
+ creds = isAccessTokenExpiring(current) ? await refreshCredential(current) : current;
246
+ if (creds === current) return;
247
+ if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
248
+ await writeItem(liveTarget(), mergeIntoLive(raw2, creds));
249
+ });
250
+ }
251
+ const identity = await fetchTokenIdentity(creds.accessToken);
252
+ if (identity.accountUuid !== oauthAccount.accountUuid) {
253
+ console.error(c.red(`the live credential belongs to ${describeIdentity(identity)}, but ~/.claude.json identifies ${oauthAccount.emailAddress} - identity drift.`));
254
+ console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} to realign them, then re-run ${c.cyan("tokenmaxxing init")}.`);
255
+ return null;
256
+ }
257
+
258
+ const id = oauthAccount.accountUuid;
259
+ const parked = JSON.stringify({ claudeAiOauth: creds });
260
+ return {
261
+ id,
262
+ email: oauthAccount.emailAddress,
263
+ tier: claudeTierLabel(creds),
264
+ oauthAccount,
265
+ sample: null,
266
+ park: () => writeItem(parkedTarget(credItemFor(id)), parked),
267
+ };
268
+ }
269
+
270
+ function ensurePathAhead(): void {
271
+ const rc = shellRcPath();
272
+ if (!rc) {
273
+ console.log(c.yellow(`⚠ add to your shell rc: export PATH="${paths.binDir}:$PATH"`));
274
+ return;
275
+ }
276
+ const outcome = ensurePathInRc(rc);
277
+ if (outcome === "added") console.log(`${c.green("✓")} added ${paths.binDir} to PATH in ${rc} - restart your shell (or \`source ${rc}\`)`);
278
+ else if (outcome === "skipped") {
279
+ const hint = managedShellRcSkipLines();
280
+ console.log(c.yellow(`⚠ ${hint.headline}`));
281
+ console.log(c.yellow(` ${hint.detail}`));
282
+ console.log(c.yellow(` ${hint.exportLine}`));
283
+ } else console.log(c.yellow(`⚠ PATH line already in ${rc} - restart your shell to pick it up`));
284
+ }
285
+
286
+ function install(): void {
287
+ const out = installSupervisor();
288
+ console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/SessionStart hooks`);
289
+ if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
290
+ else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
291
+ if (!out.pathAhead) {
292
+ console.log();
293
+ ensurePathAhead();
294
+ }
295
+ }
296
+
297
+ export const claude: Provider = {
298
+ name: "claude",
299
+ flag: "",
300
+ pool: claudePool,
301
+ waitsWhenDepleted: true,
302
+ switchMargin: 1,
303
+ switchNote: "",
304
+ liveId: () => readOAuthAccount()?.accountUuid ?? null,
305
+ liveOwner,
306
+ presentIds: () => new Set(),
307
+ gatedFamilies: (cfg) => gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels),
308
+ observeLive,
309
+ samplePool,
310
+ mergeWindows,
311
+ swap: performSwap,
312
+ classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" : isSkippableSwapError(e) ? "skip" : "fatal"),
313
+ removeCredentials,
314
+ login,
315
+ importLive,
316
+ preflight: () => pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() }),
317
+ install,
318
+ loginStep: (who) => `In the session that opens, run ${c.bold("/login")} with ${who}. It closes itself once you're in.`,
319
+ windowLabel: (name) => name.toLowerCase(),
320
+ };