claude-multiacc 2.0.57 → 2.0.59

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.
Files changed (150) hide show
  1. package/README.md +111 -3
  2. package/bin/claude +323 -84
  3. package/bin/claude-accounts +273 -42
  4. package/bin/codex +15 -0
  5. package/bin/codex-accounts +31 -4
  6. package/docs/ACCOUNT_OPERATIONS.md +62 -2
  7. package/docs/AUTORESUME-HANDOFF.md +30 -3
  8. package/docs/AUTORESUME.md +275 -18
  9. package/docs/CODEX.md +1 -0
  10. package/docs/INSTALLATION.md +7 -1
  11. package/docs/VERIFICATION.md +4 -0
  12. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  29. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  30. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  34. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  35. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  36. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  37. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  38. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  39. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  40. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  41. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  42. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  43. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  44. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  45. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  46. package/lib/audit.py +88 -22
  47. package/lib/autoresume.py +1197 -82
  48. package/lib/autoresume_cross.py +3 -1
  49. package/lib/autoresume_handoff.py +82 -10
  50. package/lib/autoresume_probe.py +2 -2
  51. package/lib/claude_client_error.py +9 -4
  52. package/lib/codex_trust.py +263 -0
  53. package/lib/common.sh +131 -3
  54. package/lib/credential.py +5 -2
  55. package/lib/install_actions.sh +19 -0
  56. package/lib/keychain.py +180 -36
  57. package/lib/mcp_oauth.py +1748 -0
  58. package/lib/model_catalog.json +404 -0
  59. package/lib/model_catalog.py +201 -0
  60. package/lib/model_match.py +104 -0
  61. package/lib/report.py +8 -5
  62. package/lib/session_settings.py +123 -0
  63. package/package.json +2 -2
  64. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  65. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  68. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  69. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  101. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  102. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  108. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  109. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  111. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  112. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  113. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  116. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  117. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  121. package/tests/fake_codex_tui.py +25 -0
  122. package/tests/fake_security.sh +24 -2
  123. package/tests/fixtures/cross_provider_cli.py +111 -1
  124. package/tests/run-tests.sh +1028 -23
  125. package/tests/test_autoresume.py +215 -8
  126. package/tests/test_autoresume_catalog.py +226 -0
  127. package/tests/test_autoresume_catalog_cli.py +185 -0
  128. package/tests/test_autoresume_codex_records.py +16 -0
  129. package/tests/test_autoresume_codex_trust.py +247 -0
  130. package/tests/test_autoresume_cross_guards.py +28 -0
  131. package/tests/test_autoresume_cross_origin.py +70 -0
  132. package/tests/test_autoresume_cross_tmux.py +27 -2
  133. package/tests/test_autoresume_cross_tokens.py +32 -2
  134. package/tests/test_autoresume_handoff_argv.py +120 -16
  135. package/tests/test_autoresume_handoff_origins.py +67 -0
  136. package/tests/test_autoresume_memory.py +1141 -0
  137. package/tests/test_autoresume_memory_tmux.py +94 -0
  138. package/tests/test_autoresume_probe.py +2 -1
  139. package/tests/test_autoresume_relaunch_settings.py +317 -0
  140. package/tests/test_autoresume_settings.py +135 -0
  141. package/tests/test_autoresume_sidechain.py +1518 -0
  142. package/tests/test_autoresume_sidechain_tmux.py +79 -0
  143. package/tests/test_autoresume_trust_tmux.py +63 -0
  144. package/tests/test_claude_client_error.py +10 -7
  145. package/tests/test_claude_org_block.py +1 -0
  146. package/tests/test_claude_pool_prepare.py +4 -1
  147. package/tests/test_claude_reset.py +3 -0
  148. package/tests/test_keychain_nologin.py +340 -0
  149. package/tests/test_mcp_oauth.py +1414 -0
  150. package/tests/test_packaged_commands.py +41 -0
package/README.md CHANGED
@@ -59,6 +59,7 @@ MAC (source of truth) SERVER (mirror)
59
59
  server.token / .credentials.json
