tokenmaxxing 1.34.0 → 1.38.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.
Files changed (54) hide show
  1. package/DESIGN.md +3 -5
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +23 -40
  5. package/src/cli/auth.ts +38 -52
  6. package/src/cli/check.ts +2 -1
  7. package/src/cli/doctor.ts +13 -13
  8. package/src/cli/init.ts +29 -126
  9. package/src/cli/rename.ts +11 -45
  10. package/src/cli/render.ts +0 -7
  11. package/src/cli/rm.ts +24 -39
  12. package/src/cli/setuptoken.ts +16 -16
  13. package/src/cli/status.ts +95 -238
  14. package/src/cli/switch.ts +54 -46
  15. package/src/entries/codexstophook.ts +23 -23
  16. package/src/entries/codexsupervisor.ts +3 -3
  17. package/src/entries/sessionstart.ts +5 -3
  18. package/src/entries/statusline.ts +17 -19
  19. package/src/entries/stopfailurehook.ts +9 -7
  20. package/src/entries/stophook.ts +5 -4
  21. package/src/entries/subagentstatusline.ts +2 -7
  22. package/src/lib/atomic.ts +2 -8
  23. package/src/lib/claude.ts +320 -0
  24. package/src/lib/claudelock.ts +2 -3
  25. package/src/lib/codex.ts +345 -0
  26. package/src/lib/codexauth.ts +9 -7
  27. package/src/lib/codexoauth.ts +5 -11
  28. package/src/lib/codexpresence.ts +3 -19
  29. package/src/lib/codexusage.ts +17 -41
  30. package/src/lib/credstore.ts +3 -7
  31. package/src/lib/decide.ts +86 -174
  32. package/src/lib/http.ts +3 -6
  33. package/src/lib/install.ts +4 -7
  34. package/src/lib/oauth.ts +14 -12
  35. package/src/lib/paths.ts +15 -7
  36. package/src/lib/picker.ts +56 -37
  37. package/src/lib/proc.ts +4 -4
  38. package/src/lib/provider.ts +34 -0
  39. package/src/lib/sample.ts +33 -33
  40. package/src/lib/state.ts +51 -18
  41. package/src/lib/swap.ts +41 -38
  42. package/src/lib/types.ts +31 -51
  43. package/src/lib/usage.ts +28 -20
  44. package/src/main.ts +20 -20
  45. package/src/cli/codexadd.ts +0 -89
  46. package/src/cli/codexinit.ts +0 -120
  47. package/src/cli/codexrm.ts +0 -36
  48. package/src/cli/codexswitch.ts +0 -113
  49. package/src/cli/onboard.ts +0 -92
  50. package/src/lib/codexdecide.ts +0 -157
  51. package/src/lib/codexpick.ts +0 -80
  52. package/src/lib/codexsample.ts +0 -48
  53. package/src/lib/codexstate.ts +0 -22
  54. package/src/lib/codexswap.ts +0 -70
package/src/cli/rename.ts CHANGED
@@ -1,73 +1,39 @@
1
1
  import { withLock } from "../lib/lock.ts";
2
- import { codexPaths, paths } from "../lib/paths.ts";
2
+ import type { Provider } from "../lib/provider.ts";
3
3
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
4
- import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
5
4
  import { c, emitError, plain } from "./render.ts";
6
- import type { Account, CodexAccount } from "../lib/types.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.accountId.toLowerCase().startsWith(s))
12
+ accounts.find((a) => a.id.toLowerCase().startsWith(s))
23
13
  );
24
14
  }
25
15
 
