tokenmaxxing 1.129.0 → 1.131.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
@@ -30,7 +30,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
30
30
  - A `claude` **supervisor** on your PATH ahead of the real binary (`~/.config/tokenmaxxing/bin/claude`) - you invoke it identically; a `claude` shell function or alias would shadow it, and `doctor` warns about one.
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
- - `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/opencodeBin/piBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
33
+ - `config.json` - SPARSE overrides only (thresholds.session/weekly/accounts, claudeBin/codexBin/grokBin/piBin pins, policy.*); defaults apply at read time, `xx config` prints the path and the effective values, and edits happen in an editor.
34
34
  - `accounts.json` - non-secret index `{id, label, email, tier, addedAt, windows, lastUsageAt, lastProbeAt, probeFails, storeFails, usageRetryAt, 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`). Neither index names an active account: the schema is `{version, accounts}`, and an older file that still carries `activeId` loads and drops the key on the next save.
35
35
  - `stores/<uuid8>/` - one Claude Code credential store per pooled account (see below).
36
36
  - `codex-stores/<uuid8>/` - one Codex credential store per pooled account: its `auth.json`, with everything else symlinked to the shared `~/.codex` so sessions, history, and config stay shared and `codex resume` works across stores.
@@ -40,7 +40,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
40
40
  - `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most eight sessions wait on one account; a session that arrives when every reset within `policy.maxWaitMs` is full waits for the next reset with room up to twice `policy.maxWaitMs` out, joining the least-crowded reset when the horizon is full.
41
41
  - `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
42
42
  - `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
43
- - `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` and `opencode-go-*` state of the status-only pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
43
+ - `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` state of the status-only grok pool: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
44
44
  - Per-account **credential stores** follow the platform's Claude Code store: macOS = a login-keychain item keyed by the store path (never plaintext on disk); Linux = a 0600 `.credentials.json` inside the 0700 store directory (the same plaintext model claude itself uses - its Linux build has no keyring path at all, binary-verified 2.1.205). One `credstore` facade dispatches on a `{kind: keychain|file}` target; call sites never branch on platform. tokenmaxxing writes a store once, at onboarding; Claude Code's own refresh is the only writer afterwards.
45
45
 
46
46
  - A periodic check job (`com.tokenmaxxing.check` launchd agent on macOS, `tokenmaxxing-check.timer` systemd user timer on Linux) running `tokenmaxxing check` once per tick (`policy.checkIntervalMs`, 60s by default): it runs the decision with no seat (nothing moves), folds fresh tees into the index, samples up to three accounts whose last sample attempt is oldest (the direct no-spend usage GET), deletes leftover atomic-write temp files under the state root, and on a Bun global install checks npm for a newer version once a day, so the cached figures every placement reads stay fresh.
@@ -118,7 +118,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
118
118
 
119
119
  **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.
120
120
 
121
- **Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, per-account `codex-stores/`, 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 stay 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 store per pooled account with `CODEX_HOME` set per session (everything but `auth.json` symlinked to the shared home, so resume works across stores), 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 (persisted into the account's own store the instant it returns, never refreshed for an account running in another session), and the codex Stop hook drives the auto-switch through a codex supervisor shim that respawns `codex resume <session-id>` under the target's store (hooks must be trusted once via `/hooks`). Since 1.44.0 two status-only pools, grok and opencode-go, share the engine's account commands and state layout with `statusOnly: true` and no supervisor or hooks ([Status-only pools](docs/content/docs/status-only-pools.mdx)). grok `status` reads the weekly subscription credit window from the credits billing GET; opencode-go has no usage source.
121
+ **Shipped since (0.13.0):** Codex as a second pool with its own state (`codex-accounts.json`, per-account `codex-stores/`, 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 stay 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 store per pooled account with `CODEX_HOME` set per session (everything but `auth.json` symlinked to the shared home, so resume works across stores), 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 (persisted into the account's own store the instant it returns, never refreshed for an account running in another session), and the codex Stop hook drives the auto-switch through a codex supervisor shim that respawns `codex resume <session-id>` under the target's store (hooks must be trusted once via `/hooks`). The status-only grok pool shares the engine's account commands and state layout with `statusOnly: true` and no supervisor or hooks ([Status-only pools](docs/content/docs/status-only-pools.mdx)). grok `status` reads the weekly subscription credit window from the credits billing GET.
122
122
 
123
123
  **Removed (issue #173):** the Cursor Cloud relay and its setup tokens: `tokenmaxxing setup-token`, `tokenmaxxing cursor init`, `tokenmaxxing cloud run`, `setup-tokens.json`, `TOKENMAXXING_TOKENS`, and the `cloud/` state.
124
124
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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, compacted on the account it leaves and restarted under another account's credential store with a first prompt that asks Claude to continue. 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 queues first come first served for a reset (at most eight sessions wait on one account; overflow waits for the next reset with room) and pauses with a countdown until its assigned reset, when that reset falls within twice `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
4
4
 
5
- > **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI as a second switching pool; grok and opencode-go as status-only pools. It pools **subscription** accounts (Pro/Max), not API keys; the opencode-go pool, which holds Zen API keys, is the one exception.
5
+ > **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI as a second switching pool; grok as a status-only pool. It pools **subscription** accounts (Pro/Max), not API keys.
6
6
 
7
7
  ```
8
8
  $ claude
@@ -66,15 +66,15 @@ claude # use claude as always; each session gets its own accoun
66
66
  | `tokenmaxxing init` | verify and pin the real `claude`, log the first account in (isolated) + install supervisor, hooks, PATH line, check timer, and hub service |
67
67
  | `tokenmaxxing init --codex` | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
68
68
  | `tokenmaxxing init --pi` | verify and pin the real `pi` and install the `pi` supervisor, which runs pi sessions on pooled Claude and ChatGPT accounts that have a pi login |
69
- | `tokenmaxxing init --grok` / `init --opencode-go` | pool the first grok login or opencode-go key, status-only: no supervisor, no switching |
70
- | `tokenmaxxing add [--codex \| --grok \| --opencode-go]` | register an additional account in that pool (isolated login, harvested once into its own store) |
71
- | `tokenmaxxing auth [--codex \| --grok \| --opencode-go] [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 account that is flagged or has no usable credential in its store, one by one |
69
+ | `tokenmaxxing init --grok` | pool the first grok login, status-only: no supervisor, no switching |
70
+ | `tokenmaxxing add [--codex \| --grok]` | register an additional account in that pool (isolated login, harvested once into its own store) |
71
+ | `tokenmaxxing auth [--codex \| --grok] [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 account that is flagged or has no usable credential in its store, one by one |
72
72
  | `tokenmaxxing auth --pi [--codex] [sel \| --all]` | log a pooled Claude account (or a codex account with `--codex`) into pi, isolated, and keep that login as the account's pi credential after an identity check; `--all` walks every account without a pi login |
73
73
  | `tokenmaxxing status [--cached]` | every pool: accounts with 5h / weekly / per-model usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
74
74
  | `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 |
75
75
  | `tokenmaxxing check` | fold fresh tees, sample up to three stale accounts, and self-update a Bun global install once a day; the periodic timer runs this every tick |
76
76
  | `tokenmaxxing doctor` | verify the install: PATH order, `claudeBin`, the five settings entries, the timer, the hub service, the credential identity of every Codex store and of every Claude store whose access token is still fresh (an expiring Claude token is reported as unverifiable, not as a failure), codex hook trust, and shell aliases that shadow `claude` |
77
- | `tokenmaxxing rename [--codex \| --grok \| --opencode-go] <sel> <label>` / `rm [...] <sel>` | manage a pool (one email can hold both a claude and a codex account); `rm` is refused for an account with a running supervised session |
77
+ | `tokenmaxxing rename [--codex \| --grok] <sel> <label>` / `rm [...] <sel>` | manage a pool (one email can hold both a claude and a codex account); `rm` is refused for an account with a running supervised session |
78
78
  | `tokenmaxxing serve` | serve the CLIProxyAPI-compatible usage API on `http://localhost:8317` (`hub.port`) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows the quota of every pooled Claude and Codex account; the management key is the contents of `hub-key` in the state directory, minted on the first run, and `init` runs it as a background service beside the check timer. Details in [Usage hub](docs/content/docs/hub.mdx) |
79
79
  | `tokenmaxxing uninstall [--yes]` | print the targets, then remove the shims, settings entries, codex Stop hook, check timer, hub service, and rc PATH line (accounts/stores kept); refused without `--yes` when `HOME` is the login home |
80
80
  | `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, and `check` (`ok` mirrors the exit code, failures add `error`) |
@@ -112,7 +112,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
112
112
  }
113
113
  ```
114
114
 
115
- `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 the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left, and a session on it past its bars moves to such an account; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a 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. `claudeBin`, `codexBin`, `grokBin`, `opencodeBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, `TOKENMAXXING_OPENCODE_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
115
+ `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 the session bar only (default 0), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `thresholds.accounts` gives one account its own pair, keyed by label (`{"shared-seat": {"session": 75, "weekly": 75}}`), and `"credits": true` in an entry keeps an account with purchased usage credits usable past its windows, ranked after every account with plan headroom left, and a session on it past its bars moves to such an account; `policy.accountReleaseMs` (30 minutes for the session window, 5 hours for a weekly window by default) lifts each of its windows to the higher of its own and the global bar that close to the window's reset; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is the shortest sample interval, the one an account near a bar gets, for a seat's own sample, for `status`, and for the accounts each check tick samples; `maxWaitMs` bounds the depleted-pool countdown - a 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. `claudeBin`, `codexBin`, `grokBin`, and `piBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, and `TOKENMAXXING_PI_BIN` override them for one process, except that `init` pins the binary it resolved into `config.json`.
116
116
 
117
117
  State lives in `~/.config/tokenmaxxing/`; outside it the install touches Claude Code's `settings.json`, codex's `hooks.json`, the timer and usage hub units, and your shell rc. 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`).
118
118
 
@@ -151,7 +151,7 @@ pi keeps its own credential and refreshes it itself, so each pooled account gets
151
151
  - **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.
152
152
  - **Keychain (macOS).** The first keychain access for a new store happens inside the interactive `init`, `add`, or `auth` run, where a prompt can be answered, not inside a headless hook.
153
153
  - **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
154
- - **Status-only pools do not switch.** grok `status` shows the weekly subscription credit window; opencode-go accounts still render with no bars. Neither pool moves.
154
+ - **The status-only grok pool does not switch.** grok `status` shows the weekly subscription credit window, and no grok session moves.
155
155
 
156
156
  ## How it's built
157
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.129.0",
3
+ "version": "1.131.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/auth.ts CHANGED
@@ -11,7 +11,7 @@ import { usageNote } from "./add.ts";
11
11
  import { c, count } from "./render.ts";
12
12
  import type { Account, AccountsIndex } from "../lib/types.ts";
13
13
 
14
- const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok | --opencode-go] [<email|label|id> | --all]\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
14
+ const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok] [<email|label|id> | --all]\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
15
15
 
16
16
  export type AuthPlan = { kind: "usage" } | { kind: "error"; message: string } | { kind: "pick" } | { kind: "targets"; ids: string[] };
17
17
 
package/src/cli/rename.ts CHANGED
@@ -16,7 +16,7 @@ export function findAccount(accounts: Account[], selector: string): Account | un
16
16
  export async function cmdRename(p: Provider, argv: string[]): Promise<number> {
17
17
  const [selector, newLabel] = argv;
18
18
  if (!selector || !newLabel) {
19
- emitError({ message: "usage: tokenmaxxing rename [--codex | --grok | --opencode-go] <email|label|id> <new-label>", paint: plain });
19
+ emitError({ message: "usage: tokenmaxxing rename [--codex | --grok] <email|label|id> <new-label>", paint: plain });
20
20
  return 2;
21
21
  }
22
22
  return withLock(p.pool.lockFile, async () => {
package/src/cli/seat.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { borrowClaudeSeat } from "../lib/claude.ts";
2
2
  import { borrowCodexSeat } from "../lib/codex.ts";
3
- import { borrowOpencodeGoSeat } from "../lib/opencodego.ts";
4
3
  import { emitError } from "./render.ts";
5
4
 
6
5
  const POOLS = {
@@ -14,11 +13,6 @@ const POOLS = {
14
13
  usage: "usage: tokenmaxxing seat --codex <pid> - print the CODEX_HOME of one pooled account, lent to <pid> until it exits and shared with codex sessions and other borrowers; codex processes that share one auth.json can lose a refresh race that signs the account out until `tokenmaxxing auth --codex` runs again",
15
14
  none: "no usable codex account (pool empty, or every account at a limit, flagged for reauthentication, held by a pi session, or held by the supervised codex session this runs in) - use the ambient codex login",
16
15
  },
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
16
  };
23
17
 
24
18
  export async function cmdSeat(pool: keyof typeof POOLS, pidRaw: string | undefined, extra: string[]): Promise<number> {
package/src/cli/status.ts CHANGED
@@ -2,10 +2,9 @@ import { chunk, sortBy } from "es-toolkit";
2
2
  import { claude } from "../lib/claude.ts";
3
3
  import { codex } from "../lib/codex.ts";
4
4
  import { grok } from "../lib/grok.ts";
5
- import { opencodeGo } from "../lib/opencodego.ts";
6
5
  import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
7
6
  import { withLock } from "../lib/lock.ts";
8
- import { codexPool, grokPool, opencodeGoPool } from "../lib/paths.ts";
7
+ import { codexPool, grokPool } from "../lib/paths.ts";
9
8
  import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
10
9
  import type { Provider, SampleReport } from "../lib/provider.ts";
11
10
  import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
@@ -39,7 +38,7 @@ type PoolReport = {
39
38
  accounts: StatusAccount[];
40
39
  };
41
40
 
42
- export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; opencodeGo: PoolReport };
41
+ export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport };
43
42
 
44
43
  function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
45
44
  const passed = w.resetsAt != null && w.resetsAt <= now;
@@ -263,10 +262,9 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
263
262
  if (!json) {
264
263
  const codexPooled = loadAccounts(codexPool).accounts.length > 0;
265
264
  const grokPooled = loadAccounts(grokPool).accounts.length > 0;
266
- const opencodeGoPooled = loadAccounts(opencodeGoPool).accounts.length > 0;
267
265
  if (claudeReport.accounts.length === 0) {
268
- if (!codexPooled && !grokPooled && !opencodeGoPooled) {
269
- console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`, `--opencode-go`)"));
266
+ if (!codexPooled && !grokPooled) {
267
+ console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`)"));
270
268
  return 0;
271
269
  }
272
270
  console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
@@ -278,9 +276,8 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
278
276
  }
279
277
  const codexReport = await collect(codex, cfg, now, cached);
280
278
  const grokReport = await collect(grok, cfg, now, cached);
281
- const opencodeGoReport = await collect(opencodeGo, cfg, now, cached);
282
279
  if (json) {
283
- const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, opencodeGo: opencodeGoReport };
280
+ const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport };
284
281
  emitJson({ ok: true, ...report });
285
282
  return 0;
286
283
  }
@@ -290,8 +287,5 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
290
287
  if (grokReport.accounts.length > 0) {
291
288
  renderPool(grok, grokReport, `grok (${count({ n: grokReport.accounts.length, noun: "account" })}, status-only)`, Date.now(), cfg.policy.usagePollTtlMs);
292
289
  }
293
- if (opencodeGoReport.accounts.length > 0) {
294
- renderPool(opencodeGo, opencodeGoReport, `opencode-go (${count({ n: opencodeGoReport.accounts.length, noun: "account" })}, status-only)`, Date.now(), cfg.policy.usagePollTtlMs);
295
- }
296
290
  return 0;
297
291
  }
@@ -5,6 +5,7 @@ import { escape } from "es-toolkit";
5
5
  import { z } from "zod";
6
6
  import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.ts";
7
7
  import { writeFileAtomic } from "./atomic.ts";
8
+ import { shellQuote } from "./proc.ts";
8
9
  import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
9
10
  import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
10
11
  import { loadConfig, readJsonFile, type BinKey } from "./state.ts";
@@ -84,7 +85,7 @@ export PATH
84
85
  if command -v tokenmaxxing >/dev/null 2>&1; then
85
86
  exec tokenmaxxing "$@"
86
87
  fi
87
- exec ${JSON.stringify(bun)} --no-env-file run ${JSON.stringify(entry)} "$@"
88
+ exec ${shellQuote(bun)} --no-env-file run ${shellQuote(entry)} "$@"
88
89
  `;
89
90
  }
90
91
 
@@ -94,15 +95,15 @@ function installEntryPoints(): string {
94
95
  if (isNixPackaged()) {
95
96
  writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
96
97
  } else {
97
- writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} --no-env-file run ${JSON.stringify(entry)} "$@"\n`, 0o755);
98
+ writeFileAtomic(target, `#!/bin/sh\nexec ${shellQuote(process.execPath)} --no-env-file run ${shellQuote(entry)} "$@"\n`, 0o755);
98
99
  }
