tokenmaxxing 1.82.0 → 1.84.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, claudeBin/codexBin/grokBin/opencodeBin 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, 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.
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.
@@ -38,7 +38,8 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
38
38
  - `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
39
39
  - `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account, the session to resume, a `waitUntil`, a `compact` flag, the `origin` (the hook or the seat watch that wrote it), and the launch time the marker was written for.
40
40
  - `wait-queue.json` - the depleted-pool wait claims (`{sessionId, accountId, at, waitUntil}`), so at most four sessions wait on one account; a session that arrives when every account already has four waiters takes the soonest reset anyway.
41
- - `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points.
41
+ - `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
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)).
42
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
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.
44
45
 
package/README.md CHANGED
@@ -65,9 +65,11 @@ claude # use claude as always; each session gets its own accoun
65
65
  |---|---|
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
+ | `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 |
68
69
  | `tokenmaxxing init --grok` / `init --opencode-go` | pool the first grok login or opencode-go key, status-only: no supervisor, no switching |
69
70
  | `tokenmaxxing add [--codex \| --grok \| --opencode-go]` | register an additional account in that pool (isolated login, harvested once into its own store) |
70
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 |
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 |
71
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 |
72
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 |
73
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 |
@@ -110,7 +112,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
110
112
  }
111
113
  ```
112
114
 
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 the session bar only (default 3), 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; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is how long a usage sample stays fresh, for a seat's own sample 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 `opencodeBin` pin the real binaries; `init` writes them, and `TOKENMAXXING_CLAUDE_BIN`, `TOKENMAXXING_CODEX_BIN`, `TOKENMAXXING_GROK_BIN`, and `TOKENMAXXING_OPENCODE_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 3), 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; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is how long a usage sample stays fresh, for a seat's own sample 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`.
114
116
 
115
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`).
116
118
 
@@ -128,6 +130,19 @@ Codex mechanics differ from Claude Code in one hard way: a running codex process
128
130
 
129
131
  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 each codex session runs on its own store, so no live `auth.json` is ever shared: tokenmaxxing refreshes a parked account's token only when it reads usage for it, never one a session is running on, and a move is a restart at an idle turn boundary. `init --codex` refuses a `~/.codex/config.toml` that pins `cli_auth_credentials_store` away from `file`.
130
132
 
133
+ ## pi support
134
+
135
+ The [pi](https://pi.dev) coding agent runs on the same two pools when it signs in with a Claude subscription (`anthropic`) or a ChatGPT subscription (`openai-codex`):
136
+
137
+ ```sh
138
+ tokenmaxxing init --pi # install the pi supervisor
139
+ tokenmaxxing auth --pi --all # log each pooled Claude account into pi once
140
+ tokenmaxxing auth --pi --codex --all # same for the pooled codex accounts
141
+ pi # use pi as always
142
+ ```
143
+
144
+ pi keeps its own credential and refreshes it itself, so each pooled account gets a pi login of its own (a separate grant, never a copy) in `pi-stores/`, with everything else linked to the shared `~/.pi/agent`. The `pi` supervisor picks the pool from `--provider`, the `--model` prefix, or pi's `defaultProvider`, starts the session on a pooled account through `PI_CODING_AGENT_DIR`, watches that account every ten seconds, and at a bar relaunches `pi --session-id <id>` on a fresher account. Details and limits in [pi support](docs/content/docs/pi.mdx).
145
+
131
146
  ## Honest limitations
132
147
 
133
148
  - **A move restarts the process.** The session resumes the same transcript under the new account and continues on its own from a first prompt the supervisor submits, but the process restarts, which ends every background Bash task, Monitor, and Workflow run the session started (the first prompt asks Claude to relaunch what the task still needs); a bar-triggered move compacts the conversation on the old account first, so the first turn on the new account uploads the summary (the full context when the compaction does not land), while a move after a refusal re-uploads the full context once (prompt cache is org-scoped).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.82.0",
3
+ "version": "1.84.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
@@ -1,5 +1,9 @@
1
1
  import { partition } from "es-toolkit";
2
2
  import { withLock } from "../lib/lock.ts";
3
+ import { log } from "../lib/log.ts";
4
+ import type { PiPool } from "../lib/paths.ts";
5
+ import { piLogin, piLoginStep } from "../lib/pi.ts";
6
+ import { piStoreUsable, writePiStore } from "../lib/piauth.ts";
3
7
  import type { Provider } from "../lib/provider.ts";
4
8
  import { loadAccounts, saveAccounts, upsertAccount } from "../lib/state.ts";
5
9
  import { findAccount } from "./rename.ts";
@@ -7,7 +11,7 @@ import { usageNote } from "./add.ts";
7
11
  import { c, count } from "./render.ts";
8
12
  import type { Account, AccountsIndex } from "../lib/types.ts";
9
13
 
10
- const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok | --opencode-go] [<email|label|id> | --all]";
14
+ const AUTH_USAGE = "usage: tokenmaxxing auth [--codex | --grok | --opencode-go] [<email|label|id> | --all]\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
11
15
 
