tokenmaxxing 1.174.0 → 1.176.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 +6 -4
- package/package.json +1 -1
- package/src/cli/auth.ts +1 -1
- package/src/cli/rename.ts +1 -1
- package/src/cli/status.ts +16 -7
- package/src/entries/supervisor.ts +15 -10
- package/src/lib/cursor.ts +185 -0
- package/src/lib/onboard.ts +1 -1
- package/src/lib/paths.ts +18 -0
- package/src/lib/provider.ts +1 -1
- package/src/lib/sessions.ts +2 -2
- package/src/lib/state.ts +2 -1
- package/src/lib/statusonly.ts +4 -1
- package/src/lib/types.ts +1 -0
- package/src/main.ts +14 -11
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Automatic Claude Code account switching.** Run `claude` exactly as you always do; each session starts on the pooled account with room for it whose weekly quota is closest to expiring unused, and when that account nears its usage limit, tokenmaxxing resumes the session on a fresher account at a safe turn boundary - same conversation, compacted on the account it leaves and restarted under another account's credential store with a first prompt that asks Claude to continue. Works across many concurrent sessions, which spread over the pool instead of draining one account together. When the whole pool is at its limit, a session queues first come first served for a reset (at most eight sessions wait on one account; overflow waits for the next reset with room) and pauses with a countdown until its assigned reset, when that reset falls within twice `policy.maxWaitMs` (default 1h - a longer wait stays put rather than parking your terminal for hours).
|
|
4
4
|
|
|
5
|
-
> **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI and Muse Code as further switching pools; grok as
|
|
5
|
+
> **Scope:** Claude Code on macOS and Linux, plus OpenAI's Codex CLI and Muse Code as further switching pools; grok and Cursor as status-only pools. It pools **subscription** accounts (Pro/Max), not API keys.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
$ claude
|
|
@@ -40,15 +40,16 @@ claude # use claude as always; each session gets its own accoun
|
|
|
40
40
|
| `tokenmaxxing init --codex` | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
|
|
41
41
|
| `tokenmaxxing init --pi` | verify and pin the real `pi` and install the `pi` supervisor, which runs pi sessions on pooled Claude and ChatGPT accounts that have a pi login |
|
|
42
42
|
| `tokenmaxxing init --grok` | pool the first grok login, status-only: no supervisor, no switching |
|
|
43
|
+
| `tokenmaxxing init --cursor` | pool the Cursor CLI login, status-only: no supervisor, no switching |
|
|
43
44
|
| `tokenmaxxing init --muse` | same for Muse Code: log in the first account (isolated), install the `muse` supervisor, which starts each session on the freshest account and resumes it on another near quota |
|
|
44
|
-
| `tokenmaxxing add [--codex \| --grok \| --muse]` | register an additional account in that pool (isolated login, harvested once into its own store) |
|
|
45
|
-
| `tokenmaxxing auth [--codex \| --grok \| --muse] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every account that is flagged or has no usable credential in its store, one by one |
|
|
45
|
+
| `tokenmaxxing add [--codex \| --grok \| --muse \| --cursor]` | register an additional account in that pool (isolated login, harvested once into its own store) |
|
|
46
|
+
| `tokenmaxxing auth [--codex \| --grok \| --muse \| --cursor] [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every account that is flagged or has no usable credential in its store, one by one |
|
|
46
47
|
| `tokenmaxxing auth --pi [--codex] [sel \| --all]` | log a pooled Claude account (or a codex account with `--codex`) into pi, isolated, and keep that login as the account's pi credential after an identity check; `--all` walks every account without a pi login |
|
|
47
48
|
| `tokenmaxxing status [--cached]` | every pool: accounts with 5h / weekly / per-model usage bars, live session counts, exhausted-until-reset; `--cached` renders the stored figures without sampling |
|
|
48
49
|
| `tokenmaxxing config` | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
|
|
49
50
|
| `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 |
|
|
50
51
|
| `tokenmaxxing doctor` | verify the install: PATH order, `claudeBin`, the five settings entries, the timer, the hub service, 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, and shell aliases that shadow `claude` |
|
|
51
|
-
| `tokenmaxxing rename [--codex \| --grok \| --muse] <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 |
|
|
52
|
+
| `tokenmaxxing rename [--codex \| --grok \| --muse \| --cursor] <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 |
|
|
52
53
|
| `tokenmaxxing serve` | serve the CLIProxyAPI-compatible usage API on `http://localhost:8317` (`hub.port`) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows the quota of every pooled Claude and Codex account; the management key is the contents of `hub-key` in the state directory, minted on the first run, and `init` runs it as a background service beside the check timer. Details in [Usage hub](docs/content/docs/hub.mdx) |
|
|
53
54
|
| `tokenmaxxing uninstall [--yes]` | print the targets, then remove the shims, settings entries, codex Stop hook, check timer, hub service, and rc PATH line (accounts/stores kept); refused without `--yes` when `HOME` is the login home |
|
|
54
55
|
| `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, and `check` (`ok` mirrors the exit code, failures add `error`) |
|
|
@@ -126,6 +127,7 @@ pi keeps its own credential and refreshes it itself, so each pooled account gets
|
|
|
126
127
|
- **Keychain (macOS).** The first keychain access for a new store happens inside the interactive `init`, `add`, or `auth` run, where a prompt can be answered, not inside a headless hook.
|
|
127
128
|
- **Plaintext credentials (Linux).** Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
|
|
128
129
|
- **The status-only grok pool does not switch.** grok `status` shows the weekly subscription credit window, and no grok session moves.
|
|
130
|
+
- **The status-only Cursor pool does not switch, and reads only a file login.** Cursor `status` shows the included usage of the monthly billing cycle. The Cursor CLI keeps its login in the keychain on macOS, which tokenmaxxing does not read, so the pool works where the login is `auth.json` (Linux).
|
|
129
131
|
- **A Muse Code move stops the running turn.** `muse resume` takes no prompt, so a turn that was running when the move landed has to be sent again; `muse exec` and `muse serve` keep the account they started on.
|
|
130
132
|
|
|
131
133
|
## How it's built
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.176.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",
|
package/src/cli/auth.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { c, count } from "./render.ts";
|
|
|
13
13
|
import type { Account, AccountsIndex } from "../lib/types.ts";
|
|
14
14
|
|
|
15
15
|
const AUTH_USAGE =
|
|
16
|
-
"usage: tokenmaxxing auth [--codex | --grok | --muse] [<email|label|id> | --all]\n tokenmaxxing auth [--codex | --grok | --muse] --emails <email>...\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
|
|
16
|
+
"usage: tokenmaxxing auth [--codex | --grok | --muse | --cursor] [<email|label|id> | --all]\n tokenmaxxing auth [--codex | --grok | --muse | --cursor] --emails <email>...\n tokenmaxxing auth --pi [--codex] [<email|label|id> | --all]";
|
|
17
17
|
const EMAILS_FLAG = "--emails";
|
|
18
18
|
|
|
19
19
|
export type AuthPlan = { kind: "usage" } | { kind: "error"; message: string } | { kind: "pick" } | { kind: "targets"; ids: string[] };
|
package/src/cli/rename.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function findAccount(accounts: Account[], selector: string): Account | un
|
|
|
16
16
|
export async function cmdRename(p: Provider, argv: string[]): Promise<number> {
|
|
17
17
|
const [selector, newLabel] = argv;
|
|
18
18
|
if (!selector || !newLabel) {
|
|
19
|
-
emitError({ message: "usage: tokenmaxxing rename [--codex | --grok | --muse] <email|label|id> <new-label>", paint: plain });
|
|
19
|
+
emitError({ message: "usage: tokenmaxxing rename [--codex | --grok | --muse | --cursor] <email|label|id> <new-label>", paint: plain });
|
|
20
20
|
return 2;
|
|
21
21
|
}
|
|
22
22
|
return withLock(p.pool.lockFile, async () => {
|
package/src/cli/status.ts
CHANGED
|
@@ -3,16 +3,19 @@ import { creditLeft, keySessions, loadApiKeys } from "../lib/apikeys.ts";
|
|
|
3
3
|
import { claude } from "../lib/claude.ts";
|
|
4
4
|
import { codex } from "../lib/codex.ts";
|
|
5
5
|
import { grok } from "../lib/grok.ts";
|
|
6
|
+
import { cursor } from "../lib/cursor.ts";
|
|
6
7
|
import { muse } from "../lib/muse.ts";
|
|
7
8
|
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
8
9
|
import { withLock } from "../lib/lock.ts";
|
|
9
|
-
import { codexPool, grokPool, musePool } from "../lib/paths.ts";
|
|
10
|
+
import { codexPool, cursorPool, grokPool, musePool } from "../lib/paths.ts";
|
|
10
11
|
import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
11
12
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
12
13
|
import { usd } from "./key.ts";
|
|
13
14
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
14
15
|
import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
|
|
15
16
|
|
|
17
|
+
const WEEK_S = 7 * 86400;
|
|
18
|
+
|
|
16
19
|
export type WindowReport = { usedPercentage: number; bufferedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
|
|
17
20
|
|
|
18
21
|
export type UsageReport = { fiveHour: WindowReport | null; week: WindowReport | null; limits: (WindowReport & { name: string })[] };
|
|
@@ -53,7 +56,7 @@ type StatusApiKey = {
|
|
|
53
56
|
sessions: number;
|
|
54
57
|
};
|
|
55
58
|
|
|
56
|
-
export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; muse: PoolReport; apiKeys?: StatusApiKey[] };
|
|
59
|
+
export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; muse: PoolReport; cursor: PoolReport; apiKeys?: StatusApiKey[] };
|
|
57
60
|
|
|
58
61
|
function apiKeyReport(): StatusApiKey[] {
|
|
59
62
|
const sessions = keySessions();
|
|
@@ -89,7 +92,7 @@ function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): W
|
|
|
89
92
|
return {
|
|
90
93
|
usedPercentage: liveUsed(w, now),
|
|
91
94
|
bufferedPercentage: bars == null ? liveUsed(w, now) : bufferedUsed(a, w, bars, now),
|
|
92
|
-
resetsAt: isSessionWindow(w) ? (passed ? null : w.resetsAt) : nextWeeklyReset(w.resetsAt, now),
|
|
95
|
+
resetsAt: isSessionWindow(w) || (w.windowSeconds ?? 0) > WEEK_S ? (passed ? null : w.resetsAt) : nextWeeklyReset(w.resetsAt, now),
|
|
93
96
|
windowSeconds: w.windowSeconds,
|
|
94
97
|
};
|
|
95
98
|
}
|
|
@@ -161,6 +164,7 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
161
164
|
if (a.lastUsageAt != null && (stored.lastUsageAt == null || a.lastUsageAt > stored.lastUsageAt)) {
|
|
162
165
|
landWindows(stored, p.mergeWindows(a.windows, stored.windows), a.lastUsageAt, bars);
|
|
163
166
|
stored.hasCredits = a.hasCredits;
|
|
167
|
+
if (a.email != null && stored.email == null && stored.label === stored.id.slice(0, 8)) stored.label = a.email;
|
|
164
168
|
if (a.email != null) stored.email = a.email;
|
|
165
169
|
if (a.tier != null) stored.tier = a.tier;
|
|
166
170
|
dirty = true;
|
|
@@ -267,7 +271,7 @@ function card(p: Provider, a: StatusAccount, now: number, staleAfterMs: number):
|
|
|
267
271
|
const lines = [headerLine({ ...a, name: a.label })];
|
|
268
272
|
if (a.usage) {
|
|
269
273
|
if (a.usage.fiveHour) lines.push(usageRow(`${Math.round((a.usage.fiveHour.windowSeconds ?? 0) / 3600)}h`, a.usage.fiveHour));
|
|
270
|
-
if (a.usage.week) lines.push(usageRow("week", a.usage.week));
|
|
274
|
+
if (a.usage.week) lines.push(usageRow((a.usage.week.windowSeconds ?? 0) > WEEK_S ? "month" : "week", a.usage.week));
|
|
271
275
|
for (const w of a.usage.limits) lines.push(usageRow(p.windowLabel(w.name), w));
|
|
272
276
|
}
|
|
273
277
|
const notes: Note[] = [];
|
|
@@ -308,9 +312,10 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
308
312
|
const codexPooled = loadAccounts(codexPool).accounts.length > 0;
|
|
309
313
|
const grokPooled = loadAccounts(grokPool).accounts.length > 0;
|
|
310
314
|
const musePooled = loadAccounts(musePool).accounts.length > 0;
|
|
315
|
+
const cursorPooled = loadAccounts(cursorPool).accounts.length > 0;
|
|
311
316
|
if (claudeReport.accounts.length === 0) {
|
|
312
|
-
if (!codexPooled && !grokPooled && !musePooled && apiKeys.length === 0) {
|
|
313
|
-
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`, `--muse`)"));
|
|
317
|
+
if (!codexPooled && !grokPooled && !musePooled && !cursorPooled && apiKeys.length === 0) {
|
|
318
|
+
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`, `--muse`, `--cursor`)"));
|
|
314
319
|
return 0;
|
|
315
320
|
}
|
|
316
321
|
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
@@ -324,8 +329,9 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
324
329
|
const codexReport = await collect(codex, cfg, now, cached);
|
|
325
330
|
const grokReport = await collect(grok, cfg, now, cached);
|
|
326
331
|
const museReport = await collect(muse, cfg, now, cached);
|
|
332
|
+
const cursorReport = await collect(cursor, cfg, now, cached);
|
|
327
333
|
if (json) {
|
|
328
|
-
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, muse: museReport, ...(apiKeys.length > 0 ? { apiKeys } : {}) };
|
|
334
|
+
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, muse: museReport, cursor: cursorReport, ...(apiKeys.length > 0 ? { apiKeys } : {}) };
|
|
329
335
|
emitJson({ ok: true, ...report });
|
|
330
336
|
return 0;
|
|
331
337
|
}
|
|
@@ -338,5 +344,8 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
338
344
|
if (museReport.accounts.length > 0) {
|
|
339
345
|
renderPool(muse, museReport, `muse (${count({ n: museReport.accounts.length, noun: "account" })})`, Date.now(), cfg.policy.usagePollTtlMs);
|
|
340
346
|
}
|
|
347
|
+
if (cursorReport.accounts.length > 0) {
|
|
348
|
+
renderPool(cursor, cursorReport, `cursor (${count({ n: cursorReport.accounts.length, noun: "account" })}, status-only)`, Date.now(), cfg.policy.usagePollTtlMs);
|
|
349
|
+
}
|
|
341
350
|
return 0;
|
|
342
351
|
}
|
|
@@ -5,7 +5,7 @@ import { maxBy, omit } from "es-toolkit";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
7
7
|
import { CLAUDE_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
8
|
-
import { apiKeyEnv, clearSessionCost, foldCompactionCost, foldSessionCost, loadApiKeys, readApiKey, sendApiKey, usableApiKey, writeSessionCost } from "../lib/apikeys.ts";
|
|
8
|
+
import { apiKeyEnv, clearSessionCost, foldCompactionCost, foldSessionCost, isOpenRouterKey, loadApiKeys, readApiKey, sendApiKey, usableApiKey, writeSessionCost } from "../lib/apikeys.ts";
|
|
9
9
|
import { claude, pickLaunch, pickSeat, type LaunchTarget } from "../lib/claude.ts";
|
|
10
10
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
11
11
|
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
@@ -366,11 +366,14 @@ function depletedLines(sid: string, until: number, text: string): string {
|
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; onKey: boolean }): string {
|
|
369
|
+
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; onKey: boolean; openRouter: boolean }): string {
|
|
370
370
|
const where = input.onKey ? "on an API key, because every pooled account is at its limit or the previous key ran out" : "on an account with quota headroom";
|
|
371
|
-
const
|
|
371
|
+
const resumed = input.compacted
|
|
372
372
|
? `tokenmaxxing compacted this conversation and resumed the session ${where}.`
|
|
373
373
|
: `tokenmaxxing resumed this session ${where}.`;
|
|
374
|
+
const moved = input.openRouter
|
|
375
|
+
? `${resumed} The session now runs on a base URL that is not an Anthropic host, so Claude Code has turned tool search off: every tool schema is already loaded and the ToolSearch tool does not exist in this session. Call tools directly.`
|
|
376
|
+
: resumed;
|
|
374
377
|
if (input.origin === "stop") {
|
|
375
378
|
return `${moved} The previous turn finished before the move. If it waited on the user or completed the task, end this turn without restating it. If it waited on a background Bash task, Monitor, or Workflow run, relaunch that work, because the move ended it.`;
|
|
376
379
|
}
|
|
@@ -715,7 +718,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
715
718
|
if (stream) process.stdout.write(systemLine(noticeSid, text));
|
|
716
719
|
else process.stderr.write(terminal);
|
|
717
720
|
};
|
|
718
|
-
let
|
|
721
|
+
let pendingResume: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"] } | null = null;
|
|
719
722
|
let respawns = 0;
|
|
720
723
|
let wanted: { accountId: string | null; apiKeyId: string | null } | null = null;
|
|
721
724
|
let refused: string[] = [];
|
|
@@ -741,13 +744,17 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
741
744
|
}
|
|
742
745
|
const picked = target?.account ?? null;
|
|
743
746
|
const key = target?.apiKey ?? null;
|
|
744
|
-
|
|
747
|
+
const prompt = pendingResume === null ? null : resumePrompt({ ...pendingResume, onKey: key != null, openRouter: key != null && isOpenRouterKey(key) });
|
|
748
|
+
pendingResume = null;
|
|
749
|
+
const args = relay === null && prompt !== null ? [...launchArgs.slice(0, 2), prompt, ...launchArgs.slice(2)] : launchArgs;
|
|
750
|
+
const firstLine = relay !== null && prompt !== null ? userLine(prompt) : null;
|
|
751
|
+
log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, key: key?.id, args: args.join(" "), injected: firstLine !== null });
|
|
745
752
|
const open: OpenTasks = new Map();
|
|
746
753
|
const tee = stream && (picked || existsSync(claudePool.apiKeysJson)) ? seatTee(picked?.id ?? null, model, open, sid) : null;
|
|
747
754
|
const secret = key ? readApiKey(key) : null;
|
|
748
755
|
const extra: "pipe"[] = secret == null ? [] : ["pipe"];
|
|
749
756
|
const scrubbed = scrubCredEnv(childEnv);
|
|
750
|
-
const spawned = Bun.spawn([real, ...
|
|
757
|
+
const spawned = Bun.spawn([real, ...args], {
|
|
751
758
|
stdio: [relay === null ? "inherit" : "pipe", tee === null ? childStdout() : "pipe", "inherit", ...extra],
|
|
752
759
|
env: {
|
|
753
760
|
...(picked ? { ...scrubbed, CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(picked.id) } : key ? apiKeyEnv(scrubbed, key) : childEnv),
|
|
@@ -763,7 +770,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
763
770
|
if (secret != null) sendApiKey(spawned, secret);
|
|
764
771
|
drainStdout = spawned.stdout instanceof ReadableStream ? relayStdout(spawned.stdout, tee ?? undefined) : null;
|
|
765
772
|
if (relay !== null) relay.attach(spawned.stdin!, firstLine, pendingTranscript);
|
|
766
|
-
firstLine = null;
|
|
767
773
|
if (picked || key) {
|
|
768
774
|
await recordPresenceOrStop({
|
|
769
775
|
child: spawned,
|
|
@@ -870,11 +876,10 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
870
876
|
wanted = depleted ? (key ? { accountId: null, apiKeyId: key.id } : null) : m.apiKeyId != null ? { accountId: null, apiKeyId: m.apiKeyId } : { accountId: m.accountId, apiKeyId: null };
|
|
871
877
|
refused = m.refused ?? refused;
|
|
872
878
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
873
|
-
|
|
874
|
-
firstLine = relay !== null && prompt !== null ? userLine(prompt) : null;
|
|
879
|
+
pendingResume = resumable ? { compacted, origin: m.origin } : null;
|
|
875
880
|
pendingTranscript = resumable ? null : transcriptPath(m.sessionId);
|
|
876
881
|
if (!restored.has(m.sessionId)) restored.set(m.sessionId, resumable ? restoredCost(m.sessionId) : 0);
|
|
877
|
-
launchArgs = [resumable ? "--resume" : "--session-id", m.sessionId, ...
|
|
882
|
+
launchArgs = [resumable ? "--resume" : "--session-id", m.sessionId, ...persistable];
|
|
878
883
|
continue;
|
|
879
884
|
}
|
|
880
885
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
4
|
+
import { http, safeErrorDetail } from "./http.ts";
|
|
5
|
+
import { errorMessage } from "./log.ts";
|
|
6
|
+
import { cursorAuthJsonFor, cursorPaths, cursorPool, cursorStoreDirFor, env } from "./paths.ts";
|
|
7
|
+
import type { SampleReport } from "./provider.ts";
|
|
8
|
+
import { readJsonFile, type Harvest } from "./state.ts";
|
|
9
|
+
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
10
|
+
import { ErrnoSchema, JsonTextSchema, type Account, type Window } from "./types.ts";
|
|
11
|
+
import { c } from "../cli/render.ts";
|
|
12
|
+
|
|
13
|
+
const CursorAuthSchema = z.looseObject({ accessToken: z.string().optional() });
|
|
14
|
+
type CursorAuth = z.infer<typeof CursorAuthSchema>;
|
|
15
|
+
|
|
16
|
+
const CliConfigSchema = z.looseObject({
|
|
17
|
+
authInfo: z.looseObject({ authId: z.string().optional(), email: z.string().nullish() }).optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const JwtPayloadSchema = z.looseObject({ sub: z.string().min(1) });
|
|
21
|
+
|
|
22
|
+
function readOptional<T>(path: string, schema: z.ZodType<T>): T | null {
|
|
23
|
+
try {
|
|
24
|
+
return readJsonFile(path, schema);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tokenSubject(token: string): string | null {
|
|
32
|
+
const payload = token.split(".")[1] ?? "";
|
|
33
|
+
return JwtPayloadSchema.safeParse(JsonTextSchema.safeParse(Buffer.from(payload, "base64url").toString("utf8")).data).data?.sub ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The CLI caches the signed-in user next to auth.json; it is the only local copy of the email, because the token carries none.
|
|
37
|
+
function cachedEmail(authPath: string, subject: string): string | null {
|
|
38
|
+
let config: z.infer<typeof CliConfigSchema> | null;
|
|
39
|
+
try {
|
|
40
|
+
config = readOptional(join(dirname(authPath), "cli-config.json"), CliConfigSchema);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const info = config?.authInfo;
|
|
45
|
+
if (info?.authId !== subject) return null;
|
|
46
|
+
const email = info.email?.trim().toLowerCase() ?? "";
|
|
47
|
+
return email === "" ? null : email;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const harvestTokens = new WeakMap<Harvest, string>();
|
|
51
|
+
|
|
52
|
+
function readAuth(path: string): AuthEntry[] {
|
|
53
|
+
const file: CursorAuth | null = readOptional(path, CursorAuthSchema);
|
|
54
|
+
const token = file?.accessToken ?? "";
|
|
55
|
+
if (file == null || token === "") return [];
|
|
56
|
+
const subject = tokenSubject(token);
|
|
57
|
+
if (subject == null) return [{ id: "", usable: false, harvest: null }];
|
|
58
|
+
const id = new Bun.CryptoHasher("sha256").update(subject).digest("hex");
|
|
59
|
+
const harvest: Harvest = {
|
|
60
|
+
id,
|
|
61
|
+
email: cachedEmail(path, subject),
|
|
62
|
+
tier: null,
|
|
63
|
+
sample: null,
|
|
64
|
+
park: async () => {
|
|
65
|
+
writeFileAtomic(cursorAuthJsonFor(id), JSON.stringify(file, null, 2), 0o600);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
harvestTokens.set(harvest, token);
|
|
69
|
+
return [{ id, usable: true, harvest }];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const EpochMsSchema = z.coerce.number();
|
|
73
|
+
|
|
74
|
+
const PeriodUsageSchema = z.looseObject({
|
|
75
|
+
billingCycleStart: EpochMsSchema,
|
|
76
|
+
billingCycleEnd: EpochMsSchema,
|
|
77
|
+
planUsage: z
|
|
78
|
+
.looseObject({
|
|
79
|
+
includedSpend: z.number().default(0),
|
|
80
|
+
limit: z.number().default(0),
|
|
81
|
+
totalPercentUsed: z.number().optional(),
|
|
82
|
+
apiPercentUsed: z.number().optional(),
|
|
83
|
+
})
|
|
84
|
+
.optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const PlanInfoSchema = z.looseObject({ planInfo: z.looseObject({ planName: z.string().optional() }).optional() });
|
|
88
|
+
|
|
89
|
+
const MeSchema = z.looseObject({ email: z.string().optional() });
|
|
90
|
+
|
|
91
|
+
function dashboardUrl(method: string): string {
|
|
92
|
+
return `${env("TOKENMAXXING_CURSOR_API_URL", "https://api2.cursor.sh")}/aiserver.v1.DashboardService/${method}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function dashboard(token: string, method: string): Promise<Response> {
|
|
96
|
+
return http.post(dashboardUrl(method), { headers: { Authorization: `Bearer ${token}`, "Connect-Protocol-Version": "1" }, json: {} });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function dashboardField<T>(token: string, method: string, schema: z.ZodType<T>): Promise<T | null> {
|
|
100
|
+
try {
|
|
101
|
+
const res = await dashboard(token, method);
|
|
102
|
+
return res.ok ? (schema.safeParse(JsonTextSchema.safeParse(await res.text()).data).data ?? null) : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readCursorUsage(account: Account, now: number): Promise<SampleReport> {
|
|
109
|
+
let token: string;
|
|
110
|
+
try {
|
|
111
|
+
token = readOptional(cursorAuthJsonFor(account.id), CursorAuthSchema)?.accessToken ?? "";
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { ok: false, reason: `this account's store is unreadable (${errorMessage(e)}) - run \`tokenmaxxing auth --cursor\`` };
|
|
114
|
+
}
|
|
115
|
+
if (token === "") return { ok: false, reason: "no accessToken in this account's store - run `tokenmaxxing auth --cursor`" };
|
|
116
|
+
let res: Response;
|
|
117
|
+
let text: string;
|
|
118
|
+
const plan = dashboardField(token, "GetPlanInfo", PlanInfoSchema);
|
|
119
|
+
const me = account.email == null ? dashboardField(token, "GetMe", MeSchema) : null;
|
|
120
|
+
try {
|
|
121
|
+
res = await dashboard(token, "GetCurrentPeriodUsage");
|
|
122
|
+
text = await res.text();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return { ok: false, reason: `Cursor usage read failed: endpoint unreachable: ${errorMessage(e)}` };
|
|
125
|
+
}
|
|
126
|
+
if (res.status === 401 || res.status === 403) {
|
|
127
|
+
account.needsReauth = true;
|
|
128
|
+
return { ok: false, reason: `Cursor rejected the stored login (HTTP ${res.status}) - run \`tokenmaxxing auth --cursor\`` };
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok) return { ok: false, reason: `Cursor usage read failed: HTTP ${res.status}: ${safeErrorDetail({ text })}` };
|
|
131
|
+
const parsed = PeriodUsageSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
132
|
+
if (!parsed.success) return { ok: false, reason: "Cursor usage read failed: endpoint returned an unexpected body shape (withheld)" };
|
|
133
|
+
const { billingCycleStart, billingCycleEnd, planUsage } = parsed.data;
|
|
134
|
+
const total = planUsage?.totalPercentUsed ?? (planUsage != null && planUsage.limit > 0 ? Math.min(100, (planUsage.includedSpend / planUsage.limit) * 100) : null);
|
|
135
|
+
if (total == null) return { ok: false, reason: "Cursor reports no included-usage limit for this account" };
|
|
136
|
+
const cycle = { resetsAt: billingCycleEnd, windowSeconds: billingCycleEnd > billingCycleStart ? Math.round((billingCycleEnd - billingCycleStart) / 1000) : null, sampledAt: now };
|
|
137
|
+
const windows: Window[] = [{ name: null, usedPercentage: total, ...cycle }];
|
|
138
|
+
if (planUsage?.apiPercentUsed != null) windows.push({ name: "API", usedPercentage: planUsage.apiPercentUsed, ...cycle });
|
|
139
|
+
account.windows = windows;
|
|
140
|
+
account.lastUsageAt = now;
|
|
141
|
+
const planName = (await plan)?.planInfo?.planName?.trim().toLowerCase();
|
|
142
|
+
if (planName) account.tier = planName;
|
|
143
|
+
const email = (await me)?.email?.trim().toLowerCase();
|
|
144
|
+
if (email) account.email = email;
|
|
145
|
+
return { ok: true, source: "probe" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function identify(harvest: Harvest): Promise<void> {
|
|
149
|
+
const token = harvestTokens.get(harvest);
|
|
150
|
+
if (harvest.email != null || token == null) return;
|
|
151
|
+
const email = (await dashboardField(token, "GetMe", MeSchema))?.email?.trim().toLowerCase();
|
|
152
|
+
if (email) harvest.email = email;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function samplePool(accounts: Account[], now: number): Promise<Map<string, SampleReport>> {
|
|
156
|
+
const reports = new Map<string, SampleReport>();
|
|
157
|
+
await Promise.all(accounts.map(async (account) => reports.set(account.id, await readCursorUsage(account, now))));
|
|
158
|
+
return reports;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const cursor = statusOnlyProvider({
|
|
162
|
+
name: "cursor",
|
|
163
|
+
flag: " --cursor",
|
|
164
|
+
pool: cursorPool,
|
|
165
|
+
binName: "cursor-agent",
|
|
166
|
+
binKey: "cursorBin",
|
|
167
|
+
storeDirFor: cursorStoreDirFor,
|
|
168
|
+
authJsonFor: cursorAuthJsonFor,
|
|
169
|
+
homeEnv: "XDG_CONFIG_HOME",
|
|
170
|
+
loginArgs: ["login"],
|
|
171
|
+
onboardAuthRel: "cursor/auth.json",
|
|
172
|
+
liveAuthPath: () => cursorPaths.authJson,
|
|
173
|
+
readAuth,
|
|
174
|
+
liveId: () => null,
|
|
175
|
+
presence: () => new Map(),
|
|
176
|
+
versionOk: (out) => /^\d{4}\.\d{2}\.\d{2}-[0-9a-f]+$/.test(out.trim()),
|
|
177
|
+
importIntro: "Opening an isolated Cursor login for your first pooled account - the login you already have stays as it is.",
|
|
178
|
+
foundLive: () => `found the Cursor login in ${cursorPaths.authJson} - pooling it; use \`tokenmaxxing add --cursor\` for more.`,
|
|
179
|
+
install: () => {
|
|
180
|
+
console.log(c.dim("cursor pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled Cursor accounts."));
|
|
181
|
+
},
|
|
182
|
+
loginStep: () => `Approve the login in the browser that ${c.bold("cursor-agent login")} opens.`,
|
|
183
|
+
samplePool,
|
|
184
|
+
identify,
|
|
185
|
+
});
|
package/src/lib/onboard.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { paths } from "./paths.ts";
|
|
4
4
|
import { pidExists } from "./proc.ts";
|
|
5
5
|
|
|
6
|
-
const POOLS = ["claude", "codex", "pi", "grok", "muse"] as const;
|
|
6
|
+
const POOLS = ["claude", "codex", "pi", "grok", "muse", "cursor"] as const;
|
|
7
7
|
|
|
8
8
|
export type OnboardPool = (typeof POOLS)[number];
|
|
9
9
|
|
package/src/lib/paths.ts
CHANGED
|
@@ -63,6 +63,11 @@ export const musePool = {
|
|
|
63
63
|
lockFile: join(TM_HOME, "muse-lock"),
|
|
64
64
|
} as const;
|
|
65
65
|
|
|
66
|
+
export const cursorPool = {
|
|
67
|
+
accountsJson: join(TM_HOME, "cursor-accounts.json"),
|
|
68
|
+
lockFile: join(TM_HOME, "cursor-lock"),
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
66
71
|
export type PoolPaths = { accountsJson: string; lockFile: string };
|
|
67
72
|
|
|
68
73
|
const CODEX_HOME = env("TOKENMAXXING_CODEX_HOME", env("CODEX_HOME", join(HOME, ".codex")));
|
|
@@ -161,6 +166,19 @@ export function museSeatFromEnv(accountIds: string[], env: Record<string, string
|
|
|
161
166
|
return accountIds.find((id) => museStoreDirFor(id) === home) ?? null;
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
export const cursorPaths = {
|
|
170
|
+
authJson: join(env("XDG_CONFIG_HOME", join(HOME, ".config")), "cursor", "auth.json"),
|
|
171
|
+
storesDir: join(TM_HOME, "cursor-stores"),
|
|
172
|
+
} as const;
|
|
173
|
+
|
|
174
|
+
export function cursorStoreDirFor(accountId: string): string {
|
|
175
|
+
return join(cursorPaths.storesDir, shortId(accountId));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function cursorAuthJsonFor(accountId: string): string {
|
|
179
|
+
return join(cursorStoreDirFor(accountId), "auth.json");
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
export function grokSeatFromEnv(accountIds: string[], env: Record<string, string | undefined> = process.env): string | null {
|
|
165
183
|
const home = env.GROK_HOME;
|
|
166
184
|
if (home == null || home === "") return null;
|
package/src/lib/provider.ts
CHANGED
package/src/lib/sessions.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, realpathSync, rmSync, statSync, type Dirent }
|
|
|
2
2
|
import { basename, join, sep } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { errorMessage, log } from "./log.ts";
|
|
5
|
-
import { codexPaths, grokPaths, musePaths, paths, piPaths } from "./paths.ts";
|
|
5
|
+
import { codexPaths, cursorPaths, grokPaths, musePaths, paths, piPaths } from "./paths.ts";
|
|
6
6
|
import { stagingTarget, writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { presencePid } from "./presence.ts";
|
|
8
8
|
import { readJsonFile } from "./state.ts";
|
|
@@ -44,7 +44,7 @@ function recordLiveSession(sid: string, current: string): void {
|
|
|
44
44
|
|
|
45
45
|
const TMP_GRACE_MS = 3600 * 1000;
|
|
46
46
|
|
|
47
|
-
const SWEEP_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, musePaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex"), paths.costDir];
|
|
47
|
+
const SWEEP_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, musePaths.storesDir, cursorPaths.storesDir, join(piPaths.storesDir, "claude"), join(piPaths.storesDir, "codex"), paths.costDir];
|
|
48
48
|
|
|
49
49
|
function listDir(dir: string, root: string): Dirent[] {
|
|
50
50
|
if (!existsSync(dir)) return [];
|
package/src/lib/state.ts
CHANGED
|
@@ -35,11 +35,12 @@ export function loadConfig(): Config {
|
|
|
35
35
|
cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
|
|
36
36
|
cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
|
|
37
37
|
cfg.museBin = optionalEnv("TOKENMAXXING_MUSE_BIN") ?? cfg.museBin;
|
|
38
|
+
cfg.cursorBin = optionalEnv("TOKENMAXXING_CURSOR_BIN") ?? cfg.cursorBin;
|
|
38
39
|
cfg.piBin = optionalEnv("TOKENMAXXING_PI_BIN") ?? cfg.piBin;
|
|
39
40
|
return cfg;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "museBin" | "piBin";
|
|
43
|
+
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "museBin" | "cursorBin" | "piBin";
|
|
43
44
|
|
|
44
45
|
export function pinBinOverride(input: { key: BinKey; bin: string }): void {
|
|
45
46
|
const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
|
package/src/lib/statusonly.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type StatusOnlySpec = {
|
|
|
15
15
|
flag: string;
|
|
16
16
|
pool: PoolPaths;
|
|
17
17
|
binName: string;
|
|
18
|
-
binKey: "grokBin" | "museBin";
|
|
18
|
+
binKey: "grokBin" | "museBin" | "cursorBin";
|
|
19
19
|
storeDirFor: (accountId: string) => string;
|
|
20
20
|
authJsonFor: (accountId: string) => string;
|
|
21
21
|
homeEnv: string;
|
|
@@ -31,6 +31,7 @@ export type StatusOnlySpec = {
|
|
|
31
31
|
install: () => void;
|
|
32
32
|
loginStep: () => string;
|
|
33
33
|
samplePool: Provider["samplePool"];
|
|
34
|
+
identify?: (harvest: Harvest) => Promise<void>;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
// An entry with no identity (a Muse Code provider without an email) has no harvest; the first one that has one is pooled.
|
|
@@ -62,6 +63,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
|
|
|
62
63
|
try {
|
|
63
64
|
const harvest = p.exitCode === 0 ? firstHarvest(spec.readAuth(join(onboardDir, spec.onboardAuthRel))) : null;
|
|
64
65
|
if (!harvest) console.error(c.red(`no ${spec.name} login landed in the isolated home - nothing added.`));
|
|
66
|
+
else await spec.identify?.(harvest);
|
|
65
67
|
return harvest;
|
|
66
68
|
} finally {
|
|
67
69
|
rmSync(onboardDir, { recursive: true, force: true });
|
|
@@ -75,6 +77,7 @@ export function statusOnlyProvider(spec: StatusOnlySpec): Provider {
|
|
|
75
77
|
const harvest = live[0]?.harvest;
|
|
76
78
|
if (!harvest) return login();
|
|
77
79
|
console.log(c.dim(spec.foundLive(live.length)));
|
|
80
|
+
await spec.identify?.(harvest);
|
|
78
81
|
return harvest;
|
|
79
82
|
}
|
|
80
83
|
|
package/src/lib/types.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { runMuseSupervisor } from "./entries/musesupervisor.ts";
|
|
|
14
14
|
import { claude } from "./lib/claude.ts";
|
|
15
15
|
import { codex } from "./lib/codex.ts";
|
|
16
16
|
import { grok } from "./lib/grok.ts";
|
|
17
|
+
import { cursor } from "./lib/cursor.ts";
|
|
17
18
|
import { muse } from "./lib/muse.ts";
|
|
18
19
|
import { cmdInit, cmdInitPi } from "./cli/init.ts";
|
|
19
20
|
import { cmdAdd } from "./cli/add.ts";
|
|
@@ -39,6 +40,7 @@ const YES_FLAG = "--yes";
|
|
|
39
40
|
const CODEX_FLAG = "--codex";
|
|
40
41
|
const GROK_FLAG = "--grok";
|
|
41
42
|
const MUSE_FLAG = "--muse";
|
|
43
|
+
const CURSOR_FLAG = "--cursor";
|
|
42
44
|
const PI_FLAG = "--pi";
|
|
43
45
|
const JSON_COMMANDS = new Set(["status", "config", "check"]);
|
|
44
46
|
const CODEX_COMMANDS = new Set(["init", "add", "auth", "emails", "rm", "rename", "seat"]);
|
|
@@ -55,17 +57,18 @@ function printHelp(): void {
|
|
|
55
57
|
${c.cyan("tokenmaxxing init --pi")} install the pi supervisor: pi sessions run on pooled Claude or ChatGPT accounts that are logged into pi
|
|
56
58
|
${c.cyan("tokenmaxxing init --grok")} pool grok Build logins (status-only: no supervisor yet)
|
|
57
59
|
${c.cyan("tokenmaxxing init --muse")} same for Muse Code: log in the first account, isolated, install the muse supervisor
|
|
60
|
+
${c.cyan("tokenmaxxing init --cursor")} pool Cursor CLI logins (status-only: no supervisor yet)
|
|
58
61
|
${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
|
|
59
|
-
${c.cyan("tokenmaxxing add")} --codex | --grok | --muse same for the codex, grok, or
|
|
60
|
-
${c.cyan("tokenmaxxing auth")} [--codex | --grok | --muse] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
|
|
61
|
-
${c.cyan("tokenmaxxing auth")} [--codex | --grok | --muse] --emails <email>... log in and pool each listed email, skipping the ones already pooled (for moving to a new host: paste the output of ${c.cyan("emails")} from the old one)
|
|
62
|
-
${c.cyan("tokenmaxxing emails")} [--codex | --grok | --muse] print every pooled account's email, one per line, to copy into ${c.cyan("auth --emails")} on another host
|
|
62
|
+
${c.cyan("tokenmaxxing add")} --codex | --grok | --muse | --cursor same for the codex, grok, muse, or cursor pool
|
|
63
|
+
${c.cyan("tokenmaxxing auth")} [--codex | --grok | --muse | --cursor] [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every account that is flagged or has no usable credential in its store, one by one)
|
|
64
|
+
${c.cyan("tokenmaxxing auth")} [--codex | --grok | --muse | --cursor] --emails <email>... log in and pool each listed email, skipping the ones already pooled (for moving to a new host: paste the output of ${c.cyan("emails")} from the old one)
|
|
65
|
+
${c.cyan("tokenmaxxing emails")} [--codex | --grok | --muse | --cursor] print every pooled account's email, one per line, to copy into ${c.cyan("auth --emails")} on another host
|
|
63
66
|
${c.cyan("tokenmaxxing auth --pi")} [--codex] [sel | --all] log a pooled Claude account (or a codex account with --codex) into pi, isolated (bare = pick from a list; --all = every account without a pi login)
|
|
64
67
|
${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
|
|
65
68
|
${c.cyan("tokenmaxxing config")} print the config path and the effective values (edit the file in an editor)
|
|
66
69
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
67
|
-
${c.cyan("tokenmaxxing rename")} [--codex | --grok | --muse] <sel> <label>
|
|
68
|
-
${c.cyan("tokenmaxxing rm")} [--codex | --grok | --muse] <sel>
|
|
70
|
+
${c.cyan("tokenmaxxing rename")} [--codex | --grok | --muse | --cursor] <sel> <label>
|
|
71
|
+
${c.cyan("tokenmaxxing rm")} [--codex | --grok | --muse | --cursor] <sel>
|
|
69
72
|
${c.cyan("tokenmaxxing key add")} <label> [credit-usd] [workspace-id] store an Anthropic or OpenRouter (sk-or-...) API key read from stdin or the prompt; Claude sessions run on it only while every pooled Claude account is at its limit (workspace-id: for a key that spans several workspaces; an OpenRouter key takes neither argument and reads its credit from OpenRouter)
|
|
70
73
|
${c.cyan("tokenmaxxing key credit")} <label> <usd> set the key's balance as the Console shows it and restart its spend count (an OpenRouter key takes no amount: key credit <label> clears its refusal)
|
|
71
74
|
${c.cyan("tokenmaxxing key rm")} <label> delete the key and its stored secret (refused while a session runs on it)
|
|
@@ -108,14 +111,14 @@ async function main(): Promise<number> {
|
|
|
108
111
|
const json = jsonMode;
|
|
109
112
|
const cached = argv.includes(CACHED_FLAG);
|
|
110
113
|
const yes = argv.includes(YES_FLAG);
|
|
111
|
-
const providerFlags = [CODEX_FLAG, GROK_FLAG, MUSE_FLAG].filter((f) => argv.includes(f));
|
|
114
|
+
const providerFlags = [CODEX_FLAG, GROK_FLAG, MUSE_FLAG, CURSOR_FLAG].filter((f) => argv.includes(f));
|
|
112
115
|
if (providerFlags.length > 1) {
|
|
113
116
|
emitError({ json, message: `${providerFlags.join(" and ")} are mutually exclusive - pick one pool` });
|
|
114
117
|
return 2;
|
|
115
118
|
}
|
|
116
|
-
const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : argv.includes(MUSE_FLAG) ? muse : claude;
|
|
119
|
+
const provider = argv.includes(CODEX_FLAG) ? codex : argv.includes(GROK_FLAG) ? grok : argv.includes(MUSE_FLAG) ? muse : argv.includes(CURSOR_FLAG) ? cursor : claude;
|
|
117
120
|
const pi = argv.includes(PI_FLAG);
|
|
118
|
-
const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== MUSE_FLAG && a !== PI_FLAG);
|
|
121
|
+
const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG && a !== YES_FLAG && a !== CODEX_FLAG && a !== GROK_FLAG && a !== MUSE_FLAG && a !== CURSOR_FLAG && a !== PI_FLAG);
|
|
119
122
|
const sub = args[0];
|
|
120
123
|
|
|
121
124
|
if (pi && sub !== "init" && sub !== "auth") {
|
|
@@ -143,7 +146,7 @@ async function main(): Promise<number> {
|
|
|
143
146
|
return 2;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
if ((provider === grok || provider === muse) && (sub == null || !POOL_COMMANDS.has(sub))) {
|
|
149
|
+
if ((provider === grok || provider === muse || provider === cursor) && (sub == null || !POOL_COMMANDS.has(sub))) {
|
|
147
150
|
emitError({ json, message: `${provider.flag.trim()} applies to ${[...POOL_COMMANDS].join(", ")}, not ${sub ?? "status"}` });
|
|
148
151
|
return 2;
|
|
149
152
|
}
|
|
@@ -215,7 +218,7 @@ async function main(): Promise<number> {
|
|
|
215
218
|
if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
|
|
216
219
|
if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
|
|
217
220
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
218
|
-
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/; muse: muse-stores/), plus api-keys.json and the API keys in api-keys/ - remove accounts with \`xx rm\` and keys with \`xx key rm\` to delete their credentials`);
|
|
221
|
+
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; pi: pi-stores/; grok: grok-stores/; muse: muse-stores/; cursor: cursor-stores/), plus api-keys.json and the API keys in api-keys/ - remove accounts with \`xx rm\` and keys with \`xx key rm\` to delete their credentials`);
|
|
219
222
|
return 0;
|
|
220
223
|
}
|
|
221
224
|
case "help":
|