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
package/src/lib/claudejson.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { paths } from "./paths.ts";
|
|
4
|
-
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
-
import { OAuthAccountSchema, type OAuthAccount } from "./types.ts";
|
|
6
|
-
|
|
7
|
-
const ClaudeJsonSchema = z.record(z.string(), z.unknown());
|
|
8
|
-
|
|
9
|
-
function readClaudeJson(): Record<string, unknown> {
|
|
10
|
-
if (!existsSync(paths.claudeJson)) return {};
|
|
11
|
-
return ClaudeJsonSchema.parse(JSON.parse(readFileSync(paths.claudeJson, "utf8")));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function readOAuthAccount(): OAuthAccount | null {
|
|
15
|
-
const parsed = OAuthAccountSchema.safeParse(readClaudeJson()["oauthAccount"]);
|
|
16
|
-
return parsed.success ? parsed.data : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isApiKeyMode(): boolean {
|
|
20
|
-
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) return true;
|
|
21
|
-
const j = readClaudeJson();
|
|
22
|
-
if (z.string().min(1).safeParse(j["apiKeyHelper"]).success) return true;
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function swapOAuthAccount(next: OAuthAccount): void {
|
|
27
|
-
const j = readClaudeJson();
|
|
28
|
-
j["oauthAccount"] = next;
|
|
29
|
-
writeFileAtomic(paths.claudeJson, JSON.stringify(j, null, 2) + "\n", 0o600);
|
|
30
|
-
}
|
package/src/lib/codexpresence.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { codexPaths } from "./paths.ts";
|
|
5
|
-
import { writeFileAtomic } from "./atomic.ts";
|
|
6
|
-
import { pidExists, pidStartTime } from "./proc.ts";
|
|
7
|
-
import { ErrnoSchema, JsonTextSchema } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
const PresenceSchema = z.object({
|
|
10
|
-
accountId: z.string(),
|
|
11
|
-
pid: z.number(),
|
|
12
|
-
startedAt: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export function writeCodexPresence(input: { supervisorId: string; accountId: string; pid?: number }): void {
|
|
16
|
-
const pid = input.pid ?? process.pid;
|
|
17
|
-
const startedAt = pidStartTime(pid);
|
|
18
|
-
if (startedAt == null) throw new Error(`could not read pid ${pid}'s start time (ps lstart) - refusing to write an unverifiable presence file`);
|
|
19
|
-
mkdirSync(codexPaths.presenceDir, { recursive: true });
|
|
20
|
-
writeFileAtomic(
|
|
21
|
-
join(codexPaths.presenceDir, input.supervisorId),
|
|
22
|
-
JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid, startedAt })),
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function clearCodexPresence(input: { supervisorId: string }): void {
|
|
27
|
-
rmSync(join(codexPaths.presenceDir, input.supervisorId), { force: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const LivingPresenceSchema = z.object({ supervisorId: z.string(), accountId: z.string() });
|
|
31
|
-
export type LivingPresence = z.infer<typeof LivingPresenceSchema>;
|
|
32
|
-
|
|
33
|
-
export function livingCodexPresences(): LivingPresence[] {
|
|
34
|
-
const living: LivingPresence[] = [];
|
|
35
|
-
if (!existsSync(codexPaths.presenceDir)) return living;
|
|
36
|
-
for (const name of readdirSync(codexPaths.presenceDir)) {
|
|
37
|
-
const file = join(codexPaths.presenceDir, name);
|
|
38
|
-
let raw: string;
|
|
39
|
-
try {
|
|
40
|
-
raw = readFileSync(file, "utf8");
|
|
41
|
-
} catch (e) {
|
|
42
|
-
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
|
|
43
|
-
throw e;
|
|
44
|
-
}
|
|
45
|
-
const parsed = PresenceSchema.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
46
|
-
if (!parsed.success) {
|
|
47
|
-
throw new Error(`${file} is not a readable presence record - it may belong to a RUNNING codex session, refusing to treat it as absent; remove the file (or respawn that session) to proceed`);
|
|
48
|
-
}
|
|
49
|
-
const observed = pidStartTime(parsed.data.pid);
|
|
50
|
-
if (observed !== parsed.data.startedAt) {
|
|
51
|
-
if (observed == null && pidExists(parsed.data.pid)) {
|
|
52
|
-
throw new Error(`ps could not read the start time of live pid ${parsed.data.pid} (${file}) - refusing to clear a presence file that may guard a RUNNING codex session`);
|
|
53
|
-
}
|
|
54
|
-
rmSync(file, { force: true });
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
living.push(LivingPresenceSchema.parse({ supervisorId: name, accountId: parsed.data.accountId }));
|
|
58
|
-
}
|
|
59
|
-
return living;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function presentCodexAccountIds(): Set<string> {
|
|
63
|
-
return new Set(livingCodexPresences().map((presence) => presence.accountId));
|
|
64
|
-
}
|
package/src/lib/swap.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { clearDepletedWait, clearUsageSnapshots, loadAccounts, saveAccounts, saveLastSwapAt } from "./state.ts";
|
|
2
|
-
import { readItem, writeItem, liveTarget, parkedTarget, claudeAiOauthOnly, mergeIntoLive } from "./credstore.ts";
|
|
3
|
-
import { refreshCredential, isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity, IdentityUnavailableError, InvalidGrantError, RefreshRejectedError } from "./oauth.ts";
|
|
4
|
-
import { swapOAuthAccount } from "./claudejson.ts";
|
|
5
|
-
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
6
|
-
import { claudePool, credItemFor } from "./paths.ts";
|
|
7
|
-
import { log } from "./log.ts";
|
|
8
|
-
import { CredentialBlobSchema, type Account, type OAuthCreds, type TokenIdentity } from "./types.ts";
|
|
9
|
-
|
|
10
|
-
function parseBlob(raw: string) {
|
|
11
|
-
return CredentialBlobSchema.parse(JSON.parse(raw));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function performSwap(target: Account): Promise<void> {
|
|
15
|
-
const idx = loadAccounts(claudePool);
|
|
16
|
-
const oauthAccount = target.oauthAccount;
|
|
17
|
-
if (!oauthAccount) throw new Error(`${target.label} has no oauthAccount on its record - re-add it with \`tokenmaxxing auth ${target.label}\``);
|
|
18
|
-
|
|
19
|
-
const preLive = await readItem(liveTarget());
|
|
20
|
-
let liveOwner: Account | null = null;
|
|
21
|
-
let liveCreds: OAuthCreds | null = null;
|
|
22
|
-
let expectedLiveToken: string | null = null;
|
|
23
|
-
if (preLive) {
|
|
24
|
-
liveCreds = parseBlob(preLive).claudeAiOauth;
|
|
25
|
-
expectedLiveToken = liveCreds.accessToken;
|
|
26
|
-
if (isDeadCredential(liveCreds)) {
|
|
27
|
-
liveCreds = null;
|
|
28
|
-
log("swap.live_dead", {});
|
|
29
|
-
} else if (isAccessTokenExpiring(liveCreds, 60_000)) {
|
|
30
|
-
try {
|
|
31
|
-
await withClaudeRefreshLock(async (lock) => {
|
|
32
|
-
const raw2 = await readItem(liveTarget());
|
|
33
|
-
if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
|
|
34
|
-
const current = parseBlob(raw2).claudeAiOauth;
|
|
35
|
-
const next = isAccessTokenExpiring(current, 60_000) ? await refreshCredential(current) : current;
|
|
36
|
-
liveCreds = next;
|
|
37
|
-
expectedLiveToken = next.accessToken;
|
|
38
|
-
if (next === current) return;
|
|
39
|
-
if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
|
|
40
|
-
await writeItem(liveTarget(), mergeIntoLive(raw2, next));
|
|
41
|
-
});
|
|
42
|
-
} catch (e) {
|
|
43
|
-
if (!(e instanceof InvalidGrantError)) throw e;
|
|
44
|
-
liveCreds = null;
|
|
45
|
-
log("swap.harvest_skipped_dead_live", {});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (liveCreds != null) {
|
|
49
|
-
let identity: TokenIdentity;
|
|
50
|
-
try {
|
|
51
|
-
identity = await fetchTokenIdentity(liveCreds.accessToken);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
throw new Error(`cannot resolve the live credential's owner (${e instanceof Error ? e.message : String(e)}) - refusing to swap over it; the next check retries`);
|
|
54
|
-
}
|
|
55
|
-
liveOwner = idx.accounts.find((a) => a.id === identity.accountUuid) ?? null;
|
|
56
|
-
if (!liveOwner) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`live credential belongs to ${describeIdentity(identity)}, which is not in the pool - refusing to swap over it; import it first with \`tokenmaxxing add\``,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
if (liveOwner.id !== idx.activeId) {
|
|
62
|
-
log("swap.harvest_drift", {
|
|
63
|
-
labeled: idx.activeId?.slice(0, 8) ?? null,
|
|
64
|
-
actual: liveOwner.id.slice(0, 8),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const selfSwap = liveOwner != null && liveOwner.id === target.id;
|
|
71
|
-
let fresh: OAuthCreds | null = null;
|
|
72
|
-
if (!selfSwap) {
|
|
73
|
-
const parkedRaw = await readItem(parkedTarget(credItemFor(target.id)));
|
|
74
|
-
if (!parkedRaw) throw new Error(`no parked credential for ${target.label}`);
|
|
75
|
-
const parked = parseBlob(parkedRaw).claudeAiOauth;
|
|
76
|
-
const markDead = (detail: string): InvalidGrantError => {
|
|
77
|
-
const t = idx.accounts.find((a) => a.id === target.id);
|
|
78
|
-
if (t) { t.needsReauth = true; saveAccounts(claudePool, idx); }
|
|
79
|
-
log("swap.invalid_grant", { account: target.id.slice(0, 8), detail });
|
|
80
|
-
return new InvalidGrantError(detail);
|
|
81
|
-
};
|
|
82
|
-
if (isDeadCredential(parked)) throw markDead("parked credential was cleared after a failed refresh - re-auth with `tokenmaxxing auth`");
|
|
83
|
-
let parkedOwner: TokenIdentity | null = null;
|
|
84
|
-
try {
|
|
85
|
-
parkedOwner = await fetchTokenIdentity(parked.accessToken);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
if (!(e instanceof IdentityUnavailableError && e.status === 401)) throw e;
|
|
88
|
-
log("swap.parked_token_stale", { account: target.id.slice(0, 8) });
|
|
89
|
-
}
|
|
90
|
-
if (parkedOwner != null && parkedOwner.accountUuid !== target.id) {
|
|
91
|
-
const owner = idx.accounts.find((a) => a.id === parkedOwner.accountUuid) ?? null;
|
|
92
|
-
let kept = "was left in place";
|
|
93
|
-
if (owner != null && owner.id !== liveOwner?.id) {
|
|
94
|
-
await writeItem(parkedTarget(credItemFor(owner.id)), JSON.stringify({ claudeAiOauth: parked }));
|
|
95
|
-
log("swap.parked_relocated", { account: owner.id.slice(0, 8) });
|
|
96
|
-
kept = "was copied to that account's slot";
|
|
97
|
-
}
|
|
98
|
-
throw markDead(`parked credential belongs to ${describeIdentity(parkedOwner)} - refusing to spend another account's grant; the pair ${kept}; re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
99
|
-
}
|
|
100
|
-
const refreshParked = async (): Promise<OAuthCreds> => {
|
|
101
|
-
try {
|
|
102
|
-
return await refreshCredential(parked);
|
|
103
|
-
} catch (e) {
|
|
104
|
-
if (e instanceof InvalidGrantError) throw markDead(e.detail);
|
|
105
|
-
if (e instanceof RefreshRejectedError) log("swap.refresh_rejected", { account: target.id.slice(0, 8), status: e.status, detail: e.detail });
|
|
106
|
-
throw e;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
if (parkedOwner != null) {
|
|
110
|
-
fresh = await refreshParked();
|
|
111
|
-
} else {
|
|
112
|
-
fresh = await withClaudeRefreshLock(async (lock) => {
|
|
113
|
-
const rotated = await refreshParked();
|
|
114
|
-
let freshOwner: TokenIdentity;
|
|
115
|
-
try {
|
|
116
|
-
freshOwner = await fetchTokenIdentity(rotated.accessToken);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
await writeItem(parkedTarget(credItemFor(target.id)), JSON.stringify({ claudeAiOauth: rotated }));
|
|
119
|
-
throw new Error(`refreshed the parked credential but cannot verify its owner (${e instanceof Error ? e.message : String(e)}) - kept the rotated token parked; the next check retries`);
|
|
120
|
-
}
|
|
121
|
-
if (freshOwner.accountUuid !== target.id) {
|
|
122
|
-
const owner = idx.accounts.find((a) => a.id === freshOwner.accountUuid) ?? null;
|
|
123
|
-
const kept = await keepRotatedPair({ fresh: rotated, owner, fallback: target, liveOwnerId: liveOwner?.id ?? null, expectedLiveToken, lock });
|
|
124
|
-
throw markDead(`parked credential belongs to ${describeIdentity(freshOwner)}, whose grant this refresh rotated - the rotated token ${kept}; re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
125
|
-
}
|
|
126
|
-
return rotated;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
await writeItem(parkedTarget(credItemFor(target.id)), JSON.stringify({ claudeAiOauth: fresh }));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await withClaudeRefreshLock(async (lock) => {
|
|
133
|
-
const currentLive = await readItem(liveTarget());
|
|
134
|
-
if (lock.compromised()) throw new Error("refresh lock compromised - aborting the swap before any write");
|
|
135
|
-
|
|
136
|
-
const currentToken = currentLive == null ? null : parseBlob(currentLive).claudeAiOauth.accessToken;
|
|
137
|
-
if (currentToken !== expectedLiveToken) {
|
|
138
|
-
throw new Error("live credential changed while unlocked (concurrent /login or refresh) - aborting this swap; the next check re-resolves the owner and retries");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (liveOwner && currentLive) {
|
|
142
|
-
await writeItem(parkedTarget(credItemFor(liveOwner.id)), claudeAiOauthOnly(currentLive));
|
|
143
|
-
log("swap.harvest", { account: liveOwner.id.slice(0, 8) });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (fresh != null) {
|
|
147
|
-
await writeItem(liveTarget(), mergeIntoLive(currentLive, fresh));
|
|
148
|
-
}
|
|
149
|
-
swapOAuthAccount(oauthAccount);
|
|
150
|
-
idx.activeId = target.id;
|
|
151
|
-
const t2 = idx.accounts.find((a) => a.id === target.id);
|
|
152
|
-
if (t2) { t2.needsReauth = false; }
|
|
153
|
-
saveAccounts(claudePool, idx);
|
|
154
|
-
clearUsageSnapshots();
|
|
155
|
-
clearDepletedWait();
|
|
156
|
-
saveLastSwapAt(claudePool, Date.now());
|
|
157
|
-
});
|
|
158
|
-
log("swap.done", { account: target.id.slice(0, 8), label: target.label });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function keepRotatedPair(input: {
|
|
162
|
-
fresh: OAuthCreds;
|
|
163
|
-
owner: Account | null;
|
|
164
|
-
fallback: Account;
|
|
165
|
-
liveOwnerId: string | null;
|
|
166
|
-
expectedLiveToken: string | null;
|
|
167
|
-
lock: { compromised: () => boolean };
|
|
168
|
-
}): Promise<string> {
|
|
169
|
-
const { fresh, owner } = input;
|
|
170
|
-
if (owner == null) {
|
|
171
|
-
await writeItem(parkedTarget(credItemFor(input.fallback.id)), JSON.stringify({ claudeAiOauth: fresh }));
|
|
172
|
-
log("swap.rotated_kept_in_stamped_slot", { account: input.fallback.id.slice(0, 8) });
|
|
173
|
-
return "was kept in this account's stamped slot because its owner is not in the pool";
|
|
174
|
-
}
|
|
175
|
-
const park = async (): Promise<void> => {
|
|
176
|
-
await writeItem(parkedTarget(credItemFor(owner.id)), JSON.stringify({ claudeAiOauth: fresh }));
|
|
177
|
-
log("swap.rotated_parked_rescued", { account: owner.id.slice(0, 8) });
|
|
178
|
-
};
|
|
179
|
-
if (input.liveOwnerId !== owner.id) {
|
|
180
|
-
await park();
|
|
181
|
-
return "was parked under that account";
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
const currentLive = await readItem(liveTarget());
|
|
185
|
-
if (input.lock.compromised()) throw new Error("refresh lock compromised");
|
|
186
|
-
if (currentLive == null || parseBlob(currentLive).claudeAiOauth.accessToken !== input.expectedLiveToken) throw new Error("live credential changed while unlocked");
|
|
187
|
-
await writeItem(liveTarget(), mergeIntoLive(currentLive, fresh));
|
|
188
|
-
log("swap.rotated_live_rescued", { account: owner.id.slice(0, 8) });
|
|
189
|
-
return "was installed into the live store so that account's sessions continue";
|
|
190
|
-
} catch (e) {
|
|
191
|
-
await park();
|
|
192
|
-
return `was parked under that account (${e instanceof Error ? e.message : String(e)})`;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function isSkippableSwapError(e: unknown): boolean {
|
|
197
|
-
return e instanceof InvalidGrantError || e instanceof RefreshRejectedError || e instanceof IdentityUnavailableError;
|
|
198
|
-
}
|