cdx-manager 0.9.11 → 0.9.13

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
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.9.11-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.9.13-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -0,0 +1,36 @@
1
+ # CDX Manager 0.9.12
2
+
3
+ ## Highlights
4
+
5
+ - Prevented accidental concurrent launches that reuse the same provider profile and can invalidate shared OAuth credentials.
6
+ - Made Claude add/login prefer `claude setup-token` so new Claude sessions use the long-lived token flow by default.
7
+
8
+ ## Changes
9
+
10
+ ### Session launch safety
11
+
12
+ `cdx` now detects when another live session already uses the same provider profile before launching a new one.
13
+
14
+ On an interactive terminal, the CLI asks for confirmation before starting the second session. In non-interactive contexts, the launch fails with a clear message so automation does not silently create conflicting sessions.
15
+
16
+ This protects profiles that share one credentials file, where concurrent token refreshes can rotate credentials and log out the other session.
17
+
18
+ ### Claude token setup
19
+
20
+ Claude add and login flows now route through `claude setup-token` by default, which produces a longer-lived token than the standard login flow.
21
+
22
+ When this setup-token flow succeeds, stale Claude credentials are removed so the newly configured token is the one used at launch. The existing `--setup-token` flag remains accepted for compatibility.
23
+
24
+ Regression coverage verifies the default Claude setup-token behavior, stale credential cleanup, and compatibility of the retained flag.
25
+
26
+ ## Validation
27
+
28
+ - `npm run release:validate`
29
+ - `npm run lint`
30
+ - `npm test`
31
+ - `logics-manager lint --require-status`
32
+ - `logics-manager audit`
33
+ - `git diff --check`
34
+ - `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
35
+ - `python -m build`
36
+ - `python -m twine check dist/*`
@@ -0,0 +1,27 @@
1
+ # CDX Manager 0.9.13
2
+
3
+ ## Highlights
4
+
5
+ - Fixed Claude `setup-token` capture on Linux by using the util-linux `script` flush flag.
6
+
7
+ ## Changes
8
+
9
+ ### Claude setup-token capture
10
+
11
+ Linux terminals now run transcript capture with `script -f` instead of the BSD/macOS-only `script -F` spelling.
12
+
13
+ This prevents util-linux `script` from exiting with `invalid option -- 'F'`, which previously made `cdx` fall back to running `claude setup-token` without transcript capture and miss the printed token.
14
+
15
+ macOS keeps the existing `script -F` behavior.
16
+
17
+ ## Validation
18
+
19
+ - `npm run release:validate`
20
+ - `npm run lint`
21
+ - `npm test`
22
+ - `logics-manager lint --require-status`
23
+ - `logics-manager audit`
24
+ - `git diff --check`
25
+ - `npm --cache /private/tmp/cdx-npm-cache pack --dry-run`
26
+ - `python -m build`
27
+ - `python -m twine check dist/*`
@@ -108,6 +108,14 @@
108
108
  "v0.9.10": {
109
109
  "github_tarball_sha256": "4e876d3552eca1b441492a36606f2f0583bff9523801a923d834fe2a4da3c644",
110
110
  "github_zip_sha256": "b6c9435f7e36558ac8698dbd6d6c4888e58c64a08d2b06c3bcbb45ce2cd25f7c"
111
+ },
112
+ "v0.9.11": {
113
+ "github_tarball_sha256": "8ab8f673af4ad9c0e4fd64110d47285a1de02c095fd0f6d79eb67e061255093d",
114
+ "github_zip_sha256": "ee5fffa543b8a02c082a180c624d31951c5ff662d492132cd83d4f099da70441"
115
+ },
116
+ "v0.9.12": {
117
+ "github_tarball_sha256": "ed9de98e1b4df5e42a1e389580f03d4b4b2e09ee8f63d23110090e6f164b8ae6",
118
+ "github_zip_sha256": "55f9ead146411ae740db1cae3f91c56d6ebe33c03345f9d4bfd487e45962e412"
111
119
  }
112
120
  }
113
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
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.9.11"
7
+ version = "0.9.13"
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
@@ -66,7 +66,7 @@ from .status_view import (
66
66
  )
67
67
  from .update_check import check_for_update, check_logics_manager_for_update
68
68
 
69
- VERSION = "0.9.11"
69
+ VERSION = "0.9.13"
70
70
 
71
71
  # Public surface: this module is a facade. Names below are imported above
72
72
  # purely to be re-exported (consumed by tests and external callers); listing
@@ -209,7 +209,14 @@ def _bootstrap_claude_setup_token(session, ctx):
209
209
  os.remove(transcript_path)
210
210
  except OSError:
211
211
  pass
212
- return _write_claude_oauth_token(session.get("authHome") or "", token)
212
+ auth_home = session.get("authHome") or ""
213
+ cred_path = _write_claude_oauth_token(auth_home, token)
214
+ # ponytail: drop short-lived claude login creds so the ~1yr token wins at launch (provider_runtime _read_claude_launch_oauth_token)
215
+ try:
216
+ os.remove(os.path.join(auth_home, ".claude", ".credentials.json"))
217
+ except OSError:
218
+ pass
219
+ return cred_path
213
220
 
214
221
 
215
222
  def handle_add(rest, ctx):
@@ -223,16 +230,20 @@ def handle_add(rest, ctx):
223
230
  if parsed.get("model"):
224
231
  session = ctx["service"]["set_launch_settings"](parsed["name"], {"model": parsed["model"]})
225
232
  message = f"Created session {parsed['name']} ({parsed['provider']})"
226
- _ensure_session_authentication(
227
- session,
228
- ctx["service"],
229
- spawn=ctx.get("spawn"),
230
- spawn_sync=ctx.get("spawn_sync"),
231
- env_override=ctx.get("env"),
232
- stdin_is_tty=ctx["stdin_is_tty"],
233
- behavior="bootstrap",
234
- signal_emitter=ctx.get("signal_emitter"),
235
- )
233
+ if session["provider"] == PROVIDER_CLAUDE and ctx["stdin_is_tty"]:
234
+ # ponytail: mint a ~1yr setup-token instead of the ~daily claude login token
235
+ _bootstrap_claude_setup_token(session, ctx)
236
+ else:
237
+ _ensure_session_authentication(
238
+ session,
239
+ ctx["service"],
240
+ spawn=ctx.get("spawn"),
241
+ spawn_sync=ctx.get("spawn_sync"),
242
+ env_override=ctx.get("env"),
243
+ stdin_is_tty=ctx["stdin_is_tty"],
244
+ behavior="bootstrap",
245
+ signal_emitter=ctx.get("signal_emitter"),
246
+ )
236
247
  now = _local_now_iso()
237
248
  ctx["service"]["update_auth_state"](parsed["name"], lambda auth: {
238
249
  **auth,
@@ -1809,6 +1820,7 @@ def handle_import(rest, ctx):
1809
1820
 
1810
1821
 
1811
1822
  def handle_login(rest, ctx):
1823
+ rest = [arg for arg in rest if arg != "--setup-token"] # accepted for back-compat; setup-token is now the default for Claude
1812
1824
  json_flag, args = _parse_json_flag(rest)
1813
1825
  if len(args) != 1:
1814
1826
  raise CdxError("Usage: cdx login <name> [--json]")
@@ -1817,10 +1829,14 @@ def handle_login(rest, ctx):
1817
1829
  session = ctx["service"]["get_session"](args[0])
1818
1830
  if not session:
1819
1831
  raise CdxError(f"Unknown session: {args[0]}")
1820
- _run_interactive_provider_command(
1821
- session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1822
- signal_emitter=ctx.get("signal_emitter")
1823
- )
1832
+ if session["provider"] == PROVIDER_CLAUDE:
1833
+ # ponytail: setup-token mints a ~1yr token; claude login mints a ~daily one that forces reconnects
1834
+ _bootstrap_claude_setup_token(session, ctx)
1835
+ else:
1836
+ _run_interactive_provider_command(
1837
+ session, "login", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
1838
+ signal_emitter=ctx.get("signal_emitter")
1839
+ )
1824
1840
  auth_probe = _ensure_session_authentication(
1825
1841
  session,
1826
1842
  ctx["service"],
@@ -1829,16 +1845,6 @@ def handle_login(rest, ctx):
1829
1845
  behavior="probe-only",
1830
1846
  trust_local_credentials=False,
1831
1847
  )
1832
- if not auth_probe.get("authenticated") and session["provider"] == PROVIDER_CLAUDE:
1833
- _bootstrap_claude_setup_token(session, ctx)
1834
- auth_probe = _ensure_session_authentication(
1835
- session,
1836
- ctx["service"],
1837
- spawn_sync=ctx.get("spawn_sync"),
1838
- env_override=ctx.get("env"),
1839
- behavior="probe-only",
1840
- trust_local_credentials=False,
1841
- )
1842
1848
  if not auth_probe.get("authenticated"):
1843
1849
  raise CdxError(
1844
1850
  f"Login command completed, but session {session['name']} is still not authenticated."
@@ -2054,11 +2060,33 @@ def handle_resume(rest, ctx):
2054
2060
  return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
2055
2061
 
2056
2062
 
2063
+ def _warn_if_session_already_running(name, ctx):
2064
+ # ponytail: concurrent sessions on one profile share its single OAuth
2065
+ # .credentials.json; both refresh near token expiry and the rotated refresh
2066
+ # token invalidates the other session -> surprise logout. Warn before adding
2067
+ # a second live session on the same profile.
2068
+ active = ctx["service"].get("active_session_runtime")
2069
+ runtime = active(name) if active else None
2070
+ if not runtime:
2071
+ return
2072
+ pid = runtime.get("pid")
2073
+ ctx["out"](f"{_warn(f'A session named {name} is already running (pid {pid}).', ctx['use_color'])}\n")
2074
+ ctx["out"](
2075
+ f"{_dim('Two sessions on one profile share its login and can log each other out. Use a different profile instead.', ctx['use_color'])}\n"
2076
+ )
2077
+ if not ctx["stdin_is_tty"]:
2078
+ return
2079
+ answer = input(f"Launch a second {name} session anyway? [y/N] ")
2080
+ if answer.strip().lower() not in ("y", "yes"):
2081
+ raise CdxError("Launch cancelled.")
2082
+
2083
+
2057
2084
  def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
2058
2085
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
2059
2086
  if force_json is not None:
2060
2087
  json_flag = force_json
2061
2088
  warnings = _update_notice_warnings(ctx)
2089
+ _warn_if_session_already_running(command, ctx)
2062
2090
  session = ctx["service"]["launch_session"](command)
2063
2091
  capability = _resume_capability_for_session(session, ctx) if resume else None
2064
2092
  if capability and not capability["resumable"]:
@@ -375,8 +375,9 @@ def _wrap_launch_with_transcript(session, spec, capture_transcript=True, env=Non
375
375
 
376
376
  def _default_script_args(transcript_path, spec):
377
377
  if sys.platform.startswith("linux"):
378
+ # util-linux `script` flushes with -f (lowercase); -F is the BSD/macOS spelling and errors here.
378
379
  command = shlex.join([spec["command"]] + spec["args"])
379
- return ["-q", "-F", "-c", command, transcript_path]
380
+ return ["-q", "-f", "-c", command, transcript_path]
380
381
  return ["-q", "-F", transcript_path, spec["command"]] + spec["args"]
381
382
 
382
383
 
@@ -1391,6 +1391,7 @@ def create_session_service(options=None):
1391
1391
  "copy_session": copy_session,
1392
1392
  "rename_session": rename_session,
1393
1393
  "launch_session": launch_session,
1394
+ "active_session_runtime": _session_runtime,
1394
1395
  "start_session_runtime": start_session_runtime,
1395
1396
  "finish_session_runtime": finish_session_runtime,
1396
1397
  "ensure_session_state": ensure_session_state,