tokenmaxxing 1.9.1 → 1.11.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.
@@ -151,7 +151,7 @@ export function createTokenmaxxingMcpServer(): McpServer {
151
151
  inputSchema: {},
152
152
  },
153
153
  async () => {
154
- const cap = await captureCli(() => cmdStatus(false));
154
+ const cap = await captureCli(() => cmdStatus());
155
155
  return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
156
156
  },
157
157
  );
@@ -13,7 +13,7 @@ import { fetchCodexUsage } from "./codexusage.ts";
13
13
  import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
14
14
  import { liveCodexAccountId } from "./codexsample.ts";
15
15
  import { livingCodexPresences, presentCodexAccountIds, targetableCodexAccounts } from "./codexpresence.ts";
16
- import { effectiveBars } from "./picker.ts";
16
+ import { terminalBars } from "./picker.ts";
17
17
  import { log } from "./log.ts";
18
18
  import { CodexAccountSchema, CodexReconcileMarkerSchema, type CodexAccount } from "./types.ts";
19
19
 
@@ -100,7 +100,7 @@ function postSwapResweep(input: { liveAccountId: string; bars: { session: number
100
100
  export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promise<CodexSwapDecision> {
101
101
  const now = input.now ?? Date.now();
102
102
  const cfg = loadConfig();
103
- const bars = effectiveBars(cfg);
103
+ const bars = terminalBars(cfg);
104
104
 
105
105
  return withLock(codexPaths.lockFile, async () => {
106
106
  let index = loadCodexAccounts();
package/src/lib/decide.ts CHANGED
@@ -5,11 +5,11 @@ import { paths } from "./paths.ts";
5
5
  import { MAX_CHECK_DELAY_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsage, loadModelUsage, saveAccounts, saveDepletedWait, saveModelUsage, usageTeeAt, writeUsage } from "./state.ts";
6
6
  import { readOAuthAccount } from "./claudejson.ts";
7
7
  import { chooseAndSwap, performSwap } from "./swap.ts";
8
- import { currentWins, effectiveBars, hardBars, isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, usableAt } from "./picker.ts";
8
+ import { currentWins, effectiveBars, hardBars, isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, sessionLadder, usableAt } from "./picker.ts";
9
9
  import { InvalidGrantError } from "./oauth.ts";
10
10
  import { familyTokens, gatedFamilies, probeUsage, type EnforcedClass } from "./usage.ts";
11
11
  import { log } from "./log.ts";
12
- import { AccountSchema, ModelUsageStateSchema, UsageStateSchema, type Account, type Config, type EnforcedLimit, type ModelUsageState, type UsageState, type UsageWindow } from "./types.ts";
12
+ import { AccountSchema, ModelUsageStateSchema, UsageStateSchema, type Account, type Config, type EnforcedLimit, type ModelUsageState, type Thresholds, type UsageState, type UsageWindow } from "./types.ts";
13
13
 
14
14
  const SwapDecisionSchema = z.object({
15
15
  swapped: z.boolean(),
@@ -36,9 +36,17 @@ function capForFamily(mu: ModelUsageState, family: string, now: number): UsageWi
36
36
  return maxBy(rows, (w) => liveUsed({ window: w, windowMs: WEEK_MS, sampledAt: mu.sampledAt ?? mu.ts, now }));
37
37
  }
38
38
 
39
- function isOver(u: UsageState | null, mu: ModelUsageState | null, org: string | null, cfg: Config, now: number): boolean {
39
+ function overlayLive(accounts: Account[], u: UsageState | null, mu: ModelUsageState | null, org: string | null): Account[] {
40
+ return accounts.map((a) => {
41
+ if (org == null || a.organizationUuid !== org) return a;
42
+ const live = u && u.org === org ? { lastUsage: { fiveHour: u.fiveHour, sevenDay: u.sevenDay }, lastUsageAt: u.ts } : {};
43
+ const perModel = mu && mu.org === org && Object.keys(mu.perModel).length > 0 ? { lastPerModel: mu.perModel, lastPerModelAt: mu.sampledAt ?? mu.ts } : {};
44
+ return { ...a, ...live, ...perModel };
45
+ });
46
+ }
47
+
48
+ function isOver(u: UsageState | null, mu: ModelUsageState | null, org: string | null, bars: Thresholds, cfg: Config, now: number): boolean {
40
49
  if (!u || !org || u.org !== org) return false;
41
- const bars = effectiveBars(cfg);
42
50
  if (
43
51
  liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= bars.session ||
44
52
  liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }) >= bars.weekly
@@ -56,9 +64,9 @@ function needsPerModel(u: UsageState | null, cfg: Config): boolean {
56
64
  return u != null && gatedFamilies(u.model, cfg.policy.switchModels).length > 0;
57
65
  }
58
66
 
59
- function isEngaged(u: UsageState | null, mu: ModelUsageState | null, org: string | null, cfg: Config, now: number): boolean {
67
+ function isEngaged(u: UsageState | null, mu: ModelUsageState | null, org: string | null, bars: Thresholds, cfg: Config, now: number): boolean {
60
68
  if (!u || !org || u.org !== org) return false;
61
- return liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= cfg.policy.greedySessionFloor || isOver(u, mu, org, cfg, now);
69
+ return liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= cfg.policy.greedySessionFloor || isOver(u, mu, org, bars, cfg, now);
62
70
  }
63
71
 
64
72
  const SnapshotsSchema = z.object({
@@ -120,8 +128,13 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
120
128
  const cfg = loadConfig();
121
129
 
122
130
  const { u: usage, mu } = await loadFreshSnapshots(cfg, activeOrg, now);
131
+ const bars0 = effectiveBars(cfg, {
132
+ accounts: overlayLive(loadAccounts().accounts, usage, mu, activeOrg),
133
+ now,
134
+ switchFamilies: gatedFamilies(usage?.model ?? null, cfg.policy.switchModels),
135
+ });
123
136
 
124
- if (!enforced0 && !isEngaged(usage, mu, activeOrg, cfg, now)) {
137
+ if (!enforced0 && !isEngaged(usage, mu, activeOrg, bars0, cfg, now)) {
125
138
  const measured = usage != null && activeOrg != null && usage.org === activeOrg;
126
139
  if (!measured) {
127
140
  const replay = depletedReplay(now);
@@ -157,7 +170,12 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
157
170
  if (sampled) saveAccounts(idx);
158
171
  }
159
172
 
160
- if (!enforced2 && !isEngaged(u2, mu2, org2, cfg, now)) {
173
+ const gated = gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
174
+ const switchFamilies = enforced2?.family && !gated.includes(enforced2.family) ? [...gated, enforced2.family] : gated;
175
+ const barsOf = (accounts: Account[]): Thresholds => effectiveBars(cfg, { accounts, now, switchFamilies });
176
+ const bars = barsOf(idx.accounts);
177
+
178
+ if (!enforced2 && !isEngaged(u2, mu2, org2, bars, cfg, now)) {
161
179
  return depletedReplay(now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
162
180
  }
163
181
 
@@ -166,14 +184,11 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
166
184
  idx2.accounts.find((a) => a.accountUuid === idx2.activeAccountUuid) ??
167
185
  null;
168
186
 
169
- const gated = gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
170
- const switchFamilies = enforced2?.family && !gated.includes(enforced2.family) ? [...gated, enforced2.family] : gated;
171
-
172
- if (!enforced2 && !isOver(u2, mu2, org2, cfg, now)) {
173
- const ctxAll = { now, thresholds: effectiveBars(cfg), currentAccountUuid: null, switchFamilies };
187
+ const greedy = async (): Promise<SwapDecision> => {
174
188
  while (true) {
175
189
  const cur = loadAccounts();
176
190
  const active = seatOf(cur);
191
+ const ctxAll = { now, thresholds: barsOf(cur.accounts), currentAccountUuid: null, switchFamilies };
177
192
  if (currentWins(active, cur.accounts, ctxAll)) {
178
193
  return { swapped: false, account: null, reason: "current-best" };
179
194
  }
@@ -188,10 +203,24 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
188
203
  log("decide.greedy_swap", { account: best.accountUuid.slice(0, 8) });
189
204
  return { swapped: true, account: best, reason: "swapped" };
190
205
  }
191
- }
206
+ };
207
+ if (!enforced2 && !isOver(u2, mu2, org2, bars, cfg, now)) return greedy();
192
208
 
193
- const landed = await chooseAndSwap({ now, thresholds: effectiveBars(cfg), switchFamilies, currentAccountUuid: seatOf(loadAccounts())?.accountUuid ?? null });
194
- if (landed) return { swapped: true, account: landed, reason: "swapped" };
209
+ while (true) {
210
+ const cur = loadAccounts();
211
+ const seat = seatOf(cur);
212
+ const thresholds = barsOf(cur.accounts);
213
+ if (!enforced2 && seat && !seat.needsReauth && !isExhausted(seat, { now, thresholds, currentAccountUuid: null, switchFamilies })) return greedy();
214
+ const best = pickBest(cur.accounts, { now, thresholds, currentAccountUuid: seat?.accountUuid ?? null, switchFamilies });
215
+ if (!best) break;
216
+ try {
217
+ await performSwap(best);
218
+ } catch (e) {
219
+ if (e instanceof InvalidGrantError) continue;
220
+ throw e;
221
+ }
222
+ return { swapped: true, account: best, reason: "swapped" };
223
+ }
195
224
 
196
225
  const hardCtx = { now, thresholds: hardBars(cfg), currentAccountUuid: null, switchFamilies };
197
226
  const seat = seatOf(loadAccounts());
@@ -315,6 +344,7 @@ export async function recordEnforcedLimit(input: { limit: EnforcedClass; org: st
315
344
 
316
345
  export const CHECK_DELAY_FLOOR_MS = 60_000;
317
346
  const CHECK_DELAY_UNKNOWN_MS = 180_000;
347
+ const STAGE_CEILING_MS = [MAX_CHECK_DELAY_MS, 180_000, 120_000];
318
348
 
319
349
  export function checkDelayMs(input: { cfg: Config; org: string | null; now: number; decision: SwapDecision }): number {
320
350
  const { cfg, org, now, decision } = input;
@@ -323,21 +353,24 @@ export function checkDelayMs(input: { cfg: Config; org: string | null; now: numb
323
353
  if (swapAt != null && now - swapAt < POST_SWAP_COOLDOWN_MS) return swapAt + POST_SWAP_COOLDOWN_MS - now;
324
354
  const u = loadUsage();
325
355
  if (!org || !u || !usageFresh(u, org, cfg.policy.usagePollTtlMs, now)) return CHECK_DELAY_UNKNOWN_MS;
326
- const bars = effectiveBars(cfg);
356
+ const mu = loadModelUsage();
357
+ const muSame = mu && mu.org === org ? mu : null;
358
+ const families = gatedFamilies(u.model, cfg.policy.switchModels);
359
+ const bars = effectiveBars(cfg, { accounts: overlayLive(loadAccounts().accounts, u, muSame, org), now, switchFamilies: families });
327
360
  const heads = [
328
361
  bars.session - liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }),
329
362
  bars.weekly - liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }),
330
363
  ];
331
- const mu = loadModelUsage();
332
- const muSame = mu && mu.org === org ? mu : null;
333
364
  let capMissing = false;
334
365
  const capFresh = muSame != null && now - (muSame.sampledAt ?? muSame.ts) <= cfg.policy.usagePollTtlMs;
335
- for (const family of gatedFamilies(u.model, cfg.policy.switchModels)) {
366
+ for (const family of families) {
336
367
  const cap = muSame && capFresh ? capForFamily(muSame, family, now) : undefined;
337
368
  if (cap && muSame) heads.push(bars.weekly - liveUsed({ window: cap, windowMs: WEEK_MS, sampledAt: muSame.sampledAt ?? muSame.ts, now }));
338
369
  else capMissing = true;
339
370
  }
340
371
  const headroom = Math.min(...heads);
341
372
  const banded = headroom >= 40 ? MAX_CHECK_DELAY_MS : headroom >= 20 ? 180_000 : headroom >= 8 ? 120_000 : CHECK_DELAY_FLOOR_MS;
342
- return capMissing ? Math.min(banded, 120_000) : banded;
373
+ const stage = sessionLadder(cfg).indexOf(bars.session);
374
+ const staged = Math.min(banded, STAGE_CEILING_MS[stage] ?? CHECK_DELAY_FLOOR_MS);
375
+ return capMissing ? Math.min(staged, 120_000) : staged;
343
376
  }
package/src/lib/picker.ts CHANGED
@@ -3,13 +3,26 @@ import { z } from "zod";
3
3
  import { familyTokens } from "./usage.ts";
4
4
  import { AccountSchema, ThresholdsSchema, type Account, type Config, type Thresholds, type UsageWindow } from "./types.ts";
5
5
 
6
- export function effectiveBars(cfg: Config): Thresholds {
6
+ export function sessionLadder(cfg: Config): number[] {
7
+ return cfg.thresholds.session.map((rung) => rung - cfg.policy.projectionMargin);
8
+ }
9
+
10
+ export function terminalBars(cfg: Config): Thresholds {
7
11
  return {
8
- session: cfg.thresholds.session - cfg.policy.projectionMargin,
12
+ session: Math.max(...sessionLadder(cfg)),
9
13
  weekly: cfg.thresholds.weekly - cfg.policy.projectionMargin,
10
14
  };
11
15
  }
12
16
 
17
+ export function effectiveBars(cfg: Config, pool: { accounts: Account[]; now: number; switchFamilies: string[] }): Thresholds {
18
+ const top = terminalBars(cfg);
19
+ const holdsAt = (session: number) =>
20
+ pool.accounts.some(
21
+ (a) => !a.needsReauth && !isExhausted(a, { now: pool.now, thresholds: { session, weekly: top.weekly }, currentAccountUuid: null, switchFamilies: pool.switchFamilies }),
22
+ );
23
+ return { session: sessionLadder(cfg).find(holdsAt) ?? top.session, weekly: top.weekly };
24
+ }
25
+
13
26
  export function hardBars(cfg: Config): Thresholds {
14
27
  return {
15
28
  session: cfg.hardThresholds.session - cfg.policy.projectionMargin,
package/src/lib/state.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  LastSwapSchema,
10
10
  ModelUsageStateSchema,
11
11
  NextCheckSchema,
12
+ SessionLadderSchema,
12
13
  UsageStateSchema,
13
14
  type AccountsIndex,
14
15
  type Config,
@@ -17,7 +18,7 @@ import {
17
18
  } from "./types.ts";
18
19
 
19
20
  const DEFAULT_CONFIG: Config = {
20
- thresholds: { session: 95, weekly: 98 },
21
+ thresholds: { session: [50, 80, 95], weekly: 98 },
21
22
  hardThresholds: { session: 100, weekly: 100 },
22
23
  claudeBin: "",
23
24
  codexBin: "",
@@ -28,7 +29,7 @@ const PercentSchema = z.number().min(0).max(100);
28
29
 
29
30
  export const ConfigFileSchema = z
30
31
  .object({
31
- thresholds: z.object({ session: PercentSchema, weekly: PercentSchema }).partial(),
32
+ thresholds: z.object({ session: SessionLadderSchema, weekly: PercentSchema }).partial(),
32
33
  hardThresholds: z.object({ session: PercentSchema, weekly: PercentSchema }).partial(),
33
34
  claudeBin: z.string(),
34
35
  codexBin: z.string(),
package/src/lib/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { isEqual, uniq } from "es-toolkit";
1
2
  import { z } from "zod";
2
3
 
3
4
  const OAuthCredsSchema = z.looseObject({
@@ -99,9 +100,21 @@ export const ThresholdsSchema = z.object({
99
100
  });
100
101
  export type Thresholds = z.infer<typeof ThresholdsSchema>;
101
102
 
103
+ export const SessionLadderSchema = z
104
+ .array(z.number().min(0).max(100))
105
+ .min(1)
106
+ .refine((rungs) => isEqual(rungs, uniq(rungs).toSorted((a, b) => a - b)), {
107
+ message: "thresholds.session rungs must be strictly ascending",
108
+ });
109
+
110
+ export const ScreeningThresholdsSchema = z.object({
111
+ session: SessionLadderSchema,
112
+ weekly: z.number().min(0).max(100),
113
+ });
114
+
102
115
  export const ConfigSchema = z
103
116
  .object({
104
- thresholds: ThresholdsSchema,
117
+ thresholds: ScreeningThresholdsSchema,
105
118
  hardThresholds: ThresholdsSchema,
106
119
  claudeBin: z.string(),
107
120
  codexBin: z.string(),
@@ -113,11 +126,11 @@ export const ConfigSchema = z
113
126
  maxWaitMs: z.number().int().positive(),
114
127
  }),
115
128
  })
116
- .refine((cfg) => cfg.policy.projectionMargin < Math.min(cfg.thresholds.session, cfg.thresholds.weekly), {
117
- message: "policy.projectionMargin must be strictly below both thresholds (effectiveBars would hit zero and every account would read as exhausted)",
129
+ .refine((cfg) => cfg.policy.projectionMargin < Math.min(...cfg.thresholds.session, cfg.thresholds.weekly), {
130
+ message: "policy.projectionMargin must be strictly below every threshold (effectiveBars would hit zero and every account would read as exhausted)",
118
131
  })
119
- .refine((cfg) => cfg.hardThresholds.session >= cfg.thresholds.session && cfg.hardThresholds.weekly >= cfg.thresholds.weekly, {
120
- message: "hardThresholds (the Layer 2 wall) must be at or above thresholds (the Layer 1 screening bars) for both windows",
132
+ .refine((cfg) => cfg.hardThresholds.session >= Math.max(...cfg.thresholds.session) && cfg.hardThresholds.weekly >= cfg.thresholds.weekly, {
133
+ message: "hardThresholds (the Layer 2 wall) must be at or above thresholds (the Layer 1 screening bars, the top session rung) for both windows",
121
134
  });
122
135
  export type Config = z.infer<typeof ConfigSchema>;
123
136
 
package/src/main.ts CHANGED
@@ -26,7 +26,10 @@ import { cmdSwitch } from "./cli/switch.ts";
26
26
  import { cmdCheck } from "./cli/check.ts";
27
27
  import { cmdConfig } from "./cli/config.ts";
28
28
  import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
29
- import { c } from "./cli/render.ts";
29
+ import { c, emitError, emitJson } from "./cli/render.ts";
30
+
31
+ const JSON_FLAG = "--json";
32
+ const INTERACTIVE_COMMANDS = new Set(["init", "add", "auth"]);
30
33
 
31
34
  function printHelp(): void {
32
35
  console.log(`${c.bold("tokenmaxxing")} - automatic Claude Code account switching
@@ -50,30 +53,45 @@ function printHelp(): void {
50
53
  ${c.cyan("tokenmaxxing rm")} [--codex] <sel>
51
54
  ${c.cyan("tokenmaxxing uninstall")} remove supervisor + settings entries
52
55
 
56
+ ${c.cyan("--json")} print one JSON document on stdout instead of text (status, ls, config, doctor, check, switch, rename, rm, uninstall; one per tick for watch); every document carries ${c.bold("ok")}, failures add ${c.bold("error")}
57
+
53
58
  ${c.dim("(aliased as")} ${c.cyan("xx")}${c.dim(")")} - then just run ${c.bold("claude")} as always; it switches accounts near quota automatically.`);
54
59
  }
55
60
 
61
+ let jsonMode = false;
62
+
56
63
  async function main(): Promise<number> {
57
64
  if (process.platform !== "darwin" && process.platform !== "linux") {
58
65
  console.error(`tokenmaxxing supports macOS and Linux only (this is ${process.platform})`);
59
66
  return 1;
60
67
  }
61
- const args = process.argv.slice(2);
68
+ const argv = process.argv.slice(2);
62
69
  const argv0 = basename(process.argv0 || process.argv[0] || "");
63
- const sub = args[0];
64
70
 
65
- if (argv0 === "claude" || sub === "__supervise") {
66
- return runSupervisor(sub === "__supervise" ? args.slice(1) : args);
71
+ if (argv0 === "claude" || argv[0] === "__supervise") {
72
+ return runSupervisor(argv[0] === "__supervise" ? argv.slice(1) : argv);
67
73
  }
68
- if (argv0 === "codex" || sub === "__supervise-codex") {
69
- return runCodexSupervisor({ argv: sub === "__supervise-codex" ? args.slice(1) : args });
74
+ if (argv0 === "codex" || argv[0] === "__supervise-codex") {
75
+ return runCodexSupervisor({ argv: argv[0] === "__supervise-codex" ? argv.slice(1) : argv });
70
76
  }
71
77
 
78
+ jsonMode = argv.includes(JSON_FLAG);
79
+ const json = jsonMode;
80
+ const args = argv.filter((a) => a !== JSON_FLAG);
81
+ const sub = args[0];
82
+
83
+ if (json && sub != null && INTERACTIVE_COMMANDS.has(sub)) {
84
+ emitError({ json, message: `${sub} is interactive (it runs a login flow) and has no --json form` });
85
+ return 2;
86
+ }
72
87
  if (!(sub != null && sub.startsWith("__")) && !process.env.TOKENMAXXING_PROBE) {
73
88
  const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
74
89
  const ambient = nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
75
90
  if (ambient != null) {
76
- console.error(c.red(`CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): claude uses a namespaced credential store there that tokenmaxxing does not manage - unset it (or run from a clean shell) and retry.`));
91
+ emitError({
92
+ json,
93
+ message: `CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): claude uses a namespaced credential store there that tokenmaxxing does not manage - unset it (or run from a clean shell) and retry.`,
94
+ });
77
95
  return 1;
78
96
  }
79
97
  }
@@ -85,26 +103,26 @@ async function main(): Promise<number> {
85
103
  case "__stop-failure-hook": return runStopFailureHook();
86
104
  case "__session-start": return runSessionStart();
87
105
  case "__codex-stop-hook": return runCodexStopHook();
88
- case undefined: return cmdStatus();
89
- case "--force": return cmdStatus(true);
106
+ case undefined: return cmdStatus({ json });
107
+ case "--force": return cmdStatus({ force: true, json });
90
108
  case "switch": {
91
109
  const rest = args.slice(1).filter((a) => a !== "--codex");
92
- return args.includes("--codex") ? cmdCodexSwitch(rest[0]) : cmdSwitch(rest[0]);
110
+ return args.includes("--codex") ? cmdCodexSwitch(rest[0], json) : cmdSwitch(rest[0], json);
93
111
  }
94
- case "check": return cmdCheck(args.slice(1));
95
- case "config": return cmdConfig(args.slice(1));
112
+ case "check": return cmdCheck(args.slice(1), json);
113
+ case "config": return cmdConfig(args.slice(1), json);
96
114
  case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
97
115
  case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
98
116
  case "auth": return cmdAuth(args.slice(1));
99
- case "ls": return cmdLs();
100
- case "status": return cmdStatus(args.includes("--force"));
101
- case "watch": return cmdWatch(args[1]);
102
- case "doctor": return cmdDoctor();
117
+ case "ls": return cmdLs(json);
118
+ case "status": return cmdStatus({ force: args.includes("--force"), json });
119
+ case "watch": return cmdWatch(args[1], json);
120
+ case "doctor": return cmdDoctor(json);
103
121
  case "rm": {
104
122
  const rest = args.slice(1).filter((a) => a !== "--codex");
105
- return args.includes("--codex") ? cmdCodexRm(rest[0]) : cmdRm(rest[0]);
123
+ return args.includes("--codex") ? cmdCodexRm(rest[0], json) : cmdRm(rest[0], json);
106
124
  }
107
- case "rename": return cmdRename(args.slice(1));
125
+ case "rename": return cmdRename(args.slice(1), json);
108
126
  case "uninstall": {
109
127
  const out = uninstallSupervisor();
110
128
  const removed = [
@@ -113,6 +131,10 @@ async function main(): Promise<number> {
113
131
  ...(out.timerDeactivated ? ["check timer"] : []),
114
132
  ...(out.pathLineRemoved ? ["rc PATH line"] : []),
115
133
  ];
134
+ if (json) {
135
+ emitJson({ ok: true, removed, timerDeactivated: out.timerDeactivated, pathLineRemoved: out.pathLineRemoved });
136
+ return 0;
137
+ }
116
138
  console.log(`removed ${removed.join(", ")}`);
117
139
  if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
118
140
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
@@ -125,8 +147,8 @@ async function main(): Promise<number> {
125
147
  printHelp();
126
148
  return 0;
127
149
  default:
128
- console.error(c.red(`unknown command: ${sub}`));
129
- printHelp();
150
+ emitError({ json, message: `unknown command: ${sub}` });
151
+ if (!json) printHelp();
130
152
  return 2;
131
153
  }
132
154
  }
@@ -134,6 +156,6 @@ async function main(): Promise<number> {
134
156
  try {
135
157
  process.exit(await main());
136
158
  } catch (e) {
137
- console.error(c.red(e instanceof Error ? e.message : String(e)));
159
+ emitError({ json: jsonMode, message: e instanceof Error ? e.message : String(e) });
138
160
  process.exit(1);
139
161
  }