cdx-manager 0.11.0 → 0.11.1
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 +5 -1
- package/changelogs/CHANGELOGS_0_11_1.md +34 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +5 -1
- package/src/cli_args.py +16 -0
- package/src/cli_commands.py +16 -0
- package/src/cli_render.py +5 -0
- package/src/session_service.py +43 -0
- package/src/status_view.py +11 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -36,6 +36,7 @@ One command to launch any session. Zero auth juggling.
|
|
|
36
36
|
- **Instant launch.** `cdx work` opens your "work" session. `cdx personal` opens another. No config files to edit mid-flow.
|
|
37
37
|
- **Quick relaunch.** `cdx last` reopens the most recently launched assistant profile.
|
|
38
38
|
- **Auth guardrails.** `cdx` checks authentication before launching. If a session is not logged in, it tells you exactly what to run — no silent failures.
|
|
39
|
+
- **Optional session labels.** `cdx label <name> <label>` adds a short human label; list and full status tables show a `LABEL` column only when at least one session has one.
|
|
39
40
|
- **Usage at a glance.** `cdx status` shows token usage, 5-hour window quota, weekly quota, last-updated timestamps, priority guidance, and the last launched session in one aligned table.
|
|
40
41
|
- **Next-ready notification.** `cdx ready` schedules a native system notification for the next assistant that comes back from cooldown, then returns immediately.
|
|
41
42
|
- **Session control.** Disable a session without deleting it when an account is temporarily out of credits; disabled sessions remain visible and sort last.
|
|
@@ -345,6 +346,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
345
346
|
| `cdx add [provider] <name> [--model MODEL] [--json]` | Register a new session (`provider`: `codex`, `claude`, `antigravity`, or `ollama`; Ollama requires `--model`) |
|
|
346
347
|
| `cdx cp <source> <dest> [--json]` | Copy a session into another session name, overwriting the destination if it exists |
|
|
347
348
|
| `cdx ren <source> <dest> [--json]` | Rename a session and move its auth data |
|
|
349
|
+
| `cdx label <name> <label> [--json]` | Attach one short label to a session; tables show `LABEL` only when at least one label exists |
|
|
350
|
+
| `cdx label <name> --clear [--json]` | Clear a session label |
|
|
348
351
|
| `cdx login <name> [--json]` | Re-authenticate a session (logout + login) |
|
|
349
352
|
| `cdx logout <name> [--json]` | Log out of a session |
|
|
350
353
|
| `cdx disable <name> [--json]` | Disable a session without deleting it; disabled sessions stay visible and cannot launch |
|
|
@@ -399,6 +402,7 @@ Commands with machine-readable output:
|
|
|
399
402
|
- `cdx add ... --json`
|
|
400
403
|
- `cdx cp ... --json`
|
|
401
404
|
- `cdx ren ... --json`
|
|
405
|
+
- `cdx label ... --json`
|
|
402
406
|
- `cdx rmv ... --json`
|
|
403
407
|
- `cdx clean ... --json`
|
|
404
408
|
- `cdx disk ... --json`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# CDX Manager 0.11.1
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Added optional labels for saved sessions.
|
|
6
|
+
- Kept existing list and compact status output clean when no labels are present.
|
|
7
|
+
- Closed the session-label Logics corpus with full validation evidence.
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
### Optional session labels
|
|
12
|
+
|
|
13
|
+
`cdx label <name> <label>` now attaches one short human-readable label to a saved session. Labels are stored as session metadata outside the `launch` dictionary, so they do not affect provider commands, auth checks, runtime state, ready/next selection, or launch history behavior.
|
|
14
|
+
|
|
15
|
+
`cdx label <name> --clear` removes the label and returns the session to the same shape as legacy unlabeled records.
|
|
16
|
+
|
|
17
|
+
The default `cdx` list and full `cdx status` table add a `LABEL` column only when at least one row has a label. Unlabeled rows render as `-`, and `cdx status --small` remains compact.
|
|
18
|
+
|
|
19
|
+
JSON outputs for `cdx --json`, `cdx status --json`, `cdx status <name> --json`, and `cdx label ... --json` expose label values for automation consumers.
|
|
20
|
+
|
|
21
|
+
Session copy, rename, export, and import preserve labels.
|
|
22
|
+
|
|
23
|
+
### Documentation and workflow
|
|
24
|
+
|
|
25
|
+
README and CLI help now document `cdx label <name> <label>` and `cdx label <name> --clear`.
|
|
26
|
+
|
|
27
|
+
The optional session-label Logics request, backlog item, product brief, and orchestration task are closed with validation evidence.
|
|
28
|
+
|
|
29
|
+
## Validation
|
|
30
|
+
|
|
31
|
+
- `python3 -m unittest discover -s test`
|
|
32
|
+
- `logics-manager flow validate req_012_add_optional_labels_to_cdx_sessions`
|
|
33
|
+
- `logics-manager status`
|
|
34
|
+
- `logics-manager health`
|
|
@@ -136,6 +136,10 @@
|
|
|
136
136
|
"v0.10.0": {
|
|
137
137
|
"github_tarball_sha256": "20c473f4b17806d565c454ad4aa28758433665d8431251ebb937ab6a909e4adb",
|
|
138
138
|
"github_zip_sha256": "b6a7be359d8dc2a11d8c77749878875419fabd7cd5a6951502dec09d7791e211"
|
|
139
|
+
},
|
|
140
|
+
"v0.11.0": {
|
|
141
|
+
"github_tarball_sha256": "bc42f83f20ba1c6ff81f824c382801457f93cf8cb166acbe952c12d009560a70",
|
|
142
|
+
"github_zip_sha256": "1c03b4f03f53c46d0a5ba580fc8ade7b26fa3cb1bb5f029f703681f136b9ac1f"
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -26,6 +26,7 @@ from .cli_commands import (
|
|
|
26
26
|
handle_handoff,
|
|
27
27
|
handle_history,
|
|
28
28
|
handle_import,
|
|
29
|
+
handle_label,
|
|
29
30
|
handle_last,
|
|
30
31
|
handle_launch,
|
|
31
32
|
handle_launch_setting_alias,
|
|
@@ -76,7 +77,7 @@ from .status_view import (
|
|
|
76
77
|
)
|
|
77
78
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
78
79
|
|
|
79
|
-
VERSION = "0.11.
|
|
80
|
+
VERSION = "0.11.1"
|
|
80
81
|
|
|
81
82
|
# Public surface: this module is a facade. Names below are imported above
|
|
82
83
|
# purely to be re-exported (consumed by tests and external callers); listing
|
|
@@ -112,6 +113,7 @@ _COMMAND_HANDLERS = {
|
|
|
112
113
|
"handoff": handle_handoff,
|
|
113
114
|
"history": handle_history,
|
|
114
115
|
"import": handle_import,
|
|
116
|
+
"label": handle_label,
|
|
115
117
|
"last": handle_last,
|
|
116
118
|
"login": handle_login,
|
|
117
119
|
"logout": handle_logout,
|
|
@@ -200,6 +202,8 @@ def _print_help(use_color=False):
|
|
|
200
202
|
f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
|
|
201
203
|
f" {_style('cdx cp <source> <dest> [--json]', '36', use_color)}",
|
|
202
204
|
f" {_style('cdx ren <source> <dest> [--json]', '36', use_color)}",
|
|
205
|
+
f" {_style('cdx label <name> <label> [--json]', '36', use_color)}",
|
|
206
|
+
f" {_style('cdx label <name> --clear [--json]', '36', use_color)}",
|
|
203
207
|
f" {_style('cdx login <name> [--json]', '36', use_color)}",
|
|
204
208
|
f" {_style('cdx logout <name> [--json]', '36', use_color)}",
|
|
205
209
|
f" {_style('cdx disable <name> [--json]', '36', use_color)}",
|
package/src/cli_args.py
CHANGED
|
@@ -20,6 +20,7 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
|
|
|
20
20
|
IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
|
|
21
21
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
|
|
22
22
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
|
|
23
|
+
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
|
|
23
24
|
SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
|
|
24
25
|
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
|
|
25
26
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
@@ -107,6 +108,21 @@ def _parse_rename_args(args):
|
|
|
107
108
|
return {"source": args[0], "dest": args[1]}
|
|
108
109
|
|
|
109
110
|
|
|
111
|
+
def _parse_label_args(args):
|
|
112
|
+
parsed = _parse_flag_args(args, {
|
|
113
|
+
"--clear": {"key": "clear", "type": "bool", "default": False},
|
|
114
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
115
|
+
}, LABEL_USAGE, positionals_key="values", max_positionals=2)
|
|
116
|
+
values = parsed["values"]
|
|
117
|
+
if parsed["clear"]:
|
|
118
|
+
if len(values) != 1:
|
|
119
|
+
raise CdxError(LABEL_USAGE)
|
|
120
|
+
return {"name": values[0], "label": None, "clear": True, "json": parsed["json"]}
|
|
121
|
+
if len(values) != 2:
|
|
122
|
+
raise CdxError(LABEL_USAGE)
|
|
123
|
+
return {"name": values[0], "label": values[1], "clear": False, "json": parsed["json"]}
|
|
124
|
+
|
|
125
|
+
|
|
110
126
|
def _parse_remove_args(args):
|
|
111
127
|
parsed = _parse_flag_args(args, {
|
|
112
128
|
"--force": {"key": "force", "type": "bool", "default": False},
|
package/src/cli_commands.py
CHANGED
|
@@ -37,6 +37,7 @@ from .cli_args import (
|
|
|
37
37
|
_parse_history_args,
|
|
38
38
|
_parse_import_args,
|
|
39
39
|
_parse_json_flag,
|
|
40
|
+
_parse_label_args,
|
|
40
41
|
_parse_launch_setting_alias_args,
|
|
41
42
|
_parse_next_args,
|
|
42
43
|
_parse_remove_args,
|
|
@@ -732,6 +733,21 @@ def handle_rename(rest, ctx):
|
|
|
732
733
|
return 0
|
|
733
734
|
|
|
734
735
|
|
|
736
|
+
def handle_label(rest, ctx):
|
|
737
|
+
parsed = _parse_label_args(rest)
|
|
738
|
+
if parsed["clear"]:
|
|
739
|
+
session = ctx["service"]["clear_session_label"](parsed["name"])
|
|
740
|
+
message = f"Cleared label for {parsed['name']}"
|
|
741
|
+
else:
|
|
742
|
+
session = ctx["service"]["set_session_label"](parsed["name"], parsed["label"])
|
|
743
|
+
message = f"Set label for {parsed['name']}: {session['label']}"
|
|
744
|
+
if parsed["json"]:
|
|
745
|
+
_write_json(ctx, _json_success("label", message, session=session, label=session.get("label")))
|
|
746
|
+
return 0
|
|
747
|
+
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
748
|
+
return 0
|
|
749
|
+
|
|
750
|
+
|
|
735
751
|
def handle_remove(rest, ctx):
|
|
736
752
|
json_flag, args = _parse_json_flag(rest)
|
|
737
753
|
parsed = _parse_remove_args(args)
|
package/src/cli_render.py
CHANGED
|
@@ -123,10 +123,13 @@ def format_error(error, env=None, stderr=None):
|
|
|
123
123
|
def _format_sessions(service, use_color=False):
|
|
124
124
|
rows = service["format_list_rows"]()
|
|
125
125
|
has_provider = any(r.get("provider") for r in rows)
|
|
126
|
+
has_label = any(r.get("label") for r in rows)
|
|
126
127
|
has_launch = any(r.get("launch") for r in rows)
|
|
127
128
|
headers = ["SESSION"]
|
|
128
129
|
if has_provider:
|
|
129
130
|
headers.append("PROVIDER")
|
|
131
|
+
if has_label:
|
|
132
|
+
headers.append("LABEL")
|
|
130
133
|
headers.append("STATUS")
|
|
131
134
|
if has_launch:
|
|
132
135
|
headers.append("LAUNCH")
|
|
@@ -138,6 +141,8 @@ def _format_sessions(service, use_color=False):
|
|
|
138
141
|
parts = [name]
|
|
139
142
|
if has_provider:
|
|
140
143
|
parts.append(r.get("provider") or "n/a")
|
|
144
|
+
if has_label:
|
|
145
|
+
parts.append(r.get("label") or "-")
|
|
141
146
|
status = r.get("enabled_status") or ("enabled" if r.get("enabled", True) else "disabled")
|
|
142
147
|
parts.append(_style(status, "2" if status == "disabled" else "32", use_color))
|
|
143
148
|
if has_launch:
|
package/src/session_service.py
CHANGED
|
@@ -22,6 +22,7 @@ from .status_source import find_latest_status_artifact
|
|
|
22
22
|
DEFAULT_PROVIDER = PROVIDER_CODEX
|
|
23
23
|
ALLOWED_PROVIDERS = set(PROVIDERS)
|
|
24
24
|
MAX_SESSION_NAME_LENGTH = 64
|
|
25
|
+
MAX_SESSION_LABEL_LENGTH = 64
|
|
25
26
|
RESERVED_SESSION_NAMES = {
|
|
26
27
|
"add",
|
|
27
28
|
"can-resume",
|
|
@@ -40,6 +41,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
40
41
|
"history",
|
|
41
42
|
"import",
|
|
42
43
|
"last",
|
|
44
|
+
"label",
|
|
43
45
|
"login",
|
|
44
46
|
"logout",
|
|
45
47
|
"model",
|
|
@@ -534,10 +536,21 @@ def create_session_service(options=None):
|
|
|
534
536
|
if name in RESERVED_SESSION_NAMES:
|
|
535
537
|
raise CdxError(f"Session name is reserved: {name}")
|
|
536
538
|
|
|
539
|
+
def _normalize_session_label(label):
|
|
540
|
+
text = str(label or "").strip()
|
|
541
|
+
if not text:
|
|
542
|
+
raise CdxError("Session label is required")
|
|
543
|
+
if len(text) > MAX_SESSION_LABEL_LENGTH:
|
|
544
|
+
raise CdxError(f"Session label is too long (max {MAX_SESSION_LABEL_LENGTH} characters)")
|
|
545
|
+
if any(ord(ch) < 32 or ord(ch) == 127 for ch in text):
|
|
546
|
+
raise CdxError("Session label cannot contain control characters")
|
|
547
|
+
return text
|
|
548
|
+
|
|
537
549
|
def _build_export_session_record(session):
|
|
538
550
|
return {
|
|
539
551
|
"name": session["name"],
|
|
540
552
|
"provider": session["provider"],
|
|
553
|
+
"label": session.get("label"),
|
|
541
554
|
"enabled": session.get("enabled", True) is not False,
|
|
542
555
|
"createdAt": session.get("createdAt"),
|
|
543
556
|
"updatedAt": session.get("updatedAt"),
|
|
@@ -698,6 +711,7 @@ def create_session_service(options=None):
|
|
|
698
711
|
"lastStatusAt": None,
|
|
699
712
|
"lastStatus": None,
|
|
700
713
|
**({"launch": source.get("launch")} if source.get("launch") else {}),
|
|
714
|
+
**({"label": source.get("label")} if source.get("label") else {}),
|
|
701
715
|
"auth": {
|
|
702
716
|
"status": "unknown",
|
|
703
717
|
"lastCheckedAt": None,
|
|
@@ -888,6 +902,31 @@ def create_session_service(options=None):
|
|
|
888
902
|
"updatedAt": now,
|
|
889
903
|
})
|
|
890
904
|
|
|
905
|
+
def set_session_label(name, label):
|
|
906
|
+
session = store["get_session"](name)
|
|
907
|
+
if not session:
|
|
908
|
+
raise CdxError(f"Unknown session: {name}")
|
|
909
|
+
normalized = _normalize_session_label(label)
|
|
910
|
+
now = _local_now_iso()
|
|
911
|
+
return store["update_session"](name, lambda s: {
|
|
912
|
+
**s,
|
|
913
|
+
"label": normalized,
|
|
914
|
+
"updatedAt": now,
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
def clear_session_label(name):
|
|
918
|
+
session = store["get_session"](name)
|
|
919
|
+
if not session:
|
|
920
|
+
raise CdxError(f"Unknown session: {name}")
|
|
921
|
+
now = _local_now_iso()
|
|
922
|
+
|
|
923
|
+
def updater(s):
|
|
924
|
+
updated = {**s, "updatedAt": now}
|
|
925
|
+
updated.pop("label", None)
|
|
926
|
+
return updated
|
|
927
|
+
|
|
928
|
+
return store["update_session"](name, updater)
|
|
929
|
+
|
|
891
930
|
def set_launch_settings(name, settings):
|
|
892
931
|
session = store["get_session"](name)
|
|
893
932
|
if not session:
|
|
@@ -1106,6 +1145,7 @@ def create_session_service(options=None):
|
|
|
1106
1145
|
row_status = status if enabled else None
|
|
1107
1146
|
return {
|
|
1108
1147
|
"session_name": s["name"],
|
|
1148
|
+
"label": s.get("label"),
|
|
1109
1149
|
"provider": s["provider"],
|
|
1110
1150
|
"enabled": enabled,
|
|
1111
1151
|
"active": bool(_session_runtime(s["name"])) if enabled else False,
|
|
@@ -1273,6 +1313,7 @@ def create_session_service(options=None):
|
|
|
1273
1313
|
)
|
|
1274
1314
|
return [{
|
|
1275
1315
|
"name": s["name"],
|
|
1316
|
+
"label": s.get("label"),
|
|
1276
1317
|
"provider": s["provider"] if has_multiple else None,
|
|
1277
1318
|
"enabled": s.get("enabled", True) is not False,
|
|
1278
1319
|
"active": bool(_session_runtime(s["name"])) if s.get("enabled", True) is not False else False,
|
|
@@ -1519,6 +1560,8 @@ def create_session_service(options=None):
|
|
|
1519
1560
|
"list_sessions": list_sessions,
|
|
1520
1561
|
"get_session": get_session,
|
|
1521
1562
|
"set_session_enabled": set_session_enabled,
|
|
1563
|
+
"set_session_label": set_session_label,
|
|
1564
|
+
"clear_session_label": clear_session_label,
|
|
1522
1565
|
"set_launch_settings": set_launch_settings,
|
|
1523
1566
|
"unset_launch_settings": unset_launch_settings,
|
|
1524
1567
|
"record_status": record_status,
|
package/src/status_view.py
CHANGED
|
@@ -75,12 +75,19 @@ def _format_credits(value, empty="n/a"):
|
|
|
75
75
|
|
|
76
76
|
def _format_status_rows(rows, use_color=False, small=False):
|
|
77
77
|
has_provider = len({r["provider"] for r in rows}) > 1 and not small
|
|
78
|
+
has_label = any(r.get("label") for r in rows) and not small
|
|
78
79
|
if small:
|
|
79
80
|
headers = ["SESSION", "STATUS", "OK", "5H", "WEEK", "RESETS", "RESET 5H", "RESET WEEK"]
|
|
80
81
|
elif has_provider:
|
|
81
|
-
headers = ["SESSION", "PROV."
|
|
82
|
+
headers = ["SESSION", "PROV."]
|
|
83
|
+
if has_label:
|
|
84
|
+
headers.append("LABEL")
|
|
85
|
+
headers += ["STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
82
86
|
else:
|
|
83
|
-
headers = ["SESSION"
|
|
87
|
+
headers = ["SESSION"]
|
|
88
|
+
if has_label:
|
|
89
|
+
headers.append("LABEL")
|
|
90
|
+
headers += ["STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
84
91
|
if not rows:
|
|
85
92
|
if small:
|
|
86
93
|
return "SESSION STATUS OK 5H WEEK RESETS RESET 5H RESET WEEK\nNo saved sessions yet."
|
|
@@ -102,6 +109,8 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
102
109
|
base = [_format_session_name(r)]
|
|
103
110
|
if has_provider:
|
|
104
111
|
base.append(r.get("provider") or "n/a")
|
|
112
|
+
if has_label:
|
|
113
|
+
base.append(r.get("label") or "-")
|
|
105
114
|
status = r.get("status") or ("enabled" if r.get("enabled", True) else "disabled")
|
|
106
115
|
base.append(_style(status, "2" if status == "disabled" else "32", use_color))
|
|
107
116
|
if not small:
|