tokenmaxxing 1.76.0 → 1.78.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/doctor.ts +2 -2
- package/src/entries/codexstophook.ts +2 -3
- package/src/entries/codexsupervisor.ts +2 -3
- package/src/entries/supervisor.ts +2 -2
- package/src/lib/claude.ts +2 -2
- package/src/lib/claudebin.ts +28 -33
- package/src/lib/codex.ts +4 -5
- package/src/lib/grok.ts +15 -22
- package/src/lib/grokusage.ts +4 -8
- package/src/lib/install.ts +2 -2
- package/src/lib/opencodego.ts +1 -1
- package/src/lib/state.ts +3 -1
- package/src/lib/statusonly.ts +8 -25
- package/src/lib/types.ts +2 -0
- package/src/lib/usage.ts +2 -2
- package/src/lib/codexbin.ts +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.78.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { CLAUDE_BIN, verifyRealBin } from "../lib/claudebin.ts";
|
|
3
3
|
import { checkSettings, installedBin } from "../lib/settings.ts";
|
|
4
4
|
import { checkTimerHealthy, codexStoreHookTrust, findClaudeShadowers, hubServiceHealthy, isBinDirAhead, shellRcPath, skipImperativeHub, timerActivationHint } from "../lib/install.ts";
|
|
5
5
|
import { errorMessage } from "../lib/log.ts";
|
|
@@ -95,7 +95,7 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
95
95
|
const cfg = loadConfig();
|
|
96
96
|
check(!!cfg.claudeBin && existsSync(cfg.claudeBin), "real claude binary resolved", "set claudeBin in config.json");
|
|
97
97
|
if (cfg.claudeBin && existsSync(cfg.claudeBin)) {
|
|
98
|
-
const fail =
|
|
98
|
+
const fail = verifyRealBin({ ...CLAUDE_BIN, bin: cfg.claudeBin });
|
|
99
99
|
check(fail === null, "claudeBin launches the real claude", fail ?? undefined);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -2,9 +2,8 @@ import { join } from "node:path";
|
|
|
2
2
|
import type { z } from "zod";
|
|
3
3
|
import { codexPaths, optionalEnv } from "../lib/paths.ts";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
|
-
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "../lib/claudebin.ts";
|
|
5
|
+
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin } from "../lib/claudebin.ts";
|
|
6
6
|
import { codex, codexPickCtx } from "../lib/codex.ts";
|
|
7
|
-
import { resolveRealCodex } from "../lib/codexbin.ts";
|
|
8
7
|
import { compactCodexThread } from "../lib/compact.ts";
|
|
9
8
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
10
9
|
import { isExhausted } from "../lib/picker.ts";
|
|
@@ -26,7 +25,7 @@ async function compactBeforeMove(input: { sessionId: string | null; now: number
|
|
|
26
25
|
const env: Record<string, string | undefined> = { ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) };
|
|
27
26
|
delete env[CODEX_SUPERVISOR_ID_ENV];
|
|
28
27
|
log("codexstop.compact_start", { thread: sessionId.slice(0, 8), account: live.id.slice(0, 8) });
|
|
29
|
-
await compactCodexThread({ real:
|
|
28
|
+
await compactCodexThread({ real: resolveRealBin(CODEX_BIN), threadId: sessionId, env });
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
async function handleCodexStop(rawStdin: string): Promise<void> {
|
|
@@ -3,8 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
5
5
|
import { withLock } from "../lib/lock.ts";
|
|
6
|
-
import { UNMANAGED_ENV, WRAP_DEPTH_ENV, wrapDepth } from "../lib/claudebin.ts";
|
|
7
|
-
import { resolveRealCodex } from "../lib/codexbin.ts";
|
|
6
|
+
import { CODEX_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
8
7
|
import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
9
8
|
import { codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
10
9
|
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
@@ -74,7 +73,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
74
73
|
const { argv } = input;
|
|
75
74
|
if (loopGuardTripped("codex")) return 1;
|
|
76
75
|
|
|
77
|
-
const real =
|
|
76
|
+
const real = resolveRealBin(CODEX_BIN);
|
|
78
77
|
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
79
78
|
|
|
80
79
|
if (!shouldManageCodex({ argv }) || process.env[UNMANAGED_ENV]) {
|
|
@@ -4,7 +4,7 @@ import type { FileSink, Subprocess } from "bun";
|
|
|
4
4
|
import { maxBy } from "es-toolkit";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
7
|
-
import { UNMANAGED_ENV, WRAP_DEPTH_ENV,
|
|
7
|
+
import { CLAUDE_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
8
8
|
import { claude, pickSeat } from "../lib/claude.ts";
|
|
9
9
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
10
10
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
@@ -353,7 +353,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
353
353
|
}
|
|
354
354
|
let real: string;
|
|
355
355
|
try {
|
|
356
|
-
real =
|
|
356
|
+
real = resolveRealBin(CLAUDE_BIN);
|
|
357
357
|
} catch (e) {
|
|
358
358
|
if (!earlyStream) throw e;
|
|
359
359
|
process.stdout.write(systemLine(earlySid, `tokenmaxxing: ${errorMessage(e)}`));
|
package/src/lib/claude.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
|
|
6
6
|
import { ensurePathInRc, hubActivationHint, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
|
|
7
7
|
import { withLock } from "./lock.ts";
|
|
8
8
|
import { errorMessage, log } from "./log.ts";
|
|
@@ -151,7 +151,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
151
151
|
const iso = isolatedTarget(onboardDir);
|
|
152
152
|
await deleteItem(iso);
|
|
153
153
|
const cjPath = join(onboardDir, ".claude.json");
|
|
154
|
-
const real =
|
|
154
|
+
const real = resolveRealBin(CLAUDE_BIN);
|
|
155
155
|
|
|
156
156
|
const savedTermios = saveTermios();
|
|
157
157
|
const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
|
package/src/lib/claudebin.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { uniq } from "es-toolkit";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { errorMessage } from "./log.ts";
|
|
6
6
|
import { paths } from "./paths.ts";
|
|
7
|
-
import { loadConfig } from "./state.ts";
|
|
7
|
+
import { loadConfig, type BinKey } from "./state.ts";
|
|
8
8
|
import { writeFileAtomic } from "./atomic.ts";
|
|
9
9
|
|
|
10
10
|
export const WRAP_DEPTH_ENV = "TOKENMAXXING_WRAP_DEPTH";
|
|
@@ -49,75 +49,70 @@ export function pointsBackAtUs(bin: string): boolean {
|
|
|
49
49
|
return resolved != null && binDir != null && dirname(resolved) === binDir;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function scanPathCandidates(name: string): string[] {
|
|
53
53
|
const seen = new Set<string>();
|
|
54
54
|
const out: string[] = [];
|
|
55
55
|
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
56
56
|
if (!d) continue;
|
|
57
|
-
const cand = join(d,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
out.push(cand);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch { }
|
|
57
|
+
const cand = join(d, name);
|
|
58
|
+
if (!existsSync(cand) || !statSync(cand, { throwIfNoEntry: false })?.isFile() || pointsBackAtUs(cand)) continue;
|
|
59
|
+
const key = realpathOrNull(cand) ?? cand;
|
|
60
|
+
if (seen.has(key)) continue;
|
|
61
|
+
seen.add(key);
|
|
62
|
+
out.push(cand);
|
|
67
63
|
}
|
|
68
64
|
return out;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
export function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const cfg = loadConfig();
|
|
77
|
-
if (cfg.claudeBin) {
|
|
78
|
-
if (!existsSync(cfg.claudeBin)) {
|
|
79
|
-
throw new Error(`configured claudeBin does not exist: ${cfg.claudeBin} - fix config.json`);
|
|
67
|
+
export function resolveRealBin(input: { name: string; key: BinKey }): string {
|
|
68
|
+
const configured = loadConfig()[input.key];
|
|
69
|
+
if (configured) {
|
|
70
|
+
if (!existsSync(configured)) {
|
|
71
|
+
throw new Error(`configured ${input.key} does not exist: ${configured} - fix config.json`);
|
|
80
72
|
}
|
|
81
|
-
if (pointsBackAtUs(
|
|
73
|
+
if (pointsBackAtUs(configured)) {
|
|
82
74
|
throw new Error(
|
|
83
|
-
`configured
|
|
75
|
+
`configured ${input.key} (${configured}) is tokenmaxxing's own wrapper - spawning it recurses. Point ${input.key} at the real ${input.name} binary in ${paths.configJson}`,
|
|
84
76
|
);
|
|
85
77
|
}
|
|
86
|
-
return
|
|
78
|
+
return configured;
|
|
87
79
|
}
|
|
88
|
-
const scanned =
|
|
80
|
+
const scanned = scanPathCandidates(input.name)[0];
|
|
89
81
|
if (scanned) return scanned;
|
|
90
|
-
throw new Error(
|
|
82
|
+
throw new Error(`could not locate the real \`${input.name}\` binary (set ${input.key} in config.json)`);
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
export function
|
|
85
|
+
export function verifyRealBin(input: { bin: string; name: string; versionOk: (versionOutput: string) => boolean }): string | null {
|
|
94
86
|
const env = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH), TOKENMAXXING_PROBE: "1" };
|
|
95
87
|
let p;
|
|
96
88
|
try {
|
|
97
|
-
p = Bun.spawnSync([bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
89
|
+
p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
98
90
|
} catch (e) {
|
|
99
91
|
return errorMessage(e);
|
|
100
92
|
}
|
|
101
93
|
const outText = p.stdout.toString().trim();
|
|
102
94
|
const err = p.stderr.toString().trim();
|
|
103
95
|
if (p.exitCode === 0) {
|
|
104
|
-
if (
|
|
105
|
-
return `--version output does not identify
|
|
96
|
+
if (input.versionOk(outText)) return null;
|
|
97
|
+
return `--version output does not identify ${input.name}: "${outText.slice(0, 80)}"`;
|
|
106
98
|
}
|
|
107
99
|
if (err.includes(LOOP_DIAGNOSIS)) return "it leads back into the tokenmaxxing wrapper (recursion)";
|
|
108
100
|
return `--version exited ${p.exitCode ?? "on signal/timeout"}: ${(err || outText).slice(0, 160)}`;
|
|
109
101
|
}
|
|
110
102
|
|
|
103
|
+
export const CLAUDE_BIN = { name: "claude", key: "claudeBin", versionOk: (out: string) => out.toLowerCase().includes("claude") } as const;
|
|
104
|
+
export const CODEX_BIN = { name: "codex", key: "codexBin", versionOk: (out: string) => out.toLowerCase().includes("codex") } as const;
|
|
105
|
+
|
|
111
106
|
export function resolveVerifiedClaude(): string {
|
|
112
107
|
const candidates: string[] = [];
|
|
113
108
|
try {
|
|
114
|
-
candidates.push(
|
|
109
|
+
candidates.push(resolveRealBin(CLAUDE_BIN));
|
|
115
110
|
} catch { }
|
|
116
|
-
candidates.push(...
|
|
111
|
+
candidates.push(...scanPathCandidates(CLAUDE_BIN.name));
|
|
117
112
|
|
|
118
113
|
const failures: string[] = [];
|
|
119
114
|
for (const cand of uniq(candidates)) {
|
|
120
|
-
const fail =
|
|
115
|
+
const fail = verifyRealBin({ ...CLAUDE_BIN, bin: cand });
|
|
121
116
|
if (fail === null) return cand;
|
|
122
117
|
failures.push(`${cand}: ${fail}`);
|
|
123
118
|
}
|
package/src/lib/codex.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { writeFileAtomic } from "./atomic.ts";
|
|
4
|
-
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV } from "./claudebin.ts";
|
|
4
|
+
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
5
5
|
import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
|
|
6
|
-
import { resolveRealCodex, verifyRealCodex } from "./codexbin.ts";
|
|
7
6
|
import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
|
|
8
7
|
import { seatCounts } from "./presence.ts";
|
|
9
8
|
import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
|
|
@@ -113,7 +112,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
async function login(): Promise<Harvest | null> {
|
|
116
|
-
const real =
|
|
115
|
+
const real = resolveRealBin(CODEX_BIN);
|
|
117
116
|
const onboardDir = codexPaths.onboardDir;
|
|
118
117
|
rmSync(onboardDir, { recursive: true, force: true });
|
|
119
118
|
writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
|
|
@@ -179,8 +178,8 @@ function storePinnedAwayFromFile(): boolean {
|
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
function preflight(): void {
|
|
182
|
-
const real =
|
|
183
|
-
const fail =
|
|
181
|
+
const real = resolveRealBin(CODEX_BIN);
|
|
182
|
+
const fail = verifyRealBin({ ...CODEX_BIN, bin: real });
|
|
184
183
|
if (fail !== null) throw new Error(`codex binary failed verification: ${real}: ${fail}`);
|
|
185
184
|
pinBinOverride({ key: "codexBin", bin: real });
|
|
186
185
|
if (storePinnedAwayFromFile()) {
|
package/src/lib/grok.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
-
import { fetchGrokUsage, GrokUsageReadError } from "./grokusage.ts";
|
|
5
|
+
import { fetchGrokUsage, GrokUsageReadError, type GrokUsage } from "./grokusage.ts";
|
|
6
6
|
import { errorMessage } from "./log.ts";
|
|
7
7
|
import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
|
|
8
8
|
import type { Provider, SampleReport } from "./provider.ts";
|
|
9
9
|
import { loadAccounts, type Harvest } from "./state.ts";
|
|
10
10
|
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
11
|
-
import type
|
|
11
|
+
import { InstantSchema, type Account } from "./types.ts";
|
|
12
12
|
import { c } from "../cli/render.ts";
|
|
13
13
|
|
|
14
14
|
const GrokAuthEntrySchema = z.looseObject({
|
|
@@ -69,8 +69,7 @@ function credsIn(path: string): GrokCred[] {
|
|
|
69
69
|
const id = entry.user_id ?? entry.principal_id ?? key;
|
|
70
70
|
const token = entry.key ?? "";
|
|
71
71
|
if (id === "" || token === "") continue;
|
|
72
|
-
|
|
73
|
-
out.push({ id, token, expiresAt: expiresAt != null && Number.isFinite(expiresAt) ? expiresAt : null });
|
|
72
|
+
out.push({ id, token, expiresAt: InstantSchema.safeParse(entry.expires_at).data ?? null });
|
|
74
73
|
}
|
|
75
74
|
return out;
|
|
76
75
|
}
|
|
@@ -91,27 +90,21 @@ function bearerFor(accountId: string, now: number): { token: string; from: "stor
|
|
|
91
90
|
async function readGrokUsage(account: Account, now: number): Promise<{ ok: true; at: number } | { ok: false; reason: string }> {
|
|
92
91
|
const first = bearerFor(account.id, now);
|
|
93
92
|
if (first == null) return { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --grok`" };
|
|
93
|
+
let usage: GrokUsage;
|
|
94
94
|
try {
|
|
95
|
-
|
|
96
|
-
account.windows = usage.windows;
|
|
97
|
-
account.lastUsageAt = usage.at;
|
|
98
|
-
return { ok: true, at: usage.at };
|
|
95
|
+
usage = await fetchGrokUsage({ token: first.token, at: now });
|
|
99
96
|
} catch (e) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
account.lastUsageAt = usage.at;
|
|
107
|
-
return { ok: true, at: usage.at };
|
|
108
|
-
} catch (retry) {
|
|
109
|
-
return { ok: false, reason: retry instanceof GrokUsageReadError ? retry.message : errorMessage(retry) };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
97
|
+
const live = e instanceof GrokUsageReadError && e.status === 401 && first.from === "store" ? liveCred(account.id) : null;
|
|
98
|
+
if (live == null || live.token === first.token) return { ok: false, reason: errorMessage(e) };
|
|
99
|
+
try {
|
|
100
|
+
usage = await fetchGrokUsage({ token: live.token, at: now });
|
|
101
|
+
} catch (retry) {
|
|
102
|
+
return { ok: false, reason: errorMessage(retry) };
|
|
112
103
|
}
|
|
113
|
-
return { ok: false, reason: e instanceof GrokUsageReadError ? e.message : errorMessage(e) };
|
|
114
104
|
}
|
|
105
|
+
account.windows = usage.windows;
|
|
106
|
+
account.lastUsageAt = usage.at;
|
|
107
|
+
return { ok: true, at: usage.at };
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
async function samplePool(accounts: Account[], _liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
|
|
@@ -140,7 +133,7 @@ const base = statusOnlyProvider({
|
|
|
140
133
|
liveAuthPath: () => join(grokPaths.home, "auth.json"),
|
|
141
134
|
readAuth,
|
|
142
135
|
liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
|
|
143
|
-
versionOk: (out) => out.
|
|
136
|
+
versionOk: (out) => out.toLowerCase().includes("grok"),
|
|
144
137
|
importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
|
|
145
138
|
foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
|
|
146
139
|
installNotice: "grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts.",
|
package/src/lib/grokusage.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
3
|
import { errorMessage } from "./log.ts";
|
|
4
4
|
import { env } from "./paths.ts";
|
|
5
|
-
import { JsonTextSchema, type Window } from "./types.ts";
|
|
5
|
+
import { InstantSchema, JsonTextSchema, type Window } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
export class GrokUsageReadError extends Error {
|
|
8
8
|
status: number | null;
|
|
@@ -13,8 +13,6 @@ export class GrokUsageReadError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const InstantSchema = z.iso.datetime({ offset: true });
|
|
17
|
-
|
|
18
16
|
const CreditsPeriodSchema = z.looseObject({
|
|
19
17
|
start: InstantSchema,
|
|
20
18
|
end: InstantSchema,
|
|
@@ -57,17 +55,15 @@ export async function fetchGrokUsage(input: { token: string; at: number }): Prom
|
|
|
57
55
|
if (!parsed.success) {
|
|
58
56
|
throw new GrokUsageReadError("endpoint returned an unexpected body shape (withheld)");
|
|
59
57
|
}
|
|
60
|
-
const
|
|
61
|
-
const start = Date.parse(period.start);
|
|
62
|
-
const end = Date.parse(period.end);
|
|
58
|
+
const { start, end } = parsed.data.config.currentPeriod;
|
|
63
59
|
return {
|
|
64
60
|
at,
|
|
65
61
|
windows: [
|
|
66
62
|
{
|
|
67
63
|
name: null,
|
|
68
64
|
usedPercentage: parsed.data.config.creditUsagePercent,
|
|
69
|
-
resetsAt:
|
|
70
|
-
windowSeconds:
|
|
65
|
+
resetsAt: end,
|
|
66
|
+
windowSeconds: end > start ? Math.round((end - start) / 1000) : null,
|
|
71
67
|
sampledAt: at,
|
|
72
68
|
},
|
|
73
69
|
],
|
package/src/lib/install.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.ts";
|
|
7
7
|
import { writeFileAtomic } from "./atomic.ts";
|
|
8
8
|
import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
|
|
10
10
|
import { loadConfig } from "./state.ts";
|
|
11
11
|
import { ErrnoSchema } from "./types.ts";
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ export function isBinDirAhead(): boolean {
|
|
|
17
17
|
const ourIdx = dirs.indexOf(paths.binDir);
|
|
18
18
|
if (ourIdx < 0) return false;
|
|
19
19
|
try {
|
|
20
|
-
const realDir = dirname(
|
|
20
|
+
const realDir = dirname(resolveRealBin(CLAUDE_BIN));
|
|
21
21
|
const realIdx = dirs.indexOf(realDir);
|
|
22
22
|
return realIdx < 0 || ourIdx < realIdx;
|
|
23
23
|
} catch {
|
package/src/lib/opencodego.ts
CHANGED
|
@@ -63,7 +63,7 @@ export const opencodeGo: Provider = statusOnlyProvider({
|
|
|
63
63
|
liveAuthPath,
|
|
64
64
|
readAuth,
|
|
65
65
|
liveId: () => null,
|
|
66
|
-
versionOk: (out) => out
|
|
66
|
+
versionOk: (out) => out !== "",
|
|
67
67
|
importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
|
|
68
68
|
foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
|
|
69
69
|
installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
|
package/src/lib/state.ts
CHANGED
|
@@ -39,7 +39,9 @@ export function loadConfig(): Config {
|
|
|
39
39
|
return cfg;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export
|
|
42
|
+
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin";
|
|
43
|
+
|
|
44
|
+
export function pinBinOverride(input: { key: BinKey; bin: string }): void {
|
|
43
45
|
let raw: Record<string, unknown> = {};
|
|
44
46
|
if (existsSync(paths.configJson)) {
|
|
45
47
|
raw = z.record(z.string(), z.unknown()).parse(JSON.parse(readFileSync(paths.configJson, "utf8")));
|
package/src/lib/statusonly.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
3
4
|
import type { PoolPaths } from "./paths.ts";
|
|
4
5
|
import type { Provider, SampleReport } from "./provider.ts";
|
|
5
|
-
import {
|
|
6
|
+
import { pinBinOverride, type Harvest } from "./state.ts";
|
|
6
7
|
import type { Account } from "./types.ts";
|
|
7
8
|
import { c } from "../cli/render.ts";
|
|
8
9
|
|
|
@@ -31,23 +32,7 @@ export type StatusOnlySpec = {
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
|
|
34
|
-
|
|
35
|
-
const configured = loadConfig()[spec.binKey];
|
|
36
|
-
if (configured) {
|
|
37
|
-
if (!existsSync(configured)) throw new Error(`configured ${spec.binKey} does not exist: ${configured} - fix config.json`);
|
|
38
|
-
return configured;
|
|
39
|
-
}
|
|
40
|
-
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
41
|
-
if (!d) continue;
|
|
42
|
-
const cand = join(d, spec.binName);
|
|
43
|
-
try {
|
|
44
|
-
if (existsSync(cand)) return cand;
|
|
45
|
-
} catch {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
throw new Error(`could not locate the real \`${spec.binName}\` binary (set ${spec.binKey} in config.json)`);
|
|
50
|
-
}
|
|
35
|
+
const bin = { name: spec.binName, key: spec.binKey, versionOk: spec.versionOk };
|
|
51
36
|
|
|
52
37
|
async function storeUsable(a: Account): Promise<boolean> {
|
|
53
38
|
try {
|
|
@@ -66,7 +51,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
|
|
|
66
51
|
}
|
|
67
52
|
|
|
68
53
|
async function login(): Promise<Harvest | null> {
|
|
69
|
-
const real =
|
|
54
|
+
const real = resolveRealBin(bin);
|
|
70
55
|
const onboardDir = spec.onboardDir;
|
|
71
56
|
rmSync(onboardDir, { recursive: true, force: true });
|
|
72
57
|
mkdirSync(onboardDir, { recursive: true });
|
|
@@ -102,11 +87,9 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
|
|
|
102
87
|
}
|
|
103
88
|
|
|
104
89
|
function preflight(): void {
|
|
105
|
-
const real =
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
throw new Error(`${spec.binName} binary failed verification: ${real}`);
|
|
109
|
-
}
|
|
90
|
+
const real = resolveRealBin(bin);
|
|
91
|
+
const fail = verifyRealBin({ ...bin, bin: real });
|
|
92
|
+
if (fail !== null) throw new Error(`${spec.binName} binary failed verification: ${real}: ${fail}`);
|
|
110
93
|
pinBinOverride({ key: spec.binKey, bin: real });
|
|
111
94
|
}
|
|
112
95
|
|
package/src/lib/types.ts
CHANGED
|
@@ -152,6 +152,8 @@ export const RespawnMarkerSchema = z.object({
|
|
|
152
152
|
|
|
153
153
|
export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
|
|
154
154
|
|
|
155
|
+
export const InstantSchema = z.iso.datetime({ offset: true }).transform((iso) => Date.parse(iso));
|
|
156
|
+
|
|
155
157
|
const StdinWindowSchema = z.looseObject({ used_percentage: z.number(), resets_at: EpochSecondsSchema.nullable().optional() });
|
|
156
158
|
|
|
157
159
|
export const RateLimitsStdinSchema = z.looseObject({
|
package/src/lib/usage.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { http } from "./http.ts";
|
|
4
4
|
import { errorMessage, log } from "./log.ts";
|
|
5
5
|
import { env } from "./paths.ts";
|
|
6
|
-
import { EpochSecondsSchema, JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
|
|
6
|
+
import { EpochSecondsSchema, InstantSchema, JsonTextSchema, RateLimitsStdinSchema, type ModelInfo, type UsageWindow, type UsageWindows, type Window } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
const win = (w: { used_percentage: number; resets_at?: number | null }): UsageWindow => ({
|
|
9
9
|
usedPercentage: w.used_percentage,
|
|
@@ -185,7 +185,7 @@ export const OAUTH_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
|
185
185
|
const USAGE_URL = env("TOKENMAXXING_OAUTH_USAGE_URL", OAUTH_USAGE_URL);
|
|
186
186
|
const USAGE_DEADLINE_MS = 10_000;
|
|
187
187
|
|
|
188
|
-
const ResetsAtSchema =
|
|
188
|
+
const ResetsAtSchema = InstantSchema.nullish();
|
|
189
189
|
const UsageLimitSchema = z.looseObject({ utilization: z.number(), resets_at: ResetsAtSchema });
|
|
190
190
|
const UsageScopedLimitSchema = z.looseObject({
|
|
191
191
|
kind: z.string(),
|
package/src/lib/codexbin.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
|
|
4
|
-
import { errorMessage } from "./log.ts";
|
|
5
|
-
import { loadConfig } from "./state.ts";
|
|
6
|
-
import { paths } from "./paths.ts";
|
|
7
|
-
|
|
8
|
-
function scanPathForCodex(): string | null {
|
|
9
|
-
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
10
|
-
if (!d) continue;
|
|
11
|
-
const cand = join(d, "codex");
|
|
12
|
-
try {
|
|
13
|
-
if (existsSync(cand) && statSync(cand).isFile() && !pointsBackAtUs(cand)) return cand;
|
|
14
|
-
} catch {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveRealCodex(): string {
|
|
22
|
-
const cfg = loadConfig();
|
|
23
|
-
if (cfg.codexBin) {
|
|
24
|
-
if (!existsSync(cfg.codexBin)) {
|
|
25
|
-
throw new Error(`configured codexBin does not exist: ${cfg.codexBin} - fix config.json`);
|
|
26
|
-
}
|
|
27
|
-
if (pointsBackAtUs(cfg.codexBin)) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`configured codexBin (${cfg.codexBin}) is tokenmaxxing's own wrapper - spawning it recurses. Point codexBin at the real codex binary in ${paths.configJson}`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
return cfg.codexBin;
|
|
33
|
-
}
|
|
34
|
-
const scanned = scanPathForCodex();
|
|
35
|
-
if (scanned) return scanned;
|
|
36
|
-
throw new Error("could not locate the real `codex` binary (set codexBin in config.json)");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function verifyRealCodex(input: { bin: string }): string | null {
|
|
40
|
-
const env = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH), TOKENMAXXING_PROBE: "1" };
|
|
41
|
-
let p;
|
|
42
|
-
try {
|
|
43
|
-
p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
44
|
-
} catch (e) {
|
|
45
|
-
return errorMessage(e);
|
|
46
|
-
}
|
|
47
|
-
const outText = p.stdout.toString().trim();
|
|
48
|
-
const err = p.stderr.toString().trim();
|
|
49
|
-
if (p.exitCode === 0) {
|
|
50
|
-
if (outText.toLowerCase().includes("codex")) return null;
|
|
51
|
-
return `--version output does not identify codex: "${outText.slice(0, 80)}"`;
|
|
52
|
-
}
|
|
53
|
-
if (err.includes(LOOP_DIAGNOSIS)) return "it leads back into the tokenmaxxing wrapper (recursion)";
|
|
54
|
-
return `--version exited ${p.exitCode ?? "on signal/timeout"}: ${(err || outText).slice(0, 160)}`;
|
|
55
|
-
}
|