tokenmaxxing 0.10.0 → 0.11.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
@@ -99,6 +99,8 @@ The decision engages at `five_hour >= 50%` (policy.greedySessionFloor): from the
99
99
 
100
100
  **v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
101
101
 
102
+ **Shipped since (0.11.0):** a programmatic SDK surface (`src/sdk.ts`, the package's `exports["."]`) for pairing with the Claude Agent SDK - personal use across the owner's own pooled accounts. The Agent SDK reads credentials per subprocess spawn and has no statusLine, so the surface is boundary-driven: run the shared switch decision before a spawn (`ensureBestAccount`) and at Stop-hook turn boundaries (`stopHookCheck`), and hand the SDK a pinned real-claude path plus a full replacement env scrubbed of credential overrides (`pooledOptions`).
103
+
102
104
  **Later:** Codex as a second pool; tool-agnostic picker.
103
105
 
104
106
  **Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the single refresh-grant call in the swap.
package/README.md CHANGED
@@ -75,6 +75,31 @@ The **target** is chosen greedily off each account's cached windows: among usabl
75
75
 
76
76
  State lives entirely in `~/.config/tokenmaxxing/`. Per-account credentials follow the platform's Claude Code store: the login keychain on macOS (`tokenmaxxing-cred-<uuid8>` items, never plaintext on disk), 0600 files under `~/.config/tokenmaxxing/creds/` on Linux (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
77
77
 
78
+ ## Pairing with the Claude Agent SDK
79
+
80
+ For agents you build on the [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) against **your own** pooled accounts, `tokenmaxxing` is importable as a library (your agent app must run under Bun: tokenmaxxing ships TypeScript source and uses `bun:ffi`):
81
+
82
+ ```ts
83
+ import { query } from "@anthropic-ai/claude-agent-sdk";
84
+ import { ensureBestAccount, pooledOptions, stopHookCheck } from "tokenmaxxing";
85
+
86
+ await ensureBestAccount(); // run the switch decision before the spawn (swaps once it engages - see below)
87
+
88
+ for await (const message of query({
89
+ prompt: "...",
90
+ options: {
91
+ ...pooledOptions(), // pinned real claude + scrubbed env -> the pooled live credential
92
+ hooks: { Stop: [{ hooks: [stopHookCheck] }] }, // re-decide at every turn boundary
93
+ },
94
+ })) {
95
+ // capture the session id from the init message if you want `resume` across swaps
96
+ }
97
+ ```
98
+
99
+ The SDK reads credentials when it spawns the claude subprocess and has no statusLine, so none of the CLI-side supervisor machinery applies; the integration is boundary-driven instead. `ensureBestAccount()` runs the exact greedy decision the CLI hooks and timer run (screening bars, pace-pressure target, post-swap cooldown - all shared code); like them, it deliberately does nothing until the decision engages (the active session past `policy.greedySessionFloor`, or a bar crossed), so a fresh account rides instead of churning. `pooledOptions()` pins `pathToClaudeCodeExecutable` to the real claude binary and supplies a full replacement `env` with every ambient credential override (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, ...) scrubbed, so the subprocess resolves the pool's live credential and nothing else. The pooled surface requires the default Claude Code credential store: it fails fast if `CLAUDE_CONFIG_DIR` or `CLAUDE_SECURESTORAGE_CONFIG_DIR` is set in your app's environment, because a swap would write the live credential where those point while the spawned subprocess reads the default store. `stopHookCheck` re-runs the decision at turn boundaries; a swap it lands takes effect on the next subprocess spawn (it never yanks a mid-query token). If your app loads user settings (see the SDK's `settingSources`), the Stop hook `tokenmaxxing init` installed may already fire in SDK sessions too - `stopHookCheck` makes the check explicit and works when settings are restricted.
100
+
101
+ This is for pooling **your own** subscription accounts in agents you run yourself - the same personal-use posture as the CLI. Anthropic does not allow third-party products to offer claude.ai login or rate limits, including agents built on the Agent SDK; don't ship this surface to third parties.
102
+
78
103
  ## Honest limitations
79
104
 
80
105
  - **One cold turn.** The first turn after resuming on a new account re-uploads context once (prompt cache is org-scoped).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,6 +13,10 @@
13
13
  "bin": {
14
14
  "tokenmaxxing": "./src/main.ts"
15
15
  },
16
+ "exports": {
17
+ ".": "./src/sdk.ts",
18
+ "./package.json": "./package.json"
19
+ },
16
20
  "files": [
17
21
  "src",
18
22
  "README.md",
package/src/lib/usage.ts CHANGED
@@ -197,7 +197,7 @@ export function parseUsageText(text: string, now = Date.now()): UsageWindows | n
197
197
  /** Env-var identity/credential overrides the claude binary honors BEFORE its
198
198
  * keychain lookup (verified 2.1.205). A probe MUST scrub every one of these or
199
199
  * an ambient value silently meters the wrong account. */
200
- const CRED_ENV_OVERRIDES = [
200
+ export const CRED_ENV_OVERRIDES = [
201
201
  "ANTHROPIC_API_KEY",
202
202
  "ANTHROPIC_AUTH_TOKEN",
203
203
  "CLAUDE_CODE_OAUTH_TOKEN",
package/src/sdk.ts ADDED
@@ -0,0 +1,110 @@
1
+ // Programmatic surface for pairing tokenmaxxing with the Claude Code Agent SDK
2
+ // (personal use across your own pooled accounts - user decision 2026-07-16).
3
+ //
4
+ // The Agent SDK spawns a claude CLI subprocess per query() and that subprocess
5
+ // reads credentials at spawn time: no statusLine tee, no supervisor, no
6
+ // mid-query hot-swap. So the integration is boundary-driven - run the switch
7
+ // decision BEFORE a spawn so it lands on the best account, and again at
8
+ // Stop-hook turn boundaries so the NEXT spawn does; a running subprocess keeps
9
+ // its snapshotted token either way, which is exactly the clean-boundary
10
+ // semantics the CLI supervisor enforces with markers.
11
+ //
12
+ // Nothing here imports the Agent SDK: the helpers return plain values that
13
+ // spread structurally into its Options, so tokenmaxxing keeps its exact
14
+ // dependency set (zod, es-toolkit, ky).
15
+
16
+ import { z } from "zod";
17
+ import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./lib/claudebin.ts";
18
+ import { evaluateAndMaybeSwap, type SwapDecision } from "./lib/decide.ts";
19
+ import { CRED_ENV_OVERRIDES } from "./lib/usage.ts";
20
+ import { log } from "./lib/log.ts";
21
+
22
+ export { evaluateAndMaybeSwap };
23
+ export type { SwapDecision };
24
+
25
+ /**
26
+ * Run the same greedy pace-pressure decision the hooks and check timer run
27
+ * (never anticipatory: there is no supervisor to pause an SDK session, so a
28
+ * depleted pre-park would yank it onto a known-blocked account for nothing).
29
+ * Call it right before query() so the subprocess spawns on the best account.
30
+ */
31
+ export async function ensureBestAccount(now = Date.now()): Promise<SwapDecision> {
32
+ return evaluateAndMaybeSwap(now, false);
33
+ }
34
+
35
+ /** The pinned real claude binary, for Options.pathToClaudeCodeExecutable.
36
+ * Never the supervisor wrapper: an SDK subprocess is headless print mode, so
37
+ * the wrapper's respawn machinery buys nothing and only adds recursion risk. */
38
+ export function claudeExecutablePath(): string {
39
+ return resolveRealClaude();
40
+ }
41
+
42
+ /** The pooled surface requires the DEFAULT Claude Code credential store. An
43
+ * ambient config-dir override desyncs the two sides of a swap on Linux: the
44
+ * swap (running in THIS process) writes the live credential where these vars
45
+ * point (credDir() honors them), while the scrubbed subprocess reads the
46
+ * default store - so the subprocess silently runs on a stale or absent
47
+ * credential. Fail fast on both platforms rather than platform-split the
48
+ * behavior (adversarial review catch, 2026-07-16). */
49
+ const AMBIENT_STORE_VARS = ["CLAUDE_SECURESTORAGE_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
50
+
51
+ /**
52
+ * The env an SDK-spawned claude must run under to meter the POOLED live
53
+ * credential: every ambient credential override is scrubbed (claude honors
54
+ * them BEFORE its keychain/file lookup, so one inherited ANTHROPIC_API_KEY
55
+ * silently meters the wrong account), and the wrap depth is preset to the cap
56
+ * so a poisoned claudeBin pin that leads back into the tokenmaxxing wrapper
57
+ * aborts on first entry instead of fork-bombing.
58
+ *
59
+ * Returns a FULL environment, not a patch: the Agent SDK's Options.env
60
+ * REPLACES the subprocess env rather than merging over process.env (verified
61
+ * against the official TS reference 2026-07-16), which is what makes deleting
62
+ * keys from this copy effective.
63
+ */
64
+ export function pooledSpawnEnv(): Record<string, string> {
65
+ for (const k of AMBIENT_STORE_VARS) {
66
+ if (process.env[k]) {
67
+ throw new Error(
68
+ `${k} is set: the pooled SDK surface requires the default Claude Code credential store (a swap writes the live credential where ${k} points, while the spawned subprocess reads the default store). Unset it in the process running tokenmaxxing.`,
69
+ );
70
+ }
71
+ }
72
+ const env: Record<string, string> = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) };
73
+ for (const k of CRED_ENV_OVERRIDES) delete env[k];
74
+ return env;
75
+ }
76
+
77
+ const PooledOptionsSchema = z.object({
78
+ pathToClaudeCodeExecutable: z.string(),
79
+ env: z.record(z.string(), z.string()),
80
+ });
81
+ export type PooledOptions = z.infer<typeof PooledOptionsSchema>;
82
+
83
+ /** Options fragment to spread into the Agent SDK's Options. */
84
+ export function pooledOptions(): PooledOptions {
85
+ return PooledOptionsSchema.parse({
86
+ pathToClaudeCodeExecutable: claudeExecutablePath(),
87
+ env: pooledSpawnEnv(),
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Agent SDK Stop-hook callback (structurally matches HookCallback; the args
93
+ * are irrelevant here; `{}` is the documented no-op output). Runs the switch
94
+ * decision at the turn boundary; a swap landed here takes effect on the next
95
+ * subprocess spawn. Errors are caught LOUDLY (stderr + log), not rethrown:
96
+ * the SDK hooks reference states an unhandled exception can interrupt the
97
+ * agent (verified 2026-07-16), and aborting the caller's turn because a
98
+ * switch check failed costs more than riding out the current account. Call
99
+ * ensureBestAccount() directly where a broken pool should throw.
100
+ */
101
+ export async function stopHookCheck(): Promise<Record<string, never>> {
102
+ try {
103
+ await evaluateAndMaybeSwap(Date.now(), false);
104
+ } catch (e) {
105
+ const err = String((e as Error).message ?? e);
106
+ console.error(`tokenmaxxing: switch check failed at turn boundary: ${err}`);
107
+ log("sdk.stop_error", { err });
108
+ }
109
+ return {};
110
+ }