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
@@ -1,7 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { http, safeErrorDetail } from "./http.ts";
3
+ import { errorMessage } from "./log.ts";
3
4
  import { env } from "./paths.ts";
4
- import { CodexUsageSchema, JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
5
+ import { JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
5
6
  import { codexIdentityOf } from "./codexauth.ts";
6
7
  import { familyTokens } from "./usage.ts";
7
8
 
@@ -63,8 +64,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
63
64
  },
64
65
  });
65
66
  } catch (e) {
66
- const message = e instanceof Error ? e.message : String(e);
67
- throw new CodexUsageReadError(`endpoint unreachable: ${message}`);
67
+ throw new CodexUsageReadError(`endpoint unreachable: ${errorMessage(e)}`);
68
68
  }
69
69
  const text = await res.text();
70
70
  if (!res.ok) {
@@ -76,7 +76,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
76
76
  }
77
77
  const wire = parsed.data;
78
78
 
79
- return CodexUsageSchema.parse({
79
+ return {
80
80
  accountId: wire.account_id,
81
81
  email: wire.email ?? null,
82
82
  planType: wire.plan_type ?? null,
@@ -84,7 +84,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
84
84
  ...toWindows(wire.rate_limit, null, at),
85
85
  ...(wire.additional_rate_limits ?? []).flatMap((row) => toWindows(row.rate_limit, row.limit_name, at)),
86
86
  ],
87
- });
87
+ };
88
88
  }
89
89
 
90
90
  const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
@@ -2,14 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { delay } from "es-toolkit";
4
4
  import { z } from "zod";
5
- import { log } from "./log.ts";
5
+ import { errorMessage, log } from "./log.ts";
6
+ import { readLines } from "./proc.ts";
6
7
  import { JsonTextSchema } from "./types.ts";
7
8
 
8
- const CompactOutcomeSchema = z.discriminatedUnion("ok", [
9
- z.object({ ok: z.literal(true) }),
10
- z.object({ ok: z.literal(false), reason: z.string() }),
11
- ]);
12
- export type CompactOutcome = z.infer<typeof CompactOutcomeSchema>;
9
+ export type CompactOutcome = { ok: true } | { ok: false; reason: string };
13
10
 
14
11
  export const CLAUDE_COMPACT_KILL_MS = 300_000;
15
12
  const PIPE_GRACE_MS = 2_000;
@@ -108,20 +105,10 @@ export async function compactCodexThread(input: { real: string; threadId: string
108
105
  }
109
106
  };
110
107
  (async () => {
111
- let buffered = "";
112
- for await (const chunk of p.stdout) {
113
- buffered += Buffer.from(chunk).toString("utf8");
114
- let nl = buffered.indexOf("\n");
115
- while (nl >= 0) {
116
- onLine(buffered.slice(0, nl));
117
- buffered = buffered.slice(nl + 1);
118
- nl = buffered.indexOf("\n");
119
- }
120
- }
121
- if (buffered.trim() !== "") onLine(buffered);
108
+ for await (const line of readLines(p.stdout)) onLine(line);
122
109
  const err = (await stderrText).trim().slice(0, 200);
123
110
  finish({ ok: false, reason: p.exitCode === null ? `app-server killed after ${CODEX_COMPACT_KILL_MS / 1000}s` : `app-server exited ${p.exitCode}: ${err}` });
124
- })().catch((e) => finish({ ok: false, reason: e instanceof Error ? e.message : String(e) }));
111
+ })().catch((e) => finish({ ok: false, reason: errorMessage(e) }));
125
112
  send({ id: INIT_ID, method: "initialize", params: { clientInfo: { name: "tokenmaxxing", version: packageVersion() } } });
126
113
  });
127
114
 
