tokenmaxxing 1.157.0 → 1.159.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Automatic Claude Code account switching. You run `claude` exactly as always; every session starts on the pooled account with the most session-window headroom per running session, and when the account a session runs on crosses its bar (**90%** of the 5h session window or **98%** of a weekly window by default), tokenmaxxing resumes that session on a fresher account at a safe turn boundary. Works across many concurrent sessions at once, each on its own account; a fully depleted pool queues sessions first come first served onto reset targets (at most eight waiters per account; overflow waits for the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset only when the horizon is full), pauses each one with a countdown, and auto-resumes at its assigned reset (further out than twice `policy.maxWaitMs`, the session stays put rather than parking for hours).
4
4
 
5
- > Scope: **Claude Code first (macOS + Linux, the latter since 2026-07-09).** Codex support landed in 0.13.0 (2026-07-16) with its own parallel state and supervisor; since 1.35.0 both run on one engine. Codex's verified internals live in AGENTS.md's Codex sections.
5
+ > Scope: **Claude Code first (macOS + Linux, the latter since 2026-07-09).** Codex support landed in 0.13.0 (2026-07-16) with its own parallel state and supervisor; since 1.35.0 both run on one engine. Codex's verified internals live in `.claude/rules/codex.md`.
6
6
  >
7
7
  > Status: **implemented** (v0.1.0, 2026-07-09; per-account stores since 1.37.0, 2026-09-12). TypeScript on Bun, shipped as SOURCE with a bun-shebang bin (the compiled-binary distribution was deleted in 0.2.1); Zod validates every external-boundary payload, JSON config, es-toolkit for utilities, `flock(2)` via `bun:ffi`. Load-bearing external facts were adversarially verified against the `2.1.204` binary + docs when first shipped and re-verified against `2.1.269` for the store model (§2). What the acceptance gates showed is in §9.
8
8
 
@@ -127,7 +127,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
127
127
  ---
128
128
 
129
129
  ## 8. Stack
