tokenmaxxing 1.54.0 → 1.56.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
@@ -124,7 +124,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
124
124
  ---
125
125
 
126
126
  ## 8. Stack
127
- TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` and `codex` supervisors, the statusLine and subagent statusLine shims, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry (a longer `/bin/sh` shim under Nix that drops its own directory from PATH; the Stop path runs every turn, and bun's start-up stays low-millisecond). A Bun global install refreshes itself from npm on the check tick, once a day. Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a merge that changes a packed file releases itself: CI assigns the next minor version on main, publishes, and tags it (details in AGENTS.md "Release and CI"). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
127
+ TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` and `codex` supervisors, the statusLine and subagent statusLine shims, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry with `--no-env-file`, so a `.env` in the working directory is not imported into the supervisor or the session (a longer `/bin/sh` shim under Nix that drops its own directory from PATH; the Stop path runs every turn, and bun's start-up stays low-millisecond). A Bun global install refreshes itself from npm on the check tick, once a day. Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a merge that changes a packed file releases itself: CI assigns the next minor version on main, publishes, and tags it (details in AGENTS.md "Release and CI"). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
128
128
 
129
129
  ---
130
130
 
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.54.0",
3
+ "version": "1.56.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",
@@ -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";
@@ -86,7 +86,7 @@ export PATH
86
86
  if command -v tokenmaxxing >/dev/null 2>&1; then
87
87
  exec tokenmaxxing "$@"
88
88
  fi
89
- exec ${JSON.stringify(bun)} run ${JSON.stringify(entry)} "$@"
89
+ exec ${JSON.stringify(bun)} --no-env-file run ${JSON.stringify(entry)} "$@"
90
90
  `;
91
91
  }
92
92
 
@@ -97,7 +97,7 @@ export function installSupervisor(): InstallOutcome {
97
97
  if (isNixPackaged()) {
98
98
  writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
99
99
  } else {
100
- writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
100
+ writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} --no-env-file run ${JSON.stringify(entry)} "$@"\n`, 0o755);
101
101
  }
102
102
 
103
103
  writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
@@ -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;