12
16
  export type AuthPlan = { kind: "usage" } | { kind: "error"; message: string } | { kind: "pick" } | { kind: "targets"; ids: string[] };
13
17
 
@@ -33,13 +37,11 @@ export function pickerOrder(accounts: Account[], needsAuth: Set<string>): Accoun
33
37
  return [...flagged, ...healthy];
34
38
  }
35
39
 
36
- function askWhichAccount(idx: AccountsIndex, needsAuth: Set<string>): Account | null {
37
- const ordered = pickerOrder(idx.accounts, needsAuth);
38
- console.log("which account do you want to reauthenticate?");
40
+ function askWhichAccount(input: { idx: AccountsIndex; needsAuth: Set<string>; question: string; flags: (a: Account) => string[] }): Account | null {
41
+ const ordered = pickerOrder(input.idx.accounts, input.needsAuth);
42
+ console.log(input.question);
39
43
  for (const [i, a] of ordered.entries()) {
40
- const flags: string[] = [];
41
- if (a.needsReauth) flags.push(c.red("needs-reauth"));
42
- else if (needsAuth.has(a.id)) flags.push(c.red("no-credential"));
44
+ const flags = input.flags(a);
43
45
  const labelNote = a.email != null && a.label !== a.email ? ` (${a.label})` : "";
44
46
  const tag = flags.length ? ` ${flags.join(" ")}` : "";
45
47
  console.log(` ${i + 1}. ${c.bold(a.email ?? a.label)}${labelNote}${tag}`);
@@ -94,11 +96,48 @@ async function reauthOne(p: Provider, target: Account): Promise<boolean> {
94
96
  return true;
95
97
  }
96
98
 
97
- export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
99
+ async function piLoginOne(p: Provider, pool: PiPool, target: Account): Promise<boolean> {
100
+ console.log(c.cyan(`Logging ${c.bold(target.label)} into pi - sign in as ${c.bold(target.email ?? target.label)}.`));
101
+ console.log(c.dim(piLoginStep(pool)));
102
+ console.log();
103
+
104
+ const login = await piLogin(pool);
105
+ if (!login) return false;
106
+
107
+ if (login.id !== target.id) {
108
+ const pooled = loadAccounts(p.pool).accounts.some((a) => a.id === login.id);
109
+ const hint = pooled
110
+ ? `run \`tokenmaxxing auth --pi${p.flag}\` for that account separately`
111
+ : `a pi login attaches to an account already in the pool; pool that account first with \`tokenmaxxing add${p.flag}\``;
112
+ console.error(
113
+ c.red(
114
+ `that login is ${c.bold(login.email ?? login.id.slice(0, 8))}, but ${target.label} is ${c.bold(target.email ?? target.id.slice(0, 8))} - nothing changed. To log that account into pi, ${hint}.`,
115
+ ),
116
+ );
117
+ return false;
118
+ }
119
+
120
+ const written = await withLock(p.pool.lockFile, () => {
121
+ if (!loadAccounts(p.pool).accounts.some((a) => a.id === target.id)) {
122
+ console.error(c.red(`${target.label} was removed from the pool while the login was open - nothing written`));
123
+ return false;
124
+ }
125
+ writePiStore(pool, target.id, login.cred);
126
+ return true;
127
+ });
128
+ if (!written) return false;
129
+ log("pi.login", { pool, account: target.id.slice(0, 8) });
130
+
131
+ console.log(`${c.green("✓")} logged ${c.bold(target.email ?? target.label)} into pi`);
132
+ return true;
133
+ }
134
+
135
+ export async function cmdAuth(p: Provider, argv: string[], pi: PiPool | null): Promise<number> {
98
136
  const idx = loadAccounts(p.pool);
99
137
  const needsAuth = new Set<string>();
100
138
  for (const a of idx.accounts) {
101
- if (a.needsReauth === true || !(await p.storeUsable(a))) needsAuth.add(a.id);
139
+ const usable = pi ? piStoreUsable(pi, a.id) : a.needsReauth !== true && (await p.storeUsable(a));
140
+ if (!usable) needsAuth.add(a.id);
102
141
  }
103
142
  const plan = planAuth({ p, accounts: idx.accounts, argv, needsAuth });
104
143
  if (plan.kind === "usage") {
@@ -112,7 +151,16 @@ export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
112
151
 
113
152
  const targets: Account[] = [];
114
153
  if (plan.kind === "pick") {
115
- const picked = askWhichAccount(idx, needsAuth);
154
+ const picked = askWhichAccount(
155
+ pi
156
+ ? { idx, needsAuth, question: "which account do you want to log into pi?", flags: (a) => (needsAuth.has(a.id) ? [c.red("no-pi-login")] : []) }
157
+ : {
158
+ idx,
159
+ needsAuth,
160
+ question: "which account do you want to reauthenticate?",
161
+ flags: (a) => (a.needsReauth ? [c.red("needs-reauth")] : needsAuth.has(a.id) ? [c.red("no-credential")] : []),
162
+ },
163
+ );
116
164
  if (!picked) return 1;
117
165
  targets.push(picked);
118
166
  } else {
@@ -121,7 +169,7 @@ export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
121
169
  if (account) targets.push(account);
122
170
  }
123
171
  if (targets.length === 0) {
124
- console.log(`${c.green("✓")} every account has a usable credential`);
172
+ console.log(`${c.green("✓")} ${pi ? "every account has a pi login" : "every account has a usable credential"}`);
125
173
  return 0;
126
174
  }
127
175
  }
@@ -130,12 +178,12 @@ export async function cmdAuth(p: Provider, argv: string[]): Promise<number> {
130
178
  for (const [i, target] of targets.entries()) {
131
179
  console.log();
132
180
  if (targets.length > 1) console.log(c.bold(`[${i + 1}/${targets.length}]`));
133
- if (await reauthOne(p, target)) ok += 1;
181
+ if (await (pi ? piLoginOne(p, pi, target) : reauthOne(p, target))) ok += 1;
134
182
  }
135
183
 
136
184
  if (targets.length > 1) {
137
185
  console.log();
138
- console.log(`reauthed ${count({ n: ok, noun: "account" })} of ${targets.length}`);
186
+ console.log(`${pi ? "logged into pi" : "reauthed"} ${count({ n: ok, noun: "account" })} of ${targets.length}`);
139
187
  }
140
188
  return ok === targets.length ? 0 : 1;
141
189
  }
package/src/cli/doctor.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { CLAUDE_BIN, verifyRealBin } from "../lib/claudebin.ts";
2
+ import { CLAUDE_BIN, PI_BIN, verifyRealBin } from "../lib/claudebin.ts";
3
3
  import { checkSettings, installedBin } from "../lib/settings.ts";
4
- import { activationHint, CHECK_JOB, codexStoreHookTrust, findClaudeShadowers, HUB_JOB, isBinDirAhead, jobHealthy, shellRcPath } from "../lib/install.ts";
4
+ import { activationHint, CHECK_JOB, codexStoreHookTrust, findClaudeShadowers, HUB_JOB, isBinDirAhead, jobHealthy, piSupervisorLink, shellRcPath } from "../lib/install.ts";
5
+ import { readPiStore } from "../lib/piauth.ts";
5
6
  import { errorMessage } from "../lib/log.ts";
6
7
  import { claudePool, codexPool, paths } from "../lib/paths.ts";
7
8
  import { loadAccounts, loadConfig } from "../lib/state.ts";
8
9
  import { readStore } from "../lib/credstore.ts";
9
- import { codexIdentityOf, readCodexStoreAuth } from "../lib/codexauth.ts";
10
+ import { chatgptAccountIdOf, codexIdentityOf, readCodexStoreAuth } from "../lib/codexauth.ts";
10
11
  import { isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
11
12
  import { c } from "./render.ts";
12
13
 
@@ -99,6 +100,44 @@ export async function cmdDoctor(): Promise<number> {
99
100
  check(fail === null, "claudeBin launches the real claude", fail ?? undefined);
100
101
  }
101
102
 
103
+ if (existsSync(piSupervisorLink())) {
104
+ check(!!cfg.piBin && existsSync(cfg.piBin), "real pi binary resolved", "run `tokenmaxxing init --pi`");
105
+ if (cfg.piBin && existsSync(cfg.piBin)) {
106
+ const fail = verifyRealBin({ ...PI_BIN, bin: cfg.piBin });
107
+ check(fail === null, "piBin launches the real pi", fail ?? undefined);
108
+ check(isBinDirAhead(PI_BIN), `${paths.binDir} is ahead of the real pi on PATH`, `export PATH="${paths.binDir}:$PATH"`);
109
+ }
110
+ for (const [pool, poolPaths, flag] of [["claude", claudePool, ""], ["codex", codexPool, " --codex"]] as const) {
111
+ for (const a of loadAccounts(poolPaths).accounts) {
112
+ let cred;
113
+ try {
114
+ cred = readPiStore(pool, a.id);
115
+ } catch (e) {
116
+ check(false, `pi login readable for ${a.label}`, errorMessage(e).slice(0, 100));
117
+ continue;
118
+ }
119
+ if (!cred) {
120
+ note(`${a.label} (${pool}) has no pi login - pi sessions skip it (run \`tokenmaxxing auth --pi${flag} ${a.label}\`)`);
121
+ continue;
122
+ }
123
+ if (pool === "codex") {
124
+ check(chatgptAccountIdOf({ jwt: cred.access }) === a.id, `pi login identity matches ${a.label} (codex)`, `run \`tokenmaxxing auth --pi --codex ${a.label}\``);
125
+ continue;
126
+ }
127
+ if (cred.expires <= Date.now()) {
128
+ note(`${a.label} pi login identity unverifiable (access token expired; pi refreshes it in its next session)`);
129
+ continue;
130
+ }
131
+ try {
132
+ const identity = await fetchTokenIdentity(cred.access);
133
+ check(identity.accountUuid === a.id, `pi login identity matches ${a.label}`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing auth --pi ${a.label}\``);
134
+ } catch (e) {
135
+ check(false, `pi login identity matches ${a.label}`, errorMessage(e).slice(0, 100));
136
+ }
137
+ }
138
+ }
139
+ }
140
+
102
141
  const rc = shellRcPath();
103
142
  if (rc && existsSync(rc)) {
104
143
  for (const s of findClaudeShadowers(readFileSync(rc, "utf8"))) {
package/src/cli/init.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { withLock } from "../lib/lock.ts";
3
- import { paths } from "../lib/paths.ts";
3
+ import { claudePool, codexPool, paths } from "../lib/paths.ts";
4
+ import { piInstall, piPreflight } from "../lib/pi.ts";
5
+ import { piStoreUsable } from "../lib/piauth.ts";
4
6
  import type { Provider } from "../lib/provider.ts";
5
7
  import { loadAccounts, loadConfig, saveAccounts, upsertAccount } from "../lib/state.ts";
6
8
  import { c, count } from "./render.ts";
@@ -18,6 +20,25 @@ export function printUsage(p: Provider): void {
18
20
  console.log(` ${c.cyan("xx help")} everything else`);
19
21
  }
20
22
 
23
+ export function cmdInitPi(): number {
24
+ mkdirSync(paths.home, { recursive: true });
25
+ loadConfig();
26
+ piPreflight();
27
+ piInstall();
28
+ console.log();
29
+ for (const [pool, poolPaths, flag] of [["claude", claudePool, ""], ["codex", codexPool, " --codex"]] as const) {
30
+ const accounts = loadAccounts(poolPaths).accounts;
31
+ if (accounts.length === 0) {
32
+ console.log(` ${pool} pool: empty - ${c.cyan(`xx init${flag}`)} pools the first account`);
33
+ continue;
34
+ }
35
+ const ready = accounts.filter((a) => piStoreUsable(pool, a.id)).length;
36
+ console.log(` ${pool} pool: ${ready} of ${count({ n: accounts.length, noun: "account" })} logged into pi - ${c.cyan(`xx auth --pi${flag} --all`)} logs in the rest`);
37
+ }
38
+ console.log(` then run ${c.cyan("pi")} as always: a session on an ${c.bold("anthropic")} or ${c.bold("openai-codex")} model starts on a pooled account and moves near quota`);
39
+ return 0;
40
+ }
41
+
21
42
  export async function cmdInit(p: Provider): Promise<number> {
22
43
  mkdirSync(paths.home, { recursive: true });
23
44
  loadConfig();