130
- TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` and `codex` supervisors, the statusLine and subagent statusLine shims, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry with `--no-env-file`, so a `.env` in the working directory is not imported into the supervisor or the session (a longer `/bin/sh` shim under Nix that drops its own directory from PATH; the Stop path runs every turn, and bun's start-up stays low-millisecond). A Bun global install refreshes itself from npm on the check tick, once a day. Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a merge that changes a packed file releases itself: CI assigns the next minor version on main, publishes, and tags it (details in AGENTS.md "Release and CI"). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
130
+ TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` and `codex` supervisors, the statusLine and subagent statusLine shims, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry with `--no-env-file`, so a `.env` in the working directory is not imported into the supervisor or the session (a longer `/bin/sh` shim under Nix that drops its own directory from PATH; the Stop path runs every turn, and bun's start-up stays low-millisecond). A Bun global install refreshes itself from npm on the check tick, once a day. Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a merge that changes a packed file releases itself: CI assigns the next minor version on main, publishes, and tags it (details in `.claude/rules/ci.md` and the `release` skill). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
131
131
 
132
132
  ---
133
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.157.0",
3
+ "version": "1.159.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/check.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { refreshOpenRouterCredit } from "../lib/apikeys.ts";
1
2
  import { claude } from "../lib/claude.ts";
2
3
  import { codex } from "../lib/codex.ts";
3
4
  import { evaluateAndMaybeSwap, useBankedResets } from "../lib/decide.ts";
@@ -16,6 +17,7 @@ export async function cmdCheck(json = false): Promise<number> {
16
17
  d = await evaluateAndMaybeSwap(claude, now);
17
18
  await useBankedResets(codex, loadConfig(), now);
18
19
  await sampleOldest(loadConfig());
20
+ await refreshOpenRouterCredit();
19
21
  await maybeAutoUpdate();
20
22
  } catch (e) {
21
23
  const detail = errorMessage(e);
package/src/cli/key.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { z } from "zod";
2
- import { keySessions, loadApiKeys, removeApiKeySecret, saveApiKeys, settleLiveSessionCosts, writeApiKeySecret } from "../lib/apikeys.ts";
2
+ import { isOpenRouterKey, keySessions, loadApiKeys, OPENROUTER_PREFIX, readOpenRouterCredit, removeApiKeySecret, saveApiKeys, settleLiveSessionCosts, writeApiKeySecret } from "../lib/apikeys.ts";
3
3
  import { withLock } from "../lib/lock.ts";
4
+ import { errorMessage } from "../lib/log.ts";
4
5
  import { claudePool } from "../lib/paths.ts";
5
6
  import { readStdin } from "../lib/proc.ts";
6
7
  import type { ApiKey } from "../lib/types.ts";
7
8
  import { c, emitError, plain } from "./render.ts";
8
9
 
9
- const USAGE = "usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key on stdin) | key credit <label> <usd> | key rm <label>";
10
+ const USAGE =
11
+ "usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key at the prompt, or on stdin when piped) | key credit <label> <usd> (an OpenRouter key takes no amount and clears its refusal) | key rm <label>";
10
12
 
11
13
  const UsdSchema = z.string().trim().min(1).transform(Number).pipe(z.number().min(0));
12
14
 
@@ -44,13 +46,9 @@ async function add(args: string[]): Promise<number> {
44
46
  return 2;
45
47
  }
46
48
  const workspaceId = workspace?.data;
47
- if (process.stdin.isTTY) {
48
- emitError({ message: "refusing to read the API key from a terminal, which would echo it - pipe it in: read -rs KEY && printf '%s' \"$KEY\" | tokenmaxxing key add <label> ...; unset KEY" });
49
- return 2;
50
- }
51
- const secret = (await readStdin()).trim();
49
+ const secret = (process.stdin.isTTY ? (prompt("API key:") ?? "") : await readStdin()).trim();
52
50
  if (secret === "") {
53
- emitError({ message: "no API key on stdin - pipe the key in, for example from your password manager" });
51
+ emitError({ message: "no API key given - paste it at the prompt or pipe it in" });
54
52
  return 2;
55
53
  }
56
54
  if (secret.includes("\n") || secret.includes("\r")) {
@@ -61,6 +59,19 @@ async function add(args: string[]): Promise<number> {
61
59
  emitError({ message: "that is an Admin API key, which cannot run inference - create a regular API key in the Console" });
62
60
  return 2;
63
61
  }
62
+ let openrouter: ApiKey["openrouter"];
63
+ if (secret.startsWith(OPENROUTER_PREFIX)) {
64
+ if (credit != null || workspaceId != null) {
65
+ emitError({ message: "an OpenRouter key takes no credit-usd or workspace-id - its credit left is the key's limit_remaining on OpenRouter" });
66
+ return 2;
67
+ }
68
+ try {
69
+ openrouter = await readOpenRouterCredit(secret);
70
+ } catch (e) {
71
+ emitError({ message: `could not read that OpenRouter key: ${errorMessage(e)}` });
72
+ return 1;
73
+ }
74
+ }
64
75
  return withLock(claudePool.lockFile, () => {
65
76
  const idx = loadApiKeys();
66
77
  const taken = findKey(idx.keys, label);
@@ -80,20 +91,24 @@ async function add(args: string[]): Promise<number> {
80
91
  spentUsd: 0,
81
92
  creditSetAt: now,
82
93
  addedAt: now,
94
+ ...(openrouter != null ? { openrouter } : {}),
83
95
  });
84
96
  saveApiKeys(idx);
85
- console.log(`added API key ${c.bold(label)} (${credit != null ? `credit ${usd(credit)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`);
97
+ const left = openrouter != null ? openrouter.remainingUsd : credit;
98
+ console.log(
99
+ `added ${openrouter != null ? "OpenRouter" : "API"} key ${c.bold(label)} (${left != null ? `credit ${usd(left)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`,
100
+ );
86
101
  return 0;
87
102
  });
88
103
  }
89
104
 
90
105
  async function credit(args: string[]): Promise<number> {
91
106
  const [label, amountRaw] = args;
92
- if (!label || amountRaw == null || args.length > 2) {
107
+ if (!label || args.length > 2) {
93
108
  emitError({ message: USAGE, paint: plain });
94
109
  return 2;
95
110
  }
96
- const amount = parseUsd(amountRaw);
111
+ const amount = amountRaw == null ? undefined : parseUsd(amountRaw);
97
112
  if (amount === null) return 2;
98
113
  return withLock(claudePool.lockFile, () => {
99
114
  const idx = loadApiKeys();
@@ -102,6 +117,20 @@ async function credit(args: string[]): Promise<number> {
102
117
  emitError({ message: `no API key is labeled "${label}"` });
103
118
  return 1;
104
119
  }
120
+ if (isOpenRouterKey(key)) {
121
+ if (amount != null) {
122
+ emitError({ message: `${key.label} is an OpenRouter key - its credit left is the key's limit_remaining, read from OpenRouter on every check tick; set the limit on OpenRouter. \`key credit ${key.label}\` with no amount clears a refusal` });
123
+ return 1;
124
+ }
125
+ key.refusedAt = undefined;
126
+ saveApiKeys(idx);
127
+ console.log(`cleared the refusal on ${c.bold(key.label)}`);
128
+ return 0;
129
+ }
130
+ if (amount == null) {
131
+ emitError({ message: USAGE, paint: plain });
132
+ return 2;
133
+ }
105
134
  key.creditUsd = amount;
106
135
  key.spentUsd = 0;
107
136
  key.creditSetAt = Date.now();
@@ -332,7 +332,7 @@ function systemLine(sid: string, text: string): string {
332
332
  }
333
333
 
334
334
  function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; onKey: boolean }): string {
335
- const where = input.onKey ? "on an Anthropic API key, because every pooled account is at its limit or the previous key ran out" : "on an account with quota headroom";
335
+ const where = input.onKey ? "on an API key, because every pooled account is at its limit or the previous key ran out" : "on an account with quota headroom";
336
336
  const moved = input.compacted
337
337
  ? `tokenmaxxing compacted this conversation and resumed the session ${where}.`
338
338
  : `tokenmaxxing resumed this session ${where}.`;
@@ -1,13 +1,16 @@
1
1
  import { chmodSync, closeSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { countBy, isEqual, sortBy } from "es-toolkit";
4
+ import { z } from "zod";
4
5
  import { writeFileAtomic } from "./atomic.ts";
6
+ import { http, safeErrorDetail } from "./http.ts";
5
7
  import { withLock } from "./lock.ts";
6
- import { claudePool, optionalEnv, paths } from "./paths.ts";
8
+ import { errorMessage, log } from "./log.ts";
9
+ import { claudePool, env, optionalEnv, paths } from "./paths.ts";
7
10
  import { livingPresences } from "./presence.ts";
8
11
  import type { ApiKeyPool } from "./provider.ts";
9
12
  import { readJsonFile } from "./state.ts";
10
- import { ApiKeysIndexSchema, SessionCostSchema, type ApiKey, type ApiKeysIndex, type SessionCost } from "./types.ts";
13
+ import { ApiKeysIndexSchema, JsonTextSchema, SessionCostSchema, type ApiKey, type ApiKeysIndex, type SessionCost } from "./types.ts";
11
14
 
12
15
  export const API_KEY_ENV = "TOKENMAXXING_API_KEY_ID";
13
16
  export const API_KEY_FD = 3;
@@ -15,7 +18,58 @@ export const API_KEY_FD = 3;
15
18
  export function apiKeyEnv(env: Record<string, string | undefined>, key: ApiKey): Record<string, string | undefined> {
16
19
  const header = key.workspaceId == null ? null : `anthropic-workspace-id: ${key.workspaceId}`;
17
20
  const headers = header == null ? env.ANTHROPIC_CUSTOM_HEADERS : env.ANTHROPIC_CUSTOM_HEADERS ? `${env.ANTHROPIC_CUSTOM_HEADERS}\n${header}` : header;
18
- return { ...env, CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR: String(API_KEY_FD), ...(headers != null ? { ANTHROPIC_CUSTOM_HEADERS: headers } : {}) };
21
+ return {
22
+ ...env,
23
+ CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR: String(API_KEY_FD),
24
+ ...(headers != null ? { ANTHROPIC_CUSTOM_HEADERS: headers } : {}),
25
+ ...(isOpenRouterKey(key) ? { ANTHROPIC_BASE_URL: OPENROUTER_URL } : {}),
26
+ };
27
+ }
28
+
29
+ export const OPENROUTER_PREFIX = "sk-or-";
30
+
31
+ export function isOpenRouterKey(key: ApiKey): boolean {
32
+ return readApiKey(key).startsWith(OPENROUTER_PREFIX);
33
+ }
34
+
35
+ const OPENROUTER_URL = env("TOKENMAXXING_OPENROUTER_URL", "https://openrouter.ai/api");
36
+ const OPENROUTER_READ_DEADLINE_MS = 10_000;
37
+ const OpenRouterKeySchema = z.looseObject({ data: z.looseObject({ limit_remaining: z.number().nullable() }) });
38
+
39
+ export async function readOpenRouterCredit(secret: string): Promise<NonNullable<ApiKey["openrouter"]>> {
40
+ const at = Date.now();
41
+ const res = await http.get(`${OPENROUTER_URL}/v1/key`, { headers: { Authorization: `Bearer ${secret}` }, signal: AbortSignal.timeout(OPENROUTER_READ_DEADLINE_MS) });
42
+ const text = await res.text();
43
+ if (!res.ok) throw new Error(`OpenRouter key read answered HTTP ${res.status}: ${safeErrorDetail({ text })}`);
44
+ return { remainingUsd: OpenRouterKeySchema.parse(JsonTextSchema.safeParse(text).data).data.limit_remaining, at };
45
+ }
46
+
47
+ export async function refreshOpenRouterCredit(): Promise<void> {
48
+ if (!existsSync(claudePool.apiKeysJson)) return;
49
+ const reads = await Promise.all(
50
+ availableKeys().map(async (k) => {
51
+ try {
52
+ if (!isOpenRouterKey(k)) return null;
53
+ return { id: k.id, credit: await readOpenRouterCredit(readApiKey(k).trim()) };
54
+ } catch (e) {
55
+ log("apikey.openrouter_read_failed", { key: k.id, err: errorMessage(e) });
56
+ return null;
57
+ }
58
+ }),
59
+ );
60
+ const landed = reads.filter((r) => r != null);
61
+ if (landed.length === 0) return;
62
+ await withLock(claudePool.lockFile, () => {
63
+ const idx = loadApiKeys();
64
+ let changed = false;
65
+ for (const r of landed) {
66
+ const key = idx.keys.find((k) => k.id === r.id);
67
+ if (key == null || r.credit.at <= (key.openrouter?.at ?? 0)) continue;
68
+ key.openrouter = r.credit;
69
+ changed = true;
70
+ }
71
+ if (changed) saveApiKeys(idx);
72
+ });
19
73
  }
20
74
 
21
75
  export function readApiKey(key: ApiKey): string {
@@ -57,6 +111,7 @@ export function removeApiKeySecret(id: string): void {
57
111
  }
58
112
 
59
113
  export function creditLeft(k: ApiKey): number | null {
114
+ if (k.openrouter != null) return k.openrouter.remainingUsd;
60
115
  return k.creditUsd == null ? null : k.creditUsd - k.spentUsd;
61
116
  }
62
117
 
@@ -157,7 +212,7 @@ export async function foldSessionCost(sid: string, keyId: string | null, restore
157
212
  const prev = idx.baselines[seen.sessionId];
158
213
  if (prev != null && seen.usd <= prev.usd) continue;
159
214
  const delta = Math.max(0, seen.usd - (prev?.usd ?? restoredCost(seen.sessionId)));
160
- if (key) key.spentUsd += delta;
215
+ if (key && !isOpenRouterKey(key)) key.spentUsd += delta;
161
216
  idx.baselines[seen.sessionId] = { usd: seen.usd, at: Date.now() };
162
217
  changed = true;
163
218
  }
package/src/lib/types.ts CHANGED
@@ -181,6 +181,7 @@ export const ApiKeySchema = z.object({
181
181
  creditSetAt: z.number(),
182
182
  refusedAt: z.number().optional(),
183
183
  addedAt: z.number(),
184
+ openrouter: z.object({ remainingUsd: z.number().nullable(), at: z.number() }).optional(),
184
185
  });
185
186
  export type ApiKey = z.infer<typeof ApiKeySchema>;
186
187
 
package/src/main.ts CHANGED
@@ -62,8 +62,8 @@ function printHelp(): void {
62
62
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
63
63
  ${c.cyan("tokenmaxxing rename")} [--codex | --grok] <sel> <label>
64
64
  ${c.cyan("tokenmaxxing rm")} [--codex | --grok] <sel>
65
- ${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic API key read from stdin; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces)
66
- ${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count
65
+ ${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic or OpenRouter (sk-or-...) API key read from stdin or the prompt; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces; an OpenRouter key takes neither argument and reads its credit from OpenRouter)
66
+ ${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count (an OpenRouter key takes no amount: key credit <label> clears its refusal)
67
67
  ${c.cyan("tokenmaxxing key rm")} <label> delete the key and its stored secret (refused while a session runs on it)
68
68
  ${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
69
69
  ${c.cyan("tokenmaxxing seat")} <pid> --store <id> lend the Claude store with that 8-character id instead of the ranked pick, checking only that it is pooled, holds a usable credential, and is not flagged for reauthentication; bars, the credits flag, and holds are the caller's job; exit 0 = granted, 1 = the store is unusable, 2 = unknown store, <pid> already holds another store, or bad usage