tokenmaxxing 1.126.0 → 1.127.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/package.json +1 -1
- package/src/cli/add.ts +10 -6
- package/src/cli/auth.ts +1 -1
- package/src/cli/serve.ts +6 -5
- package/src/cli/status.ts +12 -10
- package/src/entries/statusline.ts +21 -13
- package/src/lib/picker.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.127.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/add.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { withLock } from "../lib/lock.ts";
|
|
2
2
|
import type { Provider } from "../lib/provider.ts";
|
|
3
|
-
import { loadAccounts, saveAccounts, upsertAccount } from "../lib/state.ts";
|
|
4
|
-
import { sessionWindow, weeklyWindow } from "../lib/picker.ts";
|
|
3
|
+
import { loadAccounts, loadConfig, saveAccounts, upsertAccount } from "../lib/state.ts";
|
|
4
|
+
import { bufferedUsed, liveUsed, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
5
5
|
import { c, count } from "./render.ts";
|
|
6
|
-
import type { Account } from "../lib/types.ts";
|
|
6
|
+
import type { Account, Window } from "../lib/types.ts";
|
|
7
7
|
|
|
8
|
-
export function usageNote(account: Account): string {
|
|
8
|
+
export function usageNote(p: Provider, account: Account): string {
|
|
9
9
|
const session = sessionWindow(account);
|
|
10
10
|
const week = weeklyWindow(account);
|
|
11
|
-
|
|
11
|
+
if (!session || !week) return "";
|
|
12
|
+
const bars = thresholdBars(loadConfig());
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const pct = (w: Window) => Math.round(p.statusOnly ? liveUsed(w, now) : bufferedUsed(account, w, bars, now));
|
|
15
|
+
return ` (session ${pct(session)}% / week ${pct(week)}%)`;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export async function cmdAdd(p: Provider): Promise<number> {
|
|
@@ -28,7 +32,7 @@ export async function cmdAdd(p: Provider): Promise<number> {
|
|
|
28
32
|
});
|
|
29
33
|
|
|
30
34
|
console.log();
|
|
31
|
-
const note = harvested.sample ? usageNote(account) : "";
|
|
35
|
+
const note = harvested.sample ? usageNote(p, account) : "";
|
|
32
36
|
console.log(`${c.green("✓")} added ${c.bold(account.label)} (${account.tier ?? "?"})${note} → pool now has ${count({ n: poolSize, noun: "account" })}`);
|
|
33
37
|
return 0;
|
|
34
38
|
}
|
package/src/cli/auth.ts
CHANGED
|
@@ -91,7 +91,7 @@ async function reauthOne(p: Provider, target: Account): Promise<boolean> {
|
|
|
91
91
|
});
|
|
92
92
|
if (account === null) return false;
|
|
93
93
|
|
|
94
|
-
const note = harvested.sample ? usageNote(account) : "";
|
|
94
|
+
const note = harvested.sample ? usageNote(p, account) : "";
|
|
95
95
|
console.log(`${c.green("✓")} reauthed ${c.bold(account.email ?? account.label)} (${account.tier ?? "?"})${note}`);
|
|
96
96
|
return true;
|
|
97
97
|
}
|
package/src/cli/serve.ts
CHANGED
|
@@ -34,11 +34,11 @@ function iso(epochMs: number | null): string | null {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function claudeBody(usage: UsageReport): unknown {
|
|
37
|
-
const win = (w: WindowReport | null) => (w ? { utilization: w.
|
|
37
|
+
const win = (w: WindowReport | null) => (w ? { utilization: w.bufferedPercentage, resets_at: iso(w.resetsAt) } : null);
|
|
38
38
|
return {
|
|
39
39
|
five_hour: win(usage.fiveHour),
|
|
40
40
|
seven_day: win(usage.week),
|
|
41
|
-
limits: usage.limits.map((w) => ({ kind: "weekly_scoped", percent: w.
|
|
41
|
+
limits: usage.limits.map((w) => ({ kind: "weekly_scoped", percent: w.bufferedPercentage, resets_at: iso(w.resetsAt), scope: { model: { display_name: w.name } } })),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ function codexBody(usage: UsageReport, account: Account): unknown {
|
|
|
46
46
|
const win = (w: WindowReport | undefined) =>
|
|
47
47
|
w
|
|
48
48
|
? {
|
|
49
|
-
used_percent: w.
|
|
49
|
+
used_percent: w.bufferedPercentage,
|
|
50
50
|
reset_at: w.resetsAt == null ? null : Math.round(w.resetsAt / 1000),
|
|
51
51
|
...(w.windowSeconds == null ? {} : { limit_window_seconds: w.windowSeconds }),
|
|
52
52
|
}
|
|
@@ -142,8 +142,9 @@ async function apiCall(req: Request): Promise<Response> {
|
|
|
142
142
|
const now = Date.now();
|
|
143
143
|
const cfg = loadConfig();
|
|
144
144
|
const current = await pool.read(account, cfg, now);
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
const bars = thresholdBars(cfg);
|
|
146
|
+
pool.fold(current, bars);
|
|
147
|
+
const usage = usageReport({ ...current, windows: current.windows.filter((w) => now - w.sampledAt <= STALE_AFTER_MS) }, bars, now);
|
|
147
148
|
if (!usage || (!usage.fiveHour && !usage.week)) return json({ error: `no usage figure newer than ${STALE_AFTER_MS / 3_600_000}h for this account` }, 502);
|
|
148
149
|
return json({ status_code: 200, header: { "Content-Type": ["application/json"] }, body: JSON.stringify(pool.body(usage, current)) });
|
|
149
150
|
}
|
package/src/cli/status.ts
CHANGED
|
@@ -6,12 +6,12 @@ import { opencodeGo } from "../lib/opencodego.ts";
|
|
|
6
6
|
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
7
7
|
import { withLock } from "../lib/lock.ts";
|
|
8
8
|
import { codexPool, grokPool, opencodeGoPool } from "../lib/paths.ts";
|
|
9
|
-
import { barFor, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
|
+
import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
10
10
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
11
11
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
12
12
|
import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
|
|
13
13
|
|
|
14
|
-
export type WindowReport = { usedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
|
|
14
|
+
export type WindowReport = { usedPercentage: number; bufferedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
|
|
15
15
|
|
|
16
16
|
export type UsageReport = { fiveHour: WindowReport | null; week: WindowReport | null; limits: (WindowReport & { name: string })[] };
|
|
17
17
|
|
|
@@ -41,23 +41,24 @@ type PoolReport = {
|
|
|
41
41
|
|
|
42
42
|
export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; opencodeGo: PoolReport };
|
|
43
43
|
|
|
44
|
-
function currentWindow(w: Window, now: number): WindowReport {
|
|
44
|
+
function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
|
|
45
45
|
const passed = w.resetsAt != null && w.resetsAt <= now;
|
|
46
46
|
return {
|
|
47
|
-
usedPercentage:
|
|
47
|
+
usedPercentage: liveUsed(w, now),
|
|
48
|
+
bufferedPercentage: bars == null ? liveUsed(w, now) : bufferedUsed(a, w, bars, now),
|
|
48
49
|
resetsAt: isSessionWindow(w) ? (passed ? null : w.resetsAt) : nextWeeklyReset(w.resetsAt, now),
|
|
49
50
|
windowSeconds: w.windowSeconds,
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function usageReport(a: Account, now: number): UsageReport | null {
|
|
54
|
+
export function usageReport(a: Account, bars: Bars | null, now: number): UsageReport | null {
|
|
54
55
|
if (a.lastUsageAt == null) return null;
|
|
55
56
|
const session = sessionWindow(a);
|
|
56
57
|
const week = weeklyWindow(a);
|
|
57
58
|
return {
|
|
58
|
-
fiveHour: session ? currentWindow(session, now) : null,
|
|
59
|
-
week: week ? currentWindow(week, now) : null,
|
|
60
|
-
limits: limitWindows(a).map((w) => ({ name: w.name ?? "", ...currentWindow(w, now) })),
|
|
59
|
+
fiveHour: session ? currentWindow(a, session, bars, now) : null,
|
|
60
|
+
week: week ? currentWindow(a, week, bars, now) : null,
|
|
61
|
+
limits: limitWindows(a).map((w) => ({ name: w.name ?? "", ...currentWindow(a, w, bars, now) })),
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -151,7 +152,7 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
151
152
|
needsReauth: a.needsReauth === true,
|
|
152
153
|
exhausted: isExhausted(a, ctx),
|
|
153
154
|
thresholds: own.get(a.label) ?? null,
|
|
154
|
-
usage: usageReport(a, now),
|
|
155
|
+
usage: usageReport(a, p.statusOnly ? null : bars, now),
|
|
155
156
|
usageAt: a.lastUsageAt ?? null,
|
|
156
157
|
limitsAt: limits.length > 0 ? Math.max(...limits.map((w) => w.sampledAt)) : null,
|
|
157
158
|
sample: reports.get(a.id) ?? (cached ? { ok: true, source: "cached" } : { ok: false, reason: "not sampled" }),
|
|
@@ -194,7 +195,7 @@ function renderGrid(cards: Card[]): void {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
function usageRow(name: string, w: WindowReport): string {
|
|
197
|
-
return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.
|
|
198
|
+
return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.bufferedPercentage)}`;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
function sampleFailedNotes(input: { cached: boolean; usageAt: number | null; reason: string; now: number }): Note[] {
|
|
@@ -245,6 +246,7 @@ function card(p: Provider, a: StatusAccount, now: number, staleAfterMs: number):
|
|
|
245
246
|
|
|
246
247
|
function renderPool(p: Provider, pool: PoolReport, header: string, now: number, staleAfterMs: number): void {
|
|
247
248
|
console.log(c.dim(header));
|
|
249
|
+
if (!p.statusOnly) console.log(c.dim("buffered usage quota: percent of each account's switch threshold, so it reads higher than the usage Claude or Codex reports"));
|
|
248
250
|
console.log();
|
|
249
251
|
renderGrid(pool.accounts.map((a) => card(p, a, now, staleAfterMs)));
|
|
250
252
|
if (pool.gatedNote) {
|
|
@@ -3,7 +3,7 @@ import { claudePool, seatFromEnv } from "../lib/paths.ts";
|
|
|
3
3
|
import { errorMessage } from "../lib/log.ts";
|
|
4
4
|
import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
|
|
5
5
|
import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
|
|
6
|
-
import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
|
|
6
|
+
import { bufferedUsed, earliestReset, limitWindows, liveUsed, thresholdBars, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
|
|
7
7
|
import { readStdin } from "../lib/proc.ts";
|
|
8
8
|
import { worktreeName } from "../lib/worktree.ts";
|
|
9
9
|
import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
|
|
@@ -12,13 +12,18 @@ import {
|
|
|
12
12
|
StatusLineStdinSchema,
|
|
13
13
|
type Account,
|
|
14
14
|
type AccountsIndex,
|
|
15
|
+
type Bars,
|
|
15
16
|
type UsageState,
|
|
16
17
|
type UsageWindow,
|
|
17
18
|
type Window,
|
|
18
19
|
} from "../lib/types.ts";
|
|
19
20
|
|
|
21
|
+
const FIVE_HOUR_S = 5 * 3600;
|
|
22
|
+
const SEVEN_DAY_S = 7 * 24 * 3600;
|
|
23
|
+
|
|
20
24
|
export type RenderCtx = {
|
|
21
25
|
accounts: AccountsIndex;
|
|
26
|
+
bars: Bars;
|
|
22
27
|
perModel: Record<string, Window>;
|
|
23
28
|
switchModels: string[];
|
|
24
29
|
worktree: string | null;
|
|
@@ -31,7 +36,7 @@ export type RenderCtx = {
|
|
|
31
36
|
export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
32
37
|
const col = makeColors(ctx.color);
|
|
33
38
|
const paint = makeUsagePaint({ enabled: ctx.color, truecolor: ctx.truecolor });
|
|
34
|
-
const used = (w:
|
|
39
|
+
const used = (a: Account, w: Window) => bufferedUsed(a, w, ctx.bars, ctx.now);
|
|
35
40
|
const reset = (epochMs: number | null) => fmtResetShort(epochMs, ctx.now);
|
|
36
41
|
|
|
37
42
|
const parsed = StatusLineStdinSchema.safeParse(stdinObj);
|
|
@@ -51,8 +56,7 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
51
56
|
const removed = d?.cost?.total_lines_removed ?? 0;
|
|
52
57
|
if (added > 0 || removed > 0) info.push(`${col.green(`+${added}`)}/-${removed}`);
|
|
53
58
|
|
|
54
|
-
const seg = (label: string,
|
|
55
|
-
const u = used(w);
|
|
59
|
+
const seg = (label: string, u: number, resetAt: number | null) => {
|
|
56
60
|
if (!ctx.color) return `${label}${reset(resetAt)}${Math.round(u)}`;
|
|
57
61
|
const body = label !== "" ? label : reset(resetAt);
|
|
58
62
|
return col.bold(paint(u)(body !== "" ? body : u > 0 ? "?" : "0"));
|
|
@@ -64,15 +68,17 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
64
68
|
if (family && !Object.keys(ctx.perModel).some((k) => familyTokens(k).includes(family))) {
|
|
65
69
|
windows.push(`${initial(family)}?`);
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
const seatUuid = ctx.liveAccount;
|
|
72
|
+
const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
|
|
73
|
+
const seatUsed = (w: Window) => (seat ? used(seat, w) : liveUsed(w, ctx.now));
|
|
74
|
+
const stdinWindow = (w: UsageWindow, windowSeconds: number): Window => ({ name: null, ...w, windowSeconds, sampledAt: ctx.now });
|
|
75
|
+
for (const [name, w] of Object.entries(ctx.perModel)) windows.push(seg(initial(name), seatUsed(w), null));
|
|
68
76
|
if (wins) {
|
|
69
|
-
windows.push(seg("", wins.fiveHour, wins.fiveHour.resetsAt));
|
|
70
|
-
windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
|
|
77
|
+
windows.push(seg("", seatUsed(stdinWindow(wins.fiveHour, FIVE_HOUR_S)), wins.fiveHour.resetsAt));
|
|
78
|
+
windows.push(seg("", seatUsed(stdinWindow(wins.sevenDay, SEVEN_DAY_S)), wins.sevenDay.resetsAt));
|
|
71
79
|
}
|
|
72
|
-
const seatUuid = ctx.liveAccount;
|
|
73
80
|
const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
|
|
74
|
-
const wallSeg = (wall: number) => seg("",
|
|
75
|
-
const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
|
|
81
|
+
const wallSeg = (wall: number) => seg("", 100, wall);
|
|
76
82
|
if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
|
|
77
83
|
const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
|
|
78
84
|
const active =
|
|
@@ -91,15 +97,16 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
91
97
|
if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
|
|
92
98
|
const week = weeklyWindow(a);
|
|
93
99
|
if (week == null) return `${marker} ?`;
|
|
94
|
-
const weekUsed = used(week);
|
|
100
|
+
const weekUsed = used(a, week);
|
|
95
101
|
if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
|
|
96
102
|
|
|
97
103
|
const parts: string[] = [];
|
|
98
104
|
for (const w of limitWindows(a)) {
|
|
99
|
-
|
|
105
|
+
const u = used(a, w);
|
|
106
|
+
if (u > weekUsed) parts.push(seg(initial(w.name ?? ""), u, null));
|
|
100
107
|
}
|
|
101
108
|
const expiry = weeklyExpiry(a, ctx.now);
|
|
102
|
-
parts.push(seg("",
|
|
109
|
+
parts.push(seg("", weekUsed, Number.isFinite(expiry) ? expiry : null));
|
|
103
110
|
return `${marker} ${parts.join(" ")}`;
|
|
104
111
|
};
|
|
105
112
|
|
|
@@ -130,6 +137,7 @@ export async function runStatusline(): Promise<number> {
|
|
|
130
137
|
const live = accounts.accounts.find((a) => a.id === account);
|
|
131
138
|
const ctx: RenderCtx = {
|
|
132
139
|
accounts,
|
|
140
|
+
bars: thresholdBars(cfg),
|
|
133
141
|
perModel: Object.fromEntries((live ? limitWindows(live) : []).map((w) => [w.name ?? "", w])),
|
|
134
142
|
switchModels: cfg.policy.switchModels,
|
|
135
143
|
worktree: dir == null ? null : worktreeName(dir),
|
package/src/lib/picker.ts
CHANGED
|
@@ -64,6 +64,13 @@ export function barFor(a: Account, w: Window, bars: Bars, now: number): number {
|
|
|
64
64
|
return released ? Math.max(pick(own), pick(bars)) : pick(own);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function bufferedUsed(a: Account, w: Window, bars: Bars, now: number): number {
|
|
68
|
+
const used = liveUsed(w, now);
|
|
69
|
+
const bar = barFor(a, w, bars, now);
|
|
70
|
+
if (bar <= 0) return used > 0 ? 100 : 0;
|
|
71
|
+
return Math.min(100, (used / bar) * 100);
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
function windowEnd(w: Window): number {
|
|
68
75
|
return w.resetsAt ?? w.sampledAt + (w.windowSeconds ?? 0) * 1000;
|
|
69
76
|
}
|