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.
Files changed (53) hide show
  1. package/DESIGN.md +6 -6
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/src/cli/auth.ts +6 -17
  5. package/src/cli/check.ts +2 -2
  6. package/src/cli/cloudrun.ts +12 -19
  7. package/src/cli/config.ts +2 -1
  8. package/src/cli/doctor.ts +5 -4
  9. package/src/cli/init.ts +0 -1
  10. package/src/cli/render.ts +24 -9
  11. package/src/cli/rm.ts +0 -4
  12. package/src/cli/status.ts +34 -57
  13. package/src/entries/codexstophook.ts +10 -23
  14. package/src/entries/codexsupervisor.ts +30 -72
  15. package/src/entries/statusline.ts +17 -26
  16. package/src/entries/stopfailurehook.ts +4 -5
  17. package/src/entries/stophook.ts +12 -10
  18. package/src/entries/subagentstatusline.ts +4 -10
  19. package/src/entries/supervisor.ts +51 -103
  20. package/src/lib/claude.ts +11 -12
  21. package/src/lib/claudebin.ts +3 -2
  22. package/src/lib/claudelock.ts +2 -2
  23. package/src/lib/cloud.ts +4 -8
  24. package/src/lib/codex.ts +3 -3
  25. package/src/lib/codexauth.ts +2 -11
  26. package/src/lib/codexbin.ts +2 -1
  27. package/src/lib/codexoauth.ts +3 -8
  28. package/src/lib/codexusage.ts +5 -5
  29. package/src/lib/compact.ts +5 -18
  30. package/src/lib/credstore.ts +1 -6
  31. package/src/lib/decide.ts +6 -24
  32. package/src/lib/grok.ts +38 -152
  33. package/src/lib/http.ts +1 -2
  34. package/src/lib/install.ts +20 -38
  35. package/src/lib/keychain.ts +1 -4
  36. package/src/lib/log.ts +5 -5
  37. package/src/lib/oauth.ts +6 -5
  38. package/src/lib/opencodego.ts +31 -140
  39. package/src/lib/paths.ts +6 -22
  40. package/src/lib/picker.ts +9 -20
  41. package/src/lib/presence.ts +4 -5
  42. package/src/lib/proc.ts +22 -0
  43. package/src/lib/sample.ts +25 -24
  44. package/src/lib/sessions.ts +5 -6
  45. package/src/lib/settings.ts +1 -8
  46. package/src/lib/state.ts +6 -29
  47. package/src/lib/statusonly.ts +143 -0
  48. package/src/lib/supervise.ts +111 -0
  49. package/src/lib/types.ts +27 -36
  50. package/src/lib/update.ts +6 -7
  51. package/src/lib/usage.ts +40 -191
  52. package/src/main.ts +5 -5
  53. package/src/entries/sessionstart.ts +0 -33
package/src/lib/claude.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
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 { CRED_ENV_OVERRIDES, fetchUsageDirect, gatedFamilies, mergeWindows, windowsOf } from "./usage.ts";
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, { retries: 0 });
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 (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
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
- const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
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: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
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 (${e instanceof Error ? e.message : String(e)}) - nothing changed.`));
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) {
@@ -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 claude";
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 e instanceof Error ? e.message : String(e);
100
+ return errorMessage(e);
100
101
  }
101
102
  const outText = (p.stdout?.toString() ?? "").trim();
102
103
  const err = (p.stderr?.toString() ?? "").trim();
@@ -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: e instanceof Error ? e.message : String(e) });
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 { CloudTokenSchema, CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
10
- import { normalizeResetsAt, parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
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
- const PickSchema = z.discriminatedUnion("ok", [
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 != null ? normalizeResetsAt(row.quotaLimits.resetsAt) : null;
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 instanceof Error ? e.message : String(e)).slice(0, 80)})`, deadGrant: false };
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 (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
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 });
@@ -84,12 +84,7 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
84
84
  return JSON.parse(payload);
85
85
  }
86
86
 
87
- const CodexIdentitySchema = z.object({
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 CodexIdentitySchema.parse({
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 {
@@ -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 e instanceof Error ? e.message : String(e);
45
+ return errorMessage(e);
45
46
  }
46
47
  const outText = (p.stdout?.toString() ?? "").trim();
47
48
  const err = (p.stderr?.toString() ?? "").trim();
@@ -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
- headers: { "Content-Type": "application/json" },
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
- const message = e instanceof Error ? e.message : String(e);
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();
@@ -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 { CodexUsageSchema, JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
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
- const message = e instanceof Error ? e.message : String(e);
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 CodexUsageSchema.parse({
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"]]);
@@ -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
- const CompactOutcomeSchema = z.discriminatedUnion("ok", [
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
- let buffered = "";
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: e instanceof Error ? e.message : String(e) }));
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
 
@@ -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
- const CredTargetSchema = z.discriminatedUnion("kind", [
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 { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadLastSwapAt, saveAccounts } from "./state.ts";
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 { AccountSchema, type Account, type EnforcedLimit } from "./types.ts";
7
+ import type { Account, EnforcedLimit } from "./types.ts";
9
8
 
10
- const SwapDecisionSchema = z.object({
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: { activeId: string | null; accounts: Account[] }): Account | null =>
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: e instanceof Error ? e.message : String(e) });
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 { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
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 { Observation, Provider, SampleReport } from "./provider.ts";
7
- import { loadAccounts, loadConfig, pinBinOverride, type Harvest } from "./state.ts";
8
- import type { Account } from "./types.ts";
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 parseGrokAuthFile(path: string): { key: string; entry: GrokAuthEntry }[] {
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
- async function login(): Promise<Harvest | null> {
110
- const real = resolveRealGrok();
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
- if (p.exitCode !== 0) {
123
- console.error(c.red("no grok login landed in the isolated home - nothing added."));
124
- return null;
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
- return login();
147
- }
148
-
149
- function preflight(): void {
150
- const real = resolveRealGrok();
151
- const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
152
- const out = (p.stdout?.toString() ?? "").trim().toLowerCase();
153
- if (p.exitCode !== 0 || !out.includes("grok")) {
154
- throw new Error(`grok binary failed verification: ${real}`);
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
- pinBinOverride({ key: "grokBin", bin: real });
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
- seats: "live",
168
- waitsWhenDepleted: false,
169
- statusOnly: true,
170
- liveId,
171
- presence,
172
- gatedFamilies: () => null,
173
- observeLive,
174
- samplePool,
175
- mergeWindows: (next) => next,
176
- swap: async () => {
177
- throw new Error("grok moves are not supported yet (status-only pool)");
178
- },
179
- classifySwapError: () => "fatal",
180
- removeCredentials,
181
- storeUsable,
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
- windowLabel: (name) => name.toLowerCase(),
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
- const ErrorDetailSchema = z.object({ code: z.string().nullable(), fields: z.array(z.string()) });
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;