tokenmaxxing 1.39.0 → 1.41.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 +31 -31
- package/package.json +2 -2
- package/src/cli/auth.ts +14 -10
- 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 +130 -154
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +19 -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 +3 -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 +11 -18
- 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,139 @@
|
|
|
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
|
-
|
|
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;
|
|
92
|
+
try {
|
|
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}\``);
|
|
96
|
+
}
|
|
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);
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function storeUsable(a: Account): Promise<boolean> {
|
|
101
112
|
try {
|
|
102
|
-
await
|
|
113
|
+
const creds = await readStore(a.id);
|
|
114
|
+
return creds != null && !isDeadCredential(creds);
|
|
103
115
|
} catch {
|
|
116
|
+
return false;
|
|
104
117
|
}
|
|
105
|
-
await Promise.all(accounts.filter((a) => a !== active).map(probeOne));
|
|
106
|
-
return reports;
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
export function pickSeat(now: number): Account | null {
|
|
121
|
+
const cfg = loadConfig();
|
|
122
|
+
const idx = loadAccounts(claudePool);
|
|
123
|
+
let dirty = false;
|
|
124
|
+
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
125
|
+
if (dirty) saveAccounts(claudePool, idx);
|
|
126
|
+
const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: cfg.policy.switchModels, seats: presence() };
|
|
127
|
+
return pickBest(idx.accounts, ctx) ?? pickEarliestReset(idx.accounts, ctx)?.account ?? null;
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
async function removeCredentials(a: Account): Promise<void> {
|
|
131
|
+
await deleteItem(storeTarget(a.id));
|
|
132
|
+
rmSync(storeDirFor(a.id), { recursive: true, force: true });
|
|
133
|
+
rmSync(`${storeDirFor(a.id)}.lock`, { recursive: true, force: true });
|
|
134
|
+
for (const dir of [sampleDirFor(a.id), sampleDirFor(a.id, "-tick")]) rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
clearUsageSnapshot(a.id);
|
|
117
136
|
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
137
|
if (setupTokens.tokens.some((t) => t.accountUuid === a.id)) {
|
|
125
138
|
saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.id) });
|
|
126
139
|
}
|
|
@@ -169,7 +182,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
169
182
|
await onExit;
|
|
170
183
|
restoreTermios(savedTermios);
|
|
171
184
|
|
|
172
|
-
const blobRaw = await readItem(iso);
|
|
185
|
+
const blobRaw = await withClaudeRefreshLock(onboardDir, () => readItem(iso));
|
|
173
186
|
if (!blobRaw || !identityReady(cjPath)) {
|
|
174
187
|
console.error(c.red("no login detected in the isolated session - nothing changed."));
|
|
175
188
|
return null;
|
|
@@ -183,9 +196,20 @@ async function login(): Promise<Harvest | null> {
|
|
|
183
196
|
console.error(c.red("could not parse the onboarded account's credential/identity."));
|
|
184
197
|
return null;
|
|
185
198
|
}
|
|
199
|
+
let identity;
|
|
200
|
+
try {
|
|
201
|
+
identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error(c.red(`could not verify which account the login belongs to (${e instanceof Error ? e.message : String(e)}) - nothing changed.`));
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
|
207
|
+
console.error(c.red(`the login's credential belongs to ${describeIdentity(identity)}, but its identity file names ${oauthAccount.emailAddress} - nothing changed.`));
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
186
210
|
|
|
187
211
|
console.log(c.dim("sampling usage..."));
|
|
188
|
-
const sampled = await probeUsage(onboardDir);
|
|
212
|
+
const sampled = await probeUsage({ configDir: onboardDir });
|
|
189
213
|
if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
|
|
190
214
|
const at = Date.now();
|
|
191
215
|
const id = oauthAccount.accountUuid;
|
|
@@ -195,7 +219,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
195
219
|
tier: claudeTierLabel(blob.claudeAiOauth),
|
|
196
220
|
oauthAccount,
|
|
197
221
|
sample: sampled ? { windows: windowsOf(sampled, at), at } : null,
|
|
198
|
-
park: () => writeItem(
|
|
222
|
+
park: () => writeItem(storeTarget(id), claudeAiOauthOnly(blobRaw)),
|
|
199
223
|
};
|
|
200
224
|
} finally {
|
|
201
225
|
if (p.exitCode === null) {
|
|
@@ -208,63 +232,13 @@ async function login(): Promise<Harvest | null> {
|
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
|
|
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
|
-
}
|
|
235
|
+
const loginStep = (who: string) => `In the session that opens, run ${c.bold("/login")} with ${who}. It closes itself once you're in.`;
|
|
217
236
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}
|
|
257
|
-
|
|
258
|
-
const id = oauthAccount.accountUuid;
|
|
259
|
-
const parked = JSON.stringify({ claudeAiOauth: creds });
|
|
260
|
-
return {
|
|
261
|
-
id,
|
|
262
|
-
email: oauthAccount.emailAddress,
|
|
263
|
-
tier: claudeTierLabel(creds),
|
|
264
|
-
oauthAccount,
|
|
265
|
-
sample: null,
|
|
266
|
-
park: () => writeItem(parkedTarget(credItemFor(id)), parked),
|
|
267
|
-
};
|
|
237
|
+
async function importLive(): Promise<Harvest | null> {
|
|
238
|
+
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."));
|
|
239
|
+
console.log(c.dim(loginStep("the first account to pool")));
|
|
240
|
+
console.log();
|
|
241
|
+
return login();
|
|
268
242
|
}
|
|
269
243
|
|
|
270
244
|
function ensurePathAhead(): void {
|
|
@@ -285,7 +259,7 @@ function ensurePathAhead(): void {
|
|
|
285
259
|
|
|
286
260
|
function install(): void {
|
|
287
261
|
const out = installSupervisor();
|
|
288
|
-
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/SessionStart hooks`);
|
|
262
|
+
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/StopFailure/SessionStart hooks`);
|
|
289
263
|
if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
|
|
290
264
|
else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
|
|
291
265
|
if (!out.pathAhead) {
|
|
@@ -298,23 +272,25 @@ export const claude: Provider = {
|
|
|
298
272
|
name: "claude",
|
|
299
273
|
flag: "",
|
|
300
274
|
pool: claudePool,
|
|
275
|
+
seats: "shared",
|
|
301
276
|
waitsWhenDepleted: true,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
277
|
+
liveId,
|
|
278
|
+
presence,
|
|
279
|
+
gatedFamilies: (cfg) => {
|
|
280
|
+
const seat = liveId();
|
|
281
|
+
return gatedFamilies(seat == null ? null : (loadUsageSnapshot(seat)?.state.model ?? null), cfg.policy.switchModels);
|
|
282
|
+
},
|
|
308
283
|
observeLive,
|
|
309
284
|
samplePool,
|
|
310
285
|
mergeWindows,
|
|
311
|
-
swap:
|
|
312
|
-
classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" :
|
|
286
|
+
swap: prepareMove,
|
|
287
|
+
classifySwapError: (e) => (e instanceof InvalidGrantError ? "dead-grant" : e instanceof StoreUnusableError ? "skip" : "fatal"),
|
|
313
288
|
removeCredentials,
|
|
289
|
+
storeUsable,
|
|
314
290
|
login,
|
|
315
291
|
importLive,
|
|
316
292
|
preflight: () => pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() }),
|
|
317
293
|
install,
|
|
318
|
-
loginStep
|
|
294
|
+
loginStep,
|
|
319
295
|
windowLabel: (name) => name.toLowerCase(),
|
|
320
296
|
};
|
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,
|
|
@@ -336,6 +334,13 @@ export const codex: Provider = {
|
|
|
336
334
|
swap,
|
|
337
335
|
classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : "fatal"),
|
|
338
336
|
removeCredentials: async (a) => deleteParkedCodexAuth({ credFile: codexCredItemFor(a.id) }),
|
|
337
|
+
storeUsable: async (a) => {
|
|
338
|
+
try {
|
|
339
|
+
return readParkedCodexAuth({ credFile: codexCredItemFor(a.id) }) != null;
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
339
344
|
login,
|
|
340
345
|
importLive,
|
|
341
346
|
preflight,
|