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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ Verify with:
codex exec "say hello"
```

## Build a GUI with JSON

If you are building a GUI on top of the current `codex-auth` client, use the supported `--json` command variants to receive machine-readable results. See the [JSON API documentation](./docs/json-api.md) for supported commands and the compatibility contract.

## Disclaimer

This project is provided as-is and use is at your own risk.
Expand Down
26 changes: 13 additions & 13 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,49 +79,49 @@ That scope includes:

`chatgpt_user_id` is the user identity for this flow. A single user may have multiple workspace `chatgpt_account_id` values, and those values can be legacy account ids or organization fallback ids.

This means a `free`, `plus`, or `pro` record can still trigger a grouped Team-name refresh when it belongs to the same `chatgpt_user_id` as Team records.
This means a personal-plan record can still participate in a grouped workspace-name refresh when it belongs to the same `chatgpt_user_id` as Business, Enterprise, or Edu workspace records.

Account metadata refresh is attempted only when:

- the scope contains more than one record
- the scope contains at least one Team record
- at least one Team record in that scope still has `account_name = null`
- the scope contains at least one Business, Enterprise, or Edu workspace record
- at least one workspace record in that scope still has `account_name = null`

## Apply Rules

After a successful account metadata response:

- returned entries are matched by `chatgpt_account_id`
- matched records overwrite the stored `account_name`, even when a Team record already had an older value
- in-scope Team records, or in-scope records that already had an `account_name`, are cleared back to `null` when they are not returned by the response
- matched records overwrite the stored `account_name`, even when a workspace record already had an older value
- in-scope workspace records, or in-scope records that already had an `account_name`, are cleared back to `null` when they are not returned by the response
- records outside the scope are left unchanged

## Examples

Example 1:

- active record: `user@example.com / Team #1 / account_name = null`
- same grouped scope: `user@example.com / Team #2 / account_name = null`
- active record: `user@example.com / Business #1 / account_name = null`
- same grouped scope: `user@example.com / Business #2 / account_name = null`

Running `codex-auth list` should issue an account metadata request. If the API returns:

- `team-1 -> "Workspace Alpha"`
- `team-2 -> "Workspace Beta"`

Then both grouped Team records are updated.
Then both grouped Business workspace records are updated.

Example 2:

- active record: `user@example.com / Pro / account_name = null`
- same grouped scope: `user@example.com / Team #1 / account_name = null`
- same grouped scope: `user@example.com / Team #2 / account_name = "Old Workspace"`
- same grouped scope: `user@example.com / Business #1 / account_name = null`
- same grouped scope: `user@example.com / Business #2 / account_name = "Old Workspace"`

Running `codex-auth list` should still issue an account metadata request, because the grouped scope still has missing Team names. If the API returns:
Running `codex-auth list` should still issue an account metadata request, because the grouped scope still has missing workspace names. If the API returns:

- `team-1 -> "Prod Workspace"`
- `team-2 -> "Sandbox Workspace"`

Then:

- `Team #1` is filled with `Prod Workspace`
- `Team #2` is overwritten from `Old Workspace` to `Sandbox Workspace`
- `Business #1` is filled with `Prod Workspace`
- `Business #2` is overwritten from `Old Workspace` to `Sandbox Workspace`
6 changes: 6 additions & 0 deletions docs/commands/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ codex-auth list --active
codex-auth list --live
codex-auth list --api
codex-auth list --skip-api
codex-auth list --json
```

## Behavior
Expand All @@ -25,6 +26,7 @@ codex-auth list --skip-api
- `--api` is accepted as an explicit equivalent to default mode.
- `--skip-api` forbids remote API calls for this command.
- `--live` keeps refreshing the terminal view and requires a TTY.
- `--json` emits one machine-readable JSON document and cannot be combined with `--live`.

When local-only refresh is active, only the active account can be updated from local rollout files. Non-active rows use the stored registry snapshot.

Expand All @@ -37,4 +39,8 @@ When local-only refresh is active, only the active account can be updated from l
- In non-live output, `RESET CREDITS` shows the stored reset-credit count when remote usage refresh provides it.
- Remote refresh failures can render row overlays such as `401`, `403`, `TimedOut`, or `MissingAuth`.
- `LAST ACTIVITY` is based on the last stored usage update time.
- `--json` returns accounts in the same display order and includes the same row numbers shown by the table.
- JSON plan values are final product plans; callers do not map backend Team/Business identifiers.
- JSON usage keeps the displayable snapshot separate from the current invocation's refresh result, so cached values remain visible after refresh failure.
- Non-fatal warnings are written to stderr and are not JSON fields.
- Shared table layout policy is documented in [docs/table-layout.md](../table-layout.md).
13 changes: 12 additions & 1 deletion docs/commands/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ codex-auth remove [--api|--skip-api]
codex-auth remove --live [--api|--skip-api]
codex-auth remove <query> [<query>...]
codex-auth remove --all
codex-auth remove (<query> [<query>...]|--all) --json
```

