Metadata-Version: 2.4
Name: codex-swap
Version: 0.1.4
Summary: Multi-account switcher for the OpenAI Codex CLI — auto-picks the ChatGPT Pro account with the lowest usage.
Project-URL: Homepage, https://github.com/aneym/codex-swap
Project-URL: Issues, https://github.com/aneym/codex-swap/issues
Project-URL: Changelog, https://github.com/aneym/codex-swap/blob/main/CHANGELOG.md
Author: Alex Neyman
License: MIT
License-File: LICENSE
Keywords: chatgpt,cli,codex,openai,rate-limit
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# codex-swap

[![CI](https://github.com/aneym/codex-swap/actions/workflows/ci.yml/badge.svg)](https://github.com/aneym/codex-swap/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/codex-swap.svg)](https://pypi.org/project/codex-swap/)
[![Release](https://img.shields.io/github/v/release/aneym/codex-swap?display_name=tag)](https://github.com/aneym/codex-swap/releases/latest)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Pick the ChatGPT Pro account with the lowest usage every time you launch [Codex CLI](https://github.com/openai/codex). One command, no thinking.

```
$ cx
cx: using slot 2 (alex@example.com, 5h 4%, 7d 12%)
╭─── OpenAI Codex (v0.128.0) ───╮
│ model: gpt-5.5  fast          │
╰────────────────────────────────╯
```

If you have multiple Codex Pro accounts and you keep hitting the 5-hour cap on whichever one you happened to be logged into, this is for you. It's the Codex equivalent of [`cswap`](https://pypi.org/project/claude-swap/) for Claude Code.

## What it does

- Saves a snapshot of each account's `~/.codex/auth.json` into its own slot.
- Keeps a durable per-slot usage store (5-hour + 7-day windows) and decays each window automatically when the API's `resets_at` time passes — so the picker stays accurate without re-measuring.
- On each `cx` launch, does a cheap rollout scan and merges any fresh findings into that store. Slots not seen in this scan keep their previous record.
- Swaps `auth.json` to the slot with the lowest usage, then `exec`s `codex`.
- Snapshots back any refreshed tokens so the rotation chain never breaks.
- `codex-swap seed` populates usage for every slot in parallel via isolated `CODEX_HOME`s — one-time setup; no re-seeding on every launch.

## Install

Recommended — installs two commands (`codex-swap` and `cx`) into `~/.local/bin`, using PyPI when available and falling back to the latest GitHub release. It uses `uv` when available, then `pipx`, then a private Python venv:

```bash
curl -fsSL https://github.com/aneym/codex-swap/releases/latest/download/install.sh | bash
```

Or install directly from PyPI with your preferred isolated Python tool manager:

```bash
uv tool install codex-swap
# or:
pipx install codex-swap
```

If PyPI is unavailable, install the latest GitHub release wheel directly:

```bash
uv tool install https://github.com/aneym/codex-swap/releases/download/v0.1.3/codex_swap-0.1.3-py3-none-any.whl
# or:
pipx install https://github.com/aneym/codex-swap/releases/download/v0.1.3/codex_swap-0.1.3-py3-none-any.whl
```

Make sure `~/.local/bin` is on your `PATH`. If not:

```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
exec zsh
```

Verify:

```bash
codex-swap --version
cx --help
```

More install, upgrade, and shell-helper options live in [`docs/INSTALL.md`](docs/INSTALL.md).

## First-time setup

You need to be logged into Codex with one of your ChatGPT Pro accounts already (`codex login`). Then:

```bash
# 1. Save the account you're already logged into as slot 1.
codex-swap add

# 2. Add the rest of your accounts. codex-swap will pop a browser per account.
#    onboard auto-seeds usage for every slot at the end (parallel ~50 tokens
#    per slot), so `cx` has informed picks from the very first launch.
codex-swap onboard 2

# 3. Confirm everything works.
codex-swap verify
```

Output of `verify` should be all `ok` (or `rate_limited` if a window is currently capped — that's fine, auth is still healthy). If a row says `error`, the probe failed for a local Codex/model/config reason rather than a dead refresh token.

If you imported slots manually (e.g. via `add` / `import-profile`) and skipped onboard, run the seed step yourself once:

```bash
codex-swap seed         # probes any slot without a usage record, in parallel
codex-swap seed --all   # re-seed every slot (re-measure)
```

If you already have Codex profile directories, import them instead of logging in again:

```bash
codex-swap import-profile personal
codex-swap import-profile work --label work
```

`import-profile` accepts a profile name under `~/.codex-profiles`, a profile directory, or an `auth.json` path. It supports both ChatGPT OAuth profiles and API-key profiles; API-key profiles do not expose 5-hour/7-day ChatGPT usage telemetry, so they show as unknown usage and are best pinned explicitly when needed.

## Daily use

Replace `codex` with `cx` in your workflow:

```bash
cx                # auto-pick lowest-usage slot, then run codex
cx exec "fix bug" # all codex args are forwarded
```

That's the whole interface.

## When something goes wrong

If a slot's refresh token dies (you ran `codex logout` somewhere, the token aged out, etc.), `cx` will still try to use it and codex will print "refresh token was already used" or similar. Fix everything in one shot:

```bash
codex-swap reconnect    # finds dead slots, walks you through fresh logins
```

If you want to fix one specific slot:

```bash
codex-swap reauth 1     # opens browser, log in to that slot's account
```

## All commands

| Command                               | What it does                                                                    |
| ------------------------------------- | ------------------------------------------------------------------------------- |
| `cx`                                  | Auto-pick lowest-usage slot and exec codex (this is what you'll use)            |
| `codex-swap add`                      | Save the currently logged-in account as a new slot                              |
| `codex-swap import-profile <profile>` | Save an existing `~/.codex-profiles/<profile>/auth.json` or auth file as a slot |
| `codex-swap remove <slot>`            | Remove a slot                                                                   |
| `codex-swap list`                     | Show all slots with usage %                                                     |
| `codex-swap status`                   | Show which slot is active right now                                             |
| `codex-swap switch [<slot>]`          | Switch to a slot (no arg → rotate to next)                                      |
| `codex-swap reauth <slot>`            | Re-mint a slot via fresh `codex login`                                          |
| `codex-swap reconnect`                | Verify all slots, then reauth every broken one                                  |
| `codex-swap onboard [N]`              | Guided login for N accounts in a row                                            |
| `codex-swap verify`                   | Test every slot with a real `codex exec` call                                   |
| `codex-swap usage`                    | Refresh & print the per-slot usage cache (decay-aware)                          |
| `codex-swap seed [<slot>...]`         | Probe slots in parallel (isolated `CODEX_HOME`) to populate usage data          |
| `codex-swap stash`                    | Snapshot live `auth.json` back into its slot                                    |
| `codex-swap purge --yes`              | Delete all codex-swap state                                                     |

`<slot>` accepts a slot number, an email, or an account_id.

### Pinning and bypass

```bash
CXSWAP_SLOT=2 cx       # force a specific slot for this run
CXSWAP_SKIP_AUTO=1 cx  # skip auto-pick (use whatever's currently in auth.json)
```

The explicit names `CODEX_SWAP_SLOT` and `CODEX_SWAP_SKIP_AUTO` work too.

## Optional shell aliases

If you like shorter names, add to your `~/.zshrc`:

```bash
cxraw()       { CXSWAP_SKIP_AUTO=1 cx "$@"; }
cxslot()      { CXSWAP_SLOT="$1" cx "${@:2}"; }
cxaccounts()  { codex-swap list "$@"; }
cxstatus()    { codex-swap status "$@"; }
cxverify()    { codex-swap verify "$@"; }
cxreauth()    { codex-swap reauth "$@"; }
cxreconnect() { codex-swap reconnect "$@"; }
```

Reload with `exec zsh`.

## How "lowest usage" is computed

Codex persists a `token_count` event with `rate_limits.primary` (5-hour window) and `rate_limits.secondary` (7-day window) into `~/.codex/sessions/**/*.jsonl` after every turn. codex-swap correlates conversation IDs to account IDs via `~/.codex/logs_2.sqlite` (`user.account_id="..."` + `conversation.id=...` in the otel log bodies) to figure out which slot owns each rollout.

The persisted store at `~/.codex-swap/cache/usage.json` is **durable**: each rescan merges new findings (per-slot, higher `scanned_at` wins) instead of overwriting. Slots not seen in a given scan keep their previous record. When a window's `resets_at` time passes, codex-swap reports that window as 0% via decay — the picker doesn't need to re-measure to notice the reset.

Slots with no record at all (e.g. freshly added, never used here) get usage data via `codex-swap seed`, which probes each named slot in parallel using its own temporary `CODEX_HOME` (no contention with the live `~/.codex/auth.json`). `onboard` runs this automatically; otherwise call it manually once.

The picker sorts in three buckets, lower wins:

1. Known + healthy (both windows below 80% after decay)
2. Unknown (no record yet — assumed fresh; bias toward learning)
3. Known + at/above 80% on either window (last resort)

Within a bucket, ties break on `(5h%, 7d%, slot#)`.

## Critical: never run `codex logout`

`codex logout` calls a server-side revoke that **invalidates the refresh token** at the OAuth provider. Every other slot whose snapshot pre-dates that revoke is then permanently dead. codex-swap's onboarding and reauth flows use `rm ~/.codex/auth.json` instead — same effect locally, no server-side blast radius.

If a slot ever goes bad (you ran `codex logout` by hand, or the token aged out), `codex-swap reconnect` will detect and fix it.

## Refresh token rotation, briefly

ChatGPT issues single-use refresh tokens that rotate on every successful refresh. codex-swap snapshots the live `auth.json` back into its slot before every swap-out, so the slot's snapshot always carries the latest rotated token. You only get into trouble if you run `codex` twice on the same slot in parallel — both refresh independently, one of them ends up with a token the server has already burned.

**Safe:** sequential `cx` runs, even across many accounts.
**Unsafe:** two terminals running `cx` against the same slot at the same time.

## Files

```
~/.codex-swap/
├── accounts/<N>/auth.json   # per-slot snapshots (chmod 600)
├── sequence.json            # slot order + email/account_id metadata
├── state.json               # last switched slot + timestamp
└── cache/usage.json         # durable per-slot usage records (with resets_at decay)
```

`~/.codex/auth.json` is the live file Codex reads. codex-swap only ever swaps that one file in and out.

## Caveats

- macOS only right now.
- Requires `codex` v0.122 or newer (the rollout schema with `rate_limits.primary/secondary` was added around that time). If multiple Codex installs exist on `PATH`, codex-swap chooses the newest detected binary.
- `OPENAI_API_KEY` in your environment will override `auth.json`. codex-swap unsets it when running `codex login` so the OAuth path wins; if you want `cx` to do the same for the launched session, wrap it: `alias cx='env -u OPENAI_API_KEY codex-swap launch'`.

## Uninstall

```bash
codex-swap purge --yes              # before uninstall, removes ~/.codex-swap
uv tool uninstall codex-swap        # or: pipx uninstall codex-swap
```

## Development

```bash
git clone https://github.com/aneym/codex-swap
cd codex-swap
bash scripts/install-dev.sh     # installs editable via uv tool
pytest -q                       # run the unit tests
ruff check .                    # lint
```

## Releasing

Maintainer flow:

```bash
# 1. Move bullet points into the [Unreleased] block of CHANGELOG.md
# 2. Cut the release:
scripts/release.sh 0.1.1
```

The script runs the full packaging gate, bumps `src/codex_swap/__init__.py`, promotes `[Unreleased]` into a versioned section, tags `v0.1.1`, and pushes. A successful `main` CI run also auto-creates a missing version tag, so release publication stays automated even when maintainers only push the release commit. The `publish.yml` workflow builds, checks, uploads to PyPI via Trusted Publishing, and creates a GitHub release with the changelog notes attached. No long-lived API tokens involved.

See [`docs/RELEASE.md`](docs/RELEASE.md) for the exact automation path.

### One-time PyPI Trusted Publisher setup

Before the first publish runs cleanly:

1. In PyPI's pending publisher flow, create the `codex-swap` project publisher:
   https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/
2. Configure the GitHub publisher fields exactly:
   - PyPI project name: `codex-swap`
   - Owner: `aneym`
   - Repository: `codex-swap`
   - Workflow: `publish.yml`
   - Environment: `pypi`
3. On GitHub → repo _Settings_ → _Environments_ → confirm `pypi` exists (no secrets needed — the OIDC token handles auth).
4. Rerun the failed `Publish to PyPI` job, or push the next version tag.

After that, every `git push` of a `vX.Y.Z` tag publishes automatically.

## License

MIT
