tokenmaxxing 1.81.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.
@@ -7,17 +7,17 @@ import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.
7
7
  import { writeFileAtomic } from "./atomic.ts";
8
8
  import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
9
9
  import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
10
- import { loadConfig, readJsonFile } from "./state.ts";
10
+ import { loadConfig, readJsonFile, type BinKey } from "./state.ts";
11
11
  import { ErrnoSchema } from "./types.ts";
12
12
 
13
13
  export type InstallOutcome = { claudeWrapper: string; installedBin: string; pathAhead: boolean; timerLoaded: boolean; hubLoaded: boolean; checkIntervalS: number };
14
14
 
15
- export function isBinDirAhead(): boolean {
15
+ export function isBinDirAhead(bin: { name: string; key: BinKey } = CLAUDE_BIN): boolean {
16
16
  const dirs = (process.env.PATH ?? "").split(":");
17
17
  const ourIdx = dirs.indexOf(paths.binDir);
18
18
  if (ourIdx < 0) return false;
19
19
  try {
20
- const realDir = dirname(resolveRealBin(CLAUDE_BIN));
20
+ const realDir = dirname(resolveRealBin(bin));
21
21
  const realIdx = dirs.indexOf(realDir);
22
22
  return realIdx < 0 || ourIdx < realIdx;
23
23
  } catch {
@@ -88,7 +88,7 @@ exec ${JSON.stringify(bun)} --no-env-file run ${JSON.stringify(entry)} "$@"
88
88
  `;
89
89
  }
90
90
 
91
- export function installSupervisor(): InstallOutcome {
91
+ function installEntryPoints(): string {
92
92
  const target = installedBin();
93
93
  const entry = realpathSync(Bun.main);
94
94
  if (isNixPackaged()) {
@@ -96,9 +96,13 @@ export function installSupervisor(): InstallOutcome {
96
96
  } else {
97
97
  writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} --no-env-file run ${JSON.stringify(entry)} "$@"\n`, 0o755);
98
98
  }
99
+ writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${JSON.stringify(target)} "$@"\n`, 0o755);
100
+ return target;
101
+ }
99
102
 
103
+ export function installSupervisor(): InstallOutcome {
104
+ const target = installEntryPoints();
100
105
  writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
101
- writeFileAtomic(join(paths.binDir, "xx"), `#!/bin/sh\nexec ${JSON.stringify(target)} "$@"\n`, 0o755);
102
106
 
103
107
  installSettings();
104
108
  const checkIntervalS = Math.ceil(loadConfig().policy.checkIntervalMs / 1000);
@@ -205,6 +209,14 @@ export function uninstallCodexSupervisor(): void {
205
209
  if (existsSync(codexSupervisorLink())) rmSync(codexSupervisorLink(), { force: true });
206
210
  }
207
211
 
212
+ export function piSupervisorLink(): string {
213
+ return join(paths.binDir, "pi");
214
+ }
215
+
216
+ export function installPiSupervisor(): void {
217
+ writeFileAtomic(piSupervisorLink(), `#!/bin/sh\nexec ${JSON.stringify(installEntryPoints())} __supervise-pi "$@"\n`, 0o755);
218
+ }
219
+
208
220
  export type UnitJob = {
209
221
  label: string;
210
222
  arg: string;
@@ -508,6 +520,7 @@ export function uninstallTargets(live: boolean): string[] {
508
520
  ...[CHECK_JOB, HUB_JOB].filter((job) => !job.skip()).map((job) => uninstallTarget(job, live)),
509
521
  paths.supervisorLink,
510
522
  codexSupervisorLink(),
523
+ piSupervisorLink(),
511
524
  join(paths.binDir, "xx"),
512
525
  installedBin(),
513
526
  ...(rc == null ? [] : [`${rc}: the ${PATH_LINE_MARK} line`]),
@@ -519,7 +532,7 @@ export function uninstallSupervisor(input: { live: boolean }): UninstallOutcome
519
532
  const timer = uninstallJob(CHECK_JOB, input.live);
520
533
  const hub = uninstallJob(HUB_JOB, input.live);
521
534
  uninstallCodexSupervisor();
522
- for (const f of [paths.supervisorLink, join(paths.binDir, "xx"), installedBin()]) {
535
+ for (const f of [paths.supervisorLink, piSupervisorLink(), join(paths.binDir, "xx"), installedBin()]) {
523
536
  if (existsSync(f)) rmSync(f, { force: true });
524
537
  }
525
538
  const rc = shellRcPath();
package/src/lib/paths.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { homedir, userInfo } from "node:os";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  import { z } from "zod";
4
4
 
5
5
  const HOME = homedir();
@@ -88,6 +88,26 @@ export function codexSeatFromEnv(accountIds: string[], env: Record<string, strin
88
88
  return accountIds.find((id) => codexStoreDirFor(id) === home) ?? null;
89
89
  }
90
90
 
91
+ export type PiPool = "claude" | "codex";
92
+
93
+ export function expandTilde(p: string): string {
94
+ return p === "~" ? HOME : p.startsWith("~/") ? join(HOME, p.slice(2)) : p;
95
+ }
96
+
97
+ export const piPaths = {
98
+ home: resolve(expandTilde(env("PI_CODING_AGENT_DIR", join(HOME, ".pi", "agent")))),
99
+ storesDir: join(TM_HOME, "pi-stores"),
100
+ onboardDir: join(TM_HOME, "pi-onboard"),
101
+ } as const;
102
+
103
+ export function piStoreDirFor(pool: PiPool, accountId: string): string {
104
+ return join(piPaths.storesDir, pool, shortId(accountId));
105
+ }
106
+
107
+ export function piAuthJsonFor(pool: PiPool, accountId: string): string {
108
+ return join(piStoreDirFor(pool, accountId), "auth.json");
109
+ }
110
+
91
111
  const GROK_HOME_DEFAULT = join(HOME, ".grok");
92
112
 
93
113
  export const grokPaths = {
package/src/lib/pi.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { claude, ensurePathAhead, pickSeat } from "./claude.ts";
4
+ import { MAX_WRAP_DEPTH, PI_BIN, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
5
+ import { codex, pickCodexSeat } from "./codex.ts";
6
+ import { chatgptAccountIdOf } from "./codexauth.ts";
7
+ import { CHECK_JOB, installPiSupervisor, isBinDirAhead, jobHealthy, piSupervisorLink } from "./install.ts";
8
+ import { errorMessage, log } from "./log.ts";
9
+ import { fetchTokenIdentity } from "./oauth.ts";
10
+ import { claudePool, piPaths, type PiPool } from "./paths.ts";
11
+ import { linkSharedPiHome, piStoreUsable, readPiAuthAt, type PiOAuth } from "./piauth.ts";
12
+ import { StoreUnusableError, type Provider } from "./provider.ts";
13
+ import { loadAccounts, pinBinOverride } from "./state.ts";
14
+ import { restoreTermios, saveTermios } from "./tty.ts";
15
+ import type { Account, ModelInfo } from "./types.ts";
16
+ import { c } from "../cli/render.ts";
17
+
18
+ const PROVIDER_TITLE: Record<PiPool, string> = { claude: "Anthropic (Claude Pro/Max)", codex: "OpenAI (ChatGPT Plus/Pro)" };
19
+
20
+ export function piLoginStep(pool: PiPool): string {
21
+ return `In the pi session that opens, run ${c.bold("/login")} , pick ${c.bold(PROVIDER_TITLE[pool])}, and sign in with that account. It closes itself once you're in.`;
22
+ }
23
+
24
+ export type PiLogin = { id: string; email: string | null; cred: PiOAuth };
25
+
26
+ function landed(path: string, pool: PiPool): PiOAuth | null {
27
+ try {
28
+ return readPiAuthAt({ path, pool });
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ async function identityOf(pool: PiPool, cred: PiOAuth): Promise<{ id: string; email: string | null } | null> {
35
+ if (pool === "codex") {
36
+ const id = chatgptAccountIdOf({ jwt: cred.access });
37
+ if (id == null) {
38
+ console.error(c.red("the pi login's access token carries no ChatGPT account id - nothing changed."));
39
+ return null;
40
+ }
41
+ return { id, email: null };
42
+ }
43
+ try {
44
+ const identity = await fetchTokenIdentity(cred.access);
45
+ return { id: identity.accountUuid, email: identity.email };
46
+ } catch (e) {
47
+ console.error(c.red(`could not verify which account the pi login belongs to (${errorMessage(e)}) - nothing changed.`));
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export async function piLogin(pool: PiPool): Promise<PiLogin | null> {
53
+ const real = resolveRealBin(PI_BIN);
54
+ const onboardDir = piPaths.onboardDir;
55
+ rmSync(onboardDir, { recursive: true, force: true });
56
+ linkSharedPiHome(onboardDir);
57
+ const authPath = join(onboardDir, "auth.json");
58
+
59
+ const savedTermios = saveTermios();
60
+ const p = Bun.spawn([real, "--no-session", "--no-approve"], {
61
+ stdin: "inherit",
62
+ stdout: "inherit",
63
+ stderr: "inherit",
64
+ env: { ...process.env, PI_CODING_AGENT_DIR: onboardDir, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) },
65
+ });
66
+
67
+ try {
68
+ let cred: PiOAuth | null = null;
69
+ let exited = false;
70
+ const onExit = p.exited.then(() => { exited = true; });
71
+ while (!exited) {
72
+ await Bun.sleep(400);
73
+ cred = landed(authPath, pool);
74
+ if (cred) {
75
+ p.kill();
76
+ break;
77
+ }
78
+ }
79
+ await onExit;
80
+ restoreTermios(savedTermios);
81
+ cred ??= landed(authPath, pool);
82
+ if (!cred) {
83
+ console.error(c.red(`no ${PROVIDER_TITLE[pool]} login landed in the isolated pi home - nothing changed.`));
84
+ return null;
85
+ }
86
+ const identity = await identityOf(pool, cred);
87
+ return identity ? { ...identity, cred } : null;
88
+ } finally {
89
+ if (p.exitCode === null) {
90
+ p.kill();
91
+ await p.exited;
92
+ }
93
+ restoreTermios(savedTermios);
94
+ rmSync(onboardDir, { recursive: true, force: true });
95
+ }
96
+ }
97
+
98
+ function piMover(base: Provider, pool: PiPool): Provider {
99
+ return {
100
+ ...base,
101
+ swap: async (target) => {
102
+ if (!piStoreUsable(pool, target.id)) {
103
+ throw new StoreUnusableError(`${target.label} has no pi login - run \`tokenmaxxing auth --pi${base.flag} ${target.label}\``);
104
+ }
105
+ log("move.prepared", { account: target.id.slice(0, 8), label: target.label, pi: true });
106
+ },
107
+ classifySwapError: (e) => (e instanceof StoreUnusableError ? "skip" : "fatal"),
108
+ };
109
+ }
110
+
111
+ export const piMovers: Record<PiPool, Provider> = { claude: piMover(claude, "claude"), codex: piMover(codex, "codex") };
112
+
113
+ export function pickPiSeat(pool: PiPool, now: number, model: ModelInfo | null): Account | null {
114
+ const eligible = (a: Account) => piStoreUsable(pool, a.id);
115
+ return pool === "claude" ? pickSeat(now, model, eligible) : pickCodexSeat(now, eligible);
116
+ }
117
+
118
+ export function piPreflight(): void {
119
+ const real = resolveRealBin(PI_BIN);
120
+ const fail = verifyRealBin({ ...PI_BIN, bin: real });
121
+ if (fail !== null) throw new Error(`pi binary failed verification: ${real}: ${fail}`);
122
+ pinBinOverride({ key: "piBin", bin: real });
123
+ }
124
+
125
+ export function piInstall(): void {
126
+ installPiSupervisor();
127
+ console.log(`${c.green("✓")} pi supervisor installed at ${piSupervisorLink()}`);
128
+ if (!isBinDirAhead(PI_BIN)) ensurePathAhead();
129
+ if (loadAccounts(claudePool).accounts.length > 0 && !jobHealthy(CHECK_JOB)) {
130
+ console.log(c.yellow("⚠ the check timer is not active, so pi seats on Claude accounts get no fresh usage - run `tokenmaxxing init`"));
131
+ }
132
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { writeFileAtomic } from "./atomic.ts";
5
+ import { piAuthJsonFor, piPaths, piStoreDirFor, type PiPool } from "./paths.ts";
6
+ import { readJsonFile } from "./state.ts";
7
+ import { ErrnoSchema } from "./types.ts";
8
+
9
+ export const PI_PROVIDER: Record<PiPool, string> = { claude: "anthropic", codex: "openai-codex" };
10
+
11
+ const PiOAuthSchema = z.looseObject({
12
+ type: z.literal("oauth"),
13
+ access: z.string().min(1),
14
+ refresh: z.string().min(1),
15
+ expires: z.number(),
16
+ });
17
+ export type PiOAuth = z.infer<typeof PiOAuthSchema>;
18
+
19
+ const PiAuthFileSchema = z.record(z.string(), z.unknown());
20
+
21
+ export function readPiAuthAt(input: { path: string; pool: PiPool }): PiOAuth | null {
22
+ let file: z.infer<typeof PiAuthFileSchema>;
23
+ try {
24
+ file = readJsonFile(input.path, PiAuthFileSchema);
25
+ } catch (e) {
26
+ if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
27
+ throw e;
28
+ }
29
+ const entry = file[PI_PROVIDER[input.pool]];
30
+ if (entry === undefined) return null;
31
+ const parsed = PiOAuthSchema.safeParse(entry);
32
+ if (!parsed.success) throw new Error(`${input.path}: the ${PI_PROVIDER[input.pool]} entry is not a pi OAuth credential`);
33
+ return parsed.data;
34
+ }
35
+
36
+ export function readPiStore(pool: PiPool, accountId: string): PiOAuth | null {
37
+ return readPiAuthAt({ path: piAuthJsonFor(pool, accountId), pool });
38
+ }
39
+
40
+ export function piStoreUsable(pool: PiPool, accountId: string): boolean {
41
+ try {
42
+ return readPiStore(pool, accountId) != null;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function writePiStore(pool: PiPool, accountId: string, cred: PiOAuth): void {
49
+ writeFileAtomic(piAuthJsonFor(pool, accountId), JSON.stringify({ [PI_PROVIDER[pool]]: cred }, null, 2), 0o600);
50
+ }
51
+
52
+ export function deletePiStore(pool: PiPool, accountId: string): void {
53
+ rmSync(piStoreDirFor(pool, accountId), { recursive: true, force: true });
54
+ }
55
+
56
+ const SHARED_DIRS = ["sessions", "bin"];
57
+ const SHARED_FILES = ["settings.json", "models-store.json", "trust.json"];
58
+
59
+ export function linkSharedPiHome(dir: string): void {
60
+ mkdirSync(dir, { recursive: true });
61
+ for (const name of SHARED_DIRS) mkdirSync(join(piPaths.home, name), { recursive: true });
62
+ for (const name of SHARED_FILES) {
63
+ try {
64
+ writeFileSync(join(piPaths.home, name), "{}", { flag: "wx", mode: 0o600 });
65
+ } catch (e) {
66
+ if (ErrnoSchema.safeParse(e).data?.code !== "EEXIST") throw e;
67
+ }
68
+ }
69
+ for (const name of readdirSync(piPaths.home)) {
70
+ if (name.startsWith("auth.json") || name.endsWith(".lock")) continue;
71
+ const link = join(dir, name);
72
+ if (existsSync(link)) continue;
73
+ try {
74
+ symlinkSync(join(piPaths.home, name), link);
75
+ } catch (e) {
76
+ if (ErrnoSchema.safeParse(e).data?.code !== "EEXIST") throw e;
77
+ }
78
+ }
79
+ }
80
+
81
+ export function ensurePiStoreHome(pool: PiPool, accountId: string): string {
82
+ const store = piStoreDirFor(pool, accountId);
83
+ linkSharedPiHome(store);
84
+ return store;
85
+ }
@@ -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 } from "./paths.ts";
5
+ import { codexPaths, grokPaths, opencodeGoPaths, 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];
48
+ const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, opencodeGoPaths.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 [];
package/src/lib/state.ts CHANGED
@@ -35,10 +35,11 @@ export function loadConfig(): Config {
35
35
  cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
36
36
  cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
37
37
  cfg.opencodeBin = optionalEnv("TOKENMAXXING_OPENCODE_BIN") ?? cfg.opencodeBin;
38
+ cfg.piBin = optionalEnv("TOKENMAXXING_PI_BIN") ?? cfg.piBin;
38
39
  return cfg;
39
40
  }
40
41
 
41
- export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin";
42
+ export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin" | "piBin";
42
43
 
43
44
  export function pinBinOverride(input: { key: BinKey; bin: string }): void {
44
45
  const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
@@ -5,7 +5,37 @@ import { paths } from "./paths.ts";
5
5
  import { writePresence } from "./presence.ts";
6
6
  import { restoreTermios } from "./tty.ts";
7
7
 
8
- export type WrappedProduct = "claude" | "codex";
8
+ export type WrappedProduct = "claude" | "codex" | "pi";
9
+
10
+ export const SEAT_POLL_MS = 10_000;
11
+ export const SEAT_RETRY_MS = 60_000;
12
+
13
+ export type Say = (terminal: string, text: string) => void;
14
+
15
+ const clockFmt = new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZoneName: "short" });
16
+
17
+ export async function countdownWait(acct: string, until: number, out: { stream: boolean; say: Say }): Promise<boolean> {
18
+ let aborted = false;
19
+ const onInt = () => { aborted = true; };
20
+ process.on("SIGINT", onInt);
21
+ const minutes = Math.max(1, Math.ceil((until - Date.now()) / 60000));
22
+ out.say(
23
+ `\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`,
24
+ `tokenmaxxing: all accounts at their limit. Resuming at ${clockFmt.format(until)} (in ${minutes} min) on ${acct}.`,
25
+ );
26
+ while (!aborted && Date.now() < until) {
27
+ if (!out.stream) {
28
+ const left = until - Date.now();
29
+ const m = Math.floor(left / 60000);
30
+ const s = Math.floor((left % 60000) / 1000);
31
+ process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
32
+ }
33
+ await Bun.sleep(1000);
34
+ }
35
+ process.removeListener("SIGINT", onInt);
36
+ out.say(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`, `tokenmaxxing: resuming on ${acct}.`);
37
+ return aborted;
38
+ }
9
39
 
10
40
  type ChildHandle = {
11
41
  pid: number;
@@ -18,6 +48,7 @@ type ChildHandle = {
18
48
  const WRAPPED: Record<WrappedProduct, { setting: string; binary: string; events: string }> = {
19
49
  claude: { setting: "claudeBin", binary: "Claude", events: "supervisor" },
20
50
  codex: { setting: "codexBin", binary: "codex", events: "codexsupervisor" },
51
+ pi: { setting: "piBin", binary: "pi", events: "pisupervisor" },
21
52
  };
22
53
 
23
54
  export function exitStatus(child: { exitCode: number | null; signalCode: string | null }): number {
package/src/lib/types.ts CHANGED
@@ -104,6 +104,7 @@ export const ConfigSchema = z
104
104
  codexBin: z.string().default(""),
105
105
  grokBin: z.string().default(""),
106
106
  opencodeBin: z.string().default(""),
107
+ piBin: z.string().default(""),
107
108
  policy: z
108
109
  .object({
109
110
  projectionMargin: z.number().min(0).max(100).default(3),
package/src/main.ts CHANGED
@@ -8,11 +8,12 @@ import { runBoundaryHook } from "./entries/stophook.ts";
8
8
  import { runStopFailureHook } from "./entries/stopfailurehook.ts";
9
9
  import { runCodexSupervisor } from "./entries/codexsupervisor.ts";
10
10
  import { runCodexStopHook } from "./entries/codexstophook.ts";
11
+ import { runPiSupervisor } from "./entries/pisupervisor.ts";
11
12
  import { claude } from "./lib/claude.ts";
12
13
  import { codex } from "./lib/codex.ts";
13
14
  import { grok } from "./lib/grok.ts";
14
15
  import { opencodeGo } from "./lib/opencodego.ts";
15
- import { cmdInit } from "./cli/init.ts";
16
+ import { cmdInit, cmdInitPi } from "./cli/init.ts";
16
17
  import { cmdAdd } from "./cli/add.ts";
17
18
  import { cmdAuth } from "./cli/auth.ts";
18
19
  import { cmdStatus } from "./cli/status.ts";
@@ -34,6 +35,7 @@ const YES_FLAG = "--yes";
34
35
  const CODEX_FLAG = "--codex";
35
36
  const GROK_FLAG = "--grok";
36
37
  const OPENCODE_GO_FLAG = "--opencode-go";
38
+ const PI_FLAG = "--pi";
37
39
  const JSON_COMMANDS = new Set(["status", "config", "check"]);
38
40
  const CODEX_COMMANDS = new Set(["init", "add", "auth", "rm", "rename", "seat"]);
39
41
  const STATUS_ONLY_COMMANDS = new Set(["init", "add", "auth", "rm", "rename"]);
@@ -45,11 +47,13 @@ function printHelp(): void {
45
47
  ${c.cyan("tokenmaxxing check")} sample up to three accounts whose last usage attempts are oldest (run by the periodic timer)
46
48
  ${c.cyan("tokenmaxxing init")} log in the first account (isolated) + install supervisor & hooks
47
49
  ${c.cyan("tokenmaxxing init --codex")} same for codex: log in the first account, isolated, install codex supervisor + Stop hook
50
+ ${c.cyan("tokenmaxxing init --pi")} install the pi supervisor: pi sessions run on pooled Claude or ChatGPT accounts that are logged into pi
48
51
  ${c.cyan("tokenmaxxing init --grok")} pool grok Build logins (status-only: no supervisor yet)
49
52
  ${c.cyan("tokenmaxxing init --opencode-go")} pool opencode-go API keys (status-only: no supervisor yet)
50
53
  ${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
51
54
  ${c.cyan("tokenmaxxing add")} --codex | --grok | --opencode-go same for the codex, grok, or opencode-go pool
52
55
  ${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)
56
+ ${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)
53
57
  ${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
54
58
  ${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)
55
59
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
@@ -80,6 +84,9 @@ async function main(): Promise<number> {
80
84
  if (argv0 === "codex" || argv[0] === "__supervise-codex") {
81
85
  return runCodexSupervisor({ argv: argv[0] === "__supervise-codex" ? argv.slice(1) : argv });
82
86
  }
87
+ if (argv0 === "pi" || argv[0] === "__supervise-pi") {
88
+ return runPiSupervisor(argv[0] === "__supervise-pi" ? argv.slice(1) : argv);
89
+ }
83
90
 
84
91
  jsonMode = argv.includes(JSON_FLAG);
85
92
  const json = jsonMode;
@@ -91,9 +98,20 @@ async function main(): Promise<number> {
91
98
  return 2;
92
99
  }
93
100
  const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : argv.includes(OPENCODE_GO_FLAG) ? opencodeGo : claude;
94
- const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== OPENCODE_GO_FLAG);
101
+ const pi = argv.includes(PI_FLAG);
102
+ 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);
95
103
  const sub = args[0];
96
104
 
105
+ if (pi && sub !== "init" && sub !== "auth") {
106
+ emitError({ json, message: `${PI_FLAG} applies to init and auth, not ${sub ?? "status"}` });
107
+ return 2;
108
+ }
109
+
110
+ if (pi && (sub === "init" ? provider !== claude : provider !== claude && provider !== codex)) {
111
+ emitError({ json, message: sub === "init" ? `init ${PI_FLAG} takes no pool flag` : `auth ${PI_FLAG} logs a Claude account (no pool flag) or a codex account (${CODEX_FLAG}) into pi` });
112
+ return 2;
113
+ }
114
+
97
115
  if (cached && sub != null && sub !== "status") {
98
116
  emitError({ json, message: `${CACHED_FLAG} applies to status only, not ${sub}` });
99
117
  return 2;
@@ -143,9 +161,9 @@ async function main(): Promise<number> {
143
161
  return cmdCheck(json);
144
162
  }
145
163
  case "config": return cmdConfig(args.slice(1), json);
146
- case "init": return cmdInit(provider);
164
+ case "init": return pi ? cmdInitPi() : cmdInit(provider);
147
165
  case "add": return cmdAdd(provider);
148
- case "auth": return cmdAuth(provider, args.slice(1));
166
+ case "auth": return cmdAuth(provider, args.slice(1), pi ? (provider === codex ? "codex" : "claude") : null);
149
167
  case "doctor": return cmdDoctor();
150
168
  case "rm": return cmdRm(provider, args[1]);
151
169
  case "rename": return cmdRename(provider, args.slice(1));
@@ -180,7 +198,7 @@ async function main(): Promise<number> {
180
198
  if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
181
199
  if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
182
200
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
183
- console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-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/; opencode-go: opencode-go-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
184
202
  return 0;
185
203
  }
186
204
  case "help":