cdx-manager 0.9.0 → 0.9.2

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.8.0-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.1-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
 
@@ -42,7 +42,7 @@ One command to launch any session. Zero auth juggling.
42
42
  - **Persistent launch settings.** Pin per-session power, permission, and fast-mode preferences once; `cdx` reapplies them on every launch until you unset them.
43
43
  - **Launch history.** Inspect recent launches with provider, result, duration, working directory, launch settings, and transcript path.
44
44
  - **Update prompts.** Periodic update checks surface `cdx update` directly in the `cdx`, `cdx status`, and launch output when a newer release is available. When `logics-manager` is installed, `cdx` can also suggest `logics-manager self-update`.
45
- - **Logics viewer shortcut.** `cdx view` opens the Logics browser/focus viewer through `logics-manager view` when the companion CLI is installed; `cdx view --json` reports availability and update diagnostics without opening the viewer.
45
+ - **Logics viewer shortcut.** `cdx view` opens the Logics browser/focus viewer through `logics-manager view` when the companion CLI is installed. All viewer flags are forwarded: `--lan`, `--lan-rw`, `--focus <ref>`, `--read`, `--port`, `--host`, `--refresh-interval`, `--tls`, `--tls-cert`, `--tls-key`, `--open`, `--no-open`. `cdx view --json` reports availability and update diagnostics without opening the viewer.
46
46
  - **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.
47
47
  - **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.
48
48
  - **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
@@ -352,7 +352,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
352
352
  | `cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
353
353
  | `cdx doctor [--json]` | Inspect CLI dependencies, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines |
354
354
  | `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
355
- | `cdx view [--json]` | Open the Logics browser/focus viewer by delegating to `logics-manager view`; JSON mode reports diagnostics without launching it |
355
+ | `cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--tls-cert <path>] [--tls-key <path>] [--open] [--no-open]` | Open the Logics browser/focus viewer by delegating to `logics-manager view`; all viewer flags are forwarded; JSON mode reports diagnostics without launching it |
356
356
  | `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
357
357
  | `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
