cdx-manager 0.6.2 → 0.6.3
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 +2 -2
- package/changelogs/CHANGELOGS_0_6_3.md +28 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_usage.py +11 -1
- package/src/cli.py +1 -1
- package/src/cli_commands.py +88 -2
- package/src/provider_runtime.py +74 -9
- package/src/session_service.py +2 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -134,7 +134,7 @@ For a specific version:
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
137
|
-
CDX_VERSION=v0.6.
|
|
137
|
+
CDX_VERSION=v0.6.3 sh install.sh
|
|
138
138
|
```
|
|
139
139
|
|
|
140
140
|
From source:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog (`0.6.2 -> 0.6.3`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-29
|
|
4
|
+
|
|
5
|
+
## Claude Authentication
|
|
6
|
+
|
|
7
|
+
- Fixed Claude Code 2.1.145 isolated auth handling by using `ANTHROPIC_CONFIG_DIR` instead of the older `CLAUDE_CONFIG_DIR` override.
|
|
8
|
+
- Removed leaked `CODEX_HOME` from Claude auth, launch, and status environments so Claude sessions do not write nested `.cdx` state inside isolated Claude homes.
|
|
9
|
+
- Stopped running a destructive Claude logout before `cdx login <name>`, preserving existing credentials when reauthenticating a session.
|
|
10
|
+
- Added profile email hints to Claude login so account-specific sessions open the expected Claude account in the browser.
|
|
11
|
+
|
|
12
|
+
## Claude Token Fallback
|
|
13
|
+
|
|
14
|
+
- Added automatic `claude setup-token` fallback when browser login completes but does not create isolated credentials.
|
|
15
|
+
- Captured the one-time setup token through a temporary transcript, wrote it to `claude-home/credentials/default.json`, and removed the temporary transcript after extraction.
|
|
16
|
+
- Added support for the new Anthropic `credentials/default.json` OAuth format during auth probes, launches, status refreshes, and auth bundle exports.
|
|
17
|
+
|
|
18
|
+
## Release Metadata and Documentation
|
|
19
|
+
|
|
20
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.6.3`.
|
|
21
|
+
|
|
22
|
+
## Validation and Regression Coverage
|
|
23
|
+
|
|
24
|
+
- Added regression coverage for Claude login without pre-logout, email-hinted login, setup-token fallback, modern Anthropic credentials, and cleaned Claude environments.
|
|
25
|
+
|
|
26
|
+
## Validation and Regression Evidence
|
|
27
|
+
|
|
28
|
+
- `python3 -m unittest discover -s test -p 'test_*_py.py'`
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"v0.6.1": {
|
|
13
13
|
"github_tarball_sha256": "5ae4032894e1806ffb3a713e555c12b2faa3bfa6fd7c9fa468664a8577abe827",
|
|
14
14
|
"github_zip_sha256": "7949e99dcfe21dc4e2b82f720b515aea8f0ac9f2059142032d488bcfbc4dfaf1"
|
|
15
|
+
},
|
|
16
|
+
"v0.6.2": {
|
|
17
|
+
"github_tarball_sha256": "0a00fba132453d265a72fa551bff750a9a400011f895d9da33149b335324e02e",
|
|
18
|
+
"github_zip_sha256": "424b1c9652da5054bdc4cf26f31764ea34ddc4609d6eb8075efbf8068112cd50"
|
|
15
19
|
}
|
|
16
20
|
}
|
|
17
21
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/claude_usage.py
CHANGED
|
@@ -15,6 +15,16 @@ CLAUDE_AUTH_STATUS_TIMEOUT_SECONDS = 15
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _read_claude_credentials(auth_home):
|
|
18
|
+
anthropic_cred_path = os.path.join(auth_home, "credentials", "default.json")
|
|
19
|
+
try:
|
|
20
|
+
with open(anthropic_cred_path, "r", encoding="utf-8") as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
token = data.get("access_token") if isinstance(data, dict) else None
|
|
23
|
+
if token:
|
|
24
|
+
return {"accessToken": token}
|
|
25
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
18
28
|
cred_path = os.path.join(auth_home, ".claude", ".credentials.json")
|
|
19
29
|
try:
|
|
20
30
|
with open(cred_path, "r", encoding="utf-8") as f:
|
|
@@ -27,7 +37,7 @@ def _read_claude_credentials(auth_home):
|
|
|
27
37
|
def _home_env_overrides(auth_home):
|
|
28
38
|
overrides = {
|
|
29
39
|
"HOME": auth_home,
|
|
30
|
-
"
|
|
40
|
+
"ANTHROPIC_CONFIG_DIR": auth_home,
|
|
31
41
|
"CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
|
|
32
42
|
}
|
|
33
43
|
if sys.platform == "win32":
|
package/src/cli.py
CHANGED
package/src/cli_commands.py
CHANGED
|
@@ -9,7 +9,7 @@ from datetime import datetime, timedelta
|
|
|
9
9
|
|
|
10
10
|
from .claude_refresh import _refresh_claude_sessions
|
|
11
11
|
from .cli_render import _dim, _info, _success, _warn
|
|
12
|
-
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CODEX, PROVIDER_OLLAMA, PROVIDERS
|
|
12
|
+
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA, PROVIDERS
|
|
13
13
|
from .context_store import (
|
|
14
14
|
clear_context,
|
|
15
15
|
edit_context,
|
|
@@ -837,6 +837,72 @@ def _resolve_confirmation(confirm_fn, name):
|
|
|
837
837
|
return confirmed
|
|
838
838
|
|
|
839
839
|
|
|
840
|
+
def _extract_claude_oauth_token(text):
|
|
841
|
+
if not text:
|
|
842
|
+
return None
|
|
843
|
+
patterns = [
|
|
844
|
+
r"CLAUDE_CODE_OAUTH_TOKEN=([^\s\"']+)",
|
|
845
|
+
r"(sk-ant-oat[0-9A-Za-z._-]+)",
|
|
846
|
+
]
|
|
847
|
+
for pattern in patterns:
|
|
848
|
+
match = re.search(pattern, text)
|
|
849
|
+
if match:
|
|
850
|
+
return match.group(1).strip()
|
|
851
|
+
return None
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _write_claude_oauth_token(auth_home, token):
|
|
855
|
+
cred_dir = os.path.join(auth_home, "credentials")
|
|
856
|
+
os.makedirs(cred_dir, exist_ok=True)
|
|
857
|
+
cred_path = os.path.join(cred_dir, "default.json")
|
|
858
|
+
payload = {
|
|
859
|
+
"version": "1.0",
|
|
860
|
+
"type": "oauth_token",
|
|
861
|
+
"access_token": token,
|
|
862
|
+
}
|
|
863
|
+
with open(cred_path, "w", encoding="utf-8") as handle:
|
|
864
|
+
json.dump(payload, handle, indent=2)
|
|
865
|
+
handle.write("\n")
|
|
866
|
+
try:
|
|
867
|
+
os.chmod(cred_path, 0o600)
|
|
868
|
+
except OSError:
|
|
869
|
+
pass
|
|
870
|
+
return cred_path
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _bootstrap_claude_setup_token(session, ctx):
|
|
874
|
+
ctx["out"](
|
|
875
|
+
"Claude login did not create isolated credentials; falling back to claude setup-token.\n"
|
|
876
|
+
)
|
|
877
|
+
run_info = _run_interactive_provider_command(
|
|
878
|
+
session,
|
|
879
|
+
"setup-token",
|
|
880
|
+
spawn=ctx.get("spawn"),
|
|
881
|
+
env_override=ctx.get("env"),
|
|
882
|
+
signal_emitter=ctx.get("signal_emitter"),
|
|
883
|
+
)
|
|
884
|
+
transcript_path = run_info.get("transcript_path")
|
|
885
|
+
if not transcript_path or not os.path.isfile(transcript_path):
|
|
886
|
+
raise CdxError(
|
|
887
|
+
"Claude setup-token completed, but cdx could not capture the token. "
|
|
888
|
+
"Run claude setup-token and save the token under credentials/default.json."
|
|
889
|
+
)
|
|
890
|
+
try:
|
|
891
|
+
with open(transcript_path, "r", encoding="utf-8", errors="replace") as handle:
|
|
892
|
+
transcript = handle.read()
|
|
893
|
+
finally:
|
|
894
|
+
try:
|
|
895
|
+
os.remove(transcript_path)
|
|
896
|
+
except OSError:
|
|
897
|
+
pass
|
|
898
|
+
token = _extract_claude_oauth_token(transcript)
|
|
899
|
+
if not token:
|
|
900
|
+
raise CdxError(
|
|
901
|
+
"Claude setup-token completed, but cdx could not find CLAUDE_CODE_OAUTH_TOKEN in the output."
|
|
902
|
+
)
|
|
903
|
+
return _write_claude_oauth_token(session.get("authHome") or "", token)
|
|
904
|
+
|
|
905
|
+
|
|
840
906
|
def handle_add(rest, ctx):
|
|
841
907
|
json_flag, args = _parse_json_flag(rest)
|
|
842
908
|
parsed = _parse_add_args(args)
|
|
@@ -1666,7 +1732,7 @@ def handle_login(rest, ctx):
|
|
|
1666
1732
|
session = ctx["service"]["get_session"](args[0])
|
|
1667
1733
|
if not session:
|
|
1668
1734
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
1669
|
-
if session["provider"]
|
|
1735
|
+
if session["provider"] == PROVIDER_CODEX:
|
|
1670
1736
|
_run_interactive_provider_command(
|
|
1671
1737
|
session, "logout", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1672
1738
|
signal_emitter=ctx.get("signal_emitter")
|
|
@@ -1675,6 +1741,26 @@ def handle_login(rest, ctx):
|
|
|
1675
1741
|
session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1676
1742
|
signal_emitter=ctx.get("signal_emitter")
|
|
1677
1743
|
)
|
|
1744
|
+
auth_probe = _ensure_session_authentication(
|
|
1745
|
+
session,
|
|
1746
|
+
ctx["service"],
|
|
1747
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
1748
|
+
env_override=ctx.get("env"),
|
|
1749
|
+
behavior="probe-only",
|
|
1750
|
+
)
|
|
1751
|
+
if not auth_probe.get("authenticated") and session["provider"] == PROVIDER_CLAUDE:
|
|
1752
|
+
_bootstrap_claude_setup_token(session, ctx)
|
|
1753
|
+
auth_probe = _ensure_session_authentication(
|
|
1754
|
+
session,
|
|
1755
|
+
ctx["service"],
|
|
1756
|
+
spawn_sync=ctx.get("spawn_sync"),
|
|
1757
|
+
env_override=ctx.get("env"),
|
|
1758
|
+
behavior="probe-only",
|
|
1759
|
+
)
|
|
1760
|
+
if not auth_probe.get("authenticated"):
|
|
1761
|
+
raise CdxError(
|
|
1762
|
+
f"Login command completed, but session {session['name']} is still not authenticated."
|
|
1763
|
+
)
|
|
1678
1764
|
now = _local_now_iso()
|
|
1679
1765
|
ctx["service"]["update_auth_state"](args[0], lambda auth: {
|
|
1680
1766
|
**auth, "status": "authenticated",
|
package/src/provider_runtime.py
CHANGED
|
@@ -31,13 +31,16 @@ LAUNCH_PERMISSION_ARGS = {
|
|
|
31
31
|
def _home_env_overrides(auth_home):
|
|
32
32
|
"""Return env vars that point the claude CLI to the given home directory.
|
|
33
33
|
|
|
34
|
-
On Unix, only HOME is needed.
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
On Unix, only HOME is needed. Claude Code resolves its auth files relative
|
|
35
|
+
to HOME; forcing CLAUDE_CONFIG_DIR to this directory makes current Claude
|
|
36
|
+
Code builds ignore otherwise valid isolated credentials. On Windows, Node.js
|
|
37
|
+
resolves the home directory via USERPROFILE (and falls back to
|
|
38
|
+
HOMEDRIVE+HOMEPATH), so we set all three to ensure profile isolation works
|
|
39
|
+
regardless of the platform.
|
|
37
40
|
"""
|
|
38
41
|
overrides = {
|
|
39
42
|
"HOME": auth_home,
|
|
40
|
-
"
|
|
43
|
+
"ANTHROPIC_CONFIG_DIR": auth_home,
|
|
41
44
|
"CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
|
|
42
45
|
}
|
|
43
46
|
if sys.platform == "win32":
|
|
@@ -47,6 +50,48 @@ def _home_env_overrides(auth_home):
|
|
|
47
50
|
return overrides
|
|
48
51
|
|
|
49
52
|
|
|
53
|
+
def _anthropic_profile_name():
|
|
54
|
+
return "default"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _anthropic_credentials_path(auth_home):
|
|
58
|
+
return os.path.join(auth_home, "credentials", f"{_anthropic_profile_name()}.json")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _claude_env(base_env, auth_home):
|
|
62
|
+
env = {**base_env, **_home_env_overrides(auth_home)}
|
|
63
|
+
env.pop("CLAUDE_CONFIG_DIR", None)
|
|
64
|
+
env.pop("CODEX_HOME", None)
|
|
65
|
+
env.setdefault("ANTHROPIC_PROFILE", _anthropic_profile_name())
|
|
66
|
+
return env
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_anthropic_oauth_token(auth_home):
|
|
70
|
+
try:
|
|
71
|
+
with open(_anthropic_credentials_path(auth_home), "r", encoding="utf-8") as handle:
|
|
72
|
+
credentials = json.load(handle)
|
|
73
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
74
|
+
return None
|
|
75
|
+
token = credentials.get("access_token") if isinstance(credentials, dict) else None
|
|
76
|
+
if not token:
|
|
77
|
+
return None
|
|
78
|
+
return str(token)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _read_claude_account_email(auth_home):
|
|
82
|
+
config_path = os.path.join(auth_home, ".claude.json")
|
|
83
|
+
try:
|
|
84
|
+
with open(config_path, "r", encoding="utf-8") as handle:
|
|
85
|
+
config = json.load(handle)
|
|
86
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
87
|
+
return None
|
|
88
|
+
account = config.get("oauthAccount") if isinstance(config, dict) else None
|
|
89
|
+
email = account.get("emailAddress") if isinstance(account, dict) else None
|
|
90
|
+
if not email:
|
|
91
|
+
return None
|
|
92
|
+
return str(email).strip()
|
|
93
|
+
|
|
94
|
+
|
|
50
95
|
def _antigravity_env_overrides(auth_home):
|
|
51
96
|
overrides = {"HOME": auth_home}
|
|
52
97
|
if sys.platform == "win32":
|
|
@@ -187,12 +232,17 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
187
232
|
args = ["--name", session["name"]] + _launch_config_args(session)
|
|
188
233
|
if initial_prompt:
|
|
189
234
|
args.append(initial_prompt)
|
|
235
|
+
auth_home = _get_auth_home(session)
|
|
236
|
+
claude_env = _claude_env(env, auth_home)
|
|
237
|
+
oauth_token = _read_anthropic_oauth_token(auth_home)
|
|
238
|
+
if oauth_token:
|
|
239
|
+
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
190
240
|
return _wrap_launch_with_transcript(session, {
|
|
191
241
|
"command": "claude",
|
|
192
242
|
"args": args,
|
|
193
243
|
"options": {
|
|
194
244
|
"cwd": cwd,
|
|
195
|
-
"env":
|
|
245
|
+
"env": claude_env,
|
|
196
246
|
},
|
|
197
247
|
"label": "claude",
|
|
198
248
|
}, env=env)
|
|
@@ -241,7 +291,11 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
241
291
|
def _build_login_status_spec(session, env_override=None):
|
|
242
292
|
env = {**os.environ, **(env_override or {})}
|
|
243
293
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
244
|
-
|
|
294
|
+
auth_home = _get_auth_home(session)
|
|
295
|
+
env = _claude_env(env, auth_home)
|
|
296
|
+
oauth_token = _read_anthropic_oauth_token(auth_home)
|
|
297
|
+
if oauth_token:
|
|
298
|
+
env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
245
299
|
|
|
246
300
|
def parser(output):
|
|
247
301
|
try:
|
|
@@ -273,9 +327,20 @@ def _build_auth_action_spec(session, action, cwd=None, env_override=None):
|
|
|
273
327
|
cwd = cwd or os.getcwd()
|
|
274
328
|
env = {**os.environ, **(env_override or {})}
|
|
275
329
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
330
|
+
auth_home = _get_auth_home(session)
|
|
331
|
+
env = _claude_env(env, auth_home)
|
|
332
|
+
args = ["auth", action]
|
|
333
|
+
label = f"claude auth {action}"
|
|
334
|
+
if action == "setup-token":
|
|
335
|
+
spec = {"command": "claude", "args": ["setup-token"],
|
|
336
|
+
"options": {"cwd": cwd, "env": env}, "label": "claude setup-token"}
|
|
337
|
+
return _wrap_launch_with_transcript(session, spec, env=env)
|
|
338
|
+
if action == "login":
|
|
339
|
+
email = _read_claude_account_email(auth_home)
|
|
340
|
+
if email:
|
|
341
|
+
args += ["--email", email]
|
|
342
|
+
return {"command": "claude", "args": args,
|
|
343
|
+
"options": {"cwd": cwd, "env": env}, "label": label}
|
|
279
344
|
if session["provider"] == PROVIDER_ANTIGRAVITY:
|
|
280
345
|
if action == "logout":
|
|
281
346
|
raise CdxError("Antigravity logout is managed inside agy. Launch the session and run /logout.")
|
package/src/session_service.py
CHANGED
|
@@ -433,6 +433,8 @@ def create_session_service(options=None):
|
|
|
433
433
|
def _auth_bundle_paths(provider):
|
|
434
434
|
if provider == PROVIDER_CLAUDE:
|
|
435
435
|
return [
|
|
436
|
+
"claude-home/configs/default.json",
|
|
437
|
+
"claude-home/credentials/default.json",
|
|
436
438
|
"claude-home/.claude/.credentials.json",
|
|
437
439
|
"claude-home/.claude.json",
|
|
438
440
|
"claude-home/auth.json",
|