tokenmaxxing 1.10.0 → 1.11.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/README.md +1 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/pool-status/SKILL.md +2 -2
- package/agent-plugin/skills/pool-status/references/commands.md +1 -0
- package/package.json +1 -1
- package/src/cli/check.ts +17 -4
- package/src/cli/codexrm.ts +8 -7
- package/src/cli/codexswitch.ts +48 -18
- package/src/cli/config.ts +86 -42
- package/src/cli/doctor.ts +25 -9
- package/src/cli/ls.ts +37 -2
- package/src/cli/rename.ts +17 -14
- package/src/cli/render.ts +20 -0
- package/src/cli/rm.ts +12 -8
- package/src/cli/status.ts +239 -110
- package/src/cli/switch.ts +64 -28
- package/src/cli/watch.ts +13 -7
- package/src/entries/mcp.ts +1 -1
- package/src/main.ts +44 -22
package/src/cli/switch.ts
CHANGED
|
@@ -7,15 +7,24 @@ import { currentWins, effectiveBars, pickBest, pickEarliestReset, weeklyExpiry,
|
|
|
7
7
|
import { InvalidGrantError } from "../lib/oauth.ts";
|
|
8
8
|
import { gatedFamilies } from "../lib/usage.ts";
|
|
9
9
|
import { findAccount } from "./rename.ts";
|
|
10
|
-
import { c, fmtReset } from "./render.ts";
|
|
10
|
+
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
11
11
|
import type { Account } from "../lib/types.ts";
|
|
12
12
|
|
|
13
|
-
export async function cmdSwitch(selector?: string): Promise<number> {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
export async function cmdSwitch(selector?: string, json = false): Promise<number> {
|
|
14
|
+
const deadGrants: string[] = [];
|
|
15
|
+
const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
|
|
16
|
+
const emit = (text: string, report: Record<string, unknown>): void => {
|
|
17
|
+
if (json) emitJson({ ok: true, ...withDeadGrants(report) });
|
|
18
|
+
else console.log(text);
|
|
19
|
+
};
|
|
20
|
+
const fail = (message: string, opts: { paint?: (s: string) => string; extra?: Record<string, unknown> } = {}): number => {
|
|
21
|
+
emitError({ json, message, paint: opts.paint, extra: withDeadGrants(opts.extra ?? {}) });
|
|
17
22
|
return 1;
|
|
18
|
-
}
|
|
23
|
+
};
|
|
24
|
+
const deadGrantMessage = (a: Account) => `${a.label}'s refresh token is dead - run \`tokenmaxxing auth ${a.label}\``;
|
|
25
|
+
|
|
26
|
+
const idx0 = loadAccounts();
|
|
27
|
+
if (idx0.accounts.length < 2) return fail("need at least 2 accounts to switch - add one with `tokenmaxxing add`", { paint: c.yellow });
|
|
19
28
|
const cfg = loadConfig();
|
|
20
29
|
const now = Date.now();
|
|
21
30
|
|
|
@@ -26,23 +35,29 @@ export async function cmdSwitch(selector?: string): Promise<number> {
|
|
|
26
35
|
const claimedOrg = liveClaim?.organizationUuid ?? null;
|
|
27
36
|
const drifted = claimed != null && claimed !== idx.activeAccountUuid;
|
|
28
37
|
|
|
29
|
-
const swapTo = async (target: Account): Promise<number> => {
|
|
38
|
+
const swapTo = async (target: Account, reason: string, extra: Record<string, unknown> = {}): Promise<number> => {
|
|
30
39
|
try {
|
|
31
40
|
await performSwap(target);
|
|
32
41
|
} catch (e) {
|
|
33
|
-
if (e instanceof InvalidGrantError) {
|
|
42
|
+
if (e instanceof InvalidGrantError) {
|
|
43
|
+
deadGrants.push(target.label);
|
|
44
|
+
return fail(deadGrantMessage(target));
|
|
45
|
+
}
|
|
34
46
|
throw e;
|
|
35
47
|
}
|
|
36
|
-
|
|
48
|
+
emit(`${c.green("↻")} switched to ${c.bold(target.label)}`, { switched: true, account: target.label, reason, ...extra });
|
|
37
49
|
return 0;
|
|
38
50
|
};
|
|
39
51
|
|
|
40
52
|
if (selector) {
|
|
41
53
|
const target = findAccount(idx.accounts, selector);
|
|
42
|
-
if (!target)
|
|
43
|
-
if (target.accountUuid === idx.activeAccountUuid && !drifted) {
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
if (!target) return fail(`no account matches "${selector}"`);
|
|
55
|
+
if (target.accountUuid === idx.activeAccountUuid && !drifted) {
|
|
56
|
+
emit(`already on ${c.bold(target.label)}`, { switched: false, account: target.label, reason: "already-on" });
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (target.needsReauth) return fail(`${target.label} needs re-auth - run \`tokenmaxxing auth ${target.label}\``);
|
|
60
|
+
return swapTo(target, "selected");
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
const switchFamilies = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
@@ -60,10 +75,15 @@ export async function cmdSwitch(selector?: string): Promise<number> {
|
|
|
60
75
|
cur.accounts.find((a) => a.accountUuid === cur.activeAccountUuid) ??
|
|
61
76
|
null;
|
|
62
77
|
if (active != null && currentWins(active, cur.accounts, everyone)) {
|
|
63
|
-
if (drifted) return swapTo(active);
|
|
78
|
+
if (drifted) return swapTo(active, "drift-reconciled");
|
|
64
79
|
const expiry = weeklyExpiry(active, now);
|
|
65
80
|
const why = Number.isFinite(expiry) ? ` (weekly ${fmtReset(expiry, now)})` : "";
|
|
66
|
-
|
|
81
|
+
emit(`already on the best account: ${c.bold(active.label)}${why}`, {
|
|
82
|
+
switched: false,
|
|
83
|
+
account: active.label,
|
|
84
|
+
reason: "current-wins",
|
|
85
|
+
weeklyResetsAt: Number.isFinite(expiry) ? expiry : null,
|
|
86
|
+
});
|
|
67
87
|
return 0;
|
|
68
88
|
}
|
|
69
89
|
const best = pickBest(cur.accounts, { ...everyone, currentAccountUuid: active?.accountUuid ?? null });
|
|
@@ -72,37 +92,53 @@ export async function cmdSwitch(selector?: string): Promise<number> {
|
|
|
72
92
|
await performSwap(best);
|
|
73
93
|
} catch (e) {
|
|
74
94
|
if (e instanceof InvalidGrantError) {
|
|
75
|
-
|
|
95
|
+
deadGrants.push(best.label);
|
|
96
|
+
if (!json) console.error(c.red(deadGrantMessage(best)));
|
|
76
97
|
continue;
|
|
77
98
|
}
|
|
78
99
|
throw e;
|
|
79
100
|
}
|
|
80
|
-
|
|
101
|
+
emit(`${c.green("↻")} switched to ${c.bold(best.label)}`, { switched: true, account: best.label, reason: "best" });
|
|
81
102
|
return 0;
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
const fresh = loadAccounts();
|
|
85
106
|
const earliest = pickEarliestReset(fresh.accounts, everyoneIn(fresh.accounts));
|
|
107
|
+
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
86
108
|
if (!earliest) {
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
if (reauth.length > 0) {
|
|
110
|
+
return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth --all\`): ${reauth.join(", ")}`, {
|
|
111
|
+
paint: c.yellow,
|
|
112
|
+
extra: { reauthNeeded: reauth },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
89
115
|
const freshActive = fresh.accounts.find((a) => a.accountUuid === fresh.activeAccountUuid) ?? null;
|
|
90
|
-
if (drifted && freshActive) return swapTo(freshActive);
|
|
91
|
-
|
|
116
|
+
if (drifted && freshActive) return swapTo(freshActive, "drift-reconciled");
|
|
117
|
+
emit(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"), {
|
|
118
|
+
switched: false,
|
|
119
|
+
account: freshActive?.label ?? null,
|
|
120
|
+
reason: "unknown-resets",
|
|
121
|
+
});
|
|
92
122
|
return 0;
|
|
93
123
|
}
|
|
94
|
-
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
95
124
|
const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
|
|
125
|
+
const availableAt = earliest.availableAt > now ? earliest.availableAt : null;
|
|
96
126
|
if (earliest.account.accountUuid === fresh.activeAccountUuid && !drifted) {
|
|
97
|
-
const msg =
|
|
127
|
+
const msg = availableAt == null
|
|
98
128
|
? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
|
|
99
|
-
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(
|
|
100
|
-
|
|
129
|
+
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(availableAt, now)})${reauthNote}`;
|
|
130
|
+
emit(c.yellow(msg), {
|
|
131
|
+
switched: false,
|
|
132
|
+
account: earliest.account.label,
|
|
133
|
+
reason: availableAt == null ? "no-target" : "all-at-limit",
|
|
134
|
+
availableAt,
|
|
135
|
+
reauthNeeded: reauth,
|
|
136
|
+
});
|
|
101
137
|
return 0;
|
|
102
138
|
}
|
|
103
|
-
const code = await swapTo(earliest.account);
|
|
104
|
-
if (code === 0 &&
|
|
105
|
-
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(
|
|
139
|
+
const code = await swapTo(earliest.account, "earliest-reset", { availableAt, reauthNeeded: reauth });
|
|
140
|
+
if (code === 0 && availableAt != null && !json) {
|
|
141
|
+
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(availableAt, now)})${reauthNote}`));
|
|
106
142
|
}
|
|
107
143
|
return code;
|
|
108
144
|
});
|
package/src/cli/watch.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clamp, delay } from "es-toolkit";
|
|
2
2
|
import { loadAccounts } from "../lib/state.ts";
|
|
3
|
+
import { loadCodexAccounts } from "../lib/codexstate.ts";
|
|
3
4
|
import { cmdStatus } from "./status.ts";
|
|
4
|
-
import { c } from "./render.ts";
|
|
5
|
+
import { c, emitError, emitJson } from "./render.ts";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_INTERVAL_S = 120;
|
|
7
8
|
const MIN_INTERVAL_S = 30;
|
|
@@ -16,13 +17,13 @@ export function resolveWatchInterval(arg?: string): number | null {
|
|
|
16
17
|
|
|
17
18
|
const CLEAR = "\x1b[H\x1b[2J\x1b[3J";
|
|
18
19
|
|
|
19
|
-
export async function cmdWatch(intervalArg?: string): Promise<number> {
|
|
20
|
+
export async function cmdWatch(intervalArg?: string, json = false): Promise<number> {
|
|
20
21
|
const intervalS = resolveWatchInterval(intervalArg);
|
|
21
22
|
if (intervalS === null) {
|
|
22
|
-
|
|
23
|
+
emitError({ json, message: `watch interval must be a positive number of seconds, got: ${intervalArg}` });
|
|
23
24
|
return 2;
|
|
24
25
|
}
|
|
25
|
-
if (loadAccounts().accounts.length === 0) return cmdStatus();
|
|
26
|
+
if (loadAccounts().accounts.length === 0 && loadCodexAccounts().accounts.length === 0) return cmdStatus({ json });
|
|
26
27
|
|
|
27
28
|
const paintHeader = () => {
|
|
28
29
|
process.stdout.write(process.stdout.isTTY ? CLEAR : "\n");
|
|
@@ -30,10 +31,15 @@ export async function cmdWatch(intervalArg?: string): Promise<number> {
|
|
|
30
31
|
};
|
|
31
32
|
while (true) {
|
|
32
33
|
try {
|
|
33
|
-
await cmdStatus(
|
|
34
|
+
await cmdStatus(json ? { json } : { preRender: paintHeader });
|
|
34
35
|
} catch (e) {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const message = `status failed this tick: ${e instanceof Error ? e.message : String(e)}`;
|
|
37
|
+
if (json) {
|
|
38
|
+
emitJson({ ok: false, error: message });
|
|
39
|
+
} else {
|
|
40
|
+
paintHeader();
|
|
41
|
+
console.error(c.red(message));
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
await delay(intervalS * 1000);
|
|
39
45
|
}
|
package/src/entries/mcp.ts
CHANGED
|
@@ -151,7 +151,7 @@ export function createTokenmaxxingMcpServer(): McpServer {
|
|
|
151
151
|
inputSchema: {},
|
|
152
152
|
},
|
|
153
153
|
async () => {
|
|
154
|
-
const cap = await captureCli(() => cmdStatus(
|
|
154
|
+
const cap = await captureCli(() => cmdStatus());
|
|
155
155
|
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
156
156
|
},
|
|
157
157
|
);
|
package/src/main.ts
CHANGED
|
@@ -26,7 +26,10 @@ import { cmdSwitch } from "./cli/switch.ts";
|
|
|
26
26
|
import { cmdCheck } from "./cli/check.ts";
|
|
27
27
|
import { cmdConfig } from "./cli/config.ts";
|
|
28
28
|
import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
|
|
29
|
-
import { c } from "./cli/render.ts";
|
|
29
|
+
import { c, emitError, emitJson } from "./cli/render.ts";
|
|
30
|
+
|
|
31
|
+
const JSON_FLAG = "--json";
|
|
32
|
+
const INTERACTIVE_COMMANDS = new Set(["init", "add", "auth"]);
|
|
30
33
|
|
|
31
34
|
function printHelp(): void {
|
|
32
35
|
console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
|
|
@@ -50,30 +53,45 @@ function printHelp(): void {
|
|
|
50
53
|
${c.cyan("tokenmaxxing rm")} [--codex] <sel>
|
|
51
54
|
${c.cyan("tokenmaxxing uninstall")} remove supervisor + settings entries
|
|
52
55
|
|
|
56
|
+
${c.cyan("--json")} print one JSON document on stdout instead of text (status, ls, config, doctor, check, switch, rename, rm, uninstall; one per tick for watch); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
|
|
57
|
+
|
|
53
58
|
${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
let jsonMode = false;
|
|
62
|
+
|
|
56
63
|
async function main(): Promise<number> {
|
|
57
64
|
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
58
65
|
console.error(`tokenmaxxing supports macOS and Linux only (this is ${process.platform})`);
|
|
59
66
|
return 1;
|
|
60
67
|
}
|
|
61
|
-
const
|
|
68
|
+
const argv = process.argv.slice(2);
|
|
62
69
|
const argv0 = basename(process.argv0 || process.argv[0] || "");
|
|
63
|
-
const sub = args[0];
|
|
64
70
|
|
|
65
|
-
if (argv0 === "claude" ||
|
|
66
|
-
return runSupervisor(
|
|
71
|
+
if (argv0 === "claude" || argv[0] === "__supervise") {
|
|
72
|
+
return runSupervisor(argv[0] === "__supervise" ? argv.slice(1) : argv);
|
|
67
73
|
}
|
|
68
|
-
if (argv0 === "codex" ||
|
|
69
|
-
return runCodexSupervisor({ argv:
|
|
74
|
+
if (argv0 === "codex" || argv[0] === "__supervise-codex") {
|
|
75
|
+
return runCodexSupervisor({ argv: argv[0] === "__supervise-codex" ? argv.slice(1) : argv });
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
jsonMode = argv.includes(JSON_FLAG);
|
|
79
|
+
const json = jsonMode;
|
|
80
|
+
const args = argv.filter((a) => a !== JSON_FLAG);
|
|
81
|
+
const sub = args[0];
|
|
82
|
+
|
|
83
|
+
if (json && sub != null && INTERACTIVE_COMMANDS.has(sub)) {
|
|
84
|
+
emitError({ json, message: `${sub} is interactive (it runs a login flow) and has no --json form` });
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
72
87
|
if (!(sub != null && sub.startsWith("__")) && !process.env.TOKENMAXXING_PROBE) {
|
|
73
88
|
const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
|
|
74
89
|
const ambient = nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
|
|
75
90
|
if (ambient != null) {
|
|
76
|
-
|
|
91
|
+
emitError({
|
|
92
|
+
json,
|
|
93
|
+
message: `CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): claude uses a namespaced credential store there that tokenmaxxing does not manage - unset it (or run from a clean shell) and retry.`,
|
|
94
|
+
});
|
|
77
95
|
return 1;
|
|
78
96
|
}
|
|
79
97
|
}
|
|
@@ -85,26 +103,26 @@ async function main(): Promise<number> {
|
|
|
85
103
|
case "__stop-failure-hook": return runStopFailureHook();
|
|
86
104
|
case "__session-start": return runSessionStart();
|
|
87
105
|
case "__codex-stop-hook": return runCodexStopHook();
|
|
88
|
-
case undefined: return cmdStatus();
|
|
89
|
-
case "--force": return cmdStatus(true);
|
|
106
|
+
case undefined: return cmdStatus({ json });
|
|
107
|
+
case "--force": return cmdStatus({ force: true, json });
|
|
90
108
|
case "switch": {
|
|
91
109
|
const rest = args.slice(1).filter((a) => a !== "--codex");
|
|
92
|
-
return args.includes("--codex") ? cmdCodexSwitch(rest[0]) : cmdSwitch(rest[0]);
|
|
110
|
+
return args.includes("--codex") ? cmdCodexSwitch(rest[0], json) : cmdSwitch(rest[0], json);
|
|
93
111
|
}
|
|
94
|
-
case "check": return cmdCheck(args.slice(1));
|
|
95
|
-
case "config": return cmdConfig(args.slice(1));
|
|
112
|
+
case "check": return cmdCheck(args.slice(1), json);
|
|
113
|
+
case "config": return cmdConfig(args.slice(1), json);
|
|
96
114
|
case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
|
|
97
115
|
case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
|
|
98
116
|
case "auth": return cmdAuth(args.slice(1));
|
|
99
|
-
case "ls": return cmdLs();
|
|
100
|
-
case "status": return cmdStatus(args.includes("--force"));
|
|
101
|
-
case "watch": return cmdWatch(args[1]);
|
|
102
|
-
case "doctor": return cmdDoctor();
|
|
117
|
+
case "ls": return cmdLs(json);
|
|
118
|
+
case "status": return cmdStatus({ force: args.includes("--force"), json });
|
|
119
|
+
case "watch": return cmdWatch(args[1], json);
|
|
120
|
+
case "doctor": return cmdDoctor(json);
|
|
103
121
|
case "rm": {
|
|
104
122
|
const rest = args.slice(1).filter((a) => a !== "--codex");
|
|
105
|
-
return args.includes("--codex") ? cmdCodexRm(rest[0]) : cmdRm(rest[0]);
|
|
123
|
+
return args.includes("--codex") ? cmdCodexRm(rest[0], json) : cmdRm(rest[0], json);
|
|
106
124
|
}
|
|
107
|
-
case "rename": return cmdRename(args.slice(1));
|
|
125
|
+
case "rename": return cmdRename(args.slice(1), json);
|
|
108
126
|
case "uninstall": {
|
|
109
127
|
const out = uninstallSupervisor();
|
|
110
128
|
const removed = [
|
|
@@ -113,6 +131,10 @@ async function main(): Promise<number> {
|
|
|
113
131
|
...(out.timerDeactivated ? ["check timer"] : []),
|
|
114
132
|
...(out.pathLineRemoved ? ["rc PATH line"] : []),
|
|
115
133
|
];
|
|
134
|
+
if (json) {
|
|
135
|
+
emitJson({ ok: true, removed, timerDeactivated: out.timerDeactivated, pathLineRemoved: out.pathLineRemoved });
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
116
138
|
console.log(`removed ${removed.join(", ")}`);
|
|
117
139
|
if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
|
|
118
140
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
@@ -125,8 +147,8 @@ async function main(): Promise<number> {
|
|
|
125
147
|
printHelp();
|
|
126
148
|
return 0;
|
|
127
149
|
default:
|
|
128
|
-
|
|
129
|
-
printHelp();
|
|
150
|
+
emitError({ json, message: `unknown command: ${sub}` });
|
|
151
|
+
if (!json) printHelp();
|
|
130
152
|
return 2;
|
|
131
153
|
}
|
|
132
154
|
}
|
|
@@ -134,6 +156,6 @@ async function main(): Promise<number> {
|
|
|
134
156
|
try {
|
|
135
157
|
process.exit(await main());
|
|
136
158
|
} catch (e) {
|
|
137
|
-
|
|
159
|
+
emitError({ json: jsonMode, message: e instanceof Error ? e.message : String(e) });
|
|
138
160
|
process.exit(1);
|
|
139
161
|
}
|