cdx-manager 0.20.4 → 0.20.5

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.20.4-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.20.5-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
8
8
 
9
9
  **Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
10
10
 
@@ -1092,11 +1092,15 @@ For example, a user-owned daily cron entry can run:
1092
1092
  ```
1093
1093
 
1094
1094
  `locked` means an interactive session owns the profile refresh lock and was left
1095
- untouched. `login_required` means the provider rejected the local refresh token;
1096
- run the supplied `cdx login <name>` command interactively. Hosts keep independent
1097
- rotating credentials, so copying an auth file is not ongoing credential sync.
1098
-
1099
- - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
1095
+ untouched. If every selected profile is locked, the command is non-zero because
1096
+ it verified no credential; a mix of valid and locked profiles remains successful.
1097
+ Bulk refresh skips disabled profiles, while `cdx auth refresh <name>` still lets
1098
+ an operator inspect one explicitly. `login_required` means the provider rejected
1099
+ the local refresh token; run the supplied `cdx login <name>` command interactively.
1100
+ Hosts keep independent rotating credentials, so copying an auth file is not ongoing
1101
+ credential sync.
1102
+
1103
+ - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first. `cdx doctor` uses the same Codex app-server probe as `cdx auth refresh`, so a local auth file alone is never reported as live authentication; a rejected refresh token is reported as `login_required` with the matching login command.
1100
1104
  - **One of two Codex accounts keeps asking for login** — run `cdx doctor --json` and inspect each Codex session's `codex_auth_file`, `codex_live_auth`, and `codex_stale_auth_logs` checks. If recent logs mention expired auth, repair only that isolated profile with `cdx login <name>`. Codex Business profiles can share the same `tokens.account_id`; `cdx doctor` reports `codex_shared_account_id` as a workspace-level clue, not proof that two profiles use the same user. Newly created sessions seed from the current global `~/.codex/auth.json` when one exists. For two separate Codex accounts, create or repair each session by running `cdx login <name>` for that session; `cdx login` does not log out first, so use `cdx logout <name>` explicitly only when you want to clear that isolated profile.
1101
1105
  - **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
1102
1106
  - **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.4
1
+ 0.20.5
@@ -0,0 +1,22 @@
1
+ # CDX Manager 0.20.5
2
+
3
+ ## Fixes
4
+
5
+ - `cdx doctor` now uses the live Codex app-server authentication probe instead
6
+ of trusting a local auth file, so an expired or rejected refresh token is
7
+ reported as `login_required` with an actionable `cdx login <name>` remedy.
8
+ - `cdx auth refresh all` skips disabled Codex profiles, while an explicit
9
+ `cdx auth refresh <name>` still lets an operator inspect a disabled profile
10
+ deliberately.
11
+ - A bulk Codex auth refresh where every selected profile is locked now exits
12
+ non-zero as `auth_refresh_indeterminate` instead of looking like a silent
13
+ success without verifying any credential.
14
+ - Codex app-server diagnostics no longer keep an unread stderr pipe while
15
+ waiting for stdout, avoiding a possible deadlock or timeout from noisy
16
+ provider diagnostics.
17
+
18
+ ## Validation
19
+
20
+ - Package smoke coverage now runs `cdx doctor --json` from both packed npm and
21
+ wheel installs with isolated local provider shims, so packaged doctor
22
+ regressions are caught outside the source checkout.
@@ -353,6 +353,17 @@
353
353
  "x86_64-pc-windows-msvc": "1f9971c1aee7e92685370db1e6a4bcf37b6012fa7452fc14ca3dd545998af82e",
354
354
  "x86_64-unknown-linux-musl": "2aabbc00b0fa3a2c8862cc1a5425a074fec99f072bf7b695d31e6e96b9b08c5d"
355
355
  }
356
+ },
357
+ "v0.20.4": {
358
+ "github_tarball_sha256": "55323fe428d6d297f14a210a590757443080dc55a87bb6fae4d967fddb505af3",
359
+ "github_zip_sha256": "37f4abb392b372d0f5fe5d5eb7c46e1e81a30b319d1961b5518f20e776f59b75",
360
+ "tray_assets": {
361
+ "aarch64-apple-darwin": "6d61a45ab6f310a83aefbb80d77dc31b1f3a738640197bcca1264c6ca7b70d26",
362
+ "aarch64-unknown-linux-musl": "f2f3a2c45c7deb2ea69cdc7c7b68524e6b5db5d1eda0ff902bda49d875636402",
363
+ "x86_64-apple-darwin": "93d3d9180588edcf573ffa8f9f2e870e8dff9659e198cde531bec0acfc37184f",
364
+ "x86_64-pc-windows-msvc": "7508e04ee07adb7a500b346be1d3891d93aec54dc0ff53643bd8c57e4e411118",
365
+ "x86_64-unknown-linux-musl": "47a1c8cff60c896db659d30d74496045d8c343e74a00f826e608f3973e735532"
366
+ }
356
367
  }
357
368
  }
358
369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.20.4",
3
+ "version": "0.20.5",
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.20.4"
7
+ version = "0.20.5"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -199,6 +199,15 @@ def fetch_codex_rate_limit_diagnostic(session, timeout=5, popen_factory=None):
199
199
  return _probe_codex_rate_limit_diagnostic(session, auth_home, timeout, popen_factory)
200
200
 
201
201
 
202
+ def diagnostic_needs_codex_login(diagnostic):
203
+ """Classify known auth failures without returning provider text."""
204
+ text = json.dumps(diagnostic.get("response") or {}).lower()
205
+ return any(marker in text for marker in (
206
+ "not logged in", "unauthorized", "authentication token is expired",
207
+ "token_expired", "invalid_grant", "http 401",
208
+ ))
209
+
210
+
202
211
  def _probe_codex_rate_limit_diagnostic(session, auth_home, timeout=5, popen_factory=None):
203
212
  diagnostic = _request_codex_app_server(
204
213
  auth_home,
@@ -236,7 +245,7 @@ def _request_codex_app_server(auth_home, method, params, failure_reason, timeout
236
245
  ["codex", "app-server", "--listen", "stdio://"],
237
246
  stdin=subprocess.PIPE,
238
247
  stdout=subprocess.PIPE,
239
- stderr=subprocess.PIPE,
248
+ stderr=subprocess.DEVNULL,
240
249
  env=env,
241
250
  text=True,
242
251
  bufsize=1,
@@ -3,7 +3,6 @@
3
3
  Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
4
4
  """
5
5
 
6
- import json
7
6
  import time
8
7
  import uuid
9
8
 
@@ -18,7 +17,11 @@ from ..cli_helpers import (
18
17
  _write_json,
19
18
  )
20
19
  from ..cli_render import _success, _warn
21
- from ..codex_usage import consume_codex_rate_limit_reset_credit, fetch_codex_rate_limit_diagnostic
20
+ from ..codex_usage import (
21
+ consume_codex_rate_limit_reset_credit,
22
+ diagnostic_needs_codex_login,
23
+ fetch_codex_rate_limit_diagnostic,
24
+ )
22
25
  from ..config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
23
26
  from ..errors import CdxError
24
27
  from ..notify import (
@@ -36,21 +39,12 @@ def _confirm_reset(name):
36
39
  return answer.strip().lower() in ("y", "yes")
37
40
 
38
41
 
39
- def _refresh_needs_login(diagnostic):
40
- """Classify known provider auth failures without returning provider text."""
41
- text = json.dumps(diagnostic.get("response") or {}).lower()
42
- return any(marker in text for marker in (
43
- "not logged in", "unauthorized", "authentication token is expired",
44
- "token_expired", "invalid_grant", "http 401",
45
- ))
46
-
47
-
48
42
  def _refresh_result(session, diagnostic):
49
43
  if diagnostic.get("ok"):
50
44
  return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "valid"}
51
45
  if diagnostic.get("reason") == "auth_locked":
52
46
  return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "locked"}
53
- if _refresh_needs_login(diagnostic):
47
+ if diagnostic_needs_codex_login(diagnostic):
54
48
  return {
55
49
  "session": session["name"], "provider": PROVIDER_CODEX,
56
50
  "outcome": "login_required", "login_command": f"cdx login {session['name']}",
@@ -63,7 +57,7 @@ def handle_auth(rest, ctx):
63
57
  target = parsed["target"]
64
58
  if target == "all":
65
59
  sessions = [session for session in ctx["service"]["list_sessions"]()
66
- if session["provider"] == PROVIDER_CODEX]
60
+ if session["provider"] == PROVIDER_CODEX and session.get("enabled", True)]
67
61
  else:
68
62
  session = ctx["service"]["get_session"](target)
69
63
  if not session:
@@ -71,24 +65,32 @@ def handle_auth(rest, ctx):
71
65
  if session["provider"] != PROVIDER_CODEX:
72
66
  raise CdxError("Authentication refresh is only available for Codex sessions.")
73
67
  sessions = [session]
74
- if not sessions:
68
+ if not sessions and target != "all":
75
69
  raise CdxError("No managed Codex sessions to refresh.")
76
70
 
77
71
  probe = ctx["options"].get("fetchCodexRateLimitDiagnostic") or fetch_codex_rate_limit_diagnostic
78
72
  results = [_refresh_result(session, probe(session)) for session in sessions]
79
73
  failures = [result for result in results if result["outcome"] in ("login_required", "failed")]
80
- message = "Codex authentication probe completed" if not failures else "Codex authentication probe needs attention"
74
+ all_locked = bool(results) and all(result["outcome"] == "locked" for result in results)
75
+ message = (
76
+ "Codex authentication probe needs attention" if failures
77
+ else "Codex authentication probe could not verify any session" if all_locked
78
+ else "No enabled Codex sessions to refresh" if not results
79
+ else "Codex authentication probe completed"
80
+ )
81
81
  if parsed["json"]:
82
82
  payload = (
83
83
  _json_failure("auth.refresh", "auth_refresh_failed", message, results=results)
84
84
  if failures else _json_success("auth.refresh", message, results=results)
85
85
  )
86
+ if all_locked:
87
+ payload = _json_failure("auth.refresh", "auth_refresh_indeterminate", message, results=results)
86
88
  _write_json(ctx, payload)
87
89
  else:
88
90
  for result in results:
89
91
  suffix = f"; run {result['login_command']}" if result.get("login_command") else ""
90
92
  ctx["out"](f"{result['session']}: {result['outcome']}{suffix}\n")
91
- return 1 if failures else 0
93
+ return 1 if failures or all_locked else 0
92
94
 
93
95
  def handle_ready(rest, ctx):
94
96
  """`cdx ready`: schedule an OS notification for the next session to come back.
package/src/health.py CHANGED
@@ -337,11 +337,16 @@ def _codex_auth_issues(session, diag):
337
337
  ]
338
338
  live_status = diag.get("live_status")
339
339
  live_ok = live_status == "authenticated"
340
- live_warn = live_status in ("logged_out", "error")
340
+ live_warn = live_status in ("logged_out", "login_required", "error")
341
+ live_message = (
342
+ f"session {name} Codex authentication requires login; run cdx login {name}"
343
+ if live_status == "login_required"
344
+ else f"session {name} live Codex auth status: {live_status}"
345
+ )
341
346
  issues.append(_issue(
342
347
  "OK" if live_ok else "WARN" if live_warn else "WARN",
343
348
  "codex_live_auth",
344
- f"session {name} live Codex auth status: {live_status}",
349
+ live_message,
345
350
  {
346
351
  "session": name,
347
352
  "auth_home": diag.get("auth_home"),
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
13
13
 
14
14
  from .agent_notify import launch_notify_env, notifications_enabled
15
15
  from .claude_usage import _clean_oauth_token, _decode_jwt_claims
16
- from .codex_usage import codex_auth_lock
16
+ from .codex_usage import codex_auth_lock, diagnostic_needs_codex_login, fetch_codex_rate_limit_diagnostic
17
17
  from .config import (
18
18
  PROVIDER_ANTIGRAVITY,
19
19
  PROVIDER_CLAUDE,
@@ -294,19 +294,21 @@ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
294
294
  "live_status": "unknown",
295
295
  "live_error": None,
296
296
  }
297
- try:
298
- status = _probe_provider_auth_status(
299
- session,
300
- spawn_sync=spawn_sync,
301
- env_override=env_override,
302
- trust_local_credentials=False,
303
- )
304
- result["live_status"] = status
305
- if status == AUTH_PROBE_DEGRADED:
306
- result["live_error"] = _auth_probe_degraded_message(session)
307
- except CdxError as error:
297
+ if not result["local_tokens_present"]:
298
+ result["live_status"] = AUTH_PROBE_LOGGED_OUT
299
+ return result
300
+ diagnostic = fetch_codex_rate_limit_diagnostic(session)
301
+ if diagnostic["ok"]:
302
+ result["live_status"] = AUTH_PROBE_AUTHENTICATED
303
+ elif diagnostic["reason"] == "auth_locked":
304
+ result["live_status"] = AUTH_PROBE_DEGRADED
305
+ result["live_error"] = "Codex authentication probe is locked by an active session."
306
+ elif diagnostic_needs_codex_login(diagnostic):
307
+ result["live_status"] = "login_required"
308
+ result["live_error"] = f"Codex authentication requires login; run cdx login {session['name']}."
309
+ else:
308
310
  result["live_status"] = "error"
309
- result["live_error"] = str(error)
311
+ result["live_error"] = f"Codex app-server authentication probe failed ({diagnostic['reason']})."
310
312
  return result
311
313
 
312
314