cdx-manager 0.9.3 → 0.9.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
@@ -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.1-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.4-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
 
@@ -329,6 +329,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
329
329
  | `cdx --json` | List all sessions as a machine-readable JSON payload |
330
330
  | `cdx <name>` | Launch a session (checks auth first) |
331
331
  | `cdx <name> [--json]` | Launch a session; `--json` returns a structured success payload after the interactive run ends |
332
+ | `cdx <name> -r` / `cdx <name> --resume` | Resume the provider-native conversation for a session when supported |
332
333
  | `cdx add [provider] <name> [--model MODEL] [--json]` | Register a new session (`provider`: `codex`, `claude`, `antigravity`, or `ollama`; Ollama requires `--model`) |
333
334
  | `cdx cp <source> <dest> [--json]` | Copy a session into another session name, overwriting the destination if it exists |
334
335
  | `cdx ren <source> <dest> [--json]` | Rename a session and move its auth data |
@@ -343,6 +344,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
343
344
  | `cdx unset <name>\|--sessions all\|a,b\|--provider PROVIDER (--power\|--permission\|--fast\|--rtk\|--logics\|--model\|--priority\|--all) [--json]` | Remove persisted launch settings and fall back to provider defaults |
344
345
  | `cdx history [name] [--limit N] [--summary] [--since 7d\|today\|DATE] [--from DATE] [--to DATE] [--json]` | Show recent launch history or aggregate total launch time per assistant, optionally filtered by period |
345
346
  | `cdx last [--json]` | Launch the most recent existing session from launch history |
347
+ | `cdx resume <name> [--json]` | Resume the provider-native conversation for a session using the named command form |
348
+ | `cdx can-resume <name> [--json]` | Check whether a session supports native resume without launching the provider |
346
349
  | `cdx context show\|path\|init\|edit\|clear\|set [text...] [--json]` | Manage the shared Markdown context for the current workspace |
347
350
  | `cdx handoff <name> [--json]` | Install the current workspace context into a target session and launch it unless `--json` is used |
348
351
  | `cdx handoff <source> <target> [--json]` | Build shared context from the source session's latest launch transcript, install it into the target session, and launch the target unless `--json` is used; supports cross-provider handoff |
@@ -0,0 +1,43 @@
1
+ # CDX Manager 0.9.4
2
+
3
+ ## Highlights
4
+
5
+ - Added provider-native resume commands for named sessions.
6
+ - Added a non-launching capability check for resume support.
7
+ - Closed the Logics workflow for the resume command delivery.
8
+
9
+ ## Changes
10
+
11
+ ### Provider-native resume commands
12
+
13
+ `cdx` can now resume supported provider conversations without requiring users to remember provider-specific commands:
14
+
15
+ ```
16
+ cdx main -r
17
+ cdx main --resume
18
+ cdx resume main
19
+ ```
20
+
21
+ Codex sessions resume with `codex resume --last --cd <cwd>` inside the named session's isolated `CODEX_HOME`. Claude sessions resume with `claude --continue --name <name>` inside the named session's isolated `HOME`.
22
+
23
+ Providers without a verified native resume mode, currently Antigravity and Ollama, return a clear unsupported result instead of falling back to a normal launch.
24
+
25
+ ### Resume capability checks
26
+
27
+ `cdx can-resume <name>` reports whether a session can resume without launching an interactive provider. JSON mode exposes a provider-neutral payload with the session name, provider, resumable state, strategy, reason, and command preview.
28
+
29
+ ### Workflow traceability
30
+
31
+ The Logics request, backlog item, task, and ADR for provider-native resume are complete, with validation evidence attached to the delivery task.
32
+
33
+ ## Validation
34
+
35
+ - `python -m unittest discover -s test -p 'test_runtime_py.py' -k resume`
36
+ - `python -m unittest discover -s test -p 'test_cli_py.py' -k resume`
37
+ - `python -m unittest discover -s test -p 'test_cli_py.py' -k help`
38
+ - `python -m unittest discover -s test -p 'test_*_py.py' -k resume`
39
+ - `npm run lint`
40
+ - `npm test`
41
+ - `logics-manager lint --require-status`
42
+ - `logics-manager audit`
43
+ - `git diff --check`
@@ -76,6 +76,10 @@
76
76
  "v0.9.2": {
77
77
  "github_tarball_sha256": "3e3ae4e4efc63a97dc6623ae8ddff36f04f4b7a0b531878a28c041298223d0b8",
78
78
  "github_zip_sha256": "09acd2866770f4dc7f9aaba6aff8308766bb211dabddd7f28bdfde9ace427e78"
79
+ },
80
+ "v0.9.3": {
81
+ "github_tarball_sha256": "cb3cfd9134447d1049b510836014001fb6c38a89de588adc8cdbb1fe5deb4d6d",
82
+ "github_zip_sha256": "056eb2d1a3f0fa721a7a0e597cd0409e496618cfa35791a78d4e184397596754"
79
83
  }