99
- writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${JSON.stringify(target)} "$@"\n`, 0o755);
100
+ writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${shellQuote(target)} "$@"\n`, 0o755);
100
101
  return target;
101
102
  }
102
103
 
103
104
  export function installSupervisor(): InstallOutcome {
104
105
  const target = installEntryPoints();
105
- writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
106
+ writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${shellQuote(target)} __supervise "$@"\n`, 0o755);
106
107
 
107
108
  installSettings();
108
109
  const checkIntervalS = Math.ceil(loadConfig().policy.checkIntervalMs / 1000);
@@ -127,7 +128,7 @@ const CodexHooksFileSchema = z.looseObject({
127
128
  const CODEX_STOP_HOOK_SUBCOMMAND = "__codex-stop-hook";
128
129
 
129
130
  function codexStopHookCommand(): string {
130
- return `${JSON.stringify(installedBin())} ${CODEX_STOP_HOOK_SUBCOMMAND}`;
131
+ return `${shellQuote(installedBin())} ${CODEX_STOP_HOOK_SUBCOMMAND}`;
131
132
  }
132
133
 
133
134
  function withoutOurCodexStopHooks(groups: { hooks: { type?: string; command?: string }[] }[]): typeof groups {
@@ -200,7 +201,7 @@ export function codexSupervisorLink(): string {
200
201
  }
201
202
 
202
203
  export function installCodexSupervisor(): void {
203
- writeFileAtomic(codexSupervisorLink(), `#!/bin/sh\nexec ${JSON.stringify(installEntryPoints())} __supervise-codex "$@"\n`, 0o755);
204
+ writeFileAtomic(codexSupervisorLink(), `#!/bin/sh\nexec ${shellQuote(installEntryPoints())} __supervise-codex "$@"\n`, 0o755);
204
205
  installCodexStopHook();
205
206
  }