60
60
  (or the login lives in the macOS Keychain — see below)
61
61
  selection.log sync.log health.log /usr/local/bin/claude -> repo shim
62
+ mcp-oauth.json (MCP OAuth, never synced)
62
63
  repo bin/ first on PATH (rc block + hook) /root/claude-multiacc/ (addon repo)
63
64
  ```
64
65
 
@@ -297,6 +298,18 @@ account's `.credentials.json` (0600), or its macOS Keychain item.
297
298
  > identity checked out prunes every sibling; `remove` deletes every item.
298
299
  > `python3 lib/keychain.py prune <acct dir>` prunes by hand (only when the canonical item
299
300
  > is a live login; it never writes).
301
+ > **A login, not any credentials document.** The same document (file or Keychain item) also
302
+ > holds the client's MCP servers' OAuth state (`mcpOAuth`), and from ssh/tmux Claude Code
303
+ > writes a `.credentials.json` holding only that once an OAuth MCP server is discovered. The
304
+ > pool counts a document as a login only when it carries `claudeAiOauth`: one with MCP
305
+ > state alone leaves the account to its Keychain login or `server.token` (`list` shows
306
+ > `auth=token`, or `NO-LOGIN`), expiry is read from the login object alone, such a rewrite
307
+ > never heals an `.expired` park, and every login multiacc writes (refresh, migration,
308
+ > `import --creds`) lands INTO the document with its MCP state kept. The limits pass's
309
+ > refresh and migration write under the client's own `<acct>/.storage-write.lock`, never
310
+ > across the token request: a refresh that finds the lock held skips (the next pass
311
+ > retries), and the rotated login waits for a client write in flight instead of being
312
+ > rolled back by it.
300
313
  > Override: `CLAUDE_MULTIACC_KEYCHAIN=0` disables the lookup. This is what keeps idle accounts' telemetry fresh so they win
301
314
  selection over busy accounts; without it, stale telemetry ranks neutral and a truly-idle
302
315
  account would lose to a busy-but-fresh one. Refresh failures fail open and back off via
@@ -324,7 +337,10 @@ the session → account mapping comes from `<acct>/sessions/<pid>.json`, which t
324
337
  maintains only while a run is alive. The shim spawns a short detached poll before `exec`
325
338
  (the pid survives `exec`, so it knows the filename) that records the session id — with the
326
339
  session's own start time — in `<acct>/.sessions-index`; selection then tail-scans those
327
- transcripts.
340
+ transcripts. The poll also records which credential the run carries, the portable token
341
+ or the login, by PROCESS (`<acct>/.sessions-via/<pid>`, valid only while the registry's
342
+ `startedAt` matches), so a session the client switches the run to (`/clear`, `/resume`
343
+ inside the TUI) is charged to the same credential when it fails to authenticate.
328
344
 
329
345
  Fresh account seeding creates `~/.claude/projects` before linking each account, even
330
346
  if stock Claude has never run on that machine. For an existing pool with private
@@ -360,6 +376,12 @@ next selection. It has no automatic expiry; full `claude-accounts verify` must p
360
376
  access after the organization enables it. `.client-org-cleared` suppresses transcript
361
377
  errors recorded before that verification.
362
378
 
379
+ An interactive `authentication_failed` parks the credential that run carried. The poll
380
+ tags a session's index line `token` when the shim launched the run on the account's
381
+ portable token, and only such a failure retires the token (`setup-token-invalid`); any
382
+ other is the login's (`auth-error`) — a run that carried no token says nothing about it.
383
+ The verification watermark above supersedes these records too, so a PASS sticks.
384
+
363
385
  The resulting usage marker is tagged `reason=client-rate-limit` and outlives a clean telemetry
364
386
  pass while its window is still open — first-hand evidence beats a usage payload that
365
387
  disagrees. A **weekly** rejection is absolute: nothing — neither a shim invocation nor the
@@ -441,7 +463,8 @@ Interactive sessions are not retried this way. They get auto-resume, described n
441
463
  ## Auto-resume
442
464
 
443
465
  An interactive `claude` or `codex` session in **tmux** can stop on a usage limit, a failed
444
- login, an API error or (claude) a crash. It is then **resumed automatically, same session
466
+ login, an API error or (claude) a crash, or show Claude Code's "Critical memory usage"
467
+ warning. It is then **resumed automatically, same session
445
468
  id**, on another pooled account with headroom. If a usage limit exhausts that provider's
446
469
  usable accounts, it can continue in the other provider with instructions to read the
447
470
  original conversation's history. There is no `/exit`, no `--resume` and no
@@ -479,9 +502,26 @@ touch ~/.codex-accounts/autoresume.off # codex pool off
479
502
  CLAUDE_MULTIACC_AUTORESUME=0 claude # one launch (CODEX_MULTIACC_AUTORESUME=0 codex)
480
503
  ```
