tokenmaxxing 1.10.0 → 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/README.md +1 -0
- 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/package.json +1 -1
- package/src/cli/check.ts +17 -4
- package/src/cli/codexrm.ts +8 -7
- package/src/cli/codexswitch.ts +48 -18
- 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 +239 -110
- package/src/cli/switch.ts +64 -28
- package/src/cli/watch.ts +13 -7
- package/src/entries/mcp.ts +1 -1
- package/src/main.ts +44 -22
package/src/cli/rename.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { withLock } from "../lib/lock.ts";
|
|
|
2
2
|
import { codexPaths, paths } from "../lib/paths.ts";
|
|
3
3
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
4
4
|
import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
|
|
5
|
-
import { c } from "./render.ts";
|
|
5
|
+
import { c, emitError, emitJson, plain } from "./render.ts";
|
|
6
6
|
import type { Account, CodexAccount } from "../lib/types.ts";
|
|
7
7
|
|
|
8
8
|
export function findAccount(accounts: Account[], selector: string): Account | undefined {
|
|
@@ -23,51 +23,54 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
|
|
26
|
+
async function renameCodexAccount(input: { selector: string; newLabel: string; json: boolean }): Promise<number> {
|
|
27
|
+
const { selector, newLabel, json } = input;
|
|
27
28
|
return withLock(codexPaths.lockFile, async () => {
|
|
28
29
|
const index = loadCodexAccounts();
|
|
29
|
-
const account = findCodexAccount(index.accounts,
|
|
30
|
+
const account = findCodexAccount(index.accounts, selector);
|
|
30
31
|
if (!account) {
|
|
31
|
-
|
|
32
|
+
emitError({ json, message: `no codex account matches "${selector}"` });
|
|
32
33
|
return 1;
|
|
33
34
|
}
|
|
34
|
-
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() ===
|
|
35
|
+
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
35
36
|
if (taken) {
|
|
36
|
-
|
|
37
|
+
emitError({ json, message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
|
|
37
38
|
return 1;
|
|
38
39
|
}
|
|
39
40
|
const old = account.label;
|
|
40
|
-
account.label =
|
|
41
|
+
account.label = newLabel;
|
|
41
42
|
saveCodexAccounts({ index });
|
|
42
|
-
|
|
43
|
+
if (json) emitJson({ ok: true, pool: "codex", from: old, to: newLabel });
|
|
44
|
+
else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
43
45
|
return 0;
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export async function cmdRename(argv: string[]): Promise<number> {
|
|
49
|
+
export async function cmdRename(argv: string[], json = false): Promise<number> {
|
|
48
50
|
const codex = argv.includes("--codex");
|
|
49
51
|
const [selector, newLabel] = argv.filter((a) => a !== "--codex");
|
|
50
52
|
if (!selector || !newLabel) {
|
|
51
|
-
|
|
53
|
+
emitError({ json, message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
|
|
52
54
|
return 2;
|
|
53
55
|
}
|
|
54
|
-
if (codex) return renameCodexAccount({ selector, newLabel });
|
|
56
|
+
if (codex) return renameCodexAccount({ selector, newLabel, json });
|
|
55
57
|
return withLock(paths.lockFile, async () => {
|
|
56
58
|
const idx = loadAccounts();
|
|
57
59
|
const a = findAccount(idx.accounts, selector);
|
|
58
60
|
if (!a) {
|
|
59
|
-
|
|
61
|
+
emitError({ json, message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
|
|
60
62
|
return 1;
|
|
61
63
|
}
|
|
62
64
|
const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
63
65
|
if (taken) {
|
|
64
|
-
|
|
66
|
+
emitError({ json, message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
|
|
65
67
|
return 1;
|
|
66
68
|
}
|
|
67
69
|
const old = a.label;
|
|
68
70
|
a.label = newLabel;
|
|
69
71
|
saveAccounts(idx);
|
|
70
|
-
|
|
72
|
+
if (json) emitJson({ ok: true, pool: "claude", from: old, to: newLabel });
|
|
73
|
+
else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
71
74
|
return 0;
|
|
72
75
|
});
|
|
73
76
|
}
|
package/src/cli/render.ts
CHANGED
|
@@ -75,3 +75,23 @@ export function fmtAgo(epochMs: number, now = Date.now()): string {
|
|
|
75
75
|
return `${m}m ago`;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export const plain = (s: string): string => s;
|
|
79
|
+
|
|
80
|
+
export function emitJson(value: unknown): void {
|
|
81
|
+
console.log(JSON.stringify(value, null, 2));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function emitError(input: {
|
|
85
|
+
json: boolean;
|
|
86
|
+
message: string;
|
|
87
|
+
notes?: string[];
|
|
88
|
+
extra?: Record<string, unknown>;
|
|
89
|
+
paint?: (s: string) => string;
|
|
90
|
+
}): void {
|
|
91
|
+
if (input.json) {
|
|
92
|
+
emitJson({ ok: false, error: input.message, ...input.extra });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.error((input.paint ?? c.red)(input.message));
|
|
96
|
+
for (const note of input.notes ?? []) console.error(c.dim(note));
|
|
97
|
+
}
|
package/src/cli/rm.ts
CHANGED
|
@@ -7,22 +7,22 @@ import { credItemFor, paths } from "../lib/paths.ts";
|
|
|
7
7
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
8
8
|
import { CredentialBlobSchema } from "../lib/types.ts";
|
|
9
9
|
import { findAccount } from "./rename.ts";
|
|
10
|
-
import { c } from "./render.ts";
|
|
10
|
+
import { c, emitError, emitJson, plain } from "./render.ts";
|
|
11
11
|
|
|
12
|
-
export async function cmdRm(selector?: string): Promise<number> {
|
|
12
|
+
export async function cmdRm(selector?: string, json = false): Promise<number> {
|
|
13
13
|
if (!selector) {
|
|
14
|
-
|
|
14
|
+
emitError({ json, message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
|
|
15
15
|
return 2;
|
|
16
16
|
}
|
|
17
17
|
return withLock(paths.lockFile, async () => {
|
|
18
18
|
const idx = loadAccounts();
|
|
19
19
|
const a = findAccount(idx.accounts, selector);
|
|
20
20
|
if (!a) {
|
|
21
|
-
|
|
21
|
+
emitError({ json, message: `no account matches "${selector}"` });
|
|
22
22
|
return 1;
|
|
23
23
|
}
|
|
24
24
|
if (a.accountUuid === idx.activeAccountUuid) {
|
|
25
|
-
|
|
25
|
+
emitError({ json, message: `${a.email} is the ACTIVE account - switch away before removing it.` });
|
|
26
26
|
return 1;
|
|
27
27
|
}
|
|
28
28
|
const live = await readItem(liveTarget());
|
|
@@ -32,11 +32,14 @@ export async function cmdRm(selector?: string): Promise<number> {
|
|
|
32
32
|
const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
|
|
33
33
|
liveOrg = (await fetchTokenOrg(liveCreds.accessToken)).organization_uuid;
|
|
34
34
|
} catch (e) {
|
|
35
|
-
|
|
35
|
+
emitError({
|
|
36
|
+
json,
|
|
37
|
+
message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`,
|
|
38
|
+
});
|
|
36
39
|
return 1;
|
|
37
40
|
}
|
|
38
41
|
if (liveOrg === a.organizationUuid) {
|
|
39
|
-
|
|
42
|
+
emitError({ json, message: `${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.` });
|
|
40
43
|
return 1;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -46,7 +49,8 @@ export async function cmdRm(selector?: string): Promise<number> {
|
|
|
46
49
|
rmSync(sampleDir, { recursive: true, force: true });
|
|
47
50
|
idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
|
|
48
51
|
saveAccounts(idx);
|
|
49
|
-
|
|
52
|
+
if (json) emitJson({ ok: true, pool: "claude", removed: a.label, remaining: idx.accounts.length });
|
|
53
|
+
else console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
|
|
50
54
|
return 0;
|
|
51
55
|
});
|
|
52
56
|
}
|
package/src/cli/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -9,42 +10,94 @@ 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 { bar, c, claudeTierLabel, count, emitJson, fmtAgo, fmtReset } from "./render.ts";
|
|
13
14
|
import { gatedFamilies, type FullUsage } from "../lib/usage.ts";
|
|
14
|
-
import
|
|
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,72 +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
|
-
|
|
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
|
+
});
|
|
147
|
+
}
|
|
94
148
|
|
|
95
|
-
preRender?.();
|
|
96
149
|
const families = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
97
150
|
const bars = effectiveBars(cfg, { accounts: idx.accounts, now, switchFamilies: families });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const a of displayAccounts) {
|
|
104
|
-
const active = displayActiveOrg != null && a.organizationUuid === displayActiveOrg;
|
|
105
|
-
const outcome = outcomes.get(a.accountUuid);
|
|
106
|
-
const failed = outcome ? !outcome.ok : false;
|
|
107
|
-
const usage = outcome?.ok ? outcome.usage : undefined;
|
|
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;
|
|
108
156
|
const aggregate = usage ? { fiveHour: usage.session, sevenDay: usage.weekAll } : a.lastUsage;
|
|
109
|
-
const perModel = usage ? usage.perModel : a.lastPerModel;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
|
|
134
|
-
}
|
|
135
|
-
console.log();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
await renderCodexSection({ cfg, now, row });
|
|
139
|
-
return 0;
|
|
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
|
+
};
|
|
140
181
|
}
|
|
141
182
|
|
|
142
|
-
async function
|
|
143
|
-
cfg
|
|
144
|
-
|
|
145
|
-
row: (name: string, w: UsageWindow, weekly: boolean) => void;
|
|
146
|
-
}): Promise<void> {
|
|
147
|
-
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);
|
|
148
186
|
let index = loadCodexAccounts();
|
|
149
|
-
if (index.accounts.length === 0) return;
|
|
187
|
+
if (index.accounts.length === 0) return { bars, accounts: [] };
|
|
150
188
|
|
|
151
189
|
console.error(c.dim("sampling codex usage..."));
|
|
152
190
|
const outcomes = new Map<string, CodexSampleOutcome>();
|
|
@@ -171,11 +209,7 @@ async function renderCodexSection(input: {
|
|
|
171
209
|
saveCodexAccounts({ index });
|
|
172
210
|
});
|
|
173
211
|
|
|
174
|
-
|
|
175
|
-
console.log();
|
|
176
|
-
const windowLabel = (window: CodexWindow) =>
|
|
177
|
-
isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
|
|
178
|
-
const displayAccounts = sortBy(index.accounts, [
|
|
212
|
+
const ordered = sortBy(index.accounts, [
|
|
179
213
|
(a) => (a.needsReauth ? 1 : 0),
|
|
180
214
|
(a) => {
|
|
181
215
|
const windows = [...(a.lastUsage?.aggregate ?? []), ...Object.values(a.lastUsage?.perLimit ?? {}).flat()];
|
|
@@ -183,27 +217,122 @@ async function renderCodexSection(input: {
|
|
|
183
217
|
return resets.length > 0 ? Math.min(...resets) : Number.POSITIVE_INFINITY;
|
|
184
218
|
},
|
|
185
219
|
]);
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
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("○");
|
|
189
255
|
const badges: string[] = [];
|
|
190
|
-
if (active) badges.push(c.green("active"));
|
|
256
|
+
if (account.active) badges.push(c.green("active"));
|
|
191
257
|
if (account.needsReauth) badges.push(c.red("needs-reauth"));
|
|
192
|
-
if (
|
|
258
|
+
if (account.exhausted) badges.push(c.yellow("exhausted"));
|
|
193
259
|
console.log(`${marker} ${c.bold(account.label)}${account.planType ? ` ${c.dim(account.planType)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for (const [name, windows] of Object.entries(usage.perLimit)) {
|
|
199
|
-
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);
|
|
200
264
|
}
|
|
201
265
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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")}`);
|
|
206
315
|
}
|
|
207
316
|
console.log();
|
|
208
317
|
}
|
|
209
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
|
+
}
|