claude-multiacc 2.0.26 → 2.0.28
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/README.md +50 -138
- package/bin/claude-accounts +2 -3
- package/bin/claude-multiacc +3 -0
- package/bin/cli.mjs +16 -18
- package/bin/codex +124 -0
- package/bin/codex-accounts +2 -3
- package/bin/multiacc-select +4 -0
- package/docs/ACCOUNT_OPERATIONS.md +3 -61
- package/docs/CODEX.md +83 -0
- package/docs/INSTALLATION.md +91 -0
- package/docs/UNIFIED_SELECTOR.md +2 -0
- package/docs/VERIFICATION.md +68 -0
- package/install.sh +9 -284
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/common.sh +92 -2
- package/lib/install-help.txt +20 -0
- package/lib/install_actions.sh +238 -0
- package/lib/install_calendar.sh +59 -0
- package/package.json +5 -2
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/packaged_command_support.py +81 -0
- package/tests/run-tests.sh +295 -2
- package/tests/test_packaged_commands.py +170 -0
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ completely independent — separate manifests, credentials, telemetry, and logs
|
|
|
16
16
|
either provider can be used, re-authenticated, or emptied without touching the other.
|
|
17
17
|
|
|
18
18
|
Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207+, Codex CLI 0.147) and Ubuntu 24.04
|
|
19
|
-
(bash 5.2). The compatibility suite
|
|
20
|
-
|
|
19
|
+
(bash 5.2). The compatibility suite exercises both providers with no network/quota
|
|
20
|
+
use; the unified selector and packed npm commands have their own contract suites.
|
|
21
21
|
|
|
22
22
|
## Unified selector for app-robot
|
|
23
23
|
|
|
@@ -315,6 +315,45 @@ same ownership rule. Note that the **installed layout deliberately shares that t
|
|
|
315
315
|
default codex pool this scan stays off and the usage endpoint remains codex's only limit
|
|
316
316
|
signal. Claude accounts keep a private `sessions/` registry, so it is fully active there.
|
|
317
317
|
|
|
318
|
+
A shared tree also gets **one shared rollout index**. codex refuses to start until its
|
|
319
|
+
`state_<schema>.sqlite` has indexed every rollout under `$CODEX_HOME/sessions` (each file
|
|
320
|
+
read whole), so a private index per account meant one full scan of the shared tree per
|
|
321
|
+
account — 10 GB × 12 on a busy Mac — and any launch that met another process's unfinished
|
|
322
|
+
scan died after 30 s with "timed out waiting for state db backfill … (status: running)",
|
|
323
|
+
surfaced as the CLI's "local database appears to be damaged". A scan cut short (a probe with
|
|
324
|
+
a timeout, Ctrl-C) also left its 15-minute worker lease behind, keeping the account
|
|
325
|
+
unstartable long after its killer was gone. Since 2.0.27 `<acct>/state_N.sqlite` is a symlink
|
|
326
|
+
to `~/.codex/state_N.sqlite`, exactly like the tree it describes: SQLite resolves the link
|
|
327
|
+
before naming its `-wal`/`-shm` companions, so every process shares one lock set — the same
|
|
328
|
+
multi-process mode a single `CODEX_HOME` already runs in. The shim applies this at every
|
|
329
|
+
launch, and the index name the installed binary carries is linked ahead of time, so a codex
|
|
330
|
+
schema bump still costs one backfill per machine rather than one per account.
|
|
331
|
+
|
|
332
|
+
An account with **no index of its own** — every newly added one, and the case the change
|
|
333
|
+
exists for — is simply linked. Nothing is renamed and nothing about the shared index's state
|
|
334
|
+
can block it. An account that already has one is only ever moved while **nothing can open
|
|
335
|
+
it** (no `-wal` and no `-shm` beside it). codex holds the state database through an sqlx pool
|
|
336
|
+
whose connections are opened lazily and *by path* (`max_connections(5)`,
|
|
337
|
+
`create_if_missing(true)`), so renaming an index under a live holder would leave its first
|
|
338
|
+
connection on the old inode while every later one follows the new link — one process, two
|
|
339
|
+
databases. A codex killed mid-write leaves the pair behind too, and the next clean session on
|
|
340
|
+
that account removes it, so an account excluded this way rejoins on its own. Once it is
|
|
341
|
+
closed, the account's index is either **retired** beside the link as `*.private` (never
|
|
342
|
+
deleted, never overwritten — a second retirement gets its own suffix) when the home already
|
|
343
|
+
has one, or **promoted** into the home when it does not. The promote goes through `link(2)`,
|
|
344
|
+
which refuses an existing target: two shims racing to be the first to promote could otherwise
|
|
345
|
+
rename one's fresh symlink onto the file the other had just promoted, leaving the shared index
|
|
346
|
+
pointing at itself — `ELOOP`, every account on the machine unable to start, and repaired by
|
|
347
|
+
neither codex nor the shim.
|
|
348
|
+
|
|
349
|
+
Two things stop the sharing entirely. An account whose `sessions/` is a real directory keeps a
|
|
350
|
+
private index — the rule is structural, not a heuristic. And codex's own corruption recovery
|
|
351
|
+
has the last word: when it judges a database damaged it renames it (with its `-wal`/`-shm`)
|
|
352
|
+
into `<CODEX_HOME>/db-backups/sqlite-<ts>-<n>/` and rebuilds, which under a link renames the
|
|
353
|
+
*link*. A link sitting in that folder pointing at the shared file is codex's verdict on that
|
|
354
|
+
file, so the account keeps the index codex rebuilt for it and that name is left alone —
|
|
355
|
+
handing the link back would hand the damage back.
|
|
356
|
+
|
|
318
357
|
**Auto-retry** (`-p`/`--print` only, default on, `CLAUDE_SHIM_RETRY=0` disables): on an
|
|
319
358
|
auth- or rate-limit-looking failure the shim retries once on a different account and
|
|
320
359
|
marks the failed one — a **10-minute cooldown** for a rate limit (it heals on its own),
|
|
@@ -327,149 +366,22 @@ and ≥2 accounts are eligible; service-spawned pipes take the plain exec path u
|
|
|
327
366
|
|
|
328
367
|
## Codex support (OpenAI Codex CLI)
|
|
329
368
|
|
|
330
|
-
|
|
331
|
-
above about the claude pool holds for the codex pool with these translations:
|
|
332
|
-
|
|
333
|
-
| Claude side | Codex side |
|
|
334
|
-
| --- | --- |
|
|
335
|
-
| `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
|
|
336
|
-
| pool `~/.claude-accounts` | pool `~/.codex-accounts` |
|
|
337
|
-
| `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
|
|
338
|
-
| `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
|
|
339
|
-
| `claude -p` auto-retry | `codex exec` auto-retry |
|
|
340
|
-
| Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
|
|
341
|
-
| `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
|
|
342
|
-
|
|
343
|
-
Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
|
|
344
|
-
then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
|
|
345
|
-
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
|
|
346
|
-
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
|
|
347
|
-
safety guards. The `codex` shim engages the buffered auto-retry only for
|
|
348
|
-
`codex exec` runs with finite stdin, exactly like `-p` on the claude side.
|
|
349
|
-
|
|
350
|
-
Codex-specific notes:
|
|
351
|
-
|
|
352
|
-
- **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
|
|
353
|
-
a URL + one-time code you can open in ANY browser (this machine, your laptop, a
|
|
354
|
-
phone), so it works identically on a local Mac, over SSH, and on servers — there
|
|
355
|
-
is no portable setup-token equivalent for Codex, so a server account is signed in
|
|
356
|
-
ON the server the same way. `--browser` opts into the localhost browser-callback
|
|
357
|
-
flow instead (only works when the browser runs on the same machine — the callback
|
|
358
|
-
goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
|
|
359
|
-
the account dir, and identity is verified offline from the id-token before
|
|
360
|
-
anything is registered.
|
|
361
|
-
- **`auth.json` is never synced** in either direction, for the same reason
|
|
362
|
-
`.credentials.json` never is: the refresh token rotates, and two machines
|
|
363
|
-
refreshing one grant strand each other. Sync pushes manifest + `config.toml`
|
|
364
|
-
seeds + advisory limit state only.
|
|
365
|
-
- **Windows are classified by length**, not by name: any window ≤6h counts as the
|
|
366
|
-
self-healing session signal, anything longer as durable/weekly. A hard
|
|
367
|
-
`limit_reached`/`allowed:false` verdict marks the account even if no window shows
|
|
368
|
-
≥90%, and a reshaped payload is still scanned recursively for window-shaped
|
|
369
|
-
objects (fail open if nothing parses).
|
|
370
|
-
- **Idle-account telemetry** stays fresh the same way: a long-expired access token is
|
|
371
|
-
renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
|
|
372
|
-
own public client id) and the rotated credential is persisted 0600. Overrides for
|
|
373
|
-
tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
|
|
374
|
-
`CODEX_MULTIACC_USAGE_URL`.
|
|
375
|
-
- **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
|
|
376
|
-
when the backend reports the limit finished. The limits refresher checks the reset
|
|
377
|
-
credits belonging to that same account, uses the soonest-expiring available credit,
|
|
378
|
-
and writes a per-account/window idempotency key before redemption so a lost response
|
|
379
|
-
or simultaneous fleet poll cannot spend a second reset. Set
|
|
380
|
-
`CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
|
|
381
|
-
A finished-limit verdict takes precedence over a conflicting percentage. The
|
|
382
|
-
redemption key excludes unused model windows whose reset clocks move between reads.
|
|
383
|
-
Eligible accounts, including spent ones, check the credit endpoint every five
|
|
384
|
-
minutes even if the usage summary still says zero credits. A newly granted reset
|
|
385
|
-
is redeemed during that pass and clears the old limit; no-credit results are
|
|
386
|
-
checked again next pass, without waiting for the weekly reset date.
|
|
387
|
-
All authenticated accounts also report `usage.reset_credits_available` and
|
|
388
|
-
`usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
|
|
389
|
-
The count comes from the credit endpoint, including below 95% used and with
|
|
390
|
-
automatic redemption disabled. The scheduled usage cadence bounds these reads.
|
|
391
|
-
Redemption shares that read, then reads back the remaining count after a POST;
|
|
392
|
-
no subtraction or embedded usage summary substitutes for provider evidence.
|
|
393
|
-
Zero means no available resets; missing fields mean the read was unavailable.
|
|
394
|
-
Credit identifiers remain local and selection does not use these fields.
|
|
395
|
-
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
396
|
-
addon's no-API-keys rule.
|
|
397
|
-
|
|
398
|
-
```bash
|
|
399
|
-
codex-accounts add # sign in a new ChatGPT account (device code —
|
|
400
|
-
# open the URL in any browser, enter the code)
|
|
401
|
-
codex-accounts add --browser # localhost browser-callback flow instead
|
|
402
|
-
codex-accounts import <email> --id acct-01 && codex-accounts adopt acct-01
|
|
403
|
-
# adopt this machine's existing ~/.codex login
|
|
404
|
-
codex-accounts list | status | expired | relogin | verify | limits | health | sync
|
|
405
|
-
claude-multiacc codex <cmd> # same commands via the npm wrapper
|
|
406
|
-
```
|
|
369
|
+
Both pools use the same selection policy. See [Codex account commands and behavior](docs/CODEX.md).
|
|
407
370
|
|
|
408
371
|
## Install / update / uninstall
|
|
409
372
|
|
|
410
|
-
### npm (recommended)
|
|
411
|
-
|
|
412
|
-
```bash
|
|
413
|
-
npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
|
|
414
|
-
# or run it once without installing globally:
|
|
415
|
-
npx claude-multiacc
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
`claude-multiacc` then wraps everything:
|
|
419
|
-
|
|
420
373
|
```bash
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
claude-multiacc uninstall [--purge-data]
|
|
424
|
-
claude-multiacc list | status | add <email> | ... # passthrough to claude-accounts
|
|
374
|
+
npm install -g claude-multiacc@latest
|
|
375
|
+
codex-accounts verify
|
|
425
376
|
```
|
|
426
377
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
reinstall` for git checkouts; set `CLAUDE_MULTIACC_AUTOUPDATE=0` before install to
|
|
432
|
-
opt out. `update-notifier` also prints a one-line nudge when a newer version is on npm.
|
|
433
|
-
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
|
|
434
|
-
`claude-multiacc install` once.)
|
|
435
|
-
|
|
436
|
-
New versions publish to npm automatically from `main` via GitHub Actions
|
|
437
|
-
(`.github/workflows/publish.yml`, OIDC Trusted Publishing — no long-lived token after the
|
|
438
|
-
first bootstrap publish; `scripts/auto-version.mjs` bumps the patch above the last release).
|
|
439
|
-
|
|
440
|
-
### From a git checkout
|
|
441
|
-
|
|
442
|
-
```bash
|
|
443
|
-
git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
|
|
444
|
-
./install.sh --server root@1.2.3.4 # override sync target
|
|
445
|
-
git pull && ./install.sh # update (data untouched)
|
|
446
|
-
./install.sh --uninstall # restores stock behavior, keeps account data
|
|
447
|
-
./install.sh --uninstall --purge-data # also deletes ~/.claude-accounts
|
|
448
|
-
```
|
|
378
|
+
npm installs `claude-multiacc`, `claude-accounts`, `codex-accounts`, and `multiacc-select`
|
|
379
|
+
into its global bin directory, so account commands work in the current shell. Open a
|
|
380
|
+
new shell for the `claude` and `codex` shims. If auto-setup was skipped, run
|
|
381
|
+
`claude-multiacc install` to initialize the pools and shell setup.
|
|
449
382
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
**Servers / machines without repo access:** install via **npm** — the registry is
|
|
453
|
-
public, so the daily self-update needs no credentials at all (no rsync, no keys).
|
|
454
|
-
A git checkout self-updates too, but only if `git pull` can authenticate; for a
|
|
455
|
-
private repo on a server that means a read-only deploy key (the 138 server runs
|
|
456
|
-
this way: `core.sshCommand` pinned to its deploy key). A plain copied tree is the
|
|
457
|
-
one layout that can NOT self-update — don't ship the addon that way.
|
|
458
|
-
|
|
459
|
-
What install does (all reversible, nothing else):
|
|
460
|
-
|
|
461
|
-
- **macOS:** marked PATH block at the END of `~/.zshenv`, `~/.zprofile`, `~/.zshrc`
|
|
462
|
-
(+ bash rc files if present) — end-of-file placement matters because those files
|
|
463
|
-
re-prepend `~/.local/bin`; launchd agents `com.claude-multiacc.limits` +
|
|
464
|
-
`.codex-limits` (5m), `.health` + `.codex-health` (weekly Mon morning), and
|
|
465
|
-
`.update` (daily 04:07). Notes when this Mac keeps Claude Code logins in the
|
|
466
|
-
Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for
|
|
467
|
-
accounts that must work from everywhere).
|
|
468
|
-
- **Linux (root):** PATH block in `~/.bashrc` + `/etc/profile.d/claude-multiacc.sh`,
|
|
469
|
-
shim symlinks at `/usr/local/bin/claude` and `/usr/local/bin/codex` (shadow via
|
|
470
|
-
PATH order — on the systemd default PATH too; the original binaries are untouched),
|
|
471
|
-
cron entries for limits/health (both providers) + the daily auto-update.
|
|
472
|
-
- Both: `~/.claude-accounts/` skeleton + `accounts.json` manifest.
|
|
383
|
+
See [installation, updates, removal, and command-not-found recovery](docs/INSTALLATION.md)
|
|
384
|
+
for npm, npx, git checkout, and server instructions.
|
|
473
385
|
|
|
474
386
|
Account lifecycle, synchronization, fleet integration, environment switches,
|
|
475
387
|
verification, troubleshooting, and security guidance continue in
|
package/bin/claude-accounts
CHANGED
|
@@ -86,8 +86,7 @@ USAGE
|
|
|
86
86
|
Skips accounts fetched in the last 45s and honors 429/refresh backoff;
|
|
87
87
|
--force ignores all three.
|
|
88
88
|
claude-accounts health limits + full verify; logs to health.log
|
|
89
|
-
claude-accounts self-update update
|
|
90
|
-
pull + reinstall); logs to update.log
|
|
89
|
+
claude-accounts self-update update this npm/git install; logs to update.log
|
|
91
90
|
claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
92
91
|
|
|
93
92
|
ENV
|
|
@@ -103,7 +102,7 @@ ENV
|
|
|
103
102
|
EOF
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run install
|
|
105
|
+
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
|
|
107
106
|
|
|
108
107
|
next_id() {
|
|
109
108
|
local n=1 id
|
package/bin/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// claude-multiacc npm entrypoint. Thin dispatcher over the bash addon:
|
|
3
3
|
// claude-multiacc [install] run install.sh (install or update, idempotent)
|
|
4
4
|
// claude-multiacc uninstall [--purge-data]
|
|
5
|
-
// claude-multiacc self-update
|
|
5
|
+
// claude-multiacc self-update update the running npm/git installation
|
|
6
6
|
// claude-multiacc doctor environment + auth summary
|
|
7
7
|
// claude-multiacc <accounts...> passthrough to claude-accounts (list/status/add/...)
|
|
8
8
|
// A globally-installed package auto-runs install.sh on (re)install via postinstall, so
|
|
@@ -59,10 +59,10 @@ mirror to a deploy server. No API keys — Claude and ChatGPT subscription login
|
|
|
59
59
|
USAGE
|
|
60
60
|
claude-multiacc [install] install or update the addon (idempotent)
|
|
61
61
|
claude-multiacc uninstall [--purge-data]
|
|
62
|
-
claude-multiacc self-update update
|
|
62
|
+
claude-multiacc self-update update this npm/git installation + re-install
|
|
63
63
|
claude-multiacc doctor show environment + account health (both pools)
|
|
64
64
|
claude-multiacc <cmd> [args...] run a claude-accounts command, e.g.
|
|
65
|
-
list | status | add
|
|
65
|
+
list | status | add [email] | login <acct-NN> |
|
|
66
66
|
remove <acct-NN> | sync | verify | limits | health
|
|
67
67
|
claude-multiacc codex <cmd> [args...] same commands for the Codex pool
|
|
68
68
|
(runs codex-accounts <cmd>)
|
|
@@ -71,6 +71,12 @@ USAGE
|
|
|
71
71
|
claude-multiacc -v | --version
|
|
72
72
|
claude-multiacc -h | --help
|
|
73
73
|
|
|
74
|
+
ACCOUNT COMMANDS (also installed directly by npm)
|
|
75
|
+
claude-accounts verify verify the Claude pool
|
|
76
|
+
codex-accounts verify verify the Codex pool
|
|
77
|
+
claude-accounts --help all Claude account commands and options
|
|
78
|
+
codex-accounts --help all Codex account commands and options
|
|
79
|
+
|
|
74
80
|
INSTALL
|
|
75
81
|
npm install -g claude-multiacc # postinstall wires up the PATH shims + schedules
|
|
76
82
|
# or, no global install:
|
|
@@ -91,23 +97,14 @@ switch (cmd) {
|
|
|
91
97
|
case 'uninstall':
|
|
92
98
|
status = runInstall(['--uninstall', ...argv.slice(1)]);
|
|
93
99
|
break;
|
|
94
|
-
case 'self-update':
|
|
95
|
-
|
|
96
|
-
const up = sh('npm', ['install', '-g', `${pkg.name}@latest`]);
|
|
97
|
-
if (up !== 0) {
|
|
98
|
-
console.error(
|
|
99
|
-
'npm update failed — is this a global npm install? '
|
|
100
|
-
+ '(git checkouts update with: git pull && ./install.sh)',
|
|
101
|
-
);
|
|
102
|
-
status = up;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
// The freshly-installed global package re-runs its own postinstall, so the addon
|
|
106
|
-
// is already reconciled; nothing else to do here.
|
|
107
|
-
console.log('Updated. Open a new shell to pick up any PATH changes.');
|
|
100
|
+
case 'self-update':
|
|
101
|
+
status = runAccounts(argv);
|
|
108
102
|
break;
|
|
109
|
-
}
|
|
110
103
|
case 'doctor': {
|
|
104
|
+
if (argv.slice(1).some(arg => arg === '--help' || arg === '-h')) {
|
|
105
|
+
console.log(HELP);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
111
108
|
console.log('== claude pool ==');
|
|
112
109
|
const claudeStatus = runAccounts(['status']);
|
|
113
110
|
console.log('\n== codex pool ==');
|
|
@@ -128,6 +125,7 @@ switch (cmd) {
|
|
|
128
125
|
break;
|
|
129
126
|
case '-h':
|
|
130
127
|
case '--help':
|
|
128
|
+
case 'help':
|
|
131
129
|
console.log(HELP);
|
|
132
130
|
break;
|
|
133
131
|
default:
|
package/bin/codex
CHANGED
|
@@ -122,6 +122,127 @@ sel_log() {
|
|
|
122
122
|
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" 2>/dev/null >> "$ACC_ROOT/selection.log" || true
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
# ---- one index for the shared session tree -----------------------------------------
|
|
126
|
+
# codex keeps a SQLite index of the rollout tree beside it (state_<schema>.sqlite) and
|
|
127
|
+
# refuses to start until that index has been BACKFILLED from every rollout under
|
|
128
|
+
# $CODEX_HOME/sessions — each file read whole. The installed layout shares one tree
|
|
129
|
+
# across every account (<acct>/sessions -> ~/.codex/sessions), so a private index per
|
|
130
|
+
# account meant one full scan of the whole tree per account: 10 GB × 12 on a busy Mac,
|
|
131
|
+
# minutes each, and a launch that met another process's unfinished scan died after 30 s
|
|
132
|
+
# with "timed out waiting for state db backfill … (status: running)" — surfaced as the
|
|
133
|
+
# CLI's damaged-database message. A scan cut short (a probe with a timeout, Ctrl-C) also
|
|
134
|
+
# left its 15-minute worker lease behind, so the account stayed unstartable long after
|
|
135
|
+
# its killer was gone (my-mini 2026-09-09: six fresh accounts, every launch refused).
|
|
136
|
+
#
|
|
137
|
+
# A shared tree gets ONE shared index: <acct>/state_N.sqlite is a symlink to the same
|
|
138
|
+
# file under ~/.codex, exactly like the tree it describes. SQLite resolves that symlink
|
|
139
|
+
# before naming its -wal/-shm companions, so every process shares one lock set — the
|
|
140
|
+
# multi-process mode a single CODEX_HOME already runs in. A private index that already
|
|
141
|
+
# exists and nothing holds open is retired beside the link (never deleted), or promoted
|
|
142
|
+
# into the home when the home has none yet; and the index the installed binary will
|
|
143
|
+
# create next is linked ahead of time, so a schema bump still costs one backfill, not one
|
|
144
|
+
# per account. Structural on purpose: a real (private) session tree keeps a private index.
|
|
145
|
+
# lib/common.sh carries the same function for seeding — keep the two in step.
|
|
146
|
+
# CORE — byte-identical in lib/common.sh; tests diff them.
|
|
147
|
+
state_index_names() { # $1 acct dir, $2 home dir -> the index file names either side holds
|
|
148
|
+
local f name seen=" "
|
|
149
|
+
for f in "$2"/state_[0-9]*.sqlite "$1"/state_[0-9]*.sqlite; do
|
|
150
|
+
[ -e "$f" ] || [ -L "$f" ] || continue # an unmatched glob is the pattern itself
|
|
151
|
+
name="${f##*/}"
|
|
152
|
+
case "$seen" in *" $name "*) continue ;; esac
|
|
153
|
+
seen="$seen$name "
|
|
154
|
+
printf '%s\n' "$name"
|
|
155
|
+
done
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
installed_state_index_name() { # the index file the installed binary creates; memoized per binary
|
|
159
|
+
local cache="$ACC_ROOT/.state-index" bin="$REAL" real id line name="" nat
|
|
160
|
+
# macOS `stat` reports a SYMLINK's own mtime, and codex is usually installed behind a
|
|
161
|
+
# stable launcher symlink — so the memo has to key on the file the link resolves to.
|
|
162
|
+
real="$(canon_path "$bin")"
|
|
163
|
+
id="$real:$(file_mtime "$real")"
|
|
164
|
+
if [ -f "$cache" ]; then
|
|
165
|
+
IFS= read -r line < "$cache" 2>/dev/null || line=""
|
|
166
|
+
case "$line" in "$id "*) printf '%s\n' "${line#"$id "}"; return 0 ;; esac
|
|
167
|
+
fi
|
|
168
|
+
# The npm launcher is a script; the schema name lives in the native binary vendored
|
|
169
|
+
# beside it. Try the launcher first (a bare binary answers directly), then the vendor.
|
|
170
|
+
for nat in "$bin" "$(dirname "$(canon_path "$bin")")"/../node_modules/@openai/codex-*/vendor/*/bin/codex; do
|
|
171
|
+
[ -f "$nat" ] || continue
|
|
172
|
+
name="$(LC_ALL=C grep -a -o -m1 'state_[0-9][0-9]*\.sqlite' "$nat" 2>/dev/null | head -1)"
|
|
173
|
+
[ -n "$name" ] && break
|
|
174
|
+
done
|
|
175
|
+
case "$name" in *[!A-Za-z0-9_.]*) name="" ;; esac
|
|
176
|
+
{ printf '%s %s\n' "$id" "$name" > "$cache.$$" && mv -f "$cache.$$" "$cache"; } 2>/dev/null \
|
|
177
|
+
|| rm -f "$cache.$$" 2>/dev/null
|
|
178
|
+
printf '%s\n' "$name"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# CORE — byte-identical in lib/common.sh; tests diff them.
|
|
182
|
+
shared_index_rejected() { # $1 acct dir, $2 index name, $3 shared target
|
|
183
|
+
# True when codex's own corruption recovery moved THIS account's link out of the way:
|
|
184
|
+
# it renames the database it judged damaged (and its -wal/-shm) into
|
|
185
|
+
# <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, and under a link that renames
|
|
186
|
+
# the LINK. A link sitting in there pointing at the shared file is codex's verdict on
|
|
187
|
+
# that file, so this account keeps the index codex rebuilt for it and this name is
|
|
188
|
+
# left alone — handing the link back would hand the damage back.
|
|
189
|
+
local marker
|
|
190
|
+
for marker in "$1"/db-backups/*/"$2"; do
|
|
191
|
+
[ -L "$marker" ] || continue
|
|
192
|
+
[ "$(readlink "$marker" 2>/dev/null)" = "$3" ] && return 0
|
|
193
|
+
done
|
|
194
|
+
return 1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# CORE — byte-identical in lib/common.sh; tests diff them.
|
|
198
|
+
share_state_index_links() { # $1 acct dir, $2 home dir, $3 extra index name ('' for none)
|
|
199
|
+
local d="$1" home="$2" name names target link retired n
|
|
200
|
+
[ -L "$d" ] && return 0 # adopted: the dir IS the home
|
|
201
|
+
[ -L "$d/sessions" ] || return 0 # a private tree keeps its private index
|
|
202
|
+
[ "$(readlink "$d/sessions" 2>/dev/null)" = "$home/sessions" ] || return 0
|
|
203
|
+
[ -d "$home" ] || return 0
|
|
204
|
+
names="$(state_index_names "$d" "$home")"
|
|
205
|
+
case "${3:-}" in ''|*[!A-Za-z0-9_.]*) ;; *) names="$names $3" ;; esac
|
|
206
|
+
for name in $names; do
|
|
207
|
+
target="$home/$name"; link="$d/$name"
|
|
208
|
+
[ -L "$link" ] && continue # already shared (or pointed elsewhere on purpose)
|
|
209
|
+
shared_index_rejected "$d" "$name" "$target" && continue
|
|
210
|
+
if [ ! -e "$link" ]; then
|
|
211
|
+
ln -s "$target" "$link" 2>/dev/null || true # a new account: the whole point
|
|
212
|
+
continue
|
|
213
|
+
fi
|
|
214
|
+
# This account has an index of its own, and MOVING one that a process can still open
|
|
215
|
+
# is unsafe. codex holds the state database through an sqlx pool that opens its
|
|
216
|
+
# connections lazily and BY PATH (max_connections(5), create_if_missing(true) —
|
|
217
|
+
# codex-rs/state/src/sqlite.rs open_read_write_pool), so a rename under a live holder
|
|
218
|
+
# leaves connection 1 on the old inode while every connection the pool opens
|
|
219
|
+
# afterwards follows the new link: one process, two databases. A -wal or a -shm beside
|
|
220
|
+
# the file is that proof. A codex killed mid-write leaves them behind too, and the
|
|
221
|
+
# next clean session on the account removes them, so this heals itself in time.
|
|
222
|
+
{ [ -e "$link-wal" ] || [ -e "$link-shm" ]; } && continue
|
|
223
|
+
if [ -e "$target" ]; then
|
|
224
|
+
retired="$link.private"; n=0 # never overwrite an earlier copy
|
|
225
|
+
while [ -e "$retired" ] && [ "$n" -lt 100 ]; do n=$((n+1)); retired="$link.private.$n"; done
|
|
226
|
+
[ -e "$retired" ] && continue
|
|
227
|
+
mv "$link" "$retired" 2>/dev/null || continue
|
|
228
|
+
else
|
|
229
|
+
# Atomic or nothing: link(2) refuses an existing target, so two shims racing to be
|
|
230
|
+
# the first to promote cannot rename one's fresh symlink onto the file the other
|
|
231
|
+
# just promoted — which is how a self-referential shared index (ELOOP, and every
|
|
232
|
+
# account on the Mac unable to start) could appear. A cross-device link simply
|
|
233
|
+
# fails and this account keeps its own index.
|
|
234
|
+
ln "$link" "$target" 2>/dev/null || continue
|
|
235
|
+
[ -L "$link" ] || rm -f "$link"
|
|
236
|
+
fi
|
|
237
|
+
ln -s "$target" "$link" 2>/dev/null || true
|
|
238
|
+
done
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
share_state_index() { # $1 acct dir — at launch: the home's index, plus the one this binary will create
|
|
243
|
+
share_state_index_links "$1" "${HOME:-/nonexistent}/.codex" "$(installed_state_index_name)"
|
|
244
|
+
}
|
|
245
|
+
|
|
125
246
|
marker_active() { # true if $1/.limited is still in force; clears cleanly-expired markers
|
|
126
247
|
# Parity with bin/claude's client_marker_recovered, by construction: this shim has NO
|
|
127
248
|
# telemetry-based clearing path. A marker leaves here only when its OWN reset epoch has
|
|
@@ -521,6 +642,7 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
|
|
|
521
642
|
d="$ACC_ROOT/$CODEX_ACCOUNT"
|
|
522
643
|
if [ -d "$d" ]; then
|
|
523
644
|
sel_log "$CODEX_ACCOUNT pinned pwd=$PWD"
|
|
645
|
+
share_state_index "$d"
|
|
524
646
|
export CODEX_HOME="$d"
|
|
525
647
|
export CODEX_SHIM_ACTIVE=1
|
|
526
648
|
exec "$REAL" "$@"
|
|
@@ -805,6 +927,7 @@ if [ "${CODEX_SHIM_RETRY:-1}" != "0" ] && [ "${#eligible[@]}" -ge 2 ]; then
|
|
|
805
927
|
fi
|
|
806
928
|
fi
|
|
807
929
|
|
|
930
|
+
share_state_index "$pick"
|
|
808
931
|
if [ "$wants_retry" = "0" ]; then
|
|
809
932
|
export CODEX_HOME="$pick"
|
|
810
933
|
exec "$REAL" "$@"
|
|
@@ -906,6 +1029,7 @@ while :; do
|
|
|
906
1029
|
if [ -n "$next" ]; then
|
|
907
1030
|
sel_log "retry from=$(basename "$cur") to=$(basename "$next") rc=$rc"
|
|
908
1031
|
cur="$next"
|
|
1032
|
+
share_state_index "$cur"
|
|
909
1033
|
# The account that actually serves the work is the one the next run should rotate
|
|
910
1034
|
# away from — not the one that bounced.
|
|
911
1035
|
remember_pick "$cur"
|
package/bin/codex-accounts
CHANGED
|
@@ -90,8 +90,7 @@ USAGE
|
|
|
90
90
|
stay selectable. Skips accounts fetched in the last 45s and honors
|
|
91
91
|
429/refresh backoff; --force ignores all three.
|
|
92
92
|
codex-accounts health limits + full verify; logs to health.log
|
|
93
|
-
codex-accounts self-update update
|
|
94
|
-
pull + reinstall); logs to update.log
|
|
93
|
+
codex-accounts self-update update this npm/git install; logs to update.log
|
|
95
94
|
codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
96
95
|
|
|
97
96
|
ENV
|
|
@@ -108,7 +107,7 @@ ENV
|
|
|
108
107
|
EOF
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run install
|
|
110
|
+
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
|
|
112
111
|
|
|
113
112
|
next_id() {
|
|
114
113
|
local n=1 id
|
package/bin/multiacc-select
CHANGED
|
@@ -30,6 +30,10 @@ def _strict_object(pairs: list[tuple[str, object]]) -> dict:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def main() -> int:
|
|
33
|
+
if sys.argv[1:] in (["--help"], ["-h"]):
|
|
34
|
+
print("usage: multiacc-select --request-json - --response-json -\n"
|
|
35
|
+
" multiacc-select --version")
|
|
36
|
+
return 0
|
|
33
37
|
if sys.argv[1:] == ["--version"]:
|
|
34
38
|
print(SELECTOR_VERSION)
|
|
35
39
|
return 0
|
|
@@ -370,65 +370,7 @@ The codex shim honors the same switches spelled `CODEX_*`: `CODEX_ACCOUNT`,
|
|
|
370
370
|
300s — weekly markers never clear early), `CODEX_ACCOUNTS_ROOT` (legacy
|
|
371
371
|
`CODEX_ACCOUNTS_DIR`), `CODEX_MULTIACC_SYNC_TARGET`, `CODEX_MULTIACC_THRESHOLD`.
|
|
372
372
|
|
|
373
|
-
## Verification
|
|
373
|
+
## Verification and troubleshooting
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
|
|
378
|
-
claude-accounts verify --quick# auth presence/expiry only, no inference
|
|
379
|
-
claude-accounts limits # live per-bucket usage incl. the Fable bucket
|
|
380
|
-
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
|
|
384
|
-
spread clean, real project `-p` through the shim OK on Mac (`~/pipeline`) and server
|
|
385
|
-
(`/opt/inapp-onboarding`), uninstall→stock→reinstall roundtrip byte-identical, three
|
|
386
|
-
server systemd services (`vpn-server-management`, `inapp-onboarding`, `vpn-seo-panel`)
|
|
387
|
-
repointed via `CLAUDE_BIN=/usr/local/bin/claude` and restarted healthy.
|
|
388
|
-
|
|
389
|
-
## Troubleshooting
|
|
390
|
-
|
|
391
|
-
- **`claude` resolves to the real binary, not the shim** — open a new shell, or check
|
|
392
|
-
that the marked block is the LAST PATH manipulation in your rc file
|
|
393
|
-
(`grep -A2 'claude-multiacc >>>' ~/.zshrc`).
|
|
394
|
-
- **An account never gets picked** — `claude-accounts status`: no auth on this machine,
|
|
395
|
-
a dead login (`selectable: NO`, see `claude-accounts expired`), or an active `.limited`
|
|
396
|
-
marker (shows bucket + minutes to reset).
|
|
397
|
-
- **"Failed to authenticate: OAuth session expired and could not be refreshed"** — the
|
|
398
|
-
login behind that run is dead. Run `claude-accounts expired` to see which, then
|
|
399
|
-
`claude-accounts relogin`. Accounts in that state are excluded from selection
|
|
400
|
-
automatically, so this only surfaces when *every* account needs a re-login (the shim
|
|
401
|
-
then falls back to the machine's own `~/.claude` login) or when it was pinned with
|
|
402
|
-
`CLAUDE_ACCOUNT`.
|
|
403
|
-
- **"Please run /login · API Error: 401 OAuth access token is invalid"** — the portable
|
|
404
|
-
setup-token is rejected even though its presence and `claude auth status` look healthy.
|
|
405
|
-
The shim now runs a private first-use inference, parks a rejected token, and reselects
|
|
406
|
-
before direct, TUI, or `--resume` work sees the 401. Run `claude-accounts verify` to
|
|
407
|
-
check every token immediately; `claude-accounts expired` reports token-only accounts
|
|
408
|
-
as `UNVERIFIED` until that proof exists.
|
|
409
|
-
- **Everything marked limited** — the shim still runs: the still-serving limited accounts
|
|
410
|
-
go through the same two cuts (session gate, then strict best-weekly) and one is handed
|
|
411
|
-
out anyway; check `selection.log` for `all-limited fallback=` lines.
|
|
412
|
-
- **Sync fails** — `tail ~/.claude-accounts/sync.log`; it's ssh/rsync to the manifest's
|
|
413
|
-
`server` (BatchMode — needs key auth).
|
|
414
|
-
- **A service bypasses the shim** — it spawns an absolute path. Point its env
|
|
415
|
-
(`CLAUDE_BIN` etc.) at `/usr/local/bin/claude`. Docker-internal `claude` installs
|
|
416
|
-
(e.g. openclaw live-test containers) are out of scope for a host shim.
|
|
417
|
-
- **`add` sign-in says "Sign in again to continue"** — that's Claude's step-up
|
|
418
|
-
authentication, not a tool error: granting a long-lived token needs a *recent* sign-in.
|
|
419
|
-
Open a fresh incognito window, sign in to the target account at claude.ai **first**,
|
|
420
|
-
then paste the link and approve. The approval scope is `user:inference`
|
|
421
|
-
(*"Contribute to your Claude subscription usage"*) — that's correct and minimal.
|
|
422
|
-
- **Limits look stale** — launchd: `launchctl list | grep claude-multiacc`;
|
|
423
|
-
cron: `crontab -l | grep claude-multiacc`. The shim also kicks a refresh when data
|
|
424
|
-
is >3 min old.
|
|
425
|
-
|
|
426
|
-
## Security notes & known risks
|
|
427
|
-
|
|
428
|
-
- Tokens and credentials live as 0600 files under `~/.claude-accounts` (and
|
|
429
|
-
`/root/.claude-accounts` on the server). Server compromise = account access.
|
|
430
|
-
- `selection.log` records timestamps/account/cwd only — never prompt text.
|
|
431
|
-
- Rotating multiple subscriptions to spread usage may be flagged by anti-abuse systems;
|
|
432
|
-
accounts can be banned for limit circumvention. Known and accepted by the operator.
|
|
433
|
-
- The usage endpoint is the internal one `/usage` consumes; if it changes shape, limit
|
|
434
|
-
tracking fails open (accounts stay available, error-retry is the backstop).
|
|
375
|
+
See [verification commands and troubleshooting](VERIFICATION.md), including the
|
|
376
|
+
packed npm command checks, account health, shell PATH setup, and security notes.
|
package/docs/CODEX.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Codex support (OpenAI Codex CLI)
|
|
2
|
+
|
|
3
|
+
Codex uses the same machinery as the [Claude pool](../README.md), with these translations:
|
|
4
|
+
|
|
5
|
+
| Claude side | Codex side |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
|
|
8
|
+
| pool `~/.claude-accounts` | pool `~/.codex-accounts` |
|
|
9
|
+
| `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
|
|
10
|
+
| `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
|
|
11
|
+
| `claude -p` auto-retry | `codex exec` auto-retry |
|
|
12
|
+
| Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
|
|
13
|
+
| `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |
|
|
14
|
+
|
|
15
|
+
Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
|
|
16
|
+
then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
|
|
17
|
+
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
|
|
18
|
+
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
|
|
19
|
+
safety guards. The `codex` shim engages the buffered auto-retry only for
|
|
20
|
+
`codex exec` runs with finite stdin, exactly like `-p` on the claude side.
|
|
21
|
+
|
|
22
|
+
Codex-specific notes:
|
|
23
|
+
|
|
24
|
+
- **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
|
|
25
|
+
a URL + one-time code you can open in ANY browser (this machine, your laptop, a
|
|
26
|
+
phone), so it works identically on a local Mac, over SSH, and on servers — there
|
|
27
|
+
is no portable setup-token equivalent for Codex, so a server account is signed in
|
|
28
|
+
ON the server the same way. `--browser` opts into the localhost browser-callback
|
|
29
|
+
flow instead (only works when the browser runs on the same machine — the callback
|
|
30
|
+
goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
|
|
31
|
+
the account dir, and identity is verified offline from the id-token before
|
|
32
|
+
anything is registered.
|
|
33
|
+
- **`auth.json` is never synced** in either direction, for the same reason
|
|
34
|
+
`.credentials.json` never is: the refresh token rotates, and two machines
|
|
35
|
+
refreshing one grant strand each other. Sync pushes manifest + `config.toml`
|
|
36
|
+
seeds + advisory limit state only.
|
|
37
|
+
- **Windows are classified by length**, not by name: any window ≤6h counts as the
|
|
38
|
+
self-healing session signal, anything longer as durable/weekly. A hard
|
|
39
|
+
`limit_reached`/`allowed:false` verdict marks the account even if no window shows
|
|
40
|
+
≥90%, and a reshaped payload is still scanned recursively for window-shaped
|
|
41
|
+
objects (fail open if nothing parses).
|
|
42
|
+
- **Idle-account telemetry** stays fresh the same way: a long-expired access token is
|
|
43
|
+
renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
|
|
44
|
+
own public client id) and the rotated credential is persisted 0600. Overrides for
|
|
45
|
+
tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
|
|
46
|
+
`CODEX_MULTIACC_USAGE_URL`.
|
|
47
|
+
- **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
|
|
48
|
+
when the backend reports the limit finished. The limits refresher checks the reset
|
|
49
|
+
credits belonging to that same account, uses the soonest-expiring available credit,
|
|
50
|
+
and writes a per-account/window idempotency key before redemption so a lost response
|
|
51
|
+
or simultaneous fleet poll cannot spend a second reset. Set
|
|
52
|
+
`CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
|
|
53
|
+
A finished-limit verdict takes precedence over a conflicting percentage. The
|
|
54
|
+
redemption key excludes unused model windows whose reset clocks move between reads.
|
|
55
|
+
Eligible accounts, including spent ones, check the credit endpoint every five
|
|
56
|
+
minutes even if the usage summary still says zero credits. A newly granted reset
|
|
57
|
+
is redeemed during that pass and clears the old limit; no-credit results are
|
|
58
|
+
checked again next pass, without waiting for the weekly reset date.
|
|
59
|
+
All authenticated accounts also report `usage.reset_credits_available` and
|
|
60
|
+
`usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
|
|
61
|
+
The count comes from the credit endpoint, including below 95% used and with
|
|
62
|
+
automatic redemption disabled. The scheduled usage cadence bounds these reads.
|
|
63
|
+
Redemption shares that read, then reads back the remaining count after a POST;
|
|
64
|
+
no subtraction or embedded usage summary substitutes for provider evidence.
|
|
65
|
+
Zero means no available resets; missing fields mean the read was unavailable.
|
|
66
|
+
Credit identifiers remain local and selection does not use these fields.
|
|
67
|
+
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
68
|
+
addon's no-API-keys rule.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
codex-accounts add # sign in a new ChatGPT account (device code —
|
|
72
|
+
# open the URL in any browser, enter the code)
|
|
73
|
+
codex-accounts add --browser # localhost browser-callback flow instead
|
|
74
|
+
codex-accounts import you@example.com --id acct-01
|
|
75
|
+
codex-accounts adopt acct-01
|
|
76
|
+
# adopt this machine's existing ~/.codex login
|
|
77
|
+
codex-accounts list
|
|
78
|
+
codex-accounts status
|
|
79
|
+
codex-accounts verify --quick # check local auth without inference
|
|
80
|
+
codex-accounts verify # real codex exec call per account
|
|
81
|
+
codex-accounts --help # all account commands and options
|
|
82
|
+
claude-multiacc codex status # same status command via the npm wrapper
|
|
83
|
+
```
|