481
504
 
505
+ **Model and effort.** A relaunch keeps the session on the model and effort it last ran
506
+ with, read from the session's own history, so a `/model` or `/effort` switch made after the
507
+ launch counts. A continuation in the other provider starts on the model and effort of the
508
+ same level. Both read the **model catalog**: the copy this release ships, merged with the
509
+ model caches Claude Code and Codex keep in their account dirs, written to
510
+ `<pool>/model-catalog.json`. It sends no request and reads no credential. Every install and
511
+ `self-update` refreshes it in both pools, and each limits pass refreshes its own pool's copy
512
+ once it is an hour old, so the matching follows new models between releases.
513
+
514
+ ```bash
515
+ claude-accounts model-catalog show # the merged catalog a relaunch matches with
516
+ claude-accounts model-catalog refresh # rewrite both pools' copies now
517
+ ```
518
+
519
+ `CLAUDE_MULTIACC_MODEL_CATALOG=0` or `CODEX_MULTIACC_MODEL_CATALOG=0` stops the refresh of
520
+ that pool's copy, and `MULTIACC_MODEL_CATALOG=0` stops both.
521
+
482
522
  See [auto-resume: error classes, gates, budgets, logs and limitations](docs/AUTORESUME.md).
483
523
  See [continuing between Codex and Claude](docs/AUTORESUME-HANDOFF.md) for history,
484
- permission handling and the cross-provider opt-out.
524
+ permission handling, model matching and the cross-provider opt-out.
485
525
 
486
526
  ## MCP servers for every account
487
527
 
@@ -562,6 +602,74 @@ claude-accounts mcp apply # re-apply now (repair drift)
562
602
  seed and launch and the shim's mirror; `claude-accounts mcp …`, sync and the fan-out still
563
603
  work.
564
604
 
