tokenmaxxing 1.7.0 → 1.9.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 (67) hide show
  1. package/DESIGN.md +2 -2
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +8 -2
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +11 -42
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +8 -34
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -63
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -23
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +57 -124
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -45
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -35
  67. package/src/sdk.ts +0 -59
package/src/cli/init.ts CHANGED
@@ -1,6 +1,3 @@
1
- // `tokenmaxxing init` - import the account you're already on (no prompts), then
2
- // install the supervisor + the four settings entries.
3
-
4
1
  import { mkdirSync } from "node:fs";
5
2
  import { isApiKeyMode, readOAuthAccount } from "../lib/claudejson.ts";
6
3
  import { readItem, writeItem, liveTarget, parkedTarget, mergeIntoLive } from "../lib/credstore.ts";
@@ -8,14 +5,12 @@ import { refreshCredential, isAccessTokenExpiring, fetchTokenOrg } from "../lib/
8
5
  import { withClaudeRefreshLock } from "../lib/claudelock.ts";
9
6
  import { loadAccounts, saveAccounts, loadConfig, pinBinOverride } from "../lib/state.ts";
10
7
  import { withLock } from "../lib/lock.ts";
11
- import { installSupervisor, shellRcPath, ensurePathInRc, timerActivationHint, type InstallOutcome } from "../lib/install.ts";
8
+ import { installSupervisor, shellRcPath, ensurePathInRc, managedShellRcSkipLines, timerActivationHint, type InstallOutcome } from "../lib/install.ts";
12
9
  import { resolveVerifiedClaude } from "../lib/claudebin.ts";
13
10
  import { credItemFor, paths } from "../lib/paths.ts";
14
11
  import { CredentialBlobSchema, type Account } from "../lib/types.ts";
15
12
  import { c, claudeTierLabel } from "./render.ts";
16
13
 
