Metadata-Version: 2.4
Name: claude-select
Version: 0.4.14
Summary: Manage a local registry of Claude CLI auth snapshots plus simple SDK token entries.
Author: wx.luo
License-Expression: MIT
Project-URL: Homepage, https://github.com/Nomia/claude-select
Project-URL: Repository, https://github.com/Nomia/claude-select
Project-URL: Issues, https://github.com/Nomia/claude-select/issues
Project-URL: Changelog, https://github.com/Nomia/claude-select/blob/main/CHANGELOG.md
Keywords: claude,claude-code,oauth,sdk,cli
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: rich>=13.9.0
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == "dev"
Requires-Dist: mypy>=1.11.0; extra == "dev"
Requires-Dist: pytest>=8.3.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
Requires-Dist: ruff>=0.11.0; extra == "dev"
Requires-Dist: twine>=5.1.1; extra == "dev"
Dynamic: license-file

# claude-select 🔐

[![PyPI version](https://img.shields.io/pypi/v/claude-select)](https://pypi.org/project/claude-select/)
[![Python versions](https://img.shields.io/pypi/pyversions/claude-select)](https://pypi.org/project/claude-select/)
[![CI](https://github.com/Nomia/claude-select/actions/workflows/ci.yml/badge.svg)](https://github.com/Nomia/claude-select/actions/workflows/ci.yml)

[中文说明](./README.zh-CN.md)

![claude-select overview](https://raw.githubusercontent.com/Nomia/claude-select/main/docs/images/claude-select-overview.png)

`claude-select` is a local Claude auth registry and selector for people who use multiple Claude accounts on one machine.

It stores two entry kinds:

- `cli` entries: captured from Claude CLI `auth login`; these support `watch`, `list --usage`, `whoami`, `select`, `sync-current`, `refresh`, and `relogin`
- `token` entries: captured from `claude setup-token`; these are simple long-lived SDK credentials for Python/program use

Long-lived `token` entries do **not** participate in quota monitoring, quota-aware auto-selection, or CLI account switching.

## Install 🚀

```bash
pip install claude-select
```

## How Users Get Started 👇

### 1. Capture your CLI accounts

Run the guided bootstrap:

```bash
claude-select init
```

By default, `claude-select` launches `claude auth login` in the current terminal for each account capture.

For each account:

1. choose an alias such as `work` or `personal`
2. `claude-select` launches `claude auth login`
3. finish authorization
4. return to `claude-select`
5. press Enter so `claude-select` can read the current Claude auth state
6. confirm the current `claude auth status` shown by `claude-select`
7. let `claude-select` capture the current login snapshot
8. look for a success block that confirms the account was saved and shows the current registry

You can add another CLI account later:

```bash
claude-select add work
claude-select add personal
```

If you do not want `claude-select` to launch `claude` for you, use:

```bash
claude-select add work --no-launch
```

### What `init` looks like

```bash
$ claude-select init
Claude account bootstrap
Add accounts one by one. Complete `claude auth login` for each account before capture.

Alias (blank to finish): work
Launching `claude auth login` in this terminal.
`claude auth login` will be launched in this terminal.
When login is complete, return here.
Press Enter after login is complete...
Current Claude auth status:
  email: a@company.com
  organization: Team A
  auth method: claude.ai
Use this identity for alias `work` and continue with capture? [Y/n]
Captured work <a@company.com> [Team A].
Status: healthy
Expires in: 7h 57m

Current registry:
Alias  Kind  Email            Organization  Status   Expires In  Last Selected  Last Synced
-----  ----  ---------------  ------------  -------  ----------  -------------  -----------
work   cli   a@company.com    Team A        healthy  7h 57m      -              just now
Add another account? [Y/n] y
```

### 2. See what is stored

```bash
claude-select list
claude-select list --usage
claude-select usage work
claude-select whoami
claude-select watch
claude-select watch --usage
claude-select watch --auto-refresh
```

Example `list --usage`:

```bash
$ claude-select list --usage
Alias        Kind       Email                      Organization   Status         Expires In  Last Selected  Last Synced  5h Left  5h Reset  7d Left  7d Reset
-----------  ---------  -------------------------  -------------  -------------  ----------  -------------  -----------  -------  --------  -------  --------
team-am      cli        alice.lee@example.com      Automizely     expiring_soon  13m         11h ago        42m ago      44.0%    3h 5m     80.0%    6d 6h
team-as      cli        alice.lee@example.com      AfterShip      expiring_soon  13m         1d ago         just now     44.0%    3h 5m     80.0%    6d 6h
consulting   cli        bob.chen@example.com       Studio North   healthy        6h 7m       11h ago        1h ago       41.0%    3h 5m     81.0%    4d 12h
sdk-bot      token      sdk.bot@example.com        Automation     healthy        364d        -              just now     n/a      n/a       n/a      n/a
```

Example `whoami`:

```bash
$ claude-select whoami
Current Claude live account
  matched alias: team-as
  email: alice.lee@example.com
  organization: AfterShip
  expires in: 13m
  auth method: claude.ai
  subscription: team
  5h quota left: 44.0%
  5h resets in: 3h 5m
  7d quota left: 80.0%
  7d resets in: 6d 6h
  target: config: /Users/you/.claude.json
  target: credentials store: macOS Keychain (Claude Code-credentials/you)
```

Example `watch --usage`:

```text
Current Claude live account
  matched alias: team-as
  email: alice.lee@example.com
  organization: AfterShip
  expires in: 13m
  auth method: claude.ai
  subscription: team

Local account registry
Alias        Kind  Email                  Organization  Status         Expires In  Last Selected  Last Synced  5h Left  5h Reset  7d Left  7d Reset
-----------  ----  ---------------------  ------------  -------------  ----------  -------------  -----------  -------  --------  -------  --------
team-am      cli   alice.lee@example.com  Automizely    expiring_soon  13m         11h ago        42m ago      44.0%    3h 5m     80.0%    6d 6h
team-as      cli   alice.lee@example.com  AfterShip     expiring_soon  13m         1d ago         just now     44.0%    3h 5m     80.0%    6d 6h
consulting   cli   bob.chen@example.com   Studio North  healthy        6h 7m       11h ago        1h ago       41.0%    3h 5m     81.0%    4d 12h

Heads up
  Some CLI accounts are close to expiry.
  No manual refresh is needed yet.

  Tip: run `claude-select watch --auto-refresh` to let watch try refresh right around expiry.
```

With `--usage`, `cli` entries show 5h/7d quota data and `token` entries show `n/a` because inference-only tokens do not expose quota/profile endpoints. If a value ends with `~`, it came from a stale cache because the latest usage fetch failed or was rate-limited.

### 3. Select an account for Claude CLI

```bash
claude-select select work
```

This reads the stored snapshot from the local registry and writes it back into Claude's current live auth backend:

- macOS: Keychain + Claude config
- Linux / Windows: Claude credentials file + Claude config

Example:

```bash
$ claude-select select work
Selected work <a@company.com> [Team A].
Updated Claude live auth state:
  - config: /Users/you/.claude.json
  - credentials store: macOS Keychain
Current CLI alias: work
```

If the selected CLI alias is already expired, `select` still writes it back into Claude's live auth state so you can try a fast recovery path next. The command prints a warning and recommends:

```bash
claude-select refresh <alias>
```

### 4. Use an entry in Python

```python
from claude_select import AuthManager
from claude_code_sdk import ClaudeAgentOptions, query

manager = AuthManager()
env = manager.build_sdk_env("work")
# or explicitly use a long-lived SDK token entry:
# env = manager.build_sdk_env("work-sdk")

options = ClaudeAgentOptions(env=env)

async for message in query(prompt="analyze this repo", options=options):
    print(message)
```

The Python side reads from the same local registry, but it does **not** mutate Claude's live auth state.

Full Python SDK guide:

- [Python SDK Guide](./docs/python-sdk.md)

## Optional SDK Token

If you explicitly want to store a long-lived `claude setup-token` token for Python or SDK use, you can add it later:

```bash
claude-select add-token work
```

If the alias already exists as a CLI account, the token is attached to that alias and the row appears as `cli+token`.

`add-token` launches `claude setup-token`, tries to detect the token from terminal output, then stores it as a simple SDK credential. Because these official long-lived tokens are inference-only, profile metadata detection is best-effort and may fall back to manual prompts.

## CLI Commands 🧰

```bash
claude-select init
claude-select add <alias>
claude-select add-token <alias>
claude-select refresh [alias]
claude-select relogin <alias>
claude-select list
claude-select list --usage
claude-select usage <alias>
claude-select watch
claude-select select [alias]
claude-select sync-current
claude-select rename <old-alias> <new-alias>
claude-select remove <alias>
claude-select export-env <alias> --json
claude-select current
claude-select whoami
```

Command behavior:

- `init`: guided multi-account bootstrap for CLI accounts, then optional token capture phase
- `add`: launch `claude auth login` in the current terminal by default, show the current `claude auth status`, then capture the confirmed login into the registry
- `add-token`: launch `claude setup-token` in the current terminal by default, then store a long-lived token for explicit SDK/program use; if the alias already exists as a CLI account, the token is attached to that alias
- `refresh`: try the lightweight recovery path for one CLI alias or all expired CLI aliases by doing `select -> claude -p "ping" -> sync-current`; manual refresh is only useful once the token is actually at expiry
- `relogin`: launch `claude auth login` in the current terminal by default, show the current `claude auth status`, then overwrite one stored CLI alias after the user logs in again
- `list`: show the current registry table and do a light sync of the current live account first
- `list --usage`: fetch and display 5h / 7d quota information for `cli` entries; `token` entries show `n/a`
- `usage`: show 5h / 7d quota for one alias, with stale/error diagnostics when the latest fetch did not succeed
- `watch`: keep a live Rich-powered view of the current Claude live account plus the local registry, with periodic live-state sync
- `watch --usage`: include 5h / 7d quota columns in the live registry table
- `watch --auto-refresh`: opt in to automatic `refresh` attempts only in the narrow expiry window, from about 5 seconds before expiry to about 10 seconds after expiry
- while `watch` is running, press `q` or `Esc` to exit cleanly
- `select`: write one stored `cli` snapshot back into Claude's live auth state
- `sync-current`: read Claude's current live auth state and sync any refreshed token data back into the matching `cli` registry entry
- `rename`: rename an alias without changing the underlying account snapshot or token attachment
- `remove`: delete one stored entry
- `export-env`: print SDK env vars for one alias
- `current`: show the last alias selected for CLI use
- `whoami`: show Claude's current live auth state, matched alias, auth method, subscription, and current quota summary after a light sync

## Python API 🐍

Minimal usage:

```python
from claude_select import AuthManager

manager = AuthManager()

accounts = manager.list_accounts()
accounts_with_usage = manager.list_accounts(include_usage=True)
cli_accounts = manager.list_cli_accounts(include_usage=True)
token_accounts = manager.list_token_accounts()
details = manager.get_account("work")
summary = manager.get_account_summary("work", include_usage=True)
available = manager.list_available_accounts(include_usage=True, auto_refresh=True)
picked = manager.pick_available_account(include_usage=True, auto_refresh=True)
env = manager.build_sdk_env("work", auto_refresh=True)
sdk_env = manager.build_sdk_env("work-sdk")
auth_payload = manager.export_sdk_auth("work", auto_refresh=True)
live_quota = manager.get_live_quota()
account_quota = manager.get_account_quota("work", auto_refresh=True)
all_quotas = manager.list_account_quotas(auto_refresh=True)
```

Current public surface:

```python
class AuthManager:
    def list_accounts(self, include_usage: bool = False, *, auto_refresh: bool = False) -> list[dict]: ...
    def list_cli_accounts(self, include_usage: bool = False, *, auto_refresh: bool = False) -> list[dict]: ...
    def list_token_accounts(self, include_usage: bool = False) -> list[dict]: ...
    def list_available_accounts(self, *, include_usage: bool = True, auto_refresh: bool = False, require_quota: bool = True) -> list[dict]: ...
    def pick_available_account(self, *, include_usage: bool = True, auto_refresh: bool = False, require_quota: bool = True, prefer_current: bool = True) -> dict: ...
    def get_account(self, alias: str, *, auto_refresh: bool = False): ...
    def get_account_summary(self, alias: str, *, include_usage: bool = False, auto_refresh: bool = False) -> dict: ...
    def get_current_account_summary(self, *, include_usage: bool = True) -> dict: ...
    def capture_current_account(self, alias: str, overwrite: bool = True) -> dict: ...
    def add_token_account(self, alias: str, token: str, *, email: str, organization_name: str = "", organization_id: str = "", account_uuid: str = "", overwrite: bool = True) -> dict: ...
    def relogin_account(self, alias: str) -> dict: ...
    def refresh_account(self, alias: str, *, prompt: str = "ping") -> dict: ...
    def remove_account(self, alias: str) -> None: ...
    def select_account(self, alias: str, *, auto_refresh: bool = False) -> dict: ...
    def build_sdk_env(self, alias: str, base_env: dict[str, str] | None = None, *, auto_refresh: bool = False) -> dict[str, str]: ...
    def export_sdk_auth(self, alias: str, *, auto_refresh: bool = False) -> dict: ...
    def get_live_quota(self) -> dict: ...
    def get_account_quota(self, alias: str, *, auto_refresh: bool = False) -> dict: ...
    def list_account_quotas(self, *, auto_refresh: bool = False) -> list[dict]: ...
    def current_alias(self) -> str | None: ...
    def render_table(self) -> str: ...
```

Top-level helper:

```python
from claude_select import build_sdk_env

env = build_sdk_env("work")
```

`auto_refresh=True` is explicit on purpose. For CLI-backed aliases it will try the same lightweight recovery path as `claude-select refresh <alias>` before returning data:

- select that alias into Claude's live auth state
- run `claude -p "ping"`
- sync the refreshed live state back into the registry

This is useful for Python callers that want a best-effort recovery path without forcing a separate CLI maintenance step first.

Compatibility note:

- `build_sdk_env_auto()` and `pick_sdk_account()` remain importable for older integrations, but they now raise `AccountSelectionError`.
- Reason: long-lived `claude setup-token` tokens are inference-only and do not expose the profile/quota data required for reliable quota-aware rotation.

Quota responses are cached locally for 60 seconds. `watch`, `whoami`, and repeated CLI quota reads reuse that cache instead of hitting the remote usage endpoint on every refresh.

## How Expiry Works ⏳

`claude-select` does not refresh tokens automatically.

It reads the stored `expiresAt` timestamp and derives status from it:

- `healthy`: more than 1 hour remains
- `expiring_soon`: 1 hour or less remains
- `expired`: already expired
- `unknown`: no `expiresAt` was captured

If a `cli` account is expired:

- `select` fails
- `build_sdk_env()` fails for that alias
- first try `claude-select refresh <alias>`
- if refresh works, Claude refreshes the live session and `claude-select` syncs the new state back into the registry
- if refresh fails, run `claude-select relogin <alias>`

Long-lived `token` entries do not participate in `relogin`; replace them by running `add-token` again.

## Storage Layout 🗃️

The local registry is a SQLite database:

- macOS / Linux default:
  - `~/.config/claude-select/registry.db`
- if XDG is set:
  - `$XDG_CONFIG_HOME/claude-select/registry.db`

Each entry stores:

- alias
- kind (`cli_snapshot` or `token`)
- email
- organization name / id
- account uuid
- captured time
- expiresAt
- last selected time
- `oauthAccount`
- `claudeAiOauth` credential payload

Claude's own live state remains separate:

- Claude global config
- Claude credentials file or macOS Keychain

The registry is the source of truth. `select` writes one stored `cli` snapshot back into Claude's current live state.

## Current Limitations ⚠️

- `cli` entries are the only entries that support quota monitoring, `watch`, `select`, `sync-current`, `refresh`, and `relogin`.
- `token` entries are simple SDK credentials only.
- The tool does not auto-refresh OAuth tokens.
- The implementation depends on Claude's current local auth layout.
- Expiry monitoring is based on stored `expiresAt`; it does not proactively validate every entry with a remote auth probe.

## Development 🛠️

Install development dependencies:

```bash
pip install -e .[dev]
```

Run checks:

```bash
ruff check .
ruff format --check .
mypy
pytest
python -m build
python -m twine check dist/*
```

## License 📄

MIT
