tokenmaxxing 1.55.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/README.md CHANGED
@@ -73,7 +73,7 @@ claude # use claude as always; each session gets its own accoun
73
73
  | `tokenmaxxing check` | fold fresh tees, sample up to three stale accounts, and self-update a Bun global install once a day; the periodic timer runs this every tick |
74
74
  | `tokenmaxxing doctor` | verify the install: PATH order, `claudeBin`, the five settings entries, the timer, the credential identity of every Codex store and of every Claude store whose access token is still fresh (an expiring Claude token is reported as unverifiable, not as a failure), codex hook trust, setup-token age, and shell aliases that shadow `claude` |
75
75
  | `tokenmaxxing rename [--codex \| --grok \| --opencode-go] <sel> <label>` / `rm [...] <sel>` | manage a pool (one email can hold both a claude and a codex account); `rm` is refused for an account with a running supervised session |
76
- | `tokenmaxxing uninstall` | remove the shims, settings entries, codex Stop hook, check timer, and rc PATH line (accounts/stores kept) |
76
+ | `tokenmaxxing uninstall [--yes]` | print the targets, then remove the shims, settings entries, codex Stop hook, check timer, and rc PATH line (accounts/stores kept); refused without `--yes` when `HOME` is the login home |
77
77
  | `tokenmaxxing setup-token [--print \| rm <label\|uuid>]` | Cursor Cloud only: mint one `claude setup-token` per pooled account (a browser sign-in each) and print the `TOKENMAXXING_TOKENS` secret value; `--print` prints the stored set, `rm` drops one |
78
78
  | `tokenmaxxing cursor init [dir]` | write the Claude relay subagent and `.cursor/environment.json` into a repo |
79
79
  | `tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"` | on a Cursor Cloud VM: run `claude -p` on a setup token, rotate to the next token on a usage limit |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.55.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;
@@ -1,5 +1,5 @@
1
1
  import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
3
  import { escape } from "es-toolkit";
4
4
  import { z } from "zod";
5
5
  import { codexPaths, codexStoreDirFor, HOME, paths } from "./paths.ts";
@@ -343,21 +343,35 @@ function systemdTimerActive(): "active" | "not-active" | "unavailable" {
343
343
  }
344
344
  }
345
345
 
