tokenmaxxing 1.57.0 → 1.58.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.
Files changed (53) hide show
  1. package/DESIGN.md +6 -6
  2. package/README.md +3 -3
  3. package/package.json +1 -1
  4. package/src/cli/auth.ts +6 -17
  5. package/src/cli/check.ts +2 -2
  6. package/src/cli/cloudrun.ts +12 -19
  7. package/src/cli/config.ts +2 -1
  8. package/src/cli/doctor.ts +5 -4
  9. package/src/cli/init.ts +0 -1
  10. package/src/cli/render.ts +24 -9
  11. package/src/cli/rm.ts +0 -4
  12. package/src/cli/status.ts +34 -57
  13. package/src/entries/codexstophook.ts +10 -23
  14. package/src/entries/codexsupervisor.ts +30 -72
  15. package/src/entries/statusline.ts +17 -26
  16. package/src/entries/stopfailurehook.ts +4 -5
  17. package/src/entries/stophook.ts +12 -10
  18. package/src/entries/subagentstatusline.ts +4 -10
  19. package/src/entries/supervisor.ts +51 -103
  20. package/src/lib/claude.ts +11 -12
  21. package/src/lib/claudebin.ts +3 -2
  22. package/src/lib/claudelock.ts +2 -2
  23. package/src/lib/cloud.ts +4 -8
  24. package/src/lib/codex.ts +3 -3
  25. package/src/lib/codexauth.ts +2 -11
  26. package/src/lib/codexbin.ts +2 -1
  27. package/src/lib/codexoauth.ts +3 -8
  28. package/src/lib/codexusage.ts +5 -5
  29. package/src/lib/compact.ts +5 -18
  30. package/src/lib/credstore.ts +1 -6
  31. package/src/lib/decide.ts +6 -24
  32. package/src/lib/grok.ts +38 -152
  33. package/src/lib/http.ts +1 -2
  34. package/src/lib/install.ts +20 -38
  35. package/src/lib/keychain.ts +1 -4
  36. package/src/lib/log.ts +5 -5
  37. package/src/lib/oauth.ts +6 -5
  38. package/src/lib/opencodego.ts +31 -140
  39. package/src/lib/paths.ts +6 -22
  40. package/src/lib/picker.ts +9 -20
  41. package/src/lib/presence.ts +4 -5
  42. package/src/lib/proc.ts +22 -0
  43. package/src/lib/sample.ts +25 -24
  44. package/src/lib/sessions.ts +5 -6
  45. package/src/lib/settings.ts +1 -8
  46. package/src/lib/state.ts +6 -29
  47. package/src/lib/statusonly.ts +143 -0
  48. package/src/lib/supervise.ts +111 -0
  49. package/src/lib/types.ts +27 -36
  50. package/src/lib/update.ts +6 -7
  51. package/src/lib/usage.ts +40 -191
  52. package/src/main.ts +5 -5
  53. package/src/entries/sessionstart.ts +0 -33
@@ -2,21 +2,14 @@ import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileS
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { escape } from "es-toolkit";
4
4
  import { z } from "zod";
5
- import { codexPaths, codexStoreDirFor, HOME, paths } from "./paths.ts";
5
+ import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.ts";
6
6
  import { writeFileAtomic } from "./atomic.ts";
7
7
  import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
8
8
  import { resolveRealClaude } from "./claudebin.ts";
9
9
  import { loadConfig } from "./state.ts";
10
10
  import { ErrnoSchema } from "./types.ts";
11
11
 
12
- const InstallOutcomeSchema = z.object({
13
- claudeWrapper: z.string(),
14
- installedBin: z.string(),
15
- pathAhead: z.boolean(),
16
- timerLoaded: z.boolean(),
17
- checkIntervalS: z.number().int().positive(),
18
- });
19
- export type InstallOutcome = z.infer<typeof InstallOutcomeSchema>;
12
+ export type InstallOutcome = { claudeWrapper: string; installedBin: string; pathAhead: boolean; timerLoaded: boolean; checkIntervalS: number };
20
13
 
