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/provider_runtime.py
CHANGED
|
@@ -39,6 +39,7 @@ HEADLESS_CODEX_PERMISSION_ARGS = {
|
|
|
39
39
|
"auto": ["-s", "workspace-write", "-c", 'approval_policy="never"'],
|
|
40
40
|
"full": ["--dangerously-bypass-approvals-and-sandbox"],
|
|
41
41
|
}
|
|
42
|
+
REDACTED_PROMPT_ARG = "[prompt redacted]"
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def _home_env_overrides(auth_home):
|
|
@@ -108,6 +109,12 @@ def _has_local_claude_auth(auth_home):
|
|
|
108
109
|
return bool(isinstance(oauth, dict) and _clean_oauth_token(oauth.get("accessToken")))
|
|
109
110
|
|
|
110
111
|
|
|
112
|
+
def _read_claude_launch_oauth_token(auth_home):
|
|
113
|
+
if _has_local_claude_auth(auth_home):
|
|
114
|
+
return None
|
|
115
|
+
return _read_anthropic_oauth_token(auth_home)
|
|
116
|
+
|
|
117
|
+
|
|
111
118
|
def _has_local_codex_auth(auth_home):
|
|
112
119
|
try:
|
|
113
120
|
with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
|
|
@@ -312,7 +319,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
312
319
|
args.append(initial_prompt)
|
|
313
320
|
auth_home = _get_auth_home(session)
|
|
314
321
|
claude_env = _claude_env(env, auth_home)
|
|
315
|
-
oauth_token =
|
|
322
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
316
323
|
if oauth_token:
|
|
317
324
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
318
325
|
return _wrap_launch_with_transcript(session, {
|
|
@@ -393,7 +400,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
393
400
|
args.append(initial_prompt)
|
|
394
401
|
auth_home = _get_auth_home(session)
|
|
395
402
|
claude_env = _claude_env(env, auth_home)
|
|
396
|
-
oauth_token =
|
|
403
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
397
404
|
if oauth_token:
|
|
398
405
|
claude_env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
399
406
|
return {
|
|
@@ -401,6 +408,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
401
408
|
"args": args,
|
|
402
409
|
"options": {"cwd": cwd, "env": claude_env},
|
|
403
410
|
"label": "claude",
|
|
411
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
404
412
|
}
|
|
405
413
|
|
|
406
414
|
if session["provider"] == PROVIDER_CODEX:
|
|
@@ -418,6 +426,7 @@ def _build_headless_launch_spec(session, cwd=None, env_override=None, initial_pr
|
|
|
418
426
|
"args": args,
|
|
419
427
|
"options": {"cwd": cwd, "env": {**env, "CODEX_HOME": _get_auth_home(session)}},
|
|
420
428
|
"label": "codex",
|
|
429
|
+
"sensitive_args": [initial_prompt] if initial_prompt else [],
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
return _build_launch_spec(
|
|
@@ -468,7 +477,7 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
468
477
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
469
478
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
470
479
|
"command": spec.get("command"),
|
|
471
|
-
"args":
|
|
480
|
+
"args": _redact_sensitive_args(spec),
|
|
472
481
|
"label": spec.get("label"),
|
|
473
482
|
"pid": None,
|
|
474
483
|
"returncode": returncode,
|
|
@@ -476,6 +485,14 @@ def _headless_run_info(paths, spec, start_time, returncode):
|
|
|
476
485
|
}
|
|
477
486
|
|
|
478
487
|
|
|
488
|
+
def _redact_sensitive_args(spec):
|
|
489
|
+
args = list(spec.get("args") or [])
|
|
490
|
+
sensitive = {value for value in (spec.get("sensitive_args") or []) if value}
|
|
491
|
+
if not sensitive:
|
|
492
|
+
return args
|
|
493
|
+
return [REDACTED_PROMPT_ARG if arg in sensitive else arg for arg in args]
|
|
494
|
+
|
|
495
|
+
|
|
479
496
|
def _run_headless_provider_command(session, cwd=None, env_override=None, initial_prompt=None,
|
|
480
497
|
timeout_seconds=None, spawn=None, run_id=None):
|
|
481
498
|
spawn = spawn or subprocess.Popen
|
|
@@ -545,7 +562,7 @@ def _run_headless_provider_command(session, cwd=None, env_override=None, initial
|
|
|
545
562
|
"ended_at": end_time.isoformat().replace("+00:00", "Z"),
|
|
546
563
|
"duration_ms": int((end_time - start_time).total_seconds() * 1000),
|
|
547
564
|
"command": spec.get("command"),
|
|
548
|
-
"args":
|
|
565
|
+
"args": _redact_sensitive_args(spec),
|
|
549
566
|
"label": spec.get("label"),
|
|
550
567
|
"pid": getattr(child, "pid", None),
|
|
551
568
|
"returncode": returncode,
|
|
@@ -558,7 +575,7 @@ def _build_login_status_spec(session, env_override=None):
|
|
|
558
575
|
if session["provider"] == PROVIDER_CLAUDE:
|
|
559
576
|
auth_home = _get_auth_home(session)
|
|
560
577
|
env = _claude_env(env, auth_home)
|
|
561
|
-
oauth_token =
|
|
578
|
+
oauth_token = _read_claude_launch_oauth_token(auth_home)
|
|
562
579
|
if oauth_token:
|
|
563
580
|
env["CLAUDE_CODE_OAUTH_TOKEN"] = oauth_token
|
|
564
581
|
|
|
@@ -639,13 +656,14 @@ def _resolve_command(command, env=None):
|
|
|
639
656
|
return shutil.which(command, path=env.get("PATH")) or command
|
|
640
657
|
|
|
641
658
|
|
|
642
|
-
def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
659
|
+
def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_local_credentials=True):
|
|
643
660
|
spawn_sync = spawn_sync or subprocess.run
|
|
644
661
|
spec = _build_login_status_spec(session, env_override)
|
|
645
|
-
if
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
662
|
+
if trust_local_credentials:
|
|
663
|
+
if session.get("provider") == PROVIDER_CLAUDE and _has_local_claude_auth(_get_auth_home(session)):
|
|
664
|
+
return True
|
|
665
|
+
if session.get("provider") == PROVIDER_CODEX and _has_local_codex_auth(_get_auth_home(session)):
|
|
666
|
+
return True
|
|
649
667
|
try:
|
|
650
668
|
if spawn_sync is subprocess.run:
|
|
651
669
|
command = _resolve_command(spec["command"], spec["env"])
|
|
@@ -830,8 +848,17 @@ def _should_retry_without_transcript(spec):
|
|
|
830
848
|
|
|
831
849
|
def _ensure_session_authentication(session, service, spawn=None, spawn_sync=None,
|
|
832
850
|
stdin_is_tty=True, env_override=None, behavior="launch",
|
|
833
|
-
signal_emitter=None):
|
|
834
|
-
|
|
851
|
+
signal_emitter=None, trust_local_credentials=True):
|
|
852
|
+
if behavior == "launch" and (session.get("auth") or {}).get("status") == "logged_out":
|
|
853
|
+
raise CdxError(
|
|
854
|
+
f"Session {session['name']} is not authenticated. Run: cdx login {session['name']}"
|
|
855
|
+
)
|
|
856
|
+
is_authenticated = _probe_provider_auth(
|
|
857
|
+
session,
|
|
858
|
+
spawn_sync=spawn_sync,
|
|
859
|
+
env_override=env_override,
|
|
860
|
+
trust_local_credentials=trust_local_credentials,
|
|
861
|
+
)
|
|
835
862
|
if is_authenticated:
|
|
836
863
|
return {"authenticated": True, "checked": True}
|
|
837
864
|
if behavior == "probe-only":
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from .errors import CdxError
|
|
4
|
+
from .run_usage import empty_usage, extract_run_usage
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_run_prompt(parsed):
|
|
8
|
+
if parsed.get("prompt") is not None:
|
|
9
|
+
return parsed["prompt"]
|
|
10
|
+
try:
|
|
11
|
+
with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
|
|
12
|
+
return handle.read()
|
|
13
|
+
except OSError as error:
|
|
14
|
+
raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_cdx_error_code(error):
|
|
18
|
+
message = str(error)
|
|
19
|
+
if message.startswith("Usage:"):
|
|
20
|
+
return "invalid_request"
|
|
21
|
+
if message.startswith("Invalid cwd:"):
|
|
22
|
+
return "invalid_cwd"
|
|
23
|
+
if message.startswith("Session is disabled:"):
|
|
24
|
+
return "session_disabled"
|
|
25
|
+
if "CLI not found on PATH" in message:
|
|
26
|
+
return "provider_cli_not_found"
|
|
27
|
+
if message.startswith("Failed to start "):
|
|
28
|
+
return "provider_start_failed"
|
|
29
|
+
if (
|
|
30
|
+
message.startswith("Unsupported reasoning effort:")
|
|
31
|
+
or message.startswith("Unsupported power:")
|
|
32
|
+
or "--reasoning-effort and --power must match" in message
|
|
33
|
+
):
|
|
34
|
+
return "invalid_reasoning_effort"
|
|
35
|
+
return "cdx_error"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_payload_reasoning_effort(parsed, session):
|
|
39
|
+
launch = (session.get("launch") or {}) if session else {}
|
|
40
|
+
return (
|
|
41
|
+
parsed.get("reasoning_effort")
|
|
42
|
+
or parsed.get("power")
|
|
43
|
+
or launch.get("reasoning_effort")
|
|
44
|
+
or launch.get("reasoningEffort")
|
|
45
|
+
or launch.get("power")
|
|
46
|
+
or ("low" if launch.get("fast") is True else None)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_result_payload(api_schema_version, ok, parsed, session, run_info=None,
|
|
51
|
+
error=None, error_source=None, error_code=None):
|
|
52
|
+
run_info = run_info or {}
|
|
53
|
+
usage = run_info.get("usage") if isinstance(run_info.get("usage"), dict) else (
|
|
54
|
+
extract_run_usage(session.get("provider"), run_info.get("stdout_path"))
|
|
55
|
+
if session else
|
|
56
|
+
empty_usage()
|
|
57
|
+
)
|
|
58
|
+
return {
|
|
59
|
+
"schema_version": api_schema_version,
|
|
60
|
+
"ok": bool(ok),
|
|
61
|
+
"action": "run",
|
|
62
|
+
"launcher": "cdx",
|
|
63
|
+
"session": session.get("name") if session else None,
|
|
64
|
+
"provider": session.get("provider") if session else parsed.get("provider"),
|
|
65
|
+
"model": parsed.get("model") or ((session.get("launch") or {}).get("model") if session else None),
|
|
66
|
+
"reasoning_effort": run_payload_reasoning_effort(parsed, session),
|
|
67
|
+
"power": parsed.get("power") or ((session.get("launch") or {}).get("power") if session else None),
|
|
68
|
+
"cwd": os.path.abspath(parsed.get("cwd") or os.getcwd()),
|
|
69
|
+
"run_id": run_info.get("run_id"),
|
|
70
|
+
"exit_code": run_info.get("returncode"),
|
|
71
|
+
"duration_seconds": (run_info.get("duration_ms") / 1000.0) if run_info.get("duration_ms") is not None else None,
|
|
72
|
+
"transcript_path": run_info.get("transcript_path"),
|
|
73
|
+
"stdout_path": run_info.get("stdout_path"),
|
|
74
|
+
"stderr_path": run_info.get("stderr_path"),
|
|
75
|
+
"usage": usage,
|
|
76
|
+
"warnings": [],
|
|
77
|
+
"error": None if ok else {
|
|
78
|
+
"source": error_source or "cdx",
|
|
79
|
+
"code": error_code or "cdx_error",
|
|
80
|
+
"message": str(error) if error else "Run failed.",
|
|
81
|
+
"provider_code": run_info.get("returncode") if error_source == "provider" else None,
|
|
82
|
+
},
|
|
83
|
+
}
|
package/src/session_service.py
CHANGED
|
@@ -5,6 +5,7 @@ import base64
|
|
|
5
5
|
import sys
|
|
6
6
|
import tempfile
|
|
7
7
|
import uuid
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from urllib.parse import quote
|
|
10
11
|
|
|
@@ -22,19 +23,26 @@ RESERVED_SESSION_NAMES = {
|
|
|
22
23
|
"add",
|
|
23
24
|
"clean",
|
|
24
25
|
"context",
|
|
26
|
+
"configs",
|
|
25
27
|
"cp",
|
|
26
28
|
"disable",
|
|
27
29
|
"doctor",
|
|
28
30
|
"enable",
|
|
29
31
|
"export",
|
|
32
|
+
"fast",
|
|
30
33
|
"help",
|
|
31
34
|
"handoff",
|
|
32
35
|
"history",
|
|
33
36
|
"import",
|
|
37
|
+
"last",
|
|
34
38
|
"login",
|
|
35
39
|
"logout",
|
|
40
|
+
"model",
|
|
36
41
|
"mv",
|
|
42
|
+
"next",
|
|
37
43
|
"notify",
|
|
44
|
+
"perm",
|
|
45
|
+
"power",
|
|
38
46
|
"ready",
|
|
39
47
|
"repair",
|
|
40
48
|
"ren",
|
|
@@ -44,6 +52,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
44
52
|
"select",
|
|
45
53
|
"config",
|
|
46
54
|
"set",
|
|
55
|
+
"stats",
|
|
47
56
|
"status",
|
|
48
57
|
"unset",
|
|
49
58
|
"update",
|
|
@@ -55,12 +64,17 @@ RESERVED_SESSION_NAMES = {
|
|
|
55
64
|
}
|
|
56
65
|
STATUS_CACHE_TTL_SECONDS = 60
|
|
57
66
|
CLAUDE_STATUS_CACHE_TTL_SECONDS = 10 * 60
|
|
67
|
+
MAX_STATUS_WORKERS = 8
|
|
58
68
|
LAUNCH_POWER_VALUES = {"low", "medium", "high", "xhigh", "max"}
|
|
59
69
|
LAUNCH_REASONING_EFFORT_VALUES = {"low", "medium", "high"}
|
|
60
70
|
LAUNCH_PERMISSION_VALUES = {"review", "default", "auto", "full"}
|
|
61
71
|
MAX_LAUNCH_MODEL_LENGTH = 128
|
|
62
72
|
MIN_LAUNCH_PRIORITY = 0
|
|
63
73
|
MAX_LAUNCH_PRIORITY = 100
|
|
74
|
+
DEFAULT_LAUNCH_SETTINGS = {
|
|
75
|
+
"power": "medium",
|
|
76
|
+
"fast": False,
|
|
77
|
+
}
|
|
64
78
|
|
|
65
79
|
|
|
66
80
|
def _encode(name):
|
|
@@ -448,6 +462,8 @@ def create_session_service(options=None):
|
|
|
448
462
|
raise CdxError("Session name is required")
|
|
449
463
|
if str(name) != str(name).strip():
|
|
450
464
|
raise CdxError("Session name cannot start or end with whitespace")
|
|
465
|
+
if str(name) in (".", ".."):
|
|
466
|
+
raise CdxError("Session name cannot be . or ..")
|
|
451
467
|
if len(str(name)) > MAX_SESSION_NAME_LENGTH:
|
|
452
468
|
raise CdxError(f"Session name is too long (max {MAX_SESSION_NAME_LENGTH} characters)")
|
|
453
469
|
if any(ord(ch) < 32 or ord(ch) == 127 for ch in str(name)):
|
|
@@ -542,6 +558,7 @@ def create_session_service(options=None):
|
|
|
542
558
|
"lastLaunchedAt": None,
|
|
543
559
|
"lastStatusAt": None,
|
|
544
560
|
"lastStatus": None,
|
|
561
|
+
"launch": dict(DEFAULT_LAUNCH_SETTINGS),
|
|
545
562
|
"auth": {
|
|
546
563
|
"status": "unknown",
|
|
547
564
|
"lastCheckedAt": None,
|
|
@@ -939,14 +956,9 @@ def create_session_service(options=None):
|
|
|
939
956
|
|
|
940
957
|
def get_status_rows(progress_callback=None, force_refresh=False, cache_ttl_seconds=STATUS_CACHE_TTL_SECONDS):
|
|
941
958
|
sessions = list_sessions()
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
"session_count": len(sessions),
|
|
946
|
-
})
|
|
947
|
-
resolved = []
|
|
948
|
-
for s in sessions:
|
|
949
|
-
cache_hit = (
|
|
959
|
+
|
|
960
|
+
def _status_cache_hit(s):
|
|
961
|
+
return (
|
|
950
962
|
s.get("enabled", True) is False
|
|
951
963
|
or (
|
|
952
964
|
s.get("lastStatus")
|
|
@@ -954,28 +966,56 @@ def create_session_service(options=None):
|
|
|
954
966
|
and _is_status_cache_fresh(s, ttl_seconds=cache_ttl_seconds)
|
|
955
967
|
)
|
|
956
968
|
)
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
969
|
+
|
|
970
|
+
cache_hits = {
|
|
971
|
+
s["name"]: _status_cache_hit(s)
|
|
972
|
+
for s in sessions
|
|
973
|
+
}
|
|
974
|
+
if progress_callback:
|
|
975
|
+
progress_callback({
|
|
976
|
+
"event": "status_started",
|
|
977
|
+
"session_count": len(sessions),
|
|
978
|
+
"check_count": sum(1 for cache_hit in cache_hits.values() if not cache_hit),
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
def _resolve_row_session(s):
|
|
963
982
|
status = _resolve_session_status(
|
|
964
983
|
s,
|
|
965
984
|
force_refresh=force_refresh,
|
|
966
985
|
cache_ttl_seconds=cache_ttl_seconds,
|
|
967
986
|
)
|
|
968
|
-
|
|
969
|
-
progress_callback({
|
|
970
|
-
"event": "session_finished",
|
|
971
|
-
"session_name": s["name"],
|
|
972
|
-
"has_status": bool(status),
|
|
973
|
-
})
|
|
974
|
-
resolved.append({
|
|
987
|
+
return {
|
|
975
988
|
**s,
|
|
976
989
|
"lastStatus": status,
|
|
977
990
|
"lastStatusAt": (status and status.get("updated_at")) or s.get("lastStatusAt"),
|
|
978
|
-
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
resolved_by_name = {}
|
|
994
|
+
if sessions:
|
|
995
|
+
max_workers = min(MAX_STATUS_WORKERS, len(sessions))
|
|
996
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
997
|
+
futures = {}
|
|
998
|
+
for s in sessions:
|
|
999
|
+
cache_hit = cache_hits[s["name"]]
|
|
1000
|
+
if progress_callback and not cache_hit:
|
|
1001
|
+
progress_callback({
|
|
1002
|
+
"event": "session_started",
|
|
1003
|
+
"session_name": s["name"],
|
|
1004
|
+
"provider": s["provider"],
|
|
1005
|
+
})
|
|
1006
|
+
futures[executor.submit(_resolve_row_session, s)] = s
|
|
1007
|
+
for future in as_completed(futures):
|
|
1008
|
+
s = futures[future]
|
|
1009
|
+
resolved = future.result()
|
|
1010
|
+
resolved_by_name[s["name"]] = resolved
|
|
1011
|
+
if progress_callback:
|
|
1012
|
+
progress_callback({
|
|
1013
|
+
"event": "session_finished",
|
|
1014
|
+
"session_name": s["name"],
|
|
1015
|
+
"has_status": bool(resolved.get("lastStatus")),
|
|
1016
|
+
"cache_hit": cache_hits[s["name"]],
|
|
1017
|
+
})
|
|
1018
|
+
resolved = [resolved_by_name[s["name"]] for s in sessions]
|
|
979
1019
|
|
|
980
1020
|
def sort_key(s):
|
|
981
1021
|
at = s.get("lastStatusAt") or ""
|
|
@@ -1092,7 +1132,7 @@ def create_session_service(options=None):
|
|
|
1092
1132
|
bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
|
|
1093
1133
|
if progress_callback:
|
|
1094
1134
|
progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
|
|
1095
|
-
|
|
1135
|
+
os.makedirs(os.path.dirname(os.path.abspath(file_path)) or ".", exist_ok=True)
|
|
1096
1136
|
with open(file_path, "wb") as handle:
|
|
1097
1137
|
handle.write(bundle_bytes)
|
|
1098
1138
|
if sys.platform != "win32":
|
package/src/status_view.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
2
3
|
|
|
3
4
|
from .cli_render import (
|
|
4
5
|
_dim,
|
|
@@ -61,6 +62,17 @@ def _style_reset_time(value, use_color=False):
|
|
|
61
62
|
return text
|
|
62
63
|
|
|
63
64
|
|
|
65
|
+
def _format_credits(value, empty="n/a"):
|
|
66
|
+
if value is None:
|
|
67
|
+
return empty
|
|
68
|
+
try:
|
|
69
|
+
normalized = str(value).strip().replace(",", "")
|
|
70
|
+
rounded = Decimal(normalized).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
|
71
|
+
return f"{rounded:.2f}"
|
|
72
|
+
except (InvalidOperation, ValueError):
|
|
73
|
+
return str(value)
|
|
74
|
+
|
|
75
|
+
|
|
64
76
|
def _format_status_rows(rows, use_color=False, small=False):
|
|
65
77
|
has_provider = len({r["provider"] for r in rows}) > 1 and not small
|
|
66
78
|
if small:
|
|
@@ -75,15 +87,11 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
75
87
|
return "SESSION STATUS OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
|
|
76
88
|
headers = [_style(header, "1", use_color) for header in headers]
|
|
77
89
|
active_rows = [r for r in rows if r.get("enabled", True) is not False]
|
|
78
|
-
|
|
79
|
-
r for r in active_rows
|
|
80
|
-
if _format_auth_status(r) != "logged out"
|
|
81
|
-
]
|
|
90
|
+
priority = recommend_priority_rows(rows)
|
|
82
91
|
disabled_rows = sorted(
|
|
83
92
|
[r for r in rows if r.get("enabled", True) is False],
|
|
84
93
|
key=lambda r: r.get("session_name") or "",
|
|
85
94
|
)
|
|
86
|
-
priority = _recommend_priority_sessions(priority_candidates)
|
|
87
95
|
priority_names = {r.get("session_name") for r in priority}
|
|
88
96
|
non_priority_active = [
|
|
89
97
|
r for r in active_rows
|
|
@@ -114,7 +122,7 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
114
122
|
base += usage_columns
|
|
115
123
|
else:
|
|
116
124
|
block = "-" if r.get("enabled", True) is False else _format_blocking_quota(r)
|
|
117
|
-
credits =
|
|
125
|
+
credits = _format_credits(r.get("credits"), empty="-")
|
|
118
126
|
base += usage_columns[:3] + [
|
|
119
127
|
_style(block, "33" if block not in ("?", "-") else "2", use_color),
|
|
120
128
|
_style(credits, "33" if r.get("credits") is not None else "2", use_color),
|
|
@@ -168,6 +176,19 @@ def _format_auth_status(row):
|
|
|
168
176
|
return "unknown"
|
|
169
177
|
|
|
170
178
|
|
|
179
|
+
def recommend_priority_rows(rows):
|
|
180
|
+
active_rows = [r for r in rows if r.get("enabled", True) is not False]
|
|
181
|
+
priority_candidates = [
|
|
182
|
+
r for r in active_rows
|
|
183
|
+
if _format_auth_status(r) != "logged out"
|
|
184
|
+
]
|
|
185
|
+
return _recommend_priority_sessions(priority_candidates)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def format_priority_instruction(row, position="first"):
|
|
189
|
+
return _priority_instruction(row, position)
|
|
190
|
+
|
|
191
|
+
|
|
171
192
|
def _recommend_priority_sessions(rows):
|
|
172
193
|
if not rows:
|
|
173
194
|
return []
|
|
@@ -332,7 +353,7 @@ def _format_status_detail(row, use_color=False):
|
|
|
332
353
|
f"{_style('5h left:', '1', use_color)} {_style_pct(row.get('remaining_5h_pct'), use_color)}",
|
|
333
354
|
f"{_style('Week left:', '1', use_color)} {_style_pct(row.get('remaining_week_pct'), use_color)}",
|
|
334
355
|
f"{_style('Block:', '1', use_color)} {_style(_format_blocking_quota(row), '33', use_color)}",
|
|
335
|
-
f"{_style('Credits:', '1', use_color)} {_style(row
|
|
356
|
+
f"{_style('Credits:', '1', use_color)} {_style(_format_credits(row.get('credits')), '33' if row.get('credits') is not None else '2', use_color)}",
|
|
336
357
|
f"{_style('5h reset:', '1', use_color)} {_style_reset_time(row.get('reset_5h_at'), use_color)}",
|
|
337
358
|
f"{_style('Week reset:', '1', use_color)} {_style_reset_time(row.get('reset_week_at'), use_color)}",
|
|
338
359
|
f"{_style('Updated:', '1', use_color)} {_dim(_format_relative_age(row.get('updated_at')), use_color)}",
|