claude-multiacc 2.0.64 → 2.0.66

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 (233) hide show
  1. package/bin/claude-accounts +15 -201
  2. package/bin/codex +25 -7
  3. package/bin/codex-accounts +16 -3
  4. package/docs/AUTORESUME.md +10 -1
  5. package/docs/CODEX.md +39 -0
  6. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  8. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  9. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  12. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  13. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  14. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  44. package/lib/autoresume.py +28 -8
  45. package/lib/claude_oauth_lock.py +172 -0
  46. package/lib/claude_oauth_refresh.py +225 -0
  47. package/lib/codex_speed.py +351 -0
  48. package/lib/codex_speed.sh +39 -0
  49. package/lib/common.sh +13 -3
  50. package/lib/model_match.py +4 -1
  51. package/lib/session_settings.py +25 -6
  52. package/lib/shim_path.py +6 -0
  53. package/package.json +4 -3
  54. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  55. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  56. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  57. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  58. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  59. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  60. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  61. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  62. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  63. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  64. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  65. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_claude_oauth_lock.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
  122. package/tests/claude_oauth_fixture.py +100 -0
  123. package/tests/run-tests.sh +23 -7
  124. package/tests/test_claude_oauth_lock.py +138 -0
  125. package/tests/test_claude_oauth_refresh.py +106 -0
  126. package/tests/test_claude_oauth_refresh_failures.py +112 -0
  127. package/tests/test_codex_speed.py +236 -0
  128. package/tests/test_mutate_lock.py +107 -0
  129. package/tests/test_shim_path.py +44 -4
  130. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  131. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  132. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  133. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  134. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  135. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  136. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  137. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  138. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  139. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  140. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  141. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  142. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  143. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  144. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  145. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  146. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  147. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  148. package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
  149. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  150. package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
  151. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  152. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  153. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  154. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  155. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  156. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  157. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  158. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  159. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  160. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  161. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  162. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  163. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  164. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  165. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  166. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  167. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  168. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  169. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  170. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  171. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  172. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  173. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  174. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  175. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  176. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  177. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  178. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  179. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  180. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  181. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  182. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  183. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  184. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  185. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  186. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  187. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  188. package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
  189. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
  190. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
  191. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
  192. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  193. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  194. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  195. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  196. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  197. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  198. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  199. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  200. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  201. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  202. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  203. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  204. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  205. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  206. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  207. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  208. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  209. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  210. package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
  211. package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
  212. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  213. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  214. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  215. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  216. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  217. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  218. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  219. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  220. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  221. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  222. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  223. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  224. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  225. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  226. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  227. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  228. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  229. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  230. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  231. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  232. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  233. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve Codex speed from the selected account's native model catalog.
