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
@@ -4,19 +4,21 @@ import type { FileSink, Subprocess } from "bun";
4
4
  import { maxBy } from "es-toolkit";
5
5
  import { z } from "zod";
6
6
  import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
7
- import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, resolveRealClaude, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
7
+ import { UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealClaude, wrapDepth } from "../lib/claudebin.ts";
8
8
  import { claude, pickSeat } from "../lib/claude.ts";
9
9
  import { compactClaudeSession } from "../lib/compact.ts";
10
10
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
11
11
  import { withLock } from "../lib/lock.ts";
12
12
  import { thresholdBars, usableAt } from "../lib/picker.ts";
13
- import { clearPresence, writePresence } from "../lib/presence.ts";
13
+ import { clearPresence } from "../lib/presence.ts";
14
+ import { readLines } from "../lib/proc.ts";
14
15
  import { teeObservation } from "../lib/sample.ts";
15
- import { saveTermios, restoreTermios } from "../lib/tty.ts";
16
+ import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
17
+ import { saveTermios } from "../lib/tty.ts";
16
18
  import { loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
17
19
  import { loadAccounts, loadConfig } from "../lib/state.ts";
18
20
  import { RespawnMarkerSchema, type Account } from "../lib/types.ts";
19
- import { log } from "../lib/log.ts";
21
+ import { errorMessage, log } from "../lib/log.ts";
20
22
 
21
23
  const NONINTERACTIVE_SUBCMDS = new Set([
22
24
  "mcp", "config", "doctor", "update", "install", "migrate-installer",
@@ -44,20 +46,21 @@ const OPTIONAL_VALUE_ROOT_FLAGS = new Set([
44
46
 
45
47
  const isUuid = (s: string) => z.uuid().safeParse(s).success;
46
48
 
47
- const AnalysisSchema = z.object({
48
- manage: z.boolean(),
49
- sessionId: z.string().nullable(),
50
- resumeId: z.string().nullable(),
51
- continueLatest: z.boolean(),
52
- streamInput: z.boolean(),
53
- });
54
- type Analysis = z.infer<typeof AnalysisSchema>;
49
+ type Analysis = {
50
+ manage: boolean;
51
+ sessionId: string | null;
52
+ resumeId: string | null;
53
+ continueLatest: boolean;
54
+ streamInput: boolean;
55
+ streamOutput: boolean;
56
+ };
55
57
 
56
58
  export function analyzeArgs(argv: string[]): Analysis {
57
59
  let sessionId: string | null = null;
58
60
  let resumeId: string | null = null;
59
61
  let continueLatest = false;
60
62
  let streamInput = false;
63
+ let streamOutput = false;
61
64
  let printMode = false;
62
65
  let invalidSessionArg = false;
63
66
  let pickerResume = false;
@@ -66,7 +69,8 @@ export function analyzeArgs(argv: string[]): Analysis {
66
69
 
67
70
  for (let i = 0; i < argv.length; i++) {
68
71
  const a = argv[i]!;
69
- if (a === "-p" || a === "--print") printMode = true;
72
+ if (a === "--") break;
73
+ else if (a === "-p" || a === "--print") printMode = true;
70
74
  else if (a === "--version" || a === "-v" || a === "--help" || a === "-h") printMode = true;
71
75
  else if (a === "--session-id") {
72
76
  const next = argv[++i] ?? null;
@@ -92,6 +96,8 @@ export function analyzeArgs(argv: string[]): Analysis {
92
96
  }
93
97
  else if (a === "--input-format") streamInput = argv[++i] === "stream-json";
94
98
  else if (a.startsWith("--input-format=")) streamInput = a.slice("--input-format=".length) === "stream-json";
99
+ else if (a === "--output-format") streamOutput = argv[++i] === "stream-json";
100
+ else if (a.startsWith("--output-format=")) streamOutput = a.slice("--output-format=".length) === "stream-json";
95
101
  else if (VALUE_TAKING_ROOT_FLAGS.has(a)) i++;
96
102
  else if (VARIADIC_ROOT_FLAGS.has(a)) {
97
103
  while (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) i++;
@@ -107,7 +113,7 @@ export function analyzeArgs(argv: string[]): Analysis {
107
113
  const isSubcmd = firstPositional !== null && NONINTERACTIVE_SUBCMDS.has(firstPositional);
108
114
  const forkResume = forkSession && (resumeId !== null || continueLatest);
109
115
  const manage = !printMode && !isSubcmd && !invalidSessionArg && !pickerResume && !forkResume && !process.env.TOKENMAXXING_PROBE;
110
- return { manage, sessionId, resumeId, continueLatest, streamInput };
116
+ return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput };
111
117
  }
112
118
 
113
119
  export function stripSessionFlags(argv: string[]): string[] {
@@ -164,18 +170,17 @@ function latestSessionForCwd(): string | null {
164
170
  }
165
171
  }
166
172
 
167
- const MarkerGateSchema = z.object({
168
- launchedAt: z.number(),
169
- overriddenUntil: z.number(),
170
- });
171
- type MarkerGate = z.infer<typeof MarkerGateSchema>;
173
+ type MarkerGate = {
174
+ launchedAt: number;
175
+ overriddenUntil: number;
176
+ };
172
177
 
173
178
  function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
174
179
  let m: z.infer<typeof RespawnMarkerSchema>;
175
180
  try {
176
181
  m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
177
182
  } catch (e) {
178
- log("supervisor.marker_invalid", { err: e instanceof Error ? e.message : String(e) });
183
+ log("supervisor.marker_invalid", { err: errorMessage(e) });
179
184
  throw new Error(
180
185
  `${marker} is corrupt (unparsable JSON or off-schema) - the session was stopped instead of guessing whether the pool is depleted; inspect the marker, then run \`claude --resume ${basename(marker)}\` (a fresh launch clears it)`,
181
186
  );
@@ -217,25 +222,39 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate):
217
222
  writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt }, sessionId: sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true });
218
223
  }
219
224
  } catch (e) {
220
- log("supervisor.seat_watch_error", { err: e instanceof Error ? e.message : String(e) });
225
+ log("supervisor.seat_watch_error", { err: errorMessage(e) });
221
226
  }
222
227
  return true;
223
228
  }
224
229
 
225
- async function countdownWait(acct: string, until: number): Promise<boolean> {
230
+ type Say = (terminal: string, text: string) => void;
231
+
232
+ function systemLine(sid: string, text: string): string {
233
+ return `${JSON.stringify({ type: "system", subtype: "informational", content: text, level: "warning", uuid: crypto.randomUUID(), session_id: sid })}\n`;
234
+ }
235
+
236
+ const clockFmt = new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZoneName: "short" });
237
+
238
+ async function countdownWait(acct: string, until: number, out: { stream: boolean; say: Say }): Promise<boolean> {
226
239
  let aborted = false;
227
240
  const onInt = () => { aborted = true; };
228
241
  process.on("SIGINT", onInt);
229
- process.stderr.write(`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`);
242
+ const minutes = Math.max(1, Math.ceil((until - Date.now()) / 60000));
243
+ out.say(
244
+ `\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`,
245
+ `tokenmaxxing: all accounts at their limit. Resuming at ${clockFmt.format(until)} (in ${minutes} min) on ${acct}.`,
246
+ );
230
247
  while (!aborted && Date.now() < until) {
231
- const left = until - Date.now();
232
- const m = Math.floor(left / 60000);
233
- const s = Math.floor((left % 60000) / 1000);
234
- process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
248
+ if (!out.stream) {
249
+ const left = until - Date.now();
250
+ const m = Math.floor(left / 60000);
251
+ const s = Math.floor((left % 60000) / 1000);
252
+ process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
253
+ }
235
254
  await Bun.sleep(1000);
236
255
  }
237
256
  process.removeListener("SIGINT", onInt);
238
- process.stderr.write(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`);
257
+ out.say(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`, `tokenmaxxing: resuming on ${acct}.`);
239
258
  return aborted;
240
259
  }
241
260
 
@@ -257,7 +276,7 @@ class StdinRelay {
257
276
 
258
277
  constructor() {
259
278
  this.pump()
260
- .catch((e: unknown) => log("supervisor.relay_read_failed", { err: e instanceof Error ? e.message : String(e) }))
279
+ .catch((e: unknown) => log("supervisor.relay_read_failed", { err: errorMessage(e) }))
261
280
  .finally(() => {
262
281
  this.ended = true;
263
282
  this.close();
@@ -277,19 +296,7 @@ class StdinRelay {
277
296
  }
278
297
 
279
298
  private async pump(): Promise<void> {
280
- const decoder = new TextDecoder();
281
- let pending = "";
282
- for await (const chunk of Bun.stdin.stream()) {
283
- pending += decoder.decode(chunk, { stream: true });
284
- let nl = pending.indexOf("\n");
285
- while (nl !== -1) {
286
- this.forward(pending.slice(0, nl + 1));
287
- pending = pending.slice(nl + 1);
288
- nl = pending.indexOf("\n");
289
- }
290
- }
291
- pending += decoder.decode();
292
- if (pending.length > 0) this.forward(`${pending}\n`);
299
+ for await (const line of readLines(Bun.stdin.stream())) this.forward(`${line}\n`);
293
300
  }
294
301
 
295
302
  private forward(line: string): void {
@@ -299,7 +306,7 @@ class StdinRelay {
299
306
  this.sink.flush();
300
307
  return;
301
308
  } catch (e) {
302
- log("supervisor.relay_write_failed", { err: e instanceof Error ? e.message : String(e) });
309
+ log("supervisor.relay_write_failed", { err: errorMessage(e) });
303
310
  this.sink = null;
304
311
  }
305
312
  }
@@ -311,50 +318,17 @@ class StdinRelay {
311
318
  try {
312
319
  this.sink.end();
313
320
  } catch (e) {
314
- log("supervisor.relay_end_failed", { err: e instanceof Error ? e.message : String(e) });
321
+ log("supervisor.relay_end_failed", { err: errorMessage(e) });
315
322
  }
316
323
  this.sink = null;
317
324
  }
318
325
  }
319
326
 
320
- async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
321
- for (let attempt = 0; ; attempt++) {
322
- try {
323
- writePresence({ dir: paths.presenceDir, id: sid, accountId: seat.id, pid: child.pid });
324
- return;
325
- } catch (e) {
326
- if (child.exitCode !== null || child.signalCode !== null) return;
327
- if (attempt >= 9) {
328
- log("supervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
329
- child.kill();
330
- await child.exited;
331
- restoreTermios(savedTermios);
332
- throw new Error("could not write the session presence file - refusing to run a session whose seat placement cannot see");
333
- }
334
- await Bun.sleep(100);
335
- }
336
- }
337
- }
338
-
339
327
  export async function runSupervisor(argv: string[]): Promise<number> {
340
- const depth = wrapDepth();
341
- if (depth >= MAX_WRAP_DEPTH) {
342
- console.error(
343
- `tokenmaxxing: ${LOOP_DIAGNOSIS} (depth ${depth}) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
344
- );
345
- log("supervisor.loop_abort", { depth });
346
- return 1;
347
- }
348
- if (wrapperEntryRateTripped(Date.now())) {
349
- console.error(
350
- `tokenmaxxing: ${LOOP_DIAGNOSIS} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
351
- );
352
- log("supervisor.rate_abort", { max: WRAP_RATE_MAX });
353
- return 1;
354
- }
328
+ if (loopGuardTripped("claude")) return 1;
355
329
  const real = resolveRealClaude();
356
330
  const info = analyzeArgs(argv);
357
- const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(depth + 1) };
331
+ const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
358
332
 
359
333
  let child: Subprocess | null = null;
360
334
  let terminating = false;
@@ -368,10 +342,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
368
342
  const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
369
343
  delete passthroughEnv.TOKENMAXXING_SUPERVISED;
370
344
  delete passthroughEnv.TOKENMAXXING_SESSION_ID;
371
- const p = Bun.spawn([real, ...argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: passthroughEnv });
372
- child = p;
373
- await p.exited;
374
- return p.exitCode ?? (p.signalCode ? 1 : 0);
345
+ return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
375
346
  }
376
347
 
377
348
  let base = stripSessionFlags(argv);
@@ -406,7 +377,14 @@ export async function runSupervisor(argv: string[]): Promise<number> {
406
377
  process.on("SIGINT", () => {});
407
378
  process.on("SIGHUP", () => {});
408
379
 
409
- const relay = analyzeArgs(base).streamInput ? new StdinRelay() : null;
380
+ const effective = analyzeArgs(base);
381
+ const relay = effective.streamInput ? new StdinRelay() : null;
382
+ const stream = effective.streamOutput;
383
+ let noticeSid = sid;
384
+ const say: Say = (terminal, text) => {
385
+ if (stream) process.stdout.write(systemLine(noticeSid, text));
386
+ else process.stderr.write(terminal);
387
+ };
410
388
  let firstLine: string | null = null;
411
389
  let respawns = 0;
412
390
  let overriddenUntil = 0;
@@ -432,66 +410,63 @@ export async function runSupervisor(argv: string[]): Promise<number> {
432
410
  });
433
411
  if (relay !== null) relay.attach(spawned.stdin!, firstLine);
434
412
  firstLine = null;
435
- if (picked) await recordPresence(spawned, sid, picked, savedTermios);
413
+ if (picked) {
414
+ await recordPresenceOrStop({
415
+ child: spawned,
416
+ dir: paths.presenceDir,
417
+ id: sid,
418
+ accountId: picked.id,
419
+ event: "supervisor.presence_failed",
420
+ message: "could not write the session presence file - refusing to run a session whose seat placement cannot see",
421
+ savedTermios,
422
+ });
423
+ }
436
424
  return { child: spawned, seat: picked };
437
425
  });
438
426
 
439
427
  child = proc;
440
- let done = false;
441
428
  let seatCheckAt = 0;
442
- const markerWatch = (async () => {
443
- while (!done) {
429
+ await raceMarkerOrExit({
430
+ child: proc,
431
+ tick: async () => {
444
432
  if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
445
433
  if (seat && !terminating && Date.now() >= seatCheckAt) {
446
434
  const decided = await moveExhaustedSeat(seat, sid, gate);
447
435
  seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
448
436
  }
449
- await Bun.sleep(150);
450
- }
451
- return false;
452
- })();
453
- const exited = proc.exited.then(() => { done = true; return "exit" as const; });
454
- try {
455
- const winner = await Promise.race([exited, markerWatch.then((m) => (m ? "marker" : "exit"))]);
456
- if (winner === "marker") {
437
+ return false;
438
+ },
439
+ savedTermios,
440
+ onKill: () => relay?.detach(),
441
+ onError: () => clearPresence({ dir: paths.presenceDir, id: sid }),
442
+ onExited: () => {
443
+ child = null;
457
444
  relay?.detach();
458
- proc.kill();
459
- }
460
- } catch (e) {
461
- relay?.detach();
462
- proc.kill();
463
- clearPresence({ dir: paths.presenceDir, id: sid });
464
- throw e;
465
- } finally {
466
- await proc.exited;
467
- child = null;
468
- relay?.detach();
469
- done = true;
470
- await markerWatch.catch(() => {});
471
- restoreTermios(savedTermios);
472
- }
445
+ },
446
+ });
473
447
 
474
448
  const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
475
449
  if (m) {
476
450
  rmSync(marker, { force: true });
477
451
  respawns++;
452
+ noticeSid = m.sessionId;
478
453
  const label = loadAccounts(claudePool).accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
479
454
  const resumable = existsSync(transcriptPath(m.sessionId));
480
455
  let compacted = false;
481
456
  if (m.compact && seat && resumable) {
482
- process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${seat.label} before the move...\x1b[0m\n`);
457
+ say(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${seat.label} before the move...\x1b[0m\n`, `tokenmaxxing: compacting the conversation on ${seat.label} before the move.`);
483
458
  const compactEnv: Record<string, string | undefined> = { ...childEnv, TOKENMAXXING_PROBE: "1", CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) };
484
459
  delete compactEnv.TOKENMAXXING_SUPERVISED;
485
460
  delete compactEnv.TOKENMAXXING_SESSION_ID;
486
461
  delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
487
462
  const outcome = await compactClaudeSession({ real, sid: m.sessionId, env: compactEnv });
488
463
  log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
489
- if (!outcome.ok) process.stderr.write(`\x1b[33m compaction did not land (${outcome.reason}) - resuming with the full context\x1b[0m\n`);
464
+ if (!outcome.ok) say(`\x1b[33m compaction did not land (${outcome.reason}) - resuming with the full context\x1b[0m\n`, `tokenmaxxing: compaction did not land; resuming with the full context. (${outcome.reason})`);
490
465
  compacted = outcome.ok;
491
466
  }
492
467
  if (m.waitUntil > Date.now()) {
493
- if (await countdownWait(label, m.waitUntil)) overriddenUntil = m.waitUntil;
494
- } else process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`);
468
+ if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
469
+ } else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
495
470
  wanted = m.accountId;
496
471
  saveSessionFlags(m.sessionId, persistable, process.cwd());
497
472
  const prompt = resumable ? resumePrompt(compacted) : null;
@@ -501,6 +476,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
501
476
  }
502
477
  clearPresence({ dir: paths.presenceDir, id: sid });
503
478
  log("supervisor.exit", { sid, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
504
- return proc.exitCode ?? (proc.signalCode ? 1 : 0);
479
+ return exitStatus(proc);
505
480
  }
506
481
  }
package/src/lib/claude.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
@@ -6,7 +6,7 @@ import { resolveRealClaude, resolveVerifiedClaude } from "./claudebin.ts";
6
6
  import { withClaudeRefreshLock } from "./claudelock.ts";
7
7
  import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath, timerActivationHint } from "./install.ts";
8
8
  import { withLock } from "./lock.ts";
9
- import { log } from "./log.ts";
9
+ import { errorMessage, log } from "./log.ts";
10
10
  import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
11
11
  import { claudePool, paths, sampleDirFor, seatFromEnv, storeDirFor } from "./paths.ts";
12
12
  import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
@@ -16,8 +16,8 @@ import { foldTee, sampleAccountUsage, teeObservation } from "./sample.ts";
16
16
  import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
17
17
  import { loadSetupTokens, saveSetupTokens } from "./setuptokens.ts";
18
18
  import { saveTermios, restoreTermios } from "./tty.ts";
19
- import { CRED_ENV_OVERRIDES, fetchUsageDirect, gatedFamilies, mergeWindows, windowsOf } from "./usage.ts";
20
- import { CredentialBlobSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
19
+ import { fetchUsageDirect, gatedFamilies, mergeWindows, scrubCredentialEnv, windowsOf } from "./usage.ts";
20
+ import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config } from "./types.ts";
21
21
  import { c } from "../cli/render.ts";
22
22
 
23
23
  class StoreUnusableError extends Error {
@@ -46,7 +46,7 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
46
46
  const needsPerModel = snap != null && gatedFamilies(snap.state.model, cfg.policy.switchModels).length > 0;
47
47
  if (opts.probe && !probeAttempted && (!fresh || needsPerModel)) {
48
48
  const startedAt = Date.now();
49
- const outcome = await sampleAccountUsage(account, { retries: 0 });
49
+ const outcome = await sampleAccountUsage(account);
50
50
  await withLock(claudePool.lockFile, () => {
51
51
  const idx = loadAccounts(claudePool);
52
52
  const a = idx.accounts.find((x) => x.id === account.id);
@@ -100,7 +100,7 @@ async function prepareMove(target: Account): Promise<void> {
100
100
  try {
101
101
  creds = await readStore(target.id);
102
102
  } catch (e) {
103
- throw new StoreUnusableError(`${target.label}'s store is unreadable (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
103
+ throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth ${target.label}\``);
104
104
  }