@@ -1,16 +1,11 @@
1
1
  import { mkdirSync, readFileSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { z } from "zod";
4
3
  import { writeFileAtomic } from "./atomic.ts";
5
4
  import * as kc from "./keychain.ts";
6
5
  import { keychain as kcNames, namespacedCredService, storeDirFor } from "./paths.ts";
7
6
  import { CredentialBlobSchema, ErrnoSchema, type OAuthCreds } from "./types.ts";
8
7
 
9
- const CredTargetSchema = z.discriminatedUnion("kind", [
10
- z.object({ kind: z.literal("keychain"), service: z.string(), account: z.string() }),
11
- z.object({ kind: z.literal("file"), path: z.string() }),
12
- ]);
13
- export type CredTarget = z.infer<typeof CredTargetSchema>;
8
+ export type CredTarget = ({ kind: "keychain" } & kc.KeychainTarget) | { kind: "file"; path: string };
14
9
 
15
10
  const darwin = process.platform === "darwin";
16
11
 
package/src/lib/decide.ts CHANGED
@@ -1,19 +1,12 @@
1
- import { z } from "zod";
2
1
  import { withLock } from "./lock.ts";
3
- import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadLastSwapAt, saveAccounts } from "./state.ts";
2
+ import { loadAccounts, loadConfig, saveAccounts } from "./state.ts";
4
3
  import { isExhausted, limitWindows, nextWeeklyReset, pickBest, pickEarliestReset, sessionWindow, thresholdBars, usableAt, weeklyWindow, type PickCtx } from "./picker.ts";
5
4
  import { familyTokens } from "./usage.ts";
6
- import { log } from "./log.ts";
5
+ import { errorMessage, log } from "./log.ts";
7
6
  import type { Observation, Provider } from "./provider.ts";
8
- import { AccountSchema, type Account, type EnforcedLimit } from "./types.ts";
7
+ import type { Account, EnforcedLimit } from "./types.ts";
9
8
 
10
- const SwapDecisionSchema = z.object({
11
- swapped: z.boolean(),
12
- account: AccountSchema.nullable(),
13
- reason: z.string(),
14
- waitUntil: z.number().optional(),
15
- });
16
- export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
9
+ export type SwapDecision = { swapped: boolean; account: Account | null; reason: string; waitUntil?: number };
17
10
 
18
11
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
19
12
  const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
@@ -45,12 +38,6 @@ function enforcedWall(limit: EnforcedLimit, account: Account, now: number): numb
45
38
 
46
39
  export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRespawn = false, enforced: EnforcedLimit | null = null, seatId: string | null = p.liveId()): Promise<SwapDecision> {
47
40
  const activeId = seatId;
48
-
49
- const lastSwapAt = loadLastSwapAt(p.pool);
50
- if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
51
- return { swapped: false, account: null, reason: "post-swap-cooldown" };
52
- }
53
-
54
41
  const cfg = loadConfig();
55
42
  const bars = thresholdBars(cfg);
56
43
  const stored0 = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
@@ -63,10 +50,6 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
63
50
  }
64
51
 
65
52
  return withLock(p.pool.lockFile, async () => {
66
- const lastSwapAt2 = loadLastSwapAt(p.pool);
67
- if (!enforced && lastSwapAt2 != null && now - lastSwapAt2 < POST_SWAP_COOLDOWN_MS) {
68
- return { swapped: false, account: null, reason: "raced-already-swapped" };
69
- }
70
53
  const idx = loadAccounts(p.pool);
71
54
  const id2 = seatId;
72
55
  const active = id2 ? idx.accounts.find((a) => a.id === id2) : undefined;
@@ -113,15 +96,14 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
113
96
  return { swapped: false, account: null, reason: "needs-respawn" };
114
97
  }
115
98
 
116
- const seatOf = (cur: { activeId: string | null; accounts: Account[] }): Account | null =>
117
- cur.accounts.find((a) => a.id === id2) ?? cur.accounts.find((a) => a.id === cur.activeId) ?? null;
99
+ const seatOf = (cur: { accounts: Account[] }): Account | null => cur.accounts.find((a) => a.id === id2) ?? null;
118
100
 
119
101
  const rejected = new Set<string>();
120
102
  const usable = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (p.seats === "shared" || a.id === id2 || !present.has(a.id)));
121
103
  const skipOrThrow = (e: unknown, candidate: Account): void => {
122
104
  if (p.classifySwapError(e) === "fatal") throw e;
123
105
  rejected.add(candidate.id);
124
- log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error: e instanceof Error ? e.message : String(e) });
106
+ log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error: errorMessage(e) });
125
107
  };
