tokenmaxxing 1.10.0 → 1.12.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/src/cli/doctor.ts CHANGED
@@ -7,7 +7,7 @@ import { loadAccounts, loadConfig } from "../lib/state.ts";
7
7
  import { readItem, liveTarget, parkedTarget } from "../lib/credstore.ts";
8
8
  import { isAccessTokenExpiring, fetchTokenOrg } from "../lib/oauth.ts";
9
9
  import { CredentialBlobSchema, type RolesResponse } from "../lib/types.ts";
10
- import { c } from "./render.ts";
10
+ import { c, emitJson } from "./render.ts";
11
11
 
12
12
  async function blobOrg(raw: string): Promise<RolesResponse | null> {
13
13
  const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
@@ -15,11 +15,21 @@ async function blobOrg(raw: string): Promise<RolesResponse | null> {
15
15
  return fetchTokenOrg(creds.accessToken);
16
16
  }
17
17
 
18
- export async function cmdDoctor(): Promise<number> {
19
- let ok = true;
18
+ export async function cmdDoctor(json = false): Promise<number> {
19
+ const checks: { ok: boolean; label: string; hint: string | null }[] = [];
20
+ const notes: string[] = [];
21
+ const warnings: string[] = [];
20
22
  const check = (cond: boolean, label: string, hint?: string) => {
21
- console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
22
- if (!cond) ok = false;
23
+ checks.push({ ok: cond, label, hint: cond ? null : (hint ?? null) });
24
+ if (!json) console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
25
+ };
26
+ const note = (text: string) => {
27
+ notes.push(text);
28
+ if (!json) console.log(c.dim(` - ${text}`));
29
+ };
30
+ const warn = (text: string) => {
31
+ warnings.push(text);
32
+ if (!json) console.log(c.yellow(`⚠ ${text}`));
23
33
  };
24
34
 
25
35
  check(existsSync(paths.supervisorLink), "claude supervisor wrapper present", "run `tokenmaxxing init`");
@@ -46,7 +56,7 @@ export async function cmdDoctor(): Promise<number> {
46
56
  try {
47
57
  const org = await blobOrg(live);
48
58
  if (org) check(org.organization_uuid === active.organizationUuid, `live credential identity matches active (${active.email})`, `token belongs to ${org.organization_name} - run \`tokenmaxxing switch\``);
49
- else console.log(c.dim(` - live credential identity unverifiable (access token expired)`));
59
+ else note("live credential identity unverifiable (access token expired)");
50
60
  } catch (e) {
51
61
  check(false, `live credential identity matches active (${active.email})`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
52
62
  }
@@ -59,7 +69,7 @@ export async function cmdDoctor(): Promise<number> {
59
69
  try {
60
70
  const org = await blobOrg(parked);
61
71
  if (org) check(org.organization_uuid === a.organizationUuid, `parked credential identity matches ${a.email}`, `token belongs to ${org.organization_name} - run \`tokenmaxxing auth ${a.label}\``);
62
- else console.log(c.dim(` - ${a.email} identity unverifiable (access token expired)`));
72
+ else note(`${a.email} identity unverifiable (access token expired)`);
63
73
  } catch (e) {
64
74
  check(false, `parked credential identity matches ${a.email}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
65
75
  }
@@ -77,11 +87,17 @@ export async function cmdDoctor(): Promise<number> {
77
87
  const rc = shellRcPath();
78
88
  if (rc && existsSync(rc)) {
79
89
  for (const s of findClaudeShadowers(readFileSync(rc, "utf8"))) {
80
- if (s.kind === "shadow") console.log(c.yellow(`⚠ ${rc}: \`${s.line}\` shadows the supervised claude wrapper - launches through it skip tokenmaxxing`));
81
- else console.log(c.yellow(`⚠ ${rc}: alias \`${s.name}\` hardcodes a claude path and bypasses the supervisor - use plain \`claude\` in its body instead`));
90
+ if (s.kind === "shadow") warn(`${rc}: ${s.line.startsWith("alias ") ? "alias" : "function"} \`claude\` shadows the supervised claude wrapper - launches through it skip tokenmaxxing`);
91
+ else warn(`${rc}: alias \`${s.name}\` hardcodes a claude path and bypasses the supervisor - use plain \`claude\` in its body instead`);
82
92
  }
83
93
  }
84
94
 
95
+ const failed = checks.filter((entry) => !entry.ok).length;
96
+ const ok = failed === 0;
97
+ if (json) {
98
+ emitJson(ok ? { ok, checks, notes, warnings } : { ok, error: `issues found - ${failed} of ${checks.length} checks failed`, checks, notes, warnings });
99
+ return ok ? 0 : 1;
100
+ }
85
101
  console.log();
86
102
  console.log(ok ? c.green("all good ✓") : c.yellow("issues found - see above"));
87
103
  return ok ? 0 : 1;
package/src/cli/ls.ts CHANGED
@@ -1,10 +1,45 @@
1
1
  import { loadAccounts } from "../lib/state.ts";
2
2
  import { loadCodexAccounts } from "../lib/codexstate.ts";
3
3
  import { liveCodexAccountId } from "../lib/codexsample.ts";
4
- import { c, claudeTierLabel } from "./render.ts";
4
+ import { c, claudeTierLabel, emitJson } from "./render.ts";
5
5
 
6
- export function cmdLs(): number {
6
+ export function cmdLs(json = false): number {
7
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
+
8
43
  if (idx.accounts.length === 0) {
9
44
  console.log(c.dim("no accounts yet - run `tokenmaxxing init` then `tokenmaxxing add`"));
10
45
  }
package/src/cli/rename.ts CHANGED
@@ -2,7 +2,7 @@ import { withLock } from "../lib/lock.ts";
2
2
  import { codexPaths, paths } from "../lib/paths.ts";
3
3
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
4
4
  import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
5
- import { c } from "./render.ts";
5
+ import { c, emitError, emitJson, plain } from "./render.ts";
6
6
  import type { Account, CodexAccount } from "../lib/types.ts";
7
7
 
8
8
  export function findAccount(accounts: Account[], selector: string): Account | undefined {
@@ -23,51 +23,54 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
23
23
  );
24
24
  }
25
25
 
26
- async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
26
+ async function renameCodexAccount(input: { selector: string; newLabel: string; json: boolean }): Promise<number> {
27
+ const { selector, newLabel, json } = input;
27
28
  return withLock(codexPaths.lockFile, async () => {
28
29
  const index = loadCodexAccounts();
29
- const account = findCodexAccount(index.accounts, input.selector);
30
+ const account = findCodexAccount(index.accounts, selector);
30
31
  if (!account) {
31
- console.error(c.red(`no codex account matches "${input.selector}"`));
32
+ emitError({ json, message: `no codex account matches "${selector}"` });
32
33
  return 1;
33
34
  }
34
- const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === input.newLabel.toLowerCase());
35
+ const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
35
36
  if (taken) {
36
- console.error(c.red(`label "${input.newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool`));
37
+ emitError({ json, message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
37
38
  return 1;
38
39
  }
39
40
  const old = account.label;
40
- account.label = input.newLabel;
41
+ account.label = newLabel;
41
42
  saveCodexAccounts({ index });
42
- console.log(`renamed codex account ${c.dim(old)} → ${c.bold(input.newLabel)}`);
43
+ if (json) emitJson({ ok: true, pool: "codex", from: old, to: newLabel });
44
+ else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
43
45
  return 0;
44
46
  });
45
47
  }
46
48
 
47
- export async function cmdRename(argv: string[]): Promise<number> {
49
+ export async function cmdRename(argv: string[], json = false): Promise<number> {
48
50
  const codex = argv.includes("--codex");
49
51
  const [selector, newLabel] = argv.filter((a) => a !== "--codex");
50
52
  if (!selector || !newLabel) {
51
- console.error("usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>");
53
+ emitError({ json, message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
52
54
  return 2;
53
55
  }
54
- if (codex) return renameCodexAccount({ selector, newLabel });
56
+ if (codex) return renameCodexAccount({ selector, newLabel, json });
55
57
  return withLock(paths.lockFile, async () => {
56
58
  const idx = loadAccounts();
57
59
  const a = findAccount(idx.accounts, selector);
58
60
  if (!a) {
59
- console.error(c.red(`no claude account matches "${selector}" (codex accounts rename via --codex)`));
61
+ emitError({ json, message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
60
62
  return 1;
61
63
  }
62
64
  const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
63
65
  if (taken) {
64
- console.error(c.red(`label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool`));
66
+ emitError({ json, message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
65
67
  return 1;
66
68
  }
67
69
  const old = a.label;
68
70
  a.label = newLabel;
69
71
  saveAccounts(idx);
70
- console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
72
+ if (json) emitJson({ ok: true, pool: "claude", from: old, to: newLabel });
73
+ else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
71
74
  return 0;
72
75
  });
73
76
  }
package/src/cli/render.ts CHANGED
@@ -75,3 +75,23 @@ export function fmtAgo(epochMs: number, now = Date.now()): string {
75
75
  return `${m}m ago`;
76
76
  }
77
77
 
78
+ export const plain = (s: string): string => s;
79
+
80
+ export function emitJson(value: unknown): void {
81
+ console.log(JSON.stringify(value, null, 2));
82
+ }
83
+
84
+ export function emitError(input: {
85
+ json: boolean;
86
+ message: string;
87
+ notes?: string[];
88
+ extra?: Record<string, unknown>;
89
+ paint?: (s: string) => string;
90
+ }): void {
91
+ if (input.json) {
92
+ emitJson({ ok: false, error: input.message, ...input.extra });
93
+ return;
94
+ }
95
+ console.error((input.paint ?? c.red)(input.message));
96
+ for (const note of input.notes ?? []) console.error(c.dim(note));
97
+ }
package/src/cli/rm.ts CHANGED
@@ -7,22 +7,22 @@ import { credItemFor, paths } from "../lib/paths.ts";
7
7
  import { loadAccounts, saveAccounts } from "../lib/state.ts";
8
8
  import { CredentialBlobSchema } from "../lib/types.ts";
9
9
  import { findAccount } from "./rename.ts";
10
- import { c } from "./render.ts";
10
+ import { c, emitError, emitJson, plain } from "./render.ts";
11
11
 
12
- export async function cmdRm(selector?: string): Promise<number> {
12
+ export async function cmdRm(selector?: string, json = false): Promise<number> {
13
13
  if (!selector) {
14
- console.error("usage: tokenmaxxing rm <email|label|uuid>");
14
+ emitError({ json, message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
15
15
  return 2;
16
16
  }
17
17
  return withLock(paths.lockFile, async () => {
18
18
  const idx = loadAccounts();
19
19
  const a = findAccount(idx.accounts, selector);
20
20
  if (!a) {
21
- console.error(c.red(`no account matches "${selector}"`));
21
+ emitError({ json, message: `no account matches "${selector}"` });
22
22
  return 1;
23
23
  }
24
24
  if (a.accountUuid === idx.activeAccountUuid) {
25
- console.error(c.red(`${a.email} is the ACTIVE account - switch away before removing it.`));
25
+ emitError({ json, message: `${a.email} is the ACTIVE account - switch away before removing it.` });
26
26
  return 1;
27
27
  }
28
28
  const live = await readItem(liveTarget());
@@ -32,11 +32,14 @@ export async function cmdRm(selector?: string): Promise<number> {
32
32
  const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
33
33
  liveOrg = (await fetchTokenOrg(liveCreds.accessToken)).organization_uuid;
34
34
  } catch (e) {
35
- console.error(c.red(`cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`));
35
+ emitError({
36
+ json,
37
+ message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`,
38
+ });
36
39
  return 1;
37
40
  }
38
41
  if (liveOrg === a.organizationUuid) {
39
- console.error(c.red(`${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.`));
42
+ emitError({ json, message: `${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.` });
40
43
  return 1;
41
44
  }
42
45
  }
@@ -46,7 +49,8 @@ export async function cmdRm(selector?: string): Promise<number> {
46
49
  rmSync(sampleDir, { recursive: true, force: true });
47
50
  idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
48
51
  saveAccounts(idx);
49
- console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
52
+ if (json) emitJson({ ok: true, pool: "claude", removed: a.label, remaining: idx.accounts.length });
53
+ else console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
50
54
  return 0;
51
55
  });
52
56
  }