cdx-manager 0.20.3 → 0.20.4

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.3-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.4-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
 
@@ -500,6 +500,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
500
500
  | `cdx label <name> --clear [--json]` | Clear a session label |
501
501
  | `cdx login <name> [--json]` | Re-authenticate a session (logout + login) |
502
502
  | `cdx logout <name> [--json]` | Log out of a session |
503
+ | `cdx auth refresh <name\|all> [--json]` | Make a locked, non-generation Codex auth probe; reports `login_required` when interactive login is needed |
503
504
  | `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
504
505
  | `cdx enable <name> [--json]` | Re-enable a disabled session |
505
506
  | `cdx config <name> [--json]` | Show persistent launch settings for a session |
@@ -1077,6 +1078,24 @@ Without `--yes`, an interactive confirmation is required. `cdx` sends an idempot
1077
1078
 
1078
1079
  ## Troubleshooting
1079
1080
 
1081
+ ### Codex credential refresh
1082
+
1083
+ `cdx auth refresh work` makes a provider-native, non-generation request for one
1084
+ managed Codex profile. It can refresh an expiring access token, but never starts
1085
+ an agent or submits a prompt. Use `cdx auth refresh all --json` when a scheduler
1086
+ needs a stable result; CDX never installs or enables that scheduler for you.
1087
+
1088
+ For example, a user-owned daily cron entry can run:
1089
+
1090
+ ```sh
1091
+ 0 6 * * * cdx auth refresh all --json
1092
+ ```
1093
+
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
+
1080
1099
  - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
1081
1100
  - **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.
1082
1101
  - **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.3
1
+ 0.20.4
@@ -0,0 +1,12 @@
1
+ # CDX Manager 0.20.4
2
+
3
+ ## Authentication
4
+
5
+ - Added `cdx auth refresh <name|all> [--json]`, a locked, no-generation Codex
6
+ authentication probe. It can refresh an active provider session when the
7
+ upstream CLI supports it, but never opens a browser, prompts for credentials,
8
+ or fabricates a token.
9
+ - The result distinguishes `valid`, `locked`, `login_required`, and `failed`
10
+ without exposing credential material.
11
+ - Documented manual use and an opt-in cron example. CDX never installs or
12
+ enables a scheduler on its own.
@@ -342,6 +342,17 @@
342
342
  "x86_64-pc-windows-msvc": "5ceff82ecd907fda949dfc27835112ad9e3597da78191a0ede4f7d9db82607a8",
343
343
  "x86_64-unknown-linux-musl": "db70bb77f08fd954a7378561954d00ad3084b4ada99a4930aa8f021978439e65"
344
344
  }
345
+ },
346
+ "v0.20.3": {
347
+ "github_tarball_sha256": "40990d89ea7130452a95b6cf6a3a63c804f36f08250cf3c325ce7fb63601f875",
348
+ "github_zip_sha256": "be334f6b23145a61d936d50d9609d6a9e6ba2e0749233aede0ed9ce8ff20df8e",
349
+ "tray_assets": {
350
+ "aarch64-apple-darwin": "5ec913f779c11df19aab1f09e8c68dcb4ec54d4a66b3dfbda43d73d67c597797",
351
+ "aarch64-unknown-linux-musl": "d59f41e9521175e22200b38638896d76c2d27f538e52a1f123e1dfdd6521cf4a",
352
+ "x86_64-apple-darwin": "1ec60ca652a18f3991b5d42c64cdf090eb19a658a88005d82642cb6ea9b6455e",
353
+ "x86_64-pc-windows-msvc": "1f9971c1aee7e92685370db1e6a4bcf37b6012fa7452fc14ca3dd545998af82e",
354
+ "x86_64-unknown-linux-musl": "2aabbc00b0fa3a2c8862cc1a5425a074fec99f072bf7b695d31e6e96b9b08c5d"
355
+ }
345
356
  }
346
357
  }
347
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
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.3"
7
+ version = "0.20.4"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -13,6 +13,7 @@ from .cli_commands import (
13
13
  _directory_child_sizes,
14
14
  _directory_size_bytes,
15
15
  handle_add,
16
+ handle_auth,
16
17
  handle_can_resume,
17
18
  handle_clean,
18
19
  handle_config,
@@ -151,6 +152,7 @@ __all__ = [
151
152
 
152
153
 
153
154
  _COMMAND_HANDLERS = {
155
+ "auth": handle_auth,
154
156
  "add": handle_add,
155
157
  "cp": handle_copy,
156
158
  "clean": handle_clean,
@@ -236,6 +238,7 @@ def _print_help(use_color=False):
236
238
  f" {_style('cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
237
239
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
238
240
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
241
+ f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
239
242
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
240
243
  f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--refresh] --json', '36', use_color)}",
241
244
  f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] --json', '36', use_color)}",
package/src/cli_args.py CHANGED
@@ -25,6 +25,7 @@ DOCTOR_USAGE = "Usage: cdx doctor [--severity OK|WARN|FAIL[,OK|WARN|FAIL...]] [-
25
25
  DISK_USAGE = "Usage: cdx disk [profiles] [--candidates] [--json]"
26
26
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
27
27
  UPDATE_USAGE = "Usage: cdx update [all] [--check] [--yes] [--json] [--version TAG]"
28
+ AUTH_USAGE = "Usage: cdx auth refresh <name|all> [--json]"
28
29
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
29
30
  IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
30
31
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text...] [--json]"
@@ -909,6 +910,18 @@ def _parse_update_args(args):
909
910
  return parsed
910
911
 
911
912
 
913
+ def _parse_auth_args(args):
914
+ parsed = _parse_flag_args(args, {
915
+ "--json": {"key": "json", "type": "bool", "default": False},
916
+ }, AUTH_USAGE, positionals_key="values", max_positionals=2)
917
+ if len(parsed["values"]) != 2 or parsed["values"][0] != "refresh":
918
+ raise CdxError(AUTH_USAGE)
919
+ target = parsed["values"][1]
920
+ if not target.strip():
921
+ raise CdxError(AUTH_USAGE)
922
+ return {"target": target, "json": parsed["json"]}
923
+
924
+
912
925
  def _parse_export_args(args):
913
926
  parsed = _parse_flag_args(args, {
914
927
  "--include-auth": {"key": "include_auth", "type": "bool", "default": False},
@@ -41,6 +41,9 @@ from .cli_view import handle_view as handle_view # re-export for cli.py / tests
41
41
  from .commands.auth import ( # re-export for cli.py / tests
42
42
  _confirm_reset as _confirm_reset,
43
43
  )
44
+ from .commands.auth import (
45
+ handle_auth as handle_auth,
46
+ )
44
47
  from .commands.auth import (
45
48
  handle_login as handle_login,
46
49
  )
@@ -276,4 +279,3 @@ from .commands.tray import (
276
279
  handle_tray as handle_tray,
277
280
  )
278
281
 
279
-
@@ -3,12 +3,14 @@
3
3
  Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
4
4
  """
5
5
 
6
+ import json
6
7
  import time
7
8
  import uuid
8
9
 
9
- from ..cli_args import _parse_flag_args, _parse_json_flag
10
+ from ..cli_args import _parse_auth_args, _parse_flag_args, _parse_json_flag
10
11
  from ..cli_helpers import (
11
12
  _bootstrap_claude_setup_token,
13
+ _json_failure,
12
14
  _json_success,
13
15
  _local_now_iso,
14
16
  _make_notify_progress,
@@ -16,7 +18,7 @@ from ..cli_helpers import (
16
18
  _write_json,
17
19
  )
18
20
  from ..cli_render import _success, _warn
19
- from ..codex_usage import consume_codex_rate_limit_reset_credit
21
+ from ..codex_usage import consume_codex_rate_limit_reset_credit, fetch_codex_rate_limit_diagnostic
20
22
  from ..config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
21
23
  from ..errors import CdxError
22
24
  from ..notify import (
@@ -33,6 +35,61 @@ def _confirm_reset(name):
33
35
  answer = input(f"Consume one banked Codex reset for {name}? [y/N] ")
34
36
  return answer.strip().lower() in ("y", "yes")
35
37
 
38
+
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
+ def _refresh_result(session, diagnostic):
49
+ if diagnostic.get("ok"):
50
+ return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "valid"}
51
+ if diagnostic.get("reason") == "auth_locked":
52
+ return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "locked"}
53
+ if _refresh_needs_login(diagnostic):
54
+ return {
55
+ "session": session["name"], "provider": PROVIDER_CODEX,
56
+ "outcome": "login_required", "login_command": f"cdx login {session['name']}",
57
+ }
58
+ return {"session": session["name"], "provider": PROVIDER_CODEX, "outcome": "failed"}
59
+
60
+
61
+ def handle_auth(rest, ctx):
62
+ parsed = _parse_auth_args(rest)
63
+ target = parsed["target"]
64
+ if target == "all":
65
+ sessions = [session for session in ctx["service"]["list_sessions"]()
66
+ if session["provider"] == PROVIDER_CODEX]
67
+ else:
68
+ session = ctx["service"]["get_session"](target)
69
+ if not session:
70
+ raise CdxError(f"Unknown session: {target}")
71
+ if session["provider"] != PROVIDER_CODEX:
72
+ raise CdxError("Authentication refresh is only available for Codex sessions.")
73
+ sessions = [session]
74
+ if not sessions:
75
+ raise CdxError("No managed Codex sessions to refresh.")
76
+
77
+ probe = ctx["options"].get("fetchCodexRateLimitDiagnostic") or fetch_codex_rate_limit_diagnostic
78
+ results = [_refresh_result(session, probe(session)) for session in sessions]
79
+ 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"
81
+ if parsed["json"]:
82
+ payload = (
83
+ _json_failure("auth.refresh", "auth_refresh_failed", message, results=results)
84
+ if failures else _json_success("auth.refresh", message, results=results)
85
+ )
86
+ _write_json(ctx, payload)
87
+ else:
88
+ for result in results:
89
+ suffix = f"; run {result['login_command']}" if result.get("login_command") else ""
90
+ ctx["out"](f"{result['session']}: {result['outcome']}{suffix}\n")
91
+ return 1 if failures else 0
92
+
36
93
  def handle_ready(rest, ctx):
37
94
  """`cdx ready`: schedule an OS notification for the next session to come back.
38
95