tokenmaxxing 1.39.0 → 1.40.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 +70 -62
- package/README.md +30 -30
- package/package.json +2 -2
- package/src/cli/check.ts +2 -2
- package/src/cli/doctor.ts +19 -32
- package/src/cli/init.ts +2 -2
- package/src/cli/rm.ts +3 -12
- package/src/cli/setuptoken.ts +2 -2
- package/src/cli/status.ts +8 -4
- package/src/cli/switch.ts +12 -71
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +9 -10
- package/src/entries/statusline.ts +5 -11
- package/src/entries/stopfailurehook.ts +12 -38
- package/src/entries/stophook.ts +8 -28
- package/src/entries/supervisor.ts +50 -10
- package/src/lib/claude.ts +122 -156
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +12 -14
- package/src/lib/credstore.ts +10 -21
- package/src/lib/decide.ts +24 -35
- package/src/lib/oauth.ts +2 -68
- package/src/lib/paths.ts +23 -13
- package/src/lib/picker.ts +8 -0
- package/src/lib/presence.ts +67 -0
- package/src/lib/provider.ts +2 -4
- package/src/lib/sample.ts +66 -259
- package/src/lib/sessions.ts +25 -0
- package/src/lib/state.ts +17 -42
- package/src/lib/types.ts +2 -11
- package/src/lib/usage.ts +7 -4
- package/src/main.ts +10 -17
- package/src/lib/claudejson.ts +0 -30
- package/src/lib/codexpresence.ts +0 -64
- package/src/lib/swap.ts +0 -198
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
|
+
import type { Subprocess } from "bun";
|
|
3
4
|
import { maxBy } from "es-toolkit";
|
|
4
5
|
import { z } from "zod";
|
|
5
|
-
import { paths } from "../lib/paths.ts";
|
|
6
|
+
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
6
7
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, resolveRealClaude, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
|
|
8
|
+
import { pickSeat } from "../lib/claude.ts";
|
|
9
|
+
import { withLock } from "../lib/lock.ts";
|
|
10
|
+
import { clearPresence, writePresence } from "../lib/presence.ts";
|
|
7
11
|
import { saveTermios, restoreTermios } from "../lib/tty.ts";
|
|
8
12
|
import { loadSessionFlags, pruneStaleSessions, saveSessionFlags } from "../lib/sessions.ts";
|
|
9
|
-
import {
|
|
13
|
+
import { loadAccounts } from "../lib/state.ts";
|
|
14
|
+
import { RespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
10
15
|
import { log } from "../lib/log.ts";
|
|
11
16
|
|
|
12
17
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
@@ -187,6 +192,25 @@ async function countdownWait(acct: string, until: number): Promise<boolean> {
|
|
|
187
192
|
return aborted;
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
|
|
196
|
+
for (let attempt = 0; ; attempt++) {
|
|
197
|
+
try {
|
|
198
|
+
writePresence({ dir: paths.presenceDir, id: sid, accountId: seat.id, pid: child.pid });
|
|
199
|
+
return;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
202
|
+
if (attempt >= 9) {
|
|
203
|
+
log("supervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
204
|
+
child.kill();
|
|
205
|
+
await child.exited;
|
|
206
|
+
restoreTermios(savedTermios);
|
|
207
|
+
throw new Error("could not write the session presence file - refusing to run a session whose seat placement cannot see");
|
|
208
|
+
}
|
|
209
|
+
await Bun.sleep(100);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
190
214
|
export async function runSupervisor(argv: string[]): Promise<number> {
|
|
191
215
|
const depth = wrapDepth();
|
|
192
216
|
if (depth >= MAX_WRAP_DEPTH) {
|
|
@@ -250,16 +274,28 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
250
274
|
|
|
251
275
|
let respawns = 0;
|
|
252
276
|
let overriddenUntil = 0;
|
|
277
|
+
let wanted: string | null = null;
|
|
253
278
|
while (true) {
|
|
254
279
|
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
255
|
-
log("supervisor.launch", { sid, respawns, args: launchArgs.join(" ") });
|
|
256
280
|
|
|
257
281
|
const gate: MarkerGate = { launchedAt: Date.now(), overriddenUntil };
|
|
258
|
-
const child =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
const child = await withLock(claudePool.lockFile, async () => {
|
|
283
|
+
const seat = (wanted == null ? null : (loadAccounts(claudePool).accounts.find((a) => a.id === wanted) ?? null)) ?? pickSeat(gate.launchedAt);
|
|
284
|
+
log("supervisor.launch", { sid, respawns, seat: seat?.id.slice(0, 8) ?? null, args: launchArgs.join(" ") });
|
|
285
|
+
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
286
|
+
stdin: "inherit",
|
|
287
|
+
stdout: "inherit",
|
|
288
|
+
stderr: "inherit",
|
|
289
|
+
env: {
|
|
290
|
+
...childEnv,
|
|
291
|
+
TOKENMAXXING_SUPERVISED: "1",
|
|
292
|
+
TOKENMAXXING_SESSION_ID: sid,
|
|
293
|
+
TOKENMAXXING_LAUNCHED_AT: String(gate.launchedAt),
|
|
294
|
+
...(seat ? { CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) } : {}),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
if (seat) await recordPresence(spawned, sid, seat, savedTermios);
|
|
298
|
+
return spawned;
|
|
263
299
|
});
|
|
264
300
|
|
|
265
301
|
let done = false;
|
|
@@ -276,6 +312,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
276
312
|
if (winner === "marker") child.kill();
|
|
277
313
|
} catch (e) {
|
|
278
314
|
child.kill();
|
|
315
|
+
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
279
316
|
throw e;
|
|
280
317
|
} finally {
|
|
281
318
|
await child.exited;
|
|
@@ -288,13 +325,16 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
288
325
|
if (m) {
|
|
289
326
|
rmSync(marker, { force: true });
|
|
290
327
|
respawns++;
|
|
328
|
+
const label = loadAccounts(claudePool).accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
|
|
291
329
|
if (m.waitUntil > Date.now()) {
|
|
292
|
-
if (await countdownWait(
|
|
293
|
-
} else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing:
|
|
330
|
+
if (await countdownWait(label, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
331
|
+
} else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`);
|
|
332
|
+
wanted = m.accountId;
|
|
294
333
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
295
334
|
launchArgs = ["--resume", m.sessionId, ...persistable];
|
|
296
335
|
continue;
|
|
297
336
|
}
|
|
337
|
+
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
298
338
|
log("supervisor.exit", { sid, respawns, code: child.exitCode, signal: child.signalCode });
|
|
299
339
|
return child.exitCode ?? (child.signalCode ? 1 : 0);
|
|
300
340
|
}
|
package/src/lib/claude.ts
CHANGED
|
@@ -1,126 +1,130 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { readItem, writeItem, deleteItem,
|
|
4
|
+
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
5
5
|
import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
|
|
6
6
|
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
7
|
-
import { isApiKeyMode, readOAuthAccount } from "./claudejson.ts";
|
|
8
7
|
import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
|
|
9
8
|
import { withLock } from "./lock.ts";
|
|
10
9
|
import { log } from "./log.ts";
|
|
11
|
-
import { claudeTierLabel, describeIdentity, fetchTokenIdentity,
|
|
12
|
-
import { claudePool,
|
|
10
|
+
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
11
|
+
import { claudePool, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
12
|
+
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
13
|
+
import { seatCounts } from "./presence.ts";
|
|
13
14
|
import type { Observation, Provider, SampleReport } from "./provider.ts";
|
|
14
|
-
import {
|
|
15
|
-
import { loadAccounts,
|
|
16
|
-
import { isSkippableSwapError, performSwap } from "./swap.ts";
|
|
15
|
+
import { foldTee, probeAccountUsage, teeObservation } from "./sample.ts";
|
|
16
|
+
import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
|
|
17
17
|
import { loadSetupTokens, saveSetupTokens } from "./setuptokens.ts";
|
|
18
18
|
import { saveTermios, restoreTermios } from "./tty.ts";
|
|
19
|
-
import { CRED_ENV_OVERRIDES,
|
|
20
|
-
import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config
|
|
19
|
+
import { CRED_ENV_OVERRIDES, gatedFamilies, mergeWindows, probeUsage, windowsOf } from "./usage.ts";
|
|
20
|
+
import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
|
|
21
21
|
import { c } from "../cli/render.ts";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
class StoreUnusableError extends Error {
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "StoreUnusableError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function liveId(): string | null {
|
|
31
|
+
return seatFromEnv(loadAccounts(claudePool).accounts.map((a) => a.id));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function presence(): Map<string, number> {
|
|
35
|
+
return seatCounts(paths.presenceDir);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
async function observeLive(account: Account, cfg: Config, now: number, opts: { probe: boolean }): Promise<Observation | null> {
|
|
32
|
-
let snap = loadUsageSnapshot();
|
|
33
39
|
const ttl = cfg.policy.usagePollTtlMs;
|
|
34
40
|
const probeAttempted = account.lastProbeAt != null && now - account.lastProbeAt <= ttl;
|
|
35
|
-
const
|
|
41
|
+
const snap = loadUsageSnapshot(account.id);
|
|
42
|
+
const fresh = snap != null && now - snap.at <= ttl;
|
|
36
43
|
const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
|
|
37
44
|
if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
|
|
38
45
|
const startedAt = Date.now();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
const expected = gatedFamilies(snap.state.model, cfg.policy.switchModels);
|
|
52
|
-
const rows = Object.keys(full.perModel);
|
|
53
|
-
if (expected.length > 0 && !expected.some((f) => rows.some((k) => familyTokens(k).includes(f)))) {
|
|
54
|
-
log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
|
|
55
|
-
}
|
|
46
|
+
const outcome = await probeAccountUsage(account);
|
|
47
|
+
await withLock(claudePool.lockFile, () => {
|
|
48
|
+
const idx = loadAccounts(claudePool);
|
|
49
|
+
const a = idx.accounts.find((x) => x.id === account.id);
|
|
50
|
+
if (!a) return;
|
|
51
|
+
a.lastProbeAt = startedAt;
|
|
52
|
+
a.tier = account.tier;
|
|
53
|
+
if (account.needsReauth) a.needsReauth = true;
|
|
54
|
+
if (outcome.ok && (a.lastUsageAt == null || startedAt > a.lastUsageAt)) {
|
|
55
|
+
a.windows = mergeWindows(windowsOf(outcome.usage, startedAt), a.windows);
|
|
56
|
+
a.lastUsageAt = startedAt;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const a = idx.accounts.find((x) => x.id === account.id);
|
|
60
|
-
if (!a) return;
|
|
61
|
-
a.lastProbeAt = startedAt;
|
|
62
|
-
if (full) {
|
|
63
|
-
const probed = windowsOf(full, startedAt);
|
|
64
|
-
const aggregate = a.windows.some((w) => w.name == null) ? a.windows.filter((w) => w.name == null) : probed.filter((w) => w.name == null);
|
|
65
|
-
if (a.lastUsageAt == null) a.lastUsageAt = startedAt;
|
|
66
|
-
a.windows = mergeWindows([...aggregate, ...probed.filter((w) => w.name != null)], a.windows);
|
|
67
|
-
}
|
|
68
|
-
saveAccounts(claudePool, idx);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
58
|
+
saveAccounts(claudePool, idx);
|
|
59
|
+
});
|
|
71
60
|
}
|
|
72
61
|
const current = loadAccounts(claudePool).accounts.find((a) => a.id === account.id) ?? account;
|
|
73
|
-
return teeObservation(current
|
|
62
|
+
return teeObservation(current);
|
|
74
63
|
}
|
|
75
64
|
|
|
76
|
-
async function samplePool(accounts: Account[]
|
|
65
|
+
async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
|
|
77
66
|
const reports = new Map<string, SampleReport>();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
await Promise.all(
|
|
68
|
+
accounts.map(async (a) => {
|
|
69
|
+
const tee = loadUsageSnapshot(a.id);
|
|
70
|
+
const teeAt = tee == null ? null : (tee.state.sampledAt ?? tee.state.ts);
|
|
71
|
+
if (teeAt != null && (a.lastUsageAt == null || teeAt >= a.lastUsageAt)) {
|
|
72
|
+
foldTee(a);
|
|
73
|
+
reports.set(a.id, { ok: true, source: "statusline" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const outcome = await probeAccountUsage(a);
|
|
77
|
+
if (!outcome.ok) {
|
|
78
|
+
reports.set(a.id, { ok: false, reason: outcome.reason });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const at = Date.now();
|
|
84
82
|
a.lastUsageAt = at;
|
|
85
|
-
a.windows = mergeWindows(windowsOf(
|
|
86
|
-
reports.set(a.id, { ok: true, source: "
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const at = Date.now();
|
|
95
|
-
a.lastUsageAt = at;
|
|
96
|
-
a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
|
|
97
|
-
reports.set(a.id, { ok: true, source: "probe" });
|
|
98
|
-
};
|
|
99
|
-
const active = accounts.find((a) => liveId != null && liveId === a.id) ?? null;
|
|
100
|
-
if (active) await probeOne(active);
|
|
83
|
+
a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
|
|
84
|
+
reports.set(a.id, { ok: true, source: "probe" });
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
return reports;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function prepareMove(target: Account): Promise<void> {
|
|
91
|
+
let creds;
|
|
101
92
|
try {
|
|
102
|
-
await
|
|
103
|
-
} catch {
|
|
93
|
+
creds = await readStore(target.id);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
104
96
|
}
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
if (!creds) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
98
|
+
if (isDeadCredential(creds)) {
|
|
99
|
+
const idx = loadAccounts(claudePool);
|
|
100
|
+
const t = idx.accounts.find((a) => a.id === target.id);
|
|
101
|
+
if (t) {
|
|
102
|
+
t.needsReauth = true;
|
|
103
|
+
saveAccounts(claudePool, idx);
|
|
104
|
+
}
|
|
105
|
+
log("move.invalid_grant", { account: target.id.slice(0, 8) });
|
|
106
|
+
throw new InvalidGrantError(`${target.label}'s store was cleared after a failed refresh - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
107
|
+
}
|
|
108
|
+
log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
export function pickSeat(now: number): Account | null {
|
|
112
|
+
const cfg = loadConfig();
|
|
113
|
+
const idx = loadAccounts(claudePool);
|
|
114
|
+
let dirty = false;
|
|
115
|
+
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
116
|
+
if (dirty) saveAccounts(claudePool, idx);
|
|
117
|
+
const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: cfg.policy.switchModels, seats: presence() };
|
|
118
|
+
return pickBest(idx.accounts, ctx) ?? pickEarliestReset(idx.accounts, ctx)?.account ?? null;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
async function removeCredentials(a: Account): Promise<void> {
|
|
122
|
+
await deleteItem(storeTarget(a.id));
|
|
123
|
+
rmSync(storeDirFor(a.id), { recursive: true, force: true });
|
|
124
|
+
rmSync(`${storeDirFor(a.id)}.lock`, { recursive: true, force: true });
|
|
125
|
+
for (const dir of [sampleDirFor(a.id), sampleDirFor(a.id, "-tick")]) rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
clearUsageSnapshot(a.id);
|
|
117
127
|
const setupTokens = loadSetupTokens();
|
|
118
|
-
const item = credItemFor(a.id);
|
|
119
|
-
await deleteItem(parkedTarget(item));
|
|
120
|
-
for (const sampleDir of [join(paths.sampleDir, item), join(paths.sampleDir, `${item}-tick`)]) {
|
|
121
|
-
await deleteItem(isolatedTarget(sampleDir));
|
|
122
|
-
rmSync(sampleDir, { recursive: true, force: true });
|
|
123
|
-
}
|
|
124
128
|
if (setupTokens.tokens.some((t) => t.accountUuid === a.id)) {
|
|
125
129
|
saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.id) });
|
|
126
130
|
}
|
|
@@ -169,7 +173,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
169
173
|
await onExit;
|
|
170
174
|
restoreTermios(savedTermios);
|
|
171
175
|
|
|
172
|
-
const blobRaw = await readItem(iso);
|
|
176
|
+
const blobRaw = await withClaudeRefreshLock(onboardDir, () => readItem(iso));
|
|
173
177
|
if (!blobRaw || !identityReady(cjPath)) {
|
|
174
178
|
console.error(c.red("no login detected in the isolated session - nothing changed."));
|
|
175
179
|
return null;
|
|
@@ -183,9 +187,20 @@ async function login(): Promise<Harvest | null> {
|
|
|
183
187
|
console.error(c.red("could not parse the onboarded account's credential/identity."));
|
|
184
188
|
return null;
|
|
185
189
|
}
|
|
190
|
+
let identity;
|
|
191
|
+
try {
|
|
192
|
+
identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error(c.red(`could not verify which account the login belongs to (${e instanceof Error ? e.message : String(e)}) - nothing changed.`));
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
|
198
|
+
console.error(c.red(`the login's credential belongs to ${describeIdentity(identity)}, but its identity file names ${oauthAccount.emailAddress} - nothing changed.`));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
186
201
|
|
|
187
202
|
console.log(c.dim("sampling usage..."));
|
|
188
|
-
const sampled = await probeUsage(onboardDir);
|
|
203
|
+
const sampled = await probeUsage({ configDir: onboardDir });
|
|
189
204
|
if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
|
|
190
205
|
const at = Date.now();
|
|
191
206
|
const id = oauthAccount.accountUuid;
|
|
@@ -195,7 +210,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
195
210
|
tier: claudeTierLabel(blob.claudeAiOauth),
|
|
196
211
|
oauthAccount,
|
|
197
212
|
sample: sampled ? { windows: windowsOf(sampled, at), at } : null,
|
|
198
|
-
park: () => writeItem(
|
|
213
|
+
park: () => writeItem(storeTarget(id), claudeAiOauthOnly(blobRaw)),
|
|
199
214
|
};
|
|
200
215
|
} finally {
|
|
201
216
|
if (p.exitCode === null) {
|
|
@@ -208,63 +223,13 @@ async function login(): Promise<Harvest | null> {
|
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
if (isApiKeyMode()) {
|
|
213
|
-
console.error(c.yellow("tokenmaxxing pools subscription accounts, but you're authed via API key / apiKeyHelper."));
|
|
214
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} with a Pro/Max account first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const oauthAccount = readOAuthAccount();
|
|
219
|
-
const liveRaw = await readItem(liveTarget());
|
|
220
|
-
if (!oauthAccount || !liveRaw) {
|
|
221
|
-
console.error(c.red("no active Claude subscription login found (missing oauthAccount or credential)."));
|
|
222
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let blob;
|
|
227
|
-
try {
|
|
228
|
-
blob = CredentialBlobSchema.parse(JSON.parse(liveRaw));
|
|
229
|
-
} catch {
|
|
230
|
-
console.error(c.red("the live credential is not a recognizable Claude OAuth blob."));
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
let creds = blob.claudeAiOauth;
|
|
235
|
-
if (isDeadCredential(creds)) {
|
|
236
|
-
console.error(c.red("the live credential was cleared after a failed refresh."));
|
|
237
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
if (isAccessTokenExpiring(creds)) {
|
|
241
|
-
await withClaudeRefreshLock(async (lock) => {
|
|
242
|
-
const raw2 = await readItem(liveTarget());
|
|
243
|
-
if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
|
|
244
|
-
const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
|
|
245
|
-
creds = isAccessTokenExpiring(current) ? await refreshCredential(current) : current;
|
|
246
|
-
if (creds === current) return;
|
|
247
|
-
if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
|
|
248
|
-
await writeItem(liveTarget(), mergeIntoLive(raw2, creds));
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
const identity = await fetchTokenIdentity(creds.accessToken);
|
|
252
|
-
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
|
253
|
-
console.error(c.red(`the live credential belongs to ${describeIdentity(identity)}, but ~/.claude.json identifies ${oauthAccount.emailAddress} - identity drift.`));
|
|
254
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} to realign them, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
226
|
+
const loginStep = (who: string) => `In the session that opens, run ${c.bold("/login")} with ${who}. It closes itself once you're in.`;
|
|
257
227
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
tier: claudeTierLabel(creds),
|
|
264
|
-
oauthAccount,
|
|
265
|
-
sample: null,
|
|
266
|
-
park: () => writeItem(parkedTarget(credItemFor(id)), parked),
|
|
267
|
-
};
|
|
228
|
+
async function importLive(): Promise<Harvest | null> {
|
|
229
|
+
console.log(c.cyan("Opening an isolated login for your first pooled account - the login you already have stays as it is for sessions started outside the supervisor."));
|
|
230
|
+
console.log(c.dim(loginStep("the first account to pool")));
|
|
231
|
+
console.log();
|
|
232
|
+
return login();
|
|
268
233
|
}
|
|
269
234
|
|
|
270
235
|
function ensurePathAhead(): void {
|
|
@@ -285,7 +250,7 @@ function ensurePathAhead(): void {
|
|
|
285
250
|
|
|
286
251
|
function install(): void {
|
|
287
252
|
const out = installSupervisor();
|
|
288
|
-
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/SessionStart hooks`);
|
|
253
|
+
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/StopFailure/SessionStart hooks`);
|
|
289
254
|
if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
|
|
290
255
|
else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
|
|
291
256
|
if (!out.pathAhead) {
|
|
@@ -298,23 +263,24 @@ export const claude: Provider = {
|
|
|
298
263
|
name: "claude",
|
|
299
264
|
flag: "",
|
|
300
265
|
pool: claudePool,
|
|
266
|
+
seats: "shared",
|
|
301
267
|
waitsWhenDepleted: true,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
268
|
+
liveId,
|
|
269
|
+
presence,
|
|
270
|
+
gatedFamilies: (cfg) => {
|
|
271
|
+
const seat = liveId();
|
|
272
|
+
return gatedFamilies(seat == null ? null : (loadUsageSnapshot(seat)?.state.model ?? null), cfg.policy.switchModels);
|
|
273
|
+
},
|
|
308
274
|
observeLive,
|
|
309
275
|
samplePool,
|
|
310
276
|
mergeWindows,
|
|
311
|
-
swap:
|
|
312
|
-
classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" :
|
|
277
|
+
swap: prepareMove,
|
|
278
|
+
classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" : e instanceof StoreUnusableError ? "skip" : "fatal"),
|
|
313
279
|
removeCredentials,
|
|
314
280
|
login,
|
|
315
281
|
importLive,
|
|
316
282
|
preflight: () => pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() }),
|
|
317
283
|
install,
|
|
318
|
-
loginStep
|
|
284
|
+
loginStep,
|
|
319
285
|
windowLabel: (name) => name.toLowerCase(),
|
|
320
286
|
};
|
package/src/lib/claudelock.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { delay } from "es-toolkit";
|
|
4
|
-
import { credDir } from "./paths.ts";
|
|
5
4
|
import { log } from "./log.ts";
|
|
6
5
|
import { ErrnoSchema } from "./types.ts";
|
|
7
6
|
|
|
@@ -28,12 +27,12 @@ function tryAcquire(lockDir: string): boolean {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export async function withClaudeRefreshLock<T>(
|
|
30
|
+
dir: string,
|
|
31
31
|
fn: (lock: { compromised: () => boolean }) => Promise<T> | T,
|
|
32
32
|
opts: { attempts?: number; retryMs?: number; heartbeatMs?: number } = {},
|
|
33
33
|
): Promise<T> {
|
|
34
34
|
const attempts = opts.attempts ?? ATTEMPTS;
|
|
35
35
|
const retryMs = opts.retryMs ?? RETRY_MS;
|
|
36
|
-
const dir = credDir();
|
|
37
36
|
mkdirSync(dir, { recursive: true });
|
|
38
37
|
const primary = join(dir, ".oauth_refresh.lock");
|
|
39
38
|
let legacyRoot = dir;
|
|
@@ -58,7 +57,7 @@ export async function withClaudeRefreshLock<T>(
|
|
|
58
57
|
if (attempt >= attempts) {
|
|
59
58
|
log("claudelock.contested", { attempts: attempt });
|
|
60
59
|
throw new Error(
|
|
61
|
-
"claude's credential-refresh lock is contested (a token refresh is likely mid-flight) - not touching the
|
|
60
|
+
"claude's credential-refresh lock is contested (a token refresh is likely mid-flight) - not touching the credential store; retry shortly",
|
|
62
61
|
);
|
|
63
62
|
}
|
|
64
63
|
await delay(retryMs + Math.random() * retryMs);
|
package/src/lib/codex.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "./claudebin.ts";
|
|
|
5
5
|
import { codexIdentityOf, deleteParkedCodexAuth, isCodexAccessExpiring, liveCodexAccountId, readCodexAuthAt, readLiveCodexAuth, readParkedCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
|
|
6
6
|
import { resolveRealCodex, verifyRealCodex } from "./codexbin.ts";
|
|
7
7
|
import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { livingPresences, seatCounts } from "./presence.ts";
|
|
9
9
|
import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
|
|
10
10
|
import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
11
11
|
import { withLock } from "./lock.ts";
|
|
@@ -33,7 +33,7 @@ async function sampleLiveOntoOwner(now: number): Promise<void> {
|
|
|
33
33
|
const owner = idx.accounts.find((a) => a.id === identity.accountId);
|
|
34
34
|
if (!owner) return;
|
|
35
35
|
|
|
36
|
-
if (isCodexAccessExpiring({ auth: live, now }) && !
|
|
36
|
+
if (isCodexAccessExpiring({ auth: live, now }) && !seatCounts(codexPaths.presenceDir).has(identity.accountId)) {
|
|
37
37
|
try {
|
|
38
38
|
live = await refreshCodexAuth({ auth: live, now });
|
|
39
39
|
} catch (e) {
|
|
@@ -69,7 +69,7 @@ async function sampleAccount(account: Account, liveId: string | null, now: numbe
|
|
|
69
69
|
let auth = isLive ? readLiveCodexAuth() : readParkedCodexAuth({ credFile });
|
|
70
70
|
if (!auth) return { ok: false, reason: isLive ? "live auth.json vanished" : "no parked credential", deadGrant: false };
|
|
71
71
|
if (isCodexAccessExpiring({ auth, now })) {
|
|
72
|
-
const running =
|
|
72
|
+
const running = seatCounts(codexPaths.presenceDir).has(account.id);
|
|
73
73
|
if (running && !isLive) {
|
|
74
74
|
return { ok: false, reason: "running in a live codex session (parked token refresh unsafe)", deadGrant: false };
|
|
75
75
|
}
|
|
@@ -232,7 +232,7 @@ async function importLive(): Promise<Harvest | null> {
|
|
|
232
232
|
const identity = codexIdentityOf({ auth: live });
|
|
233
233
|
const usage = await sampleLogin(live);
|
|
234
234
|
|
|
235
|
-
if (
|
|
235
|
+
if (seatCounts(codexPaths.presenceDir).has(identity.accountId)) {
|
|
236
236
|
console.error(c.red("a live supervised codex session is running this account - its token rotates under us, so parking a snapshot now could poison the backup."));
|
|
237
237
|
console.error(c.dim("close that codex session (or let it exit) and re-run `tokenmaxxing init --codex`."));
|
|
238
238
|
return null;
|
|
@@ -244,7 +244,7 @@ async function importLive(): Promise<Harvest | null> {
|
|
|
244
244
|
tier: usage?.usage.planType ?? identity.planType,
|
|
245
245
|
sample: usage ? { windows: usage.usage.windows, at: usage.at } : null,
|
|
246
246
|
park: async () => {
|
|
247
|
-
if (
|
|
247
|
+
if (seatCounts(codexPaths.presenceDir).has(identity.accountId)) {
|
|
248
248
|
throw new Error("a live supervised codex session started running this account mid-init - close it and re-run `tokenmaxxing init --codex`");
|
|
249
249
|
}
|
|
250
250
|
const fresh2 = readLiveCodexAuth();
|
|
@@ -292,7 +292,7 @@ function install(): void {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
export function codexPickCtx(now: number, currentId: string | null): PickCtx {
|
|
295
|
-
return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null };
|
|
295
|
+
return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null, seats: null };
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
export function reconcileSiblings(now: number): void {
|
|
@@ -301,9 +301,9 @@ export function reconcileSiblings(now: number): void {
|
|
|
301
301
|
const idx = loadAccounts(codexPool);
|
|
302
302
|
const liveAccount = idx.accounts.find((a) => a.id === liveId);
|
|
303
303
|
if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) return;
|
|
304
|
-
const living =
|
|
304
|
+
const living = livingPresences(codexPaths.presenceDir);
|
|
305
305
|
if (existsSync(codexPaths.reconcileDir)) {
|
|
306
|
-
const alive = new Set(living.map((presence) => presence.
|
|
306
|
+
const alive = new Set(living.map((presence) => presence.id));
|
|
307
307
|
for (const name of readdirSync(codexPaths.reconcileDir)) {
|
|
308
308
|
if (!alive.has(name)) rmSync(join(codexPaths.reconcileDir, name), { force: true });
|
|
309
309
|
}
|
|
@@ -311,11 +311,11 @@ export function reconcileSiblings(now: number): void {
|
|
|
311
311
|
for (const presence of living) {
|
|
312
312
|
if (presence.accountId === liveId) continue;
|
|
313
313
|
if (!idx.accounts.some((a) => a.id === presence.accountId)) continue;
|
|
314
|
-
const markerPath = join(codexPaths.reconcileDir, presence.
|
|
314
|
+
const markerPath = join(codexPaths.reconcileDir, presence.id);
|
|
315
315
|
if (existsSync(markerPath)) continue;
|
|
316
316
|
mkdirSync(codexPaths.reconcileDir, { recursive: true });
|
|
317
317
|
writeFileAtomic(markerPath, JSON.stringify(CodexReconcileMarkerSchema.parse({ accountId: presence.accountId, ts: now })));
|
|
318
|
-
log("codex.reconcile_signal", { supervisorId: presence.
|
|
318
|
+
log("codex.reconcile_signal", { supervisorId: presence.id.slice(0, 8), account: presence.accountId.slice(0, 8) });
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
@@ -323,12 +323,10 @@ export const codex: Provider = {
|
|
|
323
323
|
name: "codex",
|
|
324
324
|
flag: " --codex",
|
|
325
325
|
pool: codexPool,
|
|
326
|
+
seats: "live",
|
|
326
327
|
waitsWhenDepleted: false,
|
|
327
|
-
switchMargin: 1.2,
|
|
328
|
-
switchNote: " (takes effect on the next codex start)",
|
|
329
328
|
liveId: liveCodexAccountId,
|
|
330
|
-
|
|
331
|
-
presentIds: presentCodexAccountIds,
|
|
329
|
+
presence: () => seatCounts(codexPaths.presenceDir),
|
|
332
330
|
gatedFamilies: () => null,
|
|
333
331
|
observeLive,
|
|
334
332
|
samplePool,
|