80
84
  }
81
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.3",
3
+ "version": "0.9.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.9.3"
7
+ version = "0.9.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
@@ -11,6 +11,7 @@ from .cli_commands import (
11
11
  handle_clean,
12
12
  handle_config,
13
13
  handle_configs,
14
+ handle_can_resume,
14
15
  handle_context,
15
16
  handle_copy,
16
17
  handle_doctor,
@@ -30,6 +31,7 @@ from .cli_commands import (
30
31
  handle_remove,
31
32
  handle_repair,
32
33
  handle_rename,
34
+ handle_resume,
33
35
  handle_run,
34
36
  handle_run_report,
35
37
  handle_run_status,
@@ -64,7 +66,7 @@ from .status_view import (
64
66
  )
65
67
  from .update_check import check_for_update, check_logics_manager_for_update
66
68
 
67
- VERSION = "0.9.3"
69
+ VERSION = "0.9.4"
68
70
 
69
71
 
70
72
  # ---------------------------------------------------------------------------
@@ -96,6 +98,8 @@ def _print_help(use_color=False):
96
98
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
97
99
  f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
98
100
  f" {_style('cdx last [--json]', '36', use_color)}",
101
+ f" {_style('cdx resume <name> [--json]', '36', use_color)}",
102
+ f" {_style('cdx can-resume <name> [--json]', '36', use_color)}",
99
103
  f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
100
104
  f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
101
105
  f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
@@ -283,7 +287,7 @@ def main(argv, options=None):
283
287
  "version": VERSION,
284
288
  "cwd": options.get("cwd") or os.getcwd(),
285
289
  "update_notices": _get_update_notices(service, env, options) if command not in (
286
- "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
290
+ "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "view", "update", "ready", "notify", "next", "context", "config", "configs", "set", "unset", "power", "perm", "fast", "model", "history", "stats", "resume", "can-resume", "handoff", "login", "logout", "disable", "enable", "export", "import", "select", "run", "help", "version"
287
291
  ) else None,
288
292
  "use_color": use_color,
289
293
  }
@@ -365,6 +369,12 @@ def main(argv, options=None):
365
369
  if command == "last":
366
370
  return handle_last(rest, ctx)
367
371
 
372
+ if command == "resume":
373
+ return handle_resume(rest, ctx)
374
+
375
+ if command == "can-resume":
376
+ return handle_can_resume(rest, ctx)
377
+
368
378
  if command == "handoff":
369
379
  return handle_handoff(rest, ctx)
370
380
 
@@ -400,8 +410,8 @@ def main(argv, options=None):
400
410
  out(f"{_print_version()}\n")
401
411
  return 0
402
412
 
403
- if not rest or rest == ["--json"]:
404
- return handle_launch(command, ctx)
413
+ if all(arg in ("--json", "-r", "--resume") for arg in rest):
414
+ return handle_launch(command, ctx, resume=("-r" in rest or "--resume" in rest))
405
415
 
406
416
  raise CdxError(f"Unknown command: {command}. Use cdx --help.")
407
417
 
@@ -3,6 +3,7 @@ import getpass
3
3
  import json
4
4
  import os
5
5
  import re
6
+ import shlex
6
7
  import sys
7
8
  import time
8
9
  from datetime import datetime, timedelta
@@ -37,6 +38,7 @@ from .provider_runtime import (
37
38
  _list_launch_transcript_paths,
38
39
  _normalize_reasoning_effort,
39
40
  _probe_provider_auth,
41
+ get_resume_capability,
40
42
  _run_headless_provider_command,
41
43
  _run_interactive_provider_command,
42
44
  )
@@ -66,6 +68,8 @@ CONFIGS_USAGE = "Usage: cdx configs [--json]"
66
68
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
67
69
  STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
68
70
  LAST_USAGE = "Usage: cdx last [--json]"
71
+ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
72
+ CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
69
73
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
70
74
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
71
75
  RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--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] --json"
