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/sample.ts CHANGED
@@ -1,29 +1,20 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { z } from "zod";
4
3
  import { writeFileAtomic } from "./atomic.ts";
5
4
  import { readStore } from "./credstore.ts";
6
5
  import { withLock } from "./lock.ts";
7
- import { log } from "./log.ts";
6
+ import { errorMessage, log } from "./log.ts";
8
7
  import { claudeTierLabel, isAccessTokenExpiring, isDeadCredential } from "./oauth.ts";
9
8
  import { claudePool, paths, sampleDirFor, storeDirFor } from "./paths.ts";
10
9
  import { seatCounts } from "./presence.ts";
11
10
  import type { Observation } from "./provider.ts";
12
11
  import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
13
- import { fetchUsageDirect, mergeWindows, probeUsage, windowsOf } from "./usage.ts";
14
- import { UsageWindowsSchema, type Account, type Config } from "./types.ts";
12
+ import { fetchUsageDirect, mergeWindows, refreshViaProbe, windowsOf } from "./usage.ts";
13
+ import type { Account, Config, UsageWindows } from "./types.ts";
15
14
 
16
- const SampleOutcomeSchema = z.discriminatedUnion("ok", [
17
- z.object({ ok: z.literal(true), usage: UsageWindowsSchema, via: z.enum(["get", "probe"]) }),
18
- z.object({ ok: z.literal(false), reason: z.string() }),
19
- ]);
20
- export type SampleOutcome = z.infer<typeof SampleOutcomeSchema>;
15
+ export type SampleOutcome = { ok: true; usage: UsageWindows; via: "get" | "probe" } | { ok: false; reason: string };
21
16
 
22
- const PreparedSampleSchema = z.discriminatedUnion("ok", [
23
- z.object({ ok: z.literal(true), token: z.string().nullable() }),
24
- z.object({ ok: z.literal(false), reason: z.string() }),
25
- ]);
26
- type PreparedSample = z.infer<typeof PreparedSampleSchema>;
17
+ type PreparedSample = { ok: true; token: string | null } | { ok: false; reason: string };
27
18
 
28
19
  export function teeObservation(account: Account): Observation | null {
29
20
  const stored = account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
@@ -48,7 +39,7 @@ export async function prepareSample(account: Account): Promise<PreparedSample> {
48
39
  try {
49
40
  creds = await readStore(account.id);
50
41
  } catch (e) {
51
- return { ok: false, reason: `store credential unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)}) - run \`tokenmaxxing auth\`` };
42
+ return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)}) - run \`tokenmaxxing auth\`` };
52
43
  }