358
358
  | `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
@@ -0,0 +1,36 @@
1
+ # CDX Manager 0.9.1
2
+
3
+ ## Highlights
4
+
5
+ - `cdx view` now forwards all `logics-manager view` flags instead of rejecting them.
6
+ - Fixes a silent PATH resolution bug that could prevent `logics-manager` from being found.
7
+
8
+ ## Changes
9
+
10
+ ### cdx view: full flag passthrough
11
+
12
+ `cdx view` previously accepted only `--json` and raised a usage error for every other argument. All `logics-manager view` flags are now supported and forwarded transparently:
13
+
14
+ - `--lan` / `--lan-rw` — expose the viewer on the local network
15
+ - `--focus <ref>` / `--read` — open the viewer centered on a workflow document
16
+ - `--port <port>` / `--host <host>` — bind to a custom address
17
+ - `--refresh-interval <s>` — override the auto-refresh interval
18
+ - `--tls` / `--tls-cert <path>` / `--tls-key <path>` — serve over HTTPS
19
+ - `--open` / `--no-open` — control browser auto-open
20
+
21
+ ### Bug fixes
22
+
23
+ - `resolve_logics_manager`: `shutil.which` was called with `path=""` when `env` had no `PATH` key, causing it to search nothing and always return `None`. Now falls back to the process PATH.
24
+ - `run_logics_viewer`: the viewer subprocess now inherits the full OS environment (`os.environ` merged with any `env` overrides) instead of receiving a partial dict that stripped all inherited variables.
25
+
26
+ ### Docs
27
+
28
+ - README command table and feature description updated to list all forwarded viewer flags.
29
+ - `LOGICS_PROMPT` updated to reference `cdx view` instead of `logics-manager view` directly.
30
+
31
+ ## Validation
32
+
33
+ - `npm run prepublishOnly`
34
+ - `npm pack --dry-run`
35
+ - `logics-manager lint --require-status`
36
+ - `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
@@ -0,0 +1,33 @@
1
+ # CDX Manager 0.9.2
2
+
3
+ ## Highlights
4
+
5
+ - `cdx import` gains a `--merge` flag to fill in missing data without overwriting what already exists locally.
6
+ - npm and PyPI publication workflows now verify that the release tag's commit has a successful `CI` run before any registry upload step.
7
+
8
+ ## Changes
9
+
10
+ ### cdx import: `--merge` mode
11
+
12
+ `cdx import` previously offered two stances on existing sessions: reject the conflict (default) or erase and replace (`--force`). A third mode is now available:
13
+
14
+ ```
15
+ cdx import backup.cdx --merge
16
+ ```
17
+
18
+ With `--merge`, for each session that already exists locally:
19
+
20
+ - **Session fields** — existing values are kept; fields absent locally are pulled in from the bundle.
21
+ - **Session state** — same merge rule: local data wins, bundle fills the gaps.
22
+ - **Profile files** (auth.json, credentials, etc.) — files that already exist on disk are left untouched; files missing locally are restored from the bundle.
23
+ - **New sessions** in the bundle that have no local counterpart are imported normally.
24
+
25
+ `--force` and `--merge` are mutually exclusive and raise a clear error if combined.
26
+
27
+ ## Validation
28
+
29
+ - `npm run prepublishOnly`
30
+ - `npm pack --dry-run`
31
+ - `python -m unittest discover -s test -p 'test_release_ci_py.py'`
32
+ - `logics-manager lint --require-status`
33
+ - `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
@@ -64,6 +64,14 @@
64
64
  "v0.8.0": {
65
65
  "github_tarball_sha256": "62cd461f9e22636bb85d801e21209b1762859b6662623e74c01fd40c107ea29a",
66
66
  "github_zip_sha256": "4674c8b61110f52b99c76312e9962202a2e8fc6928327f8927781f45eeaa980f"
67
+ },
68
+ "v0.9.0": {
69
+ "github_tarball_sha256": "809af5746e1287f4c5dc7a2fb7583e67e212aa250c45be905c32374e5dee26d6",
70
+ "github_zip_sha256": "9118f81645c00a4a660923a1ae290f48bd458af7ebfcf9f6580a3b91023b7c76"
71
+ },
72
+ "v0.9.1": {
73
+ "github_tarball_sha256": "8cbab620c823aeaf56e72c1a4d20e251a1c7142bfeeb3d516321d59c2c0a621d",
74
+ "github_zip_sha256": "253909ede6dca61937835bd4ee29145ddacaa08fbdfba1b39810da73ae2ccc84"
67
75
  }
68
76
  }
69
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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.0"
7
+ version = "0.9.2"
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
@@ -64,7 +64,7 @@ from .status_view import (
64
64
  )
65
65
  from .update_check import check_for_update, check_logics_manager_for_update
66
66
 
67
- VERSION = "0.9.0"
67
+ VERSION = "0.9.2"
68
68
 
69
69
 
70
70
  # ---------------------------------------------------------------------------
@@ -108,7 +108,7 @@ def _print_help(use_color=False):
108
108
  f" {_style('cdx rmv <name> [--force] [--json]', '36', use_color)}",
109
109
  f" {_style('cdx clean [name] [--json]', '36', use_color)}",
110
110
  f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
- f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
+ f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force|--merge] [--json]', '36', use_color)}",
112
112
  f" {_style('cdx doctor [--json]', '36', use_color)}",
113
113
  f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
114
114
  f" {_style('cdx view [--json]', '36', use_color)}",
@@ -55,7 +55,7 @@ DOCTOR_USAGE = "Usage: cdx doctor [--json]"
55
55
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
56
56
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
57
57
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
- IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
+ IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
59
59
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
60
60
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
61
61
  SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
@@ -1000,6 +1000,7 @@ def _parse_export_args(args):
1000
1000
  def _parse_import_args(args):
1001
1001
  parsed = _parse_flag_args(args, {
1002
1002
  "--force": {"key": "force", "type": "bool", "default": False},
1003
+ "--merge": {"key": "merge", "type": "bool", "default": False},
1003
1004
  "--json": {"key": "json", "type": "bool", "default": False},
1004
1005
  "--sessions": {
1005
1006
  "key": "session_names",
@@ -1012,6 +1013,8 @@ def _parse_import_args(args):
1012
1013
  parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
1013
1014
  if not parsed["file_path"]:
1014
1015
  raise CdxError(IMPORT_USAGE)
1016
+ if parsed["force"] and parsed["merge"]:
1017
+ raise CdxError("--force and --merge are mutually exclusive.")
1015
1018
  return parsed
1016
1019
 
1017
1020
 
@@ -2643,6 +2646,7 @@ def handle_import(rest, ctx):
2643
2646
  passphrase=passphrase,
2644
2647
  session_names=parsed["session_names"],
2645
2648
  force=parsed["force"],
2649
+ merge=parsed["merge"],
2646
2650
  )
2647
2651
  session_count = len(result["session_names"])
2648
2652
  auth_suffix = " with auth" if result["include_auth"] else ""
package/src/cli_view.py CHANGED
@@ -12,7 +12,7 @@ from .logics_view import (
12
12
  from .update_check import check_logics_manager_for_update
13
13
 
14
14
 
15
- VIEW_USAGE = "Usage: cdx view [--json]"
15
+ VIEW_USAGE = "Usage: cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--open] [--no-open]"
16
16
  API_SCHEMA_VERSION = 1
17
17
 
18
18
 
@@ -67,18 +67,44 @@ def _logics_manager_update_notice(ctx, env):
67
67
  )
68
68
 
69
69
 
70
+ _VIEWER_FLAGS = {
71
+ "--lan", "--lan-rw", "--tls", "--read", "--open", "--no-open",
72
+ }
73
+ _VIEWER_VALUE_FLAGS = {
74
+ "--host", "--port", "--tls-cert", "--tls-key", "--refresh-interval", "--focus",
75
+ }
76
+
77
+
78
+ def _parse_view_args(rest):
79
+ json_flag = False
80
+ viewer_args = []
81
+ i = 0
82
+ while i < len(rest):
83
+ arg = rest[i]
84
+ if arg == "--json":
85
+ json_flag = True
86
+ elif arg in _VIEWER_FLAGS:
87
+ viewer_args.append(arg)
88
+ elif arg in _VIEWER_VALUE_FLAGS:
89
+ if i + 1 >= len(rest):
90
+ raise CdxError(f"{arg} requires a value. {VIEW_USAGE}")
91
+ viewer_args.extend([arg, rest[i + 1]])
92
+ i += 1
93
+ else:
94
+ raise CdxError(f"Unknown option: {arg}\n{VIEW_USAGE}")
95
+ i += 1
96
+ return json_flag, viewer_args
97
+
98
+
70
99
  def handle_view(rest, ctx):
71
- json_flag = "--json" in rest
72
- unknown = [arg for arg in rest if arg != "--json"]
73
- if unknown:
74
- raise CdxError(VIEW_USAGE)
100
+ json_flag, viewer_args = _parse_view_args(rest)
75
101
 
76
102
  env = ctx.get("env")
77
103
  cwd = ctx.get("cwd")
78
104
  executable = resolve_logics_manager(env=env)
79
105
  update_notice = _logics_manager_update_notice(ctx, env) if executable else None
80
106
  failure = None if executable else missing_logics_manager_failure()
81
- diagnostics = build_viewer_diagnostics(executable, cwd, update_notice=update_notice, failure=failure)
107
+ diagnostics = build_viewer_diagnostics(executable, cwd, update_notice=update_notice, failure=failure, extra_args=viewer_args)
82
108
  warnings = _update_notice_warnings([update_notice])
83
109
 
84
110
  if json_flag:
@@ -92,7 +118,7 @@ def handle_view(rest, ctx):
92
118
  ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
93
119
 
94
120
  try:
95
- result = run_logics_viewer(executable, cwd, env=env, runner=ctx.get("spawn_sync"))
121
+ result = run_logics_viewer(executable, cwd, env=env, extra_args=viewer_args, runner=ctx.get("spawn_sync"))
96
122
  except FileNotFoundError as error:
97
123
  raise CdxError(f"logics-manager is required for cdx view. {LOGICS_MANAGER_INSTALL_HINT}") from error
98
124
  except KeyboardInterrupt:
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import shutil
2
3
  import subprocess
3
4
 
@@ -7,11 +8,11 @@ LOGICS_MANAGER_INSTALL_HINT = "Install or update it with: npm install -g @grifhi
7
8
 
8
9
  def resolve_logics_manager(env=None):
9
10
  env = env or {}
10
- return shutil.which("logics-manager", path=env.get("PATH", ""))
11
+ return shutil.which("logics-manager", path=env.get("PATH") or None)
11
12
 
12
13
 
13
- def build_viewer_diagnostics(executable, cwd, update_notice=None, failure=None):
14
- command = [executable or "logics-manager", "view"]
14
+ def build_viewer_diagnostics(executable, cwd, update_notice=None, failure=None, extra_args=None):
15
+ command = [executable or "logics-manager", "view"] + (extra_args or [])
15
16
  return {
16
17
  "available": bool(executable),
17
18
  "executable": executable,
@@ -29,9 +30,10 @@ def missing_logics_manager_failure():
29
30
  }
30
31
 
31
32
 
32
- def run_logics_viewer(executable, cwd, env=None, runner=None):
33
+ def run_logics_viewer(executable, cwd, env=None, extra_args=None, runner=None):
33
34
  runner = runner or subprocess.run
34
- argv = [executable, "view"]
35
+ argv = [executable, "view"] + (extra_args or [])
36
+ merged_env = {**os.environ, **(env or {})}
35
37
  if runner is subprocess.run:
36
- return subprocess.run(argv, cwd=cwd, env=env)
37
- return runner(argv, cwd=cwd, env=env)
38
+ return subprocess.run(argv, cwd=cwd, env=merged_env)
39
+ return runner(argv, cwd=cwd, env=merged_env)
@@ -22,7 +22,7 @@ RTK_PROMPT = (
22
22
  LOGICS_PROMPT = (
23
23
  "When `logics-manager` is available, prefer it for Logics workflow operations: use "
24
24
  "`logics-manager status`, `health`, `audit`, and `lint` for workflow state and validation; "
25
- "`logics-manager view` for the browser viewer and focus workflows; "
25
+ "`cdx view` for the browser viewer and focus workflows (supports `--lan`, `--focus <ref>`, `--port`, and all other viewer flags); "
26
26
  "`logics-manager sync read-doc|list-docs|search-docs|context-pack` for bounded document context; "
27
27
  "`logics-manager flow ...` for request/backlog/task lifecycle changes; and `logics-manager mcp ...` "
28
28
  "when an MCP surface is the right fit."
@@ -1173,7 +1173,7 @@ def create_session_service(options=None):
1173
1173
  "bundle_size_bytes": len(bundle_bytes),
1174
1174
  }
1175
1175
 
1176
- def import_bundle(file_path, passphrase=None, session_names=None, force=False):
1176
+ def import_bundle(file_path, passphrase=None, session_names=None, force=False, merge=False):
1177
1177
  if not file_path or not os.path.isfile(file_path):
1178
1178
  raise CdxError(f"Bundle file not found: {file_path}")
1179
1179
  with open(file_path, "rb") as handle:
@@ -1193,15 +1193,18 @@ def create_session_service(options=None):
1193
1193
 
1194
1194
  existing = {session["name"] for session in list_sessions()}
1195
1195
  conflicts = [name for name in names if name in existing]
1196
- if conflicts and not force:
1196
+ if conflicts and not force and not merge:
1197
1197
  raise CdxError(f"Import would overwrite existing sessions: {', '.join(conflicts)}")
1198
1198
 
1199
1199
  for session_payload in imported_sessions:
1200
1200
  name = session_payload["name"]
1201
1201
  _validate_new_session_name(name)
1202
1202
  provider = _normalize_provider(session_payload["provider"])
1203
- if name in existing:
1203
+ is_existing = name in existing
1204
+
1205
+ if is_existing and force:
1204
1206
  remove_session(name)
1207
+ is_existing = False
1205
1208
 
1206
1209
  session_root = _get_session_root(name)
1207
1210
  auth_home = _get_session_auth_home(name, provider)
@@ -1210,18 +1213,38 @@ def create_session_service(options=None):
1210
1213
  _ensure_private_dir(session_root)
1211
1214
  _ensure_private_dir(auth_home)
1212
1215
 
1213
- session_record = {
1214
- **session_payload,
1215
- "provider": provider,
1216
- "enabled": session_payload.get("enabled", True) is not False,
1217
- "sessionRoot": session_root,
1218
- "authHome": auth_home,
1219
- }
1220
- store["replace_session"](name, session_record)
1216
+ if is_existing and merge:
1217
+ existing_record = store["get_session"](name) or {}
1218
+ bundle_record = {
1219
+ **session_payload,
1220
+ "provider": provider,
1221
+ "enabled": session_payload.get("enabled", True) is not False,
1222
+ "sessionRoot": session_root,
1223
+ "authHome": auth_home,
1224
+ }
1225
+ # Existing values take precedence; bundle fills in missing keys only.
1226
+ merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
1227
+ merged_record["sessionRoot"] = session_root
1228
+ merged_record["authHome"] = auth_home
1229
+ store["replace_session"](name, merged_record)
1230
+ else:
1231
+ session_record = {
1232
+ **session_payload,
1233
+ "provider": provider,
1234
+ "enabled": session_payload.get("enabled", True) is not False,
1235
+ "sessionRoot": session_root,
1236
+ "authHome": auth_home,
1237
+ }
1238
+ store["replace_session"](name, session_record)
1221
1239
 
1222
1240
  state = (payload.get("states") or {}).get(name)
1223
1241
  if state is not None:
1224
- store["write_session_state"](name, state)
1242
+ if is_existing and merge:
1243
+ existing_state = store["read_session_state"](name) or {}
1244
+ merged_state = {**state, **{k: v for k, v in existing_state.items() if v is not None}}
1245
+ store["write_session_state"](name, merged_state)
1246
+ else:
1247
+ store["write_session_state"](name, state)
1225
1248
 
1226
1249
  for item in (payload.get("profiles") or {}).get(name, []):
1227
1250
  rel_path = _safe_relpath(item.get("path"))
@@ -1230,6 +1253,9 @@ def create_session_service(options=None):
1230
1253
  except (AttributeError, ValueError, UnicodeEncodeError) as error:
1231
1254
  raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
1232
1255
  dest_path = os.path.join(session_root, rel_path)
1256
+ # In merge mode, skip files that already exist locally.
1257
+ if is_existing and merge and os.path.exists(dest_path):
1258
+ continue
1233
1259
  _ensure_private_dir(os.path.dirname(dest_path))
1234
1260
  with open(dest_path, "wb") as handle:
1235
1261
  handle.write(content)