346
- function uninstallCheckTimer(): boolean {
347
- if (skipImperativeTimer()) return true;
346
+ function removeTimerUnits(): void {
347
+ if (process.platform === "darwin") {
348
+ rmSync(launchdPlist(), { force: true });
349
+ return;
350
+ }
351
+ rmSync(join(paths.systemdUserDir, "tokenmaxxing-check.timer"), { force: true });
352
+ rmSync(join(paths.systemdUserDir, "tokenmaxxing-check.service"), { force: true });
353
+ }
354
+
355
+ type TimerOutcome = "removed" | "skipped" | "still-loaded";
356
+
357
+ function uninstallCheckTimer(live: boolean): TimerOutcome {
358
+ if (skipImperativeTimer()) return "skipped";
359
+ if (!live) {
360
+ removeTimerUnits();
361
+ return "removed";
362
+ }
348
363
  if (process.platform === "darwin") {
349
364
  const domain = launchdDomain();
350
365
  const loaded = launchdJobLoaded();
351
366
  const deactivated = loaded === "loaded" && domain != null ? run(["launchctl", "bootout", `${domain}/${LAUNCHD_LABEL}`]) : loaded === "not-loaded";
352
- rmSync(launchdPlist(), { force: true });
353
- return deactivated;
367
+ removeTimerUnits();
368
+ return deactivated ? "removed" : "still-loaded";
354
369
  }
355
370
  const active = systemdTimerActive();
356
371
  const deactivated = active === "active" ? run(["systemctl", "--user", "disable", "--now", "tokenmaxxing-check.timer"]) : active === "not-active";
357
- rmSync(join(paths.systemdUserDir, "tokenmaxxing-check.timer"), { force: true });
358
- rmSync(join(paths.systemdUserDir, "tokenmaxxing-check.service"), { force: true });
372
+ removeTimerUnits();
359
373
  run(["systemctl", "--user", "daemon-reload"]);
360
- return deactivated;
374
+ return deactivated ? "removed" : "still-loaded";
361
375
  }
362
376
 
363
377
  export function shellRcPath(): string | null {
@@ -448,17 +462,47 @@ export function removePathFromRc(rc: string): boolean {
448
462
  return true;
449
463
  }
450
464
 
451
- const UninstallOutcomeSchema = z.object({ timerDeactivated: z.boolean(), pathLineRemoved: z.boolean() });
465
+ const UninstallOutcomeSchema = z.object({ timer: z.enum(["removed", "skipped", "still-loaded"]), pathLineRemoved: z.boolean() });
452
466
  export type UninstallOutcome = z.infer<typeof UninstallOutcomeSchema>;
453
467
 
454
- export function uninstallSupervisor(): UninstallOutcome {
468
+ export function loginHome(): string {
469
+ const cmd = process.platform === "darwin" ? ["id", "-P"] : ["getent", "passwd", String(process.getuid?.() ?? "")];
470
+ const proc = Bun.spawnSync(cmd, { stdout: "pipe", stderr: "ignore", timeout: 10_000 });
471
+ const home = proc.stdout.toString().trim().split(":").at(-2);
472
+ if (proc.exitCode !== 0 || home == null || home === "") throw new Error(`cannot read the login home: ${cmd.join(" ")} failed`);
473
+ return home;
474
+ }
475
+
476
+ export function onLoginHome(): boolean {
477
+ return resolve(HOME) === resolve(loginHome());
478
+ }
479
+
480
+ export function uninstallTargets(live: boolean): string[] {
481
+ const timer =
482
+ process.platform === "darwin"
483
+ ? `${launchdPlist()}${live ? ` (and launchctl bootout gui/${process.getuid?.() ?? "?"}/${LAUNCHD_LABEL})` : ""}`
484
+ : `${join(paths.systemdUserDir, "tokenmaxxing-check.timer")} and .service${live ? " (and systemctl --user disable --now tokenmaxxing-check.timer)" : ""}`;
485
+ const rc = shellRcPath();
486
+ return [
487
+ `${paths.claudeSettings}: the hook and statusline entries`,
488
+ `${codexPaths.hooksJson}: the codex Stop hook entry`,
489
+ ...(skipImperativeTimer() ? [] : [timer]),
490
+ paths.supervisorLink,
491
+ codexSupervisorLink(),
492
+ join(paths.binDir, "xx"),
493
+ installedBin(),
494
+ ...(rc == null ? [] : [`${rc}: the ${PATH_LINE_MARK} line`]),
495
+ ];
496
+ }
497
+
498
+ export function uninstallSupervisor(input: { liveTimer: boolean }): UninstallOutcome {
455
499
  uninstallSettings();
456
- const timerDeactivated = uninstallCheckTimer();
500
+ const timer = uninstallCheckTimer(input.liveTimer);
457
501
  uninstallCodexSupervisor();
458
502
  for (const f of [paths.supervisorLink, join(paths.binDir, "xx"), installedBin()]) {
459
503
  if (existsSync(f)) rmSync(f, { force: true });
460
504
  }
461
505
  const rc = shellRcPath();
462
506
  const pathLineRemoved = rc != null && removePathFromRc(rc);
463
- return { timerDeactivated, pathLineRemoved };
507
+ return { timer, pathLineRemoved };
464
508
  }
package/src/main.ts CHANGED
@@ -25,11 +25,13 @@ import { cmdConfig } from "./cli/config.ts";
25
25
  import { cmdSetupToken } from "./cli/setuptoken.ts";
26
26
  import { cmdCloudRun } from "./cli/cloudrun.ts";
27
27
  import { cmdCursorInit } from "./cli/cursorinit.ts";
28
- import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
28
+ import { onLoginHome, timerDeactivationHint, uninstallSupervisor, uninstallTargets } from "./lib/install.ts";
29
+ import { HOME } from "./lib/paths.ts";
29
30
  import { c, emitError } from "./cli/render.ts";
30
31
 
31
32
  const JSON_FLAG = "--json";
32
33
  const CACHED_FLAG = "--cached";
34
+ const YES_FLAG = "--yes";
33
35
  const CODEX_FLAG = "--codex";
34
36
  const GROK_FLAG = "--grok";
35
37
  const OPENCODE_GO_FLAG = "--opencode-go";
@@ -54,7 +56,7 @@ function printHelp(): void {
54
56
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
55
57
  ${c.cyan("tokenmaxxing rename")} [--codex | --grok | --opencode-go] <sel> <label>
56
58
  ${c.cyan("tokenmaxxing rm")} [--codex | --grok | --opencode-go] <sel>
57
- ${c.cyan("tokenmaxxing uninstall")} remove supervisor + settings entries
59
+ ${c.cyan("tokenmaxxing uninstall")} [--yes] print the targets, then remove supervisor + settings entries (refused without ${c.cyan("--yes")} when HOME is the login home)
58
60
  ${c.cyan("tokenmaxxing setup-token")} [--print | rm <label|uuid>] Cursor Cloud only: mint one \`claude setup-token\` per pooled account (browser sign-in each) and print the TOKENMAXXING_TOKENS secret value; ${c.cyan("--print")} prints the stored set, ${c.cyan("rm")} drops one
59
61
  ${c.cyan("tokenmaxxing cursor init")} [dir] write the Claude relay subagent (.cursor/agents/claude.md) and .cursor/environment.json into a repo
60
62
  ${c.cyan("tokenmaxxing cloud run")} [--session <id>] [--max-turns <n>] "<prompt>" on a Cursor Cloud VM: run claude -p on a setup token from TOKENMAXXING_TOKENS, rotate to the next token on a usage limit
@@ -84,13 +86,14 @@ async function main(): Promise<number> {
84
86
  jsonMode = argv.includes(JSON_FLAG);
85
87
  const json = jsonMode;
86
88
  const cached = argv.includes(CACHED_FLAG);
89
+ const yes = argv.includes(YES_FLAG);
87
90
  const providerFlags = [CODEX_FLAG, GROK_FLAG, OPENCODE_GO_FLAG].filter((f) => argv.includes(f));
88
91
  if (providerFlags.length > 1) {
89
92
  emitError({ json, message: `${providerFlags.join(" and ")} are mutually exclusive - pick one pool` });
90
93
  return 2;
91
94
  }
92
95
  const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : argv.includes(OPENCODE_GO_FLAG) ? opencodeGo : claude;
93
- const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== OPENCODE_GO_FLAG);
96
+ const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== OPENCODE_GO_FLAG);
94
97
  const sub = args[0];
95
98
 
96
99
  if (cached && sub != null && sub !== "status") {
@@ -98,6 +101,11 @@ async function main(): Promise<number> {
98
101
  return 2;
99
102
  }
100
103
 
104
+ if (yes && sub !== "uninstall") {
105
+ emitError({ json, message: `${YES_FLAG} applies to uninstall only, not ${sub ?? "status"}` });
106
+ return 2;
107
+ }
108
+
101
109
  if (provider === codex && (sub == null || !CODEX_COMMANDS.has(sub))) {
102
110
  emitError({ json, message: `${CODEX_FLAG} applies to ${[...CODEX_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
103
111
  return 2;
@@ -155,15 +163,25 @@ async function main(): Promise<number> {
155
163
  return 2;
156
164
  }
157
165
  case "uninstall": {
158
- const out = uninstallSupervisor();
166
+ if (args.length > 1) {
167
+ emitError({ message: `unknown uninstall option: ${args[1]} (uninstall takes only ${YES_FLAG})` });
168
+ return 2;
169
+ }
170
+ const live = onLoginHome();
171
+ console.log(`uninstall removes:\n${uninstallTargets(live).map((t) => ` ${t}`).join("\n")}`);
172
+ if (live && !yes) {
173
+ emitError({ message: `refused: HOME is the login home (${HOME}) - rerun with ${YES_FLAG} to remove these from the live install` });
174
+ return 2;
175
+ }
176
+ const out = uninstallSupervisor({ liveTimer: live });
159
177
  const removed = [
160
178
  "supervisor wrapper",
161
179
  "settings entries",
162
- ...(out.timerDeactivated ? ["check timer"] : []),
180
+ ...(out.timer === "removed" ? ["check timer"] : []),
163
181
  ...(out.pathLineRemoved ? ["rc PATH line"] : []),
164
182
  ];
165
183
  console.log(`removed ${removed.join(", ")}`);
166
- if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
184
+ if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
167
185
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
168
186
  console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; grok: grok-stores/; opencode-go: opencode-go-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
169
187
  return 0;