tokenmaxxing 1.56.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 +7 -7
  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 +91 -116
  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
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { PoolPaths } from "./paths.ts";
4
+ import type { Provider, SampleReport } from "./provider.ts";
5
+ import { loadConfig, pinBinOverride, type Harvest } from "./state.ts";
6
+ import type { Account } from "./types.ts";
7
+ import { c } from "../cli/render.ts";
8
+
9
+ export type AuthEntry = { id: string; usable: boolean; harvest: Harvest | null };
10
+
11
+ export type StatusOnlySpec = {
12
+ name: Provider["name"];
13
+ flag: string;
14
+ pool: PoolPaths;
15
+ binName: string;
16
+ binKey: "grokBin" | "opencodeBin";
17
+ storeDirFor: (accountId: string) => string;
18
+ authJsonFor: (accountId: string) => string;
19
+ onboardDir: string;
20
+ homeEnv: string;
21
+ loginArgs: string[];
22
+ onboardAuthRel: string;
23
+ liveAuthPath: () => string;
24
+ readAuth: (path: string) => AuthEntry[];
25
+ liveId: () => string | null;
26
+ versionOk: (versionOutput: string) => boolean;
27
+ importIntro: string;
28
+ foundLive: (count: number) => string;
29
+ installNotice: string;
30
+ loginStep: () => string;
31
+ };
32
+
33
+ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
34
+ function resolveBin(): string {
35
+ const configured = loadConfig()[spec.binKey];
36
+ if (configured) {
37
+ if (!existsSync(configured)) throw new Error(`configured ${spec.binKey} does not exist: ${configured} - fix config.json`);
38
+ return configured;
39
+ }
40
+ for (const d of (process.env.PATH ?? "").split(":")) {
41
+ if (!d) continue;
42
+ const cand = join(d, spec.binName);
43
+ try {
44
+ if (existsSync(cand)) return cand;
45
+ } catch {
46
+ continue;
47
+ }
48
+ }
49
+ throw new Error(`could not locate the real \`${spec.binName}\` binary (set ${spec.binKey} in config.json)`);
50
+ }
51
+
52
+ async function storeUsable(a: Account): Promise<boolean> {
53
+ try {
54
+ return spec.readAuth(spec.authJsonFor(a.id)).some((e) => e.id === a.id && e.usable);
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
61
+ const reports = new Map<string, SampleReport>();
62
+ for (const a of accounts) {
63
+ 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${spec.flag}\`` });
64
+ }
65
+ return reports;
66
+ }
67
+
68
+ async function login(): Promise<Harvest | null> {
69
+ const real = resolveBin();
70
+ const onboardDir = spec.onboardDir;
71
+ rmSync(onboardDir, { recursive: true, force: true });
72
+ mkdirSync(onboardDir, { recursive: true });
73
+ const p = Bun.spawn([real, ...spec.loginArgs], {
74
+ stdin: "inherit",
75
+ stdout: "inherit",
76
+ stderr: "inherit",
77
+ env: { ...process.env, [spec.homeEnv]: onboardDir, TOKENMAXXING_PROBE: "1" },
78
+ });
79
+ await p.exited;
80
+ try {
81
+ const first = p.exitCode === 0 ? spec.readAuth(join(onboardDir, spec.onboardAuthRel))[0] : undefined;
82
+ if (!first) {
83
+ console.error(c.red(`no ${spec.name} login landed in the isolated home - nothing added.`));
84
+ return null;
85
+ }
86
+ return first.harvest;
87
+ } finally {
88
+ rmSync(onboardDir, { recursive: true, force: true });
89
+ }
90
+ }
91
+
92
+ async function importLive(): Promise<Harvest | null> {
93
+ console.log(c.cyan(spec.importIntro));
94
+ console.log();
95
+ const live = spec.readAuth(spec.liveAuthPath());
96
+ const first = live[0];
97
+ if (first) {
98
+ console.log(c.dim(spec.foundLive(live.length)));
99
+ if (first.harvest) return first.harvest;
100
+ }
101
+ return login();
102
+ }
103
+
104
+ function preflight(): void {
105
+ const real = resolveBin();
106
+ const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
107
+ if (p.exitCode !== 0 || !spec.versionOk(p.stdout?.toString() ?? "")) {
108
+ throw new Error(`${spec.binName} binary failed verification: ${real}`);
109
+ }
110
+ pinBinOverride({ key: spec.binKey, bin: real });
111
+ }
112
+
113
+ return {
114
+ name: spec.name,
115
+ flag: spec.flag,
116
+ pool: spec.pool,
117
+ seats: "live",
118
+ waitsWhenDepleted: false,
119
+ statusOnly: true,
120
+ liveId: spec.liveId,
121
+ presence: () => new Map(),
122
+ gatedFamilies: () => null,
123
+ observeLive: async (account) => (account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null),
124
+ samplePool,
125
+ mergeWindows: (next) => next,
126
+ swap: async () => {
127
+ throw new Error(`${spec.name} moves are not supported yet (status-only pool)`);
128
+ },
129
+ classifySwapError: () => "fatal",
130
+ removeCredentials: async (a) => {
131
+ rmSync(spec.storeDirFor(a.id), { recursive: true, force: true });
132
+ },
133
+ storeUsable,
134
+ login,
135
+ importLive,
136
+ preflight,
137
+ install: () => {
138
+ console.log(c.dim(spec.installNotice));
139
+ },
140
+ loginStep: spec.loginStep,
141
+ windowLabel: (name) => name.toLowerCase(),
142
+ };
143
+ }
@@ -0,0 +1,111 @@
1
+ import type { Subprocess } from "bun";
2
+ import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "./claudebin.ts";
3
+ import { errorMessage, log } from "./log.ts";
4
+ import { paths } from "./paths.ts";
5
+ import { writePresence } from "./presence.ts";
6
+ import { restoreTermios } from "./tty.ts";
7
+
8
+ export type WrappedProduct = "claude" | "codex";
9
+
10
+ type ChildHandle = {
11
+ pid: number;
12
+ exitCode: number | null;
13
+ signalCode: string | null;
14
+ kill: () => void;
15
+ exited: Promise<unknown>;
16
+ };
17
+
18
+ const WRAPPED: Record<WrappedProduct, { setting: string; binary: string; events: string }> = {
19
+ claude: { setting: "claudeBin", binary: "Claude", events: "supervisor" },
20
+ codex: { setting: "codexBin", binary: "codex", events: "codexsupervisor" },
21
+ };
22
+
23
+ export function exitStatus(child: { exitCode: number | null; signalCode: string | null }): number {
24
+ return child.exitCode ?? (child.signalCode ? 1 : 0);
25
+ }
26
+
27
+ export function loopGuardTripped(product: WrappedProduct): boolean {
28
+ const { setting, binary, events } = WRAPPED[product];
29
+ const depth = wrapDepth();
30
+ if (depth >= MAX_WRAP_DEPTH) {
31
+ console.error(
32
+ `tokenmaxxing: ${LOOP_DIAGNOSIS} ${binary} (depth ${depth}) - ${setting} in ${paths.configJson} does not launch the real ${binary} binary. Fix ${setting}, then run \`tokenmaxxing doctor\`.`,
33
+ );
34
+ log(`${events}.loop_abort`, { depth });
35
+ return true;
36
+ }
37
+ if (wrapperEntryRateTripped(Date.now())) {
38
+ console.error(
39
+ `tokenmaxxing: ${LOOP_DIAGNOSIS} ${binary} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - ${setting} in ${paths.configJson} does not launch the real ${binary} binary. Fix ${setting}, then run \`tokenmaxxing doctor\`.`,
40
+ );
41
+ log(`${events}.rate_abort`, { max: WRAP_RATE_MAX });
42
+ return true;
43
+ }
44
+ return false;
45
+ }
46
+
47
+ export async function runPassthrough(input: { real: string; argv: string[]; env: Record<string, string | undefined>; onSpawn?: (child: Subprocess) => void }): Promise<number> {
48
+ const p = Bun.spawn([input.real, ...input.argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: input.env });
49
+ input.onSpawn?.(p);
50
+ await p.exited;
51
+ return exitStatus(p);
52
+ }
53
+
54
+ export async function recordPresenceOrStop(input: { child: ChildHandle; dir: string; id: string; accountId: string; event: string; message: string; savedTermios: string | null }): Promise<void> {
55
+ for (let attempt = 0; ; attempt++) {
56
+ try {
57
+ writePresence({ dir: input.dir, id: input.id, accountId: input.accountId, pid: input.child.pid });
58
+ return;
59
+ } catch (e) {
60
+ if (input.child.exitCode !== null || input.child.signalCode !== null) return;
61
+ if (attempt >= 9) {
62
+ log(input.event, { err: errorMessage(e) });
63
+ input.child.kill();
64
+ await input.child.exited;
65
+ restoreTermios(input.savedTermios);
66
+ throw new Error(input.message);
67
+ }
68
+ await Bun.sleep(100);
69
+ }
70
+ }
71
+ }
72
+
73
+ export async function raceMarkerOrExit(input: {
74
+ child: { exited: Promise<unknown>; kill: () => void };
75
+ tick: () => boolean | Promise<boolean>;
76
+ savedTermios: string | null;
77
+ onKill?: () => void;
78
+ onError?: () => void;
79
+ onExited?: () => void;
80
+ }): Promise<void> {
81
+ let done = false;
82
+ const markerWatch = (async () => {
83
+ while (!done) {
84
+ if (await input.tick()) return true;
85
+ await Bun.sleep(150);
86
+ }
87
+ return false;
88
+ })();
89
+ const exited = input.child.exited.then(() => {
90
+ done = true;
91
+ return "exit" as const;
92
+ });
93
+ try {
94
+ const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
95
+ if (winner === "marker") {
96
+ input.onKill?.();
97
+ input.child.kill();
98
+ }
99
+ } catch (e) {
100
+ input.onKill?.();
101
+ input.child.kill();
102
+ input.onError?.();
103
+ throw e;
104
+ } finally {
105
+ await input.child.exited;
106
+ input.onExited?.();
107
+ done = true;
108
+ await markerWatch.catch(() => false);
109
+ restoreTermios(input.savedTermios);
110
+ }
111
+ }
package/src/lib/types.ts CHANGED
@@ -36,10 +36,7 @@ const AggregateWindowsSchema = z.object({
36
36
  sevenDay: UsageWindowSchema,
37
37
  });
