tokenmaxxing 1.107.0 → 1.109.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.107.0",
3
+ "version": "1.109.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/cli/seat.ts CHANGED
@@ -1,15 +1,36 @@
1
+ import { borrowClaudeSeat } from "../lib/claude.ts";
1
2
  import { borrowCodexSeat } from "../lib/codex.ts";
3
+ import { borrowOpencodeGoSeat } from "../lib/opencodego.ts";
2
4
  import { emitError } from "./render.ts";
3
5
 
4
- export async function cmdSeat(pidRaw: string | undefined, extra: string[]): Promise<number> {
6
+ const POOLS = {
7
+ claude: {
8
+ borrow: borrowClaudeSeat,
9
+ usage: "usage: tokenmaxxing seat <pid> - print the CLAUDE_SECURESTORAGE_CONFIG_DIR of one pooled claude account, lent to <pid> until it exits and shared with host sessions and other borrowers",
10
+ none: "no usable claude account (pool empty, or every account at a limit or flagged for reauthentication)",
11
+ },
12
+ codex: {
13
+ borrow: borrowCodexSeat,
14
+ usage: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, reserved to <pid> alone until it exits, because one codex auth.json serves one machine or one serialized job stream (https://learn.chatgpt.com/docs/auth/ci-cd-auth)",
15
+ none: "no usable codex account (pool empty, every account live in another session, or every account at a limit) - use the ambient codex login",
16
+ },
17
+ "opencode-go": {
18
+ borrow: borrowOpencodeGoSeat,
19
+ usage: "usage: tokenmaxxing seat --opencode-go <pid> - print the store directory of one pooled opencode-go key, whose auth.json is an opencode auth file, lent to <pid> until it exits and shared with other borrowers",
20
+ none: "no usable opencode-go key (pool empty, or no store holds a usable key)",
21
+ },
22
+ };
23
+
24
+ export async function cmdSeat(pool: keyof typeof POOLS, pidRaw: string | undefined, extra: string[]): Promise<number> {
25
+ const seat = POOLS[pool];
5
26
  const pid = pidRaw != null && /^(0|[1-9][0-9]*)$/.test(pidRaw) ? Number(pidRaw) : NaN;
6
27
  if (!Number.isSafeInteger(pid) || pid < 2 || extra.length > 0) {
7
- emitError({ message: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, reserved until <pid> exits" });
28
+ emitError({ message: seat.usage });
8
29
  return 2;
9
30
  }
10
- const granted = await borrowCodexSeat(pid);
31
+ const granted = await seat.borrow(pid);
11
32
  if (!granted) {
12
- emitError({ message: "no usable codex account (pool empty, every account live in another session, or every account at a limit) - use the ambient codex login" });
33
+ emitError({ message: seat.none });
13
34
  return 1;
14
35
  }
15
36
  if ("denied" in granted) {
package/src/lib/claude.ts CHANGED
@@ -11,8 +11,8 @@ import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential
11
11
  import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
12
12
  import { landWindows, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
13
13
  import { deletePiStore } from "./piauth.ts";
14
- import { livingPresences } from "./presence.ts";
15
- import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
14
+ import { livingPresences, writePresence } from "./presence.ts";
15
+ import { StoreUnusableError, type Observation, type Provider, type SampleReport, type SeatBorrow } from "./provider.ts";
16
16
  import { foldTee, readyToSample, runSample, sampleAccountUsage, sampleDue, sampleIntervalMs, teeObservation, usageBlockedUntil } from "./sample.ts";
17
17
  import { clearUsageSnapshot, liveWaitClaims, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
18
18
  import { saveTermios, restoreTermios } from "./tty.ts";
@@ -142,6 +142,39 @@ export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Acc
142
142
  return pickBest(candidates, ctx) ?? pickEarliestReset(candidates, ctx)?.account ?? null;
143
143
  }
144
144
 
145
+ export async function borrowClaudeSeat(pid: number): Promise<SeatBorrow> {
146
+ const seatId = `seat-${pid}`;
147
+ const cfg = loadConfig();
148
+ const bars = thresholdBars(cfg);
149
+ const granted = await withLock(claudePool.lockFile, async (): Promise<SeatBorrow> => {
150
+ const idx = loadAccounts(claudePool);
151
+ let dirty = false;
152
+ for (const a of idx.accounts) dirty = foldTee(a, bars) || dirty;
153
+ if (dirty) saveAccounts(claudePool, idx);
154
+ const living = livingPresences(paths.presenceDir);
155
+ const held = living.find((p) => p.id === seatId);
156
+ const heldAccount = held ? (idx.accounts.find((x) => x.id === held.accountId) ?? null) : null;
157
+ if (heldAccount) {
158
+ if (heldAccount.needsReauth === true) {
159
+ return { denied: "the account this pid holds needs reauthentication - run `tokenmaxxing auth` and borrow again" };
160
+ }
161
+ if (!(await storeUsable(heldAccount))) {
162
+ return { denied: "the account this pid holds has no usable credential in its store - refusing to hand back a credential-less seat" };
163
+ }
164
+ return { store: storeDirFor(heldAccount.id), id: heldAccount.id, reused: true };
165
+ }
166
+ const open = idx.accounts.filter((a) => a.needsReauth !== true);
167
+ const usable = await Promise.all(open.map(storeUsable));
168
+ const ctx: PickCtx = { now: Date.now(), thresholds: bars, currentId: null, families: gatedFamilies(null, cfg.policy.switchModels), seats: presence() };
169
+ const picked = pickBest(open.filter((_, i) => usable[i]), ctx);
170
+ if (!picked) return null;
171
+ writePresence({ dir: paths.presenceDir, id: seatId, accountId: picked.id, pid });
172
+ return { store: storeDirFor(picked.id), id: picked.id, reused: false };
173
+ });
174
+ if (granted && !("denied" in granted)) log("seat.grant", { account: granted.id.slice(0, 8), pid, reused: granted.reused });
175
+ return granted;
176
+ }
177
+
145
178
  async function removeCredentials(a: Account): Promise<void> {
146
179
  await deleteItem(storeTarget(a.id));
147
180
  rmSync(storeDirFor(a.id), { recursive: true, force: true });
package/src/lib/codex.ts CHANGED
@@ -13,7 +13,7 @@ import { withLock } from "./lock.ts";
13
13
  import { errorMessage, log } from "./log.ts";
14
14
  import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
15
15
  import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
16
- import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
16
+ import { StoreUnusableError, type Observation, type Provider, type SampleReport, type SeatBorrow } from "./provider.ts";
17
17
  import { loadAccounts, loadConfig, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
18
18
  import { restoreTermios, saveTermios } from "./tty.ts";
19
19
  import type { Account, CodexAuthJson, CodexUsage, Config } from "./types.ts";
@@ -239,9 +239,7 @@ export function pickCodexSeat(now: number, eligible: (a: Account) => boolean = (
239
239
  return pickBest(open.filter((a) => !isExhausted(a, ctx)), ctx) ?? pickEarliestReset(open, ctx)?.account ?? null;
240
240
  }
241
241
 
242
- export type CodexBorrow = { store: string; id: string; reused: boolean } | { denied: string } | null;
243
-
244
- export async function borrowCodexSeat(pid: number): Promise<CodexBorrow> {
242
+ export async function borrowCodexSeat(pid: number): Promise<SeatBorrow> {
245
243
  const now = Date.now();
246
244
  const seatId = `seat-${pid}`;
247
245
  const cfg = loadConfig();
@@ -250,7 +248,7 @@ export async function borrowCodexSeat(pid: number): Promise<CodexBorrow> {
250
248
  .accounts.filter((a) => a.needsReauth !== true && codexStoreUsable(a.id))
251
249
  .map((a) => observeCodex(a, cfg, now, { probe: true, refresh: false })),
252
250
  );
253
- const granted = await withLock(codexPool.lockFile, (): CodexBorrow => {
251
+ const granted = await withLock(codexPool.lockFile, (): SeatBorrow => {
254
252
  const ctx = codexPickCtx(now, null);
255
253
  const idx = loadAccounts(codexPool);
256
254
  const held = livingPresences(codexPaths.presenceDir).find((p) => p.id === seatId);
package/src/lib/grok.ts CHANGED
@@ -131,6 +131,7 @@ const base = statusOnlyProvider({
131
131
  liveAuthPath: () => join(grokPaths.home, "auth.json"),
132
132
  readAuth,
133
133
  liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
134
+ presence: () => new Map(),
134
135
  versionOk: (out) => out.toLowerCase().includes("grok"),
135
136
  importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
136
137
  foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
@@ -1,9 +1,13 @@
1
1
  import { join } from "node:path";
2
+ import { countBy, minBy } from "es-toolkit";
2
3
  import { z } from "zod";
3
4
  import { writeFileAtomic } from "./atomic.ts";
4
5
  import { env, HOME, opencodeGoAuthJsonFor, opencodeGoPaths, opencodeGoPool, opencodeGoStoreDirFor } from "./paths.ts";
5
- import type { Provider } from "./provider.ts";
6
- import { readJsonFile, type Harvest } from "./state.ts";
6
+ import { withLock } from "./lock.ts";
7
+ import { log } from "./log.ts";
8
+ import { livingPresences, seatCounts, writePresence } from "./presence.ts";
9
+ import type { Provider, SeatBorrow } from "./provider.ts";
10
+ import { loadAccounts, readJsonFile, type Harvest } from "./state.ts";
7
11
  import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
8
12
  import { ErrnoSchema } from "./types.ts";
9
13
  import { c } from "../cli/render.ts";
@@ -47,6 +51,30 @@ function readAuth(path: string): AuthEntry[] {
47
51
  return [{ id: idOfKey(entry.data.key), usable: true, harvest: harvestOf(entry.data.key) }];
48
52
  }
49
53
 
54
+ export async function borrowOpencodeGoSeat(pid: number): Promise<SeatBorrow> {
55
+ const seatId = `seat-${pid}`;
56
+ const granted = await withLock(opencodeGoPool.lockFile, async (): Promise<SeatBorrow> => {
57
+ const accounts = loadAccounts(opencodeGoPool).accounts;
58
+ const living = livingPresences(opencodeGoPaths.presenceDir);
59
+ const held = living.find((p) => p.id === seatId);
60
+ const heldAccount = held ? (accounts.find((a) => a.id === held.accountId) ?? null) : null;
61
+ if (heldAccount) {
62
+ if (!(await opencodeGo.storeUsable(heldAccount))) {
63
+ return { denied: "the key this pid holds is no longer usable in its store - run `tokenmaxxing auth --opencode-go` and borrow again" };
64
+ }
65
+ return { store: opencodeGoStoreDirFor(heldAccount.id), id: heldAccount.id, reused: true };
66
+ }
67
+ const lent = countBy(living, (p) => p.accountId);
68
+ const usable = await Promise.all(accounts.map(opencodeGo.storeUsable));
69
+ const picked = minBy(accounts.filter((_, i) => usable[i]), (a) => lent[a.id] ?? 0);
70
+ if (!picked) return null;
71
+ writePresence({ dir: opencodeGoPaths.presenceDir, id: seatId, accountId: picked.id, pid });
72
+ return { store: opencodeGoStoreDirFor(picked.id), id: picked.id, reused: false };
73
+ });
74
+ if (granted && !("denied" in granted)) log("seat.grant", { account: granted.id.slice(0, 8), pid, reused: granted.reused });
75
+ return granted;
76
+ }
77
+
50
78
  export const opencodeGo: Provider = statusOnlyProvider({
51
79
  name: "opencode-go",
52
80
  flag: " --opencode-go",
@@ -62,6 +90,7 @@ export const opencodeGo: Provider = statusOnlyProvider({
62
90
  liveAuthPath,
63
91
  readAuth,
64
92
  liveId: () => null,
93
+ presence: () => seatCounts(opencodeGoPaths.presenceDir),
65
94
  versionOk: (out) => out !== "",
66
95
  importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
67
96
  foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
package/src/lib/paths.ts CHANGED
@@ -133,6 +133,7 @@ export function grokSeatFromEnv(accountIds: string[], env: Record<string, string
133
133
  export const opencodeGoPaths = {
134
134
  storesDir: join(TM_HOME, "opencode-go-stores"),
135
135
  onboardDir: join(TM_HOME, "opencode-go-onboard"),
136
+ presenceDir: join(TM_HOME, "opencode-go-live"),
136
137
  } as const;
137
138
 
138
139
  export function opencodeGoStoreDirFor(accountId: string): string {
package/src/lib/picker.ts CHANGED
@@ -122,6 +122,7 @@ const swapPreference = (ctx: PickCtx) => [
122
122
  (a: Account) => -pacePressure(a, ctx),
123
123
  (a: Account) => weeklyExpiry(a, ctx.now),
124
124
  (a: Account) => weeklyWindow(a)?.usedPercentage ?? 101,
125
+ ...(ctx.seats == null ? [] : [(a: Account) => ctx.seats?.get(a.id) ?? 0]),
125
126
  ];
126
127
 
127
128
  export function pickBest(accounts: Account[], ctx: PickCtx): Account | null {
@@ -8,6 +8,8 @@ export type SampleReport = { ok: true; source: "statusline" | "probe" | "cached"
8
8
 
9
9
  export type SwapFailure = "dead-grant" | "skip" | "fatal";
10
10
 
11
+ export type SeatBorrow = { store: string; id: string; reused: boolean } | { denied: string } | null;
12
+
11
13
  export class StoreUnusableError extends Error {
12
14
  constructor(message: string) {
13
15
  super(message);
@@ -60,7 +60,7 @@ function listDir(dir: string, root: string): Dirent[] {
60
60
  }
61
61
 
62
62
  function tmpSweepDirs(root: string): string[] {
63
- const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
63
+ const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir, opencodeGoPaths.presenceDir];
64
64
  for (const parent of STORE_PARENTS) {
65
65
  for (const child of listDir(parent, root)) {
66
66
  if (child.isDirectory()) dirs.push(join(parent, child.name));
@@ -24,6 +24,7 @@ export type StatusOnlySpec = {
24
24
  liveAuthPath: () => string;
25
25
  readAuth: (path: string) => AuthEntry[];
26
26
  liveId: () => string | null;
27
+ presence: () => Map<string, number>;
27
28
  versionOk: (versionOutput: string) => boolean;
28
29
  importIntro: string;
29
30
  foundLive: (count: number) => string;
@@ -101,7 +102,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
101
102
  waitsWhenDepleted: false,
102
103
  statusOnly: true,
103
104
  liveId: spec.liveId,
104
- presence: () => new Map(),
105
+ presence: spec.presence,
105
106
  gatedFamilies: () => null,
106
107
  observeLive: async (account) => (account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null),
107
108
  samplePool,
package/src/main.ts CHANGED
@@ -39,6 +39,7 @@ const PI_FLAG = "--pi";
39
39
  const JSON_COMMANDS = new Set(["status", "config", "check"]);
40
40
  const CODEX_COMMANDS = new Set(["init", "add", "auth", "rm", "rename", "seat"]);
41
41
  const STATUS_ONLY_COMMANDS = new Set(["init", "add", "auth", "rm", "rename"]);
42
+ const OPENCODE_GO_COMMANDS = new Set([...STATUS_ONLY_COMMANDS, "seat"]);
42
43
 
43
44
  function printHelp(): void {
44
45
  console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
@@ -59,7 +60,9 @@ function printHelp(): void {
59
60
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
60
61
  ${c.cyan("tokenmaxxing rename")} [--codex | --grok | --opencode-go] <sel> <label>
61
62
  ${c.cyan("tokenmaxxing rm")} [--codex | --grok | --opencode-go] <sel>
62
- ${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, reserved until <pid> exits; exit 1 = none usable, fall back to the ambient login
63
+ ${c.cyan("tokenmaxxing seat")} <pid> lend one pooled Claude account to an unattended consumer until <pid> exits: prints the store directory to set as CLAUDE_SECURESTORAGE_CONFIG_DIR (the directory itself, as its owner, never a copy of its credential); host sessions and other borrowers share the account; exit 1 = none usable
64
+ ${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, reserved to <pid> alone until it exits because one auth.json serves one serialized job stream; exit 1 = none usable, fall back to the ambient login
65
+ ${c.cyan("tokenmaxxing seat --opencode-go")} <pid> lend one pooled opencode-go key to an unattended consumer until <pid> exits: prints the store directory, whose auth.json is an opencode auth file (mount it at $XDG_DATA_HOME/opencode/auth.json, then run a model such as opencode-go/mimo-v2.6-pro); other borrowers share the key; exit 1 = none usable
63
66
  ${c.cyan("tokenmaxxing serve")} serve the CLIProxyAPI-compatible usage API on http://localhost:<hub.port> (default 8317) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows every pooled Claude and Codex account's quota; the management key is the contents of hub-key in the state directory
64
67
  ${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
65
68
 
@@ -127,10 +130,13 @@ async function main(): Promise<number> {
127
130
  return 2;
128
131
  }
129
132
 
130
- if ((provider === grok || provider === opencodeGo) && (sub == null || !STATUS_ONLY_COMMANDS.has(sub))) {
133
+ if (provider === grok || provider === opencodeGo) {
131
134
  const flag = provider === grok ? GROK_FLAG : OPENCODE_GO_FLAG;
132
- emitError({ json, message: `${flag} applies to ${[...STATUS_ONLY_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
133
- return 2;
135
+ const commands = provider === grok ? STATUS_ONLY_COMMANDS : OPENCODE_GO_COMMANDS;
136
+ if (sub == null || !commands.has(sub)) {
137
+ emitError({ json, message: `${flag} applies to ${[...commands].join(", ")}, not ${sub ?? "status"}` });
138
+ return 2;
139
+ }
134
140
  }
135
141
 
136
142
  if (json && sub != null && !JSON_COMMANDS.has(sub)) {
@@ -167,13 +173,7 @@ async function main(): Promise<number> {
167
173
  case "doctor": return cmdDoctor();
168
174
  case "rm": return cmdRm(provider, args[1]);
169
175
  case "rename": return cmdRename(provider, args.slice(1));
170
- case "seat": {
171
- if (provider !== codex) {
172
- emitError({ message: `seat borrows a pooled codex account - pass ${CODEX_FLAG} (usage: tokenmaxxing seat ${CODEX_FLAG} <pid>)` });
173
- return 2;
174
- }
175
- return cmdSeat(args[1], args.slice(2));
176
- }
176
+ case "seat": return cmdSeat(provider === codex ? "codex" : provider === opencodeGo ? "opencode-go" : "claude", args[1], args.slice(2));
177
177
  case "serve": return cmdServe(args.slice(1));
178
178
  case "uninstall": {
179
179
  if (args.length > 1) {