206
207
 
@@ -214,7 +215,7 @@ export function piSupervisorLink(): string {
214
215
  }
215
216
 
216
217
  export function installPiSupervisor(): void {
217
- writeFileAtomic(piSupervisorLink(), `#!/bin/sh\nexec ${JSON.stringify(installEntryPoints())} __supervise-pi "$@"\n`, 0o755);
218
+ writeFileAtomic(piSupervisorLink(), `#!/bin/sh\nexec ${shellQuote(installEntryPoints())} __supervise-pi "$@"\n`, 0o755);
218
219
  }
219
220
 
220
221
  export type UnitJob = {
package/src/lib/paths.ts CHANGED
@@ -55,11 +55,6 @@ export const grokPool = {
55
55
  lockFile: join(TM_HOME, "grok-lock"),
56
56
  } as const;
57
57
 
58
- export const opencodeGoPool = {
59
- accountsJson: join(TM_HOME, "opencode-go-accounts.json"),
60
- lockFile: join(TM_HOME, "opencode-go-lock"),
61
- } as const;
62
-
63
58
  export type PoolPaths = { accountsJson: string; lockFile: string };
64
59
 
65
60
  const CODEX_HOME = env("TOKENMAXXING_CODEX_HOME", env("CODEX_HOME", join(HOME, ".codex")));
@@ -130,20 +125,6 @@ export function grokSeatFromEnv(accountIds: string[], env: Record<string, string
130
125
  return accountIds.find((id) => grokStoreDirFor(id) === home) ?? null;
131
126
  }
132
127
 
133
- export const opencodeGoPaths = {
134
- storesDir: join(TM_HOME, "opencode-go-stores"),
135
- onboardDir: join(TM_HOME, "opencode-go-onboard"),
136
- presenceDir: join(TM_HOME, "opencode-go-live"),
137
- } as const;
138
-
139
- export function opencodeGoStoreDirFor(accountId: string): string {
140
- return join(opencodeGoPaths.storesDir, shortId(accountId));
141
- }
142
-
143
- export function opencodeGoAuthJsonFor(accountId: string): string {
144
- return join(opencodeGoStoreDirFor(accountId), "auth.json");
145
- }
146
-
147
128
  const CLAUDE_KEYCHAIN_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/;
148
129
 
149
130
  function claudeKeychainAccount(): string {
package/src/lib/proc.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { basename } from "node:path";
2
2
  import { ErrnoSchema } from "./types.ts";
3
3
 
4
+ export function shellQuote(s: string): string {
5
+ return `'${s.replaceAll("'", "'\\''")}'`;
6
+ }
7
+
4
8
  const SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "fish"]);
