tokenmaxxing 1.82.0 → 1.83.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 +3 -2
- package/README.md +16 -1
- package/package.json +1 -1
- package/src/cli/auth.ts +61 -13
- package/src/cli/doctor.ts +42 -3
- package/src/cli/init.ts +22 -1
- package/src/entries/pisupervisor.ts +442 -0
- package/src/entries/supervisor.ts +1 -31
- package/src/lib/claude.ts +6 -3
- package/src/lib/claudebin.ts +9 -0
- package/src/lib/codex.ts +12 -8
- package/src/lib/codexauth.ts +8 -0
- package/src/lib/install.ts +19 -6
- package/src/lib/paths.ts +21 -1
- package/src/lib/pi.ts +132 -0
- package/src/lib/piauth.ts +85 -0
- package/src/lib/sessions.ts +2 -2
- package/src/lib/state.ts +2 -1
- package/src/lib/supervise.ts +32 -1
- package/src/lib/types.ts +1 -0
- package/src/main.ts +23 -5
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 `
|
|
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.
|
|
3
|
+
"version": "1.83.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
|
|
37
|
-
const ordered = pickerOrder(idx.accounts, needsAuth);
|
|
38
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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();
|