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.
- package/DESIGN.md +3 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/doctor.ts +13 -13
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +11 -45
- package/src/cli/render.ts +0 -7
- package/src/cli/rm.ts +24 -39
- package/src/cli/setuptoken.ts +16 -16
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +22 -16
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +3 -2
- package/src/entries/statusline.ts +15 -13
- package/src/entries/stopfailurehook.ts +7 -5
- package/src/entries/stophook.ts +3 -2
- package/src/lib/claude.ts +320 -0
- package/src/lib/codex.ts +353 -0
- package/src/lib/codexauth.ts +6 -0
- package/src/lib/codexpresence.ts +0 -10
- package/src/lib/codexusage.ts +14 -32
- package/src/lib/decide.ts +86 -174
- package/src/lib/oauth.ts +7 -0
- package/src/lib/paths.ts +14 -6
- package/src/lib/picker.ts +56 -37
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +49 -16
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +17 -51
- package/src/lib/usage.ts +17 -4
- package/src/main.ts +20 -20
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -36
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
package/src/cli/status.ts
CHANGED
|
@@ -1,158 +1,113 @@
|
|
|
1
1
|
import { chunk, sortBy } from "es-toolkit";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { claude } from "../lib/claude.ts";
|
|
4
|
+
import { codex } from "../lib/codex.ts";
|
|
5
|
+
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
6
6
|
import { withLock } from "../lib/lock.ts";
|
|
7
|
-
import {
|
|
8
|
-
import { earliestReset, isExhausted, nextWeeklyReset, thresholdBars } from "../lib/picker.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { codexLimitLabel, isSessionWindow } from "../lib/codexusage.ts";
|
|
13
|
-
import { bar, c, claudeTierLabel, count, emitJson, fmtAgo } from "./render.ts";
|
|
14
|
-
import { gatedFamilies, keepRows } from "../lib/usage.ts";
|
|
15
|
-
import { ThresholdsSchema, UsageWindowSchema, type Account, type CodexWindow, type Config, type UsageWindow, type UsageWindows } from "../lib/types.ts";
|
|
7
|
+
import { codexPool } from "../lib/paths.ts";
|
|
8
|
+
import { earliestReset, isExhausted, isSessionWindow, limitWindows, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
|
+
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
10
|
+
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
11
|
+
import { ThresholdsSchema, type Account, type Config, type Window } from "../lib/types.ts";
|
|
16
12
|
|
|
17
13
|
const SampleReportSchema = z.discriminatedUnion("ok", [
|
|
18
14
|
z.object({ ok: z.literal(true), source: z.enum(["statusline", "probe", "cached"]) }),
|
|
19
15
|
z.object({ ok: z.literal(false), reason: z.string() }),
|
|
20
16
|
]);
|
|
21
17
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
organizationUuid: z.string(),
|
|
27
|
-
tier: z.string().nullable(),
|
|
28
|
-
active: z.boolean(),
|
|
29
|
-
needsReauth: z.boolean(),
|
|
30
|
-
exhausted: z.boolean(),
|
|
31
|
-
usage: z.object({ fiveHour: UsageWindowSchema, week: UsageWindowSchema }).nullable(),
|
|
32
|
-
perModel: z.record(z.string(), UsageWindowSchema),
|
|
33
|
-
usageAt: z.number().nullable(),
|
|
34
|
-
perModelAt: z.number().nullable(),
|
|
35
|
-
sample: SampleReportSchema,
|
|
18
|
+
const WindowReportSchema = z.object({
|
|
19
|
+
usedPercentage: z.number(),
|
|
20
|
+
resetsAt: z.number().nullable(),
|
|
21
|
+
windowSeconds: z.number().nullable(),
|
|
36
22
|
});
|
|
37
|
-
type
|
|
38
|
-
|
|
39
|
-
const CodexWindowReportSchema = UsageWindowSchema.extend({ windowSeconds: z.number().nullable() });
|
|
23
|
+
type WindowReport = z.infer<typeof WindowReportSchema>;
|
|
40
24
|
|
|
41
|
-
const
|
|
25
|
+
const StatusAccountSchema = z.object({
|
|
42
26
|
label: z.string(),
|
|
43
27
|
email: z.string().nullable(),
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
id: z.string(),
|
|
29
|
+
tier: z.string().nullable(),
|
|
46
30
|
active: z.boolean(),
|
|
47
31
|
needsReauth: z.boolean(),
|
|
48
32
|
exhausted: z.boolean(),
|
|
49
33
|
usage: z
|
|
50
34
|
.object({
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
fiveHour: WindowReportSchema.nullable(),
|
|
36
|
+
week: WindowReportSchema.nullable(),
|
|
37
|
+
limits: z.array(WindowReportSchema.extend({ name: z.string() })),
|
|
53
38
|
})
|
|
54
39
|
.nullable(),
|
|
55
40
|
usageAt: z.number().nullable(),
|
|
41
|
+
limitsAt: z.number().nullable(),
|
|
56
42
|
sample: SampleReportSchema,
|
|
57
43
|
});
|
|
58
|
-
type
|
|
44
|
+
type StatusAccount = z.infer<typeof StatusAccountSchema>;
|
|
59
45
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
projectionMargin: z.number(),
|
|
66
|
-
accounts: z.array(ClaudeStatusAccountSchema),
|
|
67
|
-
}),
|
|
68
|
-
codex: z.object({
|
|
69
|
-
bars: ThresholdsSchema,
|
|
70
|
-
accounts: z.array(CodexStatusAccountSchema),
|
|
71
|
-
}),
|
|
46
|
+
const PoolReportSchema = z.object({
|
|
47
|
+
thresholds: ThresholdsSchema,
|
|
48
|
+
bars: ThresholdsSchema,
|
|
49
|
+
projectionMargin: z.number(),
|
|
50
|
+
accounts: z.array(StatusAccountSchema),
|
|
72
51
|
});
|
|
52
|
+
type PoolReport = z.infer<typeof PoolReportSchema>;
|
|
53
|
+
|
|
54
|
+
const StatusReportSchema = z.object({ now: z.number(), claude: PoolReportSchema, codex: PoolReportSchema });
|
|
73
55
|
export type StatusReport = z.infer<typeof StatusReportSchema>;
|
|
74
56
|
|
|
75
|
-
function currentWindow(w:
|
|
57
|
+
function currentWindow(w: Window, now: number): WindowReport {
|
|
76
58
|
const passed = w.resetsAt != null && w.resetsAt <= now;
|
|
77
59
|
return {
|
|
78
60
|
usedPercentage: passed ? 0 : w.usedPercentage,
|
|
79
|
-
resetsAt:
|
|
61
|
+
resetsAt: isSessionWindow(w) ? (passed ? null : w.resetsAt) : nextWeeklyReset(w.resetsAt, now),
|
|
62
|
+
windowSeconds: w.windowSeconds,
|
|
80
63
|
};
|
|
81
64
|
}
|
|
82
65
|
|
|
83
|
-
function
|
|
84
|
-
|
|
66
|
+
function usageReport(a: Account, now: number): StatusAccount["usage"] {
|
|
67
|
+
if (a.lastUsageAt == null) return null;
|
|
68
|
+
const session = sessionWindow(a);
|
|
69
|
+
const week = weeklyWindow(a);
|
|
70
|
+
return {
|
|
71
|
+
fiveHour: session ? currentWindow(session, now) : null,
|
|
72
|
+
week: week ? currentWindow(week, now) : null,
|
|
73
|
+
limits: limitWindows(a).map((w) => ({ name: w.name ?? "", ...currentWindow(w, now) })),
|
|
74
|
+
};
|
|
85
75
|
}
|
|
86
76
|
|
|
87
|
-
async function
|
|
88
|
-
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (isActive && liveOAuth?.organizationRateLimitTier != null) a.rateLimitTier = liveOAuth.organizationRateLimitTier;
|
|
103
|
-
const teeCurrent = teeAt != null && (a.lastUsageAt == null || teeAt >= a.lastUsageAt);
|
|
104
|
-
const fromStatusLine: UsageWindows | null =
|
|
105
|
-
isActive && live && teeCurrent && live.account === a.accountUuid ? { fiveHour: live.fiveHour, sevenDay: live.sevenDay, perModel: {} } : null;
|
|
106
|
-
const viaTee = fromStatusLine != null;
|
|
107
|
-
const outcome: SampleOutcome = fromStatusLine
|
|
108
|
-
? { ok: true, usage: fromStatusLine }
|
|
109
|
-
: isActive
|
|
110
|
-
? await probeActiveUsage(a)
|
|
111
|
-
: await probeParkedUsage(a);
|
|
112
|
-
samples.set(a.accountUuid, { outcome, viaTee });
|
|
113
|
-
if (!outcome.ok) return;
|
|
114
|
-
a.lastUsageAt = viaTee && teeAt != null ? (live?.sampledAt ?? teeAt) : Date.now();
|
|
115
|
-
a.lastUsage = keepRows(outcome.usage, a.lastUsage, a.lastUsageAt);
|
|
116
|
-
};
|
|
117
|
-
const activeAccount = idx.accounts.find((a) => liveAccount != null && liveAccount === a.accountUuid) ?? null;
|
|
118
|
-
if (activeAccount) await probeOne(activeAccount);
|
|
119
|
-
try {
|
|
120
|
-
await ensureLiveTokenFresh();
|
|
121
|
-
} catch {
|
|
122
|
-
}
|
|
123
|
-
await Promise.all(idx.accounts.filter((a) => a !== activeAccount).map(probeOne));
|
|
124
|
-
saveAccounts(idx);
|
|
125
|
-
});
|
|
77
|
+
async function collect(p: Provider, cfg: Config, now: number, cached: boolean): Promise<PoolReport> {
|
|
78
|
+
let idx = loadAccounts(p.pool);
|
|
79
|
+
let reports = new Map<string, SampleReport>();
|
|
80
|
+
let liveId: string | null = idx.activeId;
|
|
81
|
+
if (!cached) {
|
|
82
|
+
liveId = null;
|
|
83
|
+
if (idx.accounts.length > 0) {
|
|
84
|
+
await withLock(p.pool.lockFile, async () => {
|
|
85
|
+
idx = loadAccounts(p.pool);
|
|
86
|
+
console.error(c.dim(`sampling ${p.name} usage...`));
|
|
87
|
+
liveId = p.liveId();
|
|
88
|
+
reports = await p.samplePool(idx.accounts, liveId, now);
|
|
89
|
+
saveAccounts(p.pool, idx);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
126
92
|
}
|
|
127
93
|
|
|
128
|
-
const families = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
129
94
|
const bars = thresholdBars(cfg);
|
|
130
|
-
const
|
|
95
|
+
const ctx = { now, thresholds: bars, currentId: idx.activeId, families: p.gatedFamilies(cfg) };
|
|
131
96
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
132
|
-
const accounts = ordered.map((a):
|
|
133
|
-
const
|
|
134
|
-
const aggregate = a.lastUsage;
|
|
135
|
-
const perModel = aggregate?.perModel ?? {};
|
|
97
|
+
const accounts = ordered.map((a): StatusAccount => {
|
|
98
|
+
const limits = limitWindows(a);
|
|
136
99
|
return {
|
|
137
100
|
label: a.label,
|
|
138
101
|
email: a.email,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
active: liveAccount != null && a.accountUuid === liveAccount,
|
|
102
|
+
id: a.id,
|
|
103
|
+
tier: a.tier,
|
|
104
|
+
active: liveId != null && a.id === liveId,
|
|
143
105
|
needsReauth: a.needsReauth === true,
|
|
144
|
-
exhausted: isExhausted(a,
|
|
145
|
-
usage:
|
|
146
|
-
perModel: Object.fromEntries(Object.entries(perModel).map(([name, w]) => [name, currentWindow(w, true, now)])),
|
|
106
|
+
exhausted: isExhausted(a, ctx),
|
|
107
|
+
usage: usageReport(a, now),
|
|
147
108
|
usageAt: a.lastUsageAt ?? null,
|
|
148
|
-
|
|
149
|
-
sample: sampled
|
|
150
|
-
? sampled.outcome.ok
|
|
151
|
-
? { ok: true, source: sampled.viaTee ? "statusline" : "probe" }
|
|
152
|
-
: { ok: false, reason: sampled.outcome.reason }
|
|
153
|
-
: cached
|
|
154
|
-
? { ok: true, source: "cached" }
|
|
155
|
-
: { ok: false, reason: "not sampled" },
|
|
109
|
+
limitsAt: limits.length > 0 ? Math.max(...limits.map((w) => w.sampledAt)) : null,
|
|
110
|
+
sample: reports.get(a.id) ?? (cached ? { ok: true, source: "cached" } : { ok: false, reason: "not sampled" }),
|
|
156
111
|
};
|
|
157
112
|
});
|
|
158
113
|
return {
|
|
@@ -163,79 +118,6 @@ async function collectClaude(input: { cfg: Config; now: number; cached: boolean
|
|
|
163
118
|
};
|
|
164
119
|
}
|
|
165
120
|
|
|
166
|
-
async function collectCodex(input: { cfg: Config; now: number; cached: boolean }): Promise<StatusReport["codex"]> {
|
|
167
|
-
const { cfg, now, cached } = input;
|
|
168
|
-
const bars = thresholdBars(cfg);
|
|
169
|
-
let index = loadCodexAccounts();
|
|
170
|
-
if (index.accounts.length === 0) return { bars, accounts: [] };
|
|
171
|
-
|
|
172
|
-
const outcomes = new Map<string, CodexSampleOutcome>();
|
|
173
|
-
let liveId: string | null = null;
|
|
174
|
-
if (cached) {
|
|
175
|
-
liveId = index.activeAccountId;
|
|
176
|
-
} else {
|
|
177
|
-
console.error(c.dim("sampling codex usage..."));
|
|
178
|
-
await withLock(codexPaths.lockFile, async () => {
|
|
179
|
-
index = loadCodexAccounts();
|
|
180
|
-
liveId = liveCodexAccountId();
|
|
181
|
-
await Promise.all(
|
|
182
|
-
index.accounts.map(async (account) => {
|
|
183
|
-
const outcome = await sampleCodexAccount({ account, liveAccountId: liveId, now });
|
|
184
|
-
outcomes.set(account.accountId, outcome);
|
|
185
|
-
if (outcome.ok) {
|
|
186
|
-
account.lastUsage = { aggregate: outcome.usage.aggregate, perLimit: outcome.usage.perLimit };
|
|
187
|
-
account.lastUsageAt = Date.now();
|
|
188
|
-
if (outcome.usage.email != null) account.email = outcome.usage.email;
|
|
189
|
-
if (outcome.usage.planType != null) account.planType = outcome.usage.planType;
|
|
190
|
-
} else if (outcome.deadGrant) {
|
|
191
|
-
account.needsReauth = true;
|
|
192
|
-
}
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
saveCodexAccounts({ index });
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const ordered = sortBy(index.accounts, [
|
|
200
|
-
(a) => (a.needsReauth ? 1 : 0),
|
|
201
|
-
(a) => {
|
|
202
|
-
const windows = [...(a.lastUsage?.aggregate ?? []), ...Object.values(a.lastUsage?.perLimit ?? {}).flat()];
|
|
203
|
-
const resets = windows.flatMap((w) => (w.resetsAt != null && w.resetsAt > now ? [w.resetsAt] : []));
|
|
204
|
-
return resets.length > 0 ? Math.min(...resets) : Number.POSITIVE_INFINITY;
|
|
205
|
-
},
|
|
206
|
-
]);
|
|
207
|
-
const accounts = ordered.map((account): CodexStatusAccount => {
|
|
208
|
-
const outcome = outcomes.get(account.accountId);
|
|
209
|
-
const usage = account.lastUsage;
|
|
210
|
-
return {
|
|
211
|
-
label: account.label,
|
|
212
|
-
email: account.email,
|
|
213
|
-
accountId: account.accountId,
|
|
214
|
-
planType: account.planType,
|
|
215
|
-
active: account.accountId === liveId,
|
|
216
|
-
needsReauth: account.needsReauth === true,
|
|
217
|
-
exhausted: isCodexExhausted({ account, thresholds: bars, now }),
|
|
218
|
-
usage: usage
|
|
219
|
-
? {
|
|
220
|
-
aggregate: usage.aggregate.map((w) => currentCodexWindow(w, now)),
|
|
221
|
-
perLimit: Object.fromEntries(
|
|
222
|
-
Object.entries(usage.perLimit).map(([name, windows]) => [name, windows.map((w) => currentCodexWindow(w, now))]),
|
|
223
|
-
),
|
|
224
|
-
}
|
|
225
|
-
: null,
|
|
226
|
-
usageAt: account.lastUsageAt ?? null,
|
|
227
|
-
sample: outcome
|
|
228
|
-
? outcome.ok
|
|
229
|
-
? { ok: true, source: "probe" }
|
|
230
|
-
: { ok: false, reason: outcome.reason }
|
|
231
|
-
: cached
|
|
232
|
-
? { ok: true, source: "cached" }
|
|
233
|
-
: { ok: false, reason: "not sampled" },
|
|
234
|
-
};
|
|
235
|
-
});
|
|
236
|
-
return { bars, accounts };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
121
|
const CARD_GAP = 3;
|
|
240
122
|
const NOTE_INDENT = " ";
|
|
241
123
|
|
|
@@ -292,7 +174,7 @@ function renderGrid(cards: Card[]): void {
|
|
|
292
174
|
}
|
|
293
175
|
}
|
|
294
176
|
|
|
295
|
-
function usageRow(name: string, w:
|
|
177
|
+
function usageRow(name: string, w: WindowReport): string {
|
|
296
178
|
return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.usedPercentage)}`;
|
|
297
179
|
}
|
|
298
180
|
|
|
@@ -317,39 +199,13 @@ function headerLine(input: { active: boolean; needsReauth: boolean; exhausted: b
|
|
|
317
199
|
return `${marker} ${c.bold(input.name)}${input.tier ? ` ${c.dim(input.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`;
|
|
318
200
|
}
|
|
319
201
|
|
|
320
|
-
function
|
|
321
|
-
const
|
|
322
|
-
isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
|
|
323
|
-
const lines = [headerLine({ ...account, name: account.label, tier: account.planType })];
|
|
324
|
-
if (account.usage) {
|
|
325
|
-
for (const window of account.usage.aggregate) lines.push(usageRow(windowLabel(window), window));
|
|
326
|
-
for (const [name, windows] of Object.entries(account.usage.perLimit)) {
|
|
327
|
-
for (const window of windows) lines.push(usageRow(codexLimitLabel({ limitName: name }), window));
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
const notes = account.sample.ok
|
|
331
|
-
? account.sample.source === "cached"
|
|
332
|
-
? [cachedNote(account.usageAt, now)]
|
|
333
|
-
: []
|
|
334
|
-
: sampleFailedNotes({ cached: account.usage != null, usageAt: account.usageAt, reason: account.sample.reason, now });
|
|
335
|
-
return { lines, notes };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function renderCodex(input: { codex: StatusReport["codex"]; now: number }): void {
|
|
339
|
-
const { codex, now } = input;
|
|
340
|
-
if (codex.accounts.length === 0) return;
|
|
341
|
-
console.log(c.dim(`codex (${count({ n: codex.accounts.length, noun: "account" })})`));
|
|
342
|
-
console.log();
|
|
343
|
-
renderGrid(codex.accounts.map((account) => codexCard(account, now)));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function claudeCard(a: ClaudeStatusAccount, now: number, staleAfterMs: number): Card {
|
|
347
|
-
const lines = [headerLine({ ...a, name: a.label || a.email })];
|
|
202
|
+
function card(p: Provider, a: StatusAccount, now: number, staleAfterMs: number): Card {
|
|
203
|
+
const lines = [headerLine({ ...a, name: a.label })];
|
|
348
204
|
if (a.usage) {
|
|
349
|
-
lines.push(usageRow(
|
|
350
|
-
lines.push(usageRow("week", a.usage.week));
|
|
205
|
+
if (a.usage.fiveHour) lines.push(usageRow(`${Math.round((a.usage.fiveHour.windowSeconds ?? 0) / 3600)}h`, a.usage.fiveHour));
|
|
206
|
+
if (a.usage.week) lines.push(usageRow("week", a.usage.week));
|
|
207
|
+
for (const w of a.usage.limits) lines.push(usageRow(p.windowLabel(w.name), w));
|
|
351
208
|
}
|
|
352
|
-
for (const [name, w] of Object.entries(a.perModel)) lines.push(usageRow(name.toLowerCase(), w));
|
|
353
209
|
const notes: Note[] = [];
|
|
354
210
|
if (a.sample.ok && a.sample.source === "statusline") {
|
|
355
211
|
const stale = a.usageAt == null || now - a.usageAt > staleAfterMs;
|
|
@@ -358,46 +214,47 @@ function claudeCard(a: ClaudeStatusAccount, now: number, staleAfterMs: number):
|
|
|
358
214
|
}
|
|
359
215
|
if (a.sample.ok && a.sample.source === "cached") {
|
|
360
216
|
const note = cachedNote(a.usageAt, now);
|
|
361
|
-
|
|
362
|
-
notes.push(perModelAged ? { ...note, text: `${note.text}, per-model ${a.perModelAt != null ? fmtAgo(a.perModelAt, now) : "age unknown"}` } : note);
|
|
217
|
+
notes.push(a.limitsAt != null && a.limitsAt !== a.usageAt ? { ...note, text: `${note.text}, per-model ${fmtAgo(a.limitsAt, now)}` } : note);
|
|
363
218
|
}
|
|
364
219
|
if (!a.sample.ok) {
|
|
365
|
-
notes.push(...sampleFailedNotes({ cached: a.usage != null
|
|
220
|
+
notes.push(...sampleFailedNotes({ cached: a.usage != null, usageAt: a.usageAt, reason: a.sample.reason, now }));
|
|
366
221
|
}
|
|
367
222
|
return { lines, notes };
|
|
368
223
|
}
|
|
369
224
|
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
if (claude.accounts.length === 0) {
|
|
373
|
-
if (!codexPooled) {
|
|
374
|
-
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
378
|
-
console.log();
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
console.log(c.dim(`thresholds 5h ${claude.thresholds.session}% weekly ${claude.thresholds.weekly}% (${count({ n: claude.accounts.length, noun: "claude account" })})`));
|
|
225
|
+
function renderPool(p: Provider, pool: PoolReport, header: string, now: number, staleAfterMs: number): void {
|
|
226
|
+
console.log(c.dim(header));
|
|
383
227
|
console.log();
|
|
384
|
-
renderGrid(
|
|
228
|
+
renderGrid(pool.accounts.map((a) => card(p, a, now, staleAfterMs)));
|
|
385
229
|
}
|
|
386
230
|
|
|
387
231
|
export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {}): Promise<number> {
|
|
388
232
|
const { json = false, cached = false } = opts;
|
|
389
233
|
const cfg = loadConfig();
|
|
390
234
|
const now = Date.now();
|
|
391
|
-
const
|
|
235
|
+
const claudeReport = await collect(claude, cfg, now, cached);
|
|
392
236
|
if (!json) {
|
|
393
|
-
|
|
237
|
+
const codexPooled = loadAccounts(codexPool).accounts.length > 0;
|
|
238
|
+
if (claudeReport.accounts.length === 0) {
|
|
239
|
+
if (!codexPooled) {
|
|
240
|
+
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
244
|
+
console.log();
|
|
245
|
+
} else {
|
|
246
|
+
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
247
|
+
renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
|
|
248
|
+
}
|
|
394
249
|
}
|
|
395
|
-
const
|
|
250
|
+
const codexReport = await collect(codex, cfg, now, cached);
|
|
396
251
|
if (json) {
|
|
397
|
-
const report: StatusReport = { now, claude, codex };
|
|
252
|
+
const report: StatusReport = { now, claude: claudeReport, codex: codexReport };
|
|
398
253
|
emitJson({ ok: true, ...report });
|
|
399
254
|
return 0;
|
|
400
255
|
}
|
|
401
|
-
|
|
256
|
+
if (codexReport.accounts.length > 0) {
|
|
257
|
+
renderPool(codex, codexReport, `codex (${count({ n: codexReport.accounts.length, noun: "account" })})`, Date.now(), cfg.policy.usagePollTtlMs);
|
|
258
|
+
}
|
|
402
259
|
return 0;
|
|
403
260
|
}
|
package/src/cli/switch.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { withLock } from "../lib/lock.ts";
|
|
2
|
-
import {
|
|
3
|
-
import { loadAccounts, loadConfig
|
|
4
|
-
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
5
|
-
import { isSkippableSwapError, performSwap } from "../lib/swap.ts";
|
|
2
|
+
import type { Provider } from "../lib/provider.ts";
|
|
3
|
+
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
6
4
|
import { currentWins, pickBest, pickEarliestReset, thresholdBars, weeklyExpiry, type PickCtx } from "../lib/picker.ts";
|
|
7
|
-
import { InvalidGrantError } from "../lib/oauth.ts";
|
|
8
|
-
import { gatedFamilies } from "../lib/usage.ts";
|
|
9
5
|
import { findAccount } from "./rename.ts";
|
|
10
6
|
import { log } from "../lib/log.ts";
|
|
11
7
|
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
12
8
|
import type { Account } from "../lib/types.ts";
|
|
13
9
|
|
|
14
|
-
export async function cmdSwitch(selector?: string, json = false): Promise<number> {
|
|
10
|
+
export async function cmdSwitch(p: Provider, selector?: string, json = false): Promise<number> {
|
|
15
11
|
const deadGrants: string[] = [];
|
|
16
12
|
const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
|
|
17
13
|
const emit = (text: string, report: Record<string, unknown>): void => {
|
|
@@ -22,56 +18,61 @@ export async function cmdSwitch(selector?: string, json = false): Promise<number
|
|
|
22
18
|
emitError({ json, message, paint: opts.paint, extra: withDeadGrants(opts.extra ?? {}) });
|
|
23
19
|
return 1;
|
|
24
20
|
};
|
|
25
|
-
const
|
|
21
|
+
const reauthHint = (a: Account) => `run \`tokenmaxxing auth${p.flag} ${a.label}\``;
|
|
22
|
+
const deadGrantMessage = (a: Account) => `${a.label}'s refresh token is dead - ${reauthHint(a)}`;
|
|
23
|
+
const switched = (target: Account) => `${c.green("↻")} switched to ${c.bold(target.label)}${p.switchNote}`;
|
|
26
24
|
|
|
27
|
-
const idx0 = loadAccounts();
|
|
28
|
-
if (idx0.accounts.length < 2) return fail(
|
|
25
|
+
const idx0 = loadAccounts(p.pool);
|
|
26
|
+
if (idx0.accounts.length < 2) return fail(`need at least 2 accounts to switch - add one with \`tokenmaxxing add${p.flag}\``, { paint: c.yellow });
|
|
29
27
|
const cfg = loadConfig();
|
|
30
28
|
const now = Date.now();
|
|
31
29
|
|
|
32
|
-
return withLock(
|
|
33
|
-
const idx = loadAccounts();
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
30
|
+
return withLock(p.pool.lockFile, async () => {
|
|
31
|
+
const idx = loadAccounts(p.pool);
|
|
32
|
+
const claimed = p.liveId();
|
|
33
|
+
const drifted = claimed != null && claimed !== idx.activeId;
|
|
34
|
+
const present = p.presentIds();
|
|
37
35
|
|
|
38
36
|
const swapTo = async (target: Account, reason: string, extra: Record<string, unknown> = {}): Promise<number> => {
|
|
39
37
|
try {
|
|
40
|
-
await
|
|
38
|
+
await p.swap(target);
|
|
41
39
|
} catch (e) {
|
|
42
|
-
if (e
|
|
40
|
+
if (p.classifySwapError(e) === "dead-grant") {
|
|
43
41
|
deadGrants.push(target.label);
|
|
44
42
|
return fail(deadGrantMessage(target));
|
|
45
43
|
}
|
|
46
44
|
throw e;
|
|
47
45
|
}
|
|
48
|
-
log("switch.manual", { account: target.
|
|
49
|
-
emit(
|
|
46
|
+
log("switch.manual", { account: target.id.slice(0, 8), reason });
|
|
47
|
+
emit(switched(target), { switched: true, account: target.label, reason, ...extra });
|
|
50
48
|
return 0;
|
|
51
49
|
};
|
|
52
50
|
|
|
53
51
|
if (selector) {
|
|
54
52
|
const target = findAccount(idx.accounts, selector);
|
|
55
53
|
if (!target) return fail(`no account matches "${selector}"`);
|
|
56
|
-
if (target.
|
|
54
|
+
if (target.id === idx.activeId && !drifted) {
|
|
57
55
|
emit(`already on ${c.bold(target.label)}`, { switched: false, account: target.label, reason: "already-on" });
|
|
58
56
|
return 0;
|
|
59
57
|
}
|
|
60
|
-
if (target.
|
|
58
|
+
if (target.id !== claimed && present.has(target.id)) {
|
|
59
|
+
return fail(`${target.label} is running in a live ${p.name} session - swapping onto it would break that session's credential`);
|
|
60
|
+
}
|
|
61
|
+
if (target.needsReauth) return fail(`${target.label} needs re-auth - ${reauthHint(target)}`);
|
|
61
62
|
return swapTo(target, "selected");
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
const
|
|
65
|
-
const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentAccountUuid: null, switchFamilies };
|
|
65
|
+
const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: p.gatedFamilies(cfg) };
|
|
66
66
|
const rejected = new Set<string>();
|
|
67
|
+
const candidatesOf = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (a.id === claimed || !present.has(a.id)));
|
|
67
68
|
while (true) {
|
|
68
|
-
const cur = loadAccounts();
|
|
69
|
-
const pool = cur.accounts
|
|
69
|
+
const cur = loadAccounts(p.pool);
|
|
70
|
+
const pool = candidatesOf(cur.accounts);
|
|
70
71
|
const active =
|
|
71
|
-
(claimed != null ? cur.accounts.find((a) => a.
|
|
72
|
-
cur.accounts.find((a) => a.
|
|
72
|
+
(claimed != null ? cur.accounts.find((a) => a.id === claimed) : null) ??
|
|
73
|
+
cur.accounts.find((a) => a.id === cur.activeId) ??
|
|
73
74
|
null;
|
|
74
|
-
if (active != null && currentWins(active, pool, everyone)) {
|
|
75
|
+
if (active != null && currentWins(active, pool, everyone, p.switchMargin)) {
|
|
75
76
|
if (drifted) return swapTo(active, "drift-reconciled");
|
|
76
77
|
const expiry = weeklyExpiry(active, now);
|
|
77
78
|
const why = Number.isFinite(expiry) ? ` (weekly ${fmtReset(expiry, now)})` : "";
|
|
@@ -83,41 +84,47 @@ export async function cmdSwitch(selector?: string, json = false): Promise<number
|
|
|
83
84
|
});
|
|
84
85
|
return 0;
|
|
85
86
|
}
|
|
86
|
-
const best = pickBest(pool, { ...everyone,
|
|
87
|
+
const best = pickBest(pool, { ...everyone, currentId: active?.id ?? null });
|
|
87
88
|
if (!best) break;
|
|
88
89
|
try {
|
|
89
|
-
await
|
|
90
|
+
await p.swap(best);
|
|
90
91
|
} catch (e) {
|
|
91
|
-
rejected.add(best.
|
|
92
|
-
|
|
92
|
+
rejected.add(best.id);
|
|
93
|
+
const kind = p.classifySwapError(e);
|
|
94
|
+
if (kind === "dead-grant") {
|
|
93
95
|
deadGrants.push(best.label);
|
|
94
96
|
if (!json) console.error(c.red(deadGrantMessage(best)));
|
|
95
97
|
continue;
|
|
96
98
|
}
|
|
97
|
-
if (
|
|
99
|
+
if (kind === "skip") {
|
|
98
100
|
if (!json) console.error(c.yellow(`${best.label}: ${e instanceof Error ? e.message : String(e)} - skipped for this run`));
|
|
99
101
|
continue;
|
|
100
102
|
}
|
|
101
103
|
throw e;
|
|
102
104
|
}
|
|
103
|
-
log("switch.manual", { account: best.
|
|
104
|
-
emit(
|
|
105
|
+
log("switch.manual", { account: best.id.slice(0, 8), reason: "best" });
|
|
106
|
+
emit(switched(best), { switched: true, account: best.label, reason: "best" });
|
|
105
107
|
return 0;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
if (!p.waitsWhenDepleted) {
|
|
111
|
+
const reauth = loadAccounts(p.pool).accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
112
|
+
return fail(`no usable ${p.name} switch target (all at their bars, unmeasured, or needing reauth)`, { paint: c.yellow, extra: { reauthNeeded: reauth } });
|
|
113
|
+
}
|
|
114
|
+
|
|
108
115
|
while (true) {
|
|
109
|
-
const fresh = loadAccounts();
|
|
110
|
-
const pool = fresh.accounts
|
|
116
|
+
const fresh = loadAccounts(p.pool);
|
|
117
|
+
const pool = candidatesOf(fresh.accounts);
|
|
111
118
|
const earliest = pickEarliestReset(pool, everyone);
|
|
112
119
|
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
113
120
|
if (!earliest) {
|
|
114
121
|
if (reauth.length > 0) {
|
|
115
|
-
return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth --all\`): ${reauth.join(", ")}`, {
|
|
122
|
+
return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth${p.flag} --all\`): ${reauth.join(", ")}`, {
|
|
116
123
|
paint: c.yellow,
|
|
117
124
|
extra: { reauthNeeded: reauth },
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
|
-
const freshActive = fresh.accounts.find((a) => a.
|
|
127
|
+
const freshActive = fresh.accounts.find((a) => a.id === fresh.activeId) ?? null;
|
|
121
128
|
if (drifted && freshActive) return swapTo(freshActive, "drift-reconciled");
|
|
122
129
|
emit(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"), {
|
|
123
130
|
switched: false,
|
|
@@ -128,7 +135,7 @@ export async function cmdSwitch(selector?: string, json = false): Promise<number
|
|
|
128
135
|
}
|
|
129
136
|
const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
|
|
130
137
|
const availableAt = earliest.availableAt > now ? earliest.availableAt : null;
|
|
131
|
-
if (earliest.account.
|
|
138
|
+
if (earliest.account.id === fresh.activeId && !drifted) {
|
|
132
139
|
const msg = availableAt == null
|
|
133
140
|
? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
|
|
134
141
|
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(availableAt, now)})${reauthNote}`;
|
|
@@ -142,22 +149,23 @@ export async function cmdSwitch(selector?: string, json = false): Promise<number
|
|
|
142
149
|
return 0;
|
|
143
150
|
}
|
|
144
151
|
try {
|
|
145
|
-
await
|
|
152
|
+
await p.swap(earliest.account);
|
|
146
153
|
} catch (e) {
|
|
147
|
-
rejected.add(earliest.account.
|
|
148
|
-
|
|
154
|
+
rejected.add(earliest.account.id);
|
|
155
|
+
const kind = p.classifySwapError(e);
|
|
156
|
+
if (kind === "dead-grant") {
|
|
149
157
|
deadGrants.push(earliest.account.label);
|
|
150
158
|
if (!json) console.error(c.red(deadGrantMessage(earliest.account)));
|
|
151
159
|
continue;
|
|
152
160
|
}
|
|
153
|
-
if (
|
|
161
|
+
if (kind === "skip") {
|
|
154
162
|
if (!json) console.error(c.yellow(`${earliest.account.label}: ${e instanceof Error ? e.message : String(e)} - skipped for this run`));
|
|
155
163
|
continue;
|
|
156
164
|
}
|
|
157
165
|
throw e;
|
|
158
166
|
}
|
|
159
|
-
log("switch.manual", { account: earliest.account.
|
|
160
|
-
emit(
|
|
167
|
+
log("switch.manual", { account: earliest.account.id.slice(0, 8), reason: "earliest-reset" });
|
|
168
|
+
emit(switched(earliest.account), { switched: true, account: earliest.account.label, reason: "earliest-reset", availableAt, reauthNeeded: reauth });
|
|
161
169
|
if (availableAt != null && !json) {
|
|
162
170
|
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(availableAt, now)})${reauthNote}`));
|
|
163
171
|
}
|