## Interactive Remove
Expand All @@ -30,7 +31,7 @@ codex-auth remove --all

`codex-auth remove <query> [<query>...]` removes one or more accounts using stored local data.

Selectors can match:
Selectors first try an exact `account_key` match. Otherwise, they can match:

- displayed row number,
- alias fragment,
Expand All @@ -42,11 +43,16 @@ Selector-based remove does not accept `--live`, `--api`, or `--skip-api`.

If a selector matches multiple accounts in a TTY, `remove` asks for confirmation. If stdin is not a TTY, ambiguous matches fail and the user must refine the selector.

With `--json`, selector-based remove never asks for confirmation. Every selector
is resolved before deletion. If any selector is ambiguous or missing, no
account is removed and one JSON error reports all selector resolutions.

## Remove All

`codex-auth remove --all` clears all accounts tracked in `registry.json`.

- It does not accept `--live`, `--api`, or `--skip-api`.
- `--json` emits one machine-readable JSON document.
- It deletes managed account snapshots and matching managed backups.
- It leaves malformed or unidentifiable backup files in place.

Expand All @@ -60,3 +66,8 @@ When the removed account was active:
- malformed or unsyncable `auth.json` is left untouched.

After a successful deletion, stdout prints `Removed N account(s): ...` in removal order.

