tokenmaxxing 1.130.0 → 1.132.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 -3
- package/README.md +7 -7
- package/package.json +1 -1
- package/src/cli/auth.ts +1 -1
- package/src/cli/key.ts +147 -0
- package/src/cli/rename.ts +1 -1
- package/src/cli/seat.ts +0 -6
- package/src/cli/status.ts +50 -11
- package/src/entries/statusline.ts +10 -0
- package/src/entries/stopfailurehook.ts +24 -6
- package/src/entries/stophook.ts +7 -6
- package/src/entries/supervisor.ts +105 -34
- package/src/lib/apikeys.ts +142 -0
- package/src/lib/claude.ts +25 -7
- package/src/lib/compact.ts +3 -4
- package/src/lib/decide.ts +27 -6
- package/src/lib/paths.ts +3 -19
- package/src/lib/pi.ts +1 -0
- package/src/lib/presence.ts +6 -5
- package/src/lib/provider.ts +9 -2
- package/src/lib/sessions.ts +9 -4
- package/src/lib/state.ts +1 -2
- package/src/lib/statusonly.ts +1 -1
- package/src/lib/supervise.ts +2 -2
- package/src/lib/types.ts +30 -1
- package/src/lib/usage.ts +1 -1
- package/src/main.ts +25 -22
- package/src/lib/opencodego.ts +0 -99
package/src/lib/paths.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const paths = {
|
|
|
28
28
|
onboardDir: join(TM_HOME, "onboard"),
|
|
29
29
|
storesDir: join(TM_HOME, "stores"),
|
|
30
30
|
hubKeyFile: join(TM_HOME, "hub-key"),
|
|
31
|
+
apiKeysDir: join(TM_HOME, "api-keys"),
|
|
32
|
+
costDir: join(TM_HOME, "cost"),
|
|
31
33
|
|
|
32
34
|
claudeSettings: env(
|
|
33
35
|
"TOKENMAXXING_CLAUDE_SETTINGS",
|
|
@@ -43,6 +45,7 @@ export const claudePool = {
|
|
|
43
45
|
accountsJson: join(TM_HOME, "accounts.json"),
|
|
44
46
|
lockFile: join(TM_HOME, "lock"),
|
|
45
47
|
waitQueueJson: join(TM_HOME, "wait-queue.json"),
|
|
48
|
+
apiKeysJson: join(TM_HOME, "api-keys.json"),
|
|
46
49
|
} as const;
|
|
47
50
|
|
|
48
51
|
export const codexPool = {
|
|
@@ -55,11 +58,6 @@ export const grokPool = {
|
|
|
55
58
|
lockFile: join(TM_HOME, "grok-lock"),
|
|
56
59
|
} as const;
|
|
57
60
|
|
|
58
|
-
export const opencodeGoPool = {
|
|
59
|
-
accountsJson: join(TM_HOME, "opencode-go-accounts.json"),
|
|
60
|
-
lockFile: join(TM_HOME, "opencode-go-lock"),
|
|
61
|
-
} as const;
|
|
62
|
-
|
|
63
61
|
export type PoolPaths = { accountsJson: string; lockFile: string };
|
|
64
62
|
|
|
65
63
|
const CODEX_HOME = env("TOKENMAXXING_CODEX_HOME", env("CODEX_HOME", join(HOME, ".codex")));
|
|
@@ -130,20 +128,6 @@ export function grokSeatFromEnv(accountIds: string[], env: Record<string, string
|
|
|
130
128
|
return accountIds.find((id) => grokStoreDirFor(id) === home) ?? null;
|
|
131
129
|
}
|
|
132
130
|
|
|
133
|
-
export const opencodeGoPaths = {
|
|
134
|
-
storesDir: join(TM_HOME, "opencode-go-stores"),
|
|
135
|
-
onboardDir: join(TM_HOME, "opencode-go-onboard"),
|
|
136
|
-
presenceDir: join(TM_HOME, "opencode-go-live"),
|
|
137
|
-
} as const;
|
|
138
|
-
|
|
139
|
-
export function opencodeGoStoreDirFor(accountId: string): string {
|
|
140
|
-
return join(opencodeGoPaths.storesDir, shortId(accountId));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function opencodeGoAuthJsonFor(accountId: string): string {
|
|
144
|
-
return join(opencodeGoStoreDirFor(accountId), "auth.json");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
131
|
const CLAUDE_KEYCHAIN_ACCOUNT_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
148
132
|
|
|
149
133
|
function claudeKeychainAccount(): string {
|
package/src/lib/pi.ts
CHANGED
|
@@ -98,6 +98,7 @@ export async function piLogin(pool: PiPool): Promise<PiLogin | null> {
|
|
|
98
98
|
function piMover(base: Provider, pool: PiPool): Provider {
|
|
99
99
|
return {
|
|
100
100
|
...base,
|
|
101
|
+
apiKeys: undefined,
|
|
101
102
|
swap: async (target) => {
|
|
102
103
|
if (!piStoreUsable(pool, target.id)) {
|
|
103
104
|
throw new StoreUnusableError(`${target.label} has no pi login - run \`tokenmaxxing auth --pi${base.flag} ${target.label}\``);
|
package/src/lib/presence.ts
CHANGED
|
@@ -13,12 +13,13 @@ const PresenceSchema = z.object({
|
|
|
13
13
|
accountId: z.string(),
|
|
14
14
|
pid: z.number(),
|
|
15
15
|
startedAt: z.string(),
|
|
16
|
+
apiKeyId: z.string().optional(),
|
|
16
17
|
});
|
|
17
18
|
|
|
18
|
-
export function writePresence(input: { dir: string; id: string; accountId: string; pid: number }): void {
|
|
19
|
+
export function writePresence(input: { dir: string; id: string; accountId: string; pid: number; apiKeyId?: string }): void {
|
|
19
20
|
const startedAt = pidStartTimes([input.pid]).get(input.pid);
|
|
20
21
|
if (startedAt == null) throw new Error(`could not read pid ${input.pid}'s start time (ps lstart) - refusing to write an unverifiable presence file`);
|
|
21
|
-
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt })));
|
|
22
|
+
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt, ...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}) })));
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function presencePid(input: { dir: string; id: string }): number | null {
|
|
@@ -34,7 +35,7 @@ export function clearPresence(input: { dir: string; id: string }): void {
|
|
|
34
35
|
rmSync(join(input.dir, input.id), { force: true });
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
export type LivingPresence = { id: string; accountId: string };
|
|
38
|
+
export type LivingPresence = { id: string; accountId: string; apiKeyId?: string };
|
|
38
39
|
|
|
39
40
|
export function livingPresences(dir: string): LivingPresence[] {
|
|
40
41
|
if (!existsSync(dir)) return [];
|
|
@@ -65,11 +66,11 @@ export function livingPresences(dir: string): LivingPresence[] {
|
|
|
65
66
|
rmSync(file, { force: true });
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
|
-
living.push({ id: name, accountId: record.accountId });
|
|
69
|
+
living.push({ id: name, accountId: record.accountId, ...(record.apiKeyId != null ? { apiKeyId: record.apiKeyId } : {}) });
|
|
69
70
|
}
|
|
70
71
|
return living;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function seatCounts(dir: string): Map<string, number> {
|
|
74
|
-
return new Map(Object.entries(countBy(livingPresences(dir), (p) => p.accountId)));
|
|
75
|
+
return new Map(Object.entries(countBy(livingPresences(dir).filter((p) => p.apiKeyId == null), (p) => p.accountId)));
|
|
75
76
|
}
|
package/src/lib/provider.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { PoolPaths } from "./paths.ts";
|
|
2
2
|
import type { Harvest } from "./state.ts";
|
|
3
|
-
import type { Account, Config, Window } from "./types.ts";
|
|
3
|
+
import type { Account, ApiKey, Config, Window } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export type ApiKeyPool = {
|
|
6
|
+
liveId(): string | null;
|
|
7
|
+
holds(id: string): boolean;
|
|
8
|
+
pick(exclude: string[]): ApiKey | null;
|
|
9
|
+
};
|
|
4
10
|
|
|
5
11
|
export type Observation = { windows: Window[]; at: number };
|
|
6
12
|
|
|
@@ -20,12 +26,13 @@ export class StoreUnusableError extends Error {
|
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export type Provider = {
|
|
23
|
-
name: "claude" | "codex" | "grok"
|
|
29
|
+
name: "claude" | "codex" | "grok";
|
|
24
30
|
flag: string;
|
|
25
31
|
pool: PoolPaths;
|
|
26
32
|
seats: "shared" | "live";
|
|
27
33
|
waitsWhenDepleted: boolean;
|
|
28
34
|
statusOnly: boolean;
|
|
35
|
+
apiKeys?: ApiKeyPool;
|
|
29
36
|
liveId(): string | null;
|
|
30
37
|
presence(): Map<string, number>;
|
|
31
38
|
gatedFamilies(cfg: Config): string[] | null;
|
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,
|
|
5
|
+
import { codexPaths, grokPaths, paths, piPaths } from "./paths.ts";
|
|
6
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { presencePid } from "./presence.ts";
|
|
8
8
|
import { readJsonFile } from "./state.ts";
|
|
@@ -45,7 +45,7 @@ function recordLiveSession(sid: string, current: string): void {
|
|
|
45
45
|
const TMP_MARKER = ".tmp.";
|
|
46
46
|
const TMP_GRACE_MS = 3600 * 1000;
|
|
47
47
|
|
|
48
|
-
const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir,
|
|
48
|
+
const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex")];
|
|
49
49
|
|
|
50
50
|
function listDir(dir: string, root: string): Dirent[] {
|
|
51
51
|
if (!existsSync(dir)) return [];
|
|
@@ -60,7 +60,7 @@ function listDir(dir: string, root: string): Dirent[] {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function tmpSweepDirs(root: string): string[] {
|
|
63
|
-
const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir
|
|
63
|
+
const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, paths.apiKeysDir, paths.costDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
|
|
64
64
|
for (const parent of STORE_PARENTS) {
|
|
65
65
|
for (const child of listDir(parent, root)) {
|
|
66
66
|
if (child.isDirectory()) dirs.push(join(parent, child.name));
|
|
@@ -113,6 +113,10 @@ export function supervisedSession(env: Record<string, string | undefined> = proc
|
|
|
113
113
|
return { sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null, live: liveSessionId(sid) };
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
export function keyMovesSupported(env: Record<string, string | undefined> = process.env): boolean {
|
|
117
|
+
return env.TOKENMAXXING_API_KEY_ID !== undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
116
120
|
export function refusedAccounts(env: Record<string, string | undefined> = process.env): string[] {
|
|
117
121
|
return RefusedSchema.parse(env.TOKENMAXXING_REFUSED);
|
|
118
122
|
}
|
|
@@ -129,9 +133,10 @@ export function adoptLiveSession(session: SupervisedSession, stdinSid: string |
|
|
|
129
133
|
return { ...session, live: stdinSid };
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; waitUntil: number; compact: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; refused?: string[] }): void {
|
|
136
|
+
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; apiKeyId?: string; waitUntil: number; compact: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; refused?: string[] }): void {
|
|
133
137
|
const payload: z.infer<typeof RespawnMarkerSchema> = {
|
|
134
138
|
accountId: input.accountId,
|
|
139
|
+
...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}),
|
|
135
140
|
ts: Date.now(),
|
|
136
141
|
waitUntil: input.waitUntil,
|
|
137
142
|
sessionId: input.session.live,
|
package/src/lib/state.ts
CHANGED
|
@@ -34,12 +34,11 @@ export function loadConfig(): Config {
|
|
|
34
34
|
cfg.claudeBin = optionalEnv("TOKENMAXXING_CLAUDE_BIN") ?? cfg.claudeBin;
|
|
35
35
|
cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
|
|
36
36
|
cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
|
|
37
|
-
cfg.opencodeBin = optionalEnv("TOKENMAXXING_OPENCODE_BIN") ?? cfg.opencodeBin;
|
|
38
37
|
cfg.piBin = optionalEnv("TOKENMAXXING_PI_BIN") ?? cfg.piBin;
|
|
39
38
|
return cfg;
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "
|
|
41
|
+
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "piBin";
|
|
43
42
|
|
|
44
43
|
export function pinBinOverride(input: { key: BinKey; bin: string }): void {
|
|
45
44
|
const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
|
package/src/lib/statusonly.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type StatusOnlySpec = {
|
|
|
14
14
|
flag: string;
|
|
15
15
|
pool: PoolPaths;
|
|
16
16
|
binName: string;
|
|
17
|
-
binKey: "grokBin"
|
|
17
|
+
binKey: "grokBin";
|
|
18
18
|
storeDirFor: (accountId: string) => string;
|
|
19
19
|
authJsonFor: (accountId: string) => string;
|
|
20
20
|
onboardDir: string;
|
package/src/lib/supervise.ts
CHANGED
|
@@ -82,10 +82,10 @@ export async function runPassthrough(input: { real: string; argv: string[]; env:
|
|
|
82
82
|
return exitStatus(p);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
|
|
85
|
+
export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; apiKeyId?: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
|
|
86
86
|
for (let attempt = 0; ; attempt++) {
|
|
87
87
|
try {
|
|
88
|
-
writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid });
|
|
88
|
+
writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid, ...(input.apiKeyId != null ? { apiKeyId: input.apiKeyId } : {}) });
|
|
89
89
|
return;
|
|
90
90
|
} catch (e) {
|
|
91
91
|
if (input.child.exitCode !== null || input.child.signalCode !== null) return;
|
package/src/lib/types.ts
CHANGED
|
@@ -116,7 +116,6 @@ export const ConfigSchema = z
|
|
|
116
116
|
claudeBin: z.string().default(""),
|
|
117
117
|
codexBin: z.string().default(""),
|
|
118
118
|
grokBin: z.string().default(""),
|
|
119
|
-
opencodeBin: z.string().default(""),
|
|
120
119
|
piBin: z.string().default(""),
|
|
121
120
|
policy: z
|
|
122
121
|
.object({
|
|
@@ -170,8 +169,38 @@ export const RespawnMarkerSchema = z.object({
|
|
|
170
169
|
origin: z.enum(["stop", "sessionstart", "stopfailure", "seatwatch"]),
|
|
171
170
|
launchedAt: z.number().optional(),
|
|
172
171
|
refused: z.array(z.string()).optional(),
|
|
172
|
+
apiKeyId: z.string().optional(),
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
export const ApiKeySchema = z.object({
|
|
176
|
+
id: z.string(),
|
|
177
|
+
label: z.string(),
|
|
178
|
+
workspaceId: z.string().optional(),
|
|
179
|
+
creditUsd: z.number().optional(),
|
|
180
|
+
spentUsd: z.number(),
|
|
181
|
+
creditSetAt: z.number(),
|
|
182
|
+
refusedAt: z.number().optional(),
|
|
183
|
+
addedAt: z.number(),
|
|
184
|
+
});
|
|
185
|
+
export type ApiKey = z.infer<typeof ApiKeySchema>;
|
|
186
|
+
|
|
187
|
+
export const ApiKeysIndexSchema = z.object({
|
|
188
|
+
version: z.literal(1),
|
|
189
|
+
keys: z.array(ApiKeySchema).default([]),
|
|
190
|
+
baselines: z.record(z.string(), z.object({ usd: z.number(), at: z.number() })).default({}),
|
|
191
|
+
});
|
|
192
|
+
export type ApiKeysIndex = z.infer<typeof ApiKeysIndexSchema>;
|
|
193
|
+
|
|
194
|
+
export const SessionCostSchema = z.object({ sessionId: z.string(), usd: z.number() });
|
|
195
|
+
export type SessionCost = z.infer<typeof SessionCostSchema>;
|
|
196
|
+
|
|
197
|
+
export const CostLineSchema = z
|
|
198
|
+
.looseObject({ session_id: z.string(), total_cost_usd: z.number().optional(), cost: z.looseObject({ total_cost_usd: z.number() }).nullable().optional() })
|
|
199
|
+
.transform((line): SessionCost | null => {
|
|
200
|
+
const usd = line.total_cost_usd ?? line.cost?.total_cost_usd;
|
|
201
|
+
return usd == null ? null : { sessionId: line.session_id, usd };
|
|
202
|
+
});
|
|
203
|
+
|
|
175
204
|
export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
|
|
176
205
|
|
|
177
206
|
export const InstantSchema = z.iso.datetime({ offset: true }).transform((iso) => Date.parse(iso));
|
package/src/lib/usage.ts
CHANGED
|
@@ -105,7 +105,7 @@ export function transcriptRowText(row: TranscriptRow): string {
|
|
|
105
105
|
return blocks.data.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export const ENFORCED_ERRORS = ["rate_limit", "oauth_org_not_allowed"];
|
|
108
|
+
export const ENFORCED_ERRORS = ["rate_limit", "oauth_org_not_allowed", "billing_error"];
|
|
109
109
|
|
|
110
110
|
export function findEnforcedRow(input: { rows: TranscriptRow[]; error: string; lastAssistantMessage: string | undefined }): TranscriptRow | null {
|
|
111
111
|
const { rows, error, lastAssistantMessage } = input;
|
package/src/main.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { runPiSupervisor } from "./entries/pisupervisor.ts";
|
|
|
13
13
|
import { claude } from "./lib/claude.ts";
|
|
14
14
|
import { codex } from "./lib/codex.ts";
|
|
15
15
|
import { grok } from "./lib/grok.ts";
|
|
16
|
-
import { opencodeGo } from "./lib/opencodego.ts";
|
|
17
16
|
import { cmdInit, cmdInitPi } from "./cli/init.ts";
|
|
18
17
|
import { cmdAdd } from "./cli/add.ts";
|
|
19
18
|
import { cmdAuth } from "./cli/auth.ts";
|
|
@@ -21,6 +20,7 @@ import { cmdStatus } from "./cli/status.ts";
|
|
|
21
20
|
import { cmdDoctor } from "./cli/doctor.ts";
|
|
22
21
|
import { cmdRm } from "./cli/rm.ts";
|
|
23
22
|
import { cmdRename } from "./cli/rename.ts";
|
|
23
|
+
import { cmdKey } from "./cli/key.ts";
|
|
24
24
|
import { cmdCheck } from "./cli/check.ts";
|
|
25
25
|
import { cmdConfig } from "./cli/config.ts";
|
|
26
26
|
import { cmdSeat } from "./cli/seat.ts";
|
|
@@ -28,19 +28,18 @@ import { cmdServe } from "./cli/serve.ts";
|
|
|
28
28
|
import { CHECK_JOB, deactivationHint, HUB_JOB, onLoginHome, uninstallSupervisor, uninstallTargets } from "./lib/install.ts";
|
|
29
29
|
import { errorMessage } from "./lib/log.ts";
|
|
30
30
|
import { HOME } from "./lib/paths.ts";
|
|
31
|
-
import { c, emitError } from "./cli/render.ts";
|
|
31
|
+
import { c, count, emitError } from "./cli/render.ts";
|
|
32
32
|
|
|
33
33
|
const JSON_FLAG = "--json";
|
|
34
34
|
const CACHED_FLAG = "--cached";
|
|
35
35
|
const YES_FLAG = "--yes";
|
|
36
36
|
const CODEX_FLAG = "--codex";
|
|
37
37
|
const GROK_FLAG = "--grok";
|
|
38
|
-
const OPENCODE_GO_FLAG = "--opencode-go";
|
|
39
38
|
const PI_FLAG = "--pi";
|
|
40
39
|
const JSON_COMMANDS = new Set(["status", "config", "check"]);
|
|
41
40
|
const CODEX_COMMANDS = new Set(["init", "add", "auth", "rm", "rename", "seat"]);
|
|
42
41
|
const STATUS_ONLY_COMMANDS = new Set(["init", "add", "auth", "rm", "rename"]);
|
|
43
|
-
const
|
|
42
|
+
const MAX_OPERANDS = new Map([["init", 0], ["add", 0], ["rm", 1], ["rename", 2], ["key", 4]]);
|
|
44
43
|
|
|
45
44
|
function printHelp(): void {
|
|
46
45
|
console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
|
|
@@ -51,19 +50,20 @@ function printHelp(): void {
|
|
|
51
50
|
${c.cyan("tokenmaxxing init --codex")} same for codex: log in the first account, isolated, install codex supervisor + Stop hook
|
|
52
51
|
${c.cyan("tokenmaxxing init --pi")} install the pi supervisor: pi sessions run on pooled Claude or ChatGPT accounts that are logged into pi
|
|
53
52
|
${c.cyan("tokenmaxxing init --grok")} pool grok Build logins (status-only: no supervisor yet)
|
|
54
|
-
${c.cyan("tokenmaxxing init --opencode-go")} pool opencode-go API keys (status-only: no supervisor yet)
|
|
55
53
|
${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
|
|
56
|
-
${c.cyan("tokenmaxxing add")} --codex | --grok
|
|
57
|
-
${c.cyan("tokenmaxxing auth")} [--codex | --grok
|
|
54
|
+
${c.cyan("tokenmaxxing add")} --codex | --grok same for the codex or grok pool
|
|
55
|
+
${c.cyan("tokenmaxxing auth")} [--codex | --grok] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
|
|
58
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)
|
|
59
57
|
${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
|
|
60
58
|
${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)
|
|
61
59
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
62
|
-
${c.cyan("tokenmaxxing rename")} [--codex | --grok
|
|
63
|
-
${c.cyan("tokenmaxxing rm")} [--codex | --grok
|
|
60
|
+
${c.cyan("tokenmaxxing rename")} [--codex | --grok] <sel> <label>
|
|
61
|
+
${c.cyan("tokenmaxxing rm")} [--codex | --grok] <sel>
|
|
62
|
+
${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic API key read from stdin; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces)
|
|
63
|
+
${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count
|
|
64
|
+
${c.cyan("tokenmaxxing key rm")} <label> delete the key and its stored secret (refused while a session runs on it)
|
|
64
65
|
${c.cyan("tokenmaxxing seat")} <pid> lend one pooled Claude account to an unattended consumer until <pid> exits: prints the store directory to set as CLAUDE_SECURESTORAGE_CONFIG_DIR (the directory itself, as its owner, never a copy of its credential); host sessions and other borrowers share the account; exit 1 = none usable
|
|
65
66
|
${c.cyan("tokenmaxxing seat --codex")} <pid> borrow one pooled codex account for an unattended consumer (plugin, script): prints the CODEX_HOME to set, lent to <pid> until it exits; codex sessions and other borrowers share the account, and a refresh race between them can sign it out until auth --codex runs again; exit 1 = none usable, fall back to the ambient login
|
|
66
|
-
${c.cyan("tokenmaxxing seat --opencode-go")} <pid> lend one pooled opencode-go key to an unattended consumer until <pid> exits: prints the store directory, whose auth.json is an opencode auth file (mount it at $XDG_DATA_HOME/opencode/auth.json, then run a model such as opencode-go/mimo-v2.6-pro); other borrowers share the key; exit 1 = none usable
|
|
67
67
|
${c.cyan("tokenmaxxing serve")} serve the CLIProxyAPI-compatible usage API on http://localhost:<hub.port> (default 8317) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows every pooled Claude and Codex account's quota; the management key is the contents of hub-key in the state directory
|
|
68
68
|
${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
|
|
69
69
|
|
|
@@ -97,14 +97,14 @@ async function main(): Promise<number> {
|
|
|
97
97
|
const json = jsonMode;
|
|
98
98
|
const cached = argv.includes(CACHED_FLAG);
|
|
99
99
|
const yes = argv.includes(YES_FLAG);
|
|
100
|
-
const providerFlags = [CODEX_FLAG, GROK_FLAG
|
|
100
|
+
const providerFlags = [CODEX_FLAG, GROK_FLAG].filter((f) => argv.includes(f));
|
|
101
101
|
if (providerFlags.length > 1) {
|
|
102
102
|
emitError({ json, message: `${providerFlags.join(" and ")} are mutually exclusive - pick one pool` });
|
|
103
103
|
return 2;
|
|
104
104
|
}
|
|
105
|
-
const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok :
|
|
105
|
+
const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : claude;
|
|
106
106
|
const pi = argv.includes(PI_FLAG);
|
|
107
|
-
const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !==
|
|
107
|
+
const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== PI_FLAG);
|
|
108
108
|
const sub = args[0];
|
|
109
109
|
|
|
110
110
|
if (pi && sub !== "init" && sub !== "auth") {
|
|
@@ -132,19 +132,21 @@ async function main(): Promise<number> {
|
|
|
132
132
|
return 2;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (provider === grok
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (sub == null || !commands.has(sub)) {
|
|
139
|
-
emitError({ json, message: `${flag} applies to ${[...commands].join(", ")}, not ${sub ?? "status"}` });
|
|
140
|
-
return 2;
|
|
141
|
-
}
|
|
135
|
+
if (provider === grok && (sub == null || !STATUS_ONLY_COMMANDS.has(sub))) {
|
|
136
|
+
emitError({ json, message: `${GROK_FLAG} applies to ${[...STATUS_ONLY_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
|
|
137
|
+
return 2;
|
|
142
138
|
}
|
|
143
139
|
|
|
144
140
|
if (json && sub != null && !JSON_COMMANDS.has(sub)) {
|
|
145
141
|
emitError({ json, message: `${sub} has no ${JSON_FLAG} form (${JSON_FLAG} applies to ${[...JSON_COMMANDS].join(", ")})` });
|
|
146
142
|
return 2;
|
|
147
143
|
}
|
|
144
|
+
|
|
145
|
+
const maxOperands = sub == null ? undefined : MAX_OPERANDS.get(sub);
|
|
146
|
+
if (maxOperands != null && args.length - 1 > maxOperands) {
|
|
147
|
+
emitError({ json, message: `${sub} takes at most ${count({ n: maxOperands, noun: "argument" })}, got: ${args.slice(1).join(" ")}` });
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
148
150
|
switch (sub) {
|
|
149
151
|
case "__statusline": return runStatusline();
|
|
150
152
|
case "__subagent-statusline": return runSubagentStatusline();
|
|
@@ -175,7 +177,8 @@ async function main(): Promise<number> {
|
|
|
175
177
|
case "doctor": return cmdDoctor();
|
|
176
178
|
case "rm": return cmdRm(provider, args[1]);
|
|
177
179
|
case "rename": return cmdRename(provider, args.slice(1));
|
|
178
|
-
case "
|
|
180
|
+
case "key": return cmdKey(args.slice(1));
|
|
181
|
+
case "seat": return cmdSeat(provider === codex ? "codex" : "claude", args[1], args.slice(2));
|
|
179
182
|
case "serve": return cmdServe(args.slice(1));
|
|
180
183
|
case "uninstall": {
|
|
181
184
|
if (args.length > 1) {
|
|
@@ -200,7 +203,7 @@ async function main(): Promise<number> {
|
|
|
200
203
|
if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
|
|
201
204
|
if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
|
|
202
205
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
203
|
-
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores
|
|
206
|
+
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/), plus api-keys.json and the API keys in api-keys/ - remove accounts with \`xx rm\` and keys with \`xx key rm\` to delete their credentials`);
|
|
204
207
|
return 0;
|
|
205
208
|
}
|
|
206
209
|
case "help":
|
package/src/lib/opencodego.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { countBy, minBy } from "es-toolkit";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
-
import { env, HOME, opencodeGoAuthJsonFor, opencodeGoPaths, opencodeGoPool, opencodeGoStoreDirFor } from "./paths.ts";
|
|
6
|
-
import { withLock } from "./lock.ts";
|
|
7
|
-
import { log } from "./log.ts";
|
|
8
|
-
import { livingPresences, seatCounts, writePresence } from "./presence.ts";
|
|
9
|
-
import type { Provider, SeatBorrow } from "./provider.ts";
|
|
10
|
-
import { loadAccounts, readJsonFile, type Harvest } from "./state.ts";
|
|
11
|
-
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
12
|
-
import { ErrnoSchema } from "./types.ts";
|
|
13
|
-
import { c } from "../cli/render.ts";
|
|
14
|
-
|
|
15
|
-
const OpencodeAuthEntrySchema = z.looseObject({ type: z.string(), key: z.string().optional() });
|
|
16
|
-
const OpencodeAuthFileSchema = z.record(z.string(), z.unknown());
|
|
17
|
-
|
|
18
|
-
function liveAuthPath(): string {
|
|
19
|
-
return join(env("XDG_DATA_HOME", join(HOME, ".local", "share")), "opencode", "auth.json");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function idOfKey(key: string): string {
|
|
23
|
-
const h = new Bun.CryptoHasher("sha256");
|
|
24
|
-
h.update(`opencode-go:${key}`);
|
|
25
|
-
return h.digest("hex");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function harvestOf(key: string): Harvest {
|
|
29
|
-
const id = idOfKey(key);
|
|
30
|
-
return {
|
|
31
|
-
id,
|
|
32
|
-
email: null,
|
|
33
|
-
tier: "go",
|
|
34
|
-
sample: null,
|
|
35
|
-
park: async () => {
|
|
36
|
-
writeFileAtomic(opencodeGoAuthJsonFor(id), JSON.stringify({ "opencode-go": { type: "api", key } }, null, 2), 0o600);
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function readAuth(path: string): AuthEntry[] {
|
|
42
|
-
let map: z.infer<typeof OpencodeAuthFileSchema>;
|
|
43
|
-
try {
|
|
44
|
-
map = readJsonFile(path, OpencodeAuthFileSchema);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return [];
|
|
47
|
-
throw e;
|
|
48
|
-
}
|
|
49
|
-
const entry = OpencodeAuthEntrySchema.safeParse(map["opencode-go"]);
|
|
50
|
-
if (!entry.success || entry.data.type !== "api" || !entry.data.key) return [];
|
|
51
|
-
return [{ id: idOfKey(entry.data.key), usable: true, harvest: harvestOf(entry.data.key) }];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function borrowOpencodeGoSeat(pid: number): Promise<SeatBorrow> {
|
|
55
|
-
const seatId = `seat-${pid}`;
|
|
56
|
-
const granted = await withLock(opencodeGoPool.lockFile, async (): Promise<SeatBorrow> => {
|
|
57
|
-
const accounts = loadAccounts(opencodeGoPool).accounts;
|
|
58
|
-
const living = livingPresences(opencodeGoPaths.presenceDir);
|
|
59
|
-
const held = living.find((p) => p.id === seatId);
|
|
60
|
-
const heldAccount = held ? (accounts.find((a) => a.id === held.accountId) ?? null) : null;
|
|
61
|
-
if (heldAccount) {
|
|
62
|
-
if (!(await opencodeGo.storeUsable(heldAccount))) {
|
|
63
|
-
return { denied: "the key this pid holds is no longer usable in its store - run `tokenmaxxing auth --opencode-go` and borrow again" };
|
|
64
|
-
}
|
|
65
|
-
return { store: opencodeGoStoreDirFor(heldAccount.id), id: heldAccount.id, reused: true };
|
|
66
|
-
}
|
|
67
|
-
const lent = countBy(living, (p) => p.accountId);
|
|
68
|
-
const usable = await Promise.all(accounts.map(opencodeGo.storeUsable));
|
|
69
|
-
const picked = minBy(accounts.filter((_, i) => usable[i]), (a) => lent[a.id] ?? 0);
|
|
70
|
-
if (!picked) return null;
|
|
71
|
-
writePresence({ dir: opencodeGoPaths.presenceDir, id: seatId, accountId: picked.id, pid });
|
|
72
|
-
return { store: opencodeGoStoreDirFor(picked.id), id: picked.id, reused: false };
|
|
73
|
-
});
|
|
74
|
-
if (granted && !("denied" in granted)) log("seat.grant", { account: granted.id.slice(0, 8), pid, reused: granted.reused });
|
|
75
|
-
return granted;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const opencodeGo: Provider = statusOnlyProvider({
|
|
79
|
-
name: "opencode-go",
|
|
80
|
-
flag: " --opencode-go",
|
|
81
|
-
pool: opencodeGoPool,
|
|
82
|
-
binName: "opencode",
|
|
83
|
-
binKey: "opencodeBin",
|
|
84
|
-
storeDirFor: opencodeGoStoreDirFor,
|
|
85
|
-
authJsonFor: opencodeGoAuthJsonFor,
|
|
86
|
-
onboardDir: opencodeGoPaths.onboardDir,
|
|
87
|
-
homeEnv: "XDG_DATA_HOME",
|
|
88
|
-
loginArgs: ["providers", "login", "-p", "opencode-go"],
|
|
89
|
-
onboardAuthRel: join("opencode", "auth.json"),
|
|
90
|
-
liveAuthPath,
|
|
91
|
-
readAuth,
|
|
92
|
-
liveId: () => null,
|
|
93
|
-
presence: () => seatCounts(opencodeGoPaths.presenceDir),
|
|
94
|
-
versionOk: (out) => out !== "",
|
|
95
|
-
importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
|
|
96
|
-
foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
|
|
97
|
-
installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
|
|
98
|
-
loginStep: () => `Paste the opencode-go API key from ${c.bold("opencode.ai/zen")} when opencode asks for it.`,
|
|
99
|
-
});
|