105
105
  if (!creds) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth ${target.label}\``);
106
106
  if (isDeadCredential(creds)) {
@@ -147,11 +147,11 @@ async function removeCredentials(a: Account): Promise<void> {
147
147
  }
148
148
  }
149
149
 
150
+ const IdentityReadySchema = z.looseObject({ oauthAccount: z.looseObject({ accountUuid: z.string().min(1) }) });
151
+
150
152
  function identityReady(cjPath: string): boolean {
151
- if (!existsSync(cjPath)) return false;
152
153
  try {
153
- const oauthAccount = JSON.parse(readFileSync(cjPath, "utf8")).oauthAccount;
154
- return z.object({ accountUuid: z.string().min(1) }).safeParse(oauthAccount).success;
154
+ return IdentityReadySchema.safeParse(JsonTextSchema.safeParse(readFileSync(cjPath, "utf8")).data).success;
155
155
  } catch {
156
156
  return false;
157
157
  }
@@ -167,8 +167,7 @@ async function login(): Promise<Harvest | null> {
167
167
  const real = resolveRealClaude();
168
168
 
169
169
  const savedTermios = saveTermios();
170
- const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
171
- for (const key of CRED_ENV_OVERRIDES) delete env[key];
170
+ const env = scrubCredentialEnv({ ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" });
172
171
  const p = Bun.spawn([real], {
173
172
  stdin: "inherit",
174
173
  stdout: "inherit",
@@ -208,7 +207,7 @@ async function login(): Promise<Harvest | null> {
208
207
  try {
209
208
  identity = await fetchTokenIdentity(blob.claudeAiOauth.accessToken);
210
209
  } catch (e) {
211
- console.error(c.red(`could not verify which account the login belongs to (${e instanceof Error ? e.message : String(e)}) - nothing changed.`));
210
+ console.error(c.red(`could not verify which account the login belongs to (${errorMessage(e)}) - nothing changed.`));
212
211
  return null;
213
212
  }
214
213
  if (identity.accountUuid !== oauthAccount.accountUuid) {
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync } from "nod
2
2
  import { dirname, join } from "node:path";
3
3
  import { uniq } from "es-toolkit";
4
4
  import { z } from "zod";
5
+ import { errorMessage } from "./log.ts";
5
6
  import { paths } from "./paths.ts";
6
7
  import { loadConfig } from "./state.ts";
7
8
  import { writeFileAtomic } from "./atomic.ts";
@@ -9,7 +10,7 @@ import { writeFileAtomic } from "./atomic.ts";
9
10
  export const WRAP_DEPTH_ENV = "TOKENMAXXING_WRAP_DEPTH";
10
11
  export const MAX_WRAP_DEPTH = 5;
11
12
  export const UNMANAGED_ENV = "TOKENMAXXING_UNMANAGED";
12
- export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real claude";
13
+ export const LOOP_DIAGNOSIS = "wrapper re-entered without reaching the real";
13
14
 
14
15
  export function wrapDepth(env: Record<string, string | undefined> = process.env): number {
15
16
  const n = Number(env[WRAP_DEPTH_ENV] ?? "");
@@ -96,7 +97,7 @@ export function verifyRealClaude(bin: string): string | null {
96
97
  try {
97
98
  p = Bun.spawnSync([bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
98
99
  } catch (e) {
99
- return e instanceof Error ? e.message : String(e);
100
+ return errorMessage(e);
100
101
  }
101
102
  const outText = (p.stdout?.toString() ?? "").trim();
102
103
  const err = (p.stderr?.toString() ?? "").trim();
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { delay } from "es-toolkit";
4
- import { log } from "./log.ts";
4
+ import { errorMessage, log } from "./log.ts";
5
5
  import { ErrnoSchema } from "./types.ts";
6
6
 
7
7
  const STALE_MS = 60_000;
@@ -48,7 +48,7 @@ export async function withClaudeRefreshLock<T>(
48
48
  break;
49
49
  }
50
50
  } catch (e) {
51
- log("claudelock.legacy_error", { err: e instanceof Error ? e.message : String(e) });
51
+ log("claudelock.legacy_error", { err: errorMessage(e) });
52
52
  held.push(primary);
53
53
  break;
54
54
  }
package/src/lib/cloud.ts CHANGED
@@ -6,8 +6,8 @@ import { paths } from "./paths.ts";
6
6
  import { writeFileAtomic } from "./atomic.ts";
7
7
  import { UNMANAGED_ENV, resolveRealClaude } from "./claudebin.ts";
8
8
  import { pidExists } from "./proc.ts";
9
- import { CloudTokenSchema, CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
10
- import { normalizeResetsAt, parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
9
+ import { CloudTokensSchema, type CloudToken } from "./setuptokens.ts";
10
+ import { parseErrorBody, readTranscriptTail, scrubCredentialEnv } from "./usage.ts";
11
11
 
12
12
  export const TOKENS_ENV = "TOKENMAXXING_TOKENS";
13
13
  export const FALLBACK_WALL_MS = 5 * 3_600_000;
@@ -73,11 +73,7 @@ export function saveCloudWalled(walled: Record<string, number>): void {
73
73
  writeFileAtomic(paths.cloudWalledJson, JSON.stringify(WalledSchema.parse(walled), null, 2) + "\n");
74
74
  }
75
75
 
76
- const PickSchema = z.discriminatedUnion("ok", [
77
- z.object({ ok: z.literal(true), token: CloudTokenSchema }),
78
- z.object({ ok: z.literal(false), earliestReset: z.number() }),
79
- ]);
80
- export type Pick = z.infer<typeof PickSchema>;
76
+ export type Pick = { ok: true; token: CloudToken } | { ok: false; earliestReset: number };
81
77
 
82
78
  export function pickToken(input: {
83
79
  tokens: CloudToken[];
@@ -171,6 +167,6 @@ export function limitWallUntil(input: { sessionId: string; sinceBytes: number; n
171
167
  const row = rows.findLast((r) => r.isApiErrorMessage === true);
172
168
  if (!row || row.error !== "rate_limit" || row.apiErrorIsTransient === true) return null;
173
169
  if ((row.quotaLimits?.rateLimitType ?? "") === "" && parseErrorBody(row.errorDetails)?.error?.type !== "rate_limit_error") return null;
174
- const resetsAt = row.quotaLimits?.resetsAt != null ? normalizeResetsAt(row.quotaLimits.resetsAt) : null;
170
+ const resetsAt = row.quotaLimits?.resetsAt ?? null;
175
171
  return resetsAt != null && resetsAt > input.now ? resetsAt : input.now + FALLBACK_WALL_MS;
176
172
  }
package/src/lib/codex.ts CHANGED
@@ -9,7 +9,7 @@ import { seatCounts } from "./presence.ts";
9
9
  import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
10
10
  import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
11
11
  import { withLock } from "./lock.ts";
12
- import { log } from "./log.ts";
12
+ import { errorMessage, log } from "./log.ts";
13
13
  import { codexPaths, codexPool, codexSeatFromEnv } from "./paths.ts";
14
14
  import { isExhausted, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
15
15
  import type { Observation, Provider, SampleReport } from "./provider.ts";
@@ -47,7 +47,7 @@ async function readCodexUsage(account: Account, now: number): Promise<CodexReadO
47
47
  try {
48
48
  auth = readCodexStoreAuth(account.id);
49
49
  } catch (e) {
50
- return { ok: false, reason: `store credential unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})`, deadGrant: false };
50
+ return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
51
51
  }
52
52
  if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
53
53
  try {
@@ -110,7 +110,7 @@ async function prepareMove(target: Account): Promise<void> {
110
110
  try {
111
111
  auth = readCodexStoreAuth(target.id);
112
112
  } catch (e) {
113
- throw new StoreUnusableError(`${target.label}'s store is unreadable (${e instanceof Error ? e.message : String(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
113
+ throw new StoreUnusableError(`${target.label}'s store is unreadable (${errorMessage(e)}) - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
114
114
  }
115
115
  if (!auth) throw new StoreUnusableError(`${target.label} has no credential in its store - re-auth with \`tokenmaxxing auth --codex ${target.label}\``);
116
116
  log("move.prepared", { account: target.id.slice(0, 8), label: target.label });
@@ -84,12 +84,7 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
84
84
  return JSON.parse(payload);
85
85
  }
86
86
 
87
- const CodexIdentitySchema = z.object({
88
- accountId: z.string(),
89
- email: z.string().nullable(),
90
- planType: z.string().nullable(),
91
- });
92
- export type CodexIdentity = z.infer<typeof CodexIdentitySchema>;
87
+ export type CodexIdentity = { accountId: string; email: string | null; planType: string | null };
93
88
 
94
89
  export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
95
90
  const { auth } = input;
@@ -99,11 +94,7 @@ export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
99
94
  if (!accountId) {
100
95
  throw new Error("codex credential carries no account id (neither tokens.account_id nor the id_token claim)");
101
96
  }
102
- return CodexIdentitySchema.parse({
103
- accountId,
104
- email: claims.email ?? null,
105
- planType: authClaims?.chatgpt_plan_type ?? null,
106
- });
97
+ return { accountId, email: claims.email ?? null, planType: authClaims?.chatgpt_plan_type ?? null };
107
98
  }
108
99
 
109
100
  export function isCodexAccessExpiring(input: { auth: CodexAuthJson; skewMs?: number; now?: number }): boolean {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
4
+ import { errorMessage } from "./log.ts";
4
5
  import { loadConfig } from "./state.ts";
5
6
  import { paths } from "./paths.ts";
6
7
 
@@ -41,7 +42,7 @@ export function verifyRealCodex(input: { bin: string }): string | null {
41
42
  try {
42
43
  p = Bun.spawnSync([input.bin, "--version"], { env, stdout: "pipe", stderr: "pipe", timeout: 15_000, killSignal: "SIGKILL" });
43
44
  } catch (e) {
44
- return e instanceof Error ? e.message : String(e);
45
+ return errorMessage(e);
45
46
  }
46
47
  const outText = (p.stdout?.toString() ?? "").trim();
47
48
  const err = (p.stderr?.toString() ?? "").trim();
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { http, oauthErrorCode, safeErrorDetail } from "./http.ts";
3
+ import { errorMessage } from "./log.ts";
3
4
  import { env } from "./paths.ts";
4
5
  import { CodexAuthJsonSchema, JsonTextSchema, type CodexAuthJson } from "./types.ts";
5
6
 
@@ -38,16 +39,10 @@ export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: numbe
38
39
  let res: Response;
39
40
  try {
40
41
  res = await http.post(TOKEN_URL, {
41
- headers: { "Content-Type": "application/json" },
42
- body: JSON.stringify({
43
- client_id: CLIENT_ID,
44
- grant_type: "refresh_token",
45
- refresh_token: auth.tokens.refresh_token,
46
- }),
42
+ json: { client_id: CLIENT_ID, grant_type: "refresh_token", refresh_token: auth.tokens.refresh_token },
47
43
  });
48
44
  } catch (e) {
49
- const message = e instanceof Error ? e.message : String(e);
50
- throw new CodexRefreshFailedError(`endpoint unreachable: ${message}`);
45
+ throw new CodexRefreshFailedError(`endpoint unreachable: ${errorMessage(e)}`);
51
46
  }
52
47
 
53
48
  const text = await res.text();