5
9
  const MAX_SHELL_HOPS = 4;
6
10
 
@@ -20,7 +20,7 @@ export class StoreUnusableError extends Error {
20
20
  }
21
21
 
22
22
  export type Provider = {
23
- name: "claude" | "codex" | "grok" | "opencode-go";
23
+ name: "claude" | "codex" | "grok";
24
24
  flag: string;
25
25
  pool: PoolPaths;
26
26
  seats: "shared" | "live";
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, realpathSync, rmSync, statSync, type Dirent }
2
2
  import { basename, join, sep } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { errorMessage, log } from "./log.ts";
5
- import { codexPaths, grokPaths, opencodeGoPaths, paths, piPaths } from "./paths.ts";
5
+ import { codexPaths, grokPaths, paths, piPaths } from "./paths.ts";
6
6
  import { writeFileAtomic } from "./atomic.ts";
7
7
  import { presencePid } from "./presence.ts";
8
8
  import { readJsonFile } from "./state.ts";
@@ -45,7 +45,7 @@ function recordLiveSession(sid: string, current: string): void {
45
45
  const TMP_MARKER = ".tmp.";
46
46
  const TMP_GRACE_MS = 3600 * 1000;
47
47
 
48
- const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, opencodeGoPaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex")];
48
+ const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex")];
49
49
 
50
50
  function listDir(dir: string, root: string): Dirent[] {
51
51
  if (!existsSync(dir)) return [];
@@ -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, opencodeGoPaths.presenceDir];
63
+ const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
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));
@@ -4,6 +4,7 @@ import { z } from "zod";
4
4
  import { paths } from "./paths.ts";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