21
14
  export function isBinDirAhead(): boolean {
22
15
  const dirs = (process.env.PATH ?? "").split(":");
@@ -31,10 +24,10 @@ export function isBinDirAhead(): boolean {
31
24
  }
32
25
  }
33
26
 
34
- const EnvFlagSchema = z.enum(["1", "true", "yes"]).optional().catch(undefined);
27
+ const envFlag = (name: string): boolean => z.stringbool().optional().parse(optionalEnv(name)) === true;
35
28
 
36
29
  export function isNixPackaged(): boolean {
37
- if (EnvFlagSchema.parse(process.env.TOKENMAXXING_NIX) != null) return true;
30
+ if (envFlag("TOKENMAXXING_NIX")) return true;
38
31
  try {
39
32
  return realpathSync(Bun.main).startsWith("/nix/store/");
40
33
  } catch {
@@ -43,7 +36,7 @@ export function isNixPackaged(): boolean {
43
36
  }
44
37
 
45
38
  export function skipImperativeTimer(): boolean {
46
- return EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_TIMER) != null;
39
+ return envFlag("TOKENMAXXING_SKIP_TIMER");
47
40
  }
48
41
 
49
42
  function isNixStorePath(path: string): boolean {
@@ -51,7 +44,7 @@ function isNixStorePath(path: string): boolean {
51
44
  }
52
45
 
53
46
  function cannotWriteRcTarget(target: string): boolean {
54
- if (EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_SHELL_RC) != null) return true;
47
+ if (envFlag("TOKENMAXXING_SKIP_SHELL_RC")) return true;
55
48
  if (isNixStorePath(target)) return true;
56
49
  if (!existsSync(target)) return false;
57
50
  try {
@@ -177,26 +170,21 @@ export function codexStopHookGroupIndex(): number | null {
177
170
  return idx >= 0 ? idx : null;
178
171
  }
179
172
 
173
+ const CodexHookStateSchema = z.looseObject({
174
+ hooks: z.looseObject({ state: z.record(z.string(), z.looseObject({ trusted_hash: z.unknown().optional() })).optional() }).optional(),
175
+ });
176
+
180
177
  export function codexStoreHookTrust(accountId: string): "trusted" | "untrusted" | "unknown" {
181
178
  const group = codexStopHookGroupIndex();
182
179
  if (group == null) return "unknown";
183
180
  const key = `${join(codexStoreDirFor(accountId), "hooks.json")}:stop:${group}:0`;
184
- let text: string;
181
+ let config: z.infer<typeof CodexHookStateSchema>;
185
182
  try {
186
- text = readFileSync(join(codexPaths.home, "config.toml"), "utf8");
183
+ config = CodexHookStateSchema.parse(Bun.TOML.parse(readFileSync(join(codexPaths.home, "config.toml"), "utf8")));
187
184
  } catch {
188
185
  return "unknown";
189
186
  }
190
- let inBlock = false;
191
- for (const line of text.split("\n")) {
192
- const trimmed = line.trim();
193
- if (trimmed.startsWith("[")) {
194
- inBlock = trimmed.startsWith("[hooks.state.") && trimmed.includes(key);
195
- } else if (inBlock && trimmed.startsWith("trusted_hash")) {
196
- return "trusted";
197
- }
198
- }
199
- return "untrusted";
187
+ return config.hooks?.state?.[key]?.trusted_hash !== undefined ? "trusted" : "untrusted";
200
188
  }
201
189
 
202
190
  export function codexSupervisorLink(): string {
@@ -375,8 +363,8 @@ function uninstallCheckTimer(live: boolean): TimerOutcome {
375
363
  }
376
364
 
377
365
  export function shellRcPath(): string | null {
378
- const override = process.env.TOKENMAXXING_SHELL_RC;
379
- if (override && override.length > 0) return override;
366
+ const override = optionalEnv("TOKENMAXXING_SHELL_RC");
367
+ if (override != null) return override;
380
368
  const shell = basename(process.env.SHELL ?? "");
381
369
  if (shell === "zsh") return join(process.env.ZDOTDIR || HOME, ".zshrc");
382
370
  if (shell === "bash") return join(HOME, ".bashrc");
@@ -417,12 +405,7 @@ export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
417
405
  return "added";
418
406
  }
419
407
 
420
- const ShellShadowerSchema = z.object({
421
- kind: z.enum(["shadow", "bypass"]),
422
- name: z.string(),
423
- line: z.string(),
424
- });
425
- export type ShellShadower = z.infer<typeof ShellShadowerSchema>;
408
+ export type ShellShadower = { kind: "shadow" | "bypass"; name: string; line: string };
426
409
 
427
410
  export function findClaudeShadowers(rcText: string): ShellShadower[] {
428
411
  const out: ShellShadower[] = [];
@@ -433,14 +416,14 @@ export function findClaudeShadowers(rcText: string): ShellShadower[] {
433
416
  const alias = line.match(/^alias\s+([A-Za-z0-9_-]+)=(.*)$/);
434
417
  if (alias) {
435
418
  if (alias[1] === "claude") {
436
- out.push(ShellShadowerSchema.parse({ kind: "shadow", name: "claude", line }));
419
+ out.push({ kind: "shadow", name: "claude", line });
437
420
  } else if (absClaude.test(alias[2]!)) {
438
- out.push(ShellShadowerSchema.parse({ kind: "bypass", name: alias[1]!, line }));
421
+ out.push({ kind: "bypass", name: alias[1]!, line });
439
422
  }
440
423
  continue;
441
424
  }
442
425
  if (/^(?:function\s+)?claude\s*\(\)/.test(line)) {
443
- out.push(ShellShadowerSchema.parse({ kind: "shadow", name: "claude", line }));
426
+ out.push({ kind: "shadow", name: "claude", line });
444
427
  }
445
428
  }
446
429
  return out;
@@ -462,8 +445,7 @@ export function removePathFromRc(rc: string): boolean {
462
445
  return true;
463
446
  }
464
447
 
465
- const UninstallOutcomeSchema = z.object({ timer: z.enum(["removed", "skipped", "still-loaded"]), pathLineRemoved: z.boolean() });
466
- export type UninstallOutcome = z.infer<typeof UninstallOutcomeSchema>;
448
+ export type UninstallOutcome = { timer: "removed" | "skipped" | "still-loaded"; pathLineRemoved: boolean };
467
449
 
468
450
  export function loginHome(): string {
469
451
  const cmd = process.platform === "darwin" ? ["id", "-P"] : ["getent", "passwd", String(process.getuid?.() ?? "")];
@@ -1,7 +1,4 @@
1
- import { z } from "zod";
2
-
3
- const KeychainTargetSchema = z.object({ service: z.string(), account: z.string() });
4
- export type KeychainTarget = z.infer<typeof KeychainTargetSchema>;
1
+ export type KeychainTarget = { service: string; account: string };
5
2
 
6
3
  const SECURITY = "/usr/bin/security";
7
4
  const INTERACTIVE_MAX_LINE = 4000;
package/src/lib/log.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { z } from "zod";
4
3
  import { paths } from "./paths.ts";
5
4
 
6
5
  const LOG_MAX_BYTES = 5_000_000;
7
6
 
7
+ export function errorMessage(e: unknown): string {
8
+ return e instanceof Error ? e.message : String(e);
9
+ }
10
+
8
11
  function redact(s: string): string {
9
12
  return s
10
13
  .replace(/\b(sk-ant-[A-Za-z0-9._-]{6,})/g, "sk-ant-***")
@@ -22,10 +25,7 @@ export function log(event: string, fields: Record<string, unknown> = {}): void {
22
25
  try {
23
26
  line = Object.entries(fields)
24
27
  .filter(([, v]) => v !== undefined)
25
- .map(([k, v]) => {
26
- const str = z.string().safeParse(v);
27
- return `${k}=${redact(str.success ? str.data : JSON.stringify(v))}`;
28
- })
28
+ .map(([k, v]) => `${k}=${redact(typeof v === "string" ? v : JSON.stringify(v))}`)
29
29
  .join(" ");
30
30
  mkdirSync(dirname(paths.logFile), { recursive: true });
31
31
  if (existsSync(paths.logFile) && statSync(paths.logFile).size > LOG_MAX_BYTES) {
package/src/lib/oauth.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { http, safeErrorDetail } from "./http.ts";
2
2
  import { env } from "./paths.ts";
3
- import { JsonTextSchema, ProfileResponseSchema, TokenIdentitySchema, type OAuthCreds, type TokenIdentity } from "./types.ts";
3
+ import { errorMessage } from "./log.ts";
4
+ import { JsonTextSchema, ProfileResponseSchema, type OAuthCreds, type TokenIdentity } from "./types.ts";
4
5
 
5
6
  const PROFILE_URL = env("TOKENMAXXING_OAUTH_PROFILE_URL", "https://api.anthropic.com/api/oauth/profile");
6
7
 
@@ -41,23 +42,23 @@ export async function fetchTokenIdentity(accessToken: string, signal?: AbortSign
41
42
  signal,
42
43
  });
43
44
  } catch (e) {
44
- throw new IdentityUnavailableError(null, e instanceof Error ? e.message : String(e));
45
+ throw new IdentityUnavailableError(null, errorMessage(e));
45
46
  }
46
47
  let text: string;
47
48
  try {
48
49
  text = await res.text();
49
50
  } catch (e) {
50
- throw new IdentityUnavailableError(res.status, `profile response body unreadable: ${e instanceof Error ? e.message : String(e)}`);
51
+ throw new IdentityUnavailableError(res.status, `profile response body unreadable: ${errorMessage(e)}`);
51
52
  }
52
53
  if (!res.ok) throw new IdentityUnavailableError(res.status, safeErrorDetail({ text }));
53
54
  const parsed = ProfileResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
54
55
  if (!parsed.success) throw new IdentityUnavailableError(res.status, `profile endpoint returned an unrecognized body (${text.length} bytes, withheld)`);
55
- return TokenIdentitySchema.parse({
56
+ return {
56
57
  accountUuid: parsed.data.account.uuid,
57
58
  email: parsed.data.account.email ?? null,
58
59
  organizationUuid: parsed.data.organization.uuid,
59
60
  organizationName: parsed.data.organization.name ?? null,
60
- });
61
+ };
61
62
  }
62
63
 
63
64
  export function describeIdentity(id: TokenIdentity): string {
@@ -1,12 +1,12 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
5
  import { writeFileAtomic } from "./atomic.ts";
6
6
  import { opencodeGoAuthJsonFor, opencodeGoPaths, opencodeGoPool, opencodeGoStoreDirFor } from "./paths.ts";
7
- import type { Observation, Provider, SampleReport } from "./provider.ts";
8
- import { loadConfig, pinBinOverride, type Harvest } from "./state.ts";
9
- import type { Account } from "./types.ts";
7
+ import type { Provider } from "./provider.ts";
8
+ import { type Harvest } from "./state.ts";
9
+ import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
10
10
  import { c } from "../cli/render.ts";
11
11
 
12
12
  const OpencodeAuthEntrySchema = z.looseObject({ type: z.string(), key: z.string().optional() });
@@ -20,77 +20,12 @@ function liveAuthPath(): string {
20
20
  return join(dataHome(), "opencode", "auth.json");
21
21
  }
22
22
 
23
- function parseAuthFile(path: string): { provider: string; key: string } | null {
24
- let raw: unknown;
25
- try {
26
- raw = JSON.parse(readFileSync(path, "utf8"));
27
- } catch {
28
- return null;
29
- }
30
- const map = OpencodeAuthFileSchema.safeParse(raw);
31
- if (!map.success) return null;
32
- const entry = OpencodeAuthEntrySchema.safeParse(map.data["opencode-go"]);
33
- if (!entry.success || entry.data.type !== "api" || !entry.data.key) return null;
34
- return { provider: "opencode-go", key: entry.data.key };
35
- }
36
-
37
23
  function idOfKey(key: string): string {
38
24
  const h = new Bun.CryptoHasher("sha256");
39
25
  h.update(`opencode-go:${key}`);
40
26
  return `opencode-go-${h.digest("hex")}`;
41
27
  }
42
28
 
43
- function resolveRealOpencode(): string {
44
- const cfg = loadConfig();
45
- if (cfg.opencodeBin) {
46
- if (!existsSync(cfg.opencodeBin)) throw new Error(`configured opencodeBin does not exist: ${cfg.opencodeBin} - fix config.json`);
47
- return cfg.opencodeBin;
48
- }
49
- for (const d of (process.env.PATH ?? "").split(":")) {
50
- if (!d) continue;
51
- const cand = join(d, "opencode");
52
- try {
53
- if (existsSync(cand)) return cand;
54
- } catch {
55
- continue;
56
- }
57
- }
58
- throw new Error("could not locate the real `opencode` binary (set opencodeBin in config.json)");
59
- }
60
-
61
- function liveId(): string | null {
62
- return null;
63
- }
64
-
65
- function presence(): Map<string, number> {
66
- return new Map();
67
- }
68
-
69
- async function observeLive(account: Account): Promise<Observation | null> {
70
- return account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
71
- }
72
-
73
- async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
74
- const reports = new Map<string, SampleReport>();
75
- for (const a of accounts) {
76
- reports.set(a.id, (await storeUsable(a)) ? { ok: true, source: "probe" } : { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --opencode-go`" });
77
- }
78
- return reports;
79
- }
80
-
81
- async function storeUsable(a: Account): Promise<boolean> {
82
- try {
83
- const found = parseAuthFile(opencodeGoAuthJsonFor(a.id));
84
- return found != null && idOfKey(found.key) === a.id;
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- async function removeCredentials(a: Account): Promise<void> {
91
- rmSync(opencodeGoStoreDirFor(a.id), { recursive: true, force: true });
92
- }
93
-
94
29
  function harvestOf(key: string): Harvest {
95
30
  const id = idOfKey(key);
96
31
  return {
@@ -105,82 +40,38 @@ function harvestOf(key: string): Harvest {
105
40
  };
106
41
  }
107
42
 
108
- async function login(): Promise<Harvest | null> {
109
- const real = resolveRealOpencode();
110
- const onboardDir = opencodeGoPaths.onboardDir;
111
- rmSync(onboardDir, { recursive: true, force: true });
112
- mkdirSync(onboardDir, { recursive: true });
113
- const p = Bun.spawn([real, "providers", "login", "-p", "opencode-go"], {
114
- stdin: "inherit",
115
- stdout: "inherit",
116
- stderr: "inherit",
117
- env: { ...process.env, XDG_DATA_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
118
- });
119
- await p.exited;
43
+ function readAuth(path: string): AuthEntry[] {
44
+ let raw: unknown;
120
45
  try {
121
- if (p.exitCode !== 0) {
122
- console.error(c.red("no opencode-go login landed in the isolated home - nothing added."));
123
- return null;
124
- }
125
- const found = parseAuthFile(join(onboardDir, "opencode", "auth.json"));
126
- if (!found) {
127
- console.error(c.red("no opencode-go login landed in the isolated home - nothing added."));
128
- return null;
129
- }
130
- return harvestOf(found.key);
131
- } finally {
132
- rmSync(onboardDir, { recursive: true, force: true });
133
- }
134
- }
135
-
136
- async function importLive(): Promise<Harvest | null> {
137
- console.log(c.cyan("Pooling your opencode-go API key - your existing opencode auth stays as it is."));
138
- console.log();
139
- const found = parseAuthFile(liveAuthPath());
140
- if (found) {
141
- console.log(c.dim(`found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`));
142
- return harvestOf(found.key);
143
- }
144
- return login();
145
- }
146
-
147
- function preflight(): void {
148
- const real = resolveRealOpencode();
149
- const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
150
- const out = (p.stdout?.toString() ?? "").trim();
151
- if (p.exitCode !== 0 || out === "") {
152
- throw new Error(`opencode binary failed verification: ${real}`);
46
+ raw = JSON.parse(readFileSync(path, "utf8"));
47
+ } catch {
48
+ return [];
153
49
  }
154
- pinBinOverride({ key: "opencodeBin", bin: real });
155
- }
156
-
157
- function install(): void {
158
- console.log(c.dim("opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys."));
50
+ const map = OpencodeAuthFileSchema.safeParse(raw);
51
+ if (!map.success) return [];
52
+ const entry = OpencodeAuthEntrySchema.safeParse(map.data["opencode-go"]);
53
+ if (!entry.success || entry.data.type !== "api" || !entry.data.key) return [];
54
+ return [{ id: idOfKey(entry.data.key), usable: true, harvest: harvestOf(entry.data.key) }];
159
55
  }
160
56
 
161
- export const opencodeGo: Provider = {
57
+ export const opencodeGo: Provider = statusOnlyProvider({
162
58
  name: "opencode-go",
163
59
  flag: " --opencode-go",
164
60
  pool: opencodeGoPool,
165
- seats: "live",
166
- waitsWhenDepleted: false,
167
- statusOnly: true,
168
- liveId,
169
- presence,
170
- gatedFamilies: () => null,
171
- observeLive,
172
- samplePool,
173
- mergeWindows: (next) => next,
174
- swap: async () => {
175
- throw new Error("opencode-go moves are not supported yet (status-only pool)");
176
- },
177
- classifySwapError: () => "fatal",
178
- removeCredentials,
179
- storeUsable,
180
- login,
181
- importLive,
182
- preflight,
183
- install,
61
+ binName: "opencode",
62
+ binKey: "opencodeBin",
63
+ storeDirFor: opencodeGoStoreDirFor,
64
+ authJsonFor: opencodeGoAuthJsonFor,
65
+ onboardDir: opencodeGoPaths.onboardDir,
66
+ homeEnv: "XDG_DATA_HOME",
67
+ loginArgs: ["providers", "login", "-p", "opencode-go"],
68
+ onboardAuthRel: join("opencode", "auth.json"),
69
+ liveAuthPath,
70
+ readAuth,
71
+ liveId: () => null,
72
+ versionOk: (out) => out.trim() !== "",
73
+ importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
74
+ foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
75
+ installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
184
76
  loginStep: () => `Paste the opencode-go API key from ${c.bold("opencode.ai/zen")} when opencode asks for it.`,
185
- windowLabel: (name) => name.toLowerCase(),
186
- };
77
+ });
package/src/lib/paths.ts CHANGED
@@ -6,8 +6,12 @@ const HOME = homedir();
6
6
 
7
7
  const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
8
8
 
9
+ export function optionalEnv(name: string, source: Record<string, string | undefined> = process.env): string | undefined {
10
+ return EnvOverrideSchema.parse(source[name]);
11
+ }
12
+
9
13
  export function env(name: string, fallback: string): string {
10
- return EnvOverrideSchema.parse(process.env[name]) ?? fallback;
14
+ return optionalEnv(name) ?? fallback;
11
15
  }
12
16
 
13
17
  const TM_HOME = env("TOKENMAXXING_HOME", join(HOME, ".config", "tokenmaxxing"));
@@ -42,29 +46,25 @@ export const paths = {
42
46
 
43
47
  export const claudePool = {
44
48
  accountsJson: join(TM_HOME, "accounts.json"),
45
- lastSwapJson: null,
46
49
  lockFile: join(TM_HOME, "lock"),
47
50
  } as const;
48
51
 
49
52
  export const codexPool = {
50
53
  accountsJson: join(TM_HOME, "codex-accounts.json"),
51
- lastSwapJson: null,
52
54
  lockFile: join(TM_HOME, "codex-lock"),
53
55
  } as const;
54
56
 
55
57
  export const grokPool = {
56
58
  accountsJson: join(TM_HOME, "grok-accounts.json"),
57
- lastSwapJson: null,
58
59
  lockFile: join(TM_HOME, "grok-lock"),
59
60
  } as const;
60
61
 
61
62
  export const opencodeGoPool = {
62
63
  accountsJson: join(TM_HOME, "opencode-go-accounts.json"),
63
- lastSwapJson: null,
64
64
  lockFile: join(TM_HOME, "opencode-go-lock"),
65
65
  } as const;
66
66
 
67
- export type PoolPaths = { accountsJson: string; lastSwapJson: string | null; lockFile: string };
67
+ export type PoolPaths = { accountsJson: string; lockFile: string };
68
68
 
69
69
  const CODEX_HOME = env("TOKENMAXXING_CODEX_HOME", env("CODEX_HOME", join(HOME, ".codex")));
70
70
 
@@ -159,20 +159,4 @@ export function namespacedCredService(configDirRaw: string): string {
159
159
  return `Claude Code-credentials-${h.digest("hex").slice(0, 8)}`;
160
160
  }
161
161
 
162
- export function realClaudeBinFromEnv(): string | undefined {
163
- return EnvOverrideSchema.parse(process.env.TOKENMAXXING_CLAUDE_BIN);
164
- }
165
-
166
- export function realCodexBinFromEnv(): string | undefined {
167
- return EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_BIN);
168
- }
169
-
170
- export function realGrokBinFromEnv(): string | undefined {
171
- return EnvOverrideSchema.parse(process.env.TOKENMAXXING_GROK_BIN);
172
- }
173
-
174
- export function realOpencodeBinFromEnv(): string | undefined {
175
- return EnvOverrideSchema.parse(process.env.TOKENMAXXING_OPENCODE_BIN);
176
- }
177
-
178
162
  export { HOME };
package/src/lib/picker.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { maxBy, minBy, sortBy } from "es-toolkit";
2
- import { z } from "zod";
3
2
  import { familyTokens } from "./usage.ts";
4
- import { AccountSchema, ThresholdsSchema, type Account, type Config, type Thresholds, type Window } from "./types.ts";
3
+ import type { Account, Config, Thresholds, Window } from "./types.ts";
5
4
 
6
5
  export function thresholdBars(cfg: Config): Thresholds {
7
6
  return {
@@ -10,14 +9,13 @@ export function thresholdBars(cfg: Config): Thresholds {
10
9
  };
11
10
  }
12
11
 
13
- const PickCtxSchema = z.object({
14
- now: z.number(),
15
- thresholds: ThresholdsSchema,
16
- currentId: z.string().nullable(),
17
- families: z.array(z.string()).nullable(),
18
- seats: z.map(z.string(), z.number()).nullable(),
19
- });
20
- export type PickCtx = z.infer<typeof PickCtxSchema>;
12
+ export type PickCtx = {
13
+ now: number;
14
+ thresholds: Thresholds;
15
+ currentId: string | null;
16
+ families: string[] | null;
17
+ seats: Map<string, number> | null;
18
+ };
21
19
 
22
20
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
23
21
  const SESSION_WINDOW_MAX_S = 6 * 3600;
@@ -123,21 +121,12 @@ export function pickBest(accounts: Account[], ctx: PickCtx): Account | null {
123
121
  return sortBy(usable, swapPreference(ctx))[0] ?? null;
124
122
  }
125
123
 
126
- export function currentWins(active: Account | null, accounts: Account[], ctx: PickCtx, margin = 1): boolean {
127
- if (!active || active.needsReauth || isExhausted(active, ctx)) return false;
128
- const best = pickBest(accounts, { ...ctx, currentId: null });
129
- if (best == null || best.id === active.id) return true;
130
- if (margin > 1) return pacePressure(best, ctx) <= pacePressure(active, ctx) * margin;
131
- return swapPreference(ctx).every((k) => k(active) === k(best));
132
- }
133
-
134
124
  export function usableAt(a: Account, ctx: PickCtx): number {
135
125
  const blocking = blockingUntil(a, ctx).filter((t) => t > ctx.now);
136
126
  return blocking.length ? Math.max(...blocking) : ctx.now;
137
127
  }
138
128
 
139
- const EarliestResetSchema = z.object({ account: AccountSchema, availableAt: z.number() });
140
- export type EarliestReset = z.infer<typeof EarliestResetSchema>;
129
+ export type EarliestReset = { account: Account; availableAt: number };
141
130
 
142
131
  export function pickEarliestReset(accounts: Account[], ctx: PickCtx): EarliestReset | null {
143
132
  const mapped = accounts
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { writeFileAtomic } from "./atomic.ts";
5
5
  import { pidExists, pidStartTime } from "./proc.ts";
6
+ import { ErrnoSchema } from "./types.ts";
6
7
 
7
8
  const PresenceSchema = z.object({
8
9
  accountId: z.string(),
@@ -21,8 +22,7 @@ export function clearPresence(input: { dir: string; id: string }): void {
21
22
  rmSync(join(input.dir, input.id), { force: true });
22
23
  }
23
24
 
24
- const LivingPresenceSchema = z.object({ id: z.string(), accountId: z.string() });
25
- export type LivingPresence = z.infer<typeof LivingPresenceSchema>;
25
+ export type LivingPresence = { id: string; accountId: string };
26
26
 
27
27
  export function livingPresences(dir: string): LivingPresence[] {
28
28
  const living: LivingPresence[] = [];
@@ -33,8 +33,7 @@ export function livingPresences(dir: string): LivingPresence[] {
33
33
  try {
34
34
  raw = readFileSync(file, "utf8");
35
35
  } catch (e) {
36
- const errno = z.object({ code: z.string() }).safeParse(e);
37
- if (errno.success && errno.data.code === "ENOENT") continue;
36
+ if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
38
37
  throw e;
39
38
  }
40
39
  const parsed = PresenceSchema.safeParse((() => {
@@ -55,7 +54,7 @@ export function livingPresences(dir: string): LivingPresence[] {
55
54
  rmSync(file, { force: true });
56
55
  continue;
57
56
  }
58
- living.push(LivingPresenceSchema.parse({ id: name, accountId: parsed.data.accountId }));
57
+ living.push({ id: name, accountId: parsed.data.accountId });
59
58
  }
60
59
  return living;
61
60
  }
package/src/lib/proc.ts CHANGED
@@ -7,6 +7,28 @@ export function pidStartTime(pid: number): string | null {
7
7
  return lstart === "" ? null : lstart;
8
8
  }
9
9
 
10
+ export async function readStdin(): Promise<string> {
11
+ const chunks: Uint8Array[] = [];
12
+ for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
13
+ return Buffer.concat(chunks).toString("utf8");
14
+ }
15
+
16
+ export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
17
+ const decoder = new TextDecoder();
18
+ let pending = "";
19
+ for await (const chunk of stream) {
20
+ pending += decoder.decode(chunk, { stream: true });
21
+ let nl = pending.indexOf("\n");
22
+ while (nl !== -1) {
23
+ yield pending.slice(0, nl);
24
+ pending = pending.slice(nl + 1);
25
+ nl = pending.indexOf("\n");
26
+ }
27
+ }
28
+ pending += decoder.decode();
29
+ if (pending.length > 0) yield pending;
30
+ }
31
+
10
32
  export function pidExists(pid: number): boolean {
11
33
  try {
12
34
  process.kill(pid, 0);