53
44
  if (!creds) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth`" };
54
45
  if (isDeadCredential(creds)) {
@@ -60,25 +51,29 @@ export async function prepareSample(account: Account): Promise<PreparedSample> {
60
51
  return { ok: true, token: isAccessTokenExpiring(creds) ? null : creds.accessToken };
61
52
  }
62
53
 
63
- async function runProbe(account: Account, suffix: string, retries: number | undefined): Promise<SampleOutcome> {
54
+ async function refreshedToken(account: Account, suffix: string): Promise<string | null> {
64
55
  const dir = sampleDirFor(account.id, suffix);
65
56
  mkdirSync(dir, { recursive: true });
66
57
  writeFileAtomic(join(dir, ".claude.json"), JSON.stringify({ oauthAccount: account.oauthAccount, hasCompletedOnboarding: true }));
67
- const usage = await probeUsage({ configDir: dir, store: storeDirFor(account.id) }, Date.now(), { retries });
68
- return usage ? { ok: true, usage, via: "probe" } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
58
+ if (!(await refreshViaProbe({ configDir: dir, store: storeDirFor(account.id) }))) return null;
59
+ const prepared = await prepareSample(account);
60
+ return prepared.ok ? prepared.token : null;
69
61
  }
70
62
 
71
- export async function runSample(account: Account, token: string | null, opts: { retries?: number; suffix?: string }): Promise<SampleOutcome> {
63
+ export async function runSample(account: Account, token: string | null, suffix = ""): Promise<SampleOutcome> {
72
64
  if (token) {
73
65
  const usage = await fetchUsageDirect(token);
74
66
  if (usage) return { ok: true, usage, via: "get" };
75
67
  }
76
- return runProbe(account, opts.suffix ?? "", opts.retries);
68
+ const fresh = await refreshedToken(account, suffix);
69
+ if (!fresh) return { ok: false, reason: "the `/usage` probe left no usable access token in the store (see log)" };
70
+ const usage = await fetchUsageDirect(fresh);
71
+ return usage ? { ok: true, usage, via: "probe" } : { ok: false, reason: "usage read failed after the `/usage` probe (see log)" };
77
72
  }
78
73
 
79
- export async function sampleAccountUsage(account: Account, opts: { retries?: number } = {}): Promise<SampleOutcome> {
74
+ export async function sampleAccountUsage(account: Account): Promise<SampleOutcome> {
80
75
  const prepared = await prepareSample(account);
81
- return prepared.ok ? runSample(account, prepared.token, opts) : prepared;
76
+ return prepared.ok ? runSample(account, prepared.token) : prepared;
82
77
  }
83
78
 
84
79
  const SAMPLE_BATCH = 3;
@@ -119,15 +114,21 @@ export async function sampleOldest(cfg: Config): Promise<void> {
119
114
  await Promise.all(
120
115
  reserved.map(async ({ account, token }) => {
121
116
  const startedAt = Date.now();
122
- const outcome = await runSample(account, token, { retries: 0, suffix: "-tick" });
117
+ const outcome = await runSample(account, token, "-tick");
123
118
  await withLock(claudePool.lockFile, () => {
124
119
  const idx = loadAccounts(claudePool);
125
120
  const stored = idx.accounts.find((a) => a.id === account.id);
121
+ let dirty = false;
122
+ if (stored && account.needsReauth === true && stored.needsReauth !== true) {
123
+ stored.needsReauth = true;
124
+ dirty = true;
125
+ }
126
126
  if (stored && outcome.ok && (stored.lastUsageAt == null || startedAt > stored.lastUsageAt)) {
127
127
  stored.windows = mergeWindows(windowsOf(outcome.usage, startedAt), stored.windows);
128
128
  stored.lastUsageAt = startedAt;
129
- saveAccounts(claudePool, idx);
129
+ dirty = true;
130
130
  }
131
+ if (dirty) saveAccounts(claudePool, idx);
131
132
  log(outcome.ok ? "sample.ok" : "sample.failed", {
132
133
  account: account.id.slice(0, 8),
133
134
  ...(outcome.ok ? { via: outcome.via } : { reason: outcome.reason.slice(0, 200) }),
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { paths } from "./paths.ts";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
- import { RespawnMarkerSchema } from "./types.ts";
6
+ import type { RespawnMarkerSchema } from "./types.ts";
7
7
 
8
8
  const SessionSchema = z.object({ flags: z.array(z.string()), cwd: z.string() });
9
9
 
@@ -36,8 +36,7 @@ export function pruneStaleSessions(now: number): void {
36
36
  }
37
37
  }
38
38
 
39
- const SupervisedSessionSchema = z.object({ sid: z.string().min(1), launchedAt: z.number().finite().nullable() });
40
- export type SupervisedSession = z.infer<typeof SupervisedSessionSchema>;
39
+ export type SupervisedSession = { sid: string; launchedAt: number | null };
41
40
 
42
41
  const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
43
42
 
@@ -45,18 +44,18 @@ export function supervisedSession(env: Record<string, string | undefined> = proc
45
44
  if (env.TOKENMAXXING_SUPERVISED !== "1") return null;
46
45
  const sid = env.TOKENMAXXING_SESSION_ID;
47
46
  if (sid == null || sid === "") return null;
48
- return SupervisedSessionSchema.parse({ sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null });
47
+ return { sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null };
49
48
  }
50
49
 
51
50
  export function writeRespawnMarker(input: { session: SupervisedSession; sessionId: string; accountId: string; waitUntil: number; compact: boolean }): void {
52
51
  mkdirSync(paths.respawnDir, { recursive: true });
53
- const payload = RespawnMarkerSchema.parse({
52
+ const payload: z.infer<typeof RespawnMarkerSchema> = {
54
53
  accountId: input.accountId,
55
54
  ts: Date.now(),
56
55
  waitUntil: input.waitUntil,
57
56
  sessionId: input.sessionId,
58
57
  compact: input.compact,
59
58
  ...(input.session.launchedAt != null ? { launchedAt: input.session.launchedAt } : {}),
60
- });
59
+ };
61
60
  writeFileAtomic(join(paths.respawnDir, input.session.sid), JSON.stringify(payload));
62
61
  }
@@ -112,14 +112,7 @@ export function uninstallSettings(): void {
112
112
  writeSettings(s);
113
113
  }
114
114
 
115
- const SettingsCheckSchema = z.object({
116
- statusLineOk: z.boolean(),
117
- subagentStatusLineOk: z.boolean(),
118
- stopOk: z.boolean(),
119
- stopFailureOk: z.boolean(),
120
- sessionStartOk: z.boolean(),
121
- });
122
- export type SettingsCheck = z.infer<typeof SettingsCheckSchema>;
115
+ export type SettingsCheck = Record<"statusLineOk" | "subagentStatusLineOk" | "stopOk" | "stopFailureOk" | "sessionStartOk", boolean>;
123
116
 
124
117
  export function checkSettings(): SettingsCheck {
125
118
  const s = readSettings();
package/src/lib/state.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import { closeSync, existsSync, fstatSync, openSync, readFileSync, rmSync, utimesSync } from "node:fs";
2
2
  import { isEqual } from "es-toolkit";
3
3
  import { z } from "zod";
4
- import { paths, realClaudeBinFromEnv, realCodexBinFromEnv, realGrokBinFromEnv, realOpencodeBinFromEnv, usageJsonFor, type PoolPaths } from "./paths.ts";
4
+ import { optionalEnv, paths, usageJsonFor, type PoolPaths } from "./paths.ts";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
6
  import {
7
7
  AccountsIndexSchema,
8
8
  ConfigSchema,
9
9
  ErrnoSchema,
10
- LastSwapSchema,
11
10
  UsageStateSchema,
12
11
  type Account,
13
12
  type AccountsIndex,
@@ -31,14 +30,10 @@ export function loadConfig(): Config {
31
30
  throw new Error(`${paths.configJson} is invalid (${fields}) - fix or remove the offending values`);
32
31
  }
33
32
  const cfg = parsed.data;
34
- const envBin = realClaudeBinFromEnv();
35
- if (envBin) cfg.claudeBin = envBin;
36
- const envCodexBin = realCodexBinFromEnv();
37
- if (envCodexBin) cfg.codexBin = envCodexBin;
38
- const envGrokBin = realGrokBinFromEnv();
39
- if (envGrokBin) cfg.grokBin = envGrokBin;
40
- const envOpencodeBin = realOpencodeBinFromEnv();
41
- if (envOpencodeBin) cfg.opencodeBin = envOpencodeBin;
33
+ cfg.claudeBin = optionalEnv("TOKENMAXXING_CLAUDE_BIN") ?? cfg.claudeBin;
34
+ cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
35
+ cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
36
+ cfg.opencodeBin = optionalEnv("TOKENMAXXING_OPENCODE_BIN") ?? cfg.opencodeBin;
42
37
  return cfg;
43
38
  }
44
39
 
@@ -51,7 +46,7 @@ export function pinBinOverride(input: { key: "claudeBin" | "codexBin" | "grokBin
51
46
  writeFileAtomic(paths.configJson, JSON.stringify(raw, null, 2) + "\n");
52
47
  }
53
48
 
54
- const emptyIndex = (): AccountsIndex => ({ version: 2, activeId: null, accounts: [] });
49
+ const emptyIndex = (): AccountsIndex => ({ version: 2, accounts: [] });
55
50
 
56
51
  export function loadAccounts(pool: PoolPaths): AccountsIndex {
57
52
  if (!existsSync(pool.accountsJson)) return emptyIndex();
@@ -127,24 +122,6 @@ export function clearUsageSnapshot(accountId: string): void {
127
122
  rmSync(usageJsonFor(accountId), { force: true });
128
123
  }
129
124
 
130
- export function loadLastSwapAt(pool: PoolPaths): number | null {
131
- if (pool.lastSwapJson == null || !existsSync(pool.lastSwapJson)) return null;
132
- let json: unknown;
133
- try {
134
- json = JSON.parse(readFileSync(pool.lastSwapJson, "utf8"));
135
- } catch {
136
- throw new Error(`${pool.lastSwapJson} is corrupt (unparsable JSON) - refusing to treat a damaged swap clock as never-swapped; repair or remove the file`);
137
- }
138
- return LastSwapSchema.parse(json).ts;
139
- }
140
-
141
- export function saveLastSwapAt(pool: PoolPaths, ts: number): void {
142
- if (pool.lastSwapJson == null) throw new Error("this pool keeps no swap clock");
143
- writeFileAtomic(pool.lastSwapJson, JSON.stringify(LastSwapSchema.parse({ ts })));
144
- }
145
-
146
- export const POST_SWAP_COOLDOWN_MS = 45_000;
147
-
148
125
  const USAGE_TS_REFRESH_MS = 10 * 60_000;
149
126
  const SAMPLED_AT_REFRESH_MS = 30_000;
150
127
 
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { PoolPaths } from "./paths.ts";
4
+ import type { Provider, SampleReport } from "./provider.ts";
5
+ import { loadConfig, pinBinOverride, type Harvest } from "./state.ts";
6
+ import type { Account } from "./types.ts";
7
+ import { c } from "../cli/render.ts";
8
+
9
+ export type AuthEntry = { id: string; usable: boolean; harvest: Harvest | null };
10
+
11
+ export type StatusOnlySpec = {
12
+ name: Provider["name"];
13
+ flag: string;
14
+ pool: PoolPaths;
15
+ binName: string;
16
+ binKey: "grokBin" | "opencodeBin";
17
+ storeDirFor: (accountId: string) => string;
18
+ authJsonFor: (accountId: string) => string;
19
+ onboardDir: string;
20
+ homeEnv: string;
21
+ loginArgs: string[];
22
+ onboardAuthRel: string;
23
+ liveAuthPath: () => string;
24
+ readAuth: (path: string) => AuthEntry[];
25
+ liveId: () => string | null;
26
+ versionOk: (versionOutput: string) => boolean;
27
+ importIntro: string;
28
+ foundLive: (count: number) => string;
29
+ installNotice: string;
30
+ loginStep: () => string;
31
+ };
32
+
33
+ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
34
+ function resolveBin(): string {
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
+ }
51
+
52
+ async function storeUsable(a: Account): Promise<boolean> {
53
+ try {
54
+ return spec.readAuth(spec.authJsonFor(a.id)).some((e) => e.id === a.id && e.usable);
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
61
+ const reports = new Map<string, SampleReport>();
62
+ for (const a of accounts) {
63
+ 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${spec.flag}\`` });
64
+ }
65
+ return reports;
66
+ }
67
+
68
+ async function login(): Promise<Harvest | null> {
69
+ const real = resolveBin();
70
+ const onboardDir = spec.onboardDir;
71
+ rmSync(onboardDir, { recursive: true, force: true });
72
+ mkdirSync(onboardDir, { recursive: true });
73
+ const p = Bun.spawn([real, ...spec.loginArgs], {
74
+ stdin: "inherit",
75
+ stdout: "inherit",
76
+ stderr: "inherit",
77
+ env: { ...process.env, [spec.homeEnv]: onboardDir, TOKENMAXXING_PROBE: "1" },
78
+ });
79
+ await p.exited;
80
+ try {
81
+ const first = p.exitCode === 0 ? spec.readAuth(join(onboardDir, spec.onboardAuthRel))[0] : undefined;
82
+ if (!first) {
83
+ console.error(c.red(`no ${spec.name} login landed in the isolated home - nothing added.`));
84
+ return null;
85
+ }
86
+ return first.harvest;
87
+ } finally {
88
+ rmSync(onboardDir, { recursive: true, force: true });
89
+ }
90
+ }
91
+
92
+ async function importLive(): Promise<Harvest | null> {
93
+ console.log(c.cyan(spec.importIntro));
94
+ console.log();
95
+ const live = spec.readAuth(spec.liveAuthPath());
96
+ const first = live[0];
97
+ if (first) {
98
+ console.log(c.dim(spec.foundLive(live.length)));
99
+ if (first.harvest) return first.harvest;
100
+ }
101
+ return login();
102
+ }
103
+
104
+ function preflight(): void {
105
+ const real = resolveBin();
106
+ const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
107
+ if (p.exitCode !== 0 || !spec.versionOk(p.stdout?.toString() ?? "")) {
108
+ throw new Error(`${spec.binName} binary failed verification: ${real}`);
109
+ }
110
+ pinBinOverride({ key: spec.binKey, bin: real });
111
+ }
112
+
113
+ return {
114
+ name: spec.name,
115
+ flag: spec.flag,
116
+ pool: spec.pool,
117
+ seats: "live",
118
+ waitsWhenDepleted: false,
119
+ statusOnly: true,
120
+ liveId: spec.liveId,
121
+ presence: () => new Map(),
122
+ gatedFamilies: () => null,
123
+ observeLive: async (account) => (account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null),
124
+ samplePool,
125
+ mergeWindows: (next) => next,
126
+ swap: async () => {
127
+ throw new Error(`${spec.name} moves are not supported yet (status-only pool)`);
128
+ },
129
+ classifySwapError: () => "fatal",
130
+ removeCredentials: async (a) => {
131
+ rmSync(spec.storeDirFor(a.id), { recursive: true, force: true });
132
+ },
133
+ storeUsable,
134
+ login,
135
+ importLive,
136
+ preflight,
137
+ install: () => {
138
+ console.log(c.dim(spec.installNotice));
139
+ },
140
+ loginStep: spec.loginStep,
141
+ windowLabel: (name) => name.toLowerCase(),
142
+ };
143
+ }
@@ -0,0 +1,111 @@
1
+ import type { Subprocess } from "bun";
2
+ import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "./claudebin.ts";
3
+ import { errorMessage, log } from "./log.ts";
4
+ import { paths } from "./paths.ts";
5
+ import { writePresence } from "./presence.ts";
6
+ import { restoreTermios } from "./tty.ts";
7
+
8
+ export type WrappedProduct = "claude" | "codex";
9
+
10
+ type ChildHandle = {
11
+ pid: number;
12
+ exitCode: number | null;
13
+ signalCode: string | null;
14
+ kill: () => void;
15
+ exited: Promise<unknown>;
16
+ };
17
+
18
+ const WRAPPED: Record<WrappedProduct, { setting: string; binary: string; events: string }> = {
19
+ claude: { setting: "claudeBin", binary: "Claude", events: "supervisor" },
20
+ codex: { setting: "codexBin", binary: "codex", events: "codexsupervisor" },
21
+ };
22
+
23
+ export function exitStatus(child: { exitCode: number | null; signalCode: string | null }): number {
24
+ return child.exitCode ?? (child.signalCode ? 1 : 0);
25
+ }
26
+
27
+ export function loopGuardTripped(product: WrappedProduct): boolean {
28
+ const { setting, binary, events } = WRAPPED[product];
29
+ const depth = wrapDepth();
30
+ if (depth >= MAX_WRAP_DEPTH) {
31
+ console.error(
32
+ `tokenmaxxing: ${LOOP_DIAGNOSIS} ${binary} (depth ${depth}) - ${setting} in ${paths.configJson} does not launch the real ${binary} binary. Fix ${setting}, then run \`tokenmaxxing doctor\`.`,
33
+ );
34
+ log(`${events}.loop_abort`, { depth });
35
+ return true;
36
+ }
37
+ if (wrapperEntryRateTripped(Date.now())) {
38
+ console.error(
39
+ `tokenmaxxing: ${LOOP_DIAGNOSIS} ${binary} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - ${setting} in ${paths.configJson} does not launch the real ${binary} binary. Fix ${setting}, then run \`tokenmaxxing doctor\`.`,
40
+ );
41
+ log(`${events}.rate_abort`, { max: WRAP_RATE_MAX });
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ export async function runPassthrough(input: { real: string; argv: string[]; env: Record<string, string | undefined>; onSpawn?: (child: Subprocess) => void }): Promise<number> {
48
+ const p = Bun.spawn([input.real, ...input.argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: input.env });
49
+ input.onSpawn?.(p);
50
+ await p.exited;
51
+ return exitStatus(p);
52
+ }
53
+
54
+ export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
55
+ for (let attempt = 0; ; attempt++) {
56
+ try {
57
+ writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid });
58
+ return;
59
+ } catch (e) {
60
+ if (input.child.exitCode !== null || input.child.signalCode !== null) return;
61
+ if (attempt >= 9) {
62
+ log(input.event, { err: errorMessage(e) });
63
+ input.child.kill();
64
+ await input.child.exited;
65
+ restoreTermios(input.savedTermios);
66
+ throw new Error(input.message);
67
+ }
68
+ await Bun.sleep(100);
69
+ }
70
+ }
71
+ }
72
+
73
+ export async function raceMarkerOrExit(input: {
74
+ child: { exited: Promise<unknown>; kill: () => void };
75
+ tick: () => boolean | Promise<boolean>;
76
+ savedTermios: string | null;
77
+ onKill?: () => void;
78
+ onError?: () => void;
79
+ onExited?: () => void;
80
+ }): Promise<void> {
81
+ let done = false;
82
+ const markerWatch = (async () => {
83
+ while (!done) {
84
+ if (await input.tick()) return true;
85
+ await Bun.sleep(150);
86
+ }
87
+ return false;
88
+ })();
89
+ const exited = input.child.exited.then(() => {
90
+ done = true;
91
+ return "exit" as const;
92
+ });
93
+ try {
94
+ const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
95
+ if (winner === "marker") {
96
+ input.onKill?.();
97
+ input.child.kill();
98
+ }
99
+ } catch (e) {
100
+ input.onKill?.();
101
+ input.child.kill();
102
+ input.onError?.();
103
+ throw e;
104
+ } finally {
105
+ await input.child.exited;
106
+ input.onExited?.();
107
+ done = true;
108
+ await markerWatch.catch(() => false);
109
+ restoreTermios(input.savedTermios);
110
+ }
111
+ }
package/src/lib/types.ts CHANGED
@@ -36,10 +36,7 @@ const AggregateWindowsSchema = z.object({
36
36
  sevenDay: UsageWindowSchema,
37
37
  });
