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/src/lib/install.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
+
}
|
package/src/lib/sessions.ts
CHANGED
|
@@ -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())) : {};
|
package/src/lib/supervise.ts
CHANGED
|
@@ -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
|
|
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":
|