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.
- package/DESIGN.md +3 -3
- package/README.md +13 -11
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/pool-status/SKILL.md +2 -2
- package/agent-plugin/skills/pool-status/references/commands.md +1 -0
- package/agent-plugin/skills/safe-contribution/SKILL.md +3 -4
- package/agent-plugin/skills/sdk-pairing/SKILL.md +2 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +2 -2
- package/agent-plugin/skills/switching-policy/references/policy.md +2 -2
- package/package.json +2 -3
- package/src/cli/check.ts +17 -4
- package/src/cli/codexrm.ts +8 -7
- package/src/cli/codexswitch.ts +50 -20
- package/src/cli/config.ts +86 -42
- package/src/cli/doctor.ts +25 -9
- package/src/cli/ls.ts +37 -2
- package/src/cli/rename.ts +17 -14
- package/src/cli/render.ts +20 -0
- package/src/cli/rm.ts +12 -8
- package/src/cli/status.ts +243 -112
- package/src/cli/switch.ts +75 -31
- package/src/cli/watch.ts +13 -7
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/mcp.ts +1 -1
- package/src/lib/codexdecide.ts +2 -2
- package/src/lib/decide.ts +54 -21
- package/src/lib/picker.ts +15 -2
- package/src/lib/state.ts +3 -2
- package/src/lib/types.ts +18 -5
- package/src/main.ts +44 -22
package/src/entries/mcp.ts
CHANGED
|
@@ -151,7 +151,7 @@ export function createTokenmaxxingMcpServer(): McpServer {
|
|
|
151
151
|
inputSchema: {},
|
|
152
152
|
},
|
|
153
153
|
async () => {
|
|
154
|
-
const cap = await captureCli(() => cmdStatus(
|
|
154
|
+
const cap = await captureCli(() => cmdStatus());
|
|
155
155
|
return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
|
|
156
156
|
},
|
|
157
157
|
);
|
package/src/lib/codexdecide.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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" ||
|
|
66
|
-
return runSupervisor(
|
|
71
|
+
if (argv0 === "claude" || argv[0] === "__supervise") {
|
|
72
|
+
return runSupervisor(argv[0] === "__supervise" ? argv.slice(1) : argv);
|
|
67
73
|
}
|
|
68
|
-
if (argv0 === "codex" ||
|
|
69
|
-
return runCodexSupervisor({ argv:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
emitError({ json: jsonMode, message: e instanceof Error ? e.message : String(e) });
|
|
138
160
|
process.exit(1);
|
|
139
161
|
}
|