38
38
 
39
- export const UsageWindowsSchema = AggregateWindowsSchema.extend({
40
- perModel: z.record(z.string(), UsageWindowSchema).default({}),
41
- });
42
- export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
39
+ export type UsageWindows = z.infer<typeof AggregateWindowsSchema> & { perModel: Record<string, UsageWindow> };
43
40
 
44
41
  export const WindowSchema = z.object({
45
42
  name: z.string().nullable(),
@@ -80,27 +77,19 @@ export type Account = z.infer<typeof AccountSchema>;
80
77
 
81
78
  export const AccountsIndexSchema = z.object({
82
79
  version: z.literal(2),
83
- activeId: z.string().nullable(),
84
80
  accounts: z.array(AccountSchema).default([]),
85
81
  });
86
-
87
- export const LastSwapSchema = z.object({ ts: z.number() });
88
82
  export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
89
83
 
90
- export const EnforcedLimitSchema = z.object({
91
- account: z.string(),
92
- kind: z.enum(["session", "weekly", "model"]),
93
- family: z.string().nullable(),
94
- resetsAt: z.number().nullable(),
95
- blind: z.boolean(),
96
- });
97
- export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
84
+ export type EnforcedLimit = {
85
+ account: string;
86
+ kind: "session" | "weekly" | "model";
87
+ family: string | null;
88
+ resetsAt: number | null;
89
+ blind: boolean;
90
+ };
98
91
 
99
- export const ThresholdsSchema = z.object({
100
- session: z.number().min(0).max(100),
101
- weekly: z.number().min(0).max(100),
102
- });
103
- export type Thresholds = z.infer<typeof ThresholdsSchema>;
92
+ export type Thresholds = { session: number; weekly: number };
104
93
 
105
94
  export const ConfigSchema = z
106
95
  .object({
@@ -142,11 +131,15 @@ export const RespawnMarkerSchema = z.object({
142
131
  launchedAt: z.number().optional(),
143
132
  });
144
133
 
134
+ export const EpochSecondsSchema = z.number().transform((seconds) => seconds * 1000);
135
+
136
+ const StdinWindowSchema = z.looseObject({ used_percentage: z.number(), resets_at: EpochSecondsSchema.nullable().optional() });
137
+
145
138
  export const RateLimitsStdinSchema = z.looseObject({
146
139
  rate_limits: z
147
140
  .looseObject({
148
- five_hour: z.looseObject({ used_percentage: z.number(), resets_at: z.number().nullable().optional() }).optional(),
149
- seven_day: z.looseObject({ used_percentage: z.number(), resets_at: z.number().nullable().optional() }).optional(),
141
+ five_hour: StdinWindowSchema.optional(),
142
+ seven_day: StdinWindowSchema.optional(),
150
143
  })
151
144
  .optional(),
152
145
  model: z.looseObject({ id: z.string().optional(), display_name: z.string().optional() }).optional(),
@@ -196,13 +189,12 @@ export const ProfileResponseSchema = z.looseObject({
196
189
  organization: z.looseObject({ uuid: z.string(), name: z.string().nullish() }),
197
190
  });
198
191
 
199
- export const TokenIdentitySchema = z.object({
200
- accountUuid: z.string(),
201
- email: z.string().nullable(),
202
- organizationUuid: z.string(),
203
- organizationName: z.string().nullable(),
204
- });
205
- export type TokenIdentity = z.infer<typeof TokenIdentitySchema>;
192
+ export type TokenIdentity = {
193
+ accountUuid: string;
194
+ email: string | null;
195
+ organizationUuid: string;
196
+ organizationName: string | null;
197
+ };
206
198
 
207
199
  const CodexTokensSchema = z.looseObject({
208
200
  id_token: z.string(),
@@ -217,13 +209,12 @@ export const CodexAuthJsonSchema = z.looseObject({
217
209
  });
218
210
  export type CodexAuthJson = z.infer<typeof CodexAuthJsonSchema>;
219
211
 
220
- export const CodexUsageSchema = z.object({
221
- accountId: z.string(),
222
- email: z.string().nullable(),
223
- planType: z.string().nullable(),
224
- windows: z.array(WindowSchema),
225
- });
226
- export type CodexUsage = z.infer<typeof CodexUsageSchema>;
212
+ export type CodexUsage = {
213
+ accountId: string;
214
+ email: string | null;
215
+ planType: string | null;
216
+ windows: Window[];
217
+ };
227
218
 
228
219
  export const CodexStopStdinSchema = z.looseObject({
229
220
  session_id: z.string().optional(),
package/src/lib/update.ts CHANGED
@@ -5,14 +5,13 @@ import { writeFileAtomic } from "./atomic.ts";
5
5
  import { http } from "./http.ts";
6
6
  import { isNixPackaged } from "./install.ts";
7
7
  import { withLock } from "./lock.ts";
8
- import { log } from "./log.ts";
9
- import { HOME, paths } from "./paths.ts";
8
+ import { errorMessage, log } from "./log.ts";
9
+ import { env, HOME, optionalEnv, paths } from "./paths.ts";
10
10
 
11
11
  const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
12
12
  const INSTALL_DEADLINE_MS = 60_000;
13
13
  const REGISTRY_DEADLINE_MS = 15_000;
14
14
 
15
- const OverrideSchema = z.string().min(1).optional().catch(undefined);
16
15
  const AttemptSchema = z.object({ attemptedAt: z.number() });
17
16
  const VersionSchema = z.object({ version: z.string().min(1) });
18
17
  const LatestSchema = z.object({ version: z.string().min(1), engines: z.looseObject({ bun: z.string().min(1).optional() }).optional() });
@@ -21,7 +20,7 @@ const updateJson = join(paths.home, "update.json");
21
20
  const updateLock = join(paths.home, "update.lock");
22
21
 
23
22
  function registry(): { base: URL; authorization: string | null } {
24
- const raw = OverrideSchema.parse(process.env.TOKENMAXXING_NPM_REGISTRY) ?? "https://registry.npmjs.org";
23
+ const raw = env("TOKENMAXXING_NPM_REGISTRY", "https://registry.npmjs.org");
25
24
  const base = new URL(raw.endsWith("/") ? raw : `${raw}/`);
26
25
  const authorization =
27
26
  base.username !== "" || base.password !== "" ? `Basic ${btoa(`${decodeURIComponent(base.username)}:${decodeURIComponent(base.password)}`)}` : null;
@@ -39,8 +38,8 @@ function realOrNull(path: string): string | null {
39
38
  }
40
39
 
41
40
  function globalRootCandidates(): string[] {
42
- const globalDir = OverrideSchema.parse(process.env.BUN_INSTALL_GLOBAL_DIR);
43
- const bunInstall = OverrideSchema.parse(process.env.BUN_INSTALL);
41
+ const globalDir = optionalEnv("BUN_INSTALL_GLOBAL_DIR");
42
+ const bunInstall = optionalEnv("BUN_INSTALL");
44
43
  return [
45
44
  ...(globalDir != null ? [globalDir] : []),
46
45
  ...(bunInstall != null ? [join(bunInstall, "install", "global")] : []),
@@ -127,7 +126,7 @@ export async function maybeAutoUpdate(): Promise<void> {
127
126
  try {
128
127
  await updateToLatest(root);
129
128
  } catch (e) {
130
- throw new Error(`self-update from npm failed: ${e instanceof Error ? e.message : String(e)}`);
129
+ throw new Error(`self-update from npm failed: ${errorMessage(e)}`);
131
130
  }
132
131
  });
133
132
  }