cdx-manager 0.9.16 → 0.11.0

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/src/cli.py CHANGED
@@ -3,10 +3,14 @@
3
3
  import json
4
4
  import os
5
5
  import sys
6
+ from datetime import datetime, timezone
6
7
 
7
8
  from .cli_commands import (
8
9
  API_SCHEMA_VERSION,
9
10
  STATUS_USAGE,
11
+ _collect_profile_cleanup_candidates,
12
+ _directory_child_sizes,
13
+ _directory_size_bytes,
10
14
  handle_add,
11
15
  handle_can_resume,
12
16
  handle_clean,
@@ -15,6 +19,7 @@ from .cli_commands import (
15
19
  handle_context,
16
20
  handle_copy,
17
21
  handle_disable,
22
+ handle_disk,
18
23
  handle_doctor,
19
24
  handle_enable,
20
25
  handle_export,
@@ -31,6 +36,7 @@ from .cli_commands import (
31
36
  handle_remove,
32
37
  handle_rename,
33
38
  handle_repair,
39
+ handle_reset,
34
40
  handle_resume,
35
41
  handle_run,
36
42
  handle_run_report,
@@ -44,6 +50,10 @@ from .cli_commands import (
44
50
  handle_update,
45
51
  handle_view,
46
52
  )
53
+ from .cli_commands import (
54
+ _format_bytes as _format_disk_bytes,
55
+ )
56
+ from .cli_helpers import _update_notice_warnings
47
57
  from .cli_render import (
48
58
  _format_sessions,
49
59
  _pad_table,
@@ -66,7 +76,7 @@ from .status_view import (
66
76
  )
67
77
  from .update_check import check_for_update, check_logics_manager_for_update
68
78
 
69
- VERSION = "0.9.16"
79
+ VERSION = "0.11.0"
70
80
 
71
81
  # Public surface: this module is a facade. Names below are imported above
72
82
  # purely to be re-exported (consumed by tests and external callers); listing
@@ -94,6 +104,7 @@ _COMMAND_HANDLERS = {
94
104
  "config": handle_config,
95
105
  "configs": handle_configs,
96
106
  "context": handle_context,
107
+ "disk": handle_disk,
97
108
  "doctor": handle_doctor,
98
109
  "disable": handle_disable,
99
110
  "enable": handle_enable,
@@ -107,6 +118,7 @@ _COMMAND_HANDLERS = {
107
118
  "next": handle_next,
108
119
  "notify": handle_notify,
109
120
  "repair": handle_repair,
121
+ "reset": handle_reset,
110
122
  "rename": handle_rename,
111
123
  "resume": handle_resume,
112
124
  "can-resume": handle_can_resume,
@@ -165,12 +177,14 @@ def _print_help(use_color=False):
165
177
  f" {_style('cdx status --small|-s [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
166
178
  f" {_style('cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]', '36', use_color)}",
167
179
  f" {_style('cdx next [--json] [--refresh]', '36', use_color)}",
180
+ f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
168
181
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
169
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--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', '36', use_color)}",
182
+ f" {_style('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', '36', use_color)}",
170
183
  f" {_style('cdx runs [--limit N] --json', '36', use_color)}",
171
184
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
172
185
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
173
186
  f" {_style('cdx context show|path|init|edit|clear|set [text...] [--json]', '36', use_color)}",
187
+ f" {_style('cdx disk [profiles] [--candidates] [--json]', '36', use_color)}",
174
188
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
175
189
  f" {_style('cdx configs [--json]', '36', use_color)}",
176
190
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
@@ -191,7 +205,8 @@ def _print_help(use_color=False):
191
205
  f" {_style('cdx disable <name> [--json]', '36', use_color)}",
192
206
  f" {_style('cdx enable <name> [--json]', '36', use_color)}",
193
207
  f" {_style('cdx rmv <name> [--force] [--json]', '36', use_color)}",
194
- f" {_style('cdx clean [name] [--json]', '36', use_color)}",
208
+ f" {_style('cdx clean [name] [--yes] [--json]', '36', use_color)}",
209
+ f" {_style('cdx clean profiles (--tmp|--old-logs DAYS) [--yes] [--json]', '36', use_color)}",
195
210
  f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force] [--json]', '36', use_color)}",
196
211
  f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR|--passphrase-stdin] [--force|--merge] [--json]', '36', use_color)}",
197
212
  f" {_style('cdx doctor [--json]', '36', use_color)}",
@@ -264,36 +279,156 @@ def _get_update_notices(service, env, options):
264
279
  )
265
280
  if logics_notice:
266
281
  notices.append(logics_notice)
282
+ disk_notice = _get_disk_cleanup_notice(service, options)
283
+ if disk_notice:
284
+ notices.append(disk_notice)
267
285
  return notices
268
286
 
269
287
 
270
- def _update_warning_payload(notices):
271
- if isinstance(notices, dict):
272
- notices = [notices]
273
- if not notices:
274
- return []
275
- warnings = []
276
- for notice in notices:
277
- tool = notice.get("tool") or "cdx-manager"
278
- current = notice.get("current_version") or VERSION
279
- command = notice.get("update_command") or ("cdx update" if tool == "cdx-manager" else None)
280
- message = f"Update available: {tool} {notice['latest_version']} (current {current})"
281
- if command:
282
- message = f"{message}. Run: {command}"
283
- warnings.append({
284
- "code": "update_available" if tool == "cdx-manager" else f"{tool.replace('-', '_')}_update_available",
285
- "message": message,
286
- "tool": tool,
287
- "latest_version": notice["latest_version"],
288
- "current_version": current,
289
- "update_command": command,
290
- "url": notice.get("url"),
291
- })
292
- return warnings
288
+ _DISK_CHECK_INTERVAL_SECONDS = 24 * 60 * 60
289
+ _DISK_HOME_THRESHOLD_BYTES = 10 * 1024 * 1024 * 1024
290
+ _DISK_RECLAIMABLE_THRESHOLD_BYTES = 1 * 1024 * 1024 * 1024
291
+ _DISK_CATEGORY_THRESHOLD_BYTES = 500 * 1024 * 1024
292
+ _DISK_PROFILE_THRESHOLD_BYTES = 2 * 1024 * 1024 * 1024
293
+
294
+
295
+ def _now_utc(options):
296
+ now_fn = options.get("now")
297
+ value = now_fn() if callable(now_fn) else None
298
+ if isinstance(value, datetime):
299
+ return value.astimezone(timezone.utc)
300
+ return datetime.now(timezone.utc)
301
+
302
+
303
+ def _read_json_file(path):
304
+ try:
305
+ with open(path, encoding="utf-8") as handle:
306
+ return json.load(handle)
307
+ except (OSError, ValueError):
308
+ return {}
309
+
310
+
311
+ def _write_json_file(path, payload):
312
+ os.makedirs(os.path.dirname(path), exist_ok=True)
313
+ tmp_path = f"{path}.tmp"
314
+ with open(tmp_path, "w", encoding="utf-8") as handle:
315
+ json.dump(payload, handle, indent=2, sort_keys=True)
316
+ handle.write("\n")
317
+ os.replace(tmp_path, path)
318
+
319
+
320
+ def _parse_iso_datetime(value):
321
+ if not value:
322
+ return None
323
+ try:
324
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00")).astimezone(timezone.utc)
325
+ except ValueError:
326
+ return None
327
+
328
+
329
+ def _get_disk_cleanup_notice(service, options):
330
+ checker = options.get("checkDiskCleanup")
331
+ if callable(checker):
332
+ return checker(service, options)
333
+
334
+ base_dir = service["base_dir"]
335
+ state_path = os.path.join(base_dir, "state", "disk-cleanup-check.json")
336
+ now = _now_utc(options)
337
+ state = _read_json_file(state_path)
338
+ last_checked = _parse_iso_datetime(state.get("last_checked_at"))
339
+ if last_checked and (now - last_checked).total_seconds() < _DISK_CHECK_INTERVAL_SECONDS:
340
+ return None
341
+
342
+ runner = options.get("diskUsageRunner")
343
+ try:
344
+ home_bytes = _directory_size_bytes(base_dir, runner=runner)
345
+ profiles_dir = os.path.join(base_dir, "profiles")
346
+ profile_rows = _directory_child_sizes(profiles_dir, runner=runner)
347
+ profile_total = sum(row["bytes"] for row in profile_rows)
348
+ candidates = _collect_profile_cleanup_candidates(base_dir, runner=runner, now=now.timestamp())
349
+ except Exception:
350
+ return None
351
+
352
+ reclaimable_bytes = sum(item["bytes"] for item in candidates)
353
+ tmp_bytes = sum(item["bytes"] for item in candidates if item["kind"].startswith("tmp-"))
354
+ old_log_bytes = sum(item["bytes"] for item in candidates if item["kind"].startswith("old-logs-"))
355
+ large_profiles = [
356
+ {
357
+ "name": row["name"],
358
+ "bytes": row["bytes"],
359
+ "size": _format_disk_bytes(row["bytes"]),
360
+ "share_pct": round((row["bytes"] / profile_total) * 100, 1) if profile_total else 0,
361
+ }
362
+ for row in profile_rows
363
+ if row["bytes"] >= _DISK_PROFILE_THRESHOLD_BYTES
364
+ or (
365
+ profile_total >= _DISK_RECLAIMABLE_THRESHOLD_BYTES
366
+ and row["bytes"] / profile_total >= 0.25
367
+ )
368
+ ]
369
+ triggered = (
370
+ home_bytes >= _DISK_HOME_THRESHOLD_BYTES
371
+ or reclaimable_bytes >= _DISK_RECLAIMABLE_THRESHOLD_BYTES
372
+ or tmp_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES
373
+ or old_log_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES
374
+ or bool(large_profiles)
375
+ )
376
+ state_payload = {
377
+ "last_checked_at": now.isoformat(),
378
+ "home_bytes": home_bytes,
379
+ "reclaimable_bytes": reclaimable_bytes,
380
+ "tmp_bytes": tmp_bytes,
381
+ "old_log_bytes": old_log_bytes,
382
+ "large_profiles": large_profiles,
383
+ }
384
+ try:
385
+ _write_json_file(state_path, state_payload)
386
+ except OSError:
387
+ pass
388
+ if not triggered:
389
+ return None
390
+
391
+ if reclaimable_bytes > 0:
392
+ message = (
393
+ f"Disk cleanup available: {_format_disk_bytes(reclaimable_bytes)} reclaimable "
394
+ f"in CDX profiles. Inspect: cdx disk profiles --candidates"
395
+ )
396
+ else:
397
+ message = (
398
+ f"CDX_HOME uses {_format_disk_bytes(home_bytes)}. "
399
+ "Inspect: cdx disk profiles --candidates"
400
+ )
401
+ if tmp_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES:
402
+ message = (
403
+ f"{message}. Safe temp cleanup: {_format_disk_bytes(tmp_bytes)} "
404
+ "with cdx clean profiles --tmp"
405
+ )
406
+ if old_log_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES:
407
+ message = (
408
+ f"{message}. Old logs: {_format_disk_bytes(old_log_bytes)} "
409
+ "with cdx clean profiles --old-logs 30d"
410
+ )
411
+ return {
412
+ "tool": "cdx-disk",
413
+ "code": "disk_cleanup_available",
414
+ "message": message,
415
+ "reclaimable_bytes": reclaimable_bytes,
416
+ "reclaimable_size": _format_disk_bytes(reclaimable_bytes),
417
+ "home_bytes": home_bytes,
418
+ "home_size": _format_disk_bytes(home_bytes),
419
+ "tmp_bytes": tmp_bytes,
420
+ "tmp_size": _format_disk_bytes(tmp_bytes),
421
+ "old_log_bytes": old_log_bytes,
422
+ "old_log_size": _format_disk_bytes(old_log_bytes),
423
+ "large_profiles": large_profiles,
424
+ "inspect_command": "cdx disk profiles --candidates",
425
+ "cleanup_tmp_command": "cdx clean profiles --tmp" if tmp_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES else None,
426
+ "cleanup_old_logs_command": "cdx clean profiles --old-logs 30d" if old_log_bytes >= _DISK_CATEGORY_THRESHOLD_BYTES else None,
427
+ }
293
428
 
294
429
 
295
430
  def _update_warning_text(notices):
296
- payloads = _update_warning_payload(notices)
431
+ payloads = _update_notice_warnings({"update_notices": notices or [], "version": VERSION})
297
432
  if not payloads:
298
433
  return None
299
434
  return "\n".join(payload["message"] for payload in payloads)
@@ -405,7 +540,7 @@ def _list_json_payload(rows, notices=None):
405
540
  "ok": True,
406
541
  "action": "list",
407
542
  "message": "Listed known sessions",
408
- "warnings": _update_warning_payload(notices),
543
+ "warnings": _update_notice_warnings({"update_notices": notices or [], "version": VERSION}),
409
544
  "sessions": rows,
410
545
  }
411
546
 
package/src/cli_args.py CHANGED
@@ -13,14 +13,15 @@ from .provider_runtime import _normalize_reasoning_effort
13
13
 
14
14
  STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECONDS] | cdx status --small|-s [--refresh|--cached] [--timeout SECONDS] | cdx status <name> [--json] [--refresh|--cached] [--timeout SECONDS]"
15
15
  DOCTOR_USAGE = "Usage: cdx doctor [--json]"
16
+ DISK_USAGE = "Usage: cdx disk [profiles] [--candidates] [--json]"
16
17
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
17
18
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
18
19
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
19
- IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
20
+ IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
20
21
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
21
22
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
22
23
  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]"