3
+
4
+ Capabilities belong to an account AND a model. Never use the merged pool catalog,
5
+ an e-mail address, a plan-name guess, or another account's cache for entitlement.
6
+ Every Ultrafast launch asks the native account client to validate its model
7
+ catalog. Unknown access falls back to Fast; later launches discover new access.
8
+ """
9
+
10
+ import argparse
11
+ from datetime import datetime, timezone
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ import re
16
+ import selectors
17
+ import signal
18
+ import subprocess
19
+ import sys
20
+ import tempfile
21
+ import time
22
+
23
+ CACHE_SECONDS = 300
24
+ PROBE_SECONDS = 5
25
+
26
+ SPEEDS = ("normal", "fast", "ultrafast")
27
+ ALIASES = {"normal": "normal", "default": "normal", "standard": "normal",
28
+ "fast": "fast", "priority": "fast", "ultrafast": "ultrafast"}
29
+
30
+
31
+ def speed(value):
32
+ return ALIASES.get(value) if isinstance(value, str) else None
33
+
34
+
35
+ def _json(path):
36
+ try:
37
+ value = json.loads(Path(path).read_text())
38
+ return value if isinstance(value, dict) else {}
39
+ except (OSError, ValueError):
40
+ return {}
41
+
42
+
43
+ def _config(path):
44
+ try:
45
+ source = Path(path).read_text()
46
+ except OSError:
47
+ return {}
48
+ try:
49
+ import tomllib
50
+ return tomllib.loads(source)
51
+ except ImportError:
52
+ # Older Python is supported by the shims. Read only simple top-level scalar
53
+ # keys; complex TOML is left to the native client, never guessed.
54
+ top = source.split("\n[", 1)[0]
55
+ return dict(re.findall(r'''(?m)^\s*(model|service_tier|profile)\s*=\s*["']([^"'\n]*)["']\s*(?:#.*)?$''', top))
56
+ except ValueError:
57
+ return {}
58
+
59
+
60
+ def configuration(account_dir, profile=None):
61
+ document = _config(Path(account_dir) / "config.toml")
62
+ profile = profile or document.get("profile")
63
+ if isinstance(profile, str) and re.fullmatch(r"[A-Za-z0-9_.-]+", profile):
64
+ profiles = document.get("profiles")
65
+ old = profiles.get(profile) if isinstance(profiles, dict) else None
66
+ if isinstance(old, dict):
67
+ document.update(old)
68
+ document.update(_config(Path(account_dir) / (profile + ".config.toml")))
69
+ return document
70
+
71
+
72
+ def current_catalog(account_dir):
73
+ """A recent native cache, invalidated when this slot's login is replaced."""
74
+ cache = Path(account_dir) / "models_cache.json"
75
+ document = _json(cache)
76
+ try:
77
+ if not isinstance(document.get("fetched_at"), str):
78
+ return {}
79
+ fetched = datetime.fromisoformat(document["fetched_at"].replace("Z", "+00:00"))
80
+ if fetched.tzinfo is None:
81
+ fetched = fetched.replace(tzinfo=timezone.utc)
82
+ age = time.time() - fetched.timestamp()
83
+ if not 0 <= age <= CACHE_SECONDS:
84
+ return {}
85
+ auth = Path(account_dir) / "auth.json"
86
+ if auth.exists() and auth.stat().st_mtime > cache.stat().st_mtime:
87
+ return {}
88
+ return document
89
+ except (KeyError, OSError, TypeError, ValueError):
90
+ return {}
91
+
92
+
93
+ def native_request(account_dir, executable, method, params, timeout=PROBE_SECONDS, isolated=True):
94
+ """One bounded task-free native request, with its own process group and index.
95
+
96
+ Temporary sqlite_home prevents a stopped probe from stranding the account's
97
+ rollout-backfill lease. Resume settings persist in the native rollout and
98
+ are read back by the real CLI using its regular index.
99
+ """
100
+ if not executable:
101
+ return {}
102
+ env = dict(os.environ, CODEX_HOME=str(account_dir), CODEX_MULTIACC_DISABLE="1", CODEX_SHIM_ACTIVE="1")
103
+ proc, scratch = None, None
104
+ try:
105
+ command = [executable, "app-server", "--listen", "stdio://"]
106
+ if isolated:
107
+ scratch = tempfile.TemporaryDirectory(prefix="codex-speed-catalog-")
108
+ command += ["-c", "sqlite_home=" + json.dumps(scratch.name)]
109
+ proc = subprocess.Popen(command,
110
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
111
+ env=env, start_new_session=True, close_fds=True)
112
+ deadline, pending = time.monotonic() + timeout, b""
113
+ with selectors.DefaultSelector() as poll:
114
+ poll.register(proc.stdout, selectors.EVENT_READ)
115
+ requests = [
116
+ {"id": 1, "method": "initialize", "params": {
117
+ "clientInfo": {"name": "claude-multiacc", "version": "1"},
118
+ "capabilities": {"experimentalApi": True}}},
119
+ {"id": 2, "method": method, "params": params},
120
+ ]
121
+ for request in requests:
122
+ proc.stdin.write((json.dumps(request) + "\n").encode())
123
+ proc.stdin.flush()
124
+ answer = None
125
+ while answer is None and time.monotonic() < deadline:
126
+ if b"\n" not in pending:
127
+ if not poll.select(max(0, deadline - time.monotonic())):
128
+ return {}
129
+ chunk = os.read(proc.stdout.fileno(), 65536)
130
+ if not chunk:
131
+ return {}
132
+ pending += chunk
133
+ if len(pending) > 8 << 20:
134
+ return {}
135
+ while b"\n" in pending:
136
+ line, pending = pending.split(b"\n", 1)
137
+ try:
138
+ message = json.loads(line)
139
+ except ValueError:
140
+ continue
141
+ if isinstance(message, dict) and message.get("id") == request["id"]:
142
+ answer = message
143
+ break
144
+ if not answer or "error" in answer:
145
+ return {}
146
+ if request["id"] == 1:
147
+ proc.stdin.write(b'{"method":"initialized","params":{}}\n')
148
+ proc.stdin.flush()
149
+ else:
150
+ result = answer.get("result")
151
+ return result if isinstance(result, dict) else {}
152
+ except (OSError, ValueError, subprocess.SubprocessError):
153
+ return {}
154
+ finally:
155
+ if proc is not None:
156
+ try:
157
+ os.killpg(proc.pid, signal.SIGTERM)
158
+ except ProcessLookupError:
159
+ pass
160
+ try:
161
+ proc.communicate(timeout=0.5)
162
+ except subprocess.TimeoutExpired:
163
+ try:
164
+ os.killpg(proc.pid, signal.SIGKILL)
165
+ except ProcessLookupError:
166
+ pass
167
+ proc.communicate()
168
+ if scratch is not None:
169
+ scratch.cleanup()
170
+
171
+
172
+ def refresh_catalog(account_dir, executable, timeout=PROBE_SECONDS):
173
+ """Native Codex validates the account identity before granting Ultrafast.
174
+
175
+ Even a fresh cache could have been copied from another account. Every native
176
+ Ultrafast launch asks the account client; inspection without a native binary
177
+ can read cache metadata but cannot authorize an actual launch.
178
+ """
179
+ if executable is None:
180
+ return None
181
+ return native_request(account_dir, executable, "model/list", {"includeHidden": True}, timeout) or {}
182
+
183
+
184
+ def resume_target(argv):
185
+ """The exact UUID a native resume names; never infer a picker/--last target."""
186
+ values = {"-c", "--config", "-m", "--model", "-p", "--profile", "-C", "--cd", "-s", "--sandbox",
187
+ "-a", "--ask-for-approval", "--enable", "--disable", "--output-schema", "-o",
188
+ "--output-last-message", "--image", "-i", "--color", "--add-dir"}
189
+ waiting, resume = False, False
190
+ for arg in argv:
191
+ if waiting:
192
+ waiting = False
193
+ continue
194
+ if arg == "--":
195
+ break
196
+ if arg in values:
197
+ waiting = True
198
+ continue
199
+ if arg.startswith("-"):
200
+ continue
201
+ if arg == "resume":
202
+ resume = True
203
+ continue
204
+ if resume:
205
+ return arg if re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", arg) else None
206
+ if arg not in ("exec", "e"):
207
+ return None
208
+ return None
209
+
210
+
211
+ def persist_resume_speed(account_dir, executable, argv, result):
212
+ """Codex 0.164 restores saved thread tiers ahead of process config on resume.
213
+
214
+ Set the named thread's tier through its native API before the real CLI resumes
215
+ it. No turn is sent, no prompt is replayed, and model/effort/permissions stay
216
+ native thread defaults. A failed write cancels instead of silently using the
217
+ wrong (possibly more expensive) speed.
218
+ """
219
+ thread_id = resume_target(argv)
220
+ if not thread_id or not executable:
221
+ return
222
+ response = native_request(account_dir, executable, "thread/resume",
223
+ {"threadId": thread_id, "serviceTier": result["service_tier"]},
224
+ timeout=30)
225
+ if not response or speed(response.get("serviceTier")) != result["effective"]:
226
+ raise ValueError("could not preserve speed in the resumed Codex thread; retry the launch")
227
+
228
+
229
+ def resolve(account_dir, requested, model=None, catalog=None):
230
+ """Return safe public launch metadata, with no credentials or account identity."""
231
+ if requested not in SPEEDS:
232
+ raise ValueError("speed must be normal, fast, or ultrafast")
233
+ document = catalog if isinstance(catalog, dict) else current_catalog(account_dir)
234
+ models = document.get("models", document.get("data", []))
235
+ models = models if isinstance(models, list) else []
236
+ if not model:
237
+ model = configuration(account_dir).get("model")
238
+ if not model:
239
+ listed = [row for row in models if isinstance(row, dict) and
240
+ (row.get("visibility") == "list" or row.get("isDefault"))]
241
+ listed.sort(key=lambda row: (not bool(row.get("isDefault")),
242
+ row.get("priority") if isinstance(row.get("priority"), int) else 1000))
243
+ if listed:
244
+ model = listed[0].get("slug", listed[0].get("id"))
245
+ row = next((row for row in models if isinstance(row, dict) and
246
+ row.get("slug", row.get("id")) == model), {})
247
+ tiers = row.get("service_tiers", row.get("serviceTiers"))
248
+ # service_tiers is authoritative when present, including an explicitly empty
249
+ # list. additional_speed_tiers is the older native catalog representation.
250
+ advertised = ([item.get("id") for item in tiers if isinstance(item, dict)]
251
+ if isinstance(tiers, list) else row.get("additional_speed_tiers", []))
252
+ supported = {speed(item) for item in advertised if isinstance(item, str)}
253
+ effective = "fast" if requested == "ultrafast" and "ultrafast" not in supported else requested
254
+ service_tier = {"normal": "default", "fast": "fast", "ultrafast": "ultrafast"}[effective]
255
+ # Current native catalogs call Fast 'priority'. Use the account's actual id.
256
+ if effective == "fast" and "priority" in advertised:
257
+ service_tier = "priority"
258
+ return {"requested": requested, "effective": effective, "service_tier": service_tier,
259
+ "model": model, "ultrafast_available": "ultrafast" in supported,
260
+ "fallback": requested != effective}
261
+
262
+
263
+ def launch_argv(account_dir, argv, environ=None, executable=None):
264
+ """Apply one explicit speed override without changing prompts or unrelated flags.
265
+
266
+ CLI overrides win over CODEX_SPEED, which wins over account/profile defaults.
267
+ This lets an autoresume's history pin replace a launch's inherited environment.
268
+ """
269
+ env = os.environ if environ is None else environ
270
+ kept, overrides, model, profile, i = [], {}, None, None, 0
271
+ ignore_user_config = False
272
+ while i < len(argv):
273
+ arg = argv[i]
274
+ if arg == "--":
275
+ kept.extend(argv[i:])
276
+ break
277
+ if arg == "--ignore-user-config":
278
+ ignore_user_config = True
279
+ consumed, override = 1, None
280
+ if arg in ("-c", "--config") and i + 1 < len(argv):
281
+ consumed, override = 2, argv[i + 1]
282
+ elif arg.startswith("--config="):
283
+ override = arg.split("=", 1)[1]
284
+ elif arg.startswith("-c") and len(arg) > 2:
285
+ override = arg[2:]
286
+ if override is not None and "=" in override:
287
+ key, value = override.split("=", 1)
288
+ key, value = key.strip(), value.strip().strip("\"'")
289
+ if key in ("model", "service_tier", "features.fast_mode"):
290
+ overrides[key] = value
291
+ if (key == "service_tier" and speed(value)) or key == "features.fast_mode":
292
+ i += consumed
293
+ continue
294
+ if arg in ("-m", "--model", "-p", "--profile") and i + 1 < len(argv):
295
+ if arg in ("-m", "--model"):
296
+ model = argv[i + 1]
297
+ else:
298
+ profile = argv[i + 1]
299
+ consumed = 2
300
+ elif arg.startswith("--model="):
301
+ model = arg.split("=", 1)[1]
302
+ elif arg.startswith("--profile="):
303
+ profile = arg.split("=", 1)[1]
304
+ kept.extend(argv[i:i + consumed])
305
+ i += consumed
306
+ config = {} if ignore_user_config else configuration(account_dir, profile)
307
+ if "service_tier" in overrides and speed(overrides["service_tier"]) is None:
308
+ return None, list(argv)
309
+ requested = (speed(overrides.get("service_tier")) or env.get("CODEX_SPEED") or
310
+ speed(config.get("service_tier")))
311
+ if not requested:
312
+ return None, list(argv)
313
+ model = model or overrides.get("model") or env.get("CODEX_SPEED_MODEL") or config.get("model")
314
+ fresh = refresh_catalog(account_dir, executable) if requested == "ultrafast" else None
315
+ result = resolve(account_dir, requested, model, fresh)
316
+ flags = ["-c", "service_tier=" + result["service_tier"]]
317
+ if result["effective"] != "normal":
318
+ flags += ["-c", "features.fast_mode=true"]
319
+ persist_resume_speed(account_dir, executable, argv, result)
320
+ return result, flags + kept
321
+
322
+
323
+ def main(argv=None):
324
+ parser = argparse.ArgumentParser(description=__doc__)
325
+ parser.add_argument("--account-dir", required=True)
326
+ parser.add_argument("--model")
327
+ parser.add_argument("--speed", choices=SPEEDS)
328
+ parser.add_argument("--json", action="store_true")
329
+ parser.add_argument("--launch-argv", action="store_true")
330
+ parser.add_argument("--native-cli", default="")
331
+ parser.add_argument("argv", nargs=argparse.REMAINDER)
332
+ args = parser.parse_args(argv)
333
+ if args.launch_argv:
334
+ native = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
335
+ result, native = launch_argv(args.account_dir, native, executable=args.native_cli)
336
+ values = [result["effective"] if result else "", *(native or [])]
337
+ sys.stdout.buffer.write(b"\0".join(os.fsencode(value) for value in values) + b"\0")
338
+ else:
339
+ requested = args.speed or "normal"
340
+ fresh = refresh_catalog(args.account_dir, args.native_cli) if requested == "ultrafast" else None
341
+ result = resolve(args.account_dir, requested, args.model, fresh)
342
+ print(json.dumps(result) if args.json else result["effective"])
343
+ return 0
344
+
345
+
346
+ if __name__ == "__main__":
347
+ try:
348
+ sys.exit(main())
349
+ except ValueError as error:
350
+ print("codex speed: " + str(error), file=sys.stderr)
351
+ sys.exit(2)
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Read-only, per-launch speed selection. Never rewrites account configuration.
3
+ codex_speed_prepare() {
4
+ CODEX_SPEED_ARGV=("$@")
5
+ [ "${CODEX_MULTIACC_DISABLE:-0}" != 1 ] || return 0
6
+ local a at command="" wanted=0 value first=1
7
+ for a in "$@"; do
8
+ [ "$a" != -- ] || break
9
+ case "$a" in --help|-h|--version|-V) return 0 ;; esac
10
+ done
11
+ at="$(cli_positional_at "$@")" && command="${!at}"
12
+ case "$command" in login|logout|mcp|features|completion|debug|sandbox|apply|a|cloud|update|help) return 0 ;; esac
13
+ case "${CODEX_SPEED:-}" in
14
+ '') ;;
15
+ normal|fast|ultrafast) wanted=1 ;;
16
+ *) printf 'codex-multiacc: CODEX_SPEED must be normal, fast, or ultrafast\n' >&2; return 2 ;;
17
+ esac
18
+ for a in "$@"; do case "$a" in *service_tier=*|-p|--profile|--profile=*) wanted=1 ;; esac; done
19
+ if [ "$wanted" = 0 ]; then
20
+ grep -qE '(service_tier|profile)[[:space:]]*=' "${CODEX_HOME:-${HOME:-/nonexistent}/.codex}/config.toml" 2>/dev/null \
21
+ || return 0
22
+ fi
23
+ local rewritten=()
24
+ while IFS= read -r -d '' value; do
25
+ if [ "$first" = 1 ]; then
26
+ first=0
27
+ export CODEX_SPEED_EFFECTIVE="$value"
28
+ else
29
+ rewritten+=("$value")
30
+ fi
31
+ done < <("$MCP_PYBIN" "$SELF_DIR/../lib/codex_speed.py" \
32
+ --account-dir "${CODEX_HOME:-${HOME:-/nonexistent}/.codex}" --native-cli "$REAL" --launch-argv -- "$@")
33
+ if [ "$first" = 1 ]; then
34
+ printf 'codex-multiacc: could not resolve the requested Codex speed; launch cancelled\n' >&2
35
+ return 2
36
+ fi
37
+ CODEX_SPEED_ARGV=(${rewritten[@]+"${rewritten[@]}"})
38
+ return 0
39
+ }
package/lib/common.sh CHANGED
@@ -844,16 +844,26 @@ PYEOF
844
844
  seed_mcp_registry "$d"
