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 +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/entries/supervisor.ts +41 -14
- package/src/lib/install.ts +56 -12
- package/src/main.ts +24 -6
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.
|
|
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.
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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;
|
package/src/lib/install.ts
CHANGED
|
@@ -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
|
|
347
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
180
|
+
...(out.timer === "removed" ? ["check timer"] : []),
|
|
163
181
|
...(out.pathLineRemoved ? ["rc PATH line"] : []),
|
|
164
182
|
];
|
|
165
183
|
console.log(`removed ${removed.join(", ")}`);
|
|
166
|
-
if (
|
|
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;
|