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/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}\``);
@@ -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
  }
@@ -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" | "opencode-go";
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;
@@ -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, piPaths } from "./paths.ts";
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, opencodeGoPaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex")];
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, opencodeGoPaths.presenceDir];
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" | "opencodeBin" | "piBin";
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())) : {};
@@ -14,7 +14,7 @@ export type StatusOnlySpec = {
14
14
  flag: string;
15
15
  pool: PoolPaths;
16
16
  binName: string;
17
- binKey: "grokBin" | "opencodeBin";
17
+ binKey: "grokBin";
18
18
  storeDirFor: (accountId: string) => string;
19
19
  authJsonFor: (accountId: string) => string;
20
20
  onboardDir: string;
@@ -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 OPENCODE_GO_COMMANDS = new Set([...STATUS_ONLY_COMMANDS, "seat"]);
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 | --opencode-go same for the codex, grok, or opencode-go pool
57
- ${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)
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 | --opencode-go] <sel> <label>
63
- ${c.cyan("tokenmaxxing rm")} [--codex | --grok | --opencode-go] <sel>
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, OPENCODE_GO_FLAG].filter((f) => argv.includes(f));
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 : argv.includes(OPENCODE_GO_FLAG) ? opencodeGo : claude;
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 !== OPENCODE_GO_FLAG && a !== PI_FLAG);
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 || provider === opencodeGo) {
136
- const flag = provider === grok ? GROK_FLAG : OPENCODE_GO_FLAG;
137
- const commands = provider === grok ? STATUS_ONLY_COMMANDS : OPENCODE_GO_COMMANDS;
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 "seat": return cmdSeat(provider === codex ? "codex" : provider === opencodeGo ? "opencode-go" : "claude", args[1], args.slice(2));
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/; opencode-go: opencode-go-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
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":
@@ -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
- });