tokenmaxxing 1.29.0 → 1.30.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 CHANGED
@@ -105,7 +105,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
105
105
  ---
106
106
 
107
107
  ## 7. Scope
108
- **v1:** `tokenmaxxing init` / `add` / `ls` / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks; threshold swap with `flock` + reset-aware picker; platform credential store (macOS keychain / Linux 0600 files, one facade); auto-respawn across concurrent sessions. macOS + Linux.
108
+ **v1:** `tokenmaxxing init` / `add` / `ls` (removed in 1.30.0, `status --cached` covers it) / `status` / `doctor`; the supervisor; statusLine shim + Stop/SessionStart hooks; threshold swap with `flock` + reset-aware picker; platform credential store (macOS keychain / Linux 0600 files, one facade); auto-respawn across concurrent sessions. macOS + Linux.
109
109
 
110
110
  **v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
111
111
 
package/README.md CHANGED
@@ -68,9 +68,7 @@ claude # use claude as always
68
68
  | `tokenmaxxing auth [sel \| --all]` | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; `--all` walks every needs-reauth account one by one |
69
69
  | `tokenmaxxing switch [sel]` | switch the claude pool: bare picks the best account (no-op when the current one wins), a selector targets one |
70
70
  | `tokenmaxxing switch --codex [sel]` | switch the codex pool (takes effect on the next codex start) |
71
- | `tokenmaxxing ls` | list pooled accounts |
72
- | `tokenmaxxing status` | accounts with 5h / weekly usage bars, active + exhausted-until-reset |
73
- | `tokenmaxxing watch [seconds]` | live status: re-render every N seconds (default 120, floor 30) |
71
+ | `tokenmaxxing status [--cached]` | accounts with 5h / weekly usage bars, active + exhausted-until-reset; `--cached` renders the stored figures without sampling |
74
72
  | `tokenmaxxing config` | effective config with sources; `get`/`set`/`unset` dotted keys, `tidy` prunes unknown keys |
75
73
  | `tokenmaxxing doctor` | verify the supervisor + settings entries survived |
76
74
  | `tokenmaxxing rename [--codex] <sel> <label>` / `rm [--codex] <sel>` | manage the pool (`--codex` targets the codex pool: one email can hold both a claude and a codex account) |
@@ -78,7 +76,7 @@ claude # use claude as always
78
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 |
79
77
  | `tokenmaxxing cursor init [dir]` | write the Claude relay subagent and `.cursor/environment.json` into a repo |
80
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 |
81
- | `--json` | machine-readable output: one JSON document on stdout for `status`, `ls`, `config`, `doctor`, `check`, `switch`, `rename`, `rm`, `uninstall`, `setup-token --print`, `cursor init`, `cloud run`, and one per tick for `watch` (`ok` mirrors the exit code, failures add `error`) |
79
+ | `--json` | machine-readable output: one JSON document on stdout for `status`, `config`, `doctor`, `check`, `switch`, `rename`, `rm`, `uninstall`, `setup-token --print`, `cursor init`, and `cloud run` (`ok` mirrors the exit code, failures add `error`) |
82
80
 
83
81
  ## How switching decides
84
82
 
@@ -126,7 +124,7 @@ tokenmaxxing add --codex # log in another account, isolated - your primary lo
126
124
  codex # use codex as always
