tokenmaxxing 1.79.0 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { codexPaths, codexPool } from "../lib/paths.ts";
@@ -10,7 +10,7 @@ import { clearPresence, livingPresences } from "../lib/presence.ts";
10
10
  import { isExhausted } from "../lib/picker.ts";
11
11
  import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
12
12
  import { saveTermios } from "../lib/tty.ts";
13
- import { loadAccounts } from "../lib/state.ts";
13
+ import { loadAccounts, readJsonFile } from "../lib/state.ts";
14
14
  import { CodexRespawnMarkerSchema, type Account } from "../lib/types.ts";
15
15
  import { errorMessage, log } from "../lib/log.ts";
16
16
 
@@ -26,7 +26,7 @@ const PASSTHROUGH_FLAGS = new Set(["--version", "-V", "--help", "-h"]);
26
26
 
27
27
  function readCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> {
28
28
  try {
29
- return CodexRespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
29
+ return readJsonFile(marker, CodexRespawnMarkerSchema);
30
30
  } catch (e) {
31
31
  log("codexsupervisor.marker_invalid", { err: errorMessage(e) });
32
32
  throw new Error(
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, readdirSync, statSync } from "node:fs";
1
+ import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
3
  import type { FileSink, Subprocess } from "bun";
4
4
  import { maxBy } from "es-toolkit";
@@ -16,7 +16,7 @@ import { teeObservation } from "../lib/sample.ts";
16
16
  import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
17
17
  import { saveTermios } from "../lib/tty.ts";
18
18
  import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
19
- import { loadAccounts, loadConfig, releaseWaitClaim } from "../lib/state.ts";
19
+ import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim } from "../lib/state.ts";
20
20
  import { gatedFamilies, modelFromFlag } from "../lib/usage.ts";
21
21
  import { RespawnMarkerSchema, type Account, type Config, type ModelInfo } from "../lib/types.ts";
22
22
  import { errorMessage, log } from "../lib/log.ts";
@@ -183,7 +183,7 @@ type MarkerGate = {
183
183
  function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
184
184
  let m: z.infer<typeof RespawnMarkerSchema>;
185
185
  try {
186
- m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
186
+ m = readJsonFile(marker, RespawnMarkerSchema);
187
187
  } catch (e) {
188
188
  log("supervisor.marker_invalid", { err: errorMessage(e) });
189
189
  throw new Error(
package/src/lib/claude.ts CHANGED
@@ -12,7 +12,7 @@ import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./pick
12
12
  import { seatCounts } from "./presence.ts";
13
13
  import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
14
14
  import { foldTee, sampleAccountUsage, teeObservation, usageBlockedUntil } from "./sample.ts";
15
- import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
15
+ import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
16
16
  import { saveTermios, restoreTermios } from "./tty.ts";
17
17
  import { fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredentialEnv, windowsOf } from "./usage.ts";
18
18
  import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
@@ -185,7 +185,7 @@ async function login(): Promise<Harvest | null> {
185
185
  let blob, oauthAccount;
186
186
  try {
187
187
  blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
188
- oauthAccount = OAuthAccountSchema.parse(JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount);
188
+ oauthAccount = readJsonFile(cjPath, z.object({ oauthAccount: OAuthAccountSchema })).oauthAccount;
189
189
  } catch {
190
190
  console.error(c.red("could not parse the onboarded account's credential/identity."));
191
191
  return null;
@@ -1,22 +1,23 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } 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 { codexAuthJsonFor, codexPaths, codexStoreDirFor } from "./paths.ts";
6
+ import { readJsonFile } from "./state.ts";
6
7
  import { CodexAuthJsonSchema, ErrnoSchema, type CodexAuthJson } from "./types.ts";
7
8
 
9
+ const CodexAuthFileSchema = z
10
+ .looseObject({ tokens: z.unknown().optional() })
11
+ .transform((auth) => (auth.tokens === undefined || auth.tokens === null ? null : auth))
12
+ .pipe(CodexAuthJsonSchema.nullable());
13
+
8
14
  export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
9
- let raw: string;
10
15
  try {
11
- raw = readFileSync(input.path, "utf8");
16
+ return readJsonFile(input.path, CodexAuthFileSchema);
12
17
  } catch (e) {
13
18
  if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
14
19
  throw e;
15
20
  }
16
- const parsed = JSON.parse(raw);
17
- const probe = z.looseObject({ tokens: z.unknown().optional() }).parse(parsed);
18
- if (probe.tokens === undefined || probe.tokens === null) return null;
19
- return CodexAuthJsonSchema.parse(parsed);
20
21
  }
21
22
 
22
23
  export function readCodexStoreAuth(accountId: string): CodexAuthJson | null {
@@ -7,7 +7,7 @@ import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.
7
7
  import { writeFileAtomic } from "./atomic.ts";
8
8
  import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
9
9
  import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
10
- import { loadConfig } from "./state.ts";
10
+ import { loadConfig, readJsonFile } from "./state.ts";
11
11
  import { ErrnoSchema } from "./types.ts";
12
12
 
13
13
  export type InstallOutcome = { claudeWrapper: string; installedBin: string; pathAhead: boolean; timerLoaded: boolean; hubLoaded: boolean; checkIntervalS: number };
@@ -134,7 +134,7 @@ function withoutOurCodexStopHooks(groups: { hooks: { type?: string; command?: st
134
134
 
135
135
  export function installCodexStopHook(): void {
136
136
  const current = existsSync(codexPaths.hooksJson)
137
- ? CodexHooksFileSchema.parse(JSON.parse(readFileSync(codexPaths.hooksJson, "utf8")))
137
+ ? readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema)
138
138
  : CodexHooksFileSchema.parse({});
139
139
  const next = {
140
140
  ...current,
@@ -151,7 +151,7 @@ export function installCodexStopHook(): void {
151
151
 
152
152
  export function uninstallCodexStopHook(): void {
153
153
  if (!existsSync(codexPaths.hooksJson)) return;
154
- const current = CodexHooksFileSchema.parse(JSON.parse(readFileSync(codexPaths.hooksJson, "utf8")));
154
+ const current = readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema);
155
155
  const next = {
156
156
  ...current,
157
157
  hooks: {
@@ -166,7 +166,7 @@ export function codexStopHookGroupIndex(): number | null {
166
166
  if (!existsSync(codexPaths.hooksJson)) return null;
167
167
  let parsed: z.infer<typeof CodexHooksFileSchema>;
168
168
  try {
169
- parsed = CodexHooksFileSchema.parse(JSON.parse(readFileSync(codexPaths.hooksJson, "utf8")));
169
+ parsed = readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema);
170
170
  } catch {
171
171
  return null;
172
172
  }
@@ -4,7 +4,8 @@ import { countBy } from "es-toolkit";
4
4
  import { z } from "zod";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
6
  import { pidExists, pidStartTime } from "./proc.ts";
7
- import { ErrnoSchema } from "./types.ts";
7
+ import { readJsonFile } from "./state.ts";
8
+ import { ErrnoSchema, JsonTextSchema } from "./types.ts";
8
9
 
9
10
  const PresenceSchema = z.object({
10
11
  accountId: z.string(),
@@ -19,14 +20,12 @@ export function writePresence(input: { dir: string; id: string; accountId: strin
19
20
  }
20
21
 
21
22
  export function presencePid(input: { dir: string; id: string }): number | null {
22
- let raw: string;
23
23
  try {
24
- raw = readFileSync(join(input.dir, input.id), "utf8");
24
+ return readJsonFile(join(input.dir, input.id), PresenceSchema).pid;
25
25
  } catch (e) {
26
26
  if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
27
27
  throw e;
28
28
  }
29
- return PresenceSchema.parse(JSON.parse(raw)).pid;
30
29
  }
31
30
 
32
31
  export function clearPresence(input: { dir: string; id: string }): void {
@@ -47,13 +46,7 @@ export function livingPresences(dir: string): LivingPresence[] {
47
46
  if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
48
47
  throw e;
49
48
  }
50
- const parsed = PresenceSchema.safeParse((() => {
51
- try {
52
- return JSON.parse(raw);
53
- } catch {
54
- return null;
55
- }
56
- })());
49
+ const parsed = PresenceSchema.safeParse(JsonTextSchema.safeParse(raw).data);
57
50
  if (!parsed.success) {
58
51
  throw new Error(`${file} is not a readable presence record - it may belong to a RUNNING session, refusing to treat it as absent; remove the file (or respawn that session) to proceed`);
59
52
  }
@@ -1,10 +1,11 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, type Dirent } from "node:fs";
1
+ import { existsSync, readdirSync, realpathSync, rmSync, statSync, type Dirent } from "node:fs";
2
2
  import { basename, join, sep } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { errorMessage, log } from "./log.ts";
5
5
  import { codexPaths, grokPaths, opencodeGoPaths, paths } from "./paths.ts";
6
6
  import { writeFileAtomic } from "./atomic.ts";
7
7
  import { presencePid } from "./presence.ts";
8
+ import { readJsonFile } from "./state.ts";
8
9
  import { spawnedThroughShellsBy } from "./proc.ts";
9
10
  import type { RespawnMarkerSchema } from "./types.ts";
10
11
 
@@ -25,7 +26,7 @@ export function saveSessionFlags(sid: string, flags: string[], cwd: string): voi
25
26
  function loadSession(sid: string): z.infer<typeof SessionSchema> | null {
26
27
  const f = sessionFile(sid);
27
28
  if (!existsSync(f)) return null;
28
- return SessionSchema.parse(JSON.parse(readFileSync(f, "utf8")));
29
+ return readJsonFile(f, SessionSchema);
29
30
  }
30
31
 
31
32
  export function loadSessionFlags(sid: string): string[] | null {
@@ -1,8 +1,10 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, statSync } from "node:fs";
2
2
  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 { readJsonFile } from "./state.ts";
7
+ import { JsonTextSchema } from "./types.ts";
6
8
 
7
9
  export function installedBin(): string {
8
10
  return join(paths.binDir, "tokenmaxxing");
@@ -31,7 +33,7 @@ const STOP_FAILURE_MATCHER = "rate_limit";
31
33
 
32
34
  function readSettings(): Settings {
33
35
  if (!existsSync(paths.claudeSettings)) return {};
34
- return SettingsSchema.parse(JSON.parse(readFileSync(paths.claudeSettings, "utf8")));
36
+ return readJsonFile(paths.claudeSettings, SettingsSchema);
35
37
  }
36
38
 
37
39
  function writeSettings(s: Settings): void {
@@ -71,13 +73,7 @@ export function isOurHookCommand(cmd: string, sub: string): boolean {
71
73
  if (!cmd.endsWith(` ${sub}`)) return false;
72
74
  const quotedPath = cmd.slice(0, cmd.length - (sub.length + 1));
73
75
  if (!quotedPath.startsWith('"') || !quotedPath.endsWith('"')) return false;
74
- let path: unknown;
75
- try {
76
- path = JSON.parse(quotedPath);
77
- } catch {
78
- return false;
79
- }
80
- const parsed = z.string().safeParse(path);
76
+ const parsed = z.string().safeParse(JsonTextSchema.safeParse(quotedPath).data);
81
77
  return parsed.success && parsed.data.endsWith("/tokenmaxxing");
82
78
  }
83
79
 
package/src/lib/state.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  AccountsIndexSchema,
8
8
  ConfigSchema,
9
9
  ErrnoSchema,
10
+ JsonTextSchema,
10
11
  UsageStateSchema,
11
12
  WaitQueueSchema,
12
13
  type Account,
@@ -17,21 +18,19 @@ import {
17
18
  type Window,
18
19
  } from "./types.ts";
19
20
 
20
- export function loadConfig(): Config {
21
- let raw: unknown = {};
22
- if (existsSync(paths.configJson)) {
23
- try {
24
- raw = JSON.parse(readFileSync(paths.configJson, "utf8"));
25
- } catch {
26
- throw new Error(`${paths.configJson} is corrupt (unparsable JSON) - fix or remove it`);
27
- }
28
- }
29
- const parsed = ConfigSchema.safeParse(raw);
21
+ export function readJsonFile<T extends z.ZodType>(path: string, schema: T): z.output<T> {
22
+ const json = JsonTextSchema.safeParse(readFileSync(path, "utf8"));
23
+ if (!json.success) throw new Error(`${path} is corrupt (unparsable JSON) - repair or remove the file`);
24
+ const parsed = schema.safeParse(json.data);
30
25
  if (!parsed.success) {
31
26
  const fields = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
32
- throw new Error(`${paths.configJson} is invalid (${fields}) - fix or remove the offending values`);
27
+ throw new Error(`${path} does not match its schema (${fields}) - repair or remove the file`);
33
28
  }
34
- const cfg = parsed.data;
29
+ return parsed.data;
30
+ }
31
+
32
+ export function loadConfig(): Config {
33
+ const cfg = existsSync(paths.configJson) ? readJsonFile(paths.configJson, ConfigSchema) : ConfigSchema.parse({});
35
34
  cfg.claudeBin = optionalEnv("TOKENMAXXING_CLAUDE_BIN") ?? cfg.claudeBin;
36
35
  cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
37
36
  cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
@@ -42,10 +41,7 @@ export function loadConfig(): Config {
42
41
  export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin";
43
42
 
44
43
  export function pinBinOverride(input: { key: BinKey; bin: string }): void {
45
- let raw: Record<string, unknown> = {};
46
- if (existsSync(paths.configJson)) {
47
- raw = z.record(z.string(), z.unknown()).parse(JSON.parse(readFileSync(paths.configJson, "utf8")));
48
- }
44
+ const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
49
45
  raw[input.key] = input.bin;
50
46
  writeFileAtomic(paths.configJson, JSON.stringify(raw, null, 2) + "\n");
51
47
  }
@@ -54,17 +50,7 @@ const emptyIndex = (): AccountsIndex => ({ version: 2, accounts: [] });
54
50
 
55
51
  export function loadAccounts(pool: PoolPaths): AccountsIndex {
56
52
  if (!existsSync(pool.accountsJson)) return emptyIndex();
57
- let json: unknown;
58
- try {
59
- json = JSON.parse(readFileSync(pool.accountsJson, "utf8"));
60
- } catch {
61
- throw new Error(`${pool.accountsJson} is corrupt (unparsable JSON) - refusing to treat a damaged pool as empty; repair or remove the file`);
62
- }
63
- const parsed = AccountsIndexSchema.safeParse(json);
64
- if (!parsed.success) {
65
- throw new Error(`${pool.accountsJson} does not match the accounts schema - refusing to treat a damaged pool as empty; repair or remove the file`);
66
- }
67
- return parsed.data;
53
+ return readJsonFile(pool.accountsJson, AccountsIndexSchema);
68
54
  }
69
55
 
70
56
  export function saveAccounts(pool: PoolPaths, idx: AccountsIndex): void {
@@ -73,17 +59,7 @@ export function saveAccounts(pool: PoolPaths, idx: AccountsIndex): void {
73
59
 
74
60
  function loadWaitQueue(): WaitClaim[] {
75
61
  if (!existsSync(claudePool.waitQueueJson)) return [];
76
- let json: unknown;
77
- try {
78
- json = JSON.parse(readFileSync(claudePool.waitQueueJson, "utf8"));
79
- } catch {
80
- throw new Error(`${claudePool.waitQueueJson} is corrupt (unparsable JSON) - refusing to treat a damaged wait queue as empty; repair or remove the file`);
81
- }
82
- const parsed = WaitQueueSchema.safeParse(json);
83
- if (!parsed.success) {
84
- throw new Error(`${claudePool.waitQueueJson} does not match the wait-queue schema - refusing to treat a damaged wait queue as empty; repair or remove the file`);
85
- }
86
- return parsed.data.claims;
62
+ return readJsonFile(claudePool.waitQueueJson, WaitQueueSchema).claims;
87
63
  }
88
64
 
89
65
  function saveWaitQueue(claims: WaitClaim[]): void {
package/src/lib/update.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, lstatSync, readFileSync } from "node:fs";
1
+ import { existsSync, lstatSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { writeFileAtomic } from "./atomic.ts";
@@ -8,6 +8,7 @@ import { isNixPackaged } from "./install.ts";
8
8
  import { withLock } from "./lock.ts";
9
9
  import { errorMessage, log } from "./log.ts";
10
10
  import { env, HOME, optionalEnv, paths } from "./paths.ts";
11
+ import { readJsonFile } from "./state.ts";
11
12
 
12
13
  const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
14
  const INSTALL_DEADLINE_MS = 60_000;
@@ -67,18 +68,12 @@ function detectGlobalRoot(): string | null {
67
68
  }
68
69
 
69
70
  function installedVersion(root: string): string {
70
- return VersionSchema.parse(JSON.parse(readFileSync(join(packageDir(root), "package.json"), "utf8"))).version;
71
+ return readJsonFile(join(packageDir(root), "package.json"), VersionSchema).version;
71
72
  }
72
73
 
73
74
  function isDue(now: number): boolean {
74
75
  if (!existsSync(updateJson)) return true;
75
- let json: unknown;
76
- try {
77
- json = JSON.parse(readFileSync(updateJson, "utf8"));
78
- } catch {
79
- throw new Error(`${updateJson} is corrupt (unparsable JSON) - repair or remove the file`);
80
- }
81
- return now - AttemptSchema.parse(json).attemptedAt >= UPDATE_INTERVAL_MS;
76
+ return now - readJsonFile(updateJson, AttemptSchema).attemptedAt >= UPDATE_INTERVAL_MS;
82
77
  }
83
78
 
84
79
  async function updateToLatest(root: string): Promise<void> {