38
38
 
39
- export const UsageWindowsSchema = AggregateWindowsSchema.extend({
40
- perModel: z.record(z.string(), UsageWindowSchema).default({}),
41
- });
42
- export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
39
+ export type UsageWindows = z.infer<typeof AggregateWindowsSchema> & { perModel: Record<string, UsageWindow> };
43
40
 
44
41
  export const WindowSchema = z.object({
45
42
  name: z.string().nullable(),
@@ -80,27 +77,19 @@ export type Account = z.infer<typeof AccountSchema>;
80
77
 
81
78
  export const AccountsIndexSchema = z.object({
82
79
  version: z.literal(2),
83
- activeId: z.string().nullable(),
84
80
  accounts: z.array(AccountSchema).default([]),
85
81
  });
86
-
87
- export const LastSwapSchema = z.object({ ts: z.number() });
88
82
  export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
89
83
 
90
- export const EnforcedLimitSchema = z.object({
91
- account: z.string(),
92
- kind: z.enum(["session", "weekly", "model"]),
93
- family: z.string().nullable(),
94
- resetsAt: z.number().nullable(),
95
- blind: z.boolean(),
96
- });
97
- export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
84
+ export type EnforcedLimit = {
85
+ account: string;
86
+ kind: "session" | "weekly" | "model";
87
+ family: string | null;
88
+ resetsAt: number | null;
89
+ blind: boolean;
90
+ };
98
91
 
99
- export const ThresholdsSchema = z.object({
100
- session: z.number().min(0).max(100),
101
- weekly: z.number().min(0).max(100),
102
- });
103
- export type Thresholds = z.infer<typeof ThresholdsSchema>;
92
+ export type Thresholds = { session: number; weekly: number };
104
93
 
105
94
  export const ConfigSchema = z
106
95
  .object({
@@ -142,11 +131,15 @@ export const RespawnMarkerSchema = z.object({
142
131
  launchedAt: z.number().optional(),
143
132
  });
144
133
 
134
+ export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
135
+
136
+ const StdinWindowSchema = z.looseObject({ used_percentage: z.number(), resets_at: EpochSecondsSchema.nullable().optional() });
137
+
145
138
  export const RateLimitsStdinSchema = z.looseObject({
146
139
  rate_limits: z
147
140
  .looseObject({
148
- five_hour: z.looseObject({ used_percentage: z.number(), resets_at: z.number().nullable().optional() }).optional(),
149
- seven_day: z.looseObject({ used_percentage: z.number(), resets_at: z.number().nullable().optional() }).optional(),
141
+ five_hour: StdinWindowSchema.optional(),
142
+ seven_day: StdinWindowSchema.optional(),
150
143
  })
151
144
  .optional(),
152
145
  model: z.looseObject({ id: z.string().optional(), display_name: z.string().optional() }).optional(),
@@ -196,13 +189,12 @@ export const ProfileResponseSchema = z.looseObject({
196
189
  organization: z.looseObject({ uuid: z.string(), name: z.string().nullish() }),
197
190
  });
198
191
 
199
- export const TokenIdentitySchema = z.object({
200
- accountUuid: z.string(),
201
- email: z.string().nullable(),
202
- organizationUuid: z.string(),
203
- organizationName: z.string().nullable(),
204
- });
205
- export type TokenIdentity = z.infer<typeof TokenIdentitySchema>;
192
+ export type TokenIdentity = {
193
+ accountUuid: string;
194
+ email: string | null;
195
+ organizationUuid: string;
196
+ organizationName: string | null;
197
+ };
206
198
 
207
199
  const CodexTokensSchema = z.looseObject({
208
200
  id_token: z.string(),
@@ -217,13 +209,12 @@ export const CodexAuthJsonSchema = z.looseObject({
217
209
  });
218
210
  export type CodexAuthJson = z.infer<typeof CodexAuthJsonSchema>;
219
211
 
220
- export const CodexUsageSchema = z.object({
221
- accountId: z.string(),
222
- email: z.string().nullable(),
223
- planType: z.string().nullable(),
224
- windows: z.array(WindowSchema),
225
- });
226
- export type CodexUsage = z.infer<typeof CodexUsageSchema>;
212
+ export type CodexUsage = {
213
+ accountId: string;
214
+ email: string | null;
215
+ planType: string | null;
216
+ windows: Window[];
217
+ };
227
218
 
228
219
  export const CodexStopStdinSchema = z.looseObject({
229
220
  session_id: z.string().optional(),
package/src/lib/update.ts CHANGED
@@ -5,14 +5,13 @@ import { writeFileAtomic } from "./atomic.ts";
5
5
  import { http } from "./http.ts";
6
6
  import { isNixPackaged } from "./install.ts";
7
7
  import { withLock } from "./lock.ts";
8
- import { log } from "./log.ts";
9
- import { HOME, paths } from "./paths.ts";
8
+ import { errorMessage, log } from "./log.ts";
9
+ import { env, HOME, optionalEnv, paths } from "./paths.ts";
10
10
 
11
11
  const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
12
12
  const INSTALL_DEADLINE_MS = 60_000;
13
13
  const REGISTRY_DEADLINE_MS = 15_000;
14
14
 
15
- const OverrideSchema = z.string().min(1).optional().catch(undefined);
16
15
  const AttemptSchema = z.object({ attemptedAt: z.number() });
17
16
  const VersionSchema = z.object({ version: z.string().min(1) });
18
17
  const LatestSchema = z.object({ version: z.string().min(1), engines: z.looseObject({ bun: z.string().min(1).optional() }).optional() });
@@ -21,7 +20,7 @@ const updateJson = join(paths.home, "update.json");
21
20
  const updateLock = join(paths.home, "update.lock");
22
21
 
23
22
  function registry(): { base: URL; authorization: string | null } {
24
- const raw = OverrideSchema.parse(process.env.TOKENMAXXING_NPM_REGISTRY) ?? "https://registry.npmjs.org";
23
+ const raw = env("TOKENMAXXING_NPM_REGISTRY", "https://registry.npmjs.org");
25
24
  const base = new URL(raw.endsWith("/") ? raw : `${raw}/`);
26
25
  const authorization =
27
26
  base.username !== "" || base.password !== "" ? `Basic ${btoa(`${decodeURIComponent(base.username)}:${decodeURIComponent(base.password)}`)}` : null;
@@ -39,8 +38,8 @@ function realOrNull(path: string): string | null {
39
38
  }
40
39
 
41
40
  function globalRootCandidates(): string[] {
42
- const globalDir = OverrideSchema.parse(process.env.BUN_INSTALL_GLOBAL_DIR);
43
- const bunInstall = OverrideSchema.parse(process.env.BUN_INSTALL);
41
+ const globalDir = optionalEnv("BUN_INSTALL_GLOBAL_DIR");
42
+ const bunInstall = optionalEnv("BUN_INSTALL");
44
43
  return [
45
44
  ...(globalDir != null ? [globalDir] : []),
46
45
  ...(bunInstall != null ? [join(bunInstall, "install", "global")] : []),
@@ -127,7 +126,7 @@ export async function maybeAutoUpdate(): Promise<void> {
127
126
  try {
128
127
  await updateToLatest(root);
129
128
  } catch (e) {
130
- throw new Error(`self-update from npm failed: ${e instanceof Error ? e.message : String(e)}`);
129
+ throw new Error(`self-update from npm failed: ${errorMessage(e)}`);
131
130
  }
132
131
  });
133
132
  }