127
125
  ```
128
126
 
129
- Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so **a restart is the switch**. The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status`/`watch`/`ls` show both pools.
127
+ Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so **a restart is the switch**. The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status` (and `status --cached`) shows both pools.
130
128
 
131
129
  Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after `init --codex` you must open codex once and trust the tokenmaxxing Stop hook via `/hooks` (auto-switching is inert until then); and codex has no cross-process lock on `auth.json`, so tokenmaxxing serializes all of its own credential writes behind its own lock and swaps only at idle turn boundaries.
132
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.29.0",
3
+ "version": "1.30.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/status.ts CHANGED
@@ -15,7 +15,7 @@ import { gatedFamilies, type FullUsage } from "../lib/usage.ts";
15
15
  import { ThresholdsSchema, UsageWindowSchema, type Account, type CodexWindow, type Config, type UsageWindow } from "../lib/types.ts";
16
16
 
17
17
  const SampleReportSchema = z.discriminatedUnion("ok", [
18
- z.object({ ok: z.literal(true), source: z.enum(["statusline", "probe"]) }),
18
+ z.object({ ok: z.literal(true), source: z.enum(["statusline", "probe", "cached"]) }),
19
19
  z.object({ ok: z.literal(false), reason: z.string() }),
20
20
  ]);
21
21
 
@@ -31,6 +31,7 @@ const ClaudeStatusAccountSchema = z.object({
31
31
  usage: z.object({ fiveHour: UsageWindowSchema, week: UsageWindowSchema }).nullable(),
32
32
  perModel: z.record(z.string(), UsageWindowSchema),
33
33
  usageAt: z.number().nullable(),
34
+ perModelAt: z.number().nullable(),
34
35
  sample: SampleReportSchema,
35
36
  });
36
37
  type ClaudeStatusAccount = z.infer<typeof ClaudeStatusAccountSchema>;
@@ -83,11 +84,11 @@ function currentCodexWindow(w: CodexWindow, now: number): CodexWindow {
83
84
  return { ...currentWindow(w, !isSessionWindow({ window: w }), now), windowSeconds: w.windowSeconds };
84
85
  }
85
86
 
86
- async function collectClaude(input: { cfg: Config; now: number }): Promise<StatusReport["claude"]> {
87
- const { cfg, now } = input;
87
+ async function collectClaude(input: { cfg: Config; now: number; cached: boolean }): Promise<StatusReport["claude"]> {
88
+ const { cfg, now, cached } = input;
88
89
  let idx = loadAccounts();
89
90
  const samples = new Map<string, { outcome: SampleOutcome; viaTee: boolean }>();
90
- if (idx.accounts.length > 0) {
91
+ if (idx.accounts.length > 0 && !cached) {
91
92
  await withLock(paths.lockFile, async () => {
92
93
  idx = loadAccounts();
93
94
  console.error(c.dim("sampling live usage..."));
@@ -137,11 +138,11 @@ async function collectClaude(input: { cfg: Config; now: number }): Promise<Statu
137
138
 
138
139
  const families = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
139
140
  const bars = thresholdBars(cfg);
140
- const liveAccount = readOAuthAccount()?.accountUuid ?? null;
141
+ const liveAccount = cached ? idx.activeAccountUuid : (readOAuthAccount()?.accountUuid ?? null);
141
142
  const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
142
143
  const accounts = ordered.map((a): ClaudeStatusAccount => {
143
- const sampled = samples.get(a.accountUuid) ?? { outcome: { ok: false, reason: "not sampled" }, viaTee: false };
144
- const usage = sampled.outcome.ok ? sampled.outcome.usage : undefined;
144
+ const sampled = samples.get(a.accountUuid);
145
+ const usage = sampled?.outcome.ok ? sampled.outcome.usage : undefined;
145
146
  const aggregate = usage ? { fiveHour: usage.session, sevenDay: usage.weekAll } : a.lastUsage;
146
147
  const perModel = usage ? usage.perModel : (a.lastPerModel ?? {});
147
148
  return {
@@ -156,7 +157,14 @@ async function collectClaude(input: { cfg: Config; now: number }): Promise<Statu
156
157
  usage: aggregate ? { fiveHour: currentWindow(aggregate.fiveHour, false, now), week: currentWindow(aggregate.sevenDay, true, now) } : null,
157
158
  perModel: Object.fromEntries(Object.entries(perModel).map(([name, w]) => [name, currentWindow(w, true, now)])),
158
159
  usageAt: a.lastUsageAt ?? null,
159
- sample: sampled.outcome.ok ? { ok: true, source: sampled.viaTee ? "statusline" : "probe" } : { ok: false, reason: sampled.outcome.reason },
160
+ perModelAt: a.lastPerModelAt ?? null,
161
+ sample: sampled
162
+ ? sampled.outcome.ok
163
+ ? { ok: true, source: sampled.viaTee ? "statusline" : "probe" }
164
+ : { ok: false, reason: sampled.outcome.reason }
165
+ : cached
166
+ ? { ok: true, source: "cached" }
167
+ : { ok: false, reason: "not sampled" },
160
168
  };
161
169
  });
162
170
  return {
@@ -167,34 +175,38 @@ async function collectClaude(input: { cfg: Config; now: number }): Promise<Statu
167
175
  };
168
176
  }
169
177
 
170
- async function collectCodex(input: { cfg: Config; now: number }): Promise<StatusReport["codex"]> {
171
- const { cfg, now } = input;
178
+ async function collectCodex(input: { cfg: Config; now: number; cached: boolean }): Promise<StatusReport["codex"]> {
179
+ const { cfg, now, cached } = input;
172
180
  const bars = thresholdBars(cfg);
173
181
  let index = loadCodexAccounts();
174
182
  if (index.accounts.length === 0) return { bars, accounts: [] };
175
183
 
176
- console.error(c.dim("sampling codex usage..."));
177
184
  const outcomes = new Map<string, CodexSampleOutcome>();
178
185
  let liveId: string | null = null;
179
- await withLock(codexPaths.lockFile, async () => {
180
- index = loadCodexAccounts();
181
- liveId = liveCodexAccountId();
182
- await Promise.all(
183
- index.accounts.map(async (account) => {
184
- const outcome = await sampleCodexAccount({ account, liveAccountId: liveId, now });
185
- outcomes.set(account.accountId, outcome);
186
- if (outcome.ok) {
187
- account.lastUsage = { aggregate: outcome.usage.aggregate, perLimit: outcome.usage.perLimit };
188
- account.lastUsageAt = Date.now();
189
- if (outcome.usage.email != null) account.email = outcome.usage.email;
190
- if (outcome.usage.planType != null) account.planType = outcome.usage.planType;
191
- } else if (outcome.deadGrant) {
192
- account.needsReauth = true;
193
- }
194
- }),
195
- );
196
- saveCodexAccounts({ index });
197
- });
186
+ if (cached) {
187
+ liveId = index.activeAccountId;
188
+ } else {
189
+ console.error(c.dim("sampling codex usage..."));
190
+ await withLock(codexPaths.lockFile, async () => {
191
+ index = loadCodexAccounts();
192
+ liveId = liveCodexAccountId();
193
+ await Promise.all(
194
+ index.accounts.map(async (account) => {
195
+ const outcome = await sampleCodexAccount({ account, liveAccountId: liveId, now });
196
+ outcomes.set(account.accountId, outcome);
197
+ if (outcome.ok) {
198
+ account.lastUsage = { aggregate: outcome.usage.aggregate, perLimit: outcome.usage.perLimit };
199
+ account.lastUsageAt = Date.now();
200
+ if (outcome.usage.email != null) account.email = outcome.usage.email;
201
+ if (outcome.usage.planType != null) account.planType = outcome.usage.planType;
202
+ } else if (outcome.deadGrant) {
203
+ account.needsReauth = true;
204
+ }
205
+ }),
206
+ );
207
+ saveCodexAccounts({ index });
208
+ });
209
+ }
198
210
 
199
211
  const ordered = sortBy(index.accounts, [
200
212
  (a) => (a.needsReauth ? 1 : 0),
@@ -205,7 +217,7 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
205
217
  },
206
218
  ]);
207
219
  const accounts = ordered.map((account): CodexStatusAccount => {
208
- const outcome = outcomes.get(account.accountId) ?? { ok: false, reason: "not sampled", deadGrant: false };
220
+ const outcome = outcomes.get(account.accountId);
209
221
  const usage = account.lastUsage;
210
222
  return {
211
223
  label: account.label,
@@ -224,7 +236,13 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
224
236
  }
225
237
  : null,
226
238
  usageAt: account.lastUsageAt ?? null,
227
- sample: outcome.ok ? { ok: true, source: "probe" } : { ok: false, reason: outcome.reason },
239
+ sample: outcome
240
+ ? outcome.ok
241
+ ? { ok: true, source: "probe" }
242
+ : { ok: false, reason: outcome.reason }
243
+ : cached
244
+ ? { ok: true, source: "cached" }
245
+ : { ok: false, reason: "not sampled" },
228
246
  };
229
247
  });
230
248
  return { bars, accounts };
@@ -298,6 +316,10 @@ function sampleFailedNotes(input: { cached: boolean; usageAt: number | null; rea
298
316
  ];
299
317
  }
300
318
 
319
+ function cachedNote(usageAt: number | null, now: number): Note {
320
+ return { paint: c.dim, text: usageAt != null ? `cached ${fmtAgo(usageAt, now)}` : "never sampled" };
321
+ }
322
+
301
323
  function headerLine(input: { active: boolean; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
302
324
  const marker = input.active ? c.green("●") : c.dim("○");
303
325
  const badges: string[] = [];
@@ -317,7 +339,11 @@ function codexCard(account: CodexStatusAccount, now: number): Card {
317
339
  for (const window of windows) lines.push(usageRow(codexLimitLabel({ limitName: name }), window));
318
340
  }
319
341
  }
320
- const notes = account.sample.ok ? [] : sampleFailedNotes({ cached: account.usage != null, usageAt: account.usageAt, reason: account.sample.reason, now });
342
+ const notes = account.sample.ok
343
+ ? account.sample.source === "cached"
344
+ ? [cachedNote(account.usageAt, now)]
345
+ : []
346
+ : sampleFailedNotes({ cached: account.usage != null, usageAt: account.usageAt, reason: account.sample.reason, now });
321
347
  return { lines, notes };
322
348
  }
323
349
 
@@ -342,6 +368,11 @@ function claudeCard(a: ClaudeStatusAccount, now: number, staleAfterMs: number):
342
368
  const age = a.usageAt != null ? fmtAgo(a.usageAt, now) : "age unknown";
343
369
  notes.push({ paint: stale ? c.yellow : c.dim, text: `statusline tee ${age}${stale ? " (stale)" : ""}` });
344
370
  }
371
+ if (a.sample.ok && a.sample.source === "cached") {
372
+ const note = cachedNote(a.usageAt, now);
373
+ const perModelAged = Object.keys(a.perModel).length > 0 && a.perModelAt !== a.usageAt;
374
+ notes.push(perModelAged ? { ...note, text: `${note.text}, per-model ${a.perModelAt != null ? fmtAgo(a.perModelAt, now) : "age unknown"}` } : note);
375
+ }
345
376
  if (!a.sample.ok) {
346
377
  notes.push(...sampleFailedNotes({ cached: a.usage != null || Object.keys(a.perModel).length > 0, usageAt: a.usageAt, reason: a.sample.reason, now }));
347
378
  }
@@ -365,17 +396,15 @@ function renderClaude(input: { claude: StatusReport["claude"]; codexPooled: bool
365
396
  renderGrid(claude.accounts.map((a) => claudeCard(a, now, staleAfterMs)));
366
397
  }
367
398
 
368
- export async function cmdStatus(opts: { json?: boolean; preRender?: () => void } = {}): Promise<number> {
369
- const { json = false } = opts;
399
+ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {}): Promise<number> {
400
+ const { json = false, cached = false } = opts;
370
401
  const cfg = loadConfig();
371
402
  const now = Date.now();
372
- const claude = await collectClaude({ cfg, now });
403
+ const claude = await collectClaude({ cfg, now, cached });
373
404
  if (!json) {
374
- opts.preRender?.();
375
- const at = Date.now();
376
- renderClaude({ claude, codexPooled: loadCodexAccounts().accounts.length > 0, now: at, staleAfterMs: cfg.policy.usagePollTtlMs });
405
+ renderClaude({ claude, codexPooled: loadCodexAccounts().accounts.length > 0, now: Date.now(), staleAfterMs: cfg.policy.usagePollTtlMs });
377
406
  }
378
- const codex = await collectCodex({ cfg, now });
407
+ const codex = await collectCodex({ cfg, now, cached });
379
408
  if (json) {
380
409
  const report: StatusReport = { now, claude, codex };
381
410
  emitJson({ ok: true, ...report });
package/src/main.ts CHANGED
@@ -15,9 +15,7 @@ import { cmdCodexInit } from "./cli/codexinit.ts";
15
15
  import { cmdCodexSwitch } from "./cli/codexswitch.ts";
16
16
  import { runCodexSupervisor } from "./entries/codexsupervisor.ts";
17
17
  import { runCodexStopHook } from "./entries/codexstophook.ts";
18
- import { cmdLs } from "./cli/ls.ts";
19
18
  import { cmdStatus } from "./cli/status.ts";
20
- import { cmdWatch } from "./cli/watch.ts";
21
19
  import { cmdDoctor } from "./cli/doctor.ts";
22
20
  import { cmdRm } from "./cli/rm.ts";
23
21
  import { cmdCodexRm } from "./cli/codexrm.ts";
@@ -32,6 +30,7 @@ import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
32
30
  import { c, emitError, emitJson } from "./cli/render.ts";
33
31
 
34
32
  const JSON_FLAG = "--json";
33
+ const CACHED_FLAG = "--cached";
35
34
  const INTERACTIVE_COMMANDS = new Set(["init", "add", "auth"]);
36
35
 
37
36
  function printHelp(): void {
@@ -46,9 +45,7 @@ function printHelp(): void {
46
45
  ${c.cyan("tokenmaxxing add --codex")} register an additional codex account (isolated login)
47
46
  ${c.cyan("tokenmaxxing auth")} [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every needs-reauth account, one by one)
48
47
  ${c.cyan("tokenmaxxing switch --codex")} [sel] switch the codex pool (takes effect on next codex start)
49
- ${c.cyan("tokenmaxxing ls")} list pooled accounts
50
- ${c.cyan("tokenmaxxing status")} accounts with 5h / weekly / per-model usage bars
51
- ${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120)
48
+ ${c.cyan("tokenmaxxing status")} [--cached] accounts with 5h / weekly / per-model usage bars (--cached: the stored figures, no sampling)
52
49
  ${c.cyan("tokenmaxxing config")} [get|set|unset|tidy] inspect and edit config.json (bare = effective config with sources)
53
50
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
54
51
  ${c.cyan("tokenmaxxing rename")} [--codex] <sel> <label>
@@ -58,7 +55,7 @@ function printHelp(): void {
58
55
  ${c.cyan("tokenmaxxing cursor init")} [dir] write the Claude relay subagent (.cursor/agents/claude.md) and .cursor/environment.json into a repo
59
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
60
57
 
61
- ${c.cyan("--json")} print one JSON document on stdout instead of text (status, ls, config, doctor, check, switch, rename, rm, uninstall, setup-token --print, cursor init, cloud run; one per tick for watch); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
58
+ ${c.cyan("--json")} print one JSON document on stdout instead of text (status, config, doctor, check, switch, rename, rm, uninstall, setup-token --print, cursor init, cloud run); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
62
59
 
63
60
  ${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
64
61
  }
@@ -82,9 +79,15 @@ async function main(): Promise<number> {
82
79
 
83
80
  jsonMode = argv.includes(JSON_FLAG);
84
81
  const json = jsonMode;
85
- const args = argv.filter((a) => a !== JSON_FLAG);
82
+ const cached = argv.includes(CACHED_FLAG);
83
+ const args = argv.filter((a) => a !== JSON_FLAG && a !== CACHED_FLAG);
86
84
  const sub = args[0];
87
85
 
86
+ if (cached && sub != null && sub !== "status") {
87
+ emitError({ json, message: `${CACHED_FLAG} applies to status only, not ${sub}` });
88
+ return 2;
89
+ }
90
+
88
91
  if (json && sub != null && INTERACTIVE_COMMANDS.has(sub)) {
89
92
  emitError({ json, message: `${sub} is interactive (it runs a login flow) and has no --json form` });
90
93
  return 2;
@@ -112,10 +115,10 @@ async function main(): Promise<number> {
112
115
  case "status": {
113
116
  const extra = args[1];
114
117
  if (extra != null) {
115
- emitError({ json, message: `unknown status option: ${extra} (status takes no options)` });
118
+ emitError({ json, message: `unknown status option: ${extra} (status takes only ${CACHED_FLAG})` });
116
119
  return 2;
117
120
  }
118
- return cmdStatus({ json });
121
+ return cmdStatus({ json, cached });
119
122
  }
120
123
  case "switch": {
121
124
  const rest = args.slice(1).filter((a) => a !== "--codex");
@@ -132,8 +135,6 @@ async function main(): Promise<number> {
132
135
  case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
133
136
  case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
134
137
  case "auth": return cmdAuth(args.slice(1));
135
- case "ls": return cmdLs(json);
136
- case "watch": return cmdWatch(args[1], json);
137
138
  case "doctor": return cmdDoctor(json);
138
139
  case "rm": {
139
140
  const rest = args.slice(1).filter((a) => a !== "--codex");
package/src/cli/ls.ts DELETED
@@ -1,75 +0,0 @@
1
- import { loadAccounts } from "../lib/state.ts";
2
- import { loadCodexAccounts } from "../lib/codexstate.ts";
3
- import { liveCodexAccountId } from "../lib/codexsample.ts";
4
- import { c, claudeTierLabel, emitJson } from "./render.ts";
5
-
6
- export function cmdLs(json = false): number {
7
- const idx = loadAccounts();
8
-
9
- if (json) {
10
- const codex = loadCodexAccounts();
11
- const liveId = codex.accounts.length > 0 ? liveCodexAccountId() : null;
12
- emitJson({
13
- ok: true,
14
- claude: {
15
- activeAccountUuid: idx.activeAccountUuid,
16
- accounts: idx.accounts.map((a) => ({
17
- label: a.label,
18
- email: a.email,
19
- accountUuid: a.accountUuid,
20
- organizationUuid: a.organizationUuid,
21
- tier: claudeTierLabel(a),
22
- active: a.accountUuid === idx.activeAccountUuid,
23
- needsReauth: a.needsReauth === true,
24
- addedAt: a.addedAt,
25
- })),
26
- },
27
- codex: {
28
- activeAccountId: liveId,
29
- accounts: codex.accounts.map((account) => ({
30
- label: account.label,
31
- email: account.email,
32
- accountId: account.accountId,
33
- planType: account.planType,
34
- active: account.accountId === liveId,
35
- needsReauth: account.needsReauth === true,
36
- addedAt: account.addedAt,
37
- })),
38
- },
39
- });
40
- return 0;
41
- }
42
-
43
- if (idx.accounts.length === 0) {
44
- console.log(c.dim("no accounts yet - run `tokenmaxxing init` then `tokenmaxxing add`"));
45
- }
46
- for (const a of idx.accounts) {
47
- const active = a.accountUuid === idx.activeAccountUuid;
48
- const marker = active ? c.green("●") : c.dim("○");
49
- const flags: string[] = [];
50
- if (active) flags.push(c.green("active"));
51
- if (a.needsReauth) flags.push(c.red("needs-reauth"));
52
- const tag = flags.length ? ` ${flags.join(" ")}` : "";
53
- const label = a.label || a.email;
54
- console.log(`${marker} ${c.bold(label)}${tag}`);
55
- console.log(` ${c.dim(`org ${a.organizationUuid.slice(0, 8)}, ${claudeTierLabel(a) ?? "?"}, uuid ${a.accountUuid.slice(0, 8)}`)}`);
56
- }
57
-
58
- const codex = loadCodexAccounts();
59
- if (codex.accounts.length > 0) {
60
- const liveId = liveCodexAccountId();
61
- console.log();
62
- console.log(c.dim("codex"));
63
- for (const account of codex.accounts) {
64
- const active = account.accountId === liveId;
65
- const marker = active ? c.green("●") : c.dim("○");
66
- const flags: string[] = [];
67
- if (active) flags.push(c.green("active"));
68
- if (account.needsReauth) flags.push(c.red("needs-reauth"));
69
- const tag = flags.length ? ` ${flags.join(" ")}` : "";
70
- console.log(`${marker} ${c.bold(account.label)}${tag}`);
71
- console.log(` ${c.dim(`${account.planType ?? "?"}, id ${account.accountId.slice(0, 8)}`)}`);
72
- }
73
- }
74
- return 0;
75
- }
package/src/cli/watch.ts DELETED
@@ -1,46 +0,0 @@
1
- import { clamp, delay } from "es-toolkit";
2
- import { loadAccounts } from "../lib/state.ts";
3
- import { loadCodexAccounts } from "../lib/codexstate.ts";
4
- import { cmdStatus } from "./status.ts";
5
- import { c, emitError, emitJson } from "./render.ts";
6
-
7
- const DEFAULT_INTERVAL_S = 120;
8
- const MIN_INTERVAL_S = 30;
9
- const MAX_INTERVAL_S = 86_400;
10
-
11
- export function resolveWatchInterval(arg?: string): number | null {
12
- if (arg === undefined) return DEFAULT_INTERVAL_S;
13
- const n = Number(arg);
14
- if (!Number.isFinite(n) || n <= 0) return null;
15
- return clamp(n, MIN_INTERVAL_S, MAX_INTERVAL_S);
16
- }
17
-
18
- const CLEAR = "\x1b[H\x1b[2J\x1b[3J";
19
-
20
- export async function cmdWatch(intervalArg?: string, json = false): Promise<number> {
21
- const intervalS = resolveWatchInterval(intervalArg);
22
- if (intervalS === null) {
23
- emitError({ json, message: `watch interval must be a positive number of seconds, got: ${intervalArg}` });
24
- return 2;
25
- }
26
- if (loadAccounts().accounts.length === 0 && loadCodexAccounts().accounts.length === 0) return cmdStatus({ json });
27
-
28
- const paintHeader = () => {
29
- process.stdout.write(process.stdout.isTTY ? CLEAR : "\n");
30
- console.log(c.dim(`watch: every ${intervalS}s, ${new Date().toLocaleTimeString()}, ctrl-c to quit`));
31
- };
32
- while (true) {
33
- try {
34
- await cmdStatus(json ? { json } : { preRender: paintHeader });
35
- } catch (e) {
36
- const message = `status failed this tick: ${e instanceof Error ? e.message : String(e)}`;
37
- if (json) {
38
- emitJson({ ok: false, error: message });
39
- } else {
40
- paintHeader();
41
- console.error(c.red(message));
42
- }
43
- }
44
- await delay(intervalS * 1000);
45
- }
46
- }