6
  import { readJsonFile } from "./state.ts";
7
+ import { shellQuote } from "./proc.ts";
7
8
  import { JsonTextSchema } from "./types.ts";
8
9
  import { ENFORCED_ERRORS } from "./usage.ts";
9
10
 
@@ -55,7 +56,7 @@ function isOurCommand(cmd: string | undefined): boolean {
55
56
  }
56
57
 
57
58
  function ourCommand(sub: string): string {
58
- return `${JSON.stringify(installedBin())} ${sub}`;
59
+ return `${shellQuote(installedBin())} ${sub}`;
59
60
  }
60
61
 
61
62
  function ourHookGroup(sub: string, matcher?: string): HookGroup {
@@ -73,9 +74,13 @@ function appendHook(s: Settings, event: string, sub: string, matcher?: string):
73
74
  export function isOurHookCommand(cmd: string, sub: string): boolean {
74
75
  if (!cmd.endsWith(` ${sub}`)) return false;
75
76
  const quotedPath = cmd.slice(0, cmd.length - (sub.length + 1));
76
- if (!quotedPath.startsWith('"') || !quotedPath.endsWith('"')) return false;
77
- const parsed = z.string().safeParse(JsonTextSchema.safeParse(quotedPath).data);
78
- return parsed.success && parsed.data.endsWith("/tokenmaxxing");
77
+ const unquoted = quotedPath.slice(1, -1).replaceAll("'\\''", "'");
78
+ const path = quotedPath.startsWith("'")
79
+ ? shellQuote(unquoted) === quotedPath
80
+ ? unquoted
81
+ : undefined
82
+ : z.string().safeParse(JsonTextSchema.safeParse(quotedPath).data).data;
83
+ return path?.endsWith("/tokenmaxxing") === true;
79
84
  }
80
85
 
81
86
  function removeHook(s: Settings, event: string, sub: string): void {
package/src/lib/state.ts CHANGED
@@ -34,12 +34,11 @@ export function loadConfig(): Config {
34
34
  cfg.claudeBin = optionalEnv("TOKENMAXXING_CLAUDE_BIN") ?? cfg.claudeBin;
35
35
  cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
36
36
  cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
37
- cfg.opencodeBin = optionalEnv("TOKENMAXXING_OPENCODE_BIN") ?? cfg.opencodeBin;
38
37
  cfg.piBin = optionalEnv("TOKENMAXXING_PI_BIN") ?? cfg.piBin;
39
38
  return cfg;
40
39
  }
41
40
 
42
- export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin" | "piBin";
41
+ export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "piBin";
43
42
 
44
43
  export function pinBinOverride(input: { key: BinKey; bin: string }): void {
45
44
  const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
@@ -14,7 +14,7 @@ export type StatusOnlySpec = {
14
14
  flag: string;
15
15
  pool: PoolPaths;
16
16
  binName: string;
17
- binKey: "grokBin" | "opencodeBin";
17
+ binKey: "grokBin";
18
18
  storeDirFor: (accountId: string) => string;
19
19
  authJsonFor: (accountId: string) => string;
20
20
  onboardDir: string;
package/src/lib/types.ts CHANGED
@@ -116,7 +116,6 @@ export const ConfigSchema = z
116
116
  claudeBin: z.string().default(""),
117
117
  codexBin: z.string().default(""),
118
118
  grokBin: z.string().default(""),
119
- opencodeBin: z.string().default(""),
120
119
  piBin: z.string().default(""),
121
120
  policy: z
122
121
  .object({
package/src/main.ts CHANGED
@@ -13,7 +13,6 @@ import { runPiSupervisor } from "./entries/pisupervisor.ts";
13
13
  import { claude } from "./lib/claude.ts";
14
14
  import { codex } from "./lib/codex.ts";
15
15
  import { grok } from "./lib/grok.ts";
16
- import { opencodeGo } from "./lib/opencodego.ts";
17
16
  import { cmdInit, cmdInitPi } from "./cli/init.ts";
18
17
  import { cmdAdd } from "./cli/add.ts";
19
18
  import { cmdAuth } from "./cli/auth.ts";
@@ -28,19 +27,18 @@ import { cmdServe } from "./cli/serve.ts";
28
27
  import { CHECK_JOB, deactivationHint, HUB_JOB, onLoginHome, uninstallSupervisor, uninstallTargets } from "./lib/install.ts";
29
28
  import { errorMessage } from "./lib/log.ts";
30
29
  import { HOME } from "./lib/paths.ts";
31
- import { c, emitError } from "./cli/render.ts";
30
+ import { c, count, emitError } from "./cli/render.ts";
32
31
 
33
32
  const JSON_FLAG = "--json";
34
33
  const CACHED_FLAG = "--cached";
35
34
  const YES_FLAG = "--yes";
36
35
  const CODEX_FLAG = "--codex";
37
36
  const GROK_FLAG = "--grok";
38
- const OPENCODE_GO_FLAG = "--opencode-go";
39
37
  const PI_FLAG = "--pi";
40
38
  const JSON_COMMANDS = new Set(["status", "config", "check"]);
41
39
  const CODEX_COMMANDS = new Set(["init", "add", "auth", "rm", "rename", "seat"]);
42
40
  const STATUS_ONLY_COMMANDS = new Set(["init", "add", "auth", "rm", "rename"]);
43
- const OPENCODE_GO_COMMANDS = new Set([...STATUS_ONLY_COMMANDS, "seat"]);
41
+ const MAX_OPERANDS = new Map([["init", 0], ["add", 0], ["rm", 1], ["rename", 2]]);
44
42
 
45
43
  function printHelp(): void {
46
44
  console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
@@ -51,19 +49,17 @@ function printHelp(): void {
51
49
  ${c.cyan("tokenmaxxing init --codex")} same for codex: log in the first account, isolated, install codex supervisor + Stop hook
52
50
  ${c.cyan("tokenmaxxing init --pi")} install the pi supervisor: pi sessions run on pooled Claude or ChatGPT accounts that are logged into pi
53
51
  ${c.cyan("tokenmaxxing init --grok")} pool grok Build logins (status-only: no supervisor yet)
54
- ${c.cyan("tokenmaxxing init --opencode-go")} pool opencode-go API keys (status-only: no supervisor yet)
55
52
  ${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
56
- ${c.cyan("tokenmaxxing add")} --codex | --grok | --opencode-go same for the codex, grok, or opencode-go pool
57
- ${c.cyan("tokenmaxxing auth")} [--codex | --grok | --opencode-go] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
53
+ ${c.cyan("tokenmaxxing add")} --codex | --grok same for the codex or grok pool
54
+ ${c.cyan("tokenmaxxing auth")} [--codex | --grok] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
58
55
  ${c.cyan("tokenmaxxing auth --pi")} [--codex] [sel | --all] log a pooled Claude account (or a codex account with --codex) into pi, isolated (bare = pick from a list; --all = every account without a pi login)
59
56
  ${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
60
57
  ${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)
61
58
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
62
- ${c.cyan("tokenmaxxing rename")} [--codex | --grok | --opencode-go] <sel> <label>
63
- ${c.cyan("tokenmaxxing rm")} [--codex | --grok | --opencode-go] <sel>
59
+ ${c.cyan("tokenmaxxing rename")} [--codex | --grok] <sel> <label>
60
+ ${c.cyan("tokenmaxxing rm")} [--codex | --grok] <sel>
64
61
  ${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
65
62
  ${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, lent to <pid> until it exits; codex sessions and other borrowers share the account, and a refresh race between them can sign it out until auth --codex runs again; exit 1 = none usable, fall back to the ambient login
66
- ${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
67
63
  ${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
68
64
  ${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
69
65
 
@@ -97,14 +93,14 @@ async function main(): Promise<number> {
97
93
  const json = jsonMode;
98
94
  const cached = argv.includes(CACHED_FLAG);
99
95
  const yes = argv.includes(YES_FLAG);
100
- const providerFlags = [CODEX_FLAG, GROK_FLAG, OPENCODE_GO_FLAG].filter((f) => argv.includes(f));
96
+ const providerFlags = [CODEX_FLAG, GROK_FLAG].filter((f) => argv.includes(f));
101
97
  if (providerFlags.length > 1) {
102
98
  emitError({ json, message: `${providerFlags.join(" and ")} are mutually exclusive - pick one pool` });
103
99
  return 2;
104
100
  }
105
- const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : argv.includes(OPENCODE_GO_FLAG) ? opencodeGo : claude;
101
+ const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : claude;
106
102
  const pi = argv.includes(PI_FLAG);
107
- const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== OPENCODE_GO_FLAG && a !== PI_FLAG);
103
+ const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== PI_FLAG);
108
104
  const sub = args[0];
109
105
 
110
106
  if (pi && sub !== "init" && sub !== "auth") {
@@ -132,19 +128,21 @@ async function main(): Promise<number> {
132
128
  return 2;
133
129
  }
134
130
 
135
- if (provider === grok || provider === opencodeGo) {
136
- const flag = provider === grok ? GROK_FLAG : OPENCODE_GO_FLAG;
137
- const commands = provider === grok ? STATUS_ONLY_COMMANDS : OPENCODE_GO_COMMANDS;
138
- if (sub == null || !commands.has(sub)) {
139
- emitError({ json, message: `${flag} applies to ${[...commands].join(", ")}, not ${sub ?? "status"}` });
140
- return 2;
141
- }
131
+ if (provider === grok && (sub == null || !STATUS_ONLY_COMMANDS.has(sub))) {
132
+ emitError({ json, message: `${GROK_FLAG} applies to ${[...STATUS_ONLY_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
133
+ return 2;
142
134
  }
143
135
 
144
136
  if (json && sub != null && !JSON_COMMANDS.has(sub)) {
145
137
  emitError({ json, message: `${sub} has no ${JSON_FLAG} form (${JSON_FLAG} applies to ${[...JSON_COMMANDS].join(", ")})` });
146
138
  return 2;
147
139
  }
140
+
141
+ const maxOperands = sub == null ? undefined : MAX_OPERANDS.get(sub);
142
+ if (maxOperands != null && args.length - 1 > maxOperands) {
143
+ emitError({ json, message: `${sub} takes at most ${count({ n: maxOperands, noun: "argument" })}, got: ${args.slice(1).join(" ")}` });
144
+ return 2;
145
+ }
148
146
  switch (sub) {
149
147
  case "__statusline": return runStatusline();
150
148
  case "__subagent-statusline": return runSubagentStatusline();
@@ -175,7 +173,7 @@ async function main(): Promise<number> {
175
173
  case "doctor": return cmdDoctor();
176
174
  case "rm": return cmdRm(provider, args[1]);
177
175
  case "rename": return cmdRename(provider, args.slice(1));
178
- case "seat": return cmdSeat(provider === codex ? "codex" : provider === opencodeGo ? "opencode-go" : "claude", args[1], args.slice(2));
176
+ case "seat": return cmdSeat(provider === codex ? "codex" : "claude", args[1], args.slice(2));
179
177
  case "serve": return cmdServe(args.slice(1));
180
178
  case "uninstall": {
181
179
  if (args.length > 1) {
@@ -200,7 +198,7 @@ async function main(): Promise<number> {
200
198
  if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
201
199
  if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
202
200
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
203
- console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/; opencode-go: opencode-go-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
201
+ console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
204
202
  return 0;
205
203
  }
206
204
  case "help":
@@ -1,99 +0,0 @@
1
- import { join } from "node:path";
2
- import { countBy, minBy } from "es-toolkit";
3
- import { z } from "zod";
4
- import { writeFileAtomic } from "./atomic.ts";
5
- import { env, HOME, opencodeGoAuthJsonFor, opencodeGoPaths, opencodeGoPool, opencodeGoStoreDirFor } from "./paths.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";
11
- import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
12
- import { ErrnoSchema } from "./types.ts";
13
- import { c } from "../cli/render.ts";
14
-
15
- const OpencodeAuthEntrySchema = z.looseObject({ type: z.string(), key: z.string().optional() });
16
- const OpencodeAuthFileSchema = z.record(z.string(), z.unknown());
17
-
18
- function liveAuthPath(): string {
19
- return join(env("XDG_DATA_HOME", join(HOME, ".local", "share")), "opencode", "auth.json");
20
- }
21
-
22
- function idOfKey(key: string): string {
23
- const h = new Bun.CryptoHasher("sha256");
24
- h.update(`opencode-go:${key}`);
25
- return h.digest("hex");
26
- }
27
-
28
- function harvestOf(key: string): Harvest {
29
- const id = idOfKey(key);
30
- return {
31
- id,
32
- email: null,
33
- tier: "go",
34
- sample: null,
35
- park: async () => {
36
- writeFileAtomic(opencodeGoAuthJsonFor(id), JSON.stringify({ "opencode-go": { type: "api", key } }, null, 2), 0o600);
37
- },
38
- };
39
- }
40
-
41
- function readAuth(path: string): AuthEntry[] {
42
- let map: z.infer<typeof OpencodeAuthFileSchema>;
43
- try {
44
- map = readJsonFile(path, OpencodeAuthFileSchema);
45
- } catch (e) {
46
- if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return [];
47
- throw e;
48
- }
49
- const entry = OpencodeAuthEntrySchema.safeParse(map["opencode-go"]);
50
- if (!entry.success || entry.data.type !== "api" || !entry.data.key) return [];
51
- return [{ id: idOfKey(entry.data.key), usable: true, harvest: harvestOf(entry.data.key) }];
52
- }
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
-
78
- export const opencodeGo: Provider = statusOnlyProvider({
79
- name: "opencode-go",
80
- flag: " --opencode-go",
81
- pool: opencodeGoPool,
82
- binName: "opencode",
83
- binKey: "opencodeBin",
84
- storeDirFor: opencodeGoStoreDirFor,
85
- authJsonFor: opencodeGoAuthJsonFor,
86
- onboardDir: opencodeGoPaths.onboardDir,
87
- homeEnv: "XDG_DATA_HOME",
88
- loginArgs: ["providers", "login", "-p", "opencode-go"],
89
- onboardAuthRel: join("opencode", "auth.json"),
90
- liveAuthPath,
91
- readAuth,
92
- liveId: () => null,
93
- presence: () => seatCounts(opencodeGoPaths.presenceDir),
94
- versionOk: (out) => out !== "",
95
- importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
96
- foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
97
- installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
98
- loginStep: () => `Paste the opencode-go API key from ${c.bold("opencode.ai/zen")} when opencode asks for it.`,
99
- });