claude-multiacc 2.0.61 → 2.0.65
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 +6 -2
- package/bin/claude-accounts +4 -4
- package/bin/codex +54 -13
- package/bin/codex-accounts +28 -7
- package/docs/ACCOUNT_OPERATIONS.md +2 -2
- package/docs/AUTORESUME.md +16 -2
- package/docs/CODEX.md +58 -2
- package/docs/CODEX_MCP_AUTH.md +58 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/lib/autoresume.py +34 -13
- package/lib/autoresume_cross.py +2 -1
- package/lib/autoresume_handoff.py +13 -5
- package/lib/codex_exec_stdin.sh +45 -0
- package/lib/codex_mcp_oauth.py +204 -0
- package/lib/codex_mcp_oauth.sh +29 -0
- package/lib/codex_mcp_oauth_locks.py +158 -0
- package/lib/codex_mcp_oauth_native.py +102 -0
- package/lib/codex_mcp_oauth_store.py +149 -0
- package/lib/codex_resume_cwd.py +57 -0
- package/lib/codex_retry_process.sh +37 -0
- package/lib/codex_speed.py +351 -0
- package/lib/codex_speed.sh +39 -0
- package/lib/common.sh +16 -3
- package/lib/model_match.py +4 -1
- package/lib/session_settings.py +25 -6
- package/lib/shim_path.py +6 -0
- package/package.json +10 -5
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
- package/tests/fake_codex_tui.py +23 -1
- package/tests/run-tests.sh +23 -7
- package/tests/test_autoresume_cwd.py +71 -0
- package/tests/test_autoresume_cwd_handoff.py +92 -0
- package/tests/test_autoresume_cwd_reader.py +105 -0
- package/tests/test_autoresume_cwd_tmux.py +54 -0
- package/tests/test_codex_exec_stdin.py +178 -0
- package/tests/test_codex_mcp_oauth.py +146 -0
- package/tests/test_codex_mcp_oauth_adopt.py +87 -0
- package/tests/test_codex_mcp_oauth_migration.py +141 -0
- package/tests/test_codex_mcp_oauth_native.py +109 -0
- package/tests/test_codex_mcp_oauth_safety.py +130 -0
- package/tests/test_codex_mcp_oauth_shim.py +188 -0
- package/tests/test_codex_retry_process.py +143 -0
- package/tests/test_codex_speed.py +236 -0
- package/tests/test_mutate_lock.py +107 -0
- package/tests/test_shim_path.py +44 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.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/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.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/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- 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
|
@@ -30,6 +30,8 @@ DEFAULT_SERVER="root@138.197.36.107"
|
|
|
30
30
|
DEFAULT_SERVER_REPO="/root/claude-multiacc"
|
|
31
31
|
|
|
32
32
|
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd -P)"
|
|
33
|
+
codex_mcp_oauth_ensure() { :; }
|
|
34
|
+
if [ -f "$LIB_DIR/codex_mcp_oauth.sh" ]; then . "$LIB_DIR/codex_mcp_oauth.sh"; fi
|
|
33
35
|
|
|
34
36
|
# INSTANCE-SCOPED POOLS: <PROVIDER>_ACCOUNTS_ROOT relocates the whole pool, so
|
|
35
37
|
# several app-robot instances on one machine/user keep completely isolated account
|
|
@@ -806,6 +808,7 @@ seed_account_dir() { # $1 = acct dir
|
|
|
806
808
|
fi
|
|
807
809
|
codex_share_state_index "$d"
|
|
808
810
|
seed_mcp_registry "$d"
|
|
811
|
+
codex_mcp_oauth_ensure "$ACC_ROOT" "$PYBIN" "$LIB_DIR/codex_mcp_oauth.py" "$d"
|
|
809
812
|
return 0
|
|
810
813
|
fi
|
|
811
814
|
if [ ! -f "$d/.claude.json" ] && [ -f "$HOME/.claude.json" ]; then
|
|
@@ -841,16 +844,26 @@ PYEOF
|
|
|
841
844
|
seed_mcp_registry "$d"
|
|
842
845
|
}
|
|
843
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
|
+
|
|
844
852
|
# Short-lived mutation lock: serialize only the brief critical sections (id reservation,
|
|
845
853
|
# manifest write), NOT long interactive work like a browser sign-in — so several
|
|
846
854
|
# `add`/`login` in different terminals run in parallel and only briefly wait on each other.
|
|
847
855
|
# Held for milliseconds; a lock older than 30s is assumed abandoned (holder died) and reclaimed.
|
|
848
856
|
MUTATE_LOCK_HELD=0
|
|
849
857
|
mutate_lock() {
|
|
850
|
-
local lock="$ACC_ROOT/.locks/mutate" tries=0
|
|
858
|
+
local lock="$ACC_ROOT/.locks/mutate" tries=0 stamp
|
|
851
859
|
mkdir -p "$ACC_ROOT/.locks" 2>/dev/null
|
|
852
|
-
while !
|
|
853
|
-
|
|
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
|
|
854
867
|
rm -rf "$lock" 2>/dev/null
|
|
855
868
|
continue
|
|
856
869
|
fi
|
package/lib/model_match.py
CHANGED
|
@@ -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
|
-
|
|
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):
|
package/lib/session_settings.py
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
3
|
+
"version": "2.0.65",
|
|
4
4
|
"description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,11 +56,16 @@
|
|
|
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 &&
|
|
59
|
+
"test:core": "bash tests/run-tests.sh && npm run test:selector && npm run test:shim",
|
|
60
|
+
"test:shim": "python3 tests/test_shim_path.py && npm run test:codex-stdin",
|
|
61
|
+
"test:codex-stdin": "python3 tests/test_codex_exec_stdin.py && python3 tests/test_codex_retry_process.py",
|
|
60
62
|
"test:selector": "python3 -m unittest discover -s tests -p 'test_selector*.py'",
|
|
61
|
-
"test:seeding": "
|
|
62
|
-
"test:codex-settings": "python3 tests/test_codex_config_edit.py && python3 tests/test_codex_settings.py",
|
|
63
|
-
"test:commands": "python3 tests/test_packaged_commands.py"
|
|
63
|
+
"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 && python3 tests/test_codex_speed.py",
|
|
65
|
+
"test:commands": "python3 tests/test_packaged_commands.py",
|
|
66
|
+
"test:codex-mcp-auth": "python3 -m unittest discover -s tests -p 'test_codex_mcp_oauth*.py'",
|
|
67
|
+
"test:account-seeding": "python3 tests/test_account_seeding.py && python3 tests/test_claude_pool_prepare.py",
|
|
68
|
+
"test:mcp": "python3 -m unittest discover -s tests -p 'test_mcp_*.py' && npm run test:codex-mcp-auth"
|
|
64
69
|
},
|
|
65
70
|
"dependencies": {
|
|
66
71
|
"update-notifier": "^7.3.1"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|