Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ codex-auth use
codex-auth list

# bare list shows the saved account/snapshot name first,
# then the remaining 5h and weekly quota percentages
# then the account type and remaining 5h/weekly quota percentages
# (the active row is marked with `*`)
# admin@compastor.com 5h=1% weekly=22%
# * recodee@portasmosonmagyarovar.hu 5h=99% weekly=44%
# admin@compastor.com type=ChatGPT seat (Business) 5h=1% weekly=22%
# * recodee@portasmosonmagyarovar.hu type=Usage based (Codex) 5h=99% weekly=44%

# list accounts with mapping metadata (email/account/user/usage)
codex-auth list --details
Expand Down Expand Up @@ -170,7 +170,7 @@ codex-auth remove-login-hook
- `codex-auth save <name> [--force]` – Validates `<name>`, ensures `auth.json` exists, then snapshots it to `~/.codex/accounts/<name>.json`. By default, it blocks overwriting a name when the existing snapshot email differs from current auth. If `name` is omitted, it first tries reusing the active snapshot name when identity matches; otherwise it infers one from auth email.
- `codex-auth login [name] [--device-auth] [--force]` – Runs `codex login` (optionally with device auth), waits for refreshed auth snapshot detection, then saves it. If `name` is omitted, it always infers one from auth email with unique-suffix handling for multi-workspace identities.
- `codex-auth use [name]` – Accepts a name or launches an interactive selector with the current account pre-selected, writes `~/.codex/auth.json` as a regular file from the chosen snapshot, and records the active name.
- `codex-auth list [--details]` – Lists all saved snapshots alphabetically. In the default view, each row starts with the saved account/snapshot name, followed by `5h=` and `weekly=` remaining values, and the active row is marked with `*`. `--details` adds per-snapshot mapping metadata (email, account id, user id, and usage metadata) for easier session/account troubleshooting.
- `codex-auth list [--details]` – Lists all saved snapshots alphabetically. In the default view, each row starts with the saved account/snapshot name, followed by `type=`, `5h=`, and `weekly=` values, and the active row is marked with `*`. `type=` renders Codex usage-based plans as `Usage based (Codex)` and ChatGPT seat plans with their tier, such as Plus, Business, Pro, or Max. `--details` adds per-snapshot mapping metadata (email, account id, user id, raw plan, usage metadata, and friendly type) for easier session/account troubleshooting.
- `codex-auth current` – Prints the active account name, or a friendly message if none is active.
- `codex-auth self-update [--check] [--reinstall] [-y]` – Checks npm for newer release metadata. `--check` prints current/latest/status only. `--reinstall` forces reinstall even when already up to date. `-y` skips confirmation prompts.
- `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Add codex-auth list account type labels

## Goal

Show the account type in `codex-auth list` rows so operators can distinguish Codex usage-based accounts from ChatGPT seat plans.

## Acceptance

- Bare `codex-auth list` includes a `type=` field before quota percentages.
- Usage-based Codex plans render as `Usage based (Codex)`.
- ChatGPT seat plans render with tier labels for Plus, Business, Pro, and Max.
- `codex-auth list --details` includes the same friendly `type=` label while keeping raw `plan=` metadata.
- Focused TypeScript tests cover the formatter.
7 changes: 4 additions & 3 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Flags } from "@oclif/core";
import prompts from "prompts";
import { BaseCommand } from "../lib/base-command";
import { formatAccountType } from "../lib/accounts/plan-display";
import {
fetchLatestNpmVersionCached,
formatGlobalInstallCommand,
Expand All @@ -17,7 +18,7 @@ export default class ListCommand extends BaseCommand {
static flags = {
details: Flags.boolean({
char: "d",
description: "Show per-account mapping metadata (email/account/user/usage)",
description: "Show per-account mapping metadata (email/account/user/type/usage)",
default: false,
}),
} as const;
Expand All @@ -38,7 +39,7 @@ export default class ListCommand extends BaseCommand {
for (const account of accounts) {
const mark = account.active ? "*" : " ";
this.log(
`${mark} ${account.name} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)}`,
`${mark} ${account.name} type=${formatAccountType(account.planType)} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)}`,
);
}
return;
Expand All @@ -57,7 +58,7 @@ export default class ListCommand extends BaseCommand {
` email=${account.email ?? "-"} account=${account.accountId ?? "-"} user=${account.userId ?? "-"}`,
);
this.log(
` plan=${account.planType ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`,
` type=${formatAccountType(account.planType)} plan=${account.planType ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`,
);
}
});
Expand Down
49 changes: 49 additions & 0 deletions src/lib/accounts/plan-display.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const CHATGPT_PLAN_LABELS: Record<string, string> = {
plus: "Plus",
business: "Business",
team: "Business",
pro: "Pro",
max: "Max",
enterprise: "Enterprise",
free: "Free",
};

const USAGE_BASED_PLAN_KEYS = new Set([
"api",
"codex_usage_based",
"codexusagebased",
"metered",
"pay_as_you_go",
"payasyougo",
"usage",
"usage_based",
"usagebased",
]);

function normalizePlanKey(planType: string): string {
return planType.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
}

function titleCasePlanType(planType: string): string {
return planType
.trim()
.split(/[^a-zA-Z0-9]+/)
.filter(Boolean)
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}`)
.join(" ");
}

export function formatAccountType(planType: string | undefined): string {
if (!planType?.trim()) {
return "-";
}

const normalized = normalizePlanKey(planType);
const withoutChatGptPrefix = normalized.replace(/^chatgpt_/, "");
if (USAGE_BASED_PLAN_KEYS.has(normalized) || USAGE_BASED_PLAN_KEYS.has(withoutChatGptPrefix)) {
return "Usage based (Codex)";
}

const chatGptPlanLabel = CHATGPT_PLAN_LABELS[withoutChatGptPrefix] ?? titleCasePlanType(planType);
return `ChatGPT seat (${chatGptPlanLabel})`;
}
24 changes: 24 additions & 0 deletions src/tests/account-plan-display.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";

import { formatAccountType } from "../lib/accounts/plan-display";

test("formatAccountType renders known ChatGPT seat tiers", () => {
assert.equal(formatAccountType("plus"), "ChatGPT seat (Plus)");
assert.equal(formatAccountType("team"), "ChatGPT seat (Business)");
assert.equal(formatAccountType("business"), "ChatGPT seat (Business)");
assert.equal(formatAccountType("pro"), "ChatGPT seat (Pro)");
assert.equal(formatAccountType("max"), "ChatGPT seat (Max)");
});

test("formatAccountType renders Codex usage-based plans", () => {
assert.equal(formatAccountType("usage_based"), "Usage based (Codex)");
assert.equal(formatAccountType("codex-usage-based"), "Usage based (Codex)");
assert.equal(formatAccountType("pay_as_you_go"), "Usage based (Codex)");
});

test("formatAccountType keeps unknown plan tiers visible", () => {
assert.equal(formatAccountType(undefined), "-");
assert.equal(formatAccountType(""), "-");
assert.equal(formatAccountType("enterprise_plus"), "ChatGPT seat (Enterprise Plus)");
});