126
108
  while (true) {
127
109
  const cur = loadAccounts(p.pool);
package/src/lib/grok.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { writeFileAtomic } from "./atomic.ts";
5
5
  import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
6
- import type { Observation, Provider, SampleReport } from "./provider.ts";
7
- import { loadAccounts, loadConfig, pinBinOverride, type Harvest } from "./state.ts";
8
- import type { Account } from "./types.ts";
6
+ import type { Provider } from "./provider.ts";
7
+ import { loadAccounts, type Harvest } from "./state.ts";
8
+ import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
9
9
  import { c } from "../cli/render.ts";
10
10
 
11
11
  const GrokAuthEntrySchema = z.looseObject({
@@ -18,85 +18,10 @@ const GrokAuthEntrySchema = z.looseObject({
18
18
  });
19
19
  type GrokAuthEntry = z.infer<typeof GrokAuthEntrySchema>;
20
20
 
21
- function parseGrokAuthFile(path: string): { key: string; entry: GrokAuthEntry }[] {
22
- let raw: unknown;
23
- try {
24
- raw = JSON.parse(readFileSync(path, "utf8"));
25
- } catch {
26
- return [];
27
- }
28
- const map = z.record(z.string(), z.unknown()).safeParse(raw);
29
- if (!map.success) return [];
30
- const out: { key: string; entry: GrokAuthEntry }[] = [];
31
- for (const [key, value] of Object.entries(map.data)) {
32
- const parsed = GrokAuthEntrySchema.safeParse(value);
33
- if (parsed.success) out.push({ key, entry: parsed.data });
34
- }
35
- return out;
36
- }
37
-
38
- function identityOf(key: string, entry: GrokAuthEntry): { id: string; email: string | null } {
39
- const id = entry.user_id ?? entry.principal_id ?? key;
40
- return { id, email: entry.email ?? null };
41
- }
42
-
43
- function resolveRealGrok(): string {
44
- const cfg = loadConfig();
45
- if (cfg.grokBin) {
46
- if (!existsSync(cfg.grokBin)) throw new Error(`configured grokBin does not exist: ${cfg.grokBin} - fix config.json`);
47
- return cfg.grokBin;
48
- }
49
- for (const d of (process.env.PATH ?? "").split(":")) {
50
- if (!d) continue;
51
- const cand = join(d, "grok");
52
- try {
53
- if (existsSync(cand)) return cand;
54
- } catch {
55
- continue;
56
- }
57
- }
58
- throw new Error("could not locate the real `grok` binary (set grokBin in config.json)");
59
- }
60
-
61
- function liveId(): string | null {
62
- return grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id));
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 --grok`" });
77
- }
78
- return reports;
79
- }
80
-
81
- async function storeUsable(a: Account): Promise<boolean> {
82
- try {
83
- const entries = parseGrokAuthFile(grokAuthJsonFor(a.id));
84
- return entries.some((e) => identityOf(e.key, e.entry).id === a.id && (e.entry.refresh_token ?? "") !== "");
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- async function removeCredentials(a: Account): Promise<void> {
91
- rmSync(grokStoreDirFor(a.id), { recursive: true, force: true });
92
- }
93
-
94
- function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
95
- const { id, email } = identityOf(key, entry);
96
- if (!id) return null;
21
+ function harvestOf(id: string, key: string, entry: GrokAuthEntry): Harvest {
97
22
  return {
98
23
  id,
99
- email,
24
+ email: entry.email ?? null,
100
25
  tier: null,
101
26
  sample: null,
102
27
  park: async () => {
@@ -106,83 +31,44 @@ function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
106
31
  };
107
32
  }
108
33
 
109
- async function login(): Promise<Harvest | null> {
110
- const real = resolveRealGrok();
111
- const onboardDir = grokPaths.onboardDir;
112
- rmSync(onboardDir, { recursive: true, force: true });
113
- mkdirSync(onboardDir, { recursive: true });
114
- const p = Bun.spawn([real, "login"], {
115
- stdin: "inherit",
116
- stdout: "inherit",
117
- stderr: "inherit",
118
- env: { ...process.env, GROK_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
119
- });
120
- await p.exited;
34
+ function readAuth(path: string): AuthEntry[] {
35
+ let raw: unknown;
121
36
  try {
122
- if (p.exitCode !== 0) {
123
- console.error(c.red("no grok login landed in the isolated home - nothing added."));
124
- return null;
125
- }
126
- const entries = parseGrokAuthFile(join(onboardDir, "auth.json"));
127
- if (entries.length === 0) {
128
- console.error(c.red("no grok login landed in the isolated home - nothing added."));
129
- return null;
130
- }
131
- return harvestOf(entries[0]!.key, entries[0]!.entry);
132
- } finally {
133
- rmSync(onboardDir, { recursive: true, force: true });
134
- }
135
- }
136
-
137
- async function importLive(): Promise<Harvest | null> {
138
- console.log(c.cyan("Opening an isolated grok login for your first pooled account - the login you already have stays as it is."));
139
- console.log();
140
- const live = parseGrokAuthFile(join(grokPaths.home, "auth.json"));
141
- if (live.length > 0) {
142
- console.log(c.dim(`found ${live.length} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`));
143
- const h = harvestOf(live[0]!.key, live[0]!.entry);
144
- if (h) return h;
37
+ raw = JSON.parse(readFileSync(path, "utf8"));
38
+ } catch {
39
+ return [];
145
40
  }
146
- return login();
147
- }
148
-
149
- function preflight(): void {
150
- const real = resolveRealGrok();
151
- const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
152
- const out = (p.stdout?.toString() ?? "").trim().toLowerCase();
153
- if (p.exitCode !== 0 || !out.includes("grok")) {
154
- throw new Error(`grok binary failed verification: ${real}`);
41
+ const map = z.record(z.string(), z.unknown()).safeParse(raw);
42
+ if (!map.success) return [];
43
+ const out: AuthEntry[] = [];
44
+ for (const [key, value] of Object.entries(map.data)) {
45
+ const parsed = GrokAuthEntrySchema.safeParse(value);
46
+ if (!parsed.success) continue;
47
+ const entry = parsed.data;
48
+ const id = entry.user_id ?? entry.principal_id ?? key;
49
+ out.push({ id, usable: (entry.refresh_token ?? "") !== "", harvest: id === "" ? null : harvestOf(id, key, entry) });
155
50
  }
156
- pinBinOverride({ key: "grokBin", bin: real });
157
- }
158
-
159
- function install(): void {
160
- console.log(c.dim("grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts."));
51
+ return out;
161
52
  }
162
53
 
163
- export const grok: Provider = {
54
+ export const grok: Provider = statusOnlyProvider({
164
55
  name: "grok",
165
56
  flag: " --grok",
166
57
  pool: grokPool,
167
- seats: "live",
168
- waitsWhenDepleted: false,
169
- statusOnly: true,
170
- liveId,
171
- presence,
172
- gatedFamilies: () => null,
173
- observeLive,
174
- samplePool,
175
- mergeWindows: (next) => next,
176
- swap: async () => {
177
- throw new Error("grok moves are not supported yet (status-only pool)");
178
- },
179
- classifySwapError: () => "fatal",
180
- removeCredentials,
181
- storeUsable,
182
- login,
183
- importLive,
184
- preflight,
185
- install,
58
+ binName: "grok",
59
+ binKey: "grokBin",
60
+ storeDirFor: grokStoreDirFor,
61
+ authJsonFor: grokAuthJsonFor,
62
+ onboardDir: grokPaths.onboardDir,
63
+ homeEnv: "GROK_HOME",
64
+ loginArgs: ["login"],
65
+ onboardAuthRel: "auth.json",
66
+ liveAuthPath: () => join(grokPaths.home, "auth.json"),
67
+ readAuth,
68
+ liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
69
+ versionOk: (out) => out.trim().toLowerCase().includes("grok"),
70
+ importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
71
+ foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
72
+ installNotice: "grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts.",
186
73
  loginStep: () => `Sign in in the browser session that opens (or run ${c.bold("grok login --device-auth")} on headless hosts first).`,
187
- windowLabel: (name) => name.toLowerCase(),
188
- };
74
+ });
package/src/lib/http.ts CHANGED
@@ -17,8 +17,7 @@ const PlainErrorBodySchema = z.looseObject({
17
17
  message: z.string().optional(),
18
18
  });
19
19
 
20
- const ErrorDetailSchema = z.object({ code: z.string().nullable(), fields: z.array(z.string()) });
21
- type ErrorDetail = z.infer<typeof ErrorDetailSchema>;
20
+ type ErrorDetail = { code: string | null; fields: string[] };
22
21
 
23
22
  function parseErrorBody(text: string): ErrorDetail | null {
24
23
  const json = JsonTextSchema.safeParse(text).data;
@@ -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 {