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,157 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { z } from "zod";
4
- import { withLock } from "./lock.ts";
5
- import { writeFileAtomic } from "./atomic.ts";
6
- import { codexPaths } from "./paths.ts";
7
- import { loadConfig } from "./state.ts";
8
- import { loadCodexAccounts, loadCodexLastSwapAt, saveCodexAccounts } from "./codexstate.ts";
9
- import { isCodexExhausted, pickBestCodex } from "./codexpick.ts";
10
- import { performCodexSwap } from "./codexswap.ts";
11
- import { CodexInvalidGrantError, refreshCodexAuth } from "./codexoauth.ts";
12
- import { fetchCodexUsage } from "./codexusage.ts";
13
- import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
14
- import { liveCodexAccountId } from "./codexsample.ts";
15
- import { livingCodexPresences, presentCodexAccountIds, targetableCodexAccounts } from "./codexpresence.ts";
16
- import { thresholdBars } from "./picker.ts";
17
- import { log } from "./log.ts";
18
- import { CodexAccountSchema, CodexReconcileMarkerSchema, type CodexAccount } from "./types.ts";
19
-
20
- const CodexSwapDecisionSchema = z.object({
21
- swapped: z.boolean(),
22
- account: CodexAccountSchema.nullable(),
23
- reason: z.string(),
24
- });
25
- export type CodexSwapDecision = z.infer<typeof CodexSwapDecisionSchema>;
26
-
27
- const POST_SWAP_COOLDOWN_MS = 45_000;
28
-
29
- async function sampleLiveOntoOwner(input: { now: number }): Promise<string | null> {
30
- const { now } = input;
31
- let live = readLiveCodexAuth();
32
- if (!live) return null;
33
- const index = loadCodexAccounts();
34
- const identity = codexIdentityOf({ auth: live });
35
- const owner = index.accounts.find((account) => account.accountId === identity.accountId);
36
- if (!owner) return null;
37
-
38
- if (isCodexAccessExpiring({ auth: live, now }) && !presentCodexAccountIds().has(identity.accountId)) {
39
- try {
40
- live = await refreshCodexAuth({ auth: live, now });
41
- } catch (e) {
42
- if (e instanceof CodexInvalidGrantError) {
43
- owner.needsReauth = true;
44
- saveCodexAccounts({ index });
45
- log("codexdecide.live_invalid_grant", { account: owner.accountId.slice(0, 8) });
46
- return owner.accountId;
47
- }
48
- throw e;
49
- }
50
- writeLiveCodexAuth({ auth: live });
51
- writeParkedCodexAuth({ credFile: owner.credFile, auth: live });
52
- }
53
- const usage = await fetchCodexUsage({ auth: live });
54
- owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit };
55
- owner.lastUsageAt = now;
56
- if (usage.email != null) owner.email = usage.email;
57
- if (usage.planType != null) owner.planType = usage.planType;
58
- saveCodexAccounts({ index });
59
- return owner.accountId;
60
- }
61
-
62
- function reconcileNonLiveSiblings(input: {
63
- index: { accounts: CodexAccount[] };
64
- liveAccountId: string;
65
- bars: { session: number; weekly: number };
66
- now: number;
67
- }): void {
68
- const { index, liveAccountId, bars, now } = input;
69
- const unusable = (account: CodexAccount): boolean =>
70
- account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
71
- const liveAccount = index.accounts.find((account) => account.accountId === liveAccountId);
72
- if (!liveAccount || unusable(liveAccount)) return;
73
- const living = livingCodexPresences();
74
- if (existsSync(codexPaths.reconcileDir)) {
75
- const alive = new Set(living.map((presence) => presence.supervisorId));
76
- for (const name of readdirSync(codexPaths.reconcileDir)) {
77
- if (!alive.has(name)) rmSync(join(codexPaths.reconcileDir, name), { force: true });
78
- }
79
- }
80
- for (const presence of living) {
81
- if (presence.accountId === liveAccountId) continue;
82
- const seated = index.accounts.find((account) => account.accountId === presence.accountId);
83
- if (!seated) continue;
84
- const markerPath = join(codexPaths.reconcileDir, presence.supervisorId);
85
- if (existsSync(markerPath)) continue;
86
- mkdirSync(codexPaths.reconcileDir, { recursive: true });
87
- writeFileAtomic(markerPath, JSON.stringify(CodexReconcileMarkerSchema.parse({ accountId: presence.accountId, ts: now })));
88
- log("codexdecide.reconcile_signal", { supervisorId: presence.supervisorId.slice(0, 8), account: presence.accountId.slice(0, 8) });
89
- }
90
- }
91
-
92
- function postSwapResweep(input: { liveAccountId: string; bars: { session: number; weekly: number }; now: number }): void {
93
- try {
94
- reconcileNonLiveSiblings({ index: loadCodexAccounts(), liveAccountId: input.liveAccountId, bars: input.bars, now: input.now });
95
- } catch (e) {
96
- log("codexdecide.resweep_failed", { err: e instanceof Error ? e.message : String(e) });
97
- }
98
- }
99
-
100
- export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promise<CodexSwapDecision> {
101
- const now = input.now ?? Date.now();
102
- const cfg = loadConfig();
103
- const bars = thresholdBars(cfg);
104
-
105
- return withLock(codexPaths.lockFile, async () => {
106
- let index = loadCodexAccounts();
107
- if (index.accounts.length === 0) return { swapped: false, account: null, reason: "no-pool" };
108
-
109
- const activeId = liveCodexAccountId();
110
- if (activeId == null || !index.accounts.some((account) => account.accountId === activeId)) {
111
- return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
112
- }
113
-
114
- reconcileNonLiveSiblings({ index, liveAccountId: activeId, bars, now });
115
-
116
- const lastSwapAt = loadCodexLastSwapAt();
117
- if (lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
118
- return { swapped: false, account: null, reason: "post-swap-cooldown" };
119
- }
120
-
121
- const activeEntry = index.accounts.find((account) => account.accountId === activeId);
122
- const stale = activeEntry?.lastUsageAt == null || now - activeEntry.lastUsageAt > cfg.policy.usagePollTtlMs;
123
- if (stale) {
124
- const sampledId = await sampleLiveOntoOwner({ now });
125
- if (sampledId == null) {
126
- return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
127
- }
128
- index = loadCodexAccounts();
129
- }
130
-
131
- const active = index.accounts.find((account) => account.accountId === activeId) ?? null;
132
- if (!active) return { swapped: false, account: null, reason: "no-active-account" };
133
-
134
- const engaged = active.needsReauth === true || isCodexExhausted({ account: active, thresholds: bars, now });
135
- if (!engaged) return { swapped: false, account: null, reason: "under-threshold-or-stale" };
136
-
137
- const tried = new Set<string>();
138
- while (true) {
139
- const current = loadCodexAccounts();
140
- const candidates = targetableCodexAccounts({ accounts: current.accounts, activeAccountId: activeId }).filter(
141
- (account) => !tried.has(account.accountId),
142
- );
143
- const best = pickBestCodex({ accounts: candidates, thresholds: bars, now, currentAccountId: activeId });
144
- if (!best) return { swapped: false, account: null, reason: "all-depleted" };
145
- tried.add(best.accountId);
146
- try {
147
- await performCodexSwap({ target: best });
148
- } catch (e) {
149
- if (e instanceof CodexInvalidGrantError) continue;
150
- throw e;
151
- }
152
- log("codexdecide.swap", { account: best.accountId.slice(0, 8) });
153
- postSwapResweep({ liveAccountId: best.accountId, bars, now });
154
- return { swapped: true, account: best, reason: "swapped" };
155
- }
156
- });
157
- }
@@ -1,80 +0,0 @@
1
- import { sortBy } from "es-toolkit";
2
- import { nextWeeklyReset } from "./picker.ts";
3
- import { isSessionWindow, weeklyWindowOf } from "./codexusage.ts";
4
- import type { CodexAccount, CodexWindow, Thresholds } from "./types.ts";
5
-
6
- function liveUsed(input: { window: CodexWindow; now: number; sampledAt: number | null }): number {
7
- const { window, now, sampledAt } = input;
8
- if (window.resetsAt != null) return window.resetsAt <= now ? 0 : window.usedPercentage;
9
- if (sampledAt != null && window.windowSeconds != null && now >= sampledAt + window.windowSeconds * 1000) return 0;
10
- return window.usedPercentage;
11
- }
12
-
13
- function allWindows(account: CodexAccount): CodexWindow[] {
14
- const usage = account.lastUsage;
15
- if (!usage) return [];
16
- return [...usage.aggregate, ...Object.values(usage.perLimit).flat()];
17
- }
18
-
19
- function barFor(input: { window: CodexWindow; thresholds: Thresholds }): number {
20
- return isSessionWindow({ window: input.window }) ? input.thresholds.session : input.thresholds.weekly;
21
- }
22
-
23
- export function isCodexExhausted(input: { account: CodexAccount; thresholds: Thresholds; now: number }): boolean {
24
- const { account, thresholds, now } = input;
25
- const sampledAt = account.lastUsageAt ?? null;
26
- return allWindows(account).some(
27
- (window) => liveUsed({ window, now, sampledAt }) >= barFor({ window, thresholds }),
28
- );
29
- }
30
-
31
- export function codexPacePressure(input: { account: CodexAccount; now: number }): number {
32
- const { account, now } = input;
33
- const weekly = account.lastUsage ? weeklyWindowOf({ aggregate: account.lastUsage.aggregate }) : null;
34
- if (!weekly) return 0;
35
- const reset = nextWeeklyReset(weekly.resetsAt, now);
36
- if (reset == null) return 0;
37
- return Math.max(0, 100 - liveUsed({ window: weekly, now, sampledAt: account.lastUsageAt ?? null })) / Math.max(1, reset - now);
38
- }
39
-
40
- function weeklyExpiryOf(input: { account: CodexAccount; now: number }): number {
41
- const { account, now } = input;
42
- const weekly = account.lastUsage ? weeklyWindowOf({ aggregate: account.lastUsage.aggregate }) : null;
43
- return nextWeeklyReset(weekly?.resetsAt ?? null, now) ?? Number.POSITIVE_INFINITY;
44
- }
45
-
46
- const codexSwapPreference = (now: number) => [
47
- (account: CodexAccount) => -codexPacePressure({ account, now }),
48
- (account: CodexAccount) => weeklyExpiryOf({ account, now }),
49
- ];
50
-
51
- export function pickBestCodex(input: {
52
- accounts: CodexAccount[];
53
- thresholds: Thresholds;
54
- now: number;
55
- currentAccountId: string | null;
56
- }): CodexAccount | null {
57
- const { accounts, thresholds, now, currentAccountId } = input;
58
- const usable = accounts.filter(
59
- (account) =>
60
- account.accountId !== currentAccountId &&
61
- account.needsReauth !== true &&
62
- !isCodexExhausted({ account, thresholds, now }),
63
- );
64
- return sortBy(usable, codexSwapPreference(now))[0] ?? null;
65
- }
66
-
67
- export const CODEX_SWAP_IMPROVEMENT = 1.2;
68
-
69
- export function codexCurrentWins(input: {
70
- active: CodexAccount | null;
71
- accounts: CodexAccount[];
72
- thresholds: Thresholds;
73
- now: number;
74
- }): boolean {
75
- const { active, accounts, thresholds, now } = input;
76
- if (!active || active.needsReauth === true || isCodexExhausted({ account: active, thresholds, now })) return false;
77
- const best = pickBestCodex({ accounts, thresholds, now, currentAccountId: null });
78
- if (best == null || best.accountId === active.accountId) return true;
79
- return codexPacePressure({ account: best, now }) <= codexPacePressure({ account: active, now }) * CODEX_SWAP_IMPROVEMENT;
80
- }
@@ -1,48 +0,0 @@
1
- import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
2
- import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
3
- import { CodexUsageReadError, fetchCodexUsage } from "./codexusage.ts";
4
- import { presentCodexAccountIds } from "./codexpresence.ts";
5
- import type { CodexAccount, CodexUsage } from "./types.ts";
6
- import { z } from "zod";
7
-
8
- const CodexSampleOutcomeSchema = z.union([
9
- z.object({ ok: z.literal(true), usage: z.custom<CodexUsage>() }),
10
- z.object({ ok: z.literal(false), reason: z.string(), deadGrant: z.boolean() }),
11
- ]);
12
- export type CodexSampleOutcome = z.infer<typeof CodexSampleOutcomeSchema>;
13
-
14
- export function liveCodexAccountId(): string | null {
15
- const live = readLiveCodexAuth();
16
- if (!live) return null;
17
- return codexIdentityOf({ auth: live }).accountId;
18
- }
19
-
20
- export async function sampleCodexAccount(input: { account: CodexAccount; liveAccountId: string | null; now?: number }): Promise<CodexSampleOutcome> {
21
- const { account, liveAccountId, now = Date.now() } = input;
22
- const isLive = liveAccountId != null && account.accountId === liveAccountId;
23
- try {
24
- let auth = isLive ? readLiveCodexAuth() : readParkedCodexAuth({ credFile: account.credFile });
25
- if (!auth) return { ok: false, reason: isLive ? "live auth.json vanished" : "no parked credential", deadGrant: false };
26
- if (isCodexAccessExpiring({ auth, now })) {
27
- const running = presentCodexAccountIds().has(account.accountId);
28
- if (running && !isLive) {
29
- return { ok: false, reason: "running in a live codex session (parked token refresh unsafe)", deadGrant: false };
30
- }
31
- if (!running) {
32
- auth = await refreshCodexAuth({ auth, now });
33
- if (isLive) writeLiveCodexAuth({ auth });
34
- writeParkedCodexAuth({ credFile: account.credFile, auth });
35
- }
36
- }
37
- const usage = await fetchCodexUsage({ auth });
38
- return { ok: true, usage };
39
- } catch (e) {
40
- if (e instanceof CodexInvalidGrantError) {
41
- return { ok: false, reason: e.message, deadGrant: true };
42
- }
43
- if (e instanceof CodexRefreshFailedError || e instanceof CodexUsageReadError) {
44
- return { ok: false, reason: e.message, deadGrant: false };
45
- }
46
- throw e;
47
- }
48
- }
@@ -1,22 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { writeFileAtomic } from "./atomic.ts";
3
- import { codexPaths } from "./paths.ts";
4
- import { CodexAccountsIndexSchema, LastSwapSchema, type CodexAccountsIndex } from "./types.ts";
5
-
6
- export function loadCodexAccounts(): CodexAccountsIndex {
7
- if (!existsSync(codexPaths.accountsJson)) return { version: 1, activeAccountId: null, accounts: [] };
8
- return CodexAccountsIndexSchema.parse(JSON.parse(readFileSync(codexPaths.accountsJson, "utf8")));
9
- }
10
-
11
- export function saveCodexAccounts(input: { index: CodexAccountsIndex }): void {
12
- writeFileAtomic(codexPaths.accountsJson, JSON.stringify(CodexAccountsIndexSchema.parse(input.index), null, 2) + "\n");
13
- }
14
-
15
- export function loadCodexLastSwapAt(): number | null {
16
- if (!existsSync(codexPaths.lastSwapJson)) return null;
17
- return LastSwapSchema.parse(JSON.parse(readFileSync(codexPaths.lastSwapJson, "utf8"))).ts;
18
- }
19
-
20
- export function saveCodexLastSwapAt(input: { ts: number }): void {
21
- writeFileAtomic(codexPaths.lastSwapJson, JSON.stringify(LastSwapSchema.parse({ ts: input.ts })));
22
- }
@@ -1,70 +0,0 @@
1
- import { codexIdentityOf, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
2
- import { CodexInvalidGrantError, refreshCodexAuth } from "./codexoauth.ts";
3
- import { loadCodexAccounts, saveCodexAccounts, saveCodexLastSwapAt } from "./codexstate.ts";
4
- import { log } from "./log.ts";
5
- import type { CodexAccount } from "./types.ts";
6
-
7
- export async function performCodexSwap(input: { target: CodexAccount }): Promise<void> {
8
- const { target } = input;
9
- const index = loadCodexAccounts();
10
-
11
- const parked = readParkedCodexAuth({ credFile: target.credFile });
12
- if (!parked) throw new Error(`no parked codex credential for ${target.label}`);
13
-
14
- const live = readLiveCodexAuth();
15
- let liveOwner = null;
16
- if (live) {
17
- const liveIdentity = codexIdentityOf({ auth: live });
18
- if (liveIdentity.accountId === target.accountId) {
19
- throw new Error(
20
- `${target.label} is already the live codex credential - refusing to swap an account onto itself`,
21
- );
22
- }
23
- liveOwner = index.accounts.find((account) => account.accountId === liveIdentity.accountId) ?? null;
24
- if (!liveOwner) {
25
- throw new Error(
26
- `live codex credential belongs to ${liveIdentity.email ?? liveIdentity.accountId.slice(0, 8)}, which is not in the pool - refusing to swap over it; import it first with \`tokenmaxxing add --codex\``,
27
- );
28
- }
29
- if (liveOwner.accountId !== index.activeAccountId) {
30
- log("codexswap.harvest_drift", {
31
- labeled: index.activeAccountId?.slice(0, 8) ?? null,
32
- actual: liveOwner.accountId.slice(0, 8),
33
- });
34
- }
35
- }
36
-
37
- let fresh;
38
- try {
39
- fresh = await refreshCodexAuth({ auth: parked });
40
- } catch (e) {
41
- if (e instanceof CodexInvalidGrantError) {
42
- const entry = index.accounts.find((account) => account.accountId === target.accountId);
43
- if (entry) {
44
- entry.needsReauth = true;
45
- saveCodexAccounts({ index });
46
- }
47
- log("codexswap.invalid_grant", { account: target.accountId.slice(0, 8) });
48
- }
49
- throw e;
50
- }
51
- writeParkedCodexAuth({ credFile: target.credFile, auth: fresh });
52
-
53
- if (live && liveOwner) {
54
- const liveNow = readLiveCodexAuth();
55
- if (!liveNow || codexIdentityOf({ auth: liveNow }).accountId !== liveOwner.accountId) {
56
- throw new Error("live codex credential changed mid-swap - refusing to harvest under a stale identity; retry");
57
- }
58
- writeParkedCodexAuth({ credFile: liveOwner.credFile, auth: liveNow });
59
- log("codexswap.harvest", { account: liveOwner.accountId.slice(0, 8) });
60
- }
61
-
62
- writeLiveCodexAuth({ auth: fresh });
63
- index.activeAccountId = target.accountId;
64
- const entry = index.accounts.find((account) => account.accountId === target.accountId);
65
- if (entry) entry.needsReauth = false;
66
- saveCodexAccounts({ index });
67
- saveCodexLastSwapAt({ ts: Date.now() });
68
- log("codexswap.done", { account: target.accountId.slice(0, 8), label: target.label });
69
- }
70
-