23
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
24
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--priority|--all) [--json]"
24
25
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
25
26
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
26
27
  CONFIGS_USAGE = "Usage: cdx configs [--json]"
@@ -185,6 +186,7 @@ def _parse_set_args(args):
185
186
  def _parse_unset_args(args):
186
187
  parsed = _parse_flag_args(args, {
187
188
  "--power": {"key": "power", "type": "bool", "default": False},
189
+ "--reasoning-effort": {"key": "reasoning_effort", "type": "bool", "default": False},
188
190
  "--permission": {"key": "permission", "type": "bool", "default": False},
189
191
  "--fast": {"key": "fast", "type": "bool", "default": False},
190
192
  "--rtk": {"key": "rtk", "type": "bool", "default": False},
@@ -206,8 +208,8 @@ def _parse_unset_args(args):
206
208
  raise CdxError(UNSET_USAGE)
207
209
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
208
210
  raise CdxError(UNSET_USAGE)
209
- keys = ["power", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
210
- key for key in ("power", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
211
+ keys = ["power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority"] if parsed["all"] else [
212
+ key for key in ("power", "reasoning_effort", "permission", "fast", "rtk", "logics", "model", "priority") if parsed[key]
211
213
  ]
212
214
  if not keys:
213
215
  raise CdxError(UNSET_USAGE)
@@ -404,18 +406,18 @@ def _format_period_datetime(value):
404
406
  return value.isoformat(timespec="seconds")
405
407
 
406
408
 
407
- def _parse_history_period(parsed, now):
409
+ def _parse_history_period(parsed, now, usage=HISTORY_USAGE):
408
410
  since = parsed.get("since")
409
411
  from_value = parsed.get("from")
410
412
  to_value = parsed.get("to")
411
413
  if since and from_value:
412
- raise CdxError("Usage: cdx history cannot combine --since and --from.")
413
- start = _parse_since_value(since, now, HISTORY_USAGE) if since else None
414
+ raise CdxError(f"{usage} cannot combine --since and --from.")
415
+ start = _parse_since_value(since, now, usage) if since else None
414
416
  if from_value:
415
- start = _parse_datetime_value(from_value, HISTORY_USAGE, end_of_day=False)
416
- end = _parse_datetime_value(to_value, HISTORY_USAGE, end_of_day=True) if to_value else None
417
+ start = _parse_datetime_value(from_value, usage, end_of_day=False)
418
+ end = _parse_datetime_value(to_value, usage, end_of_day=True) if to_value else None
417
419
  if start and end and start.timestamp() > end.timestamp():
418
- raise CdxError("Usage: cdx history period start must be before period end.")
420
+ raise CdxError(f"{usage} period start must be before period end.")
419
421
  return {
420
422
  "from": _format_period_datetime(start),
421
423
  "to": _format_period_datetime(end),
@@ -479,7 +481,7 @@ def _parse_history_args(args, now=None):
479
481
  "--to": {"key": "to", "type": "str", "default": None},
480
482
  "--json": {"key": "json", "type": "bool", "default": False},
481
483
  }, HISTORY_USAGE, positionals_key="names", max_positionals=1)
482
- period = _parse_history_period(parsed, now)
484
+ period = _parse_history_period(parsed, now, HISTORY_USAGE)
483
485
  return {
484
486
  "name": parsed["names"][0] if parsed["names"] else None,
485
487
  "limit": parsed["limit"],
@@ -497,7 +499,7 @@ def _parse_stats_args(args, now=None):
497
499
  "--to": {"key": "to", "type": "str", "default": None},
498
500
  "--json": {"key": "json", "type": "bool", "default": False},
499
501
  }, STATS_USAGE, positionals_key="names", max_positionals=1)
500
- period = _parse_history_period(parsed, now)
502
+ period = _parse_history_period(parsed, now, STATS_USAGE)
501
503
  return {
502
504
  "name": parsed["names"][0] if parsed["names"] else None,
503
505
  "period": period,
@@ -611,6 +613,7 @@ def _parse_import_args(args):
611
613
  parsed = _parse_flag_args(args, {
612
614
  "--force": {"key": "force", "type": "bool", "default": False},
613
615
  "--merge": {"key": "merge", "type": "bool", "default": False},
616
+ "--allow-authless-force": {"key": "allow_authless_force", "type": "bool", "default": False},
614
617
  "--json": {"key": "json", "type": "bool", "default": False},
615
618
  "--sessions": {
616
619
  "key": "session_names",