cdx-manager 0.5.1 → 0.5.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 +4 -3
- package/changelogs/CHANGELOGS_0_5_2.md +42 -0
- package/changelogs/CHANGELOGS_0_5_3.md +42 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +2 -1
- package/src/cli_commands.py +107 -14
- package/src/cli_render.py +1 -0
- package/src/provider_runtime.py +14 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -37,7 +37,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
37
37
|
- **Auth guardrails.** `cdx` checks authentication before launching. If a session is not logged in, it tells you exactly what to run — no silent failures.
|
|
38
38
|
- **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota, and last-updated timestamps for every session in one aligned table.
|
|
39
39
|
- **Session control.** Disable a session without deleting it when an account is temporarily out of credits; disabled sessions remain visible and sort last.
|
|
40
|
-
- **Shared handoff context.** Keep a per-workspace Markdown context and install it into another assistant session before switching providers or accounts.
|
|
40
|
+
- **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts.
|
|
41
41
|
- **Passive status resolution.** Codex status is read from the local Codex app-server rate-limit API when available, with legacy transcript/history parsing kept as a fallback.
|
|
42
42
|
- **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
|
|
43
43
|
- **Clean removal.** `cdx rmv` wipes a session and its entire auth directory. No orphaned files, no stale credentials.
|
|
@@ -126,7 +126,7 @@ For a specific version:
|
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
129
|
-
CDX_VERSION=v0.5.
|
|
129
|
+
CDX_VERSION=v0.5.3 sh install.sh
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
From source:
|
|
@@ -250,6 +250,7 @@ cdx status
|
|
|
250
250
|
| `cdx enable <name> [--json]` | Re-enable a disabled session |
|
|
251
251
|
| `cdx context show\|path\|init\|edit\|clear\|set [text...] [--json]` | Manage the shared Markdown context for the current workspace |
|
|
252
252
|
| `cdx handoff <name> [--json]` | Install the current workspace context into a target session and launch it unless `--json` is used |
|
|
253
|
+
| `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 Codex and Claude targets, including cross-provider handoff |
|
|
253
254
|
| `cdx rmv <name> [--force] [--json]` | Remove a session and its auth data (prompts for confirmation unless `--force`) |
|
|
254
255
|
| `cdx clean [name] [--json]` | Clear launch transcript logs for one session or all sessions |
|
|
255
256
|
| `cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Export sessions to a portable bundle; `--include-auth` encrypts auth data with a passphrase |
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog (`0.5.1 -> 0.5.2`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-22
|
|
4
|
+
|
|
5
|
+
## Major Highlights
|
|
6
|
+
|
|
7
|
+
- Added transcript-based handoff between two sessions of the same provider.
|
|
8
|
+
- Added automatic launch prompting so the target Codex session reads `shared-context.md` and resumes without a manual instruction.
|
|
9
|
+
- Prepared the package metadata for the 0.5.2 release across npm, PyPI, CLI version output, and README install examples.
|
|
10
|
+
|
|
11
|
+
## Transcript-Based Session Handoff
|
|
12
|
+
|
|
13
|
+
- Added `cdx handoff <source> <target> [--json]`.
|
|
14
|
+
- Builds a shared context from the source session's latest captured launch transcript.
|
|
15
|
+
- Installs the generated context into the target session as `shared-context.md`.
|
|
16
|
+
- Preserves target account authentication and does not copy source credentials.
|
|
17
|
+
- Rejects same-session and cross-provider handoffs with explicit errors.
|
|
18
|
+
- Truncates very large transcripts to the most recent tail before writing the handoff context.
|
|
19
|
+
|
|
20
|
+
## Launch and JSON Output
|
|
21
|
+
|
|
22
|
+
- Passes an initial Codex prompt during handoff launch so the target session is told to read `$CODEX_HOME/shared-context.md`.
|
|
23
|
+
- Adds `launch_prompt` to `cdx handoff ... --json` output for non-interactive integrations.
|
|
24
|
+
- Keeps `--json` handoff mode non-launching while exposing source transcript, generated context, and installed target context paths.
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
|
|
28
|
+
- Updated CLI help and session-list command hints for `cdx handoff <source> <target>`.
|
|
29
|
+
- Updated the README feature summary and command table for transcript-based handoff.
|
|
30
|
+
- Updated the README badge and pinned installer example to `v0.5.2`.
|
|
31
|
+
|
|
32
|
+
## Validation and Regression Coverage
|
|
33
|
+
|
|
34
|
+
- Added CLI coverage for source-to-target handoff context generation.
|
|
35
|
+
- Added CLI coverage proving non-JSON handoff launches the target Codex session with the resume prompt.
|
|
36
|
+
- Kept full Python test coverage and Logics lint green.
|
|
37
|
+
|
|
38
|
+
## Validation and Regression Evidence
|
|
39
|
+
|
|
40
|
+
- `python -m pytest test -q`
|
|
41
|
+
- `python logics/skills/logics.py lint --require-status`
|
|
42
|
+
- `git diff --check`
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog (`0.5.2 -> 0.5.3`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-23
|
|
4
|
+
|
|
5
|
+
## Major Highlights
|
|
6
|
+
|
|
7
|
+
- Added Claude-compatible handoff launch prompting so Claude targets are told exactly which `shared-context.md` file to read.
|
|
8
|
+
- Added Claude launch transcript capture, enabling transcript-based handoff from Claude source sessions.
|
|
9
|
+
- Enabled cross-provider transcript handoff between Codex and Claude sessions.
|
|
10
|
+
- Prepared the package metadata for the 0.5.3 release across npm, PyPI, CLI version output, and README install examples.
|
|
11
|
+
|
|
12
|
+
## Claude Handoff
|
|
13
|
+
|
|
14
|
+
- Passes the handoff prompt to Claude as the initial prompt argument during launch.
|
|
15
|
+
- Installs shared context into the target Claude session profile and references the concrete `shared-context.md` path in the prompt.
|
|
16
|
+
- Captures Claude launch transcripts under the session's `claude-home/log/` directory using the same transcript wrapper path as Codex.
|
|
17
|
+
- Preserves Claude `HOME` isolation while running through the transcript wrapper.
|
|
18
|
+
|
|
19
|
+
## Cross-Provider Handoff
|
|
20
|
+
|
|
21
|
+
- Allows `cdx handoff <source> <target>` when the source and target providers differ.
|
|
22
|
+
- Keeps generated handoff context provider-neutral while still recording source and target provider metadata.
|
|
23
|
+
- Preserves target account authentication and does not copy source credentials.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
- Updated the command table to document Codex and Claude handoff targets, including cross-provider handoff.
|
|
28
|
+
- Updated the README badge and pinned installer example to `v0.5.3`.
|
|
29
|
+
|
|
30
|
+
## Validation and Regression Coverage
|
|
31
|
+
|
|
32
|
+
- Added CLI coverage for Claude-to-Claude transcript handoff.
|
|
33
|
+
- Added CLI coverage for Codex-to-Claude handoff.
|
|
34
|
+
- Added CLI coverage proving non-JSON Claude handoff launches Claude with the resume prompt.
|
|
35
|
+
- Updated existing Claude launch coverage to verify transcript-wrapped launches.
|
|
36
|
+
|
|
37
|
+
## Validation and Regression Evidence
|
|
38
|
+
|
|
39
|
+
- `python3 -m pytest test/test_cli_py.py test/test_runtime_py.py`
|
|
40
|
+
- `python3 -m pytest`
|
|
41
|
+
- `npm run lint`
|
|
42
|
+
- `npm test`
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -49,7 +49,7 @@ from .status_view import (
|
|
|
49
49
|
)
|
|
50
50
|
from .update_check import check_for_update
|
|
51
51
|
|
|
52
|
-
VERSION = "0.5.
|
|
52
|
+
VERSION = "0.5.3"
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
# ---------------------------------------------------------------------------
|
|
@@ -68,6 +68,7 @@ def _print_help(use_color=False):
|
|
|
68
68
|
f" {_style('cdx status <name> [--json] [--refresh]', '36', use_color)}",
|
|
69
69
|
f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
|
|
70
70
|
f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
|
|
71
|
+
f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
|
|
71
72
|
f" {_style('cdx add [provider] <name> [--json]', '36', use_color)}",
|
|
72
73
|
f" {_style('cdx cp <source> <dest> [--json]', '36', use_color)}",
|
|
73
74
|
f" {_style('cdx ren <source> <dest> [--json]', '36', use_color)}",
|
package/src/cli_commands.py
CHANGED
|
@@ -43,8 +43,9 @@ UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
|
|
|
43
43
|
EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
44
44
|
IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
|
|
45
45
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
46
|
-
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json]"
|
|
46
|
+
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
47
47
|
API_SCHEMA_VERSION = 1
|
|
48
|
+
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def _local_now_iso():
|
|
@@ -67,6 +68,63 @@ def _write_json(ctx, payload):
|
|
|
67
68
|
ctx["out"](f"{json.dumps(payload, indent=2)}\n")
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def _latest_launch_transcript_path(session):
|
|
72
|
+
paths = _list_launch_transcript_paths(session)
|
|
73
|
+
if not paths:
|
|
74
|
+
return None
|
|
75
|
+
return max(paths, key=lambda path: (os.path.getmtime(path), path))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_handoff_transcript(path):
|
|
79
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
80
|
+
content = handle.read()
|
|
81
|
+
if len(content) <= HANDOFF_TRANSCRIPT_CHARS:
|
|
82
|
+
return content, False
|
|
83
|
+
return content[-HANDOFF_TRANSCRIPT_CHARS:], True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_handoff_context(source, target, transcript_path, transcript, truncated=False):
|
|
87
|
+
truncation_note = (
|
|
88
|
+
"Only the tail of the previous transcript is included because the log was large."
|
|
89
|
+
if truncated else
|
|
90
|
+
"The previous transcript is included below."
|
|
91
|
+
)
|
|
92
|
+
return "\n".join([
|
|
93
|
+
"# Shared Context",
|
|
94
|
+
"",
|
|
95
|
+
"## Goal",
|
|
96
|
+
f"Resume the work from `{source['name']}` in `{target['name']}` because the source account has no usage left.",
|
|
97
|
+
"",
|
|
98
|
+
"## Current State",
|
|
99
|
+
f"- Source session: `{source['name']}` ({source['provider']})",
|
|
100
|
+
f"- Target session: `{target['name']}` ({target['provider']})",
|
|
101
|
+
f"- Transcript source: `{transcript_path}`",
|
|
102
|
+
f"- {truncation_note}",
|
|
103
|
+
"",
|
|
104
|
+
"## Previous Session Transcript",
|
|
105
|
+
transcript.rstrip(),
|
|
106
|
+
"",
|
|
107
|
+
"## Next Steps",
|
|
108
|
+
"- Read the transcript above.",
|
|
109
|
+
"- Continue the task from the latest actionable state.",
|
|
110
|
+
"- Preserve the target account authentication; do not copy source credentials.",
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _handoff_launch_prompt(session, install=None):
|
|
115
|
+
if session.get("provider") == "codex":
|
|
116
|
+
context_ref = "$CODEX_HOME/shared-context.md"
|
|
117
|
+
else:
|
|
118
|
+
context_ref = (install or {}).get("target_path") or os.path.join(
|
|
119
|
+
session.get("authHome") or session.get("sessionRoot") or "~",
|
|
120
|
+
"shared-context.md",
|
|
121
|
+
)
|
|
122
|
+
return (
|
|
123
|
+
f"Read {context_ref} first, then resume the previous session "
|
|
124
|
+
"from the latest actionable state. Do not ask me to paste the context again."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
70
128
|
def _parse_json_flag(args):
|
|
71
129
|
json_flag = "--json" in args
|
|
72
130
|
cleaned = [arg for arg in args if arg != "--json"]
|
|
@@ -914,7 +972,7 @@ def handle_update(rest, ctx):
|
|
|
914
972
|
return 0
|
|
915
973
|
|
|
916
974
|
|
|
917
|
-
def handle_launch(command, ctx):
|
|
975
|
+
def handle_launch(command, ctx, initial_prompt=None):
|
|
918
976
|
json_flag = "--json" in ctx["options"].get("raw_args", [])
|
|
919
977
|
update_notice = ctx.get("update_notice")
|
|
920
978
|
warnings = []
|
|
@@ -949,7 +1007,7 @@ def handle_launch(command, ctx):
|
|
|
949
1007
|
ctx["out"](f"{_dim('Tip: cdx status reads Codex rate limits from the local app-server when available.', ctx['use_color'])}\n")
|
|
950
1008
|
_run_interactive_provider_command(
|
|
951
1009
|
session, "launch", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
952
|
-
signal_emitter=ctx.get("signal_emitter")
|
|
1010
|
+
signal_emitter=ctx.get("signal_emitter"), initial_prompt=initial_prompt,
|
|
953
1011
|
)
|
|
954
1012
|
if json_flag:
|
|
955
1013
|
_write_json(ctx, _json_success("launch", message, warnings=warnings, session=ctx["service"]["get_session"](session["name"])))
|
|
@@ -958,22 +1016,57 @@ def handle_launch(command, ctx):
|
|
|
958
1016
|
|
|
959
1017
|
def handle_handoff(rest, ctx):
|
|
960
1018
|
json_flag, args = _parse_json_flag(rest)
|
|
961
|
-
if len(args)
|
|
1019
|
+
if len(args) not in (1, 2):
|
|
962
1020
|
raise CdxError(HANDOFF_USAGE)
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1021
|
+
if len(args) == 1:
|
|
1022
|
+
name = args[0]
|
|
1023
|
+
session = ctx["service"]["get_session"](name)
|
|
1024
|
+
if not session:
|
|
1025
|
+
raise CdxError(f"Unknown session: {name}")
|
|
1026
|
+
install = install_context_for_session(ctx["service"]["base_dir"], session, ctx.get("cwd"))
|
|
1027
|
+
launch_prompt = _handoff_launch_prompt(session, install)
|
|
1028
|
+
if json_flag:
|
|
1029
|
+
_write_json(ctx, _json_success(
|
|
1030
|
+
"handoff",
|
|
1031
|
+
f"Installed shared context for {name}",
|
|
1032
|
+
context=install,
|
|
1033
|
+
launch_prompt=launch_prompt,
|
|
1034
|
+
session=session,
|
|
1035
|
+
))
|
|
1036
|
+
return 0
|
|
1037
|
+
text = f"Shared context installed for {name}: {install['target_path']}"
|
|
1038
|
+
ctx["out"](f"{_info(text, ctx['use_color'])}\n")
|
|
1039
|
+
return handle_launch(name, ctx, initial_prompt=launch_prompt)
|
|
1040
|
+
|
|
1041
|
+
source_name, target_name = args
|
|
1042
|
+
if source_name == target_name:
|
|
1043
|
+
raise CdxError("Source and target sessions must be different")
|
|
1044
|
+
source = ctx["service"]["get_session"](source_name)
|
|
1045
|
+
if not source:
|
|
1046
|
+
raise CdxError(f"Unknown session: {source_name}")
|
|
1047
|
+
target = ctx["service"]["get_session"](target_name)
|
|
1048
|
+
if not target:
|
|
1049
|
+
raise CdxError(f"Unknown session: {target_name}")
|
|
1050
|
+
transcript_path = _latest_launch_transcript_path(source)
|
|
1051
|
+
if not transcript_path:
|
|
1052
|
+
raise CdxError(f"No launch transcript found for session: {source_name}")
|
|
1053
|
+
transcript, truncated = _read_handoff_transcript(transcript_path)
|
|
1054
|
+
context = _build_handoff_context(source, target, transcript_path, transcript, truncated=truncated)
|
|
1055
|
+
write_result = write_context(ctx["service"]["base_dir"], context, ctx.get("cwd"))
|
|
1056
|
+
install = install_context_for_session(ctx["service"]["base_dir"], target, ctx.get("cwd"))
|
|
1057
|
+
launch_prompt = _handoff_launch_prompt(target, install)
|
|
968
1058
|
if json_flag:
|
|
969
1059
|
_write_json(ctx, _json_success(
|
|
970
1060
|
"handoff",
|
|
971
|
-
f"
|
|
1061
|
+
f"Prepared handoff from {source_name} to {target_name}",
|
|
972
1062
|
context=install,
|
|
973
|
-
|
|
1063
|
+
source_session=source,
|
|
1064
|
+
target_session=target,
|
|
1065
|
+
source_transcript=transcript_path,
|
|
1066
|
+
shared_context=write_result,
|
|
1067
|
+
launch_prompt=launch_prompt,
|
|
974
1068
|
))
|
|
975
1069
|
return 0
|
|
976
|
-
text = f"
|
|
1070
|
+
text = f"Handoff prepared from {source_name} to {target_name}: {install['target_path']}"
|
|
977
1071
|
ctx["out"](f"{_info(text, ctx['use_color'])}\n")
|
|
978
|
-
|
|
979
|
-
return handle_launch(name, ctx)
|
|
1072
|
+
return handle_launch(target_name, ctx, initial_prompt=launch_prompt)
|
package/src/cli_render.py
CHANGED
|
@@ -129,6 +129,7 @@ def _format_sessions(service, use_color=False):
|
|
|
129
129
|
f" {_style('cdx logout <name>', '36', use_color)}",
|
|
130
130
|
f" {_style('cdx context show', '36', use_color)}",
|
|
131
131
|
f" {_style('cdx handoff <name>', '36', use_color)}",
|
|
132
|
+
f" {_style('cdx handoff <source> <target>', '36', use_color)}",
|
|
132
133
|
f" {_style('cdx disable <name>', '36', use_color)}",
|
|
133
134
|
f" {_style('cdx enable <name>', '36', use_color)}",
|
|
134
135
|
f" {_style('cdx ren <source> <dest>', '36', use_color)}",
|
package/src/provider_runtime.py
CHANGED
|
@@ -98,23 +98,29 @@ def _wrap_launch_with_transcript(session, spec, capture_transcript=True, env=Non
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def _build_launch_spec(session, cwd=None, env_override=None):
|
|
101
|
+
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
102
102
|
cwd = cwd or os.getcwd()
|
|
103
103
|
env_override = env_override or {}
|
|
104
104
|
env = {**os.environ, **env_override}
|
|
105
105
|
if session["provider"] == "claude":
|
|
106
|
-
|
|
106
|
+
args = ["--name", session["name"]]
|
|
107
|
+
if initial_prompt:
|
|
108
|
+
args.append(initial_prompt)
|
|
109
|
+
return _wrap_launch_with_transcript(session, {
|
|
107
110
|
"command": "claude",
|
|
108
|
-
"args":
|
|
111
|
+
"args": args,
|
|
109
112
|
"options": {
|
|
110
113
|
"cwd": cwd,
|
|
111
114
|
"env": {**env, **_home_env_overrides(_get_auth_home(session))},
|
|
112
115
|
},
|
|
113
116
|
"label": "claude",
|
|
114
|
-
}
|
|
117
|
+
}, env=env)
|
|
118
|
+
args = ["--no-alt-screen", "--cd", cwd]
|
|
119
|
+
if initial_prompt:
|
|
120
|
+
args.append(initial_prompt)
|
|
115
121
|
return _wrap_launch_with_transcript(session, {
|
|
116
122
|
"command": "codex",
|
|
117
|
-
"args":
|
|
123
|
+
"args": args,
|
|
118
124
|
"options": {
|
|
119
125
|
"env": {**env, "CODEX_HOME": _get_auth_home(session)},
|
|
120
126
|
},
|
|
@@ -221,10 +227,11 @@ def _signal_name(sig):
|
|
|
221
227
|
|
|
222
228
|
|
|
223
229
|
def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
224
|
-
env_override=None, signal_emitter=None
|
|
230
|
+
env_override=None, signal_emitter=None,
|
|
231
|
+
initial_prompt=None):
|
|
225
232
|
spawn = spawn or subprocess.Popen
|
|
226
233
|
spec = (
|
|
227
|
-
_build_launch_spec(session, cwd=cwd, env_override=env_override)
|
|
234
|
+
_build_launch_spec(session, cwd=cwd, env_override=env_override, initial_prompt=initial_prompt)
|
|
228
235
|
if action == "launch"
|
|
229
236
|
else _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
|
|
230
237
|
)
|