cdx-manager 0.21.2 → 0.21.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <br clear="left"/>
6
6
 
7
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.21.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
7
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.21.3-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
8
8
 
9
9
  **The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
10
10
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.21.2
1
+ 0.21.3
@@ -0,0 +1,5 @@
1
+ # CDX Manager 0.21.3
2
+
3
+ ## Fixes
4
+
5
+ - Claude in Chrome now works from CDX Claude profiles (`claude --chrome`, `/chrome`, or `cdx set <name> --extra-args '--chrome'`). Profile isolation redirects HOME, so Claude Code looked for Chrome and its Claude extension inside the profile home, found neither, and never installed its native-messaging host where Chrome reads it. CDX now links the real Chrome directory into each Claude profile home, as it already did for the macOS keychain: `~/Library/Application Support/Google/Chrome` on macOS, `~/.config/google-chrome` on Linux. Windows needed no change: Chrome lives under `%LOCALAPPDATA%` and the host is registered in the registry, neither of which CDX redirects. Profile copies keep the link as a link and never copy the Chrome profile itself.
@@ -496,6 +496,17 @@
496
496
  "x86_64-pc-windows-msvc": "c316d753c055be48d74e23537ca66093a1a7c36df8fb8518d6b909484ccb9415",
497
497
  "x86_64-unknown-linux-musl": "f5a034ba301ecf10de33498cd99f00fa3aa6afda94d7c743390af05f4f718790"
498
498
  }
499
+ },
500
+ "v0.21.2": {
501
+ "github_tarball_sha256": "b6225a95b590cc931988260597b870edbff00b3175f5c8091a0e23503d408e5e",
502
+ "github_zip_sha256": "b0714ed222e17ae9bdd658c37b6ba32b9026ddddfb4d20794783b2203f4ccc88",
503
+ "tray_assets": {
504
+ "aarch64-apple-darwin": "c4b8938b62fad445351938f79e0cd42d5342a530e88eeb0f52c51e2fe77e3391",
505
+ "aarch64-unknown-linux-musl": "6f9c959e4a17a39c3d548b48afe326ca2d68bee0e298f532c4880d080d41eb20",
506
+ "x86_64-apple-darwin": "f41652aa24d27e2e4deef566cd66c11fc7cb3b5155dc8d5a917433a3e172d33b",
507
+ "x86_64-pc-windows-msvc": "856f9dc459182adb10f66f2836abc862be82feff1cc3f1cf859764336487d781",
508
+ "x86_64-unknown-linux-musl": "f1832fdbe7114b4625da21d6a3a0f0c100f18d3a441796e809e568620e4cd345"
509
+ }
499
510
  }
500
511
  }
501
512
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.21.2",
3
+ "version": "0.21.3",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.21.2"
7
+ version = "0.21.3"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -117,36 +117,52 @@ def claude_profile_store_dir(auth_home):
117
117
  return os.path.join(auth_home, "credentials")
118
118
 
119
119
 
