tokenmaxxing 1.164.0 → 1.166.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 CHANGED
@@ -40,7 +40,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
40
40
  - `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most eight sessions wait on one account; a session that arrives when every reset within `policy.maxWaitMs` is full waits for the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset when the horizon is full.
41
41
  - `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
42
42
  - `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
43
- - `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` and `muse-*` state of the status-only grok and Muse Code pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
43
+ - `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-respawn/` and `codex-live/`, and the `grok-*` and `muse-*` state of the status-only grok and Muse Code pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
44
44
  - Per-account **credential stores** follow the platform's Claude Code store: macOS = a login-keychain item keyed by the store path (never plaintext on disk); Linux = a 0600 `.credentials.json` inside the 0700 store directory (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform. tokenmaxxing writes a store once, at onboarding; Claude Code's own refresh is the only writer afterwards.
45
45
 
46
46
  - A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover atomic-write temp files under the state root, and on a Bun global install checks npm for a newer version once a day, so the cached figures every placement reads stay fresh.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.164.0",
3
+ "version": "1.166.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/lib/claude.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { countBy } from "es-toolkit";
4
4
  import { z } from "zod";
@@ -9,6 +9,7 @@ import { activationHint, CHECK_JOB, ensurePathInRc, HUB_JOB, installSupervisor,
9
9
  import { withLock } from "./lock.ts";
10
10
  import { errorMessage, log } from "./log.ts";
11
11
  import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
12
+ import { onboardHome } from "./onboard.ts";
12
13
  import { claudePool, env, paths, seatFromEnv, shortId, storeDirFor } from "./paths.ts";
13
14
  import { MAX_WAITERS_PER_ACCOUNT, pickBest, pickEarliestReset, pickWaitTarget, thresholdBars, type PickCtx } from "./picker.ts";
14
15
  import { deletePiStore } from "./piauth.ts";
@@ -232,11 +233,8 @@ function identityReady(cjPath: string): boolean {
232
233
  }
233
234
 
234
235
  async function login(): Promise<Harvest | null> {
235
- const onboardDir = paths.onboardDir;
236
- rmSync(onboardDir, { recursive: true, force: true });
237
- mkdirSync(onboardDir, { recursive: true });
236
+ const onboardDir = await onboardHome("claude", (dir) => deleteItem(isolatedTarget(dir)));
238
237
  const iso = isolatedTarget(onboardDir);
239
- await deleteItem(iso);
240
238
  const cjPath = join(onboardDir, ".claude.json");
241
239
  const real = resolveRealBin(CLAUDE_BIN);
242
240
 
package/src/lib/codex.ts CHANGED
@@ -6,6 +6,7 @@ import { writeFileAtomic } from "./atomic.ts";
6
6
  import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
7
7
  import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, ensureCodexStoreHome, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
8
8
  import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
9
+ import { onboardHome } from "./onboard.ts";
9
10
  import { deletePiStore } from "./piauth.ts";
10
11
  import { PI_PRESENCE_PREFIX, livingPresences, seatCounts, writePresence } from "./presence.ts";
11
12
  import { CodexUsageReadError, codexLimitLabel, consumeCodexReset, fetchCodexUsage } from "./codexusage.ts";
@@ -139,8 +140,7 @@ async function prepareMove(target: Account): Promise<void> {
139
140
 
140
141
  async function login(): Promise<Harvest | null> {
141
142
  const real = resolveRealBin(CODEX_BIN);
142
- const onboardDir = codexPaths.onboardDir;
143
- rmSync(onboardDir, { recursive: true, force: true });
143
+ const onboardDir = await onboardHome("codex");
144
144
  writeFileAtomic(join(onboardDir, "config.toml"), 'cli_auth_credentials_store = "file"\n');
145
145
 
146
146
  const savedTermios = saveTermios();
package/src/lib/grok.ts CHANGED
@@ -124,7 +124,6 @@ export const grok = statusOnlyProvider({
124
124
  binKey: "grokBin",
125
125
  storeDirFor: grokStoreDirFor,
126
126
  authJsonFor: grokAuthJsonFor,
127
- onboardDir: grokPaths.onboardDir,
128
127
  homeEnv: "GROK_HOME",
129
128
  loginArgs: ["login"],
130
129
  onboardAuthRel: "auth.json",
package/src/lib/muse.ts CHANGED
@@ -65,7 +65,6 @@ export const muse = statusOnlyProvider({
65
65
  binKey: "museBin",
66
66
  storeDirFor: museStoreDirFor,
67
67
  authJsonFor: museAuthJsonFor,
68
- onboardDir: musePaths.onboardDir,
69
68
  homeEnv: "XDG_CONFIG_HOME",
70
69
  loginArgs: ["login"],
71
70
  onboardAuthRel: "muse/auth.json",
@@ -0,0 +1,22 @@
1
+ import { mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { paths } from "./paths.ts";
4
+ import { pidExists } from "./proc.ts";
5
+
6
+ const POOLS = ["claude", "codex", "pi", "grok", "muse"] as const;
7
+
8
+ export type OnboardPool = (typeof POOLS)[number];
9
+
10
+ const HOME_NAME = new RegExp(`^(${POOLS.join("|")})-([1-9]\\d*)-`);
11
+
12
+ export async function onboardHome(pool: OnboardPool, onStale?: (dir: string) => Promise<unknown>): Promise<string> {
13
+ mkdirSync(paths.onboardDir, { recursive: true, mode: 0o700 });
14
+ for (const name of readdirSync(paths.onboardDir)) {
15
+ const match = HOME_NAME.exec(name);
16
+ if (match && (match[1] !== pool || pidExists(Number(match[2])))) continue;
17
+ const dir = join(paths.onboardDir, name);
18
+ if (match) await onStale?.(dir);
19
+ rmSync(dir, { recursive: true, force: true });
20
+ }
21
+ return mkdtempSync(join(paths.onboardDir, `${pool}-${process.pid}-`));
22
+ }
package/src/lib/paths.ts CHANGED
@@ -72,7 +72,6 @@ export const codexPaths = {
72
72
  authJson: join(CODEX_HOME, "auth.json"),
73
73
  hooksJson: join(CODEX_HOME, "hooks.json"),
74
74
  storesDir: join(TM_HOME, "codex-stores"),
75
- onboardDir: join(TM_HOME, "codex-onboard"),
76
75
  respawnDir: join(TM_HOME, "codex-respawn"),
77
76
  presenceDir: join(TM_HOME, "codex-live"),
78
77
  } as const;
@@ -100,7 +99,6 @@ export function expandTilde(p: string): string {
100
99
  export const piPaths = {
101
100
  home: resolve(expandTilde(env("PI_CODING_AGENT_DIR", join(HOME, ".pi", "agent")))),
102
101
  storesDir: join(TM_HOME, "pi-stores"),
103
- onboardDir: join(TM_HOME, "pi-onboard"),
104
102
  } as const;
105
103
 
106
104
  export function piStoreDirFor(pool: PiPool, accountId: string): string {
@@ -116,7 +114,6 @@ const GROK_HOME_DEFAULT = join(HOME, ".grok");
116
114
  export const grokPaths = {
117
115
  home: env("GROK_HOME", GROK_HOME_DEFAULT),
118
116
  storesDir: join(TM_HOME, "grok-stores"),
119
- onboardDir: join(TM_HOME, "grok-onboard"),
120
117
  } as const;
121
118
 
122
119
  export function grokStoreDirFor(accountId: string): string {
@@ -130,7 +127,6 @@ export function grokAuthJsonFor(accountId: string): string {
130
127
  export const musePaths = {
131
128
  authJson: join(env("XDG_CONFIG_HOME", join(HOME, ".config")), "muse", "auth.json"),
132
129
  storesDir: join(TM_HOME, "muse-stores"),
133
- onboardDir: join(TM_HOME, "muse-onboard"),
134
130
  } as const;
135
131
 
136
132
  export function museStoreDirFor(accountId: string): string {
package/src/lib/pi.ts CHANGED
@@ -7,7 +7,8 @@ import { codexSeatIdOf } from "./codexauth.ts";
7
7
  import { CHECK_JOB, installPiSupervisor, isBinDirAhead, jobHealthy, piSupervisorLink } from "./install.ts";
8
8
  import { errorMessage, log } from "./log.ts";
9
9
  import { fetchTokenIdentity } from "./oauth.ts";
10
- import { claudePool, piPaths, type PiPool } from "./paths.ts";
10
+ import { onboardHome } from "./onboard.ts";
11
+ import { claudePool, type PiPool } from "./paths.ts";
11
12
  import { linkSharedPiHome, piStoreUsable, readPiAuthAt, type PiOAuth } from "./piauth.ts";
12
13
  import { StoreUnusableError, type Provider } from "./provider.ts";
13
14
  import { loadAccounts, pinBinOverride } from "./state.ts";
@@ -51,8 +52,7 @@ async function identityOf(pool: PiPool, cred: PiOAuth): Promise<{ id: string; em
51
52
 
52
53
  export async function piLogin(pool: PiPool): Promise<PiLogin | null> {
53
54
  const real = resolveRealBin(PI_BIN);
54
- const onboardDir = piPaths.onboardDir;
55
- rmSync(onboardDir, { recursive: true, force: true });
55
+ const onboardDir = await onboardHome("pi");
56
56
  linkSharedPiHome(onboardDir);
57
57
  const authPath = join(onboardDir, "auth.json");
58
58
 
@@ -59,7 +59,7 @@ function listDir(dir: string, root: string): Dirent[] {
59
59
  }
60
60
 
61
61
  function tmpSweepDirs(root: string): string[] {
62
- const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, paths.apiKeysDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
62
+ const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, paths.apiKeysDir, codexPaths.presenceDir, codexPaths.respawnDir];
63
63
  for (const parent of SWEEP_PARENTS) {
64
64
  for (const child of listDir(parent, root)) {
65
65
  if (child.isDirectory()) dirs.push(join(parent, child.name));
@@ -1,6 +1,7 @@
1
- import { mkdirSync, rmSync } from "node:fs";
1
+ import { rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { resolveRealBin, verifyRealBin } from "./claudebin.ts";
4
+ import { onboardHome } from "./onboard.ts";
4
5
  import type { PoolPaths } from "./paths.ts";
5
6
  import type { Provider } from "./provider.ts";
6
7
  import { pinBinOverride, type Harvest } from "./state.ts";
@@ -17,7 +18,6 @@ export type StatusOnlySpec = {
17
18
  binKey: "grokBin" | "museBin";
18
19
  storeDirFor: (accountId: string) => string;
19
20
  authJsonFor: (accountId: string) => string;
20
- onboardDir: string;
21
21
  homeEnv: string;
22
22
  loginArgs: string[];
23
23
  onboardAuthRel: string;
@@ -33,6 +33,11 @@ export type StatusOnlySpec = {
33
33
  samplePool: Provider["samplePool"];
34
34
  };
35
35
 
36
+ // An entry with no identity (a Muse Code provider without an email) has no harvest; the first one that has one is pooled.
37
+ function firstHarvest(entries: AuthEntry[]): Harvest | null {
38
+ return entries.find((e) => e.harvest)?.harvest ?? null;
39
+ }
40
+
36
41
  export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
37
42
  const bin = { name: spec.binName, key: spec.binKey, versionOk: spec.versionOk };
38
43
 
@@ -46,9 +51,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
46
51
 
47
52
  async function login(): Promise<Harvest | null> {
48
53
  const real = resolveRealBin(bin);
49
- const onboardDir = spec.onboardDir;
50
- rmSync(onboardDir, { recursive: true, force: true });
51
- mkdirSync(onboardDir, { recursive: true });
54
+ const onboardDir = await onboardHome(spec.name);
52
55
  const p = Bun.spawn([real, ...spec.loginArgs], {
53
56
  stdin: "inherit",
54
57
  stdout: "inherit",
@@ -57,12 +60,9 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
57
60
  });
58
61
  await p.exited;
59
62
  try {
60
- const first = p.exitCode === 0 ? spec.readAuth(join(onboardDir, spec.onboardAuthRel))[0] : undefined;
61
- if (!first) {
62
- console.error(c.red(`no ${spec.name} login landed in the isolated home - nothing added.`));
63
- return null;
64
- }
65
- return first.harvest;
63
+ const harvest = p.exitCode === 0 ? firstHarvest(spec.readAuth(join(onboardDir, spec.onboardAuthRel))) : null;
64
+ if (!harvest) console.error(c.red(`no ${spec.name} login landed in the isolated home - nothing added.`));
65
+ return harvest;
66
66
  } finally {
67
67
  rmSync(onboardDir, { recursive: true, force: true });
68
68
  }
@@ -71,13 +71,11 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
71
71
  async function importLive(): Promise<Harvest | null> {
72
72
  console.log(c.cyan(spec.importIntro));
73
73
  console.log();
74
- const live = spec.readAuth(spec.liveAuthPath());
75
- const first = live[0];
76
- if (first) {
77
- console.log(c.dim(spec.foundLive(live.length)));
78
- if (first.harvest) return first.harvest;
79
- }
80
- return login();
74
+ const live = spec.readAuth(spec.liveAuthPath()).filter((e) => e.harvest);
75
+ const harvest = live[0]?.harvest;
76
+ if (!harvest) return login();
77
+ console.log(c.dim(spec.foundLive(live.length)));
78
+ return harvest;
81
79
  }
82
80
 
83
81
  function preflight(): void {