tokenmaxxing 1.56.0 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md CHANGED
@@ -59,7 +59,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
59
59
  4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the soonest reset when the pool is depleted. No credential is written.
60
60
 
61
61
  ### 3.3 Move and depleted-pool pause (supervisor)
62
- The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, compacts the conversation when the marker asks for it (`compact: true` from the Stop and SessionStart hooks and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <sid> /compact` under the old seat's store, bounded at five minutes, and a failure only prints why), and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The relaunch submits a first prompt that names the move (and the compaction, when it landed) and asks Claude to continue the task, so the conversation goes on without a keystroke: in a terminal session it is the positional prompt after the session id; in a stream-json session (`--input-format stream-json`, where the binary ignores a positional prompt) the supervisor owns stdin, relays the harness's lines to the child through a pipe, and writes the prompt as the first `user` line of the new process. The supervisor's own notices go to stderr, so a stream-json stdout stays parseable. The `SessionStart` hook (every source) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded, and after a Ctrl-C on the countdown a later marker whose `waitUntil` falls inside the skipped wait is ignored. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
62
+ The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, compacts the conversation when the marker asks for it (`compact: true` from the Stop and SessionStart hooks and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <sid> /compact` under the old seat's store, bounded at five minutes, and a failure only prints why), and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The relaunch submits a first prompt that names the move (and the compaction, when it landed) and asks Claude to continue the task, so the conversation goes on without a keystroke: in a terminal session it is the positional prompt after the session id; in a stream-json session (`--input-format stream-json`, where the binary ignores a positional prompt) the supervisor owns stdin, relays the harness's lines to the child through a pipe, and writes the prompt as the first `user` line of the new process. In a terminal session the supervisor's own notices go to stderr. In a stream-json session (`--output-format stream-json`, read from the effective flags like the relay) there is no terminal and no statusline, so the supervisor writes each notice (the compaction and its failure, the move, the depleted-pool wait with its resume time, the resume) to stdout as a `system` line of subtype `informational` and level `warning`, the shape Claude Code emits for its own print-mode notices, which the harness renders in the conversation while the stream stays parseable; the per-second countdown ticks stay terminal-only, and the supervisor writes only while no child is running. The `SessionStart` hook (every source) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded, and after a Ctrl-C on the countdown a later marker whose `waitUntil` falls inside the skipped wait is ignored. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
63
63
 
64
64
  ### 3.4 Placement (supervisor, under the pool lock)
65
65
  1. Fold every account's tee into the index.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@ const AnalysisSchema = z.object({
50
50
  resumeId: z.string().nullable(),
51
51
  continueLatest: z.boolean(),
52
52
  streamInput: z.boolean(),
53
+ streamOutput: z.boolean(),
53
54
  });
54
55
  type Analysis = z.infer<typeof AnalysisSchema>;
55
56
 
@@ -58,6 +59,7 @@ export function analyzeArgs(argv: string[]): Analysis {
58
59
  let resumeId: string | null = null;
59
60
  let continueLatest = false;
60
61
  let streamInput = false;
62
+ let streamOutput = false;
61
63
  let printMode = false;
62
64
  let invalidSessionArg = false;
63
65
  let pickerResume = false;
@@ -66,7 +68,8 @@ export function analyzeArgs(argv: string[]): Analysis {
66
68
 
67
69
  for (let i = 0; i < argv.length; i++) {
68
70
  const a = argv[i]!;
69
- if (a === "-p" || a === "--print") printMode = true;
71
+ if (a === "--") break;
72
+ else if (a === "-p" || a === "--print") printMode = true;
70
73
  else if (a === "--version" || a === "-v" || a === "--help" || a === "-h") printMode = true;
71
74
  else if (a === "--session-id") {
72
75
  const next = argv[++i] ?? null;
@@ -92,6 +95,8 @@ export function analyzeArgs(argv: string[]): Analysis {
92
95
  }
93
96
  else if (a === "--input-format") streamInput = argv[++i] === "stream-json";
94
97
  else if (a.startsWith("--input-format=")) streamInput = a.slice("--input-format=".length) === "stream-json";
98
+ else if (a === "--output-format") streamOutput = argv[++i] === "stream-json";
99
+ else if (a.startsWith("--output-format=")) streamOutput = a.slice("--output-format=".length) === "stream-json";
95
100
  else if (VALUE_TAKING_ROOT_FLAGS.has(a)) i++;
96
101
  else if (VARIADIC_ROOT_FLAGS.has(a)) {
97
102
  while (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) i++;
@@ -107,7 +112,7 @@ export function analyzeArgs(argv: string[]): Analysis {
107
112
  const isSubcmd = firstPositional !== null && NONINTERACTIVE_SUBCMDS.has(firstPositional);
108
113
  const forkResume = forkSession && (resumeId !== null || continueLatest);
109
114
  const manage = !printMode && !isSubcmd && !invalidSessionArg && !pickerResume && !forkResume && !process.env.TOKENMAXXING_PROBE;
110
- return { manage, sessionId, resumeId, continueLatest, streamInput };
115
+ return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput };
111
116
  }
112
117
 
113
118
  export function stripSessionFlags(argv: string[]): string[] {
@@ -222,20 +227,34 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate):
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
 
@@ -406,7 +425,14 @@ export async function runSupervisor(argv: string[]): Promise<number> {
406
425
  process.on("SIGINT", () => {});
407
426
  process.on("SIGHUP", () => {});
408
427
 
409
- const relay = analyzeArgs(base).streamInput ? new StdinRelay() : null;
428
+ const effective = analyzeArgs(base);
429
+ const relay = effective.streamInput ? new StdinRelay() : null;
430
+ const stream = effective.streamOutput;
431
+ let noticeSid = sid;
432
+ const say: Say = (terminal, text) => {
433
+ if (stream) process.stdout.write(systemLine(noticeSid, text));
434
+ else process.stderr.write(terminal);
435
+ };
410
436
  let firstLine: string | null = null;
411
437
  let respawns = 0;
412
438
  let overriddenUntil = 0;
@@ -475,23 +501,24 @@ export async function runSupervisor(argv: string[]): Promise<number> {
475
501
  if (m) {
476
502
  rmSync(marker, { force: true });
477
503
  respawns++;
504
+ noticeSid = m.sessionId;
478
505
  const label = loadAccounts(claudePool).accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
479
506
  const resumable = existsSync(transcriptPath(m.sessionId));
480
507
  let compacted = false;
481
508
  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`);
509
+ 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
510
  const compactEnv: Record<string, string | undefined> = { ...childEnv, TOKENMAXXING_PROBE: "1", CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) };
484
511
  delete compactEnv.TOKENMAXXING_SUPERVISED;
485
512
  delete compactEnv.TOKENMAXXING_SESSION_ID;
486
513
  delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
487
514
  const outcome = await compactClaudeSession({ real, sid: m.sessionId, env: compactEnv });
488
515
  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`);
516
+ 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
517
  compacted = outcome.ok;
491
518
  }
492
519
  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`);
520
+ if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
521
+ } else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
495
522
  wanted = m.accountId;
496
523
  saveSessionFlags(m.sessionId, persistable, process.cwd());
497
524
  const prompt = resumable ? resumePrompt(compacted) : null;