845
845
  }
846
846
 
847
+ # Exclusive directory creation must report a competing owner as failure. Ubuntu
848
+ # 26.04's uutils mkdir can return success to BOTH concurrent creators without -p.
849
+ # Python is already required by account operations; os.mkdir uses one mkdir(2).
850
+ atomic_mkdir() { "$PYBIN" -c 'import os, sys; os.mkdir(sys.argv[1], 0o700)' "$1"; }
851
+
847
852
  # Short-lived mutation lock: serialize only the brief critical sections (id reservation,
848
853
  # manifest write), NOT long interactive work like a browser sign-in — so several
849
854
  # `add`/`login` in different terminals run in parallel and only briefly wait on each other.
850
855
  # Held for milliseconds; a lock older than 30s is assumed abandoned (holder died) and reclaimed.
851
856
  MUTATE_LOCK_HELD=0
852
857
  mutate_lock() {
853
- local lock="$ACC_ROOT/.locks/mutate" tries=0
858
+ local lock="$ACC_ROOT/.locks/mutate" tries=0 stamp
854
859
  mkdir -p "$ACC_ROOT/.locks" 2>/dev/null
855
- while ! mkdir "$lock" 2>/dev/null; do
856
- if [ $(( $(epoch_now) - $(file_mtime "$lock") )) -gt 30 ]; then
860
+ while ! atomic_mkdir "$lock" 2>/dev/null; do
861
+ stamp="$(file_mtime "$lock")"
862
+ # The holder can release between mkdir and stat. A missing/unreadable mtime
863
+ # is not stale-lock evidence: reclaiming from zero could delete the next
864
+ # holder's newly acquired directory. Use the ordinary bounded retry instead.
865
+ case "$stamp" in ''|*[!0-9]*) stamp=0 ;; esac
866
+ if [ "$stamp" -gt 0 ] && [ $(( $(epoch_now) - stamp )) -gt 30 ]; then
857
867
  rm -rf "$lock" 2>/dev/null
858
868
  continue
859
869
  fi
@@ -92,7 +92,10 @@ def resume_settings(source, settings, target, catalog=None):
92
92
  effort = match_effort(catalog, source, (settings or {}).get("effort"), target, model)
93
93
  if model and not effort and target == "codex":
94
94
  effort = (((catalog.get("codex") or {}).get("models") or {}).get(model) or {}).get("default_effort")
95
- return {"model": model, "effort": effort}
95
+ result = {"model": model, "effort": effort}
96
+ if source == target == "codex" and (settings or {}).get("speed") in ("normal", "fast", "ultrafast"):
97
+ result["speed"] = settings["speed"]
98
+ return result
96
99
 
97
100
 
98
101
  def settings_flags(provider, model, effort):
@@ -19,6 +19,10 @@ import json
19
19
  import os
20
20
  import sys
21
21
 
22
+ # The public helper is also launched with Python -I, which omits its own directory.
23
+ # Import only the installed sibling helpers, never modules from the caller's cwd.
24
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
25
+
22
26
  CHUNK = 1 << 20
23
27
  SCAN_LIMIT = 64 << 20
24
28
  SYNTHETIC = "<synthetic>"
@@ -70,19 +74,34 @@ def _own_thread_id(path):
70
74
 
71
75
  def codex_settings(path, upto=None):
72
76
  own = _own_thread_id(path)
77
+ found, tier_found = None, False
78
+ selected_speed = None
73
79
  for line in _reverse_lines(path, upto):
74
80
  doc = _record(line)
75
81
  payload = doc.get("payload") if doc else None
76
82
  if not isinstance(payload, dict):
77
83
  continue
78
- if doc.get("type") == "turn_context" and _text(payload.get("model")):
79
- return {"model": _text(payload.get("model")), "effort": _text(payload.get("effort"))}
84
+ settings, effort_key = None, "effort"
85
+ if doc.get("type") == "turn_context":
86
+ settings = payload
80
87
  if doc.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
81
- settings = payload.get("thread_settings")
82
88
  thread = _text(payload.get("thread_id"))
83
- if isinstance(settings, dict) and _text(settings.get("model")) and (own is None or thread in (None, own)):
84
- return {"model": _text(settings.get("model")), "effort": _text(settings.get("reasoning_effort"))}
85
- return {"model": None, "effort": None}
89
+ if own is None or thread in (None, own):
90
+ settings, effort_key = payload.get("thread_settings"), "reasoning_effort"
91
+ if not isinstance(settings, dict):
92
+ continue
93
+ if found is None and _text(settings.get("model")):
94
+ found = {"model": _text(settings.get("model")), "effort": _text(settings.get(effort_key))}
95
+ if not tier_found and "service_tier" in settings:
96
+ tier_found = True
97
+ from codex_speed import speed
98
+ selected_speed = "normal" if settings["service_tier"] is None else speed(settings["service_tier"])
99
+ if found is not None and tier_found:
100
+ break
101
+ found = found or {"model": None, "effort": None}
102
+ if selected_speed:
103
+ found["speed"] = selected_speed
104
+ return found
86
105
 
87
106
 
88
107
  def claude_settings(path, upto=None):
package/lib/shim_path.py CHANGED
@@ -41,6 +41,7 @@ from __future__ import annotations
41
41
  import getpass
42
42
  import json
43
43
  import os
44
+ import re
44
45
  import shutil
45
46
  import signal
46
47
  import subprocess
@@ -66,6 +67,10 @@ MODES = (
66
67
  # which reports an alias or a function by name, so the sh script drops those first.
67
68
  _RESOLVERS = {'zsh': 'whence -p', 'bash': 'type -P', 'sh': 'command -v'}
68
69
  SENTINEL = '__claude_multiacc_probe__'
70
+ # Login shells may emit OSC shell-integration records on stdout immediately before
71
+ # or after an answer (Ubuntu's OSC 3008). Remove only complete, line-local records;
72
+ # an unterminated control string must never swallow a later sentinel line.
73
+ _OSC = re.compile(r'\x1b\][^\x07\x1b\r\n]*(?:\x07|\x1b\\)')
69
74
  # The PATH a login shell STARTS from before any rc file runs — what sshd/login/launchd
70
75
  # hand it. /usr/local/bin matters on Linux: the root install's /usr/local/bin/claude
71
76
  # symlink is how a shell with no rc block still reaches the shim, so leaving it out
@@ -139,6 +144,7 @@ def _parse(text, names):
139
144
  """First sentinel answer per name. Anything else the shell printed is ignored."""
140
145
  answers = {}
141
146
  for line in text.splitlines():
147
+ line = _OSC.sub('', line)
142
148
  if not line.startswith(SENTINEL + ' '):
143
149
  continue
144
150
  body = line[len(SENTINEL) + 1:]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.64",
3
+ "version": "2.0.66",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,12 +56,13 @@
56
56
  "scripts": {
57
57
  "postinstall": "node scripts/postinstall.mjs",
58
58
  "test": "npm run test:core && npm run test:seeding && npm run test:commands",
59
- "test:core": "bash tests/run-tests.sh && npm run test:selector && npm run test:shim",
59
+ "test:core": "bash tests/run-tests.sh && npm run test:claude-oauth && npm run test:selector && npm run test:shim",
60
+ "test:claude-oauth": "python3 -m unittest discover -s tests -p 'test_claude_oauth*.py'",
60
61
  "test:shim": "python3 tests/test_shim_path.py && npm run test:codex-stdin",
61
62
  "test:codex-stdin": "python3 tests/test_codex_exec_stdin.py && python3 tests/test_codex_retry_process.py",
62
63
  "test:selector": "python3 -m unittest discover -s tests -p 'test_selector*.py'",
63
64
  "test:seeding": "npm run test:account-seeding && npm run test:mcp && npm run test:codex-settings",
64
- "test:codex-settings": "python3 tests/test_codex_config_edit.py && python3 tests/test_codex_settings.py",
65
+ "test:codex-settings": "python3 tests/test_codex_config_edit.py && python3 tests/test_codex_settings.py && python3 tests/test_codex_speed.py",
65
66
  "test:commands": "python3 tests/test_packaged_commands.py",
66
67
  "test:codex-mcp-auth": "python3 -m unittest discover -s tests -p 'test_codex_mcp_oauth*.py'",
67
68
  "test:account-seeding": "python3 tests/test_account_seeding.py && python3 tests/test_claude_pool_prepare.py",