17
- /** Put the supervisor bin dir on PATH via the user's shell rc (idempotent).
18
- * Falls back to the manual instruction when the shell is unknown. */
19
14
  function ensurePathAhead(): void {
20
15
  const rc = shellRcPath();
21
16
  if (!rc) {
@@ -24,7 +19,12 @@ function ensurePathAhead(): void {
24
19
  }
25
20
  const outcome = ensurePathInRc(rc);
26
21
  if (outcome === "added") console.log(`${c.green("✓")} added ${paths.binDir} to PATH in ${rc} - restart your shell (or \`source ${rc}\`)`);
27
- else console.log(c.yellow(`⚠ PATH line already in ${rc} - restart your shell to pick it up`));
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
28
  }
29
29
 
30
30
  function reportTimer(out: InstallOutcome): void {
@@ -32,9 +32,6 @@ function reportTimer(out: InstallOutcome): void {
32
32
  else console.log(c.yellow(`⚠ check timer written but not activated - run: ${timerActivationHint()}`));
33
33
  }
34
34
 
35
- /** The how-to-use epilogue both init paths end on: the whole point of the tool
36
- * is that after init you just run `claude`, so say exactly that, and teach the
37
- * `xx` shorthand every other command hangs off. Exported for the render test. */
38
35
  export function printUsage(): void {
39
36
  console.log();
40
37
  console.log(` ${c.bold("how to use")} - ${c.cyan("xx")} is shorthand for ${c.cyan("tokenmaxxing")}:`);
@@ -48,25 +45,11 @@ export function printUsage(): void {
48
45
 
49
46
  export async function cmdInit(): Promise<number> {
50
47
  mkdirSync(paths.home, { recursive: true });
51
- // Fail fast on a broken merged config BEFORE installing or claiming a
52
- // successful repair: pinBinOverride writes sparsely without validating, so
53
- // without this gate a re-init printed success while hooks, switching, and
54
- // the statusline kept throwing on every loadConfig until the file was
55
- // hand-repaired (bugbot review catch, PR #33). The throw carries the
56
- // fix-or-remove recovery message.
57
48
  loadConfig();
58
49
 
59
- // Already initialized → repair install ONLY. Never re-import: ~/.claude.json's
60
- // oauthAccount can drift from the live keychain cred (after swaps / concurrent
61
- // sessions), and re-importing would park the wrong cred + mislabel active.
62
50
  const existingIdx = loadAccounts();
63
51
  if (existingIdx.accounts.length > 0) {
64
52
  const out = installSupervisor();
65
- // repair the claudeBin pin too - hooks run with claude's PATH and must
66
- // never have to guess which binary is the real claude. Verified pinning:
67
- // a pin that fails --version (or loops back into the wrapper) is replaced
68
- // by a fresh PATH scan instead of being re-saved. Sparse write: only the
69
- // pin lands in the file, never the merged config.
70
53
  pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() });
71
54
  const active = existingIdx.accounts.find((a) => a.accountUuid === existingIdx.activeAccountUuid);
72
55
  console.log(`${c.green("✓")} re-installed supervisor + hooks (pool already has ${existingIdx.accounts.length} account${existingIdx.accounts.length === 1 ? "" : "s"} - not re-importing)`);
@@ -99,13 +82,9 @@ export async function cmdInit(): Promise<number> {
99
82
  return 1;
100
83
  }
101
84
 
102
- // Verify the live credential actually belongs to the identity we're about to
103
- // file it under - ~/.claude.json's oauthAccount can drift from the live
104
- // keychain credential, and importing on drifted state parks a mislabeled blob.
105
85
  let creds = blob.claudeAiOauth;
106
86
  if (isAccessTokenExpiring(creds)) {
107
87
  await withClaudeRefreshLock(async (lock) => {
108
- // re-read inside the lock: a running claude may have rotated it already.
109
88
  const raw2 = await readItem(liveTarget());
110
89
  if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
111
90
  const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
@@ -124,12 +103,8 @@ export async function cmdInit(): Promise<number> {
124
103
 
125
104
  const uuid = oauthAccount.accountUuid;
126
105
  const keychainItem = credItemFor(uuid);
127
- // Park + index update under the tokenmaxxing flock, like every sibling
128
- // index writer (add/auth/rm/status/rename): unlocked, a concurrent `xx add`
129
- // completing between this path's load and save had its just-registered
130
- // account silently clobbered out of the index (closing-review catch).
131
106
  const account = await withLock(paths.lockFile, async () => {
132
- await writeItem(parkedTarget(keychainItem), JSON.stringify({ claudeAiOauth: creds })); // park a small backup
107
+ await writeItem(parkedTarget(keychainItem), JSON.stringify({ claudeAiOauth: creds }));
133
108
  const idx = loadAccounts();
134
109
  const existing = idx.accounts.find((a) => a.accountUuid === uuid);
135
110
  const imported: Account = {
@@ -151,7 +126,6 @@ export async function cmdInit(): Promise<number> {
151
126
  return imported;
152
127
  });
153
128
 
154
- // Sparse write: only the pin lands in the file, never the merged config.
155
129
  pinBinOverride({ key: "claudeBin", bin: resolveVerifiedClaude() });
156
130
 
157
131
  const out = installSupervisor();
package/src/cli/ls.ts CHANGED
@@ -1,5 +1,3 @@
1
- // `tokenmaxxing ls` - compact list of pooled accounts, claude then codex.
2
-
3
1
  import { loadAccounts } from "../lib/state.ts";
4
2
  import { loadCodexAccounts } from "../lib/codexstate.ts";
5
3
  import { liveCodexAccountId } from "../lib/codexsample.ts";
@@ -1,10 +1,3 @@
1
- // One isolated Claude login, harvested. Shared by `add` (register whatever
2
- // account the login lands on) and `auth` (require it to match an existing
3
- // account). Drives the login in a throwaway CLAUDE_CONFIG_DIR - the ONLY use
4
- // of CLAUDE_CONFIG_DIR in the tool - auto-exits the moment the login lands,
5
- // samples that account's usage, then returns the credential + identity. The
6
- // temp dir and isolated credential are always destroyed before returning.
7
-
8
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
9
2
  import { join } from "node:path";
10
3
  import { z } from "zod";
@@ -17,7 +10,6 @@ import { CredentialBlobSchema, OAuthAccountSchema } from "../lib/types.ts";
17
10
  import { c } from "./render.ts";
18
11
 
19
12
  const HarvestedLoginSchema = z.object({
20
- /** the raw isolated credential blob (park it via claudeAiOauthOnly). */
21
13
  blobRaw: z.string(),
22
14
  blob: CredentialBlobSchema,
23
15
  oauthAccount: OAuthAccountSchema,
@@ -25,7 +17,6 @@ const HarvestedLoginSchema = z.object({
25
17
  });
26
18
  export type HarvestedLogin = z.infer<typeof HarvestedLoginSchema>;
27
19
 
28
- /** True once `/login` has written a usable identity into the onboard dir. */
29
20
  function identityReady(cjPath: string): boolean {
30
21
  if (!existsSync(cjPath)) return false;
31
22
  try {
@@ -36,32 +27,16 @@ function identityReady(cjPath: string): boolean {
36
27
  }
37
28
  }
38
29
 
39
- /**
40
- * Run one isolated login session and harvest it. The caller prints its own
41
- * instructions (which account to sign in as) BEFORE calling. Prints progress
42
- * and failure diagnostics; returns null when no usable login landed.
43
- */
44
30
  export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
45
31
  const onboardDir = paths.onboardDir;
46
32
  rmSync(onboardDir, { recursive: true, force: true });
47
33
  mkdirSync(onboardDir, { recursive: true });
48
34
  const iso = isolatedTarget(onboardDir);
49
- // Reap a PREVIOUS run's stranded isolated credential too: on macOS the
50
- // isolated store is a namespaced KEYCHAIN item, not a file in onboardDir,
51
- // so the rmSync above never touched it - a signal-killed harvest (Ctrl-C
52
- // after /login, before the ~400ms poll) left a live credential in the
53
- // keychain that the next run's spawned claude would silently reuse as an
54
- // already-authenticated session (closing-review catch). deleteItem on a
55
- // missing item is a no-op; on Linux iso is a file inside onboardDir and the
56
- // rmSync already covered it.
57
35
  await deleteItem(iso);
58
36
  const cjPath = join(onboardDir, ".claude.json");
59
37
  const real = resolveRealClaude();
60
38
 
61
39
  const savedTermios = saveTermios();
62
- // Scrub the ambient credential/identity overrides claude honors BEFORE its
63
- // keychain lookup (verified 2.1.205) - the onboard session must authenticate
64
- // only via the /login the user performs inside it.
65
40
  const env: Record<string, string> = { ...process.env, CLAUDE_CONFIG_DIR: onboardDir, TOKENMAXXING_PROBE: "1", TOKENMAXXING_SUPERVISED: "" };
66
41
  for (const key of CRED_ENV_OVERRIDES) delete env[key];
67
42
  const p = Bun.spawn([real], {
@@ -71,15 +46,7 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
71
46
  env,
72
47
  });
73
48
 
74
- // The finally makes the "always destroyed before returning" header true for
75
- // every non-signal exit - INCLUDING a failure while the login session is
76
- // still being polled (review catch, PR #31): an exception must not strand a
77
- // plaintext credential on disk or leave the spawned claude running. (An
78
- // interactive Ctrl-C is reaped by the next run's cleanup-first, which
79
- // deletes the isolated keychain item as well as the dir.)
80
49
  try {
81
- // Auto-exit (#17): watch for a completed login - identity written AND the
82
- // isolated credential present - then SIGTERM claude. No manual /exit.
83
50
  let exited = false;
84
51
  const onExit = p.exited.then(() => { exited = true; });
85
52
  while (!exited) {
@@ -91,9 +58,6 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
91
58
  }
92
59
  await p.exited;
93
60
  await onExit;
94
- // restore promptly so the harvest's own output renders on a sane terminal;
95
- // the finally's second restore is an idempotent stty and covers the
96
- // thrown-mid-poll path.
97
61
  restoreTermios(savedTermios);
98
62
 
99
63
  const blobRaw = await readItem(iso);
@@ -111,7 +75,6 @@ export async function harvestIsolatedLogin(): Promise<HarvestedLogin | null> {
111
75
  return null;
112
76
  }
113
77
 
114
- // Sample usage now (#16) so the account isn't "not sampled yet" in status/ls.
115
78
  console.log(c.dim("sampling usage..."));
116
79
  const sampled = await probeUsage(onboardDir);
117
80
  if (!sampled) console.log(c.yellow("could not sample usage now - it will fill in on first use."));
package/src/cli/rename.ts CHANGED
@@ -1,8 +1,3 @@
1
- // `tokenmaxxing rename [--codex] <selector> <new-label>` - relabel a pooled
2
- // account. The pools are separate namespaces and one email can hold both a
3
- // claude and a codex account, so the codex pool is targeted explicitly via
4
- // `--codex` (mirroring `switch --codex`), never by searching both pools.
5
-
6
1
  import { withLock } from "../lib/lock.ts";
7
2
  import { codexPaths, paths } from "../lib/paths.ts";
8
3
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
@@ -10,7 +5,6 @@ import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
10
5
  import { c } from "./render.ts";
11
6
  import type { Account, CodexAccount } from "../lib/types.ts";
12
7
 
13
- /** Resolve a claude account by email, label, or accountUuid prefix. */
14
8
  export function findAccount(accounts: Account[], selector: string): Account | undefined {
15
9
  const s = selector.toLowerCase();
16
10
  return (
@@ -20,7 +14,6 @@ export function findAccount(accounts: Account[], selector: string): Account | un
20
14
  );
21
15
  }
22
16
 
23
- /** Resolve a codex account by email, label, or accountId prefix. */
24
17
  export function findCodexAccount(accounts: CodexAccount[], selector: string): CodexAccount | undefined {
25
18
  const s = selector.toLowerCase();
26
19
  return (
@@ -31,7 +24,6 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
31
24
  }
32
25
 
33
26
  async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
34
- // under the codex flock: a concurrent codex swap's index write must not be clobbered.
35
27
  return withLock(codexPaths.lockFile, async () => {
36
28
  const index = loadCodexAccounts();
37
29
  const account = findCodexAccount(index.accounts, input.selector);
@@ -39,11 +31,6 @@ async function renameCodexAccount(input: { selector: string; newLabel: string })
39
31
  console.error(c.red(`no codex account matches "${input.selector}"`));
40
32
  return 1;
41
33
  }
42
- // labels resolve selectors first-match: a duplicate would make the other
43
- // account unreachable by label and misdirect destructive commands like
44
- // `rm` onto the wrong one (adversarial-review catch)
45
- // case-insensitive, matching how findCodexAccount resolves selectors (PR
46
- // #37 review catch: a casing-only duplicate slipped the === guard)
47
34
  const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === input.newLabel.toLowerCase());
48
35
  if (taken) {
49
36
  console.error(c.red(`label "${input.newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool`));
@@ -65,7 +52,6 @@ export async function cmdRename(argv: string[]): Promise<number> {
65
52
  return 2;
66
53
  }
67
54
  if (codex) return renameCodexAccount({ selector, newLabel });
68
- // under the flock: a concurrent swap's index write must not be clobbered.
69
55
  return withLock(paths.lockFile, async () => {
70
56
  const idx = loadAccounts();
71
57
  const a = findAccount(idx.accounts, selector);
@@ -73,11 +59,6 @@ export async function cmdRename(argv: string[]): Promise<number> {
73
59
  console.error(c.red(`no claude account matches "${selector}" (codex accounts rename via --codex)`));
74
60
  return 1;
75
61
  }
76
- // labels resolve selectors first-match: a duplicate would make the other
77
- // account unreachable by label and misdirect destructive commands like
78
- // `rm` onto the wrong one (adversarial-review catch)
79
- // case-insensitive, matching how findAccount resolves selectors (PR #37
80
- // review catch: a casing-only duplicate slipped the === guard)
81
62
  const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
82
63
  if (taken) {
83
64
  console.error(c.red(`label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool`));
package/src/cli/render.ts CHANGED
@@ -1,9 +1,5 @@
1
- // Terminal rendering helpers for the CLI (bars, colors, relative times).
2
-
3
1
  import { clamp } from "es-toolkit";
4
2
 
5
- /** ANSI painters, no-ops when disabled. The statusLine renders through a pipe
6
- * (never a TTY), so it needs its own enable gate; the CLI gates on stdout. */
7
3
  export function makeColors(enabled: boolean) {
8
4
  const paint = (code: string) => (s: string) => (enabled ? `\x1b[${code}m${s}\x1b[0m` : s);
9
5
  return {
@@ -18,9 +14,6 @@ export function makeColors(enabled: boolean) {
18
14
 
19
15
  export const c = makeColors(!process.env.NO_COLOR && !!process.stdout.isTTY);
20
16
 
21
- /** Used% -> RGB on a continuous green->yellow->red ramp, anchored at the
22
- * semantic severity bands (pure green at 0, yellow at 75, red from 95) so the
23
- * band boundaries read exactly like the old 3-color scheme. Blue stays 0. */
24
17
  function rampRgb(usedPct: number): { r: number; g: number } {
25
18
  const u = clamp(usedPct, 0, 100);
26
19
  if (u >= 95) return { r: 255, g: 0 };
@@ -28,11 +21,6 @@ function rampRgb(usedPct: number): { r: number; g: number } {
28
21
  return { r: Math.round((255 * u) / 75), g: 255 };
29
22
  }
30
23
 
31
- /** Usage-severity painter for the statusLine (user decision 2026-07-18: colors
32
- * carry quota status instead of numbers). Truecolor when the terminal
33
- * advertises it, else the nearest 256-color cube step; disabled -> identity,
34
- * and callers must then fall back to numeric rendering (without color a
35
- * drained window must not look fresh). */
36
24
  export function makeUsagePaint(input: { enabled: boolean; truecolor: boolean }) {
37
25
  return (usedPct: number) =>
38
26
  (s: string): string => {
@@ -45,12 +33,6 @@ export function makeUsagePaint(input: { enabled: boolean; truecolor: boolean })
45
33
  };
46
34
  }
47
35
 
48
- /** Plan label for a claude account, e.g. "max 20x": subscription name plus the
49
- * multiplier segment of the rate-limit tier id ("default_claude_max_20x").
50
- * Structural, never exact-string: the multiplier is any <digits>x segment, so
51
- * tiers without one (e.g. "default_claude_zero") fall back to the bare
52
- * subscription name, and an absent blob field falls back to null (unmeasured
53
- * must not look like a tier). */
54
36
  export function claudeTierLabel(input: { subscriptionType?: string; rateLimitTier?: string }): string | null {
55
37
  const segments = input.rateLimitTier?.split("_") ?? [];
56
38
  const multiplier = segments.find((seg) => seg.length > 1 && seg.endsWith("x") && Number.isInteger(Number(seg.slice(0, -1))));
@@ -58,12 +40,10 @@ export function claudeTierLabel(input: { subscriptionType?: string; rateLimitTie
58
40
  return multiplier ? `${input.subscriptionType} ${multiplier}` : input.subscriptionType;
59
41
  }
60
42
 
61
- /** "1 account" / "3 accounts": counted nouns always pluralize properly. */
62
43
  export function count(input: { n: number; noun: string }): string {
63
44
  return `${input.n} ${input.noun}${input.n === 1 ? "" : "s"}`;
64
45
  }
65
46
 
66
- /** A fixed-width usage bar, colored by fill. */
67
47
  export function bar(pct: number, width = 16): string {
68
48
  const clamped = clamp(pct, 0, 100);
69
49
  const filled = Math.round((clamped / 100) * width);
@@ -73,7 +53,6 @@ export function bar(pct: number, width = 16): string {
73
53
  return `${paint(body)} ${label}`;
74
54
  }
75
55
 
76
- /** Relative-time string for a reset epoch, e.g. "resets in 2h13m". */
77
56
  export function fmtReset(epochMs: number | null | undefined, now = Date.now()): string {
78
57
  if (epochMs == null) return "";
79
58
  const dsec = Math.round((epochMs - now) / 1000);
@@ -85,8 +64,6 @@ export function fmtReset(epochMs: number | null | undefined, now = Date.now()):
85
64
  return `resets in ${m}m`;
86
65
  }
87
66
 
88
- /** Age of a cached sample, largest unit only: "just now", "3m ago", "2h ago",
89
- * "5d ago". */
90
67
  export function fmtAgo(epochMs: number, now = Date.now()): string {
91
68
  const dsec = Math.max(0, Math.round((now - epochMs) / 1000));
92
69
  if (dsec < 60) return "just now";
package/src/cli/rm.ts CHANGED
@@ -1,5 +1,3 @@
1
- // `tokenmaxxing rm <selector>` - remove a pooled account (not the active one).
2
-
3
1
  import { rmSync } from "node:fs";
4
2
  import { join } from "node:path";
5
3
  import { deleteItem, isolatedTarget, liveTarget, parkedTarget, readItem } from "../lib/credstore.ts";
@@ -16,7 +14,6 @@ export async function cmdRm(selector?: string): Promise<number> {
16
14
  console.error("usage: tokenmaxxing rm <email|label|uuid>");
17
15
  return 2;
18
16
  }
19
- // under the flock: a concurrent swap's index write must not be clobbered.
20
17
  return withLock(paths.lockFile, async () => {
21
18
  const idx = loadAccounts();
22
19
  const a = findAccount(idx.accounts, selector);
@@ -28,13 +25,6 @@ export async function cmdRm(selector?: string): Promise<number> {
28
25
  console.error(c.red(`${a.email} is the ACTIVE account - switch away before removing it.`));
29
26
  return 1;
30
27
  }
31
- // The label drifts (manual /login); the token cannot lie. Removing the
32
- // account whose credential is actually LIVE would destroy its only backup
33
- // and leave the next swap refusing over an unpooled credential. Fail
34
- // CLOSED (review catch, PR #31): when the live owner cannot be verified -
35
- // unparsable blob, expired token, roles outage - refuse rather than trust
36
- // the stale label; rm is destructive and can wait. The check is read-only:
37
- // an expired bearer simply fails the roles call, nothing is ever rotated.
38
28
  const live = await readItem(liveTarget());
39
29
  if (live != null) {
40
30
  let liveOrg: string;
@@ -51,15 +41,6 @@ export async function cmdRm(selector?: string): Promise<number> {
51
41
  }
52
42
  }
53
43
  await deleteItem(parkedTarget(a.keychainItem));
54
- // Sweep sample-probe residue too: a probe killed mid-run strands the
55
- // account's isolated credential (macOS: a namespaced keychain item), and
56
- // once the account leaves the pool nothing would ever probe-and-heal it
57
- // again (closing-review critic gap). deleteItem on a missing item is a
58
- // no-op. Hard delete, not trash, on purpose: the sample dir is a
59
- // throwaway CLAUDE_CONFIG_DIR that can hold PLAINTEXT credential material
60
- // on Linux - the credential-dir cleanup exception (owner 2026-07-16, same
61
- // rule sample.ts and onboard.ts follow); trashing would move credentials
62
- // into the Trash folder.
63
44
  const sampleDir = join(paths.sampleDir, credItemFor(a.accountUuid));
64
45
  await deleteItem(isolatedTarget(sampleDir));
65
46
  rmSync(sampleDir, { recursive: true, force: true });
package/src/cli/status.ts CHANGED
@@ -1,17 +1,3 @@
1
- // `tokenmaxxing status`: accounts with 5h / weekly / per-model usage bars.
2
- // Parked accounts are live-sampled in isolation (`claude -p /usage`); the active
3
- // account is read off the free statusLine feed (usage.json) so we never poll its
4
- // own busy token. A sample that fails falls back to the last-known values with a
5
- // visible "(cached)" note - never a silent stale number. Fresh figures are
6
- // persisted onto each account for the picker/switch logic.
7
- //
8
- // `--force` additionally PINGS every account (one minimal haiku request each)
9
- // before sampling, so every account's 5h session window starts ticking NOW
10
- // instead of lying dormant until first real use, and every bar is a live probe
11
- // taken after the ping. The active account still prefers the tee only as a
12
- // fallback: its own `/usage` fail-silents exactly when a live session is
13
- // running it, and that session's tee is fresher than any cache.
14
-
15
1
  import { sortBy } from "es-toolkit";
16
2
  import { loadAccounts, loadConfig, loadUsage, loadModelUsage, saveAccounts } from "../lib/state.ts";
17
3
  import { readOAuthAccount } from "../lib/claudejson.ts";
@@ -27,17 +13,11 @@ import { bar, c, claudeTierLabel, count, fmtAgo, fmtReset } from "./render.ts";
27
13
  import type { FullUsage } from "../lib/usage.ts";
28
14
  import type { Account, Config, CodexWindow, UsageWindow } from "../lib/types.ts";
29
15
 
30
- /** `preRender` runs after sampling, right before the first output line: `watch`
31
- * keeps the previous frame on screen through the multi-second sample and
32
- * clears only when the fresh frame is ready to paint (watch(1) semantics). */
33
16
  export async function cmdStatus(force = false, preRender?: () => void): Promise<number> {
34
17
  let idx = loadAccounts();
35
18
  const cfg = loadConfig();
36
19
  const now = Date.now();
37
20
 
38
- // A window whose cached reset has passed is empty again; weekly windows recur
39
- // on a fixed per-account anchor, so a stale weekly reset extrapolates forward.
40
- // Fresh samples pass through unchanged (their resets are in the future).
41
21
  const row = (name: string, w: UsageWindow, weekly: boolean) => {
42
22
  const passed = w.resetsAt != null && w.resetsAt <= now;
43
23
  const pct = passed ? 0 : w.usedPercentage;
@@ -46,10 +26,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
46
26
  };
47
27
 
48
28
  if (idx.accounts.length === 0) {
49
- // A codex-only pool is a documented standalone flow: the empty-claude
50
- // early return must still render it, and only a fully empty install gets
51
- // the init hint (closing-review catch: bare `xx` claimed "no accounts"
52
- // over a populated codex pool and pointed at the wrong init).
53
29
  if (loadCodexAccounts().accounts.length > 0) {
54
30
  console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
55
31
  console.log();
@@ -60,9 +36,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
60
36
  return 0;
61
37
  }
62
38
 
63
- // Load, sample, and save entirely under the flock: parked refreshes must not
64
- // collide with an in-flight swap, and a save of an index loaded before a
65
- // concurrent swap would clobber the swap's activeAccountUuid.
66
39
  console.error(c.dim(force ? "pinging every account (starts each 5h session timer) + sampling live usage..." : "sampling live usage..."));
67
40
  const outcomes = new Map<string, SampleOutcome>();
68
41
  await withLock(paths.lockFile, async () => {
@@ -72,19 +45,8 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
72
45
  const liveOAuth = readOAuthAccount();
73
46
  const activeOrg = liveOAuth?.organizationUuid ?? null;
74
47
  const probeOne = async (a: Account) => {
75
- // Active = the org claude's own oauthAccount names, NOT the stored
76
- // label conjunction: after a manual /login the label lags, and routing
77
- // the genuinely-live account through the parked prober would refresh
78
- // its stale parked grant and could flag a healthy account needs-reauth.
79
- // probeActiveUsage's own roles check still fail-fasts on residual drift.
80
48
  const isActive = activeOrg != null && activeOrg === a.organizationUuid;
81
- // The tee path never opens the credential blob, so the active account's
82
- // tier comes from the live oauthAccount instead - it names this very org
83
- // (uuid-matched above), so the tier is attributed to its own identity.
84
49
  if (isActive && liveOAuth?.organizationRateLimitTier != null) a.rateLimitTier = liveOAuth.organizationRateLimitTier;
85
- // Active account: prefer the free statusLine push (usage.json) so we never
86
- // poll its own token, which is busy exactly when it matters. per-model
87
- // comes from model-usage.json (also statusLine-driven).
88
50
  const fromStatusLine: FullUsage | null =
89
51
  isActive && live && live.org === a.organizationUuid
90
52
  ? {
@@ -96,13 +58,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
96
58
  let viaTee = false;
97
59
  let outcome: SampleOutcome;
98
60
  if (force) {
99
- // Force: ping + live probe for everyone. The tee predates the ping,
100
- // so it serves ONLY as the fallback for the fail-silent `/usage`
101
- // case (the probe got past every guard and claude printed no limit
102
- // lines). Any other failure - identity mismatch, dead refresh, a
103
- // refused ping - must stay VISIBLE: the old unconditional fallback
104
- // masked pre-ping failures as healthy tee data, silently skipping
105
- // the ping with no warning (closing-review catch).
106
61
  outcome = isActive ? await probeActiveUsage(a, { ping: true }) : await probeParkedUsage(a, { ping: true });
107
62
  if (!outcome.ok && fromStatusLine && outcome.reason.includes("no limit data")) {
108
63
  const failed = outcome;
@@ -121,35 +76,17 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
121
76
  outcomes.set(a.accountUuid, outcome);
122
77
  if (!outcome.ok) return;
123
78
  a.lastUsage = { fiveHour: outcome.usage.session, sevenDay: outcome.usage.weekAll };
124
- // stamp when the figures were actually measured: the statusLine tee's
125
- // own write time for the push-fed active account, else the probe time.
126
79
  a.lastUsageAt = viaTee && live ? live.ts : Date.now();
127
80
  if (Object.keys(outcome.usage.perModel).length > 0) {
128
81
  a.lastPerModel = outcome.usage.perModel;
129
- // via the tee, the per-model rows come from model-usage.json whose
130
- // own sample time may be DAYS older than the aggregate tee - dating
131
- // them by the tee's timestamp inflated the null-reset self-bound
132
- // (closing-review catch). A fresh probe's rows date by the probe.
133
82
  a.lastPerModelAt = viaTee && modelUsage ? (modelUsage.sampledAt ?? modelUsage.ts) : a.lastUsageAt;
134
83
  }
135
84
  };
136
- // The ACTIVE probe runs FIRST, alone: it refreshes an expired live access
137
- // token under claude's refresh lock, and every parked probe's fail-closed
138
- // live-owner check reads that same live token - run concurrently they
139
- // raced the refresh and, after the machine idled past the token lifetime,
140
- // every parked sample 401'd on the first `xx status` (closing-review
141
- // catch). Parked probes then run concurrently as before.
142
85
  const activeAccount = idx.accounts.find((a) => activeOrg != null && activeOrg === a.organizationUuid) ?? null;
143
86
  if (activeAccount) await probeOne(activeAccount);
144
- // even when the active account's usage came from the TEE (no active
145
- // probe, no refresh side effect), the parked probes still need a fresh
146
- // live token for their fail-closed live-owner checks (cubic review
147
- // catch, PR #35). A dead live grant is deliberately not fatal here: each
148
- // parked probe reports its own honest failure reason.
149
87
  try {
150
88
  await ensureLiveTokenFresh();
151
89
  } catch {
152
- // per-account guards surface the failure per probe
153
90
  }
154
91
  await Promise.all(idx.accounts.filter((a) => a !== activeAccount).map(probeOne));
155
92
  saveAccounts(idx);
@@ -159,18 +96,12 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
159
96
  console.log(c.dim(`thresholds 5h ${cfg.thresholds.session}% weekly ${cfg.thresholds.weekly}% (${count({ n: idx.accounts.length, noun: "claude account" })})`));
160
97
  console.log();
161
98
 
162
- // Display order (user decision 2026-07-18): earliest upcoming reset first
163
- // (5h or extrapolated weekly, from the just-refreshed samples), needs-reauth
164
- // last; the ● marker identifies the active account wherever it sorts.
165
99
  const displayAccounts = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
166
- // The marker matches the probe routing above: active = the org claude's own
167
- // oauthAccount names (the swap label can lag after a manual /login).
168
100
  const displayActiveOrg = readOAuthAccount()?.organizationUuid ?? null;
169
101
  for (const a of displayAccounts) {
170
102
  const active = displayActiveOrg != null && a.organizationUuid === displayActiveOrg;
171
103
  const outcome = outcomes.get(a.accountUuid);
172
104
  const failed = outcome ? !outcome.ok : false;
173
- // On a failed sample, fall back to the last-known values (with a note below).
174
105
  const usage = outcome?.ok ? outcome.usage : undefined;
175
106
  const aggregate = usage ? { fiveHour: usage.session, sevenDay: usage.weekAll } : a.lastUsage;
176
107
  const perModel = usage ? usage.perModel : a.lastPerModel;
@@ -184,8 +115,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
184
115
 
185
116
  const tier = claudeTierLabel(a);
186
117
  console.log(`${marker} ${c.bold(a.label || a.email)}${tier ? ` ${c.dim(tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`);
187
- // Chart label convention (user rule 2026-07-17): everything lowercase,
188
- // model names short ("fable", "spark").
189
118
  if (aggregate) {
190
119
  row("5h", aggregate.fiveHour, false);
191
120
  row("week", aggregate.sevenDay, true);
@@ -198,10 +127,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
198
127
  if (outcome?.pingError != null) {
199
128
  console.log(` ${c.yellow("ping failed (5h timer may not have started)")}: ${c.dim(outcome.pingError)}`);
200
129
  }
201
- // A successful ping ALWAYS opens the 5h window (live-verified 2026-07-16),
202
- // but the server's usage feed reflects it with a lag of up to a few
203
- // minutes, so a probe taken seconds later can still show a dormant window.
204
- // Say so rather than looking like the ping did nothing.
205
130
  if (force && outcome?.ok && outcome.pingError == null && aggregate && aggregate.fiveHour.resetsAt == null) {
206
131
  console.log(` ${c.dim("pinged - 5h timer started this run; the usage feed lags, re-run status shortly for the fresh window")}`);
207
132
  }
@@ -212,9 +137,6 @@ export async function cmdStatus(force = false, preRender?: () => void): Promise<
212
137
  return 0;
213
138
  }
214
139
 
215
- /** The codex pool, appended when it is non-empty. Every account samples via
216
- * the free usage GET (nothing metered, no window started): the live account
217
- * through the live auth.json, parked accounts through their parked blobs. */
218
140
  async function renderCodexSection(input: {
219
141
  cfg: Config;
220
142
  now: number;
@@ -251,8 +173,6 @@ async function renderCodexSection(input: {
251
173
  console.log();
252
174
  const windowLabel = (window: CodexWindow) =>
253
175
  isSessionWindow({ window }) ? `${Math.round((window.windowSeconds ?? 0) / 3600)}h` : "week";
254
- // Same display order as the claude pool: earliest upcoming reset first
255
- // across every cached window, needs-reauth last.
256
176
  const displayAccounts = sortBy(index.accounts, [
257
177
  (a) => (a.needsReauth ? 1 : 0),
258
178
  (a) => {