@@ -2844,10 +2848,83 @@ def handle_update(rest, ctx):
2844
2848
  return 0
2845
2849
 
2846
2850
 
2847
- def handle_launch(command, ctx, initial_prompt=None):
2851
+ def _resume_capability_for_session(session, ctx):
2852
+ capability = get_resume_capability(session, cwd=ctx.get("cwd") or os.getcwd())
2853
+ return {
2854
+ "session": session["name"],
2855
+ "provider": session["provider"],
2856
+ "resumable": bool(capability.get("resumable")),
2857
+ "strategy": capability.get("strategy"),
2858
+ "reason": capability.get("reason"),
2859
+ "command_preview": capability.get("command_preview") or [],
2860
+ }
2861
+
2862
+
2863
+ def _format_resume_capability(capability, use_color=False):
2864
+ name = capability["session"]
2865
+ provider = capability["provider"]
2866
+ if capability["resumable"]:
2867
+ preview = shlex.join(capability.get("command_preview") or [])
2868
+ detail = f"({provider}, {capability['strategy']})"
2869
+ return (
2870
+ f"{_success(f'{name} can resume', use_color)} "
2871
+ f"{_dim(detail, use_color)}\n"
2872
+ f"{_dim(preview, use_color)}"
2873
+ )
2874
+ reason = capability.get("reason") or "not_supported"
2875
+ return (
2876
+ f"{_warn(f'{name} cannot resume', use_color)} "
2877
+ f"{_dim(f'({provider}, {reason})', use_color)}"
2878
+ )
2879
+
2880
+
2881
+ def handle_can_resume(rest, ctx):
2882
+ json_flag, args = _parse_json_flag(rest)
2883
+ if len(args) != 1:
2884
+ raise CdxError(CAN_RESUME_USAGE)
2885
+ session = ctx["service"]["get_session"](args[0])
2886
+ if not session:
2887
+ raise CdxError(f"Unknown session: {args[0]}")
2888
+ if session.get("enabled", True) is False:
2889
+ capability = {
2890
+ "session": session["name"],
2891
+ "provider": session["provider"],
2892
+ "resumable": False,
2893
+ "strategy": "session_disabled",
2894
+ "reason": "session_disabled",
2895
+ "command_preview": [],
2896
+ }
2897
+ else:
2898
+ capability = _resume_capability_for_session(session, ctx)
2899
+ if json_flag:
2900
+ _write_json(ctx, {
2901
+ "schema_version": API_SCHEMA_VERSION,
2902
+ "ok": True,
2903
+ **capability,
2904
+ })
2905
+ return 0
2906
+ ctx["out"](f"{_format_resume_capability(capability, ctx['use_color'])}\n")
2907
+ return 0
2908
+
2909
+
2910
+ def handle_resume(rest, ctx):
2911
+ json_flag, args = _parse_json_flag(rest)
2912
+ if len(args) != 1:
2913
+ raise CdxError(RESUME_USAGE)
2914
+ return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
2915
+
2916
+
2917
+ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
2848
2918
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
2919
+ if force_json is not None:
2920
+ json_flag = force_json
2849
2921
  warnings = _update_notice_warnings(ctx)
