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 +1 -1
- package/README.md +3 -5
- package/package.json +1 -1
- package/src/cli/status.ts +69 -40
- package/src/main.ts +12 -11
- package/src/cli/ls.ts +0 -75
- package/src/cli/watch.ts +0 -46
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
|
|
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`, `
|
|
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
|
|
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.
|
|
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)
|
|
144
|
-
const usage = sampled
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (outcome.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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)
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
}
|