tokenmaxxing 1.33.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +3 -5
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/cloudrun.ts +5 -15
- package/src/cli/cursorinit.ts +4 -8
- package/src/cli/doctor.ts +22 -36
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +14 -50
- package/src/cli/render.ts +1 -8
- package/src/cli/rm.ts +27 -44
- package/src/cli/setuptoken.ts +25 -38
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +22 -16
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +3 -2
- package/src/entries/statusline.ts +15 -13
- package/src/entries/stopfailurehook.ts +7 -5
- package/src/entries/stophook.ts +3 -2
- package/src/lib/claude.ts +320 -0
- package/src/lib/codex.ts +353 -0
- package/src/lib/codexauth.ts +6 -0
- package/src/lib/codexpresence.ts +0 -10
- package/src/lib/codexusage.ts +14 -32
- package/src/lib/decide.ts +86 -174
- package/src/lib/oauth.ts +7 -0
- package/src/lib/paths.ts +14 -6
- package/src/lib/picker.ts +56 -37
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +49 -16
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +17 -51
- package/src/lib/usage.ts +17 -4
- package/src/main.ts +33 -37
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -37
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
package/src/cli/init.ts
CHANGED
|
@@ -1,149 +1,52 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
-
import { isApiKeyMode, readOAuthAccount } from "../lib/claudejson.ts";
|
|
3
|
-
import { readItem, writeItem, liveTarget, parkedTarget, mergeIntoLive } from "../lib/credstore.ts";
|
|
4
|
-
import { refreshCredential, isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
|
|
5
|
-
import { withClaudeRefreshLock } from "../lib/claudelock.ts";
|
|
6
|
-
import { loadAccounts, saveAccounts, loadConfig, pinBinOverride } from "../lib/state.ts";
|
|
7
2
|
import { withLock } from "../lib/lock.ts";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { c, claudeTierLabel } from "./render.ts";
|
|
3
|
+
import { paths } from "../lib/paths.ts";
|
|
4
|
+
import type { Provider } from "../lib/provider.ts";
|
|
5
|
+
import { loadAccounts, loadConfig, saveAccounts, upsertAccount } from "../lib/state.ts";
|
|
6
|
+
import { c, count } from "./render.ts";
|
|
13
7
|
|
|
14
|
-
function
|
|
15
|
-
const rc = shellRcPath();
|
|
16
|
-
if (!rc) {
|
|
17
|
-
console.log(c.yellow(`⚠ add to your shell rc: export PATH="${paths.binDir}:$PATH"`));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const outcome = ensurePathInRc(rc);
|
|
21
|
-
if (outcome === "added") console.log(`${c.green("✓")} added ${paths.binDir} to PATH in ${rc} - restart your shell (or \`source ${rc}\`)`);
|
|
22
|
-
else if (outcome === "skipped") {
|
|
23
|
-
const hint = managedShellRcSkipLines();
|
|
24
|
-
console.log(c.yellow(`⚠ ${hint.headline}`));
|
|
25
|
-
console.log(c.yellow(` ${hint.detail}`));
|
|
26
|
-
console.log(c.yellow(` ${hint.exportLine}`));
|
|
27
|
-
} else console.log(c.yellow(`⚠ PATH line already in ${rc} - restart your shell to pick it up`));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function reportTimer(out: InstallOutcome): void {
|
|
31
|
-
if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
|
|
32
|
-
else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function printUsage(): void {
|
|
8
|
+
export function printUsage(p: Provider): void {
|
|
36
9
|
console.log();
|
|
37
10
|
console.log(` ${c.bold("how to use")} - ${c.cyan("xx")} is shorthand for ${c.cyan("tokenmaxxing")}:`);
|
|
38
|
-
console.log(` ${c.cyan(
|
|
11
|
+
console.log(` ${c.cyan(p.name)} use ${p.name} as always; it switches accounts near quota automatically`);
|
|
39
12
|
console.log(` ${c.cyan("xx")} show the pool with usage bars (same as ${c.cyan("xx status")})`);
|
|
40
|
-
console.log(` ${c.cyan(
|
|
41
|
-
console.log(` ${c.cyan(
|
|
13
|
+
console.log(` ${c.cyan(`xx add${p.flag}`)} log in and pool another account`);
|
|
14
|
+
console.log(` ${c.cyan(`xx switch${p.flag}`)} hop to the best account right now (the automatic switching needs no command)`);
|
|
42
15
|
console.log(` ${c.cyan("xx help")} everything else`);
|
|
43
16
|
}
|
|
44
17
|
|
|
45
|
-
export async function cmdInit(): Promise<number> {
|
|
18
|
+
export async function cmdInit(p: Provider): Promise<number> {
|
|
46
19
|
mkdirSync(paths.home, { recursive: true });
|
|
47
20
|
loadConfig();
|
|
21
|
+
p.preflight();
|
|
48
22
|
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
console.log(`${c.green("✓")} re-installed supervisor + hooks (pool already has ${existingIdx.accounts.length} account${existingIdx.accounts.length === 1 ? "" : "s"} - not re-importing)`);
|
|
55
|
-
reportTimer(out);
|
|
56
|
-
if (!out.pathAhead) ensurePathAhead();
|
|
23
|
+
const existing = loadAccounts(p.pool);
|
|
24
|
+
if (existing.accounts.length > 0) {
|
|
25
|
+
p.install();
|
|
26
|
+
const active = existing.accounts.find((a) => a.id === existing.activeId);
|
|
27
|
+
console.log(`${c.green("✓")} re-installed (pool already has ${count({ n: existing.accounts.length, noun: "account" })} - not re-importing)`);
|
|
57
28
|
console.log(` active: ${c.bold(active?.label ?? "unknown")}`);
|
|
58
|
-
printUsage();
|
|
29
|
+
printUsage(p);
|
|
59
30
|
return 0;
|
|
60
31
|
}
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} with a Pro/Max account first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const oauthAccount = readOAuthAccount();
|
|
69
|
-
const liveRaw = await readItem(liveTarget());
|
|
70
|
-
if (!oauthAccount || !liveRaw) {
|
|
71
|
-
console.error(c.red("no active Claude subscription login found (missing oauthAccount or credential)."));
|
|
72
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
33
|
+
const harvested = await p.importLive();
|
|
34
|
+
if (!harvested) return 1;
|
|
75
35
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let creds = blob.claudeAiOauth;
|
|
85
|
-
if (isDeadCredential(creds)) {
|
|
86
|
-
console.error(c.red("the live credential was cleared after a failed refresh."));
|
|
87
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} first, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
88
|
-
return 1;
|
|
89
|
-
}
|
|
90
|
-
if (isAccessTokenExpiring(creds)) {
|
|
91
|
-
await withClaudeRefreshLock(async (lock) => {
|
|
92
|
-
const raw2 = await readItem(liveTarget());
|
|
93
|
-
if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
|
|
94
|
-
const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
|
|
95
|
-
creds = isAccessTokenExpiring(current) ? await refreshCredential(current) : current;
|
|
96
|
-
if (creds === current) return;
|
|
97
|
-
if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
|
|
98
|
-
await writeItem(liveTarget(), mergeIntoLive(raw2, creds));
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
const identity = await fetchTokenIdentity(creds.accessToken);
|
|
102
|
-
if (identity.accountUuid !== oauthAccount.accountUuid) {
|
|
103
|
-
console.error(c.red(`the live credential belongs to ${describeIdentity(identity)}, but ~/.claude.json identifies ${oauthAccount.emailAddress} - identity drift.`));
|
|
104
|
-
console.error(`Run ${c.cyan("claude")} → ${c.cyan("/login")} to realign them, then re-run ${c.cyan("tokenmaxxing init")}.`);
|
|
105
|
-
return 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const uuid = oauthAccount.accountUuid;
|
|
109
|
-
const keychainItem = credItemFor(uuid);
|
|
110
|
-
const account = await withLock(paths.lockFile, async () => {
|
|
111
|
-
await writeItem(parkedTarget(keychainItem), JSON.stringify({ claudeAiOauth: creds }));
|
|
112
|
-
const idx = loadAccounts();
|
|
113
|
-
const existing = idx.accounts.find((a) => a.accountUuid === uuid);
|
|
114
|
-
const imported: Account = {
|
|
115
|
-
accountUuid: uuid,
|
|
116
|
-
email: oauthAccount.emailAddress,
|
|
117
|
-
organizationUuid: oauthAccount.organizationUuid,
|
|
118
|
-
label: existing?.label ?? oauthAccount.emailAddress,
|
|
119
|
-
keychainItem,
|
|
120
|
-
oauthAccount,
|
|
121
|
-
addedAt: existing?.addedAt ?? new Date().toISOString(),
|
|
122
|
-
subscriptionType: blob.claudeAiOauth.subscriptionType,
|
|
123
|
-
rateLimitTier: blob.claudeAiOauth.rateLimitTier,
|
|
124
|
-
needsReauth: false,
|
|
125
|
-
};
|
|
126
|
-
if (existing) Object.assign(existing, imported);
|
|
127
|
-
else idx.accounts.push(imported);
|
|
128
|
-
idx.activeAccountUuid = uuid;
|
|
129
|
-
saveAccounts(idx);
|
|
36
|
+
const account = await withLock(p.pool.lockFile, async () => {
|
|
37
|
+
await harvested.park();
|
|
38
|
+
const idx = loadAccounts(p.pool);
|
|
39
|
+
const imported = upsertAccount(idx, harvested, p.mergeWindows);
|
|
40
|
+
idx.activeId = harvested.id;
|
|
41
|
+
saveAccounts(p.pool, idx);
|
|
130
42
|
return imported;
|
|
131
43
|
});
|
|
132
44
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
console.log(`${c.green("✓")} imported current account → ${c.bold(account.email)} (${claudeTierLabel(account) ?? "?"})`);
|
|
138
|
-
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/SessionStart hooks`);
|
|
139
|
-
reportTimer(out);
|
|
140
|
-
if (!out.pathAhead) {
|
|
141
|
-
console.log();
|
|
142
|
-
ensurePathAhead();
|
|
143
|
-
}
|
|
144
|
-
const poolSize = loadAccounts().accounts.length;
|
|
45
|
+
console.log(`${c.green("✓")} imported current account → ${c.bold(account.label)} (${account.tier ?? "?"})`);
|
|
46
|
+
p.install();
|
|
47
|
+
const poolSize = loadAccounts(p.pool).accounts.length;
|
|
145
48
|
console.log();
|
|
146
|
-
console.log(` pool ready (${
|
|
147
|
-
printUsage();
|
|
49
|
+
console.log(` pool ready (${count({ n: poolSize, noun: "account" })})`);
|
|
50
|
+
printUsage(p);
|
|
148
51
|
return 0;
|
|
149
52
|
}
|
package/src/cli/rename.ts
CHANGED
|
@@ -1,76 +1,40 @@
|
|
|
1
1
|
import { withLock } from "../lib/lock.ts";
|
|
2
|
-
import {
|
|
2
|
+
import type { Provider } from "../lib/provider.ts";
|
|
3
3
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type { Account, CodexAccount } from "../lib/types.ts";
|
|
4
|
+
import { c, emitError, plain } from "./render.ts";
|
|
5
|
+
import type { Account } from "../lib/types.ts";
|
|
7
6
|
|
|
8
7
|
export function findAccount(accounts: Account[], selector: string): Account | undefined {
|
|
9
|
-
const s = selector.toLowerCase();
|
|
10
|
-
return (
|
|
11
|
-
accounts.find((a) => a.email.toLowerCase() === s) ??
|
|
12
|
-
accounts.find((a) => a.label.toLowerCase() === s) ??
|
|
13
|
-
accounts.find((a) => a.accountUuid.toLowerCase().startsWith(s))
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function findCodexAccount(accounts: CodexAccount[], selector: string): CodexAccount | undefined {
|
|
18
8
|
const s = selector.toLowerCase();
|
|
19
9
|
return (
|
|
20
10
|
accounts.find((a) => a.email?.toLowerCase() === s) ??
|
|
21
11
|
accounts.find((a) => a.label.toLowerCase() === s) ??
|
|
22
|
-
accounts.find((a) => a.
|
|
12
|
+
accounts.find((a) => a.id.toLowerCase().startsWith(s))
|
|
23
13
|
);
|
|
24
14
|
}
|
|
25
15
|
|
|
26
|
-
async function
|
|
27
|
-
const
|
|
28
|
-
return withLock(codexPaths.lockFile, async () => {
|
|
29
|
-
const index = loadCodexAccounts();
|
|
30
|
-
const account = findCodexAccount(index.accounts, selector);
|
|
31
|
-
if (!account) {
|
|
32
|
-
emitError({ json, message: `no codex account matches "${selector}"` });
|
|
33
|
-
return 1;
|
|
34
|
-
}
|
|
35
|
-
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
36
|
-
if (taken) {
|
|
37
|
-
emitError({ json, message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
|
|
38
|
-
return 1;
|
|
39
|
-
}
|
|
40
|
-
const old = account.label;
|
|
41
|
-
account.label = newLabel;
|
|
42
|
-
saveCodexAccounts({ index });
|
|
43
|
-
if (json) emitJson({ ok: true, pool: "codex", from: old, to: newLabel });
|
|
44
|
-
else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
45
|
-
return 0;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function cmdRename(argv: string[], json = false): Promise<number> {
|
|
50
|
-
const codex = argv.includes("--codex");
|
|
51
|
-
const [selector, newLabel] = argv.filter((a) => a !== "--codex");
|
|
16
|
+
export async function cmdRename(p: Provider, argv: string[]): Promise<number> {
|
|
17
|
+
const [selector, newLabel] = argv;
|
|
52
18
|
if (!selector || !newLabel) {
|
|
53
|
-
emitError({
|
|
19
|
+
emitError({ message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
|
|
54
20
|
return 2;
|
|
55
21
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const idx = loadAccounts();
|
|
22
|
+
return withLock(p.pool.lockFile, async () => {
|
|
23
|
+
const idx = loadAccounts(p.pool);
|
|
59
24
|
const a = findAccount(idx.accounts, selector);
|
|
60
25
|
if (!a) {
|
|
61
|
-
emitError({
|
|
26
|
+
emitError({ message: `no ${p.name} account matches "${selector}"` });
|
|
62
27
|
return 1;
|
|
63
28
|
}
|
|
64
|
-
const taken = idx.accounts.find((x) => x.
|
|
29
|
+
const taken = idx.accounts.find((x) => x.id !== a.id && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
65
30
|
if (taken) {
|
|
66
|
-
emitError({
|
|
31
|
+
emitError({ message: `label "${newLabel}" is already used by ${taken.email ?? taken.id.slice(0, 8)} - labels must be unique within the pool` });
|
|
67
32
|
return 1;
|
|
68
33
|
}
|
|
69
34
|
const old = a.label;
|
|
70
35
|
a.label = newLabel;
|
|
71
|
-
saveAccounts(idx);
|
|
72
|
-
|
|
73
|
-
else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
36
|
+
saveAccounts(p.pool, idx);
|
|
37
|
+
console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
74
38
|
return 0;
|
|
75
39
|
});
|
|
76
40
|
}
|
package/src/cli/render.ts
CHANGED
|
@@ -33,13 +33,6 @@ export function makeUsagePaint(input: { enabled: boolean; truecolor: boolean })
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function claudeTierLabel(input: { subscriptionType?: string; rateLimitTier?: string }): string | null {
|
|
37
|
-
const segments = input.rateLimitTier?.split("_") ?? [];
|
|
38
|
-
const multiplier = segments.find((seg) => seg.length > 1 && seg.endsWith("x") && Number.isInteger(Number(seg.slice(0, -1))));
|
|
39
|
-
if (input.subscriptionType == null) return multiplier ?? null;
|
|
40
|
-
return multiplier ? `${input.subscriptionType} ${multiplier}` : input.subscriptionType;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
36
|
export function count(input: { n: number; noun: string }): string {
|
|
44
37
|
return `${input.n} ${input.noun}${input.n === 1 ? "" : "s"}`;
|
|
45
38
|
}
|
|
@@ -82,7 +75,7 @@ export function emitJson(value: unknown): void {
|
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
export function emitError(input: {
|
|
85
|
-
json
|
|
78
|
+
json?: boolean;
|
|
86
79
|
message: string;
|
|
87
80
|
notes?: string[];
|
|
88
81
|
extra?: Record<string, unknown>;
|
package/src/cli/rm.ts
CHANGED
|
@@ -1,63 +1,46 @@
|
|
|
1
|
-
import { rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { deleteItem, isolatedTarget, liveTarget, parkedTarget, readItem } from "../lib/credstore.ts";
|
|
4
|
-
import { fetchTokenIdentity } from "../lib/oauth.ts";
|
|
5
1
|
import { withLock } from "../lib/lock.ts";
|
|
6
|
-
import {
|
|
7
|
-
import { loadSetupTokens, saveSetupTokens } from "../lib/setuptokens.ts";
|
|
2
|
+
import type { Provider } from "../lib/provider.ts";
|
|
8
3
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
9
|
-
import { CredentialBlobSchema } from "../lib/types.ts";
|
|
10
4
|
import { findAccount } from "./rename.ts";
|
|
11
|
-
import { c, emitError,
|
|
5
|
+
import { c, emitError, plain } from "./render.ts";
|
|
12
6
|
|
|
13
|
-
export async function cmdRm(selector?: string
|
|
7
|
+
export async function cmdRm(p: Provider, selector?: string): Promise<number> {
|
|
14
8
|
if (!selector) {
|
|
15
|
-
emitError({
|
|
9
|
+
emitError({ message: `usage: tokenmaxxing rm${p.flag} <email|label|id>`, paint: plain });
|
|
16
10
|
return 2;
|
|
17
11
|
}
|
|
18
|
-
return withLock(
|
|
19
|
-
const idx = loadAccounts();
|
|
12
|
+
return withLock(p.pool.lockFile, async () => {
|
|
13
|
+
const idx = loadAccounts(p.pool);
|
|
20
14
|
const a = findAccount(idx.accounts, selector);
|
|
21
15
|
if (!a) {
|
|
22
|
-
emitError({
|
|
16
|
+
emitError({ message: `no account matches "${selector}"` });
|
|
23
17
|
return 1;
|
|
24
18
|
}
|
|
25
|
-
if (a.
|
|
26
|
-
emitError({
|
|
19
|
+
if (a.id === idx.activeId) {
|
|
20
|
+
emitError({ message: `${a.label} is the ACTIVE account - switch away before removing it.` });
|
|
27
21
|
return 1;
|
|
28
22
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
json,
|
|
38
|
-
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
|
-
});
|
|
40
|
-
return 1;
|
|
41
|
-
}
|
|
42
|
-
if (liveAccount === a.accountUuid) {
|
|
43
|
-
emitError({ json, 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
|
-
return 1;
|
|
45
|
-
}
|
|
23
|
+
let liveOwner: string | null;
|
|
24
|
+
try {
|
|
25
|
+
liveOwner = await p.liveOwner();
|
|
26
|
+
} catch (e) {
|
|
27
|
+
emitError({
|
|
28
|
+
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.`,
|
|
29
|
+
});
|
|
30
|
+
return 1;
|
|
46
31
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
for (const sampleDir of [join(paths.sampleDir, item), join(paths.sampleDir, `${item}-tick`)]) {
|
|
51
|
-
await deleteItem(isolatedTarget(sampleDir));
|
|
52
|
-
rmSync(sampleDir, { recursive: true, force: true });
|
|
32
|
+
if (liveOwner === a.id) {
|
|
33
|
+
emitError({ message: `${a.label}'s credential is currently LIVE (the active label is stale - a manual login drifted it); run \`tokenmaxxing switch${p.flag}\` to move off it first.` });
|
|
34
|
+
return 1;
|
|
53
35
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
saveSetupTokens({ ...setupTokens, tokens: setupTokens.tokens.filter((t) => t.accountUuid !== a.accountUuid) });
|
|
36
|
+
if (p.presentIds().has(a.id)) {
|
|
37
|
+
emitError({ message: `${a.label} is running in a live ${p.name} session - close that session before removing it.` });
|
|
38
|
+
return 1;
|
|
58
39
|
}
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
await p.removeCredentials(a);
|
|
41
|
+
idx.accounts = idx.accounts.filter((x) => x.id !== a.id);
|
|
42
|
+
saveAccounts(p.pool, idx);
|
|
43
|
+
console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
|
|
61
44
|
return 0;
|
|
62
45
|
});
|
|
63
46
|
}
|
package/src/cli/setuptoken.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { mkdirSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveRealClaude } from "../lib/claudebin.ts";
|
|
4
4
|
import { withLock } from "../lib/lock.ts";
|
|
5
|
-
import { paths } from "../lib/paths.ts";
|
|
5
|
+
import { claudePool, credItemFor, paths } from "../lib/paths.ts";
|
|
6
6
|
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";
|
|
@@ -53,19 +53,19 @@ async function mint(input: { real: string; item: string }): Promise<string | nul
|
|
|
53
53
|
|
|
54
54
|
function currentTokens(tokens: SetupToken[], accounts: Account[]): { label: string; token: string }[] {
|
|
55
55
|
return accounts.flatMap((a) => {
|
|
56
|
-
const stored = tokens.find((t) => t.accountUuid === a.
|
|
56
|
+
const stored = tokens.find((t) => t.accountUuid === a.id);
|
|
57
57
|
return stored ? [{ label: a.label, token: stored.token }] : [];
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async function rmToken(selector: string
|
|
62
|
-
const outcome = await withLock(
|
|
61
|
+
async function rmToken(selector: string): Promise<number> {
|
|
62
|
+
const outcome = await withLock(claudePool.lockFile, async (): Promise<{ error: string } | { kept: SetupToken[] }> => {
|
|
63
63
|
const store = loadSetupTokens();
|
|
64
|
-
const accounts = loadAccounts().accounts;
|
|
64
|
+
const accounts = loadAccounts(claudePool).accounts;
|
|
65
65
|
const pooled = accounts.find((a) => a.label === selector);
|
|
66
|
-
const orphan = (t: SetupToken) => !accounts.some((a) => a.
|
|
66
|
+
const orphan = (t: SetupToken) => !accounts.some((a) => a.id === t.accountUuid);
|
|
67
67
|
const targets = store.tokens.filter((t) =>
|
|
68
|
-
t.accountUuid === selector || (pooled ? t.accountUuid === pooled.
|
|
68
|
+
t.accountUuid === selector || (pooled ? t.accountUuid === pooled.id : t.label === selector && orphan(t)),
|
|
69
69
|
);
|
|
70
70
|
if (targets.length === 0) return { error: `no setup token stored for "${selector}"` };
|
|
71
71
|
if (targets.length > 1) return { error: `"${selector}" matches ${targets.length} stored tokens (${targets.map((t) => `${t.label} ${t.accountUuid}`).join(", ")}) - nothing removed` };
|
|
@@ -74,50 +74,41 @@ 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
|
-
let idx = loadAccounts();
|
|
93
|
+
let idx = loadAccounts(claudePool);
|
|
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();
|
|
109
|
-
const missing = idx.accounts.filter((a) => !store.tokens.some((t) => t.accountUuid === a.
|
|
100
|
+
const missing = idx.accounts.filter((a) => !store.tokens.some((t) => t.accountUuid === a.id));
|
|
110
101
|
if (missing.length === 0) console.log(c.dim("every pooled account already has a stored setup token (`setup-token rm <label>` drops one so it can be re-minted)"));
|
|
111
102
|
for (const a of missing) {
|
|
112
103
|
console.log();
|
|
113
|
-
console.log(`${c.bold(a.label)} - sign into this account in the browser (${a.email})`);
|
|
114
|
-
const token = await mint({ real, item: a.
|
|
104
|
+
console.log(`${c.bold(a.label)} - sign into this account in the browser (${a.email ?? a.label})`);
|
|
105
|
+
const token = await mint({ real, item: credItemFor(a.id) });
|
|
115
106
|
if (!token) return 1;
|
|
116
|
-
const stored = await withLock(
|
|
117
|
-
if (!loadAccounts().accounts.some((x) => x.
|
|
107
|
+
const stored = await withLock(claudePool.lockFile, async () => {
|
|
108
|
+
if (!loadAccounts(claudePool).accounts.some((x) => x.id === a.id)) return false;
|
|
118
109
|
const fresh = loadSetupTokens();
|
|
119
|
-
const tokens = fresh.tokens.filter((t) => t.accountUuid !== a.
|
|
120
|
-
saveSetupTokens({ ...fresh, tokens: [...tokens, { accountUuid: a.
|
|
110
|
+
const tokens = fresh.tokens.filter((t) => t.accountUuid !== a.id);
|
|
111
|
+
saveSetupTokens({ ...fresh, tokens: [...tokens, { accountUuid: a.id, label: a.label, token, mintedAt: Date.now() }] });
|
|
121
112
|
return true;
|
|
122
113
|
});
|
|
123
114
|
if (!stored) {
|
|
@@ -127,26 +118,22 @@ export async function cmdSetupToken(args: string[], json = false): Promise<numbe
|
|
|
127
118
|
console.log(`${c.green("✓")} stored the setup token for ${c.bold(a.label)} ${c.dim("(ownership not verified: an inference-only token cannot read the profile endpoint, so the label is the sign-in you chose)")}`);
|
|
128
119
|
}
|
|
129
120
|
store = loadSetupTokens();
|
|
130
|
-
idx = loadAccounts();
|
|
121
|
+
idx = loadAccounts(claudePool);
|
|
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);
|
|
137
128
|
const orphaned = store.tokens
|
|
138
|
-
.filter((t) => !idx.accounts.some((a) => a.
|
|
129
|
+
.filter((t) => !idx.accounts.some((a) => a.id === 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`));
|