tokenmaxxing 1.131.0 → 1.133.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/key.ts +147 -0
- package/src/cli/status.ts +48 -3
- package/src/entries/codexsupervisor.ts +7 -5
- package/src/entries/statusline.ts +10 -0
- package/src/entries/stopfailurehook.ts +24 -6
- package/src/entries/stophook.ts +7 -6
- package/src/entries/supervisor.ts +106 -74
- package/src/lib/apikeys.ts +142 -0
- package/src/lib/claude.ts +25 -7
- package/src/lib/compact.ts +3 -4
- package/src/lib/decide.ts +27 -6
- package/src/lib/paths.ts +3 -0
- package/src/lib/pi.ts +1 -0
- package/src/lib/presence.ts +6 -5
- package/src/lib/provider.ts +8 -1
- package/src/lib/sessions.ts +7 -2
- package/src/lib/supervise.ts +53 -3
- package/src/lib/types.ts +30 -0
- package/src/lib/usage.ts +1 -1
- package/src/main.ts +7 -2
package/src/lib/decide.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { MAX_WAITERS_PER_ACCOUNT, isExhausted, landWindows, limitWindows, liveUs
|
|
|
6
6
|
import { familyTokens } from "./usage.ts";
|
|
7
7
|
import { errorMessage, log } from "./log.ts";
|
|
8
8
|
import type { Observation, Provider, ResetClaim } from "./provider.ts";
|
|
9
|
-
import type { Account, Bars, Config, EnforcedLimit } from "./types.ts";
|
|
9
|
+
import type { Account, ApiKey, Bars, Config, EnforcedLimit } from "./types.ts";
|
|
10
10
|
|
|
11
|
-
export type SwapDecision = { swapped: boolean; account: Account | null; reason: string; waitUntil?: number };
|
|
11
|
+
export type SwapDecision = { swapped: boolean; account: Account | null; apiKey?: ApiKey; reason: string; waitUntil?: number };
|
|
12
12
|
|
|
13
13
|
export type EvalOpts = {
|
|
14
14
|
seatId?: string | null;
|
|
@@ -16,8 +16,18 @@ export type EvalOpts = {
|
|
|
16
16
|
waiterId?: string;
|
|
17
17
|
exclude?: string[];
|
|
18
18
|
refused?: string[];
|
|
19
|
+
apiKeys?: boolean;
|
|
20
|
+
apiKeyId?: string;
|
|
19
21
|
};
|
|
20
22
|
|
|
23
|
+
export type MoveTarget = { accountId: string; apiKeyId?: string; waitUntil: number | undefined };
|
|
24
|
+
|
|
25
|
+
export function moveTarget(d: SwapDecision): MoveTarget | null {
|
|
26
|
+
if (d.apiKey) return { accountId: "", apiKeyId: d.apiKey.id, waitUntil: undefined };
|
|
27
|
+
if (d.account && (d.swapped || d.waitUntil !== undefined)) return { accountId: d.account.id, waitUntil: d.waitUntil };
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
const MOVE_CLAIM_MS = 2 * CLAUDE_COMPACT_KILL_MS;
|
|
22
32
|
|
|
23
33
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -106,6 +116,8 @@ export async function useBankedResets(p: Provider, cfg: Config, now: number): Pr
|
|
|
106
116
|
|
|
107
117
|
export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRespawn = false, enforced: EnforcedLimit | null = null, opts: EvalOpts = {}): Promise<SwapDecision> {
|
|
108
118
|
const activeId = opts.seatId === undefined ? p.liveId() : opts.seatId;
|
|
119
|
+
const keys = opts.apiKeys === true ? p.apiKeys : undefined;
|
|
120
|
+
const keyId = activeId == null && keys ? (opts.apiKeyId ?? keys.liveId()) : null;
|
|
109
121
|
const waiterId = canRespawn ? opts.waiterId : undefined;
|
|
110
122
|
const cfg = loadConfig();
|
|
111
123
|
const bars = thresholdBars(cfg);
|
|
@@ -116,7 +128,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
116
128
|
const observed = stored0 ? await p.observeLive(stored0, cfg, now, { probe: enforced == null && !walled0, perModel: gated == null || gated.length > 0 }) : null;
|
|
117
129
|
const stored = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
|
|
118
130
|
|
|
119
|
-
if (!enforced && !isOver(stored, observed, { now, thresholds: bars, currentId: activeId, families: gated, seats: null })) {
|
|
131
|
+
if (!enforced && keyId == null && !isOver(stored, observed, { now, thresholds: bars, currentId: activeId, families: gated, seats: null })) {
|
|
120
132
|
if (waiterId != null && liveWaitClaims(now).some((c) => c.sessionId === waiterId)) {
|
|
121
133
|
await withLock(p.pool.lockFile, () => {
|
|
122
134
|
releaseWaitClaim(waiterId);
|
|
@@ -168,7 +180,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
168
180
|
const seats = p.seats === "shared" ? present : null;
|
|
169
181
|
|
|
170
182
|
const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars, currentId: id2, families: switchFamilies, seats });
|
|
171
|
-
if (!enforced2 && !isOver(active, obs2, { now, thresholds: bars, currentId: id2, families, seats }) && !(enforced && seatExhausted)) {
|
|
183
|
+
if (keyId == null && !enforced2 && !isOver(active, obs2, { now, thresholds: bars, currentId: id2, families, seats }) && !(enforced && seatExhausted)) {
|
|
172
184
|
return { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
173
185
|
}
|
|
174
186
|
if (p.seats === "shared" && !canRespawn) {
|
|
@@ -189,9 +201,9 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
189
201
|
const seat = seatOf(cur);
|
|
190
202
|
const ctx: PickCtx = { now, thresholds: bars, currentId: seat?.id ?? null, families: switchFamilies, seats };
|
|
191
203
|
const credited = !enforced2 && seat != null && !seat.needsReauth && !isExhausted(seat, ctx) && onCredits(seat, ctx);
|
|
192
|
-
const best = pickBest(usable(cur.accounts).filter((a) => !(credited && opts.refused?.includes(a.id))), ctx);
|
|
204
|
+
const best = pickBest(usable(cur.accounts).filter((a) => !((credited || keyId != null) && opts.refused?.includes(a.id))), ctx);
|
|
193
205
|
if (credited && (best == null || onCredits(best, ctx))) return { swapped: false, account: null, reason: "no-plan-headroom" };
|
|
194
|
-
if (!best) break;
|
|
206
|
+
if (!best || (keyId != null && onCredits(best, ctx))) break;
|
|
195
207
|
try {
|
|
196
208
|
await p.swap(best);
|
|
197
209
|
} catch (e) {
|
|
@@ -203,6 +215,15 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
203
215
|
return { swapped: true, account: best, reason: "swapped" };
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
if (keys) {
|
|
219
|
+
if (keyId != null && keys.holds(keyId)) return { swapped: false, account: null, reason: "api-key-holds" };
|
|
220
|
+
const key = keys.pick(keyId == null ? [] : [keyId]);
|
|
221
|
+
if (key) {
|
|
222
|
+
log("decide.api_key", { key: key.id, from: keyId ?? id2?.slice(0, 8) });
|
|
223
|
+
return { swapped: true, account: null, apiKey: key, reason: "api-key" };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
if (!p.waitsWhenDepleted) {
|
|
207
228
|
log("decide.depleted", { waitUntil: 0 });
|
|
208
229
|
return { swapped: false, account: null, reason: "all-depleted" };
|
package/src/lib/paths.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const paths = {
|
|
|
28
28
|
onboardDir: join(TM_HOME, "onboard"),
|
|
29
29
|
storesDir: join(TM_HOME, "stores"),
|
|
30
30
|
hubKeyFile: join(TM_HOME, "hub-key"),
|
|
31
|
+
apiKeysDir: join(TM_HOME, "api-keys"),
|
|
32
|
+
costDir: join(TM_HOME, "cost"),
|
|
31
33
|
|
|
32
34
|
claudeSettings: env(
|
|
33
35
|
"TOKENMAXXING_CLAUDE_SETTINGS",
|
|
@@ -43,6 +45,7 @@ export const claudePool = {
|
|
|
43
45
|
accountsJson: join(TM_HOME, "accounts.json"),
|
|
44
46
|
lockFile: join(TM_HOME, "lock"),
|
|
45
47
|
waitQueueJson: join(TM_HOME, "wait-queue.json"),
|
|
48
|
+
apiKeysJson: join(TM_HOME, "api-keys.json"),
|
|
46
49
|
} as const;
|
|
47
50
|
|
|
48
51
|
export const codexPool = {
|
package/src/lib/pi.ts
CHANGED
|
@@ -98,6 +98,7 @@ export async function piLogin(pool: PiPool): Promise<PiLogin | null> {
|
|
|
98
98
|
function piMover(base: Provider, pool: PiPool): Provider {
|
|
99
99
|
return {
|
|
100
100
|
...base,
|
|
101
|
+
apiKeys: undefined,
|
|
101
102
|
swap: async (target) => {
|
|
102
103
|
if (!piStoreUsable(pool, target.id)) {
|
|
103
104
|
throw new StoreUnusableError(`${target.label} has no pi login - run \`tokenmaxxing auth --pi${base.flag} ${target.label}\``);
|
package/src/lib/presence.ts
CHANGED
|
@@ -13,12 +13,13 @@ const PresenceSchema = z.object({
|
|
|
13
13
|
accountId: z.string(),
|
|
14
14
|
pid: z.number(),
|
|
15
15
|
startedAt: z.string(),
|
|
16
|
+
apiKeyId: z.string().optional(),
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
export function writePresence(input: { dir: string; id: string; accountId: string; pid: number }): void {
|
|
19
|
+
export function writePresence(input: { dir: string; id: string; accountId: string; pid: number; apiKeyId?: string }): void {
|
|
19
20
|
const startedAt = pidStartTimes([input.pid]).get(input.pid);
|
|
20
21
|
if (startedAt == null) throw new Error(`could not read pid ${input.pid}'s start time (ps lstart) - refusing to write an unverifiable presence file`);
|
|
21
|
-
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt })));
|
|
22
|
+
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt, ...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}) })));
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function presencePid(input: { dir: string; id: string }): number | null {
|
|
@@ -34,7 +35,7 @@ export function clearPresence(input: { dir: string; id: string }): void {
|
|
|
34
35
|
rmSync(join(input.dir, input.id), { force: true });
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export type LivingPresence = { id: string; accountId: string };
|
|
38
|
+
export type LivingPresence = { id: string; accountId: string; apiKeyId?: string };
|
|
38
39
|
|
|
39
40
|
export function livingPresences(dir: string): LivingPresence[] {
|
|
40
41
|
if (!existsSync(dir)) return [];
|
|
@@ -65,11 +66,11 @@ export function livingPresences(dir: string): LivingPresence[] {
|
|
|
65
66
|
rmSync(file, { force: true });
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
|
-
living.push({ id: name, accountId: record.accountId });
|
|
69
|
+
living.push({ id: name, accountId: record.accountId, ...(record.apiKeyId != null ? { apiKeyId: record.apiKeyId } : {}) });
|
|
69
70
|
}
|
|
70
71
|
return living;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function seatCounts(dir: string): Map<string, number> {
|
|
74
|
-
return new Map(Object.entries(countBy(livingPresences(dir), (p) => p.accountId)));
|
|
75
|
+
return new Map(Object.entries(countBy(livingPresences(dir).filter((p) => p.apiKeyId == null), (p) => p.accountId)));
|
|
75
76
|
}
|
package/src/lib/provider.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { PoolPaths } from "./paths.ts";
|
|
2
2
|
import type { Harvest } from "./state.ts";
|
|
3
|
-
import type { Account, Config, Window } from "./types.ts";
|
|
3
|
+
import type { Account, ApiKey, Config, Window } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type ApiKeyPool = {
|
|
6
|
+
liveId(): string | null;
|
|
7
|
+
holds(id: string): boolean;
|
|
8
|
+
pick(exclude: string[]): ApiKey | null;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export type Observation = { windows: Window[]; at: number };
|
|
6
12
|
|
|
@@ -26,6 +32,7 @@ export type Provider = {
|
|
|
26
32
|
seats: "shared" | "live";
|
|
27
33
|
waitsWhenDepleted: boolean;
|
|
28
34
|
statusOnly: boolean;
|
|
35
|
+
apiKeys?: ApiKeyPool;
|
|
29
36
|
liveId(): string | null;
|
|
30
37
|
presence(): Map<string, number>;
|
|
31
38
|
gatedFamilies(cfg: Config): string[] | null;
|
package/src/lib/sessions.ts
CHANGED
|
@@ -60,7 +60,7 @@ function listDir(dir: string, root: string): Dirent[] {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function tmpSweepDirs(root: string): string[] {
|
|
63
|
-
const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
|
|
63
|
+
const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, paths.apiKeysDir, paths.costDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
|
|
64
64
|
for (const parent of STORE_PARENTS) {
|
|
65
65
|
for (const child of listDir(parent, root)) {
|
|
66
66
|
if (child.isDirectory()) dirs.push(join(parent, child.name));
|
|
@@ -113,6 +113,10 @@ export function supervisedSession(env: Record<string, string | undefined> = proc
|
|
|
113
113
|
return { sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null, live: liveSessionId(sid) };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export function keyMovesSupported(env: Record<string, string | undefined> = process.env): boolean {
|
|
117
|
+
return env.TOKENMAXXING_API_KEY_ID !== undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
116
120
|
export function refusedAccounts(env: Record<string, string | undefined> = process.env): string[] {
|
|
117
121
|
return RefusedSchema.parse(env.TOKENMAXXING_REFUSED);
|
|
118
122
|
}
|
|
@@ -129,9 +133,10 @@ export function adoptLiveSession(session: SupervisedSession, stdinSid: string |
|
|
|
129
133
|
return { ...session, live: stdinSid };
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; waitUntil: number; compact: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; refused?: string[] }): void {
|
|
136
|
+
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; apiKeyId?: string; waitUntil: number; compact: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; refused?: string[] }): void {
|
|
133
137
|
const payload: z.infer<typeof RespawnMarkerSchema> = {
|
|
134
138
|
accountId: input.accountId,
|
|
139
|
+
...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}),
|
|
135
140
|
ts: Date.now(),
|
|
136
141
|
waitUntil: input.waitUntil,
|
|
137
142
|
sessionId: input.session.live,
|
package/src/lib/supervise.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { isatty } from "node:tty";
|
|
1
3
|
import type { Subprocess } from "bun";
|
|
2
4
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperChainTripped } from "./claudebin.ts";
|
|
3
5
|
import { errorMessage, log } from "./log.ts";
|
|
4
6
|
import { paths } from "./paths.ts";
|
|
7
|
+
import { readLines } from "./proc.ts";
|
|
5
8
|
import { writePresence } from "./presence.ts";
|
|
6
9
|
import { restoreTermios } from "./tty.ts";
|
|
7
10
|
|
|
@@ -75,17 +78,64 @@ export function loopGuardTripped(product: WrappedProduct): boolean {
|
|
|
75
78
|
return false;
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
const STDOUT_QUIET_MS = 1000;
|
|
82
|
+
|
|
83
|
+
export function childStdout(): "inherit" | "pipe" {
|
|
84
|
+
return isatty(1) ? "inherit" : "pipe";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function relayStdout(source: ReadableStream<Uint8Array>, onLine?: (line: string) => void): (abandoned?: () => boolean) => Promise<void> {
|
|
88
|
+
let ended = false;
|
|
89
|
+
let blocked = false;
|
|
90
|
+
let readAt = Date.now();
|
|
91
|
+
const forwarded = source.pipeThrough(
|
|
92
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
93
|
+
async transform(chunk, controller) {
|
|
94
|
+
readAt = Date.now();
|
|
95
|
+
if (!process.stdout.write(chunk)) {
|
|
96
|
+
blocked = true;
|
|
97
|
+
await once(process.stdout, "drain");
|
|
98
|
+
blocked = false;
|
|
99
|
+
}
|
|
100
|
+
controller.enqueue(chunk);
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
void (async () => {
|
|
105
|
+
if (onLine == null) return forwarded.pipeTo(new WritableStream());
|
|
106
|
+
for await (const line of readLines(forwarded)) {
|
|
107
|
+
try {
|
|
108
|
+
onLine(line);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
log("supervisor.tee_failed", { err: errorMessage(e) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
})()
|
|
114
|
+
.catch((e: unknown) => log("supervisor.stdout_relay_failed", { err: errorMessage(e) }))
|
|
115
|
+
.finally(() => {
|
|
116
|
+
ended = true;
|
|
117
|
+
});
|
|
118
|
+
return async (abandoned = () => false) => {
|
|
119
|
+
const exitedAt = Date.now();
|
|
120
|
+
while (!ended && (blocked ? !abandoned() : Date.now() - Math.max(readAt, exitedAt) < STDOUT_QUIET_MS)) await Bun.sleep(50);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
export async function runPassthrough(input: { real: string; argv: string[]; env: Record<string, string | undefined>; onSpawn?: (child: Subprocess) => void | Promise<void> }): Promise<number> {
|
|
79
|
-
|
|
125
|
+
let terminated = false;
|
|
126
|
+
process.on("SIGTERM", () => { terminated = true; });
|
|
127
|
+
const p = Bun.spawn([input.real, ...input.argv], { stdin: "inherit", stdout: childStdout(), stderr: "inherit", env: input.env });
|
|
128
|
+
const drain = p.stdout instanceof ReadableStream ? relayStdout(p.stdout) : null;
|
|
80
129
|
await input.onSpawn?.(p);
|
|
81
130
|
await p.exited;
|
|
131
|
+
await drain?.(() => terminated);
|
|
82
132
|
return exitStatus(p);
|
|
83
133
|
}
|
|
84
134
|
|
|
85
|
-
export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
|
|
135
|
+
export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; apiKeyId?: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
|
|
86
136
|
for (let attempt = 0; ; attempt++) {
|
|
87
137
|
try {
|
|
88
|
-
writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid });
|
|
138
|
+
writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid, ...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}) });
|
|
89
139
|
return;
|
|
90
140
|
} catch (e) {
|
|
91
141
|
if (input.child.exitCode !== null || input.child.signalCode !== null) return;
|
package/src/lib/types.ts
CHANGED
|
@@ -169,8 +169,38 @@ export const RespawnMarkerSchema = z.object({
|
|
|
169
169
|
origin: z.enum(["stop", "sessionstart", "stopfailure", "seatwatch"]),
|
|
170
170
|
launchedAt: z.number().optional(),
|
|
171
171
|
refused: z.array(z.string()).optional(),
|
|
172
|
+
apiKeyId: z.string().optional(),
|
|
172
173
|
});
|
|
173
174
|
|
|
175
|
+
export const ApiKeySchema = z.object({
|
|
176
|
+
id: z.string(),
|
|
177
|
+
label: z.string(),
|
|
178
|
+
workspaceId: z.string().optional(),
|
|
179
|
+
creditUsd: z.number().optional(),
|
|
180
|
+
spentUsd: z.number(),
|
|
181
|
+
creditSetAt: z.number(),
|
|
182
|
+
refusedAt: z.number().optional(),
|
|
183
|
+
addedAt: z.number(),
|
|
184
|
+
});
|
|
185
|
+
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
|
186
|
+
|
|
187
|
+
export const ApiKeysIndexSchema = z.object({
|
|
188
|
+
version: z.literal(1),
|
|
189
|
+
keys: z.array(ApiKeySchema).default([]),
|
|
190
|
+
baselines: z.record(z.string(), z.object({ usd: z.number(), at: z.number() })).default({}),
|
|
191
|
+
});
|
|
192
|
+
export type ApiKeysIndex = z.infer<typeof ApiKeysIndexSchema>;
|
|
193
|
+
|
|
194
|
+
export const SessionCostSchema = z.object({ sessionId: z.string(), usd: z.number() });
|
|
195
|
+
export type SessionCost = z.infer<typeof SessionCostSchema>;
|
|
196
|
+
|
|
197
|
+
export const CostLineSchema = z
|
|
198
|
+
.looseObject({ session_id: z.string(), total_cost_usd: z.number().optional(), cost: z.looseObject({ total_cost_usd: z.number() }).nullable().optional() })
|
|
199
|
+
.transform((line): SessionCost | null => {
|
|
200
|
+
const usd = line.total_cost_usd ?? line.cost?.total_cost_usd;
|
|
201
|
+
return usd == null ? null : { sessionId: line.session_id, usd };
|
|
202
|
+
});
|
|
203
|
+
|
|
174
204
|
export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
|
|
175
205
|
|
|
176
206
|
export const InstantSchema = z.iso.datetime({ offset: true }).transform((iso) => Date.parse(iso));
|
package/src/lib/usage.ts
CHANGED
|
@@ -105,7 +105,7 @@ export function transcriptRowText(row: TranscriptRow): string {
|
|
|
105
105
|
return blocks.data.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export const ENFORCED_ERRORS = ["rate_limit", "oauth_org_not_allowed"];
|
|
108
|
+
export const ENFORCED_ERRORS = ["rate_limit", "oauth_org_not_allowed", "billing_error"];
|
|
109
109
|
|
|
110
110
|
export function findEnforcedRow(input: { rows: TranscriptRow[]; error: string; lastAssistantMessage: string | undefined }): TranscriptRow | null {
|
|
111
111
|
const { rows, error, lastAssistantMessage } = input;
|
package/src/main.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { cmdStatus } from "./cli/status.ts";
|
|
|
20
20
|
import { cmdDoctor } from "./cli/doctor.ts";
|
|
21
21
|
import { cmdRm } from "./cli/rm.ts";
|
|
22
22
|
import { cmdRename } from "./cli/rename.ts";
|
|
23
|
+
import { cmdKey } from "./cli/key.ts";
|
|
23
24
|
import { cmdCheck } from "./cli/check.ts";
|
|
24
25
|
import { cmdConfig } from "./cli/config.ts";
|
|
25
26
|
import { cmdSeat } from "./cli/seat.ts";
|
|
@@ -38,7 +39,7 @@ const PI_FLAG = "--pi";
|
|
|
38
39
|
const JSON_COMMANDS = new Set(["status", "config", "check"]);
|
|
39
40
|
const CODEX_COMMANDS = new Set(["init", "add", "auth", "rm", "rename", "seat"]);
|
|
40
41
|
const STATUS_ONLY_COMMANDS = new Set(["init", "add", "auth", "rm", "rename"]);
|
|
41
|
-
const MAX_OPERANDS = new Map([["init", 0], ["add", 0], ["rm", 1], ["rename", 2]]);
|
|
42
|
+
const MAX_OPERANDS = new Map([["init", 0], ["add", 0], ["rm", 1], ["rename", 2], ["key", 4]]);
|
|
42
43
|
|
|
43
44
|
function printHelp(): void {
|
|
44
45
|
console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
|
|
@@ -58,6 +59,9 @@ function printHelp(): void {
|
|
|
58
59
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
59
60
|
${c.cyan("tokenmaxxing rename")} [--codex | --grok] <sel> <label>
|
|
60
61
|
${c.cyan("tokenmaxxing rm")} [--codex | --grok] <sel>
|
|
62
|
+
${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic API key read from stdin; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces)
|
|
63
|
+
${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count
|
|
64
|
+
${c.cyan("tokenmaxxing key rm")} <label> delete the key and its stored secret (refused while a session runs on it)
|
|
61
65
|
${c.cyan("tokenmaxxing seat")} <pid> lend one pooled Claude account to an unattended consumer until <pid> exits: prints the store directory to set as CLAUDE_SECURESTORAGE_CONFIG_DIR (the directory itself, as its owner, never a copy of its credential); host sessions and other borrowers share the account; exit 1 = none usable
|
|
62
66
|
${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, lent to <pid> until it exits; codex sessions and other borrowers share the account, and a refresh race between them can sign it out until auth --codex runs again; exit 1 = none usable, fall back to the ambient login
|
|
63
67
|
${c.cyan("tokenmaxxing serve")} serve the CLIProxyAPI-compatible usage API on http://localhost:<hub.port> (default 8317) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows every pooled Claude and Codex account's quota; the management key is the contents of hub-key in the state directory
|
|
@@ -173,6 +177,7 @@ async function main(): Promise<number> {
|
|
|
173
177
|
case "doctor": return cmdDoctor();
|
|
174
178
|
case "rm": return cmdRm(provider, args[1]);
|
|
175
179
|
case "rename": return cmdRename(provider, args.slice(1));
|
|
180
|
+
case "key": return cmdKey(args.slice(1));
|
|
176
181
|
case "seat": return cmdSeat(provider === codex ? "codex" : "claude", args[1], args.slice(2));
|
|
177
182
|
case "serve": return cmdServe(args.slice(1));
|
|
178
183
|
case "uninstall": {
|
|
@@ -198,7 +203,7 @@ async function main(): Promise<number> {
|
|
|
198
203
|
if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
|
|
199
204
|
if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
|
|
200
205
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
201
|
-
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
|
|
206
|
+
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/), plus api-keys.json and the API keys in api-keys/ - remove accounts with \`xx rm\` and keys with \`xx key rm\` to delete their credentials`);
|
|
202
207
|
return 0;
|
|
203
208
|
}
|
|
204
209
|
case "help":
|