26
- async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
27
- const { selector, newLabel } = input;
28
- return withLock(codexPaths.lockFile, async () => {
29
- const index = loadCodexAccounts();
30
- const account = findCodexAccount(index.accounts, selector);
31
- if (!account) {
32
- emitError({ 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({ 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
- console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
44
- return 0;
45
- });
46
- }
47
-
48
- export async function cmdRename(argv: string[]): Promise<number> {
49
- const codex = argv.includes("--codex");
50
- const [selector, newLabel] = argv.filter((a) => a !== "--codex");
16
+ export async function cmdRename(p: Provider, argv: string[]): Promise<number> {
17
+ const [selector, newLabel] = argv;
51
18
  if (!selector || !newLabel) {
52
19
  emitError({ message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
53
20
  return 2;
54
21
  }
55
- if (codex) return renameCodexAccount({ selector, newLabel });
56
- return withLock(paths.lockFile, async () => {
57
- const idx = loadAccounts();
22
+ return withLock(p.pool.lockFile, async () => {
23
+ const idx = loadAccounts(p.pool);
58
24
  const a = findAccount(idx.accounts, selector);
59
25
  if (!a) {
60
- emitError({ message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
26
+ emitError({ message: `no ${p.name} account matches "${selector}"` });
61
27
  return 1;
62
28
  }
63
- const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
29
+ const taken = idx.accounts.find((x) => x.id !== a.id && x.label.toLowerCase() === newLabel.toLowerCase());
64
30
  if (taken) {
65
- emitError({ message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
31
+ emitError({ message: `label "${newLabel}" is already used by ${taken.email ?? taken.id.slice(0, 8)} - labels must be unique within the pool` });
66
32
  return 1;
67
33
  }
68
34
  const old = a.label;
69
35
  a.label = newLabel;
70
- saveAccounts(idx);
36
+ saveAccounts(p.pool, idx);
71
37
  console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
72
38
  return 0;
73
39
  });
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
  }
package/src/cli/rm.ts CHANGED
@@ -1,60 +1,45 @@
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 { credItemFor, paths } from "../lib/paths.ts";
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
5
  import { c, emitError, plain } from "./render.ts";
12
6
 
13
- export async function cmdRm(selector?: string): Promise<number> {
7
+ export async function cmdRm(p: Provider, selector?: string): Promise<number> {
14
8
  if (!selector) {
15
- emitError({ message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
9
+ emitError({ message: `usage: tokenmaxxing rm${p.flag} <email|label|id>`, paint: plain });
16
10
  return 2;
17
11
  }
18
- return withLock(paths.lockFile, async () => {
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
16
  emitError({ message: `no account matches "${selector}"` });
23
17
  return 1;
24
18
  }
25
- if (a.accountUuid === idx.activeAccountUuid) {
26
- emitError({ message: `${a.email} is the ACTIVE account - switch away before removing it.` });
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
- const live = await readItem(liveTarget());
30
- if (live != null) {
31
- let liveAccount: string;
32
- try {
33
- const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
34
- liveAccount = (await fetchTokenIdentity(liveCreds.accessToken)).accountUuid;
35
- } catch (e) {
36
- emitError({
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.`,
38
- });
39
- return 1;
40
- }
41
- if (liveAccount === a.accountUuid) {
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.` });
43
- return 1;
44
- }
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;
45
31
  }
46
- const setupTokens = loadSetupTokens();
47
- await deleteItem(parkedTarget(a.keychainItem));
48
- const item = credItemFor(a.accountUuid);
49
- for (const sampleDir of [join(paths.sampleDir, item), join(paths.sampleDir, `${item}-tick`)]) {
50
- await deleteItem(isolatedTarget(sampleDir));
51
- 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;
52
35
  }
53
- idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
54
- saveAccounts(idx);
55
- if (setupTokens.tokens.some((t) => t.accountUuid === a.accountUuid)) {
56
- 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;
57
39
  }
40
+ await p.removeCredentials(a);
41
+ idx.accounts = idx.accounts.filter((x) => x.id !== a.id);
42
+ saveAccounts(p.pool, idx);
58
43
  console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
59
44
  return 0;
60
45
  });
@@ -2,7 +2,7 @@ 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";
@@ -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.accountUuid);
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
61
  async function rmToken(selector: string): Promise<number> {
62
- const outcome = await withLock(paths.lockFile, async (): Promise<{ error: string } | { kept: SetupToken[] }> => {
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.accountUuid === t.accountUuid);
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.accountUuid : t.label === selector && orphan(t)),
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` };
@@ -90,25 +90,25 @@ export async function cmdSetupToken(args: string[]): Promise<number> {
90
90
  return 2;
91
91
  }
92
92
  let store = loadSetupTokens();
93
- let idx = loadAccounts();
93
+ let idx = loadAccounts(claudePool);
94
94
  if (!print) {
95
95
  if (idx.accounts.length === 0) {
96
96
  emitError({ message: "no accounts in the pool - run `tokenmaxxing init` first" });
97
97
  return 1;
98
98
  }
99
99
  const real = resolveRealClaude();
100
- const missing = idx.accounts.filter((a) => !store.tokens.some((t) => t.accountUuid === a.accountUuid));
100
+ const missing = idx.accounts.filter((a) => !store.tokens.some((t) => t.accountUuid === a.id));
101
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)"));
102
102
  for (const a of missing) {
103
103
  console.log();
104
- console.log(`${c.bold(a.label)} - sign into this account in the browser (${a.email})`);
105
- const token = await mint({ real, item: a.keychainItem });
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) });
106
106
  if (!token) return 1;
107
- const stored = await withLock(paths.lockFile, async () => {
108
- if (!loadAccounts().accounts.some((x) => x.accountUuid === a.accountUuid)) return false;
107
+ const stored = await withLock(claudePool.lockFile, async () => {
108
+ if (!loadAccounts(claudePool).accounts.some((x) => x.id === a.id)) return false;
109
109
  const fresh = loadSetupTokens();
110
- const tokens = fresh.tokens.filter((t) => t.accountUuid !== a.accountUuid);
111
- saveSetupTokens({ ...fresh, tokens: [...tokens, { accountUuid: a.accountUuid, label: a.label, token, mintedAt: Date.now() }] });
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() }] });
112
112
  return true;
113
113
  });
114
114
  if (!stored) {
@@ -118,7 +118,7 @@ export async function cmdSetupToken(args: string[]): Promise<number> {
118
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)")}`);
119
119
  }
120
120
  store = loadSetupTokens();
121
- idx = loadAccounts();
121
+ idx = loadAccounts(claudePool);
122
122
  }
123
123
  if (store.tokens.length === 0) {
124
124
  emitError({ message: "no setup tokens stored - run `tokenmaxxing setup-token` to mint them" });
@@ -126,7 +126,7 @@ export async function cmdSetupToken(args: string[]): Promise<number> {
126
126
  }
127
127
  const tokens = currentTokens(store.tokens, idx.accounts);
128
128
  const orphaned = store.tokens
129
- .filter((t) => !idx.accounts.some((a) => a.accountUuid === t.accountUuid))
129
+ .filter((t) => !idx.accounts.some((a) => a.id === t.accountUuid))
130
130
  .map((t) => ({ label: t.label, accountUuid: t.accountUuid }));
131
131
  if (tokens.length === 0) {
132
132
  emitError({ message: "no stored setup token belongs to a pooled account - run `tokenmaxxing setup-token` to mint them" });