tokenmaxxing 1.32.0 → 1.34.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 -1
- package/package.json +1 -1
- package/src/cli/cloudrun.ts +5 -15
- package/src/cli/codexrm.ts +7 -8
- package/src/cli/cursorinit.ts +4 -8
- package/src/cli/doctor.ts +9 -23
- package/src/cli/rename.ts +12 -14
- package/src/cli/render.ts +1 -1
- package/src/cli/rm.ts +7 -9
- package/src/cli/setuptoken.ts +9 -22
- package/src/entries/statusline.ts +9 -3
- package/src/entries/stopfailurehook.ts +19 -22
- package/src/entries/supervisor.ts +1 -1
- package/src/lib/decide.ts +45 -75
- package/src/lib/types.ts +2 -2
- package/src/lib/usage.ts +2 -9
- package/src/main.ts +15 -19
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ claude # use claude as always
|
|
|
76
76
|
| `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 |
|
|
77
77
|
| `tokenmaxxing cursor init [dir]` | write the Claude relay subagent and `.cursor/environment.json` into a repo |
|
|
78
78
|
| `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 |
|
|
79
|
-
| `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, `
|
|
79
|
+
| `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, `check`, and `switch` (`ok` mirrors the exit code, failures add `error`) |
|
|
80
80
|
|
|
81
81
|
## How switching decides
|
|
82
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/cloudrun.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
spawnCloudClaude,
|
|
13
13
|
transcriptBytes,
|
|
14
14
|
} from "../lib/cloud.ts";
|
|
15
|
-
import { c, emitError,
|
|
15
|
+
import { c, emitError, fmtReset } from "./render.ts";
|
|
16
16
|
|
|
17
17
|
const USAGE = "usage: tokenmaxxing cloud run [--session <id>] [--max-turns <n>] \"<prompt>\" (the prompt is also read from stdin)";
|
|
18
18
|
|
|
@@ -39,15 +39,15 @@ function parseFlags(args: string[]): { session: string | null; maxTurns: number
|
|
|
39
39
|
return { session, maxTurns, prompt: positional.length > 0 ? positional.join(" ") : null };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export async function cmdCloudRun(args: string[]
|
|
42
|
+
export async function cmdCloudRun(args: string[]): Promise<number> {
|
|
43
43
|
const flags = parseFlags(args);
|
|
44
44
|
if ("error" in flags) {
|
|
45
|
-
emitError({
|
|
45
|
+
emitError({ message: `${flags.error} - ${USAGE}` });
|
|
46
46
|
return 2;
|
|
47
47
|
}
|
|
48
48
|
const prompt = flags.prompt ?? (process.stdin.isTTY ? null : await Bun.stdin.text());
|
|
49
49
|
if (prompt == null || prompt.trim() === "") {
|
|
50
|
-
emitError({
|
|
50
|
+
emitError({ message: USAGE });
|
|
51
51
|
return 2;
|
|
52
52
|
}
|
|
53
53
|
const tokens = readCloudTokens();
|
|
@@ -61,11 +61,7 @@ export async function cmdCloudRun(args: string[], json = false): Promise<number>
|
|
|
61
61
|
return picked;
|
|
62
62
|
});
|
|
63
63
|
if (!pick.ok) {
|
|
64
|
-
emitError({
|
|
65
|
-
json,
|
|
66
|
-
message: `every token is at its limit - earliest ${fmtReset(pick.earliestReset)}`,
|
|
67
|
-
extra: { earliestReset: pick.earliestReset, session_id: sessionId },
|
|
68
|
-
});
|
|
64
|
+
emitError({ message: `every token is at its limit - earliest ${fmtReset(pick.earliestReset)}` });
|
|
69
65
|
return 1;
|
|
70
66
|
}
|
|
71
67
|
const label = pick.token.label;
|
|
@@ -99,17 +95,11 @@ export async function cmdCloudRun(args: string[], json = false): Promise<number>
|
|
|
99
95
|
}
|
|
100
96
|
const errors = result.subtype === "success" ? [result.result] : result.errors;
|
|
101
97
|
emitError({
|
|
102
|
-
json,
|
|
103
98
|
message: `claude exited ${exitCode ?? "on signal"} (${result.subtype}): ${errors.join("; ")}`,
|
|
104
99
|
notes: [`session ${result.session_id}`],
|
|
105
|
-
extra: { session_id: result.session_id, subtype: result.subtype, errors },
|
|
106
100
|
});
|
|
107
101
|
return 1;
|
|
108
102
|
}
|
|
109
|
-
if (json) {
|
|
110
|
-
emitJson({ ok: true, session_id: result.session_id, result: result.result, num_turns: result.num_turns, total_cost_usd: result.total_cost_usd });
|
|
111
|
-
return 0;
|
|
112
|
-
}
|
|
113
103
|
console.log(result.result);
|
|
114
104
|
console.log(`session ${result.session_id}`);
|
|
115
105
|
return 0;
|
package/src/cli/codexrm.ts
CHANGED
|
@@ -5,33 +5,32 @@ import { deleteParkedCodexAuth } from "../lib/codexauth.ts";
|
|
|
5
5
|
import { liveCodexAccountId } from "../lib/codexsample.ts";
|
|
6
6
|
import { presentCodexAccountIds } from "../lib/codexpresence.ts";
|
|
7
7
|
import { findCodexAccount } from "./rename.ts";
|
|
8
|
-
import { c, emitError,
|
|
8
|
+
import { c, emitError, plain } from "./render.ts";
|
|
9
9
|
|
|
10
|
-
export async function cmdCodexRm(selector?: string
|
|
10
|
+
export async function cmdCodexRm(selector?: string): Promise<number> {
|
|
11
11
|
if (!selector) {
|
|
12
|
-
emitError({
|
|
12
|
+
emitError({ message: "usage: tokenmaxxing rm --codex <email|label|id>", paint: plain });
|
|
13
13
|
return 2;
|
|
14
14
|
}
|
|
15
15
|
return withLock(codexPaths.lockFile, async () => {
|
|
16
16
|
const index = loadCodexAccounts();
|
|
17
17
|
const account = findCodexAccount(index.accounts, selector);
|
|
18
18
|
if (!account) {
|
|
19
|
-
emitError({
|
|
19
|
+
emitError({ message: `no codex account matches "${selector}"` });
|
|
20
20
|
return 1;
|
|
21
21
|
}
|
|
22
22
|
if (liveCodexAccountId() === account.accountId) {
|
|
23
|
-
emitError({
|
|
23
|
+
emitError({ message: `${account.label} is the LIVE codex account - run \`tokenmaxxing switch --codex\` to move off it first.` });
|
|
24
24
|
return 1;
|
|
25
25
|
}
|
|
26
26
|
if (presentCodexAccountIds().has(account.accountId)) {
|
|
27
|
-
emitError({
|
|
27
|
+
emitError({ message: `${account.label} is running in a live codex session - close that session before removing it.` });
|
|
28
28
|
return 1;
|
|
29
29
|
}
|
|
30
30
|
deleteParkedCodexAuth({ credFile: account.credFile });
|
|
31
31
|
index.accounts = index.accounts.filter((x) => x.accountId !== account.accountId);
|
|
32
32
|
saveCodexAccounts({ index });
|
|
33
|
-
|
|
34
|
-
else console.log(`removed codex account ${c.bold(account.label)} from the pool (${index.accounts.length} left)`);
|
|
33
|
+
console.log(`removed codex account ${c.bold(account.label)} from the pool (${index.accounts.length} left)`);
|
|
35
34
|
return 0;
|
|
36
35
|
});
|
|
37
36
|
}
|
package/src/cli/cursorinit.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
|
-
import { c, emitError
|
|
5
|
+
import { c, emitError } from "./render.ts";
|
|
6
6
|
|
|
7
7
|
export const CLOUD_INSTALL_LINE =
|
|
8
8
|
'curl -fsSL https://bun.sh/install | bash && curl -fsSL https://claude.ai/install.sh | bash && "$HOME/.bun/bin/bun" add -g tokenmaxxing && test -x "$HOME/.bun/bin/bun" && test -x "$HOME/.local/bin/claude" && test -e "$HOME/.bun/bin/tokenmaxxing"';
|
|
@@ -42,14 +42,14 @@ const ExistingEnvironmentSchema = z.looseObject({ user: z.string().optional() })
|
|
|
42
42
|
const REPO_FILE_MODE = 0o644;
|
|
43
43
|
const USAGE = "usage: tokenmaxxing cursor init [dir]";
|
|
44
44
|
|
|
45
|
-
export function cmdCursorInit(args: string[]
|
|
45
|
+
export function cmdCursorInit(args: string[]): number {
|
|
46
46
|
if (args.length > 1) {
|
|
47
|
-
emitError({
|
|
47
|
+
emitError({ message: USAGE });
|
|
48
48
|
return 2;
|
|
49
49
|
}
|
|
50
50
|
const dir = resolve(args[0] ?? ".");
|
|
51
51
|
if (!existsSync(dir)) {
|
|
52
|
-
emitError({
|
|
52
|
+
emitError({ message: `no such directory: ${dir}` });
|
|
53
53
|
return 1;
|
|
54
54
|
}
|
|
55
55
|
const agentFile = join(dir, ".cursor", "agents", "claude.md");
|
|
@@ -64,10 +64,6 @@ export function cmdCursorInit(args: string[], json = false): number {
|
|
|
64
64
|
} else if (existing.user === "root" || existing.user === "0") {
|
|
65
65
|
warnings.push(".cursor/environment.json sets user to root; Claude Code refuses --dangerously-skip-permissions under root, so every relay call fails until user is a non-root account");
|
|
66
66
|
}
|
|
67
|
-
if (json) {
|
|
68
|
-
emitJson({ ok: true, dir, agent, environment, install: CLOUD_INSTALL_LINE, warnings });
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
67
|
console.log(`${c.green("✓")} .cursor/agents/claude.md ${agent}`);
|
|
72
68
|
if (environment === "written") {
|
|
73
69
|
console.log(`${c.green("✓")} .cursor/environment.json written`);
|
package/src/cli/doctor.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { readItem, liveTarget, parkedTarget } from "../lib/credstore.ts";
|
|
|
8
8
|
import { isAccessTokenExpiring, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
|
|
9
9
|
import { CredentialBlobSchema, type TokenIdentity } from "../lib/types.ts";
|
|
10
10
|
import { SETUP_TOKEN_STALE_MS, loadSetupTokens } from "../lib/setuptokens.ts";
|
|
11
|
-
import { c,
|
|
11
|
+
import { c, fmtAgo } from "./render.ts";
|
|
12
12
|
|
|
13
13
|
async function blobIdentity(raw: string): Promise<TokenIdentity | null> {
|
|
14
14
|
const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
|
|
@@ -16,22 +16,14 @@ async function blobIdentity(raw: string): Promise<TokenIdentity | null> {
|
|
|
16
16
|
return fetchTokenIdentity(creds.accessToken);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function cmdDoctor(
|
|
20
|
-
|
|
21
|
-
const notes: string[] = [];
|
|
22
|
-
const warnings: string[] = [];
|
|
19
|
+
export async function cmdDoctor(): Promise<number> {
|
|
20
|
+
let failed = 0;
|
|
23
21
|
const check = (cond: boolean, label: string, hint?: string) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
const note = (text: string) => {
|
|
28
|
-
notes.push(text);
|
|
29
|
-
if (!json) console.log(c.dim(` - ${text}`));
|
|
30
|
-
};
|
|
31
|
-
const warn = (text: string) => {
|
|
32
|
-
warnings.push(text);
|
|
33
|
-
if (!json) console.log(c.yellow(`⚠ ${text}`));
|
|
22
|
+
if (!cond) failed++;
|
|
23
|
+
console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
|
|
34
24
|
};
|
|
25
|
+
const note = (text: string) => console.log(c.dim(` - ${text}`));
|
|
26
|
+
const warn = (text: string) => console.log(c.yellow(`⚠ ${text}`));
|
|
35
27
|
|
|
36
28
|
check(existsSync(paths.supervisorLink), "claude supervisor wrapper present", "run `tokenmaxxing init`");
|
|
37
29
|
check(existsSync(installedBin()), "tokenmaxxing binary installed", "run `tokenmaxxing init`");
|
|
@@ -102,13 +94,7 @@ export async function cmdDoctor(json = false): Promise<number> {
|
|
|
102
94
|
}
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
const failed = checks.filter((entry) => !entry.ok).length;
|
|
106
|
-
const ok = failed === 0;
|
|
107
|
-
if (json) {
|
|
108
|
-
emitJson(ok ? { ok, checks, notes, warnings } : { ok, error: `issues found - ${failed} of ${checks.length} checks failed`, checks, notes, warnings });
|
|
109
|
-
return ok ? 0 : 1;
|
|
110
|
-
}
|
|
111
97
|
console.log();
|
|
112
|
-
console.log(
|
|
113
|
-
return
|
|
98
|
+
console.log(failed === 0 ? c.green("all good ✓") : c.yellow("issues found - see above"));
|
|
99
|
+
return failed === 0 ? 0 : 1;
|
|
114
100
|
}
|
package/src/cli/rename.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { withLock } from "../lib/lock.ts";
|
|
|
2
2
|
import { codexPaths, paths } from "../lib/paths.ts";
|
|
3
3
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
4
4
|
import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
|
|
5
|
-
import { c, emitError,
|
|
5
|
+
import { c, emitError, plain } from "./render.ts";
|
|
6
6
|
import type { Account, CodexAccount } from "../lib/types.ts";
|
|
7
7
|
|
|
8
8
|
export function findAccount(accounts: Account[], selector: string): Account | undefined {
|
|
@@ -23,54 +23,52 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async function renameCodexAccount(input: { selector: string; newLabel: string
|
|
27
|
-
const { selector, newLabel
|
|
26
|
+
async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
|
|
27
|
+
const { selector, newLabel } = input;
|
|
28
28
|
return withLock(codexPaths.lockFile, async () => {
|
|
29
29
|
const index = loadCodexAccounts();
|
|
30
30
|
const account = findCodexAccount(index.accounts, selector);
|
|
31
31
|
if (!account) {
|
|
32
|
-
emitError({
|
|
32
|
+
emitError({ message: `no codex account matches "${selector}"` });
|
|
33
33
|
return 1;
|
|
34
34
|
}
|
|
35
35
|
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
36
36
|
if (taken) {
|
|
37
|
-
emitError({
|
|
37
|
+
emitError({ message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
|
|
38
38
|
return 1;
|
|
39
39
|
}
|
|
40
40
|
const old = account.label;
|
|
41
41
|
account.label = newLabel;
|
|
42
42
|
saveCodexAccounts({ index });
|
|
43
|
-
|
|
44
|
-
else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
43
|
+
console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
45
44
|
return 0;
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
export async function cmdRename(argv: string[]
|
|
48
|
+
export async function cmdRename(argv: string[]): Promise<number> {
|
|
50
49
|
const codex = argv.includes("--codex");
|
|
51
50
|
const [selector, newLabel] = argv.filter((a) => a !== "--codex");
|
|
52
51
|
if (!selector || !newLabel) {
|
|
53
|
-
emitError({
|
|
52
|
+
emitError({ message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
|
|
54
53
|
return 2;
|
|
55
54
|
}
|
|
56
|
-
if (codex) return renameCodexAccount({ selector, newLabel
|
|
55
|
+
if (codex) return renameCodexAccount({ selector, newLabel });
|
|
57
56
|
return withLock(paths.lockFile, async () => {
|
|
58
57
|
const idx = loadAccounts();
|
|
59
58
|
const a = findAccount(idx.accounts, selector);
|
|
60
59
|
if (!a) {
|
|
61
|
-
emitError({
|
|
60
|
+
emitError({ message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
|
|
62
61
|
return 1;
|
|
63
62
|
}
|
|
64
63
|
const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
65
64
|
if (taken) {
|
|
66
|
-
emitError({
|
|
65
|
+
emitError({ message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
|
|
67
66
|
return 1;
|
|
68
67
|
}
|
|
69
68
|
const old = a.label;
|
|
70
69
|
a.label = newLabel;
|
|
71
70
|
saveAccounts(idx);
|
|
72
|
-
|
|
73
|
-
else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
71
|
+
console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
74
72
|
return 0;
|
|
75
73
|
});
|
|
76
74
|
}
|
package/src/cli/render.ts
CHANGED
package/src/cli/rm.ts
CHANGED
|
@@ -8,22 +8,22 @@ import { loadSetupTokens, saveSetupTokens } from "../lib/setuptokens.ts";
|
|
|
8
8
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
9
9
|
import { CredentialBlobSchema } from "../lib/types.ts";
|
|
10
10
|
import { findAccount } from "./rename.ts";
|
|
11
|
-
import { c, emitError,
|
|
11
|
+
import { c, emitError, plain } from "./render.ts";
|
|
12
12
|
|
|
13
|
-
export async function cmdRm(selector?: string
|
|
13
|
+
export async function cmdRm(selector?: string): Promise<number> {
|
|
14
14
|
if (!selector) {
|
|
15
|
-
emitError({
|
|
15
|
+
emitError({ message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
|
|
16
16
|
return 2;
|
|
17
17
|
}
|
|
18
18
|
return withLock(paths.lockFile, async () => {
|
|
19
19
|
const idx = loadAccounts();
|
|
20
20
|
const a = findAccount(idx.accounts, selector);
|
|
21
21
|
if (!a) {
|
|
22
|
-
emitError({
|
|
22
|
+
emitError({ message: `no account matches "${selector}"` });
|
|
23
23
|
return 1;
|
|
24
24
|
}
|
|
25
25
|
if (a.accountUuid === idx.activeAccountUuid) {
|
|
26
|
-
emitError({
|
|
26
|
+
emitError({ message: `${a.email} is the ACTIVE account - switch away before removing it.` });
|
|
27
27
|
return 1;
|
|
28
28
|
}
|
|
29
29
|
const live = await readItem(liveTarget());
|
|
@@ -34,13 +34,12 @@ export async function cmdRm(selector?: string, json = false): Promise<number> {
|
|
|
34
34
|
liveAccount = (await fetchTokenIdentity(liveCreds.accessToken)).accountUuid;
|
|
35
35
|
} catch (e) {
|
|
36
36
|
emitError({
|
|
37
|
-
json,
|
|
38
37
|
message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the profile endpoint is reachable.`,
|
|
39
38
|
});
|
|
40
39
|
return 1;
|
|
41
40
|
}
|
|
42
41
|
if (liveAccount === a.accountUuid) {
|
|
43
|
-
emitError({
|
|
42
|
+
emitError({ message: `${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.` });
|
|
44
43
|
return 1;
|
|
45
44
|
}
|
|
46
45
|
}
|
|
@@ -56,8 +55,7 @@ export async function cmdRm(selector?: string, json = false): Promise<number> {
|
|
|
56
55
|
if (setupTokens.tokens.some((t) => t.accountUuid === a.accountUuid)) {
|
|
57
56
|
saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.accountUuid) });
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
else console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
|
|
58
|
+
console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
|
|
61
59
|
return 0;
|
|
62
60
|
});
|
|
63
61
|
}
|
package/src/cli/setuptoken.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { loadAccounts } from "../lib/state.ts";
|
|
|
7
7
|
import { scrubCredentialEnv } from "../lib/usage.ts";
|
|
8
8
|
import { CURSOR_SECRET_VALUE_CAP_BYTES, cursorSecretValue, loadSetupTokens, saveSetupTokens, type SetupToken } from "../lib/setuptokens.ts";
|
|
9
9
|
import type { Account } from "../lib/types.ts";
|
|
10
|
-
import { c, emitError
|
|
10
|
+
import { c, emitError } from "./render.ts";
|
|
11
11
|
|
|
12
12
|
const TOKEN_BEGIN_MARKER = "Your OAuth token (valid for";
|
|
13
13
|
const TOKEN_END_MARKER = "Store this token securely";
|
|
@@ -58,7 +58,7 @@ function currentTokens(tokens: SetupToken[], accounts: Account[]): { label: stri
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async function rmToken(selector: string
|
|
61
|
+
async function rmToken(selector: string): Promise<number> {
|
|
62
62
|
const outcome = await withLock(paths.lockFile, async (): Promise<{ error: string } | { kept: SetupToken[] }> => {
|
|
63
63
|
const store = loadSetupTokens();
|
|
64
64
|
const accounts = loadAccounts().accounts;
|
|
@@ -74,35 +74,26 @@ async function rmToken(selector: string, json: boolean): Promise<number> {
|
|
|
74
74
|
return { kept };
|
|
75
75
|
});
|
|
76
76
|
if ("error" in outcome) {
|
|
77
|
-
emitError({
|
|
77
|
+
emitError({ message: outcome.error });
|
|
78
78
|
return 1;
|
|
79
79
|
}
|
|
80
|
-
const remaining = outcome.kept;
|
|
81
|
-
if (json) {
|
|
82
|
-
emitJson({ ok: true, removed: selector, remaining: remaining.map((t) => t.label) });
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
85
80
|
console.log(`removed the setup token for ${selector} (local only: Claude Code documents no revocation for setup tokens)`);
|
|
86
81
|
return 0;
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
export async function cmdSetupToken(args: string[]
|
|
84
|
+
export async function cmdSetupToken(args: string[]): Promise<number> {
|
|
90
85
|
const [sub, label] = args;
|
|
91
|
-
if (sub === "rm" && label !== undefined && args.length === 2) return rmToken(label
|
|
86
|
+
if (sub === "rm" && label !== undefined && args.length === 2) return rmToken(label);
|
|
92
87
|
const print = sub === "--print" && args.length === 1;
|
|
93
88
|
if (args.length > 0 && !print) {
|
|
94
|
-
emitError({
|
|
95
|
-
return 2;
|
|
96
|
-
}
|
|
97
|
-
if (json && !print) {
|
|
98
|
-
emitError({ json, message: "setup-token mints through a browser login flow and has no --json form; `setup-token --print --json` prints the stored set" });
|
|
89
|
+
emitError({ message: USAGE });
|
|
99
90
|
return 2;
|
|
100
91
|
}
|
|
101
92
|
let store = loadSetupTokens();
|
|
102
93
|
let idx = loadAccounts();
|
|
103
94
|
if (!print) {
|
|
104
95
|
if (idx.accounts.length === 0) {
|
|
105
|
-
emitError({
|
|
96
|
+
emitError({ message: "no accounts in the pool - run `tokenmaxxing init` first" });
|
|
106
97
|
return 1;
|
|
107
98
|
}
|
|
108
99
|
const real = resolveRealClaude();
|
|
@@ -130,7 +121,7 @@ export async function cmdSetupToken(args: string[], json = false): Promise<numbe
|
|
|
130
121
|
idx = loadAccounts();
|
|
131
122
|
}
|
|
132
123
|
if (store.tokens.length === 0) {
|
|
133
|
-
emitError({
|
|
124
|
+
emitError({ message: "no setup tokens stored - run `tokenmaxxing setup-token` to mint them" });
|
|
134
125
|
return 1;
|
|
135
126
|
}
|
|
136
127
|
const tokens = currentTokens(store.tokens, idx.accounts);
|
|
@@ -138,15 +129,11 @@ export async function cmdSetupToken(args: string[], json = false): Promise<numbe
|
|
|
138
129
|
.filter((t) => !idx.accounts.some((a) => a.accountUuid === t.accountUuid))
|
|
139
130
|
.map((t) => ({ label: t.label, accountUuid: t.accountUuid }));
|
|
140
131
|
if (tokens.length === 0) {
|
|
141
|
-
emitError({
|
|
132
|
+
emitError({ message: "no stored setup token belongs to a pooled account - run `tokenmaxxing setup-token` to mint them" });
|
|
142
133
|
return 1;
|
|
143
134
|
}
|
|
144
135
|
const secret = cursorSecretValue(tokens);
|
|
145
136
|
const bytes = Buffer.byteLength(secret);
|
|
146
|
-
if (json) {
|
|
147
|
-
emitJson({ ok: true, tokens, orphaned, bytes, cap: CURSOR_SECRET_VALUE_CAP_BYTES });
|
|
148
|
-
return 0;
|
|
149
|
-
}
|
|
150
137
|
console.log();
|
|
151
138
|
if (orphaned.length > 0) {
|
|
152
139
|
console.log(c.dim(`left out of the secret (no longer in the pool): ${orphaned.map((t) => `${t.label} (${t.accountUuid})`).join(", ")}; \`setup-token rm <label|uuid>\` drops one`));
|
|
@@ -80,11 +80,16 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
80
80
|
const seatUuid =
|
|
81
81
|
(ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.accountUuid === ctx.liveAccount) ? ctx.liveAccount : null) ??
|
|
82
82
|
ctx.accounts.activeAccountUuid;
|
|
83
|
+
const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
|
|
84
|
+
const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
|
|
85
|
+
const seat = ctx.accounts.accounts.find((a) => a.accountUuid === seatUuid);
|
|
86
|
+
if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
|
|
87
|
+
const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
|
|
83
88
|
const active =
|
|
84
89
|
windows.length > 0
|
|
85
|
-
? `${
|
|
90
|
+
? `${seatMarker} ${windows.join(" ")}`
|
|
86
91
|
: seatUuid != null
|
|
87
|
-
? `${
|
|
92
|
+
? `${seatMarker} ?`
|
|
88
93
|
: "";
|
|
89
94
|
|
|
90
95
|
const parked = sortBy(
|
|
@@ -92,7 +97,8 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
92
97
|
[(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
|
|
93
98
|
);
|
|
94
99
|
const poolSeg = (a: Account): string => {
|
|
95
|
-
const marker = a.needsReauth ? col.red("✗") : col.cyan("◇");
|
|
100
|
+
const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
|
|
101
|
+
if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
|
|
96
102
|
const week = a.lastUsage?.sevenDay;
|
|
97
103
|
if (week == null) return `${marker} ?`;
|
|
98
104
|
const weekUsed = used(week);
|
|
@@ -3,14 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
import { paths } from "../lib/paths.ts";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
5
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
7
|
+
import { withLock } from "../lib/lock.ts";
|
|
8
|
+
import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
8
9
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
9
10
|
import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
10
11
|
import { log } from "../lib/log.ts";
|
|
11
12
|
|
|
12
|
-
export const RETRIGGER_PROMPT = "Continue where the previous turn left off; it was interrupted by a usage limit and tokenmaxxing switched accounts.";
|
|
13
|
-
|
|
14
13
|
const StopFailureStdin = z.looseObject({
|
|
15
14
|
session_id: z.uuid().optional().catch(undefined),
|
|
16
15
|
transcript_path: z.string().optional().catch(undefined),
|
|
@@ -30,7 +29,8 @@ async function readStdin(): Promise<string> {
|
|
|
30
29
|
export async function runStopFailureHook(): Promise<number> {
|
|
31
30
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
32
31
|
|
|
33
|
-
const
|
|
32
|
+
const entry = await withLock(paths.lockFile, () => ({ swapClock: loadLastSwapAt(), account: readOAuthAccount()?.accountUuid ?? null }));
|
|
33
|
+
const account = entry.account;
|
|
34
34
|
const now = Date.now();
|
|
35
35
|
const raw = await readStdin();
|
|
36
36
|
const parsed = StopFailureStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
|
|
@@ -44,29 +44,27 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
44
44
|
const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
|
+
const lastSwapAt = loadLastSwapAt();
|
|
48
|
+
if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
49
|
+
log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
47
52
|
const cfg = loadConfig();
|
|
48
|
-
const
|
|
53
|
+
const row = stdin.transcript_path
|
|
49
54
|
? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
|
|
50
55
|
: null;
|
|
51
|
-
const limit =
|
|
56
|
+
const limit = row ? classifyEnforcedLimit(row, cfg.policy.switchModels) : null;
|
|
52
57
|
|
|
53
58
|
let enforced: EnforcedLimit | null = null;
|
|
54
|
-
if (limit &&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
log("stopfailure.enforced", { kind: limit.kind, family: limit.kind === "model" ? limit.family : undefined, outcome: stamp.outcome, resetsAt: stamp.resetsAt, subagent: !mainLoop });
|
|
58
|
-
if (stamp.outcome !== "account-moved") {
|
|
59
|
-
enforced = { account, family: limit.kind === "model" ? limit.family : null, resetsAt: stamp.resetsAt, windowMs: enforcedWindowMs(limit) };
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
log("stopfailure.unproven", { kind: limit.kind });
|
|
63
|
-
}
|
|
59
|
+
if (limit && account) {
|
|
60
|
+
enforced = { account, kind: limit.kind, family: limit.kind === "model" ? limit.family : null, resetsAt: limit.resetsAt, blind: !mainLoop && limit.kind !== "model" };
|
|
61
|
+
log("stopfailure.enforced", { kind: limit.kind, family: enforced.family ?? undefined, resetsAt: limit.resetsAt, subagent: !mainLoop });
|
|
64
62
|
} else {
|
|
65
63
|
log("stopfailure.unclassified", {
|
|
66
|
-
row:
|
|
67
|
-
type:
|
|
68
|
-
transient:
|
|
69
|
-
body:
|
|
64
|
+
row: row != null,
|
|
65
|
+
type: row?.quotaLimits?.rateLimitType,
|
|
66
|
+
transient: row?.apiErrorIsTransient,
|
|
67
|
+
body: row ? parseErrorBody(row.errorDetails)?.error?.type : undefined,
|
|
70
68
|
});
|
|
71
69
|
}
|
|
72
70
|
|
|
@@ -78,7 +76,6 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
78
76
|
ts: Date.now(),
|
|
79
77
|
waitUntil: decision.waitUntil ?? now,
|
|
80
78
|
sessionId: stdinSid ?? pinnedSid,
|
|
81
|
-
prompt: RETRIGGER_PROMPT,
|
|
82
79
|
...(launchedAt != null ? { launchedAt } : {}),
|
|
83
80
|
});
|
|
84
81
|
writeFileAtomic(marker, JSON.stringify(payload));
|
|
@@ -292,7 +292,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
292
292
|
if (await countdownWait(m.account, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
293
293
|
} else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched to ${m.account} - resuming...\x1b[0m\n`);
|
|
294
294
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
295
|
-
launchArgs = ["--resume", m.sessionId, ...persistable
|
|
295
|
+
launchArgs = ["--resume", m.sessionId, ...persistable];
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
298
298
|
log("supervisor.exit", { sid, respawns, code: child.exitCode, signal: child.signalCode });
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { maxBy } from "es-toolkit";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { withLock } from "./lock.ts";
|
|
4
4
|
import { paths } from "./paths.ts";
|
|
5
|
-
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt,
|
|
5
|
+
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsageSnapshot, saveAccounts, saveDepletedWait, writeUsage } from "./state.ts";
|
|
6
6
|
import { readOAuthAccount } from "./claudejson.ts";
|
|
7
7
|
import { isSkippableSwapError, performSwap } from "./swap.ts";
|
|
8
|
-
import { nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
|
|
9
|
-
import { familyTokens, gatedFamilies, keepRows, probeUsage
|
|
8
|
+
import { isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
|
|
9
|
+
import { familyTokens, gatedFamilies, keepRows, probeUsage } from "./usage.ts";
|
|
10
10
|
import { log } from "./log.ts";
|
|
11
11
|
import { AccountSchema, UsageStateSchema, type Account, type Config, type EnforcedLimit, type Thresholds, type UsageState, type UsageWindow } from "./types.ts";
|
|
12
12
|
|
|
@@ -40,7 +40,9 @@ function capForFamily(account: Account, family: string, now: number): UsageWindo
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function isOver(u: UsageState | null, account: Account | undefined, bars: Thresholds, cfg: Config, now: number): boolean {
|
|
43
|
-
if (!
|
|
43
|
+
if (!account) return false;
|
|
44
|
+
if (account.enforcedUntil != null && account.enforcedUntil > now) return true;
|
|
45
|
+
if (!u || u.account !== account.accountUuid) return false;
|
|
44
46
|
if (
|
|
45
47
|
liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= bars.session ||
|
|
46
48
|
liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }) >= bars.weekly
|
|
@@ -75,14 +77,15 @@ function freshest(u: UsageState | null, uAt: number | null, account: Account | u
|
|
|
75
77
|
return { ...u, ts: uAt };
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
async function loadFreshSnapshots(cfg: Config, account: string | null, now: number): Promise<Snapshots> {
|
|
80
|
+
async function loadFreshSnapshots(cfg: Config, account: string | null, now: number, probe: boolean): Promise<Snapshots> {
|
|
79
81
|
const snap = loadUsageSnapshot();
|
|
80
82
|
let u = snap?.state ?? null;
|
|
81
83
|
let uAt = snap?.at ?? null;
|
|
82
84
|
const ttl = cfg.policy.usagePollTtlMs;
|
|
83
85
|
const stored = loadAccounts().accounts.find((a) => a.accountUuid === account);
|
|
84
86
|
const probeAttempted = stored?.lastProbeAt != null && now - stored.lastProbeAt <= ttl;
|
|
85
|
-
|
|
87
|
+
const walled = stored?.enforcedUntil != null && stored.enforcedUntil > now;
|
|
88
|
+
if (probe && account && stored && !walled && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
|
|
86
89
|
const startedAt = Date.now();
|
|
87
90
|
const full = await probeUsage();
|
|
88
91
|
const ts = Date.now();
|
|
@@ -121,21 +124,20 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
121
124
|
|
|
122
125
|
export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = false, enforced: EnforcedLimit | null = null): Promise<SwapDecision> {
|
|
123
126
|
const activeAccount = readOAuthAccount()?.accountUuid ?? null;
|
|
124
|
-
const enforced0 = enforced && enforced.account === activeAccount ? enforced : null;
|
|
125
127
|
|
|
126
128
|
const lastSwapAt = loadLastSwapAt();
|
|
127
|
-
if (!
|
|
129
|
+
if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
128
130
|
return depletedReplay(now) ?? { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
const cfg = loadConfig();
|
|
132
134
|
const bars = thresholdBars(cfg);
|
|
133
135
|
|
|
134
|
-
const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now);
|
|
136
|
+
const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now, enforced == null);
|
|
135
137
|
const stored = loadAccounts().accounts.find((a) => a.accountUuid === activeAccount);
|
|
136
138
|
const usage = freshest(teeUsage, uAt, stored);
|
|
137
139
|
|
|
138
|
-
if (!
|
|
140
|
+
if (!enforced && !isOver(usage, stored, bars, cfg, now)) {
|
|
139
141
|
const measured = usage != null && activeAccount != null && usage.account === activeAccount;
|
|
140
142
|
if (!measured) {
|
|
141
143
|
const replay = depletedReplay(now);
|
|
@@ -147,11 +149,20 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
147
149
|
return withLock(paths.lockFile, async () => {
|
|
148
150
|
const idx = loadAccounts();
|
|
149
151
|
const account2 = readOAuthAccount()?.accountUuid ?? null;
|
|
150
|
-
const enforced2 = enforced0 && enforced0.account === account2 ? enforced0 : null;
|
|
151
152
|
const tee = loadUsageSnapshot();
|
|
152
153
|
const active = account2 ? idx.accounts.find((a) => a.accountUuid === account2) : undefined;
|
|
153
154
|
const u2 = tee ? freshest(tee.state, tee.at, active) : usage;
|
|
154
155
|
|
|
156
|
+
const origin = enforced ? idx.accounts.find((a) => a.accountUuid === enforced.account) : undefined;
|
|
157
|
+
const prior = origin?.enforcedUntil != null && origin.enforcedUntil > now;
|
|
158
|
+
if (enforced && origin) {
|
|
159
|
+
origin.enforcedUntil = Math.max(origin.enforcedUntil ?? 0, enforcedWall(enforced, origin, now));
|
|
160
|
+
origin.lastProbeAt = now;
|
|
161
|
+
saveAccounts(idx);
|
|
162
|
+
log("usage.enforced_limit", { kind: enforced.kind, family: enforced.family ?? undefined, resetsAt: origin.enforcedUntil, blind: prior || enforced.blind, live: origin === active });
|
|
163
|
+
}
|
|
164
|
+
const enforced2 = enforced && origin && origin === active ? enforced : null;
|
|
165
|
+
|
|
155
166
|
if (account2 != null && !active) {
|
|
156
167
|
return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
|
|
157
168
|
}
|
|
@@ -162,10 +173,14 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
162
173
|
saveAccounts(idx);
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
const
|
|
166
|
-
const
|
|
176
|
+
const walled = active?.enforcedUntil != null && active.enforcedUntil > now;
|
|
177
|
+
const blindScreen = (enforced != null && (prior || enforced.blind)) || (walled && !enforced2);
|
|
178
|
+
const gated = blindScreen ? cfg.policy.switchModels : gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
|
|
179
|
+
const family = enforced?.family ?? null;
|
|
180
|
+
const switchFamilies = family != null && !gated.includes(family) ? [...gated, family] : gated;
|
|
167
181
|
|
|
168
|
-
|
|
182
|
+
const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars, currentAccountUuid: account2, switchFamilies });
|
|
183
|
+
if (!enforced2 && !isOver(u2, active, bars, cfg, now) && !(enforced && seatExhausted)) {
|
|
169
184
|
return depletedReplay(now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
170
185
|
}
|
|
171
186
|
|
|
@@ -201,8 +216,7 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
201
216
|
const fresh = loadAccounts();
|
|
202
217
|
const current = seatOf(fresh);
|
|
203
218
|
const ctx = { now, thresholds: bars, currentAccountUuid: current?.accountUuid ?? null, switchFamilies };
|
|
204
|
-
const
|
|
205
|
-
const currentAt = current ? Math.max(usableAt(current, ctx), enforcedUntil) : Number.POSITIVE_INFINITY;
|
|
219
|
+
const currentAt = current ? usableAt(current, ctx) : Number.POSITIVE_INFINITY;
|
|
206
220
|
const other = pickEarliestReset(usable(fresh.accounts), ctx);
|
|
207
221
|
|
|
208
222
|
let target: Account | null = null;
|
|
@@ -245,63 +259,19 @@ function depletedReplay(now: number): SwapDecision | null {
|
|
|
245
259
|
return { swapped: false, account, reason: "depleted-wait", waitUntil: rec.waitUntil };
|
|
246
260
|
}
|
|
247
261
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const { limit, account: accountUuid, now } = input;
|
|
264
|
-
return withLock(paths.lockFile, () => {
|
|
265
|
-
if ((readOAuthAccount()?.accountUuid ?? null) !== accountUuid) return { outcome: "account-moved", resetsAt: limit.resetsAt };
|
|
266
|
-
const prior = loadUsage();
|
|
267
|
-
const priorSame = prior && prior.account === accountUuid ? prior : null;
|
|
268
|
-
const idx = loadAccounts();
|
|
269
|
-
const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
|
|
270
|
-
if (limit.kind === "model") {
|
|
271
|
-
const rows = account?.lastUsage?.perModel ?? {};
|
|
272
|
-
const knownReset = Object.entries(rows).filter(([k]) => familyTokens(k).includes(limit.family)).map(([, w]) => w.resetsAt).find((r): r is number => r != null) ?? null;
|
|
273
|
-
const weeklyReset = priorSame?.sevenDay.resetsAt ?? account?.lastUsage?.sevenDay.resetsAt ?? null;
|
|
274
|
-
const resetsAt = limit.resetsAt ?? nextWeeklyReset(knownReset ?? weeklyReset, now);
|
|
275
|
-
if (!account) return { outcome: "no-carrier", resetsAt };
|
|
276
|
-
if (account.lastUsage) {
|
|
277
|
-
const others = Object.fromEntries(Object.entries(rows).filter(([k]) => !familyTokens(k).includes(limit.family)));
|
|
278
|
-
account.lastUsage = { ...account.lastUsage, perModel: { ...others, [limit.family]: { usedPercentage: 100, resetsAt } }, ...(resetsAt == null ? { rowsAt: now } : {}) };
|
|
279
|
-
} else {
|
|
280
|
-
account.enforcedUntil = resetsAt ?? now + WEEK_MS;
|
|
281
|
-
}
|
|
282
|
-
account.lastProbeAt = now;
|
|
283
|
-
saveAccounts(idx);
|
|
284
|
-
log("usage.enforced_limit", { kind: limit.kind, family: limit.family, resetsAt });
|
|
285
|
-
return { outcome: "stamped", resetsAt };
|
|
286
|
-
}
|
|
287
|
-
if (account) {
|
|
288
|
-
account.lastProbeAt = now;
|
|
289
|
-
if (limit.resetsAt != null) account.enforcedUntil = limit.resetsAt;
|
|
290
|
-
saveAccounts(idx);
|
|
291
|
-
}
|
|
292
|
-
const carrier = priorSame ?? (account?.lastUsage ? { ...account.lastUsage, model: null } : null);
|
|
293
|
-
if (!carrier) return { outcome: "no-carrier", resetsAt: limit.resetsAt };
|
|
294
|
-
const window: UsageWindow = { usedPercentage: 100, resetsAt: limit.resetsAt };
|
|
295
|
-
writeUsage({
|
|
296
|
-
...(priorSame ?? {}),
|
|
297
|
-
...(priorSame == null && account?.lastUsageAt != null ? { sampledAt: account.lastUsageAt } : {}),
|
|
298
|
-
fiveHour: limit.kind === "session" ? window : carrier.fiveHour,
|
|
299
|
-
sevenDay: limit.kind === "weekly" ? window : carrier.sevenDay,
|
|
300
|
-
account: accountUuid,
|
|
301
|
-
ts: now,
|
|
302
|
-
model: carrier.model,
|
|
303
|
-
}, { stamp: true });
|
|
304
|
-
log("usage.enforced_limit", { kind: limit.kind, resetsAt: limit.resetsAt });
|
|
305
|
-
return { outcome: "stamped", resetsAt: limit.resetsAt };
|
|
306
|
-
});
|
|
262
|
+
function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
|
|
263
|
+
const cached = account.lastUsage;
|
|
264
|
+
const { family } = limit;
|
|
265
|
+
const familyReset =
|
|
266
|
+
family == null
|
|
267
|
+
? null
|
|
268
|
+
: Object.entries(cached?.perModel ?? {})
|
|
269
|
+
.filter(([k]) => familyTokens(k).includes(family))
|
|
270
|
+
.map(([, w]) => w.resetsAt)
|
|
271
|
+
.find((r): r is number => r != null) ?? null;
|
|
272
|
+
const cachedReset =
|
|
273
|
+
limit.kind === "session"
|
|
274
|
+
? cached?.fiveHour.resetsAt != null && cached.fiveHour.resetsAt > now ? cached.fiveHour.resetsAt : null
|
|
275
|
+
: nextWeeklyReset(familyReset ?? cached?.sevenDay.resetsAt ?? null, now);
|
|
276
|
+
return limit.resetsAt ?? cachedReset ?? now + (limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS);
|
|
307
277
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -82,9 +82,10 @@ export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
|
|
|
82
82
|
|
|
83
83
|
export const EnforcedLimitSchema = z.object({
|
|
84
84
|
account: z.string(),
|
|
85
|
+
kind: z.enum(["session", "weekly", "model"]),
|
|
85
86
|
family: z.string().nullable(),
|
|
86
87
|
resetsAt: z.number().nullable(),
|
|
87
|
-
|
|
88
|
+
blind: z.boolean(),
|
|
88
89
|
});
|
|
89
90
|
export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
|
|
90
91
|
|
|
@@ -128,7 +129,6 @@ export const RespawnMarkerSchema = z.object({
|
|
|
128
129
|
ts: z.number(),
|
|
129
130
|
waitUntil: z.number(),
|
|
130
131
|
sessionId: z.string(),
|
|
131
|
-
prompt: z.string().optional(),
|
|
132
132
|
launchedAt: z.number().optional(),
|
|
133
133
|
});
|
|
134
134
|
|
package/src/lib/usage.ts
CHANGED
|
@@ -169,13 +169,7 @@ export function transcriptRowText(row: TranscriptRow): string {
|
|
|
169
169
|
|
|
170
170
|
const ROW_RECENCY_MS = 60_000;
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
row: TranscriptRowSchema,
|
|
174
|
-
errorAt: z.number().nullable(),
|
|
175
|
-
});
|
|
176
|
-
export type EnforcedRow = z.infer<typeof EnforcedRowSchema>;
|
|
177
|
-
|
|
178
|
-
export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): EnforcedRow | null {
|
|
172
|
+
export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): TranscriptRow | null {
|
|
179
173
|
const { rows, lastAssistantMessage, now } = input;
|
|
180
174
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
181
175
|
const row = rows[i]!;
|
|
@@ -183,8 +177,7 @@ export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMes
|
|
|
183
177
|
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
184
178
|
const byContent = lastAssistantMessage != null && lastAssistantMessage !== "" && transcriptRowText(row) === lastAssistantMessage;
|
|
185
179
|
const byRecency = Number.isFinite(ts) && Math.abs(now - ts) <= ROW_RECENCY_MS;
|
|
186
|
-
if (
|
|
187
|
-
return { row, errorAt: Number.isFinite(ts) ? ts : null };
|
|
180
|
+
if (byContent || byRecency) return row;
|
|
188
181
|
}
|
|
189
182
|
return null;
|
|
190
183
|
}
|
package/src/main.ts
CHANGED
|
@@ -27,11 +27,11 @@ import { cmdSetupToken } from "./cli/setuptoken.ts";
|
|
|
27
27
|
import { cmdCloudRun } from "./cli/cloudrun.ts";
|
|
28
28
|
import { cmdCursorInit } from "./cli/cursorinit.ts";
|
|
29
29
|
import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
|
|
30
|
-
import { c, emitError
|
|
30
|
+
import { c, emitError } from "./cli/render.ts";
|
|
31
31
|
|
|
32
32
|
const JSON_FLAG = "--json";
|
|
33
33
|
const CACHED_FLAG = "--cached";
|
|
34
|
-
const
|
|
34
|
+
const JSON_COMMANDS = new Set(["status", "config", "check", "switch"]);
|
|
35
35
|
|
|
36
36
|
function printHelp(): void {
|
|
37
37
|
console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
|
|
@@ -55,7 +55,7 @@ function printHelp(): void {
|
|
|
55
55
|
${c.cyan("tokenmaxxing cursor init")} [dir] write the Claude relay subagent (.cursor/agents/claude.md) and .cursor/environment.json into a repo
|
|
56
56
|
${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
|
|
57
57
|
|
|
58
|
-
${c.cyan("--json")} print one JSON document on stdout instead of text (status, config,
|
|
58
|
+
${c.cyan("--json")} print one JSON document on stdout instead of text (status, config, check, switch); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
|
|
59
59
|
|
|
60
60
|
${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
|
|
61
61
|
}
|
|
@@ -88,8 +88,8 @@ async function main(): Promise<number> {
|
|
|
88
88
|
return 2;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
if (json && sub != null &&
|
|
92
|
-
emitError({ json, message: `${sub}
|
|
91
|
+
if (json && sub != null && !JSON_COMMANDS.has(sub)) {
|
|
92
|
+
emitError({ json, message: `${sub} has no ${JSON_FLAG} form (${JSON_FLAG} applies to ${[...JSON_COMMANDS].join(", ")})` });
|
|
93
93
|
return 2;
|
|
94
94
|
}
|
|
95
95
|
if (!(sub != null && sub.startsWith("__")) && !process.env.TOKENMAXXING_PROBE) {
|
|
@@ -135,21 +135,21 @@ async function main(): Promise<number> {
|
|
|
135
135
|
case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
|
|
136
136
|
case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
|
|
137
137
|
case "auth": return cmdAuth(args.slice(1));
|
|
138
|
-
case "doctor": return cmdDoctor(
|
|
138
|
+
case "doctor": return cmdDoctor();
|
|
139
139
|
case "rm": {
|
|
140
140
|
const rest = args.slice(1).filter((a) => a !== "--codex");
|
|
141
|
-
return args.includes("--codex") ? cmdCodexRm(rest[0]
|
|
141
|
+
return args.includes("--codex") ? cmdCodexRm(rest[0]) : cmdRm(rest[0]);
|
|
142
142
|
}
|
|
143
|
-
case "rename": return cmdRename(args.slice(1)
|
|
144
|
-
case "setup-token": return cmdSetupToken(args.slice(1)
|
|
143
|
+
case "rename": return cmdRename(args.slice(1));
|
|
144
|
+
case "setup-token": return cmdSetupToken(args.slice(1));
|
|
145
145
|
case "cursor": {
|
|
146
|
-
if (args[1] === "init") return cmdCursorInit(args.slice(2)
|
|
147
|
-
emitError({
|
|
146
|
+
if (args[1] === "init") return cmdCursorInit(args.slice(2));
|
|
147
|
+
emitError({ message: "usage: tokenmaxxing cursor init [dir]" });
|
|
148
148
|
return 2;
|
|
149
149
|
}
|
|
150
150
|
case "cloud": {
|
|
151
|
-
if (args[1] === "run") return cmdCloudRun(args.slice(2)
|
|
152
|
-
emitError({
|
|
151
|
+
if (args[1] === "run") return cmdCloudRun(args.slice(2));
|
|
152
|
+
emitError({ message: 'usage: tokenmaxxing cloud run [--session <id>] [--max-turns <n>] "<prompt>"' });
|
|
153
153
|
return 2;
|
|
154
154
|
}
|
|
155
155
|
case "uninstall": {
|
|
@@ -160,10 +160,6 @@ async function main(): Promise<number> {
|
|
|
160
160
|
...(out.timerDeactivated ? ["check timer"] : []),
|
|
161
161
|
...(out.pathLineRemoved ? ["rc PATH line"] : []),
|
|
162
162
|
];
|
|
163
|
-
if (json) {
|
|
164
|
-
emitJson({ ok: true, removed, timerDeactivated: out.timerDeactivated, pathLineRemoved: out.pathLineRemoved });
|
|
165
|
-
return 0;
|
|
166
|
-
}
|
|
167
163
|
console.log(`removed ${removed.join(", ")}`);
|
|
168
164
|
if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
|
|
169
165
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
@@ -176,8 +172,8 @@ async function main(): Promise<number> {
|
|
|
176
172
|
printHelp();
|
|
177
173
|
return 0;
|
|
178
174
|
default:
|
|
179
|
-
emitError({
|
|
180
|
-
|
|
175
|
+
emitError({ message: `unknown command: ${sub}` });
|
|
176
|
+
printHelp();
|
|
181
177
|
return 2;
|
|
182
178
|
}
|
|
183
179
|
}
|