2850
2922
  session = ctx["service"]["launch_session"](command)
2923
+ capability = _resume_capability_for_session(session, ctx) if resume else None
2924
+ if capability and not capability["resumable"]:
2925
+ raise CdxError(
2926
+ f"Provider {session['provider']} does not support native resume through cdx."
2927
+ )
2851
2928
  _ensure_session_authentication(
2852
2929
  session,
2853
2930
  ctx["service"],
@@ -2859,7 +2936,11 @@ def handle_launch(command, ctx, initial_prompt=None):
2859
2936
  signal_emitter=ctx.get("signal_emitter"),
2860
2937
  trust_local_credentials=False,
2861
2938
  )
2862
- message = f"Launching {session['provider']} session {session['name']}"
2939
+ message = (
2940
+ f"Resuming {session['provider']} session {session['name']}"
2941
+ if resume else
2942
+ f"Launching {session['provider']} session {session['name']}"
2943
+ )
2863
2944
  if not json_flag:
2864
2945
  ctx["out"](f"{_info(message, ctx['use_color'])}\n")
2865
2946
  _write_update_notice(ctx)
@@ -2880,7 +2961,7 @@ def handle_launch(command, ctx, initial_prompt=None):
2880
2961
 
2881
2962
  try:
2882
2963
  run_info = _run_interactive_provider_command(
2883
- session, "launch", spawn=ctx.get("spawn"), cwd=cwd, env_override=ctx.get("env"),
2964
+ session, "resume" if resume else "launch", spawn=ctx.get("spawn"), cwd=cwd, env_override=ctx.get("env"),
2884
2965
  signal_emitter=ctx.get("signal_emitter"), initial_prompt=initial_prompt,
2885
2966
  lifecycle_callback=runtime_lifecycle,
2886
2967
  )
@@ -2903,12 +2984,16 @@ def handle_launch(command, ctx, initial_prompt=None):
2903
2984
  raise
2904
2985
  ctx["service"]["record_launch_history"](session["name"], {
2905
2986
  "status": "success",
2987
+ "action": "resume" if resume else "launch",
2906
2988
  "cwd": cwd,
2907
2989
  "exit_code": 0,
2908
2990
  **run_info,
2909
2991
  })
2910
2992
  if json_flag:
2911
- _write_json(ctx, _json_success("launch", message, warnings=warnings, session=ctx["service"]["get_session"](session["name"])))
2993
+ extra = {"session": ctx["service"]["get_session"](session["name"])}
2994
+ if capability:
2995
+ extra["resume"] = capability
2996
+ _write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))
2912
2997
  return 0
2913
2998
 
2914
2999
 
@@ -511,6 +511,90 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
511
511
  }, capture_transcript=capture_transcript, env=env)
512
512
 
513
513
 