120
- def _link_macos_keychain(auth_home):
121
- """Make the operator's login keychain reachable from a redirected home.
122
-
123
- macOS resolves the login keychain through `$HOME/Library/Keychains`.
124
- Profile isolation redirects HOME (see `_home_env_overrides`), so that path
125
- does not exist: every Claude Code launch then raises a "Keychain Not Found"
126
- dialog before falling back to a plaintext credentials file. Linking the
127
- real keychain directory into the profile home restores the lookup. Failure
128
- is non-fatal, and deliberately quiet: the profile simply keeps the
129
- file-based credentials it already used.
120
+ # Real-home directories a redirected profile home must still reach on macOS.
121
+ # Keychains: the login keychain (else a "Keychain Not Found" dialog on every launch).
122
+ # Chrome: Claude in Chrome looks for its extension and installs its native-messaging
123
+ # host under $HOME/Library/Application Support/Google/Chrome; without the link,
124
+ # `claude --chrome` sees no extension and Chrome never finds the host.
125
+ # Linux keeps Chrome under ~/.config (XDG), also resolved from HOME. Windows needs
126
+ # nothing: the host is registered in the registry and Chrome lives in
127
+ # %LOCALAPPDATA%, which profile isolation does not redirect.
128
+ _MACOS_SHARED_DIRS = (
129
+ ("Library", "Keychains"),
130
+ ("Library", "Application Support", "Google", "Chrome"),
131
+ )
132
+ _LINUX_SHARED_DIRS = ((".config", "google-chrome"),)
133
+
134
+
135
+ def _link_real_home_dirs(auth_home):
136
+ """Link the real-home directories in `_MACOS_SHARED_DIRS` (or `_LINUX_SHARED_DIRS`) into a redirected home.
137
+
138
+ Profile isolation redirects HOME (see `_home_env_overrides`), so these
139
+ paths do not exist in the profile home. A real directory already there is
140
+ left alone. Failure is non-fatal, and deliberately quiet: the profile
141
+ simply keeps working without that integration.
130
142
  """
131
- if sys.platform != "darwin" or pwd is None:
143
+ shared = _MACOS_SHARED_DIRS if sys.platform == "darwin" else _LINUX_SHARED_DIRS if sys.platform.startswith("linux") else ()
144
+ if not shared or pwd is None:
132
145
  return
133
146
  try:
134
147
  real_home = pwd.getpwuid(os.getuid()).pw_dir
135
148
  except (KeyError, OSError):
136
149
  return
137
- source = os.path.join(real_home, "Library", "Keychains")
138
- if not os.path.isdir(source):
139
- return
140
- link = os.path.join(auth_home, "Library", "Keychains")
141
- if os.path.realpath(link) == os.path.realpath(source):
142
- return
143
- try:
144
- os.makedirs(os.path.dirname(link), exist_ok=True)
145
- if os.path.islink(link):
146
- os.unlink(link)
147
- os.symlink(source, link)
148
- except OSError:
149
- return
150
+ for parts in shared:
151
+ source = os.path.join(real_home, *parts)
152
+ if not os.path.isdir(source):
153
+ continue
154
+ link = os.path.join(auth_home, *parts)
155
+ if os.path.realpath(link) == os.path.realpath(source):
156
+ continue
157
+ if os.path.exists(link) and not os.path.islink(link):
158
+ continue
159
+ try:
160
+ os.makedirs(os.path.dirname(link), exist_ok=True)
161
+ if os.path.islink(link):
162
+ os.unlink(link)
163
+ os.symlink(source, link)
164
+ except OSError:
165
+ continue
150
166
 
151
167
 
152
168
  def _secure_storage_overrides(auth_home):
@@ -167,7 +183,7 @@ def _secure_storage_overrides(auth_home):
167
183
  the variable overrides that directory as well as naming the entry. Any
168
184
  other value would also move where credentials are read from.
169
185
  """
170
- _link_macos_keychain(auth_home)
186
+ _link_real_home_dirs(auth_home)
171
187
  return {"CLAUDE_SECURESTORAGE_CONFIG_DIR": os.path.join(auth_home, ".claude")}
172
188
 
173
189
 
@@ -35,7 +35,7 @@ from contextlib import nullcontext
35
35
  from functools import partial
36
36
 
37
37
  from .claude_credentials import copy_keychain_credentials, delete_keychain_credentials
38
- from .claude_usage import _link_macos_keychain
38
+ from .claude_usage import _link_real_home_dirs
39
39
  from .codex_usage import fetch_codex_rate_limits
40
40
  from .config import (
41
41
  MAX_LAUNCH_BUDGET_USD,
@@ -573,8 +573,9 @@ def copy_session(base_dir, store, source_name, dest_name):
573
573
  with credential_copy:
574
574
  if source["provider"] == PROVIDER_CLAUDE:
575
575
  shutil.copytree(source_root, temp_root, symlinks=True,
576
- ignore=lambda _directory, names: [n for n in names if n == "Keychains"])
577
- _link_macos_keychain(os.path.join(temp_root, "claude-home"))
576
+ ignore=lambda directory, names: [n for n in names if n == "Keychains" or (
577
+ n == "Chrome" and os.path.islink(os.path.join(directory, n)))])
578
+ _link_real_home_dirs(os.path.join(temp_root, "claude-home"))
578
579
  else:
579
580
  shutil.copytree(source_root, temp_root)
580
581
  if existing: