tokenmaxxing 1.9.1 → 1.11.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 -3
- package/README.md +13 -11
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/pool-status/SKILL.md +2 -2
- package/agent-plugin/skills/pool-status/references/commands.md +1 -0
- package/agent-plugin/skills/safe-contribution/SKILL.md +3 -4
- package/agent-plugin/skills/sdk-pairing/SKILL.md +2 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +2 -2
- package/agent-plugin/skills/switching-policy/references/policy.md +2 -2
- package/package.json +2 -3
- package/src/cli/check.ts +17 -4
- package/src/cli/codexrm.ts +8 -7
- package/src/cli/codexswitch.ts +50 -20
- package/src/cli/config.ts +86 -42
- package/src/cli/doctor.ts +25 -9
- package/src/cli/ls.ts +37 -2
- package/src/cli/rename.ts +17 -14
- package/src/cli/render.ts +20 -0
- package/src/cli/rm.ts +12 -8
- package/src/cli/status.ts +243 -112
- package/src/cli/switch.ts +75 -31
- package/src/cli/watch.ts +13 -7
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/mcp.ts +1 -1
- package/src/lib/codexdecide.ts +2 -2
- package/src/lib/decide.ts +54 -21
- package/src/lib/picker.ts +15 -2
- package/src/lib/state.ts +3 -2
- package/src/lib/types.ts +18 -5
- package/src/main.ts +44 -22
package/src/cli/status.ts
CHANGED
|
@@ -1,50 +1,103 @@
|
|
|
1
1
|
import { sortBy } from "es-toolkit";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { loadAccounts, loadConfig, loadUsage, loadModelUsage, saveAccounts } from "../lib/state.ts";
|
|
3
4
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
4
5
|
import { ensureLiveTokenFresh, probeActiveUsage, probeParkedUsage, type SampleOutcome } from "../lib/sample.ts";
|
|
5
6
|
import { withLock } from "../lib/lock.ts";
|
|
6
7
|
import { codexPaths, paths } from "../lib/paths.ts";
|
|
7
|
-
import { earliestReset, effectiveBars, isExhausted, nextWeeklyReset } from "../lib/picker.ts";
|
|
8
|
+
import { earliestReset, effectiveBars, isExhausted, nextWeeklyReset, terminalBars } from "../lib/picker.ts";
|
|
8
9
|
import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
|
|
9
10
|
import { liveCodexAccountId, sampleCodexAccount, type CodexSampleOutcome } from "../lib/codexsample.ts";
|
|
10
11
|
import { isCodexExhausted } from "../lib/codexpick.ts";
|
|
11
12
|
import { codexLimitLabel, isSessionWindow } from "../lib/codexusage.ts";
|
|
12
|
-
import { bar, c, claudeTierLabel, count, fmtAgo, fmtReset } from "./render.ts";
|
|
13
|
-
import type
|
|
14
|
-
import
|
|
13
|
+
import { bar, c, claudeTierLabel, count, emitJson, fmtAgo, fmtReset } from "./render.ts";
|
|
14
|
+
import { gatedFamilies, type FullUsage } from "../lib/usage.ts";
|
|
15
|
+
import { ThresholdsSchema, UsageWindowSchema, type Account, type CodexWindow, type Config, type UsageWindow } from "../lib/types.ts";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const SampleReportSchema = z.discriminatedUnion("ok", [
|
|
18
|
+
z.object({ ok: z.literal(true), source: z.enum(["statusline", "probe"]) }),
|
|
19
|
+
z.object({ ok: z.literal(false), reason: z.string() }),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const ClaudeStatusAccountSchema = z.object({
|
|
23
|
+
label: z.string(),
|
|
24
|
+
email: z.string(),
|
|
25
|
+
accountUuid: z.string(),
|
|
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
|
+
sample: SampleReportSchema,
|
|
35
|
+
pingError: z.string().nullable(),
|
|
36
|
+
pinged: z.boolean(),
|
|
37
|
+
});
|
|
38
|
+
type ClaudeStatusAccount = z.infer<typeof ClaudeStatusAccountSchema>;
|
|
39
|
+
|
|
40
|
+
const CodexWindowReportSchema = UsageWindowSchema.extend({ windowSeconds: z.number().nullable() });
|
|
20
41
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
const CodexStatusAccountSchema = z.object({
|
|
43
|
+
label: z.string(),
|
|
44
|
+
email: z.string().nullable(),
|
|
45
|
+
accountId: z.string(),
|
|
46
|
+
planType: z.string().nullable(),
|
|
47
|
+
active: z.boolean(),
|
|
48
|
+
needsReauth: z.boolean(),
|
|
49
|
+
exhausted: z.boolean(),
|
|
50
|
+
usage: z
|
|
51
|
+
.object({
|
|
52
|
+
aggregate: z.array(CodexWindowReportSchema),
|
|
53
|
+
perLimit: z.record(z.string(), z.array(CodexWindowReportSchema)),
|
|
54
|
+
})
|
|
55
|
+
.nullable(),
|
|
56
|
+
usageAt: z.number().nullable(),
|
|
57
|
+
sample: SampleReportSchema,
|
|
58
|
+
});
|
|
59
|
+
type CodexStatusAccount = z.infer<typeof CodexStatusAccountSchema>;
|
|
60
|
+
|
|
61
|
+
const StatusReportSchema = z.object({
|
|
62
|
+
now: z.number(),
|
|
63
|
+
claude: z.object({
|
|
64
|
+
thresholds: z.object({ session: z.array(z.number()), weekly: z.number() }),
|
|
65
|
+
bars: ThresholdsSchema,
|
|
66
|
+
projectionMargin: z.number(),
|
|
67
|
+
accounts: z.array(ClaudeStatusAccountSchema),
|
|
68
|
+
}),
|
|
69
|
+
codex: z.object({
|
|
70
|
+
bars: ThresholdsSchema,
|
|
71
|
+
accounts: z.array(CodexStatusAccountSchema),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
export type StatusReport = z.infer<typeof StatusReportSchema>;
|
|
75
|
+
|
|
76
|
+
function currentWindow(w: UsageWindow, weekly: boolean, now: number): UsageWindow {
|
|
77
|
+
const passed = w.resetsAt != null && w.resetsAt <= now;
|
|
78
|
+
return {
|
|
79
|
+
usedPercentage: passed ? 0 : w.usedPercentage,
|
|
80
|
+
resetsAt: weekly ? nextWeeklyReset(w.resetsAt, now) : passed ? null : w.resetsAt,
|
|
26
81
|
};
|
|
82
|
+
}
|
|
27
83
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log();
|
|
32
|
-
await renderCodexSection({ cfg, now, row });
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
84
|
+
function currentCodexWindow(w: CodexWindow, now: number): CodexWindow {
|
|
85
|
+
return { ...currentWindow(w, !isSessionWindow({ window: w }), now), windowSeconds: w.windowSeconds };
|
|
86
|
+
}
|
|
38
87
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
async function collectClaude(input: { cfg: Config; force: boolean; now: number }): Promise<StatusReport["claude"]> {
|
|
89
|
+
const { cfg, force, now } = input;
|
|
90
|
+
let idx = loadAccounts();
|
|
91
|
+
const samples = new Map<string, { outcome: SampleOutcome; viaTee: boolean }>();
|
|
92
|
+
if (idx.accounts.length > 0) {
|
|
93
|
+
console.error(c.dim(force ? "pinging every account (starts each 5h session timer) + sampling live usage..." : "sampling live usage..."));
|
|
94
|
+
await withLock(paths.lockFile, async () => {
|
|
95
|
+
idx = loadAccounts();
|
|
96
|
+
const live = loadUsage();
|
|
97
|
+
const modelUsage = loadModelUsage();
|
|
98
|
+
const liveOAuth = readOAuthAccount();
|
|
99
|
+
const activeOrg = liveOAuth?.organizationUuid ?? null;
|
|
100
|
+
const probeOne = async (a: Account) => {
|
|
48
101
|
const isActive = activeOrg != null && activeOrg === a.organizationUuid;
|
|
49
102
|
if (isActive && liveOAuth?.organizationRateLimitTier != null) a.rateLimitTier = liveOAuth.organizationRateLimitTier;
|
|
50
103
|
const fromStatusLine: FullUsage | null =
|
|
@@ -73,7 +126,7 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
|
|
|
73
126
|
? await probeActiveUsage(a)
|
|
74
127
|
: await probeParkedUsage(a);
|
|
75
128
|
}
|
|
76
|
-
|
|
129
|
+
samples.set(a.accountUuid, { outcome, viaTee });
|
|
77
130
|
if (!outcome.ok) return;
|
|
78
131
|
a.lastUsage = { fiveHour: outcome.usage.session, sevenDay: outcome.usage.weekAll };
|
|
79
132
|
a.lastUsageAt = viaTee && live ? live.ts : Date.now();
|
|
@@ -81,70 +134,57 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
|
|
|
81
134
|
a.lastPerModel = outcome.usage.perModel;
|
|
82
135
|
a.lastPerModelAt = viaTee && modelUsage ? (modelUsage.sampledAt ?? modelUsage.ts) : a.lastUsageAt;
|
|
83
136
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
preRender?.();
|
|
96
|
-
console.log(c.dim(`thresholds 5h ${cfg.thresholds.session}% weekly ${cfg.thresholds.weekly}% (${count({ n: idx.accounts.length, noun: "claude account" })})`));
|
|
97
|
-
console.log();
|
|
98
|
-
|
|
99
|
-
const displayAccounts = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
100
|
-
const displayActiveOrg = readOAuthAccount()?.organizationUuid ?? null;
|
|
101
|
-
for (const a of displayAccounts) {
|
|
102
|
-
const active = displayActiveOrg != null && a.organizationUuid === displayActiveOrg;
|
|
103
|
-
const outcome = outcomes.get(a.accountUuid);
|
|
104
|
-
const failed = outcome ? !outcome.ok : false;
|
|
105
|
-
const usage = outcome?.ok ? outcome.usage : undefined;
|
|
106
|
-
const aggregate = usage ? { fiveHour: usage.session, sevenDay: usage.weekAll } : a.lastUsage;
|
|
107
|
-
const perModel = usage ? usage.perModel : a.lastPerModel;
|
|
108
|
-
|
|
109
|
-
const marker = active ? c.green("●") : c.dim("○");
|
|
110
|
-
const badges: string[] = [];
|
|
111
|
-
if (active) badges.push(c.green("active"));
|
|
112
|
-
if (a.needsReauth) badges.push(c.red("needs-reauth"));
|
|
113
|
-
if (isExhausted(a, { now, thresholds: effectiveBars(cfg), currentAccountUuid: idx.activeAccountUuid, switchFamilies: cfg.policy.switchModels }))
|
|
114
|
-
badges.push(c.yellow("exhausted"));
|
|
115
|
-
|
|
116
|
-
const tier = claudeTierLabel(a);
|
|
117
|
-
console.log(`${marker} ${c.bold(a.label || a.email)}${tier ? ` ${c.dim(tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
118
|
-
if (aggregate) {
|
|
119
|
-
row("5h", aggregate.fiveHour, false);
|
|
120
|
-
row("week", aggregate.sevenDay, true);
|
|
121
|
-
}
|
|
122
|
-
if (perModel) for (const [name, w] of Object.entries(perModel)) row(name.toLowerCase(), w, true);
|
|
123
|
-
if (failed && outcome && !outcome.ok) {
|
|
124
|
-
const cached = aggregate || perModel ? `cached${a.lastUsageAt != null ? ` ${fmtAgo(a.lastUsageAt, now)}` : ""}, ` : "";
|
|
125
|
-
console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(outcome.reason)}`);
|
|
126
|
-
}
|
|
127
|
-
if (outcome?.pingError != null) {
|
|
128
|
-
console.log(` ${c.yellow("ping failed (5h timer may not have started)")}: ${c.dim(outcome.pingError)}`);
|
|
129
|
-
}
|
|
130
|
-
if (force && outcome?.ok && outcome.pingError == null && aggregate && aggregate.fiveHour.resetsAt == null) {
|
|
131
|
-
console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
|
|
132
|
-
}
|
|
133
|
-
console.log();
|
|
137
|
+
};
|
|
138
|
+
const activeAccount = idx.accounts.find((a) => activeOrg != null && activeOrg === a.organizationUuid) ?? null;
|
|
139
|
+
if (activeAccount) await probeOne(activeAccount);
|
|
140
|
+
try {
|
|
141
|
+
await ensureLiveTokenFresh();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
await Promise.all(idx.accounts.filter((a) => a !== activeAccount).map(probeOne));
|
|
145
|
+
saveAccounts(idx);
|
|
146
|
+
});
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const families = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
150
|
+
const bars = effectiveBars(cfg, { accounts: idx.accounts, now, switchFamilies: families });
|
|
151
|
+
const activeOrg = readOAuthAccount()?.organizationUuid ?? null;
|
|
152
|
+
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
153
|
+
const accounts = ordered.map((a): ClaudeStatusAccount => {
|
|
154
|
+
const sampled = samples.get(a.accountUuid) ?? { outcome: { ok: false, reason: "not sampled" }, viaTee: false };
|
|
155
|
+
const usage = sampled.outcome.ok ? sampled.outcome.usage : undefined;
|
|
156
|
+
const aggregate = usage ? { fiveHour: usage.session, sevenDay: usage.weekAll } : a.lastUsage;
|
|
157
|
+
const perModel = usage ? usage.perModel : (a.lastPerModel ?? {});
|
|
158
|
+
return {
|
|
159
|
+
label: a.label,
|
|
160
|
+
email: a.email,
|
|
161
|
+
accountUuid: a.accountUuid,
|
|
162
|
+
organizationUuid: a.organizationUuid,
|
|
163
|
+
tier: claudeTierLabel(a),
|
|
164
|
+
active: activeOrg != null && a.organizationUuid === activeOrg,
|
|
165
|
+
needsReauth: a.needsReauth === true,
|
|
166
|
+
exhausted: isExhausted(a, { now, thresholds: bars, currentAccountUuid: idx.activeAccountUuid, switchFamilies: families }),
|
|
167
|
+
usage: aggregate ? { fiveHour: currentWindow(aggregate.fiveHour, false, now), week: currentWindow(aggregate.sevenDay, true, now) } : null,
|
|
168
|
+
perModel: Object.fromEntries(Object.entries(perModel).map(([name, w]) => [name, currentWindow(w, true, now)])),
|
|
169
|
+
usageAt: a.lastUsageAt ?? null,
|
|
170
|
+
sample: sampled.outcome.ok ? { ok: true, source: sampled.viaTee ? "statusline" : "probe" } : { ok: false, reason: sampled.outcome.reason },
|
|
171
|
+
pingError: sampled.outcome.pingError ?? null,
|
|
172
|
+
pinged: force && sampled.outcome.ok && sampled.outcome.pingError == null && aggregate != null && aggregate.fiveHour.resetsAt == null,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
thresholds: { session: cfg.thresholds.session, weekly: cfg.thresholds.weekly },
|
|
177
|
+
bars,
|
|
178
|
+
projectionMargin: cfg.policy.projectionMargin,
|
|
179
|
+
accounts,
|
|
180
|
+
};
|
|
138
181
|
}
|
|
139
182
|
|
|
140
|
-
async function
|
|
141
|
-
cfg
|
|
142
|
-
|
|
143
|
-
row: (name: string, w: UsageWindow, weekly: boolean) => void;
|
|
144
|
-
}): Promise<void> {
|
|
145
|
-
const { cfg, now, row } = input;
|
|
183
|
+
async function collectCodex(input: { cfg: Config; now: number }): Promise<StatusReport["codex"]> {
|
|
184
|
+
const { cfg, now } = input;
|
|
185
|
+
const bars = terminalBars(cfg);
|
|
146
186
|
let index = loadCodexAccounts();
|
|
147
|
-
if (index.accounts.length === 0) return;
|
|
187
|
+
if (index.accounts.length === 0) return { bars, accounts: [] };
|
|
148
188
|
|
|
149
189
|
console.error(c.dim("sampling codex usage..."));
|
|
150
190
|
const outcomes = new Map<string, CodexSampleOutcome>();
|
|
@@ -169,11 +209,7 @@ async function renderCodexSection(input: {
|
|
|
169
209
|
saveCodexAccounts({ index });
|
|
170
210
|
});
|
|
171
211
|
|
|
172
|
-
|
|
173
|
-
console.log();
|
|
174
|
-
const windowLabel = (window: CodexWindow) =>
|
|
175
|
-
isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
|
|
176
|
-
const displayAccounts = sortBy(index.accounts, [
|
|
212
|
+
const ordered = sortBy(index.accounts, [
|
|
177
213
|
(a) => (a.needsReauth ? 1 : 0),
|
|
178
214
|
(a) => {
|
|
179
215
|
const windows = [...(a.lastUsage?.aggregate ?? []), ...Object.values(a.lastUsage?.perLimit ?? {}).flat()];
|
|
@@ -181,27 +217,122 @@ async function renderCodexSection(input: {
|
|
|
181
217
|
return resets.length > 0 ? Math.min(...resets) : Number.POSITIVE_INFINITY;
|
|
182
218
|
},
|
|
183
219
|
]);
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const
|
|
220
|
+
const accounts = ordered.map((account): CodexStatusAccount => {
|
|
221
|
+
const outcome = outcomes.get(account.accountId) ?? { ok: false, reason: "not sampled", deadGrant: false };
|
|
222
|
+
const usage = account.lastUsage;
|
|
223
|
+
return {
|
|
224
|
+
label: account.label,
|
|
225
|
+
email: account.email,
|
|
226
|
+
accountId: account.accountId,
|
|
227
|
+
planType: account.planType,
|
|
228
|
+
active: account.accountId === liveId,
|
|
229
|
+
needsReauth: account.needsReauth === true,
|
|
230
|
+
exhausted: isCodexExhausted({ account, thresholds: bars, now }),
|
|
231
|
+
usage: usage
|
|
232
|
+
? {
|
|
233
|
+
aggregate: usage.aggregate.map((w) => currentCodexWindow(w, now)),
|
|
234
|
+
perLimit: Object.fromEntries(
|
|
235
|
+
Object.entries(usage.perLimit).map(([name, windows]) => [name, windows.map((w) => currentCodexWindow(w, now))]),
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
: null,
|
|
239
|
+
usageAt: account.lastUsageAt ?? null,
|
|
240
|
+
sample: outcome.ok ? { ok: true, source: "probe" } : { ok: false, reason: outcome.reason },
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
return { bars, accounts };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function renderCodex(input: { codex: StatusReport["codex"]; now: number; row: (name: string, w: UsageWindow) => void }): void {
|
|
247
|
+
const { codex, now, row } = input;
|
|
248
|
+
if (codex.accounts.length === 0) return;
|
|
249
|
+
console.log(c.dim(`codex (${count({ n: codex.accounts.length, noun: "account" })})`));
|
|
250
|
+
console.log();
|
|
251
|
+
const windowLabel = (window: CodexWindow) =>
|
|
252
|
+
isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
|
|
253
|
+
for (const account of codex.accounts) {
|
|
254
|
+
const marker = account.active ? c.green("●") : c.dim("○");
|
|
187
255
|
const badges: string[] = [];
|
|
188
|
-
if (active) badges.push(c.green("active"));
|
|
256
|
+
if (account.active) badges.push(c.green("active"));
|
|
189
257
|
if (account.needsReauth) badges.push(c.red("needs-reauth"));
|
|
190
|
-
if (
|
|
258
|
+
if (account.exhausted) badges.push(c.yellow("exhausted"));
|
|
191
259
|
console.log(`${marker} ${c.bold(account.label)}${account.planType ? ` ${c.dim(account.planType)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
for (const [name, windows] of Object.entries(usage.perLimit)) {
|
|
197
|
-
for (const window of windows) row(codexLimitLabel({ limitName: name }), window, !isSessionWindow({ window }));
|
|
260
|
+
if (account.usage) {
|
|
261
|
+
for (const window of account.usage.aggregate) row(windowLabel(window), window);
|
|
262
|
+
for (const [name, windows] of Object.entries(account.usage.perLimit)) {
|
|
263
|
+
for (const window of windows) row(codexLimitLabel({ limitName: name }), window);
|
|
198
264
|
}
|
|
199
265
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
if (!account.sample.ok) {
|
|
267
|
+
const cached = account.usage ? `cached${account.usageAt != null ? ` ${fmtAgo(account.usageAt, now)}` : ""}, ` : "";
|
|
268
|
+
console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(account.sample.reason)}`);
|
|
269
|
+
}
|
|
270
|
+
console.log();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function rowPrinter(now: number): (name: string, w: UsageWindow) => void {
|
|
275
|
+
return (name, w) => {
|
|
276
|
+
console.log(` ${name.padEnd(5)} ${bar(w.usedPercentage)} ${c.dim(fmtReset(w.resetsAt, now))}`);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderClaude(input: { claude: StatusReport["claude"]; codexPooled: boolean; now: number; row: (name: string, w: UsageWindow) => void }): void {
|
|
281
|
+
const { claude, codexPooled, now, row } = input;
|
|
282
|
+
if (claude.accounts.length === 0) {
|
|
283
|
+
if (!codexPooled) {
|
|
284
|
+
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`)"));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
288
|
+
console.log();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
console.log(c.dim(`thresholds 5h ${claude.thresholds.session.join("/")}% (at ${claude.bars.session + claude.projectionMargin}%) weekly ${claude.thresholds.weekly}% (${count({ n: claude.accounts.length, noun: "claude account" })})`));
|
|
293
|
+
console.log();
|
|
294
|
+
for (const a of claude.accounts) {
|
|
295
|
+
const marker = a.active ? c.green("●") : c.dim("○");
|
|
296
|
+
const badges: string[] = [];
|
|
297
|
+
if (a.active) badges.push(c.green("active"));
|
|
298
|
+
if (a.needsReauth) badges.push(c.red("needs-reauth"));
|
|
299
|
+
if (a.exhausted) badges.push(c.yellow("exhausted"));
|
|
300
|
+
console.log(`${marker} ${c.bold(a.label || a.email)}${a.tier ? ` ${c.dim(a.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
301
|
+
if (a.usage) {
|
|
302
|
+
row("5h", a.usage.fiveHour);
|
|
303
|
+
row("week", a.usage.week);
|
|
304
|
+
}
|
|
305
|
+
for (const [name, w] of Object.entries(a.perModel)) row(name.toLowerCase(), w);
|
|
306
|
+
if (!a.sample.ok) {
|
|
307
|
+
const cached = a.usage || Object.keys(a.perModel).length > 0 ? `cached${a.usageAt != null ? ` ${fmtAgo(a.usageAt, now)}` : ""}, ` : "";
|
|
308
|
+
console.log(` ${c.yellow(`${cached}live sample failed`)}: ${c.dim(a.sample.reason)}`);
|
|
309
|
+
}
|
|
310
|
+
if (a.pingError != null) {
|
|
311
|
+
console.log(` ${c.yellow("ping failed (5h timer may not have started)")}: ${c.dim(a.pingError)}`);
|
|
312
|
+
}
|
|
313
|
+
if (a.pinged) {
|
|
314
|
+
console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
|
|
204
315
|
}
|
|
205
316
|
console.log();
|
|
206
317
|
}
|
|
207
318
|
}
|
|
319
|
+
|
|
320
|
+
export async function cmdStatus(opts: { force?: boolean; json?: boolean; preRender?: () => void } = {}): Promise<number> {
|
|
321
|
+
const { force = false, json = false } = opts;
|
|
322
|
+
const cfg = loadConfig();
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const row = rowPrinter(now);
|
|
325
|
+
const claude = await collectClaude({ cfg, force, now });
|
|
326
|
+
if (!json) {
|
|
327
|
+
opts.preRender?.();
|
|
328
|
+
renderClaude({ claude, codexPooled: loadCodexAccounts().accounts.length > 0, now, row });
|
|
329
|
+
}
|
|
330
|
+
const codex = await collectCodex({ cfg, now });
|
|
331
|
+
if (json) {
|
|
332
|
+
const report: StatusReport = { now, claude, codex };
|
|
333
|
+
emitJson({ ok: true, ...report });
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
renderCodex({ codex, now, row });
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
package/src/cli/switch.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { withLock } from "../lib/lock.ts";
|
|
2
2
|
import { paths } from "../lib/paths.ts";
|
|
3
|
-
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
3
|
+
import { loadAccounts, loadConfig, loadUsage } from "../lib/state.ts";
|
|
4
4
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
5
5
|
import { performSwap } from "../lib/swap.ts";
|
|
6
6
|
import { currentWins, effectiveBars, pickBest, pickEarliestReset, weeklyExpiry, type PickCtx } from "../lib/picker.ts";
|
|
7
7
|
import { InvalidGrantError } from "../lib/oauth.ts";
|
|
8
|
+
import { gatedFamilies } from "../lib/usage.ts";
|
|
8
9
|
import { findAccount } from "./rename.ts";
|
|
9
|
-
import { c, fmtReset } from "./render.ts";
|
|
10
|
+
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
10
11
|
import type { Account } from "../lib/types.ts";
|
|
11
12
|
|
|
12
|
-
export async function cmdSwitch(selector?: string): Promise<number> {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
export async function cmdSwitch(selector?: string, json = false): Promise<number> {
|
|
14
|
+
const deadGrants: string[] = [];
|
|
15
|
+
const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
|
|
16
|
+
const emit = (text: string, report: Record<string, unknown>): void => {
|
|
17
|
+
if (json) emitJson({ ok: true, ...withDeadGrants(report) });
|
|
18
|
+
else console.log(text);
|
|
19
|
+
};
|
|
20
|
+
const fail = (message: string, opts: { paint?: (s: string) => string; extra?: Record<string, unknown> } = {}): number => {
|
|
21
|
+
emitError({ json, message, paint: opts.paint, extra: withDeadGrants(opts.extra ?? {}) });
|
|
16
22
|
return 1;
|
|
17
|
-
}
|
|
23
|
+
};
|
|
24
|
+
const deadGrantMessage = (a: Account) => `${a.label}'s refresh token is dead - run \`tokenmaxxing auth ${a.label}\``;
|
|
25
|
+
|
|
26
|
+
const idx0 = loadAccounts();
|
|
27
|
+
if (idx0.accounts.length < 2) return fail("need at least 2 accounts to switch - add one with `tokenmaxxing add`", { paint: c.yellow });
|
|
18
28
|
const cfg = loadConfig();
|
|
19
29
|
const now = Date.now();
|
|
20
30
|
|
|
@@ -25,37 +35,55 @@ export async function cmdSwitch(selector?: string): Promise<number> {
|
|
|
25
35
|
const claimedOrg = liveClaim?.organizationUuid ?? null;
|
|
26
36
|
const drifted = claimed != null && claimed !== idx.activeAccountUuid;
|
|
27
37
|
|
|
28
|
-
const swapTo = async (target: Account): Promise<number> => {
|
|
38
|
+
const swapTo = async (target: Account, reason: string, extra: Record<string, unknown> = {}): Promise<number> => {
|
|
29
39
|
try {
|
|
30
40
|
await performSwap(target);
|
|
31
41
|
} catch (e) {
|
|
32
|
-
if (e instanceof InvalidGrantError) {
|
|
42
|
+
if (e instanceof InvalidGrantError) {
|
|
43
|
+
deadGrants.push(target.label);
|
|
44
|
+
return fail(deadGrantMessage(target));
|
|
45
|
+
}
|
|
33
46
|
throw e;
|
|
34
47
|
}
|
|
35
|
-
|
|
48
|
+
emit(`${c.green("↻")} switched to ${c.bold(target.label)}`, { switched: true, account: target.label, reason, ...extra });
|
|
36
49
|
return 0;
|
|
37
50
|
};
|
|
38
51
|
|
|
39
52
|
if (selector) {
|
|
40
53
|
const target = findAccount(idx.accounts, selector);
|
|
41
|
-
if (!target)
|
|
42
|
-
if (target.accountUuid === idx.activeAccountUuid && !drifted) {
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
if (!target) return fail(`no account matches "${selector}"`);
|
|
55
|
+
if (target.accountUuid === idx.activeAccountUuid && !drifted) {
|
|
56
|
+
emit(`already on ${c.bold(target.label)}`, { switched: false, account: target.label, reason: "already-on" });
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (target.needsReauth) return fail(`${target.label} needs re-auth - run \`tokenmaxxing auth ${target.label}\``);
|
|
60
|
+
return swapTo(target, "selected");
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
const
|
|
63
|
+
const switchFamilies = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
64
|
+
const everyoneIn = (accounts: Account[]): PickCtx => ({
|
|
65
|
+
now,
|
|
66
|
+
thresholds: effectiveBars(cfg, { accounts, now, switchFamilies }),
|
|
67
|
+
currentAccountUuid: null,
|
|
68
|
+
switchFamilies,
|
|
69
|
+
});
|
|
48
70
|
while (true) {
|
|
49
71
|
const cur = loadAccounts();
|
|
72
|
+
const everyone = everyoneIn(cur.accounts);
|
|
50
73
|
const active =
|
|
51
74
|
(claimedOrg != null ? cur.accounts.find((a) => a.organizationUuid === claimedOrg) : null) ??
|
|
52
75
|
cur.accounts.find((a) => a.accountUuid === cur.activeAccountUuid) ??
|
|
53
76
|
null;
|
|
54
77
|
if (active != null && currentWins(active, cur.accounts, everyone)) {
|
|
55
|
-
if (drifted) return swapTo(active);
|
|
78
|
+
if (drifted) return swapTo(active, "drift-reconciled");
|
|
56
79
|
const expiry = weeklyExpiry(active, now);
|
|
57
80
|
const why = Number.isFinite(expiry) ? ` (weekly ${fmtReset(expiry, now)})` : "";
|
|
58
|
-
|
|
81
|
+
emit(`already on the best account: ${c.bold(active.label)}${why}`, {
|
|
82
|
+
switched: false,
|
|
83
|
+
account: active.label,
|
|
84
|
+
reason: "current-wins",
|
|
85
|
+
weeklyResetsAt: Number.isFinite(expiry) ? expiry : null,
|
|
86
|
+
});
|
|
59
87
|
return 0;
|
|
60
88
|
}
|
|
61
89
|
const best = pickBest(cur.accounts, { ...everyone, currentAccountUuid: active?.accountUuid ?? null });
|
|
@@ -64,37 +92,53 @@ export async function cmdSwitch(selector?: string): Promise<number> {
|
|
|
64
92
|
await performSwap(best);
|
|
65
93
|
} catch (e) {
|
|
66
94
|
if (e instanceof InvalidGrantError) {
|
|
67
|
-
|
|
95
|
+
deadGrants.push(best.label);
|
|
96
|
+
if (!json) console.error(c.red(deadGrantMessage(best)));
|
|
68
97
|
continue;
|
|
69
98
|
}
|
|
70
99
|
throw e;
|
|
71
100
|
}
|
|
72
|
-
|
|
101
|
+
emit(`${c.green("↻")} switched to ${c.bold(best.label)}`, { switched: true, account: best.label, reason: "best" });
|
|
73
102
|
return 0;
|
|
74
103
|
}
|
|
75
104
|
|
|
76
105
|
const fresh = loadAccounts();
|
|
77
|
-
const earliest = pickEarliestReset(fresh.accounts,
|
|
106
|
+
const earliest = pickEarliestReset(fresh.accounts, everyoneIn(fresh.accounts));
|
|
107
|
+
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
78
108
|
if (!earliest) {
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
if (reauth.length > 0) {
|
|
110
|
+
return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth --all\`): ${reauth.join(", ")}`, {
|
|
111
|
+
paint: c.yellow,
|
|
112
|
+
extra: { reauthNeeded: reauth },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
81
115
|
const freshActive = fresh.accounts.find((a) => a.accountUuid === fresh.activeAccountUuid) ?? null;
|
|
82
|
-
if (drifted && freshActive) return swapTo(freshActive);
|
|
83
|
-
|
|
116
|
+
if (drifted && freshActive) return swapTo(freshActive, "drift-reconciled");
|
|
117
|
+
emit(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"), {
|
|
118
|
+
switched: false,
|
|
119
|
+
account: freshActive?.label ?? null,
|
|
120
|
+
reason: "unknown-resets",
|
|
121
|
+
});
|
|
84
122
|
return 0;
|
|
85
123
|
}
|
|
86
|
-
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
87
124
|
const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
|
|
125
|
+
const availableAt = earliest.availableAt > now ? earliest.availableAt : null;
|
|
88
126
|
if (earliest.account.accountUuid === fresh.activeAccountUuid && !drifted) {
|
|
89
|
-
const msg =
|
|
127
|
+
const msg = availableAt == null
|
|
90
128
|
? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
|
|
91
|
-
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(
|
|
92
|
-
|
|
129
|
+
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(availableAt, now)})${reauthNote}`;
|
|
130
|
+
emit(c.yellow(msg), {
|
|
131
|
+
switched: false,
|
|
132
|
+
account: earliest.account.label,
|
|
133
|
+
reason: availableAt == null ? "no-target" : "all-at-limit",
|
|
134
|
+
availableAt,
|
|
135
|
+
reauthNeeded: reauth,
|
|
136
|
+
});
|
|
93
137
|
return 0;
|
|
94
138
|
}
|
|
95
|
-
const code = await swapTo(earliest.account);
|
|
96
|
-
if (code === 0 &&
|
|
97
|
-
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(
|
|
139
|
+
const code = await swapTo(earliest.account, "earliest-reset", { availableAt, reauthNeeded: reauth });
|
|
140
|
+
if (code === 0 && availableAt != null && !json) {
|
|
141
|
+
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(availableAt, now)})${reauthNote}`));
|
|
98
142
|
}
|
|
99
143
|
return code;
|
|
100
144
|
});
|
package/src/cli/watch.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clamp, delay } from "es-toolkit";
|
|
2
2
|
import { loadAccounts } from "../lib/state.ts";
|
|
3
|
+
import { loadCodexAccounts } from "../lib/codexstate.ts";
|
|
3
4
|
import { cmdStatus } from "./status.ts";
|
|
4
|
-
import { c } from "./render.ts";
|
|
5
|
+
import { c, emitError, emitJson } from "./render.ts";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_INTERVAL_S = 120;
|
|
7
8
|
const MIN_INTERVAL_S = 30;
|
|
@@ -16,13 +17,13 @@ export function resolveWatchInterval(arg?: string): number | null {
|
|
|
16
17
|
|
|
17
18
|
const CLEAR = "\x1b[H\x1b[2J\x1b[3J";
|
|
18
19
|
|
|
19
|
-
export async function cmdWatch(intervalArg?: string): Promise<number> {
|
|
20
|
+
export async function cmdWatch(intervalArg?: string, json = false): Promise<number> {
|
|
20
21
|
const intervalS = resolveWatchInterval(intervalArg);
|
|
21
22
|
if (intervalS === null) {
|
|
22
|
-
|
|
23
|
+
emitError({ json, message: `watch interval must be a positive number of seconds, got: ${intervalArg}` });
|
|
23
24
|
return 2;
|
|
24
25
|
}
|
|
25
|
-
if (loadAccounts().accounts.length === 0) return cmdStatus();
|
|
26
|
+
if (loadAccounts().accounts.length === 0 && loadCodexAccounts().accounts.length === 0) return cmdStatus({ json });
|
|
26
27
|
|
|
27
28
|
const paintHeader = () => {
|
|
28
29
|
process.stdout.write(process.stdout.isTTY ? CLEAR : "\n");
|
|
@@ -30,10 +31,15 @@ export async function cmdWatch(intervalArg?: string): Promise<number> {
|
|
|
30
31
|
};
|
|
31
32
|
while (true) {
|
|
32
33
|
try {
|
|
33
|
-
await cmdStatus(
|
|
34
|
+
await cmdStatus(json ? { json } : { preRender: paintHeader });
|
|
34
35
|
} catch (e) {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const message = `status failed this tick: ${e instanceof Error ? e.message : String(e)}`;
|
|
37
|
+
if (json) {
|
|
38
|
+
emitJson({ ok: false, error: message });
|
|
39
|
+
} else {
|
|
40
|
+
paintHeader();
|
|
41
|
+
console.error(c.red(message));
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
await delay(intervalS * 1000);
|
|
39
45
|
}
|
|
@@ -10,7 +10,7 @@ import { livingCodexPresences } from "../lib/codexpresence.ts";
|
|
|
10
10
|
import { liveCodexAccountId } from "../lib/codexsample.ts";
|
|
11
11
|
import { loadCodexAccounts } from "../lib/codexstate.ts";
|
|
12
12
|
import { loadConfig } from "../lib/state.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { terminalBars } from "../lib/picker.ts";
|
|
14
14
|
import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
|
|
15
15
|
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, type CodexAccount } from "../lib/types.ts";
|
|
16
16
|
import { log } from "../lib/log.ts";
|
|
@@ -51,7 +51,7 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
|
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
53
|
const now = Date.now();
|
|
54
|
-
const bars =
|
|
54
|
+
const bars = terminalBars(loadConfig());
|
|
55
55
|
const index = loadCodexAccounts();
|
|
56
56
|
const unusable = (account: CodexAccount): boolean =>
|
|
57
57
|
account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
|