tokenmaxxing 1.57.0 → 1.58.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 +6 -6
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli/auth.ts +6 -17
- package/src/cli/check.ts +2 -2
- package/src/cli/cloudrun.ts +12 -19
- package/src/cli/config.ts +2 -1
- package/src/cli/doctor.ts +5 -4
- package/src/cli/init.ts +0 -1
- package/src/cli/render.ts +24 -9
- package/src/cli/rm.ts +0 -4
- package/src/cli/status.ts +34 -57
- package/src/entries/codexstophook.ts +10 -23
- package/src/entries/codexsupervisor.ts +30 -72
- package/src/entries/statusline.ts +17 -26
- package/src/entries/stopfailurehook.ts +4 -5
- package/src/entries/stophook.ts +12 -10
- package/src/entries/subagentstatusline.ts +4 -10
- package/src/entries/supervisor.ts +51 -103
- package/src/lib/claude.ts +11 -12
- package/src/lib/claudebin.ts +3 -2
- package/src/lib/claudelock.ts +2 -2
- package/src/lib/cloud.ts +4 -8
- package/src/lib/codex.ts +3 -3
- package/src/lib/codexauth.ts +2 -11
- package/src/lib/codexbin.ts +2 -1
- package/src/lib/codexoauth.ts +3 -8
- package/src/lib/codexusage.ts +5 -5
- package/src/lib/compact.ts +5 -18
- package/src/lib/credstore.ts +1 -6
- package/src/lib/decide.ts +6 -24
- package/src/lib/grok.ts +38 -152
- package/src/lib/http.ts +1 -2
- package/src/lib/install.ts +20 -38
- package/src/lib/keychain.ts +1 -4
- package/src/lib/log.ts +5 -5
- package/src/lib/oauth.ts +6 -5
- package/src/lib/opencodego.ts +31 -140
- package/src/lib/paths.ts +6 -22
- package/src/lib/picker.ts +9 -20
- package/src/lib/presence.ts +4 -5
- package/src/lib/proc.ts +22 -0
- package/src/lib/sample.ts +25 -24
- package/src/lib/sessions.ts +5 -6
- package/src/lib/settings.ts +1 -8
- package/src/lib/state.ts +6 -29
- package/src/lib/statusonly.ts +143 -0
- package/src/lib/supervise.ts +111 -0
- package/src/lib/types.ts +27 -36
- package/src/lib/update.ts +6 -7
- package/src/lib/usage.ts +40 -191
- package/src/main.ts +5 -5
- package/src/entries/sessionstart.ts +0 -33
package/src/lib/claude.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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";
|
|
@@ -6,7 +6,7 @@ import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
|
|
|
6
6
|
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
7
7
|
import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
|
|
8
8
|
import { withLock } from "./lock.ts";
|
|
9
|
-
import { log } from "./log.ts";
|
|
9
|
+
import { errorMessage, log } from "./log.ts";
|
|
10
10
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
11
11
|
import { claudePool, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
12
12
|
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
@@ -16,8 +16,8 @@ import { foldTee, sampleAccountUsage, teeObservation } from "./sample.ts";
|
|
|
16
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 {
|
|
20
|
-
import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
|
|
19
|
+
import { fetchUsageDirect, gatedFamilies, mergeWindows, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
20
|
+
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
|
|
21
21
|
import { c } from "../cli/render.ts";
|
|
22
22
|
|
|
23
23
|
class StoreUnusableError extends Error {
|
|
@@ -46,7 +46,7 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
46
46
|
const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
|
|
47
47
|
if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
|
|
48
48
|
const startedAt = Date.now();
|
|
49
|
-
const outcome = await sampleAccountUsage(account
|
|
49
|
+
const outcome = await sampleAccountUsage(account);
|
|
50
50
|
await withLock(claudePool.lockFile, () => {
|
|
51
51
|
const idx = loadAccounts(claudePool);
|
|
52
52
|
const a = idx.accounts.find((x) => x.id === account.id);
|
|
@@ -100,7 +100,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
100
100
|
try {
|
|
101
101
|
creds = await readStore(target.id);
|
|
102
102
|
} catch (e) {
|
|
103
|
-
throw new StoreUnusableError(`${target.label}'s store is unreadable (${
|
|
103
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
104
104
|
}
|
|
105
105
|
if (!creds) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth ${target.label}\``);
|
|
106
106
|
if (isDeadCredential(creds)) {
|
|
@@ -147,11 +147,11 @@ async function removeCredentials(a: Account): Promise<void> {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
const IdentityReadySchema = z.looseObject({ oauthAccount: z.looseObject({ accountUuid: z.string().min(1) }) });
|
|
151
|
+
|
|
150
152
|
function identityReady(cjPath: string): boolean {
|
|
151
|
-
if (!existsSync(cjPath)) return false;
|
|
152
153
|
try {
|
|
153
|
-
|
|
154
|
-
return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
|
|
154
|
+
return IdentityReadySchema.safeParse(JsonTextSchema.safeParse(readFileSync(cjPath, "utf8")).data).success;
|
|
155
155
|
} catch {
|
|
156
156
|
return false;
|
|
157
157
|
}
|
|
@@ -167,8 +167,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
167
167
|
const real = resolveRealClaude();
|
|
168
168
|
|
|
169
169
|
const savedTermios = saveTermios();
|
|
170
|
-
const env
|
|
171
|
-
for (const key of CRED_ENV_OVERRIDES) delete env[key];
|
|
170
|
+
const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
|
|
172
171
|
const p = Bun.spawn([real], {
|
|
173
172
|
stdin: "inherit",
|
|
174
173
|
stdout: "inherit",
|
|
@@ -208,7 +207,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
208
207
|
try {
|
|
209
208
|
identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
|
|
210
209
|
} catch (e) {
|
|
211
|
-
console.error(c.red(`could not verify which account the login belongs to (${
|
|
210
|
+
console.error(c.red(`could not verify which account the login belongs to (${errorMessage(e)}) - nothing changed.`));
|
|
212
211
|
return null;
|
|
213
212
|
}
|
|
214
213
|
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
package/src/lib/claudebin.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync } from "nod
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { uniq } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { errorMessage } from "./log.ts";
|
|
5
6
|
import { paths } from "./paths.ts";
|
|
6
7
|
import { loadConfig } from "./state.ts";
|
|
7
8
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -9,7 +10,7 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
9
10
|
export const WRAP_DEPTH_ENV = "TOKENMAXXING_WRAP_DEPTH";
|
|
10
11
|
export const MAX_WRAP_DEPTH = 5;
|
|
11
12
|
export const UNMANAGED_ENV = "TOKENMAXXING_UNMANAGED";
|
|
12
|
-
export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real
|
|
13
|
+
export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real";
|
|
13
14
|
|
|
14
15
|
export function wrapDepth(env: Record<string, string | undefined> = process.env): number {
|
|
15
16
|
const n = Number(env[WRAP_DEPTH_ENV] ?? "");
|
|
@@ -96,7 +97,7 @@ export function verifyRealClaude(bin: string): string | null {
|
|
|
96
97
|
try {
|
|
97
98
|
p = Bun.spawnSync([bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
98
99
|
} catch (e) {
|
|
99
|
-
return
|
|
100
|
+
return errorMessage(e);
|
|
100
101
|
}
|
|
101
102
|
const outText = (p.stdout?.toString() ?? "").trim();
|
|
102
103
|
const err = (p.stderr?.toString() ?? "").trim();
|
package/src/lib/claudelock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { log } from "./log.ts";
|
|
4
|
+
import { errorMessage, log } from "./log.ts";
|
|
5
5
|
import { ErrnoSchema } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
const STALE_MS = 60_000;
|
|
@@ -48,7 +48,7 @@ export async function withClaudeRefreshLock<T>(
|
|
|
48
48
|
break;
|
|
49
49
|
}
|
|
50
50
|
} catch (e) {
|
|
51
|
-
log("claudelock.legacy_error", { err:
|
|
51
|
+
log("claudelock.legacy_error", { err: errorMessage(e) });
|
|
52
52
|
held.push(primary);
|
|
53
53
|
break;
|
|
54
54
|
}
|
package/src/lib/cloud.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { paths } from "./paths.ts";
|
|
|
6
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { UNMANAGED_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
8
8
|
import { pidExists } from "./proc.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
|
|
10
|
+
import { parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
|
|
11
11
|
|
|
12
12
|
export const TOKENS_ENV = "TOKENMAXXING_TOKENS";
|
|
13
13
|
export const FALLBACK_WALL_MS = 5 * 3_600_000;
|
|
@@ -73,11 +73,7 @@ export function saveCloudWalled(walled: Record<string, number>): void {
|
|
|
73
73
|
writeFileAtomic(paths.cloudWalledJson, JSON.stringify(WalledSchema.parse(walled), null, 2) + "\n");
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
z.object({ ok: z.literal(true), token: CloudTokenSchema }),
|
|
78
|
-
z.object({ ok: z.literal(false), earliestReset: z.number() }),
|
|
79
|
-
]);
|
|
80
|
-
export type Pick = z.infer<typeof PickSchema>;
|
|
76
|
+
export type Pick = { ok: true; token: CloudToken } | { ok: false; earliestReset: number };
|
|
81
77
|
|
|
82
78
|
export function pickToken(input: {
|
|
83
79
|
tokens: CloudToken[];
|
|
@@ -171,6 +167,6 @@ export function limitWallUntil(input: { sessionId: string; sinceBytes: number; n
|
|
|
171
167
|
const row = rows.findLast((r) => r.isApiErrorMessage === true);
|
|
172
168
|
if (!row || row.error !== "rate_limit" || row.apiErrorIsTransient === true) return null;
|
|
173
169
|
if ((row.quotaLimits?.rateLimitType ?? "") === "" && parseErrorBody(row.errorDetails)?.error?.type !== "rate_limit_error") return null;
|
|
174
|
-
const resetsAt = row.quotaLimits?.resetsAt
|
|
170
|
+
const resetsAt = row.quotaLimits?.resetsAt ?? null;
|
|
175
171
|
return resetsAt != null && resetsAt > input.now ? resetsAt : input.now + FALLBACK_WALL_MS;
|
|
176
172
|
}
|
package/src/lib/codex.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { 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";
|
|
12
|
-
import { log } from "./log.ts";
|
|
12
|
+
import { errorMessage, log } from "./log.ts";
|
|
13
13
|
import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
|
|
14
14
|
import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
15
15
|
import type { Observation, Provider, SampleReport } from "./provider.ts";
|
|
@@ -47,7 +47,7 @@ async function readCodexUsage(account: Account, now: number): Promise<CodexReadO
|
|
|
47
47
|
try {
|
|
48
48
|
auth = readCodexStoreAuth(account.id);
|
|
49
49
|
} catch (e) {
|
|
50
|
-
return { ok: false, reason: `store credential unreadable (${(e
|
|
50
|
+
return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
|
|
51
51
|
}
|
|
52
52
|
if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
|
|
53
53
|
try {
|
|
@@ -110,7 +110,7 @@ async function prepareMove(target: Account): Promise<void> {
|
|
|
110
110
|
try {
|
|
111
111
|
auth = readCodexStoreAuth(target.id);
|
|
112
112
|
} catch (e) {
|
|
113
|
-
throw new StoreUnusableError(`${target.label}'s store is unreadable (${
|
|
113
|
+
throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
|
|
114
114
|
}
|
|
115
115
|
if (!auth) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
|
|
116
116
|
log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -84,12 +84,7 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
|
|
|
84
84
|
return JSON.parse(payload);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
accountId: z.string(),
|
|
89
|
-
email: z.string().nullable(),
|
|
90
|
-
planType: z.string().nullable(),
|
|
91
|
-
});
|
|
92
|
-
export type CodexIdentity = z.infer<typeof CodexIdentitySchema>;
|
|
87
|
+
export type CodexIdentity = { accountId: string; email: string | null; planType: string | null };
|
|
93
88
|
|
|
94
89
|
export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
|
|
95
90
|
const { auth } = input;
|
|
@@ -99,11 +94,7 @@ export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
|
|
|
99
94
|
if (!accountId) {
|
|
100
95
|
throw new Error("codex credential carries no account id (neither tokens.account_id nor the id_token claim)");
|
|
101
96
|
}
|
|
102
|
-
return
|
|
103
|
-
accountId,
|
|
104
|
-
email: claims.email ?? null,
|
|
105
|
-
planType: authClaims?.chatgpt_plan_type ?? null,
|
|
106
|
-
});
|
|
97
|
+
return { accountId, email: claims.email ?? null, planType: authClaims?.chatgpt_plan_type ?? null };
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
export function isCodexAccessExpiring(input: { auth: CodexAuthJson; skewMs?: number; now?: number }): boolean {
|
package/src/lib/codexbin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
|
|
4
|
+
import { errorMessage } from "./log.ts";
|
|
4
5
|
import { loadConfig } from "./state.ts";
|
|
5
6
|
import { paths } from "./paths.ts";
|
|
6
7
|
|
|
@@ -41,7 +42,7 @@ export function verifyRealCodex(input: { bin: string }): string | null {
|
|
|
41
42
|
try {
|
|
42
43
|
p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
|
|
43
44
|
} catch (e) {
|
|
44
|
-
return
|
|
45
|
+
return errorMessage(e);
|
|
45
46
|
}
|
|
46
47
|
const outText = (p.stdout?.toString() ?? "").trim();
|
|
47
48
|
const err = (p.stderr?.toString() ?? "").trim();
|
package/src/lib/codexoauth.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
|
|
3
|
+
import { errorMessage } from "./log.ts";
|
|
3
4
|
import { env } from "./paths.ts";
|
|
4
5
|
import { CodexAuthJsonSchema, JsonTextSchema, type CodexAuthJson } from "./types.ts";
|
|
5
6
|
|
|
@@ -38,16 +39,10 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
|
|
|
38
39
|
let res: Response;
|
|
39
40
|
try {
|
|
40
41
|
res = await http.post(TOKEN_URL, {
|
|
41
|
-
|
|
42
|
-
body: JSON.stringify({
|
|
43
|
-
client_id: CLIENT_ID,
|
|
44
|
-
grant_type: "refresh_token",
|
|
45
|
-
refresh_token: auth.tokens.refresh_token,
|
|
46
|
-
}),
|
|
42
|
+
json: { client_id: CLIENT_ID, grant_type: "refresh_token", refresh_token: auth.tokens.refresh_token },
|
|
47
43
|
});
|
|
48
44
|
} catch (e) {
|
|
49
|
-
|
|
50
|
-
throw new CodexRefreshFailedError(`endpoint unreachable: ${message}`);
|
|
45
|
+
throw new CodexRefreshFailedError(`endpoint unreachable: ${errorMessage(e)}`);
|
|
51
46
|
}
|
|
52
47
|
|
|
53
48
|
const text = await res.text();
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
|
+
import { errorMessage } from "./log.ts";
|
|
3
4
|
import { env } from "./paths.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
|
|
5
6
|
import { codexIdentityOf } from "./codexauth.ts";
|
|
6
7
|
import { familyTokens } from "./usage.ts";
|
|
7
8
|
|
|
@@ -63,8 +64,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
63
64
|
},
|
|
64
65
|
});
|
|
65
66
|
} catch (e) {
|
|
66
|
-
|
|
67
|
-
throw new CodexUsageReadError(`endpoint unreachable: ${message}`);
|
|
67
|
+
throw new CodexUsageReadError(`endpoint unreachable: ${errorMessage(e)}`);
|
|
68
68
|
}
|
|
69
69
|
const text = await res.text();
|
|
70
70
|
if (!res.ok) {
|
|
@@ -76,7 +76,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
76
76
|
}
|
|
77
77
|
const wire = parsed.data;
|
|
78
78
|
|
|
79
|
-
return
|
|
79
|
+
return {
|
|
80
80
|
accountId: wire.account_id,
|
|
81
81
|
email: wire.email ?? null,
|
|
82
82
|
planType: wire.plan_type ?? null,
|
|
@@ -84,7 +84,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
84
84
|
...toWindows(wire.rate_limit, null, at),
|
|
85
85
|
...(wire.additional_rate_limits ?? []).flatMap((row) => toWindows(row.rate_limit, row.limit_name, at)),
|
|
86
86
|
],
|
|
87
|
-
}
|
|
87
|
+
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
|
package/src/lib/compact.ts
CHANGED
|
@@ -2,14 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { delay } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { log } from "./log.ts";
|
|
5
|
+
import { errorMessage, log } from "./log.ts";
|
|
6
|
+
import { readLines } from "./proc.ts";
|
|
6
7
|
import { JsonTextSchema } from "./types.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
z.object({ ok: z.literal(true) }),
|
|
10
|
-
z.object({ ok: z.literal(false), reason: z.string() }),
|
|
11
|
-
]);
|
|
12
|
-
export type CompactOutcome = z.infer<typeof CompactOutcomeSchema>;
|
|
9
|
+
export type CompactOutcome = { ok: true } | { ok: false; reason: string };
|
|
13
10
|
|
|
14
11
|
export const CLAUDE_COMPACT_KILL_MS = 300_000;
|
|
15
12
|
const PIPE_GRACE_MS = 2_000;
|
|
@@ -108,20 +105,10 @@ export async function compactCodexThread(input: { real: string; threadId: string
|
|
|
108
105
|
}
|
|
109
106
|
};
|
|
110
107
|
(async () => {
|
|
111
|
-
|
|
112
|
-
for await (const chunk of p.stdout) {
|
|
113
|
-
buffered += Buffer.from(chunk).toString("utf8");
|
|
114
|
-
let nl = buffered.indexOf("\n");
|
|
115
|
-
while (nl >= 0) {
|
|
116
|
-
onLine(buffered.slice(0, nl));
|
|
117
|
-
buffered = buffered.slice(nl + 1);
|
|
118
|
-
nl = buffered.indexOf("\n");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (buffered.trim() !== "") onLine(buffered);
|
|
108
|
+
for await (const line of readLines(p.stdout)) onLine(line);
|
|
122
109
|
const err = (await stderrText).trim().slice(0, 200);
|
|
123
110
|
finish({ ok: false, reason: p.exitCode === null ? `app-server killed after ${CODEX_COMPACT_KILL_MS / 1000}s` : `app-server exited ${p.exitCode}: ${err}` });
|
|
124
|
-
})().catch((e) => finish({ ok: false, reason:
|
|
111
|
+
})().catch((e) => finish({ ok: false, reason: errorMessage(e) }));
|
|
125
112
|
send({ id: INIT_ID, method: "initialize", params: { clientInfo: { name: "tokenmaxxing", version: packageVersion() } } });
|
|
126
113
|
});
|
|
127
114
|
|
package/src/lib/credstore.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
4
|
import * as kc from "./keychain.ts";
|
|
6
5
|
import { keychain as kcNames, namespacedCredService, storeDirFor } from "./paths.ts";
|
|
7
6
|
import { CredentialBlobSchema, ErrnoSchema, type OAuthCreds } from "./types.ts";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
z.object({ kind: z.literal("keychain"), service: z.string(), account: z.string() }),
|
|
11
|
-
z.object({ kind: z.literal("file"), path: z.string() }),
|
|
12
|
-
]);
|
|
13
|
-
export type CredTarget = z.infer<typeof CredTargetSchema>;
|
|
8
|
+
export type CredTarget = ({ kind: "keychain" } & kc.KeychainTarget) | { kind: "file"; path: string };
|
|
14
9
|
|
|
15
10
|
const darwin = process.platform === "darwin";
|
|
16
11
|
|
package/src/lib/decide.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { withLock } from "./lock.ts";
|
|
3
|
-
import {
|
|
2
|
+
import { loadAccounts, loadConfig, saveAccounts } from "./state.ts";
|
|
4
3
|
import { isExhausted, limitWindows, nextWeeklyReset, pickBest, pickEarliestReset, sessionWindow, thresholdBars, usableAt, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
4
|
import { familyTokens } from "./usage.ts";
|
|
6
|
-
import { log } from "./log.ts";
|
|
5
|
+
import { errorMessage, log } from "./log.ts";
|
|
7
6
|
import type { Observation, Provider } from "./provider.ts";
|
|
8
|
-
import {
|
|
7
|
+
import type { Account, EnforcedLimit } from "./types.ts";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
swapped: z.boolean(),
|
|
12
|
-
account: AccountSchema.nullable(),
|
|
13
|
-
reason: z.string(),
|
|
14
|
-
waitUntil: z.number().optional(),
|
|
15
|
-
});
|
|
16
|
-
export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
|
|
9
|
+
export type SwapDecision = { swapped: boolean; account: Account | null; reason: string; waitUntil?: number };
|
|
17
10
|
|
|
18
11
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19
12
|
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
@@ -45,12 +38,6 @@ function enforcedWall(limit: EnforcedLimit, account: Account, now: number): numb
|
|
|
45
38
|
|
|
46
39
|
export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRespawn = false, enforced: EnforcedLimit | null = null, seatId: string | null = p.liveId()): Promise<SwapDecision> {
|
|
47
40
|
const activeId = seatId;
|
|
48
|
-
|
|
49
|
-
const lastSwapAt = loadLastSwapAt(p.pool);
|
|
50
|
-
if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
51
|
-
return { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
const cfg = loadConfig();
|
|
55
42
|
const bars = thresholdBars(cfg);
|
|
56
43
|
const stored0 = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
|
|
@@ -63,10 +50,6 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
return withLock(p.pool.lockFile, async () => {
|
|
66
|
-
const lastSwapAt2 = loadLastSwapAt(p.pool);
|
|
67
|
-
if (!enforced && lastSwapAt2 != null && now - lastSwapAt2 < POST_SWAP_COOLDOWN_MS) {
|
|
68
|
-
return { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
69
|
-
}
|
|
70
53
|
const idx = loadAccounts(p.pool);
|
|
71
54
|
const id2 = seatId;
|
|
72
55
|
const active = id2 ? idx.accounts.find((a) => a.id === id2) : undefined;
|
|
@@ -113,15 +96,14 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
113
96
|
return { swapped: false, account: null, reason: "needs-respawn" };
|
|
114
97
|
}
|
|
115
98
|
|
|
116
|
-
const seatOf = (cur: {
|
|
117
|
-
cur.accounts.find((a) => a.id === id2) ?? cur.accounts.find((a) => a.id === cur.activeId) ?? null;
|
|
99
|
+
const seatOf = (cur: { accounts: Account[] }): Account | null => cur.accounts.find((a) => a.id === id2) ?? null;
|
|
118
100
|
|
|
119
101
|
const rejected = new Set<string>();
|
|
120
102
|
const usable = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (p.seats === "shared" || a.id === id2 || !present.has(a.id)));
|
|
121
103
|
const skipOrThrow = (e: unknown, candidate: Account): void => {
|
|
122
104
|
if (p.classifySwapError(e) === "fatal") throw e;
|
|
123
105
|
rejected.add(candidate.id);
|
|
124
|
-
log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error:
|
|
106
|
+
log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error: errorMessage(e) });
|
|
125
107
|
};
|
|
126
108
|
while (true) {
|
|
127
109
|
const cur = loadAccounts(p.pool);
|
package/src/lib/grok.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, 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
5
|
import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
|
|
6
|
-
import type {
|
|
7
|
-
import { loadAccounts,
|
|
8
|
-
import type
|
|
6
|
+
import type { Provider } from "./provider.ts";
|
|
7
|
+
import { loadAccounts, type Harvest } from "./state.ts";
|
|
8
|
+
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
9
9
|
import { c } from "../cli/render.ts";
|
|
10
10
|
|
|
11
11
|
const GrokAuthEntrySchema = z.looseObject({
|
|
@@ -18,85 +18,10 @@ const GrokAuthEntrySchema = z.looseObject({
|
|
|
18
18
|
});
|
|
19
19
|
type GrokAuthEntry = z.infer<typeof GrokAuthEntrySchema>;
|
|
20
20
|
|
|
21
|
-
function
|
|
22
|
-
let raw: unknown;
|
|
23
|
-
try {
|
|
24
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
-
} catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
const map = z.record(z.string(), z.unknown()).safeParse(raw);
|
|
29
|
-
if (!map.success) return [];
|
|
30
|
-
const out: { key: string; entry: GrokAuthEntry }[] = [];
|
|
31
|
-
for (const [key, value] of Object.entries(map.data)) {
|
|
32
|
-
const parsed = GrokAuthEntrySchema.safeParse(value);
|
|
33
|
-
if (parsed.success) out.push({ key, entry: parsed.data });
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function identityOf(key: string, entry: GrokAuthEntry): { id: string; email: string | null } {
|
|
39
|
-
const id = entry.user_id ?? entry.principal_id ?? key;
|
|
40
|
-
return { id, email: entry.email ?? null };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveRealGrok(): string {
|
|
44
|
-
const cfg = loadConfig();
|
|
45
|
-
if (cfg.grokBin) {
|
|
46
|
-
if (!existsSync(cfg.grokBin)) throw new Error(`configured grokBin does not exist: ${cfg.grokBin} - fix config.json`);
|
|
47
|
-
return cfg.grokBin;
|
|
48
|
-
}
|
|
49
|
-
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
50
|
-
if (!d) continue;
|
|
51
|
-
const cand = join(d, "grok");
|
|
52
|
-
try {
|
|
53
|
-
if (existsSync(cand)) return cand;
|
|
54
|
-
} catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
throw new Error("could not locate the real `grok` binary (set grokBin in config.json)");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function liveId(): string | null {
|
|
62
|
-
return grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function presence(): Map<string, number> {
|
|
66
|
-
return new Map();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function observeLive(account: Account): Promise<Observation | null> {
|
|
70
|
-
return account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
|
|
74
|
-
const reports = new Map<string, SampleReport>();
|
|
75
|
-
for (const a of accounts) {
|
|
76
|
-
reports.set(a.id, (await storeUsable(a)) ? { ok: true, source: "probe" } : { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --grok`" });
|
|
77
|
-
}
|
|
78
|
-
return reports;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function storeUsable(a: Account): Promise<boolean> {
|
|
82
|
-
try {
|
|
83
|
-
const entries = parseGrokAuthFile(grokAuthJsonFor(a.id));
|
|
84
|
-
return entries.some((e) => identityOf(e.key, e.entry).id === a.id && (e.entry.refresh_token ?? "") !== "");
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function removeCredentials(a: Account): Promise<void> {
|
|
91
|
-
rmSync(grokStoreDirFor(a.id), { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
|
|
95
|
-
const { id, email } = identityOf(key, entry);
|
|
96
|
-
if (!id) return null;
|
|
21
|
+
function harvestOf(id: string, key: string, entry: GrokAuthEntry): Harvest {
|
|
97
22
|
return {
|
|
98
23
|
id,
|
|
99
|
-
email,
|
|
24
|
+
email: entry.email ?? null,
|
|
100
25
|
tier: null,
|
|
101
26
|
sample: null,
|
|
102
27
|
park: async () => {
|
|
@@ -106,83 +31,44 @@ function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
|
|
|
106
31
|
};
|
|
107
32
|
}
|
|
108
33
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const onboardDir = grokPaths.onboardDir;
|
|
112
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
113
|
-
mkdirSync(onboardDir, { recursive: true });
|
|
114
|
-
const p = Bun.spawn([real, "login"], {
|
|
115
|
-
stdin: "inherit",
|
|
116
|
-
stdout: "inherit",
|
|
117
|
-
stderr: "inherit",
|
|
118
|
-
env: { ...process.env, GROK_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
|
|
119
|
-
});
|
|
120
|
-
await p.exited;
|
|
34
|
+
function readAuth(path: string): AuthEntry[] {
|
|
35
|
+
let raw: unknown;
|
|
121
36
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
const entries = parseGrokAuthFile(join(onboardDir, "auth.json"));
|
|
127
|
-
if (entries.length === 0) {
|
|
128
|
-
console.error(c.red("no grok login landed in the isolated home - nothing added."));
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return harvestOf(entries[0]!.key, entries[0]!.entry);
|
|
132
|
-
} finally {
|
|
133
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function importLive(): Promise<Harvest | null> {
|
|
138
|
-
console.log(c.cyan("Opening an isolated grok login for your first pooled account - the login you already have stays as it is."));
|
|
139
|
-
console.log();
|
|
140
|
-
const live = parseGrokAuthFile(join(grokPaths.home, "auth.json"));
|
|
141
|
-
if (live.length > 0) {
|
|
142
|
-
console.log(c.dim(`found ${live.length} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`));
|
|
143
|
-
const h = harvestOf(live[0]!.key, live[0]!.entry);
|
|
144
|
-
if (h) return h;
|
|
37
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
145
40
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
41
|
+
const map = z.record(z.string(), z.unknown()).safeParse(raw);
|
|
42
|
+
if (!map.success) return [];
|
|
43
|
+
const out: AuthEntry[] = [];
|
|
44
|
+
for (const [key, value] of Object.entries(map.data)) {
|
|
45
|
+
const parsed = GrokAuthEntrySchema.safeParse(value);
|
|
46
|
+
if (!parsed.success) continue;
|
|
47
|
+
const entry = parsed.data;
|
|
48
|
+
const id = entry.user_id ?? entry.principal_id ?? key;
|
|
49
|
+
out.push({ id, usable: (entry.refresh_token ?? "") !== "", harvest: id === "" ? null : harvestOf(id, key, entry) });
|
|
155
50
|
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function install(): void {
|
|
160
|
-
console.log(c.dim("grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts."));
|
|
51
|
+
return out;
|
|
161
52
|
}
|
|
162
53
|
|
|
163
|
-
export const grok: Provider = {
|
|
54
|
+
export const grok: Provider = statusOnlyProvider({
|
|
164
55
|
name: "grok",
|
|
165
56
|
flag: " --grok",
|
|
166
57
|
pool: grokPool,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
login,
|
|
183
|
-
importLive,
|
|
184
|
-
preflight,
|
|
185
|
-
install,
|
|
58
|
+
binName: "grok",
|
|
59
|
+
binKey: "grokBin",
|
|
60
|
+
storeDirFor: grokStoreDirFor,
|
|
61
|
+
authJsonFor: grokAuthJsonFor,
|
|
62
|
+
onboardDir: grokPaths.onboardDir,
|
|
63
|
+
homeEnv: "GROK_HOME",
|
|
64
|
+
loginArgs: ["login"],
|
|
65
|
+
onboardAuthRel: "auth.json",
|
|
66
|
+
liveAuthPath: () => join(grokPaths.home, "auth.json"),
|
|
67
|
+
readAuth,
|
|
68
|
+
liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
|
|
69
|
+
versionOk: (out) => out.trim().toLowerCase().includes("grok"),
|
|
70
|
+
importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
|
|
71
|
+
foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
|
|
72
|
+
installNotice: "grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts.",
|
|
186
73
|
loginStep: () => `Sign in in the browser session that opens (or run ${c.bold("grok login --device-auth")} on headless hosts first).`,
|
|
187
|
-
|
|
188
|
-
};
|
|
74
|
+
});
|
package/src/lib/http.ts
CHANGED
|
@@ -17,8 +17,7 @@ const PlainErrorBodySchema = z.looseObject({
|
|
|
17
17
|
message: z.string().optional(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
type ErrorDetail = z.infer<typeof ErrorDetailSchema>;
|
|
20
|
+
type ErrorDetail = { code: string | null; fields: string[] };
|
|
22
21
|
|
|
23
22
|
function parseErrorBody(text: string): ErrorDetail | null {
|
|
24
23
|
const json = JsonTextSchema.safeParse(text).data;
|