cdx-manager 0.7.2 → 0.7.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 +15 -6
- package/changelogs/CHANGELOGS_0_7_3.md +37 -0
- package/changelogs/CHANGELOGS_0_7_4.md +45 -0
- package/checksums/release-archives.json +8 -0
- package/install.ps1 +5 -1
- package/install.sh +7 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +19 -1
- package/src/claude_usage.py +8 -0
- package/src/cli.py +12 -2
- package/src/cli_commands.py +278 -121
- package/src/cli_render.py +3 -0
- package/src/provider_runtime.py +39 -12
- package/src/run_command.py +83 -0
- package/src/session_service.py +63 -23
- package/src/status_view.py +28 -7
package/src/cli_commands.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
9
|
|
|
10
10
|
from .claude_refresh import _refresh_claude_sessions
|
|
11
|
-
from .cli_render import _dim, _info, _success, _warn
|
|
11
|
+
from .cli_render import _dim, _info, _style, _success, _warn
|
|
12
12
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA, PROVIDERS
|
|
13
13
|
from .context_store import (
|
|
14
14
|
clear_context,
|
|
@@ -40,8 +40,9 @@ from .provider_runtime import (
|
|
|
40
40
|
)
|
|
41
41
|
from .repair import format_repair_report, repair_health
|
|
42
42
|
from .backup_bundle import read_bundle_meta
|
|
43
|
-
from .run_usage import
|
|
44
|
-
from .
|
|
43
|
+
from .run_usage import extract_run_usage
|
|
44
|
+
from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
|
|
45
|
+
from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
45
46
|
from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
|
|
46
47
|
from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
|
|
47
48
|
|
|
@@ -58,10 +59,12 @@ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--pow
|
|
|
58
59
|
UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--model|--priority|--all) [--json]"
|
|
59
60
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
60
61
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
62
|
+
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
61
63
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
62
64
|
STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
63
65
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
64
66
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort low|medium|high] [--min-power low|medium|high] [--require-ready] [--refresh] --json"
|
|
67
|
+
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
65
68
|
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT) [--provider PROVIDER] [--model MODEL] [--reasoning-effort low|medium|high] [--power low|medium|high] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] --json"
|
|
66
69
|
API_SCHEMA_VERSION = 1
|
|
67
70
|
HANDOFF_TRANSCRIPT_CHARS = 120000
|
|
@@ -180,15 +183,24 @@ def _make_export_progress(ctx):
|
|
|
180
183
|
|
|
181
184
|
|
|
182
185
|
def _make_status_progress(ctx):
|
|
186
|
+
progress_state = {"checked": 0, "total": 0}
|
|
187
|
+
|
|
183
188
|
def progress(event):
|
|
184
189
|
kind = event.get("event")
|
|
185
190
|
if kind == "status_started":
|
|
191
|
+
progress_state["checked"] = 0
|
|
192
|
+
progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
|
|
186
193
|
message = f"Resolving status for {event.get('session_count', 0)} session(s)..."
|
|
187
194
|
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
188
195
|
elif kind == "session_started":
|
|
189
196
|
provider = event.get("provider") or "session"
|
|
190
197
|
message = f"Checking {event.get('session_name')} ({provider})..."
|
|
191
198
|
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
199
|
+
elif kind == "session_finished" and not event.get("cache_hit"):
|
|
200
|
+
progress_state["checked"] += 1
|
|
201
|
+
total = progress_state["total"] or progress_state["checked"]
|
|
202
|
+
message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
|
|
203
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
192
204
|
elif kind == "status_finished":
|
|
193
205
|
message = f"Resolved {event.get('row_count', 0)} status row(s)."
|
|
194
206
|
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
@@ -196,18 +208,27 @@ def _make_status_progress(ctx):
|
|
|
196
208
|
|
|
197
209
|
|
|
198
210
|
def _make_notify_progress(ctx):
|
|
211
|
+
progress_state = {"checked": 0, "total": 0}
|
|
212
|
+
|
|
199
213
|
def progress(event):
|
|
200
214
|
kind = event.get("event")
|
|
201
215
|
if kind == "notify_check_started":
|
|
202
216
|
target = event.get("session_name") or "next ready session"
|
|
203
217
|
ctx["out"](f"{_info(f'Checking notification target: {target}...', ctx['use_color'])}\n")
|
|
204
218
|
elif kind == "status_started":
|
|
219
|
+
progress_state["checked"] = 0
|
|
220
|
+
progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
|
|
205
221
|
message = f"Loading status for {event.get('session_count', 0)} session(s)..."
|
|
206
222
|
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
207
223
|
elif kind == "session_started":
|
|
208
224
|
provider = event.get("provider") or "session"
|
|
209
225
|
message = f"Checking {event.get('session_name')} ({provider})..."
|
|
210
226
|
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
227
|
+
elif kind == "session_finished" and not event.get("cache_hit"):
|
|
228
|
+
progress_state["checked"] += 1
|
|
229
|
+
total = progress_state["total"] or progress_state["checked"]
|
|
230
|
+
message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
|
|
231
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
211
232
|
elif kind == "notify_waiting":
|
|
212
233
|
message = f"{event.get('message')}; checking again in {event.get('poll')}s..."
|
|
213
234
|
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
@@ -580,6 +601,13 @@ def _parse_config_args(args):
|
|
|
580
601
|
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
581
602
|
|
|
582
603
|
|
|
604
|
+
def _parse_configs_args(args):
|
|
605
|
+
parsed = _parse_flag_args(args, {
|
|
606
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
607
|
+
}, CONFIGS_USAGE)
|
|
608
|
+
return {"json": parsed["json"]}
|
|
609
|
+
|
|
610
|
+
|
|
583
611
|
def _parse_select_args(args):
|
|
584
612
|
parsed = _parse_flag_args(args, {
|
|
585
613
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SELECT_USAGE)},
|
|
@@ -605,6 +633,14 @@ def _parse_select_args(args):
|
|
|
605
633
|
}
|
|
606
634
|
|
|
607
635
|
|
|
636
|
+
def _parse_next_args(args):
|
|
637
|
+
parsed = _parse_flag_args(args, {
|
|
638
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
639
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
640
|
+
}, NEXT_USAGE)
|
|
641
|
+
return {"json": parsed["json"], "refresh": parsed["refresh"]}
|
|
642
|
+
|
|
643
|
+
|
|
608
644
|
def _parse_timeout_seconds(value):
|
|
609
645
|
try:
|
|
610
646
|
parsed = float(value)
|
|
@@ -1211,70 +1247,73 @@ def handle_select(rest, ctx):
|
|
|
1211
1247
|
return 0
|
|
1212
1248
|
|
|
1213
1249
|
|
|
1214
|
-
def
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
def
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
"
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
"
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1250
|
+
def _format_next_pct(value):
|
|
1251
|
+
return "n/a" if value is None else f"{value}%"
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def _next_action(row):
|
|
1255
|
+
instruction = format_priority_instruction(row, "first")
|
|
1256
|
+
return "refresh" if instruction.startswith("refresh ") else "use"
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _format_next_selection(session, row, use_color=False):
|
|
1260
|
+
from .cli_render import _pad_table
|
|
1261
|
+
|
|
1262
|
+
action = _next_action(row)
|
|
1263
|
+
command = f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}"
|
|
1264
|
+
rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROVIDER", "OK", "5H", "WEEK", "REASON"]]]
|
|
1265
|
+
rows.append([
|
|
1266
|
+
_style(session["name"], "36", use_color),
|
|
1267
|
+
_dim(session.get("provider") or "-", use_color),
|
|
1268
|
+
_format_next_pct(row.get("available_pct")),
|
|
1269
|
+
_format_next_pct(row.get("remaining_5h_pct")),
|
|
1270
|
+
_format_next_pct(row.get("remaining_week_pct")),
|
|
1271
|
+
format_priority_instruction(row, "first"),
|
|
1272
|
+
])
|
|
1273
|
+
return "\n".join([
|
|
1274
|
+
_style("Next assistant:", "1", use_color),
|
|
1275
|
+
_pad_table(rows),
|
|
1276
|
+
"",
|
|
1277
|
+
f"{_style('Run:', '1', use_color)} {_style(command, '36', use_color)}",
|
|
1278
|
+
])
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def handle_next(rest, ctx):
|
|
1282
|
+
parsed = _parse_next_args(rest)
|
|
1283
|
+
rows = ctx["service"]["get_status_rows"](force_refresh=parsed["refresh"])
|
|
1284
|
+
priority = recommend_priority_rows(rows)
|
|
1285
|
+
if not priority:
|
|
1286
|
+
message = "No usable session status yet."
|
|
1287
|
+
if parsed["json"]:
|
|
1288
|
+
_write_json(ctx, _json_failure("next", "no_suitable_session", message, selection_policy="status_priority"))
|
|
1289
|
+
return 1
|
|
1290
|
+
ctx["out"](f"{_warn(message, ctx['use_color'])}\n")
|
|
1291
|
+
return 1
|
|
1292
|
+
row = priority[0]
|
|
1293
|
+
session_name = row.get("session_name")
|
|
1294
|
+
session = ctx["service"]["get_session"](session_name)
|
|
1295
|
+
if not session:
|
|
1296
|
+
message = f"Selected status row has no matching session: {session_name}"
|
|
1297
|
+
if parsed["json"]:
|
|
1298
|
+
_write_json(ctx, _json_failure("next", "missing_session", message, selection_policy="status_priority", row=row))
|
|
1299
|
+
return 1
|
|
1300
|
+
raise CdxError(message)
|
|
1301
|
+
action = _next_action(row)
|
|
1302
|
+
message = f"Selected next session {session['name']}"
|
|
1303
|
+
if parsed["json"]:
|
|
1304
|
+
_write_json(ctx, _json_success(
|
|
1305
|
+
"next",
|
|
1306
|
+
message,
|
|
1307
|
+
session=session,
|
|
1308
|
+
row=row,
|
|
1309
|
+
recommended_action=action,
|
|
1310
|
+
command=f"cdx status {session['name']} --refresh" if action == "refresh" else f"cdx {session['name']}",
|
|
1311
|
+
reason=format_priority_instruction(row, "first"),
|
|
1312
|
+
selection_policy="status_priority",
|
|
1313
|
+
))
|
|
1314
|
+
return 0
|
|
1315
|
+
ctx["out"](f"{_format_next_selection(session, row, ctx['use_color'])}\n")
|
|
1316
|
+
return 0
|
|
1278
1317
|
|
|
1279
1318
|
|
|
1280
1319
|
def handle_run(rest, ctx):
|
|
@@ -1309,7 +1348,7 @@ def handle_run(rest, ctx):
|
|
|
1309
1348
|
cwd = os.path.abspath(parsed["cwd"])
|
|
1310
1349
|
if not os.path.isdir(cwd):
|
|
1311
1350
|
raise CdxError(f"Invalid cwd: {parsed['cwd']}")
|
|
1312
|
-
prompt =
|
|
1351
|
+
prompt = read_run_prompt(parsed)
|
|
1313
1352
|
launch_updates = {}
|
|
1314
1353
|
if parsed.get("model"):
|
|
1315
1354
|
launch_updates["model"] = parsed["model"]
|
|
@@ -1334,6 +1373,7 @@ def handle_run(rest, ctx):
|
|
|
1334
1373
|
stdin_is_tty=False,
|
|
1335
1374
|
behavior="launch",
|
|
1336
1375
|
signal_emitter=ctx.get("signal_emitter"),
|
|
1376
|
+
trust_local_credentials=False,
|
|
1337
1377
|
)
|
|
1338
1378
|
run_info = _run_headless_provider_command(
|
|
1339
1379
|
run_session,
|
|
@@ -1353,7 +1393,7 @@ def handle_run(rest, ctx):
|
|
|
1353
1393
|
"exit_code": 0,
|
|
1354
1394
|
**run_info,
|
|
1355
1395
|
})
|
|
1356
|
-
_write_json(ctx,
|
|
1396
|
+
_write_json(ctx, run_result_payload(API_SCHEMA_VERSION, True, parsed, run_session, run_info=run_info))
|
|
1357
1397
|
return 0
|
|
1358
1398
|
message = "Provider process timed out." if run_info.get("timed_out") else "Provider process exited with a non-zero status."
|
|
1359
1399
|
error = CdxError(message, run_info.get("returncode") or 1)
|
|
@@ -1364,7 +1404,8 @@ def handle_run(rest, ctx):
|
|
|
1364
1404
|
"exit_code": error.exit_code,
|
|
1365
1405
|
**run_info,
|
|
1366
1406
|
})
|
|
1367
|
-
_write_json(ctx,
|
|
1407
|
+
_write_json(ctx, run_result_payload(
|
|
1408
|
+
API_SCHEMA_VERSION,
|
|
1368
1409
|
False,
|
|
1369
1410
|
parsed,
|
|
1370
1411
|
run_session,
|
|
@@ -1376,28 +1417,102 @@ def handle_run(rest, ctx):
|
|
|
1376
1417
|
return error.exit_code or 1
|
|
1377
1418
|
except CdxError as error:
|
|
1378
1419
|
run_info = getattr(error, "run_info", None)
|
|
1379
|
-
_write_json(ctx,
|
|
1420
|
+
_write_json(ctx, run_result_payload(
|
|
1421
|
+
API_SCHEMA_VERSION,
|
|
1380
1422
|
False,
|
|
1381
1423
|
locals().get("parsed", {}) or {},
|
|
1382
1424
|
locals().get("session"),
|
|
1383
1425
|
run_info=run_info,
|
|
1384
1426
|
error=error,
|
|
1385
1427
|
error_source="cdx",
|
|
1386
|
-
error_code=
|
|
1428
|
+
error_code=run_cdx_error_code(error),
|
|
1387
1429
|
))
|
|
1388
1430
|
return error.exit_code
|
|
1389
1431
|
|
|
1390
1432
|
|
|
1391
|
-
def _format_launch_config(session):
|
|
1433
|
+
def _format_launch_config(session, use_color=False):
|
|
1434
|
+
from .cli_render import _dim, _pad_table, _style
|
|
1435
|
+
|
|
1392
1436
|
launch = session.get("launch") or {}
|
|
1437
|
+
rows = [[_style("SETTING", "1", use_color), _style("VALUE", "1", use_color)]]
|
|
1438
|
+
for key, label in [
|
|
1439
|
+
("power", "Power"),
|
|
1440
|
+
("permission", "Permission"),
|
|
1441
|
+
("fast", "Fast"),
|
|
1442
|
+
("rtk", "RTK"),
|
|
1443
|
+
("model", "Model"),
|
|
1444
|
+
("priority", "Priority"),
|
|
1445
|
+
]:
|
|
1446
|
+
rows.append([
|
|
1447
|
+
_dim(label, use_color),
|
|
1448
|
+
_format_launch_setting_value(launch, key, use_color=use_color),
|
|
1449
|
+
])
|
|
1450
|
+
provider_label = f"({session['provider']})"
|
|
1393
1451
|
return "\n".join([
|
|
1394
|
-
|
|
1395
|
-
f"
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1452
|
+
_style("Launch settings:", "1", use_color),
|
|
1453
|
+
f"{_style(session['name'], '36', use_color)} {_dim(provider_label, use_color)}",
|
|
1454
|
+
_pad_table(rows),
|
|
1455
|
+
"",
|
|
1456
|
+
_dim(_format_launch_settings_hint(session["name"]), use_color),
|
|
1457
|
+
])
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def _format_launch_settings_hint(name="<name>"):
|
|
1461
|
+
return (
|
|
1462
|
+
f"Set a value: cdx set {name} --power medium --permission auto "
|
|
1463
|
+
"--fast on --rtk on --model MODEL --priority 80"
|
|
1464
|
+
)
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def _format_launch_setting_value(launch, key, use_color=False):
|
|
1468
|
+
if key == "fast" or key == "rtk":
|
|
1469
|
+
if launch.get(key) is True:
|
|
1470
|
+
return _style("on", "32", use_color)
|
|
1471
|
+
if launch.get(key) is False:
|
|
1472
|
+
return _style("off", "2", use_color)
|
|
1473
|
+
return _dim("default", use_color)
|
|
1474
|
+
value = launch.get(key)
|
|
1475
|
+
if value is None or value == "":
|
|
1476
|
+
return _dim("default", use_color)
|
|
1477
|
+
if key == "priority":
|
|
1478
|
+
return _style(str(value), "33", use_color)
|
|
1479
|
+
if key == "power":
|
|
1480
|
+
return _style(str(value), "96", use_color)
|
|
1481
|
+
if key == "permission":
|
|
1482
|
+
return _style(str(value), "32", use_color)
|
|
1483
|
+
return str(value)
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
def _format_launch_configs(sessions, use_color=False):
|
|
1487
|
+
from .cli_render import _dim, _pad_table, _style
|
|
1488
|
+
|
|
1489
|
+
if not sessions:
|
|
1490
|
+
return "\n".join([
|
|
1491
|
+
_style("Launch settings:", "1", use_color),
|
|
1492
|
+
"No sessions.",
|
|
1493
|
+
"",
|
|
1494
|
+
_dim(_format_launch_settings_hint(), use_color),
|
|
1495
|
+
])
|
|
1496
|
+
rows = [[_style(value, "1", use_color) for value in [
|
|
1497
|
+
"SESSION", "PROVIDER", "POWER", "PERMISSION", "FAST", "RTK", "MODEL", "PRIORITY"
|
|
1498
|
+
]]]
|
|
1499
|
+
for session in sessions:
|
|
1500
|
+
launch = session.get("launch") or {}
|
|
1501
|
+
rows.append([
|
|
1502
|
+
_style(session["name"], "36", use_color),
|
|
1503
|
+
_dim(session.get("provider") or "-", use_color),
|
|
1504
|
+
_format_launch_setting_value(launch, "power", use_color),
|
|
1505
|
+
_format_launch_setting_value(launch, "permission", use_color),
|
|
1506
|
+
_format_launch_setting_value(launch, "fast", use_color),
|
|
1507
|
+
_format_launch_setting_value(launch, "rtk", use_color),
|
|
1508
|
+
_format_launch_setting_value(launch, "model", use_color),
|
|
1509
|
+
_format_launch_setting_value(launch, "priority", use_color),
|
|
1510
|
+
])
|
|
1511
|
+
return "\n".join([
|
|
1512
|
+
_style("Launch settings:", "1", use_color),
|
|
1513
|
+
_pad_table(rows),
|
|
1514
|
+
"",
|
|
1515
|
+
_dim(_format_launch_settings_hint(), use_color),
|
|
1401
1516
|
])
|
|
1402
1517
|
|
|
1403
1518
|
|
|
@@ -1529,7 +1644,15 @@ def _format_duration_ms(value):
|
|
|
1529
1644
|
return f"{seconds:.1f}s"
|
|
1530
1645
|
minutes = int(seconds // 60)
|
|
1531
1646
|
remaining = int(seconds % 60)
|
|
1532
|
-
|
|
1647
|
+
if minutes < 60:
|
|
1648
|
+
return f"{minutes}m {remaining:02d}s"
|
|
1649
|
+
hours = minutes // 60
|
|
1650
|
+
remaining_minutes = minutes % 60
|
|
1651
|
+
if hours < 24:
|
|
1652
|
+
return f"{hours}h {remaining_minutes:02d}m"
|
|
1653
|
+
days = hours // 24
|
|
1654
|
+
remaining_hours = hours % 24
|
|
1655
|
+
return f"{days}d {remaining_hours:02d}h"
|
|
1533
1656
|
|
|
1534
1657
|
|
|
1535
1658
|
def _summarize_history(entries):
|
|
@@ -1656,49 +1779,57 @@ def _format_period_display(value):
|
|
|
1656
1779
|
return parsed.strftime("%Y-%m-%d %H:%M")
|
|
1657
1780
|
|
|
1658
1781
|
|
|
1659
|
-
def _format_history_summary(entries, period=None, use_color=False):
|
|
1782
|
+
def _format_history_summary(entries, period=None, use_color=False, active_sessions=None):
|
|
1660
1783
|
from .cli_render import _format_relative_age, _pad_table
|
|
1661
1784
|
|
|
1785
|
+
active_sessions = active_sessions or set()
|
|
1662
1786
|
summary = _summarize_history(entries)
|
|
1663
1787
|
if not summary:
|
|
1664
1788
|
return "No launch history for this period." if _has_history_period(period or {}) else "No launch history."
|
|
1665
|
-
rows = [["SESSION", "PROV.", "LAUNCHES", "OK", "FAIL", "TIME", "LAST"]]
|
|
1789
|
+
rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "LAUNCHES", "OK", "FAIL", "TIME", "LAST"]]]
|
|
1666
1790
|
for row in summary:
|
|
1791
|
+
session_name = row["session_name"]
|
|
1792
|
+
display_name = f"{session_name}*" if session_name in active_sessions else session_name
|
|
1667
1793
|
rows.append([
|
|
1668
|
-
|
|
1669
|
-
row["provider"],
|
|
1670
|
-
str(row["launches"]),
|
|
1671
|
-
str(row["successes"]),
|
|
1672
|
-
str(row["failures"]),
|
|
1673
|
-
_format_duration_ms(row["duration_ms"]),
|
|
1674
|
-
_format_relative_age(row.get("last_started_at")),
|
|
1794
|
+
_style(display_name, "36", use_color),
|
|
1795
|
+
_dim(row["provider"], use_color),
|
|
1796
|
+
_style(str(row["launches"]), "1", use_color),
|
|
1797
|
+
_style(str(row["successes"]), "32" if row["successes"] else "2", use_color),
|
|
1798
|
+
_style(str(row["failures"]), "31" if row["failures"] else "2", use_color),
|
|
1799
|
+
_style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
|
|
1800
|
+
_dim(_format_relative_age(row.get("last_started_at")), use_color),
|
|
1675
1801
|
])
|
|
1676
|
-
lines = ["Assistant time:"]
|
|
1802
|
+
lines = [_style("Assistant time:", "1", use_color)]
|
|
1677
1803
|
period_line = _format_history_period(period or {})
|
|
1678
1804
|
if period_line:
|
|
1679
|
-
lines.extend([period_line, ""])
|
|
1805
|
+
lines.extend([_dim(period_line, use_color), ""])
|
|
1680
1806
|
lines.append(_pad_table(rows))
|
|
1681
1807
|
return "\n".join(lines)
|
|
1682
1808
|
|
|
1683
1809
|
|
|
1684
|
-
def _format_history(entries, use_color=False):
|
|
1810
|
+
def _format_history(entries, use_color=False, active_sessions=None):
|
|
1685
1811
|
from .cli_render import _format_relative_age, _pad_table
|
|
1686
1812
|
|
|
1813
|
+
active_sessions = active_sessions or set()
|
|
1687
1814
|
if not entries:
|
|
1688
1815
|
return "No launch history."
|
|
1689
|
-
rows = [["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]
|
|
1816
|
+
rows = [[_style(value, "1", use_color) for value in ["SESSION", "PROV.", "RESULT", "DURATION", "WHEN", "TRANSCRIPT"]]]
|
|
1690
1817
|
for entry in entries:
|
|
1691
1818
|
transcript_path = entry.get("transcript_path")
|
|
1819
|
+
session_name = entry.get("session_name") or "-"
|
|
1820
|
+
display_name = f"{session_name}*" if session_name in active_sessions else session_name
|
|
1821
|
+
status = entry.get("status") or "-"
|
|
1822
|
+
status_color = "32" if status == "success" else "31" if status == "failed" else "2"
|
|
1692
1823
|
rows.append([
|
|
1693
|
-
|
|
1694
|
-
entry.get("provider") or "-",
|
|
1695
|
-
|
|
1696
|
-
_format_duration_ms(entry.get("duration_ms")),
|
|
1697
|
-
_format_relative_age(entry.get("started_at")),
|
|
1698
|
-
os.path.basename(transcript_path) if transcript_path else "-",
|
|
1824
|
+
_style(display_name, "36", use_color),
|
|
1825
|
+
_dim(entry.get("provider") or "-", use_color),
|
|
1826
|
+
_style(status, status_color, use_color),
|
|
1827
|
+
_style(_format_duration_ms(entry.get("duration_ms")), "33" if entry.get("duration_ms") else "2", use_color),
|
|
1828
|
+
_dim(_format_relative_age(entry.get("started_at")), use_color),
|
|
1829
|
+
_dim(os.path.basename(transcript_path) if transcript_path else "-", use_color),
|
|
1699
1830
|
])
|
|
1700
1831
|
return "\n".join([
|
|
1701
|
-
"Recent launches:",
|
|
1832
|
+
_style("Recent launches:", "1", use_color),
|
|
1702
1833
|
_pad_table(rows),
|
|
1703
1834
|
"",
|
|
1704
1835
|
_dim("Full transcript paths and cwd are available with --json.", use_color),
|
|
@@ -1717,29 +1848,34 @@ def _format_token_count(value):
|
|
|
1717
1848
|
return str(amount)
|
|
1718
1849
|
|
|
1719
1850
|
|
|
1720
|
-
def _format_stats(rows, totals, period=None, use_color=False):
|
|
1851
|
+
def _format_stats(rows, totals, period=None, use_color=False, active_sessions=None):
|
|
1721
1852
|
from .cli_render import _format_relative_age, _pad_table
|
|
1722
1853
|
|
|
1854
|
+
active_sessions = active_sessions or set()
|
|
1723
1855
|
if not rows:
|
|
1724
1856
|
return "No launch stats for this period." if _has_history_period(period or {}) else "No launch stats."
|
|
1725
|
-
table = [[
|
|
1857
|
+
table = [[_style(value, "1", use_color) for value in [
|
|
1858
|
+
"SESSION", "PROV.", "RUNS", "USAGE", "IN", "OUT", "REASON", "TOTAL", "TIME", "LAST"
|
|
1859
|
+
]]]
|
|
1726
1860
|
for row in rows:
|
|
1861
|
+
session_name = row["session_name"]
|
|
1862
|
+
display_name = f"{session_name}*" if session_name in active_sessions else session_name
|
|
1727
1863
|
table.append([
|
|
1728
|
-
|
|
1729
|
-
row["provider"],
|
|
1730
|
-
str(row["launches"]),
|
|
1731
|
-
str(row["usage_runs"]),
|
|
1732
|
-
_format_token_count(row["input_tokens"]),
|
|
1733
|
-
_format_token_count(row["output_tokens"]),
|
|
1734
|
-
_format_token_count(row["reasoning_tokens"]),
|
|
1735
|
-
_format_token_count(row["total_tokens"]),
|
|
1736
|
-
_format_duration_ms(row["duration_ms"]),
|
|
1737
|
-
_format_relative_age(row.get("last_started_at")),
|
|
1864
|
+
_style(display_name, "36", use_color),
|
|
1865
|
+
_dim(row["provider"], use_color),
|
|
1866
|
+
_style(str(row["launches"]), "1", use_color),
|
|
1867
|
+
_style(str(row["usage_runs"]), "32" if row["usage_runs"] else "2", use_color),
|
|
1868
|
+
_style(_format_token_count(row["input_tokens"]), "96" if row["input_tokens"] else "2", use_color),
|
|
1869
|
+
_style(_format_token_count(row["output_tokens"]), "96" if row["output_tokens"] else "2", use_color),
|
|
1870
|
+
_style(_format_token_count(row["reasoning_tokens"]), "95" if row["reasoning_tokens"] else "2", use_color),
|
|
1871
|
+
_style(_format_token_count(row["total_tokens"]), "1;96" if row["total_tokens"] else "2", use_color),
|
|
1872
|
+
_style(_format_duration_ms(row["duration_ms"]), "33" if row["duration_ms"] else "2", use_color),
|
|
1873
|
+
_dim(_format_relative_age(row.get("last_started_at")), use_color),
|
|
1738
1874
|
])
|
|
1739
|
-
lines = ["Assistant stats:"]
|
|
1875
|
+
lines = [_style("Assistant stats:", "1", use_color)]
|
|
1740
1876
|
period_line = _format_history_period(period or {})
|
|
1741
1877
|
if period_line:
|
|
1742
|
-
lines.extend([period_line, ""])
|
|
1878
|
+
lines.extend([_dim(period_line, use_color), ""])
|
|
1743
1879
|
lines.append(_pad_table(table))
|
|
1744
1880
|
lines.extend([
|
|
1745
1881
|
"",
|
|
@@ -1754,6 +1890,14 @@ def _format_stats(rows, totals, period=None, use_color=False):
|
|
|
1754
1890
|
return "\n".join(lines)
|
|
1755
1891
|
|
|
1756
1892
|
|
|
1893
|
+
def _active_session_names(ctx):
|
|
1894
|
+
return {
|
|
1895
|
+
row["name"]
|
|
1896
|
+
for row in ctx["service"]["format_list_rows"]()
|
|
1897
|
+
if row.get("active")
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
|
|
1757
1901
|
def _apply_launch_settings(parsed, ctx, action="set"):
|
|
1758
1902
|
targets = _resolve_bulk_launch_targets(parsed, ctx["service"])
|
|
1759
1903
|
sessions = [
|
|
@@ -1778,7 +1922,7 @@ def _apply_launch_settings(parsed, ctx, action="set"):
|
|
|
1778
1922
|
return 0
|
|
1779
1923
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1780
1924
|
if len(sessions) == 1:
|
|
1781
|
-
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1925
|
+
ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
|
|
1782
1926
|
return 0
|
|
1783
1927
|
|
|
1784
1928
|
|
|
@@ -1806,7 +1950,7 @@ def _clear_launch_settings(parsed, ctx, action="unset"):
|
|
|
1806
1950
|
return 0
|
|
1807
1951
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
1808
1952
|
if len(sessions) == 1:
|
|
1809
|
-
ctx["out"](f"{_format_launch_config(sessions[0])}\n")
|
|
1953
|
+
ctx["out"](f"{_format_launch_config(sessions[0], ctx['use_color'])}\n")
|
|
1810
1954
|
return 0
|
|
1811
1955
|
|
|
1812
1956
|
|
|
@@ -1834,7 +1978,18 @@ def handle_config(rest, ctx):
|
|
|
1834
1978
|
if parsed["json"]:
|
|
1835
1979
|
_write_json(ctx, _json_success("config", message, session=session, launch=session.get("launch") or {}))
|
|
1836
1980
|
return 0
|
|
1837
|
-
ctx["out"](f"{_format_launch_config(session)}\n")
|
|
1981
|
+
ctx["out"](f"{_format_launch_config(session, ctx['use_color'])}\n")
|
|
1982
|
+
return 0
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
def handle_configs(rest, ctx):
|
|
1986
|
+
parsed = _parse_configs_args(rest)
|
|
1987
|
+
sessions = ctx["service"]["list_sessions"]()
|
|
1988
|
+
message = f"Listed launch settings for {len(sessions)} session{'s' if len(sessions) != 1 else ''}"
|
|
1989
|
+
if parsed["json"]:
|
|
1990
|
+
_write_json(ctx, _json_success("configs", message, count=len(sessions), sessions=sessions))
|
|
1991
|
+
return 0
|
|
1992
|
+
ctx["out"](f"{_format_launch_configs(sessions, ctx['use_color'])}\n")
|
|
1838
1993
|
return 0
|
|
1839
1994
|
|
|
1840
1995
|
|
|
@@ -1859,10 +2014,11 @@ def handle_history(rest, ctx):
|
|
|
1859
2014
|
payload["summary"] = _summarize_history(entries)
|
|
1860
2015
|
_write_json(ctx, _json_success("history", message, **payload))
|
|
1861
2016
|
return 0
|
|
2017
|
+
active_sessions = _active_session_names(ctx)
|
|
1862
2018
|
if parsed["summary"]:
|
|
1863
|
-
ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'])}\n")
|
|
2019
|
+
ctx["out"](f"{_format_history_summary(entries, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
|
|
1864
2020
|
return 0
|
|
1865
|
-
ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'])}\n")
|
|
2021
|
+
ctx["out"](f"{_format_history(entries, use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
|
|
1866
2022
|
return 0
|
|
1867
2023
|
|
|
1868
2024
|
|
|
@@ -1888,7 +2044,8 @@ def handle_stats(rest, ctx):
|
|
|
1888
2044
|
totals=totals,
|
|
1889
2045
|
))
|
|
1890
2046
|
return 0
|
|
1891
|
-
|
|
2047
|
+
active_sessions = _active_session_names(ctx)
|
|
2048
|
+
ctx["out"](f"{_format_stats(rows, totals, period=parsed['period'], use_color=ctx['use_color'], active_sessions=active_sessions)}\n")
|
|
1892
2049
|
return 0
|
|
1893
2050
|
|
|
1894
2051
|
|
package/src/cli_render.py
CHANGED
|
@@ -146,6 +146,9 @@ def _format_sessions(service, use_color=False):
|
|
|
146
146
|
lines += [
|
|
147
147
|
_style("Next actions:", "1", use_color),
|
|
148
148
|
f" {_style('cdx status', '36', use_color)}",
|
|
149
|
+
f" {_style('cdx next', '36', use_color)}",
|
|
150
|
+
f" {_style('cdx configs', '36', use_color)}",
|
|
151
|
+
f" {_style('cdx stats', '36', use_color)}",
|
|
149
152
|
f" {_style('cdx ready', '36', use_color)}",
|
|
150
153
|
f" {_style('cdx perm all default', '36', use_color)}",
|
|
151
154
|
f" {_style('cdx handoff <source> <target>', '36', use_color)}",
|