tokenmaxxing 1.38.0 → 1.40.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 +70 -62
- package/README.md +30 -30
- package/package.json +2 -2
- package/src/cli/check.ts +4 -2
- package/src/cli/doctor.ts +19 -32
- package/src/cli/init.ts +2 -2
- package/src/cli/rm.ts +3 -12
- package/src/cli/setuptoken.ts +2 -2
- package/src/cli/status.ts +8 -4
- package/src/cli/switch.ts +12 -71
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +9 -10
- package/src/entries/statusline.ts +5 -11
- package/src/entries/stopfailurehook.ts +12 -38
- package/src/entries/stophook.ts +8 -28
- package/src/entries/supervisor.ts +50 -10
- package/src/lib/claude.ts +122 -156
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +12 -14
- package/src/lib/credstore.ts +10 -21
- package/src/lib/decide.ts +24 -35
- package/src/lib/oauth.ts +2 -68
- package/src/lib/paths.ts +23 -13
- package/src/lib/picker.ts +8 -0
- package/src/lib/presence.ts +67 -0
- package/src/lib/provider.ts +2 -4
- package/src/lib/sample.ts +66 -259
- package/src/lib/sessions.ts +25 -0
- package/src/lib/state.ts +17 -42
- package/src/lib/types.ts +2 -11
- package/src/lib/update.ts +133 -0
- package/src/lib/usage.ts +7 -4
- package/src/main.ts +10 -17
- package/src/lib/claudejson.ts +0 -30
- package/src/lib/codexpresence.ts +0 -64
- package/src/lib/swap.ts +0 -198
package/DESIGN.md
CHANGED
|
@@ -1,121 +1,123 @@
|
|
|
1
1
|
# tokenmaxxing - design
|
|
2
2
|
|
|
3
|
-
Automatic Claude Code account switching. You run `claude` exactly as always;
|
|
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), 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 pauses a session with a countdown and auto-resumes at the soonest reset when it lands within `policy.maxWaitMs` (default 1h; further out, 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
|
|
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.
|
|
6
6
|
>
|
|
7
|
-
> Status: **implemented** (v0.1.0, 2026-07-09). 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`.
|
|
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
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
## 1. Why there is a thin supervisor (and why that's the whole trick)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Claude Code namespaces its credential by `CLAUDE_SECURESTORAGE_CONFIG_DIR` (verified in the 2.1.269 binary, `xSe`/`y_`): the variable moves only the credential store (a `.credentials.json` inside that directory on Linux, a keychain item named `Claude Code-credentials-<sha256(dir)[0:8]>` on macOS), while `~/.claude` (settings, transcripts, plugins) and `~/.claude.json` stay shared, so `--resume` works across stores. An empty namespaced store never falls back to the default login. Hooks, the statusline, and MCP servers inherit the variable. That is the whole mechanism: give every pooled account its own store, set the variable per session, and each session runs on its own account with no credential ever moving.
|
|
14
14
|
|
|
15
|
-
So the supervisor's job is
|
|
15
|
+
What the variable cannot give you is a change of account for a running process: the store is fixed at spawn. So the supervisor's job is placement and moves: on launch it **picks the seat** (see §5) and spawns claude under that account's store; when the session's Stop hook decides the seat is over its bar, the supervisor **replaces the process at a salvageable moment** - after a turn completes, the conversation is fully written to the transcript JSONL and `claude` is idle at the prompt, so killing it there loses nothing - and relaunches `claude --resume <session-id>` under the target's store. When the whole pool is over its bars it shows an interruptible countdown to the soonest reset first. **Thresholds sit below 100: the headroom is the budget to reach a clean turn boundary before the account actually hits its real limit.** The session window moves at 90 (a 5h reset is cheap to sit out), while the weekly windows drain to 98 (weekly allowance is use-it-or-lose-it).
|
|
16
16
|
|
|
17
|
-
A hook can't do the
|
|
17
|
+
A hook can't do the placement or the relaunch - when `claude` exits, the shell owns the terminal. So tokenmaxxing installs a **supervisor** (aliased to `claude`) that owns the process lifecycle:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
supervisor (you type `claude`) → real claude
|
|
21
|
-
▲
|
|
20
|
+
supervisor (you type `claude`) → real claude, CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<uuid8> → Stop hook
|
|
21
|
+
▲______________ relaunch --resume <sid> under another store ______________|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
It is a process manager only - spawn with inherited stdio plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic
|
|
24
|
+
It is a process manager only - spawn with inherited stdio plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic and never handles tokens. Everything else about `claude` is unchanged.
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
## 2. What tokenmaxxing installs
|
|
29
29
|
|
|
30
30
|
- A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`), or a shell function - you invoke it identically.
|
|
31
|
-
-
|
|
31
|
+
- Five `~/.claude/settings.json` entries (merged - other settings keys are preserved, but the `statusLine` slot is taken over): the tokenmaxxing `statusLine` renderer (native since 2026-07-11; it also tees usage), a `subagentStatusLine` (per-subagent rows in the agents panel), a `Stop` hook, a `StopFailure` hook, a `SessionStart` hook.
|
|
32
32
|
- **`~/.config/tokenmaxxing/`** - the single home for config and state:
|
|
33
33
|
- `config.json` - SPARSE overrides only (thresholds.session/weekly, claudeBin/codexBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
|
|
34
|
-
- `accounts.json` - non-secret index `{id, label, email, tier, windows, lastUsageAt, needsReauth, oauthAccount
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
34
|
+
- `accounts.json` - non-secret index `{id, label, email, tier, windows, lastUsageAt, lastProbeAt, enforcedUntil, needsReauth, oauthAccount}` (each cached window carries its name, duration, reset, and sample time; the same shape serves the Codex pool as `codex-accounts.json`). `activeId` is `null` for Claude: there is no pool-wide active account.
|
|
35
|
+
- `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
|
|
36
|
+
- `usage/<uuid8>.json` - the statusline tee per account, written by the sessions running on it.
|
|
37
|
+
- `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
|
|
38
|
+
- `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account plus a `waitUntil`.
|
|
37
39
|
- `bin/claude` - the supervisor.
|
|
38
|
-
- Per-account **
|
|
40
|
+
- 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.
|
|
39
41
|
|
|
40
|
-
- 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):
|
|
42
|
+
- 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 folds fresh tees into the index and samples the account whose `/usage` figure is oldest, so the cached figures every placement reads stay fresh.
|
|
41
43
|
|
|
42
|
-
The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process. The `claude` binary
|
|
44
|
+
The switching path runs no long-lived daemon. The statusline pushes usage. Hooks and the supervisor react. The periodic check job above is the only recurring process. The `claude` binary, `~/.claude`, `~/.claude.json`, and Claude Code's default credential store are untouched.
|
|
43
45
|
|
|
44
46
|
---
|
|
45
47
|
|
|
46
|
-
## 3. How a
|
|
48
|
+
## 3. How a move happens
|
|
47
49
|
|
|
48
50
|
### 3.1 Usage feed (free, push-based)
|
|
49
|
-
The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage
|
|
51
|
+
The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}`, after every turn, 300ms debounce, zero cost). tokenmaxxing's statusLine tees that to `usage/<uuid8>.json` for the session's seat, read from the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` (write-on-change, O(ms)), and renders its own native line (it replaced the earlier pass-through delegation on 2026-07-11: install takes the statusLine slot outright, so a pre-existing custom statusline command is overwritten). A session with no pooled seat writes no tee. Cold-start fallback when the seat has no tee: `TOKENMAXXING_PROBE=1 claude -p '/usage'` under the seat's store, with a throwaway `CLAUDE_CONFIG_DIR` so no hooks load and `[ -n "$TOKENMAXXING_PROBE" ] && exit 0` as every hook's first line to stop the nested process recursing (hooks fire in `-p` too). The probe scrubs every ambient credential override claude reads before the store (`CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`, etc.) and then sets the store it means to read, and retries the transient empty-footer case (claude prints local stats with no percentages when its own usage fetch throttles).
|
|
50
52
|
|
|
51
|
-
### 3.2 Detect +
|
|
52
|
-
1.
|
|
53
|
-
2.
|
|
54
|
-
3.
|
|
53
|
+
### 3.2 Detect + decide + signal (Stop hook, per turn)
|
|
54
|
+
1. Resolve the seat: the pooled account whose store the inherited `CLAUDE_SECURESTORAGE_CONFIG_DIR` names. No seat (a session started outside the supervisor) means nothing to move.
|
|
55
|
+
2. Read the seat's tee and exit while it is under both bars, as defined in the [switching policy](docs/content/docs/switching.mdx).
|
|
56
|
+
3. Take the pool lock, re-check, fold every account's tee into the index, select a candidate under the switching policy from the cached figures, and verify that the target's store holds a usable credential (a store Claude Code dead-cleared flags the account for re-auth and the next candidate is tried).
|
|
57
|
+
4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the soonest reset when the pool is depleted. No credential is written.
|
|
55
58
|
|
|
56
|
-
### 3.3
|
|
57
|
-
The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, shows an interruptible countdown to the reset
|
|
59
|
+
### 3.3 Move and depleted-pool pause (supervisor)
|
|
60
|
+
The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The `SessionStart` hook (source `resume`) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. A marker that fails to parse is never deleted and treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
|
|
58
61
|
|
|
59
|
-
### 3.4
|
|
60
|
-
1.
|
|
61
|
-
2.
|
|
62
|
-
3.
|
|
63
|
-
4. **Swap identity + mark B active** - atomically rewrite only the `oauthAccount` object in `~/.claude.json` (temp+rename) to B's, and write `activeId = B` in the SAME critical section, so a crash can't leave the installed credential and the active label pointing at different accounts.
|
|
64
|
-
5. Do steps 1, 3, 4 inside Claude's own refresh locks - the primary `<credDir>/.oauth_refresh.lock` plus the legacy sibling `<realpath(credDir)>.lock` (`~/.claude.lock` by default), both mkdir-based proper-lockfile locks, binary-verified against 2.1.214 - so the writes can't collide with a token refresh. Contention fails the swap fast (claude itself gives up with `lock_timeout` rather than refreshing unlocked); the next check retries.
|
|
62
|
+
### 3.4 Placement (supervisor, under the pool lock)
|
|
63
|
+
1. Fold every account's tee into the index.
|
|
64
|
+
2. Rank the usable accounts (§5) with the live session counts from `live/`, or the soonest-recovering account when none is usable; with no pooled account or every account needing re-auth, launch without a store (unmanaged).
|
|
65
|
+
3. Spawn claude with the store env set, then write `live/<sid>` with the child's pid and start time. The lock covers the pick and the presence write, so concurrent launches see each other.
|
|
65
66
|
|
|
66
67
|
### 3.5 Multiple concurrent sessions
|
|
67
|
-
Each terminal ran the supervisor, so each has its own child `claude
|
|
68
|
+
Each terminal ran the supervisor, so each has its own child `claude`, its own `--session-id`, its own seat, and its own presence file. Sessions spread over the pool at launch (the second launch never lands on the first's account while another fresh account exists), sessions on one account share its store and coordinate refreshes through Claude Code's own store-rooted refresh lock, and each session moves on its own seat's figures. A move never touches another session. Only a shared state read is serialized: the pool lock covers decisions, placement, and the sampler's reservation, never a `/usage` child.
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|
|
71
72
|
## 4. Onboarding (no `adopt`)
|
|
72
73
|
|
|
73
|
-
- **`tokenmaxxing init`
|
|
74
|
-
|
|
75
|
-
- **`tokenmaxxing
|
|
76
|
-
- **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose refresh token died (a needs-reauth account can never heal through a swap: the dead token is exactly what a swap would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (harvested `accountUuid` must match, else nothing changes) - the credential write and the needs-reauth clear happen in one flock critical section so a concurrent swap's harvest cannot clobber the fresh backup. Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
|
|
74
|
+
- **`tokenmaxxing init` logs your first pooled account in through an isolated browser sign-in, even if you are already signed in.** The login runs in a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard`, its token's owner is verified through the profile endpoint (`GET /api/oauth/profile`, matched on `account.uuid`), and the credential is harvested once into that account's store; the throwaway home and its credential item are deleted. The login you already had keeps its own grant in Claude Code's default store for sessions started outside the supervisor; it is never copied, because a copy of one grant in two stores dies at the first refresh. `init` also installs the supervisor + the five settings entries.
|
|
75
|
+
- **`tokenmaxxing add`** - registers *additional* accounts the same way, one store each. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway homes like this one and the probe homes under `sample/`; a session's store is always set through `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
|
|
76
|
+
- **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose store died (a needs-reauth account can never heal on its own: the dead grant is exactly what Claude Code's refresh would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (verified `accountUuid` must match, else nothing changes). Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
|
|
77
77
|
- Both commands exercise `security` reads/writes interactively (where a macOS keychain ACL prompt is acceptable), so the first access never happens cold inside a headless hook.
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
81
|
-
## 5.
|
|
82
|
-
The [switching policy](docs/content/docs/switching.mdx) is one rule: under both bars, hold; at or over a bar, move to the usable account
|
|
81
|
+
## 5. Placement and rotation policy
|
|
82
|
+
The [switching policy](docs/content/docs/switching.mdx) is one rule: under both bars, hold; at or over a bar, move to the usable account that ranks first; nothing usable, wait for the soonest reset. The same ranking places a session at launch.
|
|
83
83
|
|
|
84
|
-
-
|
|
85
|
-
- Selection reads the cached figures only. Each periodic check tick samples the
|
|
84
|
+
- Ranking (decided by consult for #79): greatest remaining headroom to the session bar divided by the number of sessions after placement, `(bar - used) / (sessions + 1)`, then highest weekly pace pressure (remaining weekly percent over time to the weekly reset), then soonest weekly reset, then lowest weekly usage; unknown usage ranks last. Organization membership is not an input. Codex keeps the pure pace ordering and excludes accounts running in another codex session, because a codex session cannot share a login.
|
|
85
|
+
- Selection reads the cached figures only, after folding every account's tee. Each periodic check tick samples the account whose last `/usage` attempt is oldest, skipping any attempted within `policy.usagePollTtlMs`, under that account's store, so every account is attempted about once per tick per pooled account, or once per `policy.usagePollTtlMs` plus a tick when that is longer, and its cached figure is as fresh as its last successful attempt or its sessions' latest push. The pool lock covers only the reservation and the result write, never the `/usage` child.
|
|
86
86
|
- The depleted pause happens at the bar, not at 100; Codex has no pause and rides its account until the server refuses it, because a running sibling cannot adopt another account without restarting.
|
|
87
|
+
- The Claude pool has no post-swap cooldown and no manual `switch`: a move is a respawn of one session under a fixed store, so nothing another session reads is invalidated by it, and placement at launch replaces the manual pick. Codex keeps both.
|
|
87
88
|
|
|
88
89
|
The [profile](docs/content/docs/switching-profile.mdx) records observed cache rewrites across swaps and does not infer subscription quota savings.
|
|
89
90
|
|
|
90
|
-
**Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session
|
|
91
|
+
**Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` under the seat's store (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session moves on the Fable cap; a Sonnet session rides the aggregate. Candidate screening applies the same per-model gate: a burnt Fable cap screens an account out of a move, and a launch, whose model is unknown, gates every configured family.
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
94
95
|
## 6. Honest papercuts
|
|
95
|
-
- **
|
|
96
|
-
- **
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
99
|
-
- **
|
|
100
|
-
- **Refresh-token rotation / parked-token rot.** Step 1 re-harvest is mandatory; a parked refresh token can be invalidated by logging in elsewhere → picker must catch `invalid_grant`, mark `needs_reauth`, fall through.
|
|
96
|
+
- **A move restarts the process.** Every move is a `claude` stop and `--resume`; anything typed in the split second before the SIGTERM is lost, the supervisor resets terminal mode so nothing is left garbled, and the first turn on the new account re-uploads context once (prompt cache is org-scoped; bigger on long transcripts).
|
|
97
|
+
- **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the next prompt continues the conversation on the fresh account. Projected threshold reduces this.
|
|
98
|
+
- **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on whatever login its environment names, by default Claude Code's default store, which tokenmaxxing never writes.
|
|
99
|
+
- **One shared identity file.** `~/.claude.json` keeps a single `oauthAccount`, which Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email. The store decides which credential a session uses.
|
|
100
|
+
- **Dead stores.** A refresh Claude Code cannot complete dead-clears the store; the sampler and the decision recognize the empty tokens without a request, flag the account, and `auth` repairs it in place.
|
|
101
101
|
- **statusLine fragility.** The native statusline is the most visible surface - a bug flickers or blanks the line for every session (and install takes the slot outright, replacing any custom statusline you had). Keep it O(ms), write-on-change.
|
|
102
|
-
- **Keychain blob size & ps-safety.**
|
|
103
|
-
- **settings.json is user-owned.** Install by merge; ship `tokenmaxxing doctor` to verify the supervisor +
|
|
102
|
+
- **Keychain blob size & ps-safety.** A store's keychain item also holds per-MCP-server OAuth state once Claude Code writes it, so it can exceed `security -i`'s ~4KB interactive line buffer (verified on a real machine - a 4.3KB blob truncated). tokenmaxxing's one write, the onboarding harvest, stores the **`claudeAiOauth`-only** object (small → always the ps-safe stdin write) and never rewrites a store afterwards.
|
|
103
|
+
- **settings.json is user-owned.** Install by merge; ship `tokenmaxxing doctor` to verify the supervisor + 5 entries survive a `/config` edit or update.
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
107
|
## 7. Scope
|
|
108
|
-
**v1:** `tokenmaxxing init` / `add` / `ls` (removed in 1.30.0, `status --cached` covers it) / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks;
|
|
108
|
+
**v1:** `tokenmaxxing init` / `add` / `ls` (removed in 1.30.0, `status --cached` covers it) / `status` / `doctor`; the supervisor; statusLine shim + Stop/StopFailure/SessionStart hooks; per-session placement and moves by respawn with `flock` + reset-aware picker; platform credential stores (macOS keychain / Linux 0600 files, one facade). macOS + Linux.
|
|
109
109
|
|
|
110
110
|
**v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
|
|
111
111
|
|
|
112
112
|
**Removed (1.23.0):** the programmatic SDK surface, the stdio MCP server, and the Agent Plugin that shipped from 0.11.0 through 1.22.0. The CLI, the hooks, and the supervisor are the only surfaces.
|
|
113
113
|
|
|
114
|
-
**
|
|
114
|
+
**Removed (1.37.0, issue #79):** the shared live credential and everything that served it: the swap-time harvest, park, and install, tokenmaxxing's own OAuth refresh grant and its interlock with Claude Code's refresh lock, the `~/.claude.json` `oauthAccount` rewrite, the 45-second post-swap cooldown, the pool-wide depleted-wait record, the parked-credential slots (`creds/`, `tokenmaxxing-cred-*`), the ambient `CLAUDE_CONFIG_DIR` refusal, and manual `switch` for the Claude pool.
|
|
115
115
|
|
|
116
|
-
**Shipped since (
|
|
116
|
+
**Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, `codex-creds/`, own flock). Since 1.35.0 the engine is one: a generic account record (`id`, `label`, `email`, `tier`, `windows`), one picker, one decision, and one set of account commands run against a `Provider` (`src/lib/provider.ts`); `src/lib/claude.ts` and `src/lib/codex.ts` hold only what differs per product (credential store, identity, usage source, presence, login and install flows), and the provider's `seats` capability (`shared` for Claude, `live` for Codex) is how the engine knows whether present accounts remain candidates and whether a move needs the caller's respawn. Codex differences that shaped it: restart IS the switch (a running codex refuses another account's credential), one live `auth.json` for every codex session, usage is a free direct GET with epoch resets and a duration-classified window set (the weekly window is primary on current plans), the refresh token rotates with reuse punished (harvest-by-true-owner, persist every rotation), and codex's Stop-hook system drives the auto-swap through a codex supervisor shim that respawns `codex resume <session-id>` (hooks must be trusted once via `/hooks`). Sibling sessions left on ANY non-live account are reconciled cross-session (owner decisions 2026-07-20): the deciding actor drops a reconcile marker addressed to the sibling's supervisor, and the sibling's own Stop hook promotes it into a respawn onto the live account at its next turn boundary - the only safe respawn point - while a blocked live seat never receives a signal.
|
|
117
117
|
|
|
118
|
-
**
|
|
118
|
+
**Shipped since (1.29.0):** a Cursor Cloud relay. A cloud VM has no keychain, no pool state, and no supervisor, so the path uses what it does have (environment variables, a shell, repo files) and a different credential primitive, one-year inference-only setup tokens, minted once per pooled account by `tokenmaxxing setup-token` on the owner's machine and carried to the VM as one user-scoped Runtime Secret (`TOKENMAXXING_TOKENS`). `tokenmaxxing cursor init` writes a `claude` project subagent (`.cursor/agents/claude.md`) that forwards each task to `tokenmaxxing cloud run`, which spawns `claude -p --dangerously-skip-permissions --output-format json` under `CLAUDE_CODE_OAUTH_TOKEN` and pins the thread to that token (the API caches by exact prefix, so a rotation busts one thread, not all). A setup token carries no usage signal, so rotation is reactive. A run whose transcript gained a non-transient rate-limit row during that run (the structured row the StopFailure hook reads) walls the label until the reset the row names (five hours without one) and resumes the same session on the next token; the result's `api_error_status` alone never walls, so a transient 429 or an older row from a previous token does not cascade across the pool. The local switching engine never reads a setup token; `/usage` prints no percentages under one. Details in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
|
|
119
|
+
|
|
120
|
+
**Non-goals:** an API/MITM proxy; reimplementing OAuth beyond the read-only identity check at onboarding; Slack as a message bus; soft concurrency caps.
|
|
119
121
|
|
|
120
122
|
---
|
|
121
123
|
|
|
@@ -124,16 +126,22 @@ TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/u
|
|
|
124
126
|
|
|
125
127
|
---
|
|
126
128
|
|
|
127
|
-
## 9. What the acceptance
|
|
129
|
+
## 9. What the acceptance gates showed
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
### 2026-09-12 (per-account stores, 1.37.0)
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
2. **Terminal restoration - ✅ mechanism proven.** The supervisor saves `stty -g` and restores it between kill and respawn; the path executes in the mock-supervisor run. Visual raw-mode/resize confirmation wants a live interactive terminal.
|
|
133
|
-
3. **SessionStart swap - ✅ swap logic proven; ⚠️ "adopted by first turn" needs a 2nd real account.** The hook's decision+swap path is covered by the E2E; the claude-internal timing (SessionStart before first `getToken`) needs a second subscription to observe end-to-end.
|
|
134
|
-
4. **Keychain writes from a headless hook - ✅ proven.** The swap E2E's 4 concurrent subprocesses each wrote the live item via `security -i` (secret on stdin, never argv) with no prompt/hang; `init` parked backups headlessly too. (Residual: the live `Claude Code-credentials` ACL for the *fresh resumed claude* is mitigated by onboarding touching `security` interactively.)
|
|
135
|
-
5. **Concurrent flocked swap - ✅ proven.** 4 concurrent processes racing the trigger → exactly **one** `flock`ed swap, one OAuth refresh, all end on B. Per-supervisor respawn is proven with the mock; N *real* supervisors respawning together needs real accounts + PTYs.
|
|
136
|
-
6. **Model-aware trigger - ✅ proven.** Fable at 96% per-model cap (aggregate only 30/50) → swaps; Sonnet at 96% → no swap.
|
|
133
|
+
Hermetic run on Linux under a throwaway `HOME` and `TOKENMAXXING_HOME`, a stub `claude` that records its environment and answers `-p /usage` from a per-store fixture, and a synthetic three-account version 2 pool with equal headroom on two accounts and the third over its session bar:
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
1. **Placement - ✅.** The first supervised launch spawned the stub with `CLAUDE_SECURESTORAGE_CONFIG_DIR=stores/<first account>` and wrote its presence; the second launch, with equal headroom, landed on the other account.
|
|
136
|
+
2. **Plain move - ✅.** A 95% session tee for the second session's account made its Stop hook write the marker; the supervisor relaunched `--resume <sid>` under the first account's store within a second and rewrote the presence file.
|
|
137
|
+
3. **Wall move - ✅.** A five-hour refusal row fed to the first session's StopFailure hook stamped `enforcedUntil` on its seat and moved the session to the account that had just been freed; a Stop hook on the walled seat moved the remaining session off it too.
|
|
138
|
+
4. **Sampling - ✅.** The check tick probed the stalest account under its store with the `-tick` probe home; live `status` probed the accounts without a fresh tee under their stores and read the one with a fresh tee from the tee.
|
|
139
|
+
5. **Stores never move - ✅.** SHA-256 of every store's `.credentials.json` was identical before and after; no `creds/` directory and no root `usage.json` appeared; the SessionStart hook on a seat under its bars wrote no marker; stopping the children cleared every presence file.
|
|
140
|
+
|
|
141
|
+
The `pooledSpawnEnv` helper the issue names left with the SDK surface in 1.23.0; the supervisor's launch environment is the one spawn-env site.
|
|
142
|
+
|
|
143
|
+
### 2026-07-09 (v0.1.0)
|
|
144
|
+
|
|
145
|
+
Unit suite: **32 pass**. Hermetic swap+concurrency+model-aware E2E: **all pass**. CLI init/doctor/uninstall: **pass** (re-verified init/doctor 2026-07-09 through the npm-installed bun shim on linux-arm64 after the switch to source packaging; full suite 47 pass / 0 fail there). Transcript continuity across a process boundary was proven on a real account (`claude --session-id X -p ...` then `claude --resume X -p ...` recalled the earlier turn), the kill→restore-termios→respawn→`--resume` loop with a mock claude, headless keychain writes through `security -i`, and a concurrent flocked decision with 4 racing processes. The test suite itself was deleted on 2026-09-02 (owner ruling); verification since is the typecheck plus hermetic CLI runs.
|
|
146
|
+
|
|
147
|
+
Owed before fully trusting in the wild: the live interactive-PTY SIGTERM/terminal test (the owner declined it once), and a live run with two real subscription accounts observing placement and a move end to end.
|
package/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# tokenmaxxing
|
|
2
2
|
|
|
3
|
-
**Automatic Claude Code account switching.** Run `claude` exactly as you always do;
|
|
3
|
+
**Automatic Claude Code account switching.** Run `claude` exactly as you always do; each session starts on the pooled account with the most headroom per running session, and when that account nears its usage limit, tokenmaxxing resumes the session on a fresher account at a safe turn boundary - same conversation, restarted under another account's credential store. Works across many concurrent sessions, which spread over the pool instead of draining one account together. When the whole pool is at its limit, a session pauses with a countdown and auto-resumes at the soonest reset, when that reset falls within `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
|
|
4
4
|
|
|
5
5
|
> **Scope:** Claude Code only, macOS and Linux. It pools **subscription** accounts (Pro/Max), not API keys.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
$ claude
|
|
9
|
-
...you work normally;
|
|
9
|
+
...you work normally; the statusline's ◆ marks this session's account...
|
|
10
|
+
↻ tokenmaxxing: moving to work@acme.com - resuming...
|
|
10
11
|
⏳ tokenmaxxing: all accounts at their limit. Resuming on work@acme.com when it resets (Ctrl-C to resume now).
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
## Why
|
|
14
15
|
|
|
15
|
-
A
|
|
16
|
+
Every pooled account owns one Claude Code credential store under `~/.config/tokenmaxxing/stores/`. A thin `claude` supervisor on your PATH picks an account for each session and points `CLAUDE_SECURESTORAGE_CONFIG_DIR` at its store, so sessions on different accounts never share a credential, while sessions on the same account share its store and Claude Code's own refresh keeps them in step. A session moves only by respawn: the supervisor stops it at a committed turn boundary (the transcript is already on disk, nothing is lost) and runs `claude --resume <id>` under another store. tokenmaxxing writes a store once at onboarding and never again; it never copies a credential between stores, because a refresh rotation revokes the previous access token and two copies of one grant break each other. Everything else about `claude` is unchanged - all flags, MCP, hooks, and skills pass through.
|
|
16
17
|
|
|
17
18
|
## Install
|
|
18
19
|
|
|
@@ -23,7 +24,7 @@ bun add -g tokenmaxxing
|
|
|
23
24
|
tokenmaxxing init
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
Or with Nix (same source-run-by-Bun package; `init` still owns
|
|
27
|
+
Or with Nix (same source-run-by-Bun package; `init` still owns the stores, the `claude` shim, and settings merges). Install onto PATH first, then init: `nix run ... -- init` alone leaves supervisor shims without a stable `tokenmaxxing` on PATH after the ephemeral run exits.
|
|
27
28
|
|
|
28
29
|
```sh
|
|
29
30
|
nix profile install github:anaclumos/tokenmaxxing
|
|
@@ -50,29 +51,28 @@ programs.tokenmaxxing.package = inputs.tokenmaxxing.packages.${pkgs.system}.defa
|
|
|
50
51
|
|
|
51
52
|
With either module, run `tokenmaxxing init` afterwards.
|
|
52
53
|
|
|
53
|
-
`init`
|
|
54
|
+
`init` opens a browser sign-in for your first pooled account (even if you are already signed in; the login you already have stays for sessions started outside the supervisor), installs the `claude` supervisor + five `settings.json` entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a StopFailure hook, a SessionStart hook), and adds the supervisor's bin dir to PATH in your shell rc (idempotent; it must sit ahead of the real `claude` to intercept it). Restart your shell, then add more accounts and go:
|
|
54
55
|
|
|
55
56
|
```sh
|
|
56
|
-
tokenmaxxing add # logs one in, in isolation,
|
|
57
|
-
claude # use claude as always
|
|
57
|
+
tokenmaxxing add # logs one in, in isolation, into its own store
|
|
58
|
+
claude # use claude as always; each session gets its own account
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
## Commands
|
|
61
62
|
|
|
62
63
|
| command | what it does |
|
|
63
64
|
|---|---|
|
|
64
|
-
| `tokenmaxxing init` |
|
|
65
|
+
| `tokenmaxxing init` | log the first account in (isolated) + install supervisor & hooks |
|
|
65
66
|
| `tokenmaxxing init --codex` | same for codex: import login, install codex supervisor + Stop hook |
|
|
66
|
-
| `tokenmaxxing add` | register an additional account (isolated login, harvested into
|
|
67
|
+
| `tokenmaxxing add` | register an additional account (isolated login, harvested once into its own store) |
|
|
67
68
|
| `tokenmaxxing add --codex` | register an additional codex account (isolated login) |
|
|
68
69
|
| `tokenmaxxing auth [--codex] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every needs-reauth account one by one; `--codex` does the same for the codex pool |
|
|
69
|
-
| `tokenmaxxing switch [sel]` | switch the
|
|
70
|
-
| `tokenmaxxing
|
|
71
|
-
| `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, active + exhausted-until-reset; `--cached` renders the stored figures without sampling |
|
|
70
|
+
| `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start); the claude pool has no manual switch, each session is placed at launch |
|
|
71
|
+
| `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
|
|
72
72
|
| `tokenmaxxing config` | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
|
|
73
|
-
| `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
|
|
73
|
+
| `tokenmaxxing doctor` | verify the supervisor + settings entries survived and every store holds its account's credential |
|
|
74
74
|
| `tokenmaxxing rename [--codex] <sel> <label>` / `rm [--codex] <sel>` | manage the pool (`--codex` targets the codex pool: one email can hold both a claude and a codex account) |
|
|
75
|
-
| `tokenmaxxing uninstall` | remove supervisor + settings entries (accounts/
|
|
75
|
+
| `tokenmaxxing uninstall` | remove supervisor + settings entries (accounts/stores kept) |
|
|
76
76
|
| `tokenmaxxing setup-token [--print \| rm <label\|uuid>]` | Cursor Cloud only: mint one `claude setup-token` per pooled account (a browser sign-in each) and print the `TOKENMAXXING_TOKENS` secret value; `--print` prints the stored set, `rm` drops one |
|
|
77
77
|
| `tokenmaxxing cursor init [dir]` | write the Claude relay subagent and `.cursor/environment.json` into a repo |
|
|
78
78
|
| `tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"` | on a Cursor Cloud VM: run `claude -p` on a setup token, rotate to the next token on a usage limit |
|
|
@@ -80,16 +80,16 @@ claude # use claude as always
|
|
|
80
80
|
|
|
81
81
|
## How switching decides
|
|
82
82
|
|
|
83
|
-
One rule: while the
|
|
83
|
+
Each session runs on its own seat. One rule: while the seat is under its bars, nothing happens; once it is at or over a bar, the session moves to the usable account that ranks first. The session bar is **90%** of the 5-hour window and the weekly bar is **98%**. The bars also screen candidates on any of:
|
|
84
84
|
|
|
85
|
-
- **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine.
|
|
86
|
-
- **Per-model weekly cap** - the most capable model (Fable) has its own tighter weekly limit that binds *before* the aggregate (per-model caps currently exist only for Sonnet and Fable, and Sonnet's is generous). tokenmaxxing reads it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) whenever the active model is one of `policy.switchModels`, so a Fable session
|
|
85
|
+
- **Session** (5-hour) or **week (all models)** - the aggregate windows, fed free/push-based by the statusLine into one tee per account.
|
|
86
|
+
- **Per-model weekly cap** - the most capable model (Fable) has its own tighter weekly limit that binds *before* the aggregate (per-model caps currently exist only for Sonnet and Fable, and Sonnet's is generous). tokenmaxxing reads it from `claude -p '/usage'` under the seat's store (free, 0 tokens, TTL-cached) whenever the active model is one of `policy.switchModels`, so a Fable session moves on the Fable cap while a Sonnet session rides the aggregate.
|
|
87
87
|
|
|
88
|
-
The bars' headroom is deliberate: it's the budget to reach a clean turn boundary
|
|
88
|
+
The bars' headroom is deliberate: it's the budget to reach a clean turn boundary before the account's real limit. The session bar sits lower (90) because a 5-hour reset is cheap to sit out; weekly quota is use-it-or-lose-it, so it drains closer to the limit (98).
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
Launches and moves rank usable accounts by session-window headroom per running session, `(bar - used) / (sessions + 1)`, so two fresh accounts alternate and a third session goes to whichever has more headroom left per session; ties break by weekly pace pressure (remaining weekly percent divided by time to reset, highest first). Organization membership is not an input.
|
|
91
91
|
|
|
92
|
-
- Every periodic check tick samples the
|
|
92
|
+
- Every periodic check tick folds fresh statusline tees into the index and samples the account whose last `/usage` attempt is oldest, under that account's store, skipping any attempted within `policy.usagePollTtlMs`; a move reads the cached figures, which are as fresh as each account's last successful attempt or its sessions' latest push.
|
|
93
93
|
- When no account is usable, the session pauses until the soonest reset if that lands within `policy.maxWaitMs`, and otherwise stays put.
|
|
94
94
|
|
|
95
95
|
See [How switching decides](docs/content/docs/switching.mdx) for the policy and the [cache-cost profile](docs/content/docs/switching-profile.mdx) for measurements and their limits.
|
|
@@ -110,9 +110,9 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `switchModels` names the models whose per-model cap triggers a
|
|
113
|
+
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is how long a `/usage` attempt stays fresh, for a seat's per-model poll and for the account each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a soonest reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick.
|
|
114
114
|
|
|
115
|
-
State lives entirely in `~/.config/tokenmaxxing/`.
|
|
115
|
+
State lives entirely in `~/.config/tokenmaxxing/`. Each account's credential store is `stores/<uuid8>/`, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 `.credentials.json` inside the store (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
116
116
|
|
|
117
117
|
## Codex support
|
|
118
118
|
|
|
@@ -124,26 +124,26 @@ tokenmaxxing add --codex # log in another account, isolated - your primary lo
|
|
|
124
124
|
codex # use codex as always
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so **a restart is the switch
|
|
127
|
+
Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, and codex has no per-session credential store, so **a restart is the switch** and every codex session shares one live login. The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status` (and `status --cached`) shows both pools.
|
|
128
128
|
|
|
129
129
|
Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after `init --codex` you must open codex once and trust the tokenmaxxing Stop hook via `/hooks` (auto-switching is inert until then); and codex has no cross-process lock on `auth.json`, so tokenmaxxing serializes all of its own credential writes behind its own lock and swaps only at idle turn boundaries.
|
|
130
130
|
|
|
131
131
|
## Cursor Cloud
|
|
132
132
|
|
|
133
|
-
A Cursor Cloud Agent can hand substantial work to Claude Code running on your own accounts. The VM has no keychain and no
|
|
133
|
+
A Cursor Cloud Agent can hand substantial work to Claude Code running on your own accounts. The VM has no keychain and no supervisor, so this path uses setup tokens instead. `tokenmaxxing setup-token` mints one `claude setup-token` per pooled account on your machine and prints the value for a user-scoped Runtime Secret named `TOKENMAXXING_TOKENS`; `tokenmaxxing cursor init` writes a `claude` project subagent and an `environment.json` install line into the repo; on the VM, the subagent forwards each task to `tokenmaxxing cloud run`, which runs `claude -p` on one token per thread and moves the thread to the next token when a run ends on a usage limit. Setup tokens are inference-only and report no usage percentages, so cloud rotation is reactive and the local switching engine never reads one. Details and limits in [Cursor Cloud](docs/content/docs/cursor-cloud.mdx).
|
|
134
134
|
|
|
135
135
|
## Honest limitations
|
|
136
136
|
|
|
137
|
-
- **
|
|
138
|
-
- **Depleted-pause hiccup.**
|
|
139
|
-
- **
|
|
140
|
-
- **
|
|
137
|
+
- **A move restarts the process.** The session resumes the same transcript under the new account, but the process restarts and the first turn on the new account re-uploads context once (prompt cache is org-scoped).
|
|
138
|
+
- **Depleted-pause hiccup.** When the whole pool is at its limit, `claude` stops for the countdown; anything typed in that split second is lost.
|
|
139
|
+
- **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on Claude Code's own login, which tokenmaxxing never writes.
|
|
140
|
+
- **One shared identity file.** `~/.claude.json` holds one `oauthAccount` that Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email; the statusline's `◆` marks the account a session actually uses.
|
|
141
141
|
- **Keychain ACL (macOS).** `init`/`add` touch the keychain interactively so the first `security` access isn't cold inside a headless hook.
|
|
142
|
-
- **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file;
|
|
142
|
+
- **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
|
|
143
143
|
|
|
144
144
|
## How it's built
|
|
145
145
|
|
|
146
|
-
TypeScript on Bun. One multi-call entry (`src/main.ts`) runs the CLI, the `claude` supervisor, and the hook/statusLine shims. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin,
|
|
146
|
+
TypeScript on Bun. One multi-call entry (`src/main.ts`) runs the CLI, the `claude` supervisor, and the hook/statusLine shims. [Zod](https://zod.dev) validates every external-boundary payload (credential blobs, hook/statusLine stdin, identity responses, config). [es-toolkit](https://es-toolkit.dev) for utilities. The supervisor is process/terminal-only. It never proxies API traffic or touches tokens in flight. Cross-process coordination uses `flock(2)` via `bun:ffi` (macOS has no `flock(1)`; one codepath for both platforms). Credential I/O goes through one platform-selected store: `security(1)` generic-passwords on macOS, atomic 0600 file writes on Linux.
|
|
147
147
|
|
|
148
148
|
## License
|
|
149
149
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Automatic Claude Code account switching: pool multiple accounts
|
|
3
|
+
"version": "1.40.0",
|
|
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",
|
|
7
7
|
"author": "anaclumos",
|
package/src/cli/check.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { claude } from "../lib/claude.ts";
|
|
2
2
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { sampleOldest } from "../lib/sample.ts";
|
|
4
4
|
import { loadConfig } from "../lib/state.ts";
|
|
5
|
+
import { maybeAutoUpdate } from "../lib/update.ts";
|
|
5
6
|
import { log } from "../lib/log.ts";
|
|
6
7
|
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
7
8
|
|
|
@@ -10,7 +11,8 @@ export async function cmdCheck(json = false): Promise<number> {
|
|
|
10
11
|
let d;
|
|
11
12
|
try {
|
|
12
13
|
d = await evaluateAndMaybeSwap(claude, now);
|
|
13
|
-
await
|
|
14
|
+
await sampleOldest(loadConfig());
|
|
15
|
+
await maybeAutoUpdate();
|
|
14
16
|
} catch (e) {
|
|
15
17
|
const detail = e instanceof Error ? e.message : String(e);
|
|
16
18
|
log("check.error", { err: detail });
|