514
+ def _redacted_resume_command_preview(session, cwd=None):
515
+ capability = get_resume_capability(session, cwd=cwd)
516
+ return capability.get("command_preview")
517
+
518
+
519
+ def get_resume_capability(session, cwd=None):
520
+ provider = session.get("provider")
521
+ cwd = cwd or os.getcwd()
522
+ if provider == PROVIDER_CODEX:
523
+ return {
524
+ "resumable": True,
525
+ "provider": provider,
526
+ "strategy": "provider_last",
527
+ "reason": "supported",
528
+ "command_preview": ["codex", "resume", "--last", "--cd", cwd],
529
+ }
530
+ if provider == PROVIDER_CLAUDE:
531
+ return {
532
+ "resumable": True,
533
+ "provider": provider,
534
+ "strategy": "provider_continue",
535
+ "reason": "supported",
536
+ "command_preview": ["claude", "--continue"],
537
+ }
538
+ return {
539
+ "resumable": False,
540
+ "provider": provider,
541
+ "strategy": "not_supported",
542
+ "reason": "not_supported",
543
+ "command_preview": [],
544
+ }
545
+
546
+
547
+ def _build_resume_spec(session, cwd=None, env_override=None, capture_transcript=True):
548
+ cwd = cwd or os.getcwd()
549
+ env_override = env_override or {}
550
+ env = {**os.environ, **env_override}
551
+ capability = get_resume_capability(session, cwd=cwd)
552
+ if not capability["resumable"]:
553
+ raise CdxError(f"Provider {session.get('provider')} does not support native resume through cdx.")
554
+
555
+ resume_prompt = _with_launch_preferences(session, env=env)
556
+ _validate_initial_prompt(resume_prompt)
557
+
558
+ if session["provider"] == PROVIDER_CLAUDE:
559
+ launch = session.get("launch") or {}
560
+ args = ["--continue", "--name", session["name"]]
561
+ if launch.get("model"):
562
+ args += ["--model", _claude_cli_model(launch["model"])]
563
+ args += _launch_config_args(session)
564
+ if resume_prompt:
565
+ args.append(resume_prompt)
566
+ auth_home = _get_auth_home(session)
567
+ claude_env = _claude_env(env, auth_home)
568
+ oauth_token = _read_claude_launch_oauth_token(auth_home)
569
+ if oauth_token:
570
+ claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
571
+ return _wrap_launch_with_transcript(session, {
572
+ "command": "claude",
573
+ "args": args,
574
+ "options": {
575
+ "cwd": cwd,
576
+ "env": claude_env,
577
+ },
578
+ "label": "claude resume",
579
+ }, capture_transcript=capture_transcript, env=env)
580
+
581
+ launch = session.get("launch") or {}
582
+ args = ["resume", "--last", "--cd", cwd]
583
+ if launch.get("model"):
584
+ args += ["--model", launch["model"]]
585
+ args += _launch_config_args(session)
586
+ if resume_prompt:
587
+ args.append(resume_prompt)
588
+ return _wrap_launch_with_transcript(session, {
589
+ "command": "codex",
590
+ "args": args,
591
+ "options": {
592
+ "env": {**env, "CODEX_HOME": _get_auth_home(session)},
593
+ },
594
+ "label": "codex resume",
595
+ }, capture_transcript=capture_transcript, env=env)
596
+
597
+
514
598
  def _validate_initial_prompt(initial_prompt):
515
599
  if initial_prompt is not None:
516
600
  if not isinstance(initial_prompt, str):
@@ -846,11 +930,14 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
846
930
  env_override=None, signal_emitter=None,
847
931
  initial_prompt=None, lifecycle_callback=None):
848
932
  spawn = spawn or subprocess.Popen
849
- spec = (
850
- _build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
851
- if action == "launch"
852
- else _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
853
- )
933
+ if action == "launch":
934
+ spec = _build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
935
+ elif action == "resume":
936
+ if initial_prompt is not None:
937
+ raise CdxError("initial_prompt is not supported for resume.")
938
+ spec = _build_resume_spec(session, cwd=cwd, env_override=env_override)
939
+ else:
940
+ spec = _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
854
941
  def start_child(current_spec):
855
942
  command = current_spec["command"]
856
943
  if spawn is subprocess.Popen:
@@ -21,6 +21,7 @@ ALLOWED_PROVIDERS = set(PROVIDERS)
21
21
  MAX_SESSION_NAME_LENGTH = 64
22
22
  RESERVED_SESSION_NAMES = {
23
23
  "add",
24
+ "can-resume",
24
25
  "clean",
25
26
  "context",
26
27
  "configs",
@@ -45,6 +46,7 @@ RESERVED_SESSION_NAMES = {
45
46
  "power",
46
47
  "ready",
47
48
  "repair",
49
+ "resume",
48
50
  "ren",
49
51
  "rename",
50
52
  "rmv",