605
+ ### MCP OAuth logins: sign in once, every account has it
606
+
607
+ A server that signs in with OAuth (higgsfield, say) keeps its login in the account's
608
+ **credential store** — `mcpOAuth` in `<acct>/.credentials.json`, or in the account's
609
+ Keychain item — not in `.claude.json`, so the registry cannot carry it. Under the pool a
610
+ server authenticated in one account said *needs authentication* under every other account
611
+ the shim picked (2026-10-19: five separate higgsfield grants in five accounts,
612
+ discovery-only stubs in the rest). The claude pool now shares those logins **on this
613
+ machine**:
614
+
615
+ ```bash
616
+ claude-accounts mcp auth-status # per server: the pool's login; per account: fresh, stale, own, stub, missing or absent
617
+ claude-accounts mcp auth-status --json
618
+ claude-accounts mcp auth-sync # harvest, refresh what is due, share into every account, now
619
+ claude-accounts mcp auth-forget higgsfield # log it out of EVERY account on this machine
620
+ ```
621
+
622
+ - **Harvest**: every account's MCP logins — its file, and its Keychain item when this
623
+ session can open it — are folded into `<pool>/mcp-oauth.json` (0600). Per server the
624
+ latest save wins (the largest `expiresAt`: the client stamps it on every save; a login
625
+ saved without one counts as expiring an hour after it was found, never as the oldest); a
626
+ discovery-only stub (`accessToken: ""`) never overwrites anything.
627
+ - **Share**: the shim merges the newest login into the picked account right before exec —
628
+ no network, and python only when an account's credential file or the store changed since
629
+ that account last looked — and every claude `limits` pass shares into all accounts. A
630
+ merge goes into a store that exists; a `.credentials.json` is **created** — holding the
631
+ MCP logins alone, 0600 — only for an account that runs on its setup token (no `.expired`
632
+ park, not an adopted dir), whose client has nowhere else to read them from in an ssh/tmux
633
+ session. That file costs the token nothing: a document without `claudeAiOauth` is no
634
+ Claude login to the shim, the limits pass, the audit or verify (`list` shows `auth=token`),
635
+ and it is the file the client writes itself once it meets an OAuth server. An account
636
+ with a Keychain login gets its item updated instead and never a file. A merge takes the
637
+ client's own lock (`<acct>/.storage-write.lock`), re-reads, changes that one `mcpOAuth` key
638
+ and nothing else (the Claude login beside it is never rewritten), and keeps the file's
639
+ mtime. A Keychain item is updated only when it already exists and this session can open
640
+ it, then read back; one is never created. An account parked with `.expired` gets no
641
+ Keychain write and no new file until it is back in the pool. A login the account holds of
642
+ another grant is replaced only when it is provably older.
643
+ - **Refresh**: the limits pass is the only refresher — one login in many accounts must have
644
+ ONE, because each client refreshes under its own config dir's lock and refresh tokens
645
+ rotate. A login is refreshed at its authorization server (RFC 8414 metadata, the refresh
646
+ grant with `resource` = the server URL) two hours before it expires, or at half its
647
+ lifetime when that is shorter (never twice per half-lifetime), long before a client's own
648
+ 5-minute window, then shared everywhere. A failure backs off 15 min, doubling to 6 h (`mcp
649
+ auth-sync --force` retries now); a refused refresh token (`invalid_grant`) shows as **NEEDS
650
+ RE-AUTH**: sign in once in any account (`/mcp`) and that new login replaces it everywhere.
651
+ A login with a copy in a Keychain item this session cannot open is left to a pass that
652
+ can (a Terminal on the Mac, the scheduled agent) until 20 minutes before it expires.
653
+ - **Nothing is shared without that refresher**: not with
654
+ `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0`, not by a launch when no refreshing limits pass ran in
655
+ the last hour, and not a login within the clients' own 5-minute window, refused by its
656
+ server, or whose failing refresh would next try too late. Each account keeps what it holds;
657
+ `auth-status` says why (`NOT shared: …`, `sharing paused: …`).
658
+ - **Machine-local**: `mcp-oauth.json` is never synced or fanned out — a grant belongs to the
659
+ machine it was issued on, so each Mac signs in once.
660
+ - **Logging out**: a `/mcp` "Clear authentication", `claude mcp login <server>` or a
661
+ re-authentication in ANY one account revokes the stored login at the server (when it
662
+ supports revocation — higgsfield does) — and under the pool that login is every account's.
663
+ So the pool retires it everywhere the moment it sees the key gone or blanked in an account
664
+ that held it; sign in once in any account and the new login is shared again. `mcp
665
+ auth-forget NAME` is the same logout from the operator's side: it drops the login from the
666
+ pool and every account store, and a copy it could not reach (a Keychain item this session
667
+ cannot open) stays ignored and is cleared by a later pass.
668
+ - **Kill switches**: `CLAUDE_MULTIACC_MCP_OAUTH=0` stops sharing (the shim's merge and the
669
+ limits pass's sync); `CLAUDE_MULTIACC_MCP_OAUTH_REFRESH=0` never refreshes — and so shares
670
+ nothing. `auth-status` and `auth-forget` always run. Claude pool only: Codex keeps MCP
671
+ OAuth in its own store, and `codex-accounts mcp auth-…` says so.
672
+
565
673
  app-robot tasks are different: the panel launches Claude with `--strict-mcp-config` and a
566
674
  merged MCP document (`agent_mcp.servers_for`) and Codex with the same document as `-c`
567
675
  overrides, so a server every task must see is added there — never with `claude mcp add` on a