Selector resolution is logically atomic for both human and JSON modes. A
filesystem failure after mutation starts may still leave state partially
changed; JSON reports this as `state_uncertain`, after which callers must list
state again before retrying.
7 changes: 7 additions & 0 deletions docs/commands/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ codex-auth switch -
codex-auth switch [--api|--skip-api]
codex-auth switch --live [--api|--skip-api]
codex-auth switch <query>
codex-auth switch <query> --json
```

## Previous Switch
Expand All @@ -17,6 +18,7 @@ codex-auth switch <query>
- The command fails when no previous account has been recorded.
- The command fails when the recorded previous account was removed.
- `switch -` does not accept `--live`, `--api`, or `--skip-api`.
- Previous-account switching is a human CLI convenience and cannot be combined with `--json`.

## Interactive Switch

Expand Down Expand Up @@ -49,6 +51,9 @@ Selectors can match:

If one account matches, it switches immediately. If multiple accounts match, the command falls back to interactive selection. Query mode does not accept `--live`, `--api`, or `--skip-api`.

With `--json`, query mode never opens the interactive picker. Ambiguous queries fail with a JSON error document that includes candidate accounts.
The returned `switched_to.plan` is already normalized to the final product plan.

## Switch Effects

When switching succeeds:
Expand All @@ -58,3 +63,5 @@ When switching succeeds:
3. `active_account_key` is updated in `registry.json`.
4. `previous_active_account_key` records the account that was active before the switch, when one exists.
5. The success message uses the same identity label as singleton rows, for example `Switched to me(test@example.com)`.

The previous-account pointer is internal CLI state and is not included in JSON responses.
220 changes: 220 additions & 0 deletions docs/json-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# JSON API

`codex-auth` provides a versioned JSON contract for its same-version GUI and
for automation callers. The GUI invokes commands serially, reads exactly one
JSON document from stdout, and drains stderr without using warning text for
program logic.

## Compatibility

Every document contains `"schema_version": 1`. Clients must ignore unknown
object fields and use generic fallbacks for unknown error codes and enum
values. Adding optional fields, error codes, or enum values is non-breaking in
schema 1. Removing or renaming fields, changing field types or existing value
semantics, making optional data required, or changing exit-code behavior is
breaking and requires a schema-version increment.

Stdout contains exactly one JSON document followed by a newline. Diagnostics
and warnings use stderr and are not part of the JSON contract.

| Exit code | Meaning |
|-----------|---------|
| `0` | Success |
| `1` | Handled operation error; stdout contains a JSON error document |
| `2` | Invalid command usage; stdout contains a JSON usage error when `--json` was recognized |

## Supported Commands

```shell
codex-auth list [--api|--skip-api] [--active] --json
codex-auth switch <query> --json
codex-auth remove <selector> [<selector>...] --json
codex-auth remove --all --json
```

Interactive and live paths are not supported. `switch -` remains a human CLI
shortcut and is rejected when combined with `--json`.

## Account Objects

```json
{
"number": 1,
"account_key": "user-abc::account-123",
"email": "a@example.com",
"alias": "work",
"account_name": null,
"plan": "business",
"auth_mode": "chatgpt",
"active": true,
"created_at": 1730000000,
"last_used_at": 1730001000,
"usage": {
"source": "cache",
"updated_at": 1730002000,
"primary": {
"used_percent": 12.5,
"window_minutes": 300,
"resets_at": 1730010000
},
"secondary": null,
"credits": {
"has_credits": false,
"unlimited": false,
"balance": null
},
"reset_credits": null,
"refresh": {
"requested": true,
"method": "api",
"status": "http_error",
"http_status": 503,
"error_code": null
}
}
}
```

`account_key` is stable and should be used for switch/remove calls. `number` is
an ephemeral display selector valid only for the ordering returned by the
current invocation. Empty aliases and account names are `null`.

`plan` is already normalized by the CLI. Important mappings are:

| Input observed by the CLI | JSON plan |
|---------------------------|-----------|
| `team`, `self_serve_business_usage_based` | `business` |
| `business`, `enterprise_cbp_usage_based`, `enterprise`, `hc` | `enterprise` |
| `education`, `edu` | `edu` |

The GUI must not repeat this mapping. When both auth and stored usage provide a
plan, the usage plan wins.

### Usage

`usage.source` describes the displayed snapshot:

| Source | Meaning |
|--------|---------|
| `api` | Confirmed by an API response in this invocation |
| `local` | Read from a local Codex session in this invocation |
| `cache` | Loaded from the registry; a refresh was not requested or did not replace it |
| `none` | No displayable snapshot is available |

Snapshot fields remain present after refresh failure. `updated_at` is the
stored snapshot update timestamp and may remain unchanged after an equal
successful response.

`usage.refresh` describes only the current invocation:

| Field | Values |
|-------|--------|
| `requested` | Boolean |
| `method` | `api`, `local`, or `null` |
| `status` | `not_requested`, `ok`, `no_data`, `http_error`, `missing_auth`, `error` |
| `http_status` | HTTP status or `null` |
| `error_code` | Structured API/internal error name or `null` |

The credits object retains `has_credits`; callers must not infer it from object
presence or balance.

## Success Documents

### List

```json
{
"schema_version": 1,
"command": "list",
"active_account_key": "user-abc::account-123",
"accounts": []
}
```

### Switch

```json
{
"schema_version": 1,
"command": "switch",
"switched_to": {}
}
```

### Remove

```json
{
"schema_version": 1,
"command": "remove",
"removed": [],
"new_active_account_key": null
}
```

## Error Documents

```json
{
"schema_version": 1,
"error": {
"code": "account_not_found",
"message": "no account matches \"work\""
}
}
```

Switch ambiguity adds `candidates` containing account objects.

Remove resolves every selector before mutation. If any selector is missing or
ambiguous, no account is removed and the error contains all resolutions:

Candidate objects are abbreviated in this example; each real candidate uses
the complete account-object shape documented above.

```json
{
"schema_version": 1,
"error": {
"code": "selector_resolution_failed",
"message": "one or more selectors could not be resolved",
"resolutions": [
{
"selector": "work",
"status": "ambiguous",
"account_key": null,
"candidates": [
{
"number": 1,
"account_key": "user-a::account-a",
"email": "work-a@example.com"
},
{
"number": 2,
"account_key": "user-b::account-b",
"email": "work-b@example.com"
}
]
},
{
"selector": "missing",
"status": "not_found",
"account_key": null,
"candidates": []
}
]
}
}
```

Resolution status values are `resolved`, `ambiguous`, and `not_found`.

| Error code | Meaning |
|------------|---------|
| `account_not_found` | Switch query has no match |
| `ambiguous_query` | Switch query has multiple matches |
| `selector_resolution_failed` | Remove selector resolution failed atomically |
| `curl_unavailable` | Required API refresh cannot find curl |
| `registry_error` | State failed before mutation began |
| `state_uncertain` | Persistence failed after mutation began; run `list --json` before retrying |
| `usage` | Invalid command usage |
Loading
Loading