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/README.md +80 -10
- package/bin/cdx +2 -11
- package/changelogs/CHANGELOGS_0_10_0.md +43 -0
- package/changelogs/CHANGELOGS_0_11_0.md +51 -0
- package/checksums/release-archives.json +8 -0
- package/install.ps1 +12 -19
- package/install.sh +6 -29
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +17 -33
- package/src/cli.py +163 -28
- package/src/cli_args.py +15 -12
- package/src/cli_commands.py +531 -9
- package/src/cli_helpers.py +33 -38
- package/src/cli_view.py +5 -27
- package/src/codex_usage.py +95 -24
- package/src/notify.py +6 -10
- package/src/provider_runtime.py +58 -14
- package/src/session_service.py +141 -57
- package/src/session_store.py +2 -2
- package/src/status_source.py +47 -25
- package/src/status_view.py +14 -6
package/src/session_service.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import binascii
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
@@ -29,6 +30,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
29
30
|
"configs",
|
|
30
31
|
"cp",
|
|
31
32
|
"disable",
|
|
33
|
+
"disk",
|
|
32
34
|
"doctor",
|
|
33
35
|
"enable",
|
|
34
36
|
"export",
|
|
@@ -48,6 +50,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
48
50
|
"power",
|
|
49
51
|
"ready",
|
|
50
52
|
"repair",
|
|
53
|
+
"reset",
|
|
51
54
|
"resume",
|
|
52
55
|
"ren",
|
|
53
56
|
"rename",
|
|
@@ -260,6 +263,8 @@ def _normalize_status_payload(payload=None):
|
|
|
260
263
|
"remaining_5h_pct": _normalize_pct_value(payload.get("remaining_5h_pct")),
|
|
261
264
|
"remaining_week_pct": _normalize_pct_value(payload.get("remaining_week_pct")),
|
|
262
265
|
"credits": payload.get("credits"),
|
|
266
|
+
"reset_credits_available": payload.get("reset_credits_available"),
|
|
267
|
+
"reset_credits": payload.get("reset_credits"),
|
|
263
268
|
"reset_5h_at": payload.get("reset_5h_at"),
|
|
264
269
|
"reset_week_at": payload.get("reset_week_at"),
|
|
265
270
|
"reset_at": payload.get("reset_at") or payload.get("reset_week_at") or payload.get("reset_5h_at"),
|
|
@@ -337,6 +342,8 @@ def _status_has_more_detail(candidate, current):
|
|
|
337
342
|
"remaining_5h_pct",
|
|
338
343
|
"remaining_week_pct",
|
|
339
344
|
"credits",
|
|
345
|
+
"reset_credits_available",
|
|
346
|
+
"reset_credits",
|
|
340
347
|
"reset_5h_at",
|
|
341
348
|
"reset_week_at",
|
|
342
349
|
"reset_at",
|
|
@@ -358,6 +365,8 @@ def _merge_status_payload(current, candidate):
|
|
|
358
365
|
"remaining_5h_pct",
|
|
359
366
|
"remaining_week_pct",
|
|
360
367
|
"credits",
|
|
368
|
+
"reset_credits_available",
|
|
369
|
+
"reset_credits",
|
|
361
370
|
"reset_5h_at",
|
|
362
371
|
"reset_week_at",
|
|
363
372
|
"reset_at",
|
|
@@ -893,6 +902,11 @@ def create_session_service(options=None):
|
|
|
893
902
|
# lock; a pre-lock snapshot loses a concurrent setting change.
|
|
894
903
|
current = _normalize_launch_settings(s.get("launch") or {}, mark_fast_service_tier=False)
|
|
895
904
|
launch = {**current, **updates}
|
|
905
|
+
if "power" in updates:
|
|
906
|
+
launch.pop("reasoning_effort", None)
|
|
907
|
+
launch.pop("reasoningEffort", None)
|
|
908
|
+
if "reasoning_effort" in updates:
|
|
909
|
+
launch.pop("power", None)
|
|
896
910
|
explicit_power = "power" in updates or "reasoning_effort" in updates
|
|
897
911
|
if explicit_power and "fast" not in updates and launch.get("fastMode") != "service_tier":
|
|
898
912
|
launch["fast"] = False
|
|
@@ -915,7 +929,7 @@ def create_session_service(options=None):
|
|
|
915
929
|
raise CdxError(f"Unknown session: {name}")
|
|
916
930
|
if not keys:
|
|
917
931
|
raise CdxError("At least one launch setting is required.")
|
|
918
|
-
allowed = {"power", "permission", "fast", "rtk", "logics", "model", "priority"}
|
|
932
|
+
allowed = {"power", "reasoning_effort", "reasoningEffort", "permission", "fast", "rtk", "logics", "model", "priority"}
|
|
919
933
|
unknown = [key for key in keys if key not in allowed]
|
|
920
934
|
if unknown:
|
|
921
935
|
raise CdxError(f"Unsupported launch setting: {', '.join(unknown)}")
|
|
@@ -1101,6 +1115,8 @@ def create_session_service(options=None):
|
|
|
1101
1115
|
"remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
|
|
1102
1116
|
"remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
|
|
1103
1117
|
"credits": row_status.get("credits") if row_status else None,
|
|
1118
|
+
"reset_credits_available": row_status.get("reset_credits_available") if row_status else None,
|
|
1119
|
+
"reset_credits": row_status.get("reset_credits") if row_status else None,
|
|
1104
1120
|
"available_pct": _compute_available_pct(row_status),
|
|
1105
1121
|
"reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
|
|
1106
1122
|
"reset_week_at": row_status.get("reset_week_at") if row_status else None,
|
|
@@ -1206,7 +1222,12 @@ def create_session_service(options=None):
|
|
|
1206
1222
|
)] = s
|
|
1207
1223
|
for future in as_completed(futures):
|
|
1208
1224
|
s = futures[future]
|
|
1209
|
-
|
|
1225
|
+
try:
|
|
1226
|
+
resolved = future.result()
|
|
1227
|
+
except CdxError as error:
|
|
1228
|
+
if not str(error).startswith("Unknown session:"):
|
|
1229
|
+
raise
|
|
1230
|
+
continue
|
|
1210
1231
|
resolved_by_name[s["name"]] = resolved
|
|
1211
1232
|
if progress_callback:
|
|
1212
1233
|
progress_callback({
|
|
@@ -1215,7 +1236,7 @@ def create_session_service(options=None):
|
|
|
1215
1236
|
"has_status": bool(resolved.get("lastStatus")),
|
|
1216
1237
|
"cache_hit": cache_hits[s["name"]],
|
|
1217
1238
|
})
|
|
1218
|
-
resolved = [resolved_by_name[s["name"]] for s in sessions]
|
|
1239
|
+
resolved = [resolved_by_name[s["name"]] for s in sessions if s["name"] in resolved_by_name]
|
|
1219
1240
|
|
|
1220
1241
|
def sort_key(s):
|
|
1221
1242
|
at = s.get("lastStatusAt") or ""
|
|
@@ -1326,7 +1347,52 @@ def create_session_service(options=None):
|
|
|
1326
1347
|
"bundle_size_bytes": len(bundle_bytes),
|
|
1327
1348
|
}
|
|
1328
1349
|
|
|
1329
|
-
def
|
|
1350
|
+
def _validate_import_profile_files(payload, imported_sessions):
|
|
1351
|
+
profiles = payload.get("profiles") or {}
|
|
1352
|
+
if not isinstance(profiles, dict):
|
|
1353
|
+
raise CdxError("Bundle contains invalid profile data.")
|
|
1354
|
+
decoded_profiles = {}
|
|
1355
|
+
for session_payload in imported_sessions:
|
|
1356
|
+
name = session_payload["name"]
|
|
1357
|
+
files = []
|
|
1358
|
+
profile_items = profiles.get(name, [])
|
|
1359
|
+
if not isinstance(profile_items, list):
|
|
1360
|
+
raise CdxError(f"Bundle contains invalid profile data for session {name}.")
|
|
1361
|
+
for item in profile_items:
|
|
1362
|
+
if not isinstance(item, dict):
|
|
1363
|
+
raise CdxError(f"Bundle contains invalid profile entry for session {name}.")
|
|
1364
|
+
rel_path = _safe_relpath(item.get("path"))
|
|
1365
|
+
data_b64 = item.get("data_b64")
|
|
1366
|
+
if not isinstance(data_b64, str):
|
|
1367
|
+
raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}")
|
|
1368
|
+
try:
|
|
1369
|
+
content = base64.b64decode(data_b64.encode("ascii"), validate=True)
|
|
1370
|
+
except (binascii.Error, UnicodeEncodeError) as error:
|
|
1371
|
+
raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
|
|
1372
|
+
files.append({"path": rel_path, "content": content})
|
|
1373
|
+
decoded_profiles[name] = files
|
|
1374
|
+
return decoded_profiles
|
|
1375
|
+
|
|
1376
|
+
def _restore_import_backup(name, backup_root, session_root, old_record, old_state):
|
|
1377
|
+
if os.path.exists(session_root):
|
|
1378
|
+
remove_tree(session_root, ignore_errors=True)
|
|
1379
|
+
if backup_root and os.path.exists(backup_root):
|
|
1380
|
+
os.rename(backup_root, session_root)
|
|
1381
|
+
if old_record:
|
|
1382
|
+
store["replace_session"](name, old_record)
|
|
1383
|
+
if old_state is not None:
|
|
1384
|
+
store["write_session_state"](name, old_state)
|
|
1385
|
+
else:
|
|
1386
|
+
store["remove_session"](name)
|
|
1387
|
+
|
|
1388
|
+
def import_bundle(
|
|
1389
|
+
file_path,
|
|
1390
|
+
passphrase=None,
|
|
1391
|
+
session_names=None,
|
|
1392
|
+
force=False,
|
|
1393
|
+
merge=False,
|
|
1394
|
+
allow_authless_force=False,
|
|
1395
|
+
):
|
|
1330
1396
|
if not file_path or not os.path.isfile(file_path):
|
|
1331
1397
|
raise CdxError(f"Bundle file not found: {file_path}")
|
|
1332
1398
|
with open(file_path, "rb") as handle:
|
|
@@ -1348,73 +1414,91 @@ def create_session_service(options=None):
|
|
|
1348
1414
|
conflicts = [name for name in names if name in existing]
|
|
1349
1415
|
if conflicts and not force and not merge:
|
|
1350
1416
|
raise CdxError(f"Import would overwrite existing sessions: {', '.join(conflicts)}")
|
|
1417
|
+
if conflicts and force and not decoded["meta"].get("include_auth") and not allow_authless_force:
|
|
1418
|
+
raise CdxError(
|
|
1419
|
+
"Import --force would overwrite existing session credentials, but this bundle has no auth payloads. "
|
|
1420
|
+
"Re-export with --include-auth, use --merge, or pass --allow-authless-force to discard local credentials."
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
for session_payload in imported_sessions:
|
|
1424
|
+
_validate_new_session_name(session_payload["name"])
|
|
1425
|
+
_normalize_provider(session_payload["provider"])
|
|
1426
|
+
decoded_profiles = _validate_import_profile_files(payload, imported_sessions)
|
|
1351
1427
|
|
|
1352
1428
|
for session_payload in imported_sessions:
|
|
1353
1429
|
name = session_payload["name"]
|
|
1354
|
-
_validate_new_session_name(name)
|
|
1355
1430
|
provider = _normalize_provider(session_payload["provider"])
|
|
1356
1431
|
is_existing = name in existing
|
|
1357
1432
|
|
|
1358
|
-
if is_existing and force:
|
|
1359
|
-
remove_session(name)
|
|
1360
|
-
is_existing = False
|
|
1361
|
-
|
|
1362
1433
|
session_root = _get_session_root(name)
|
|
1363
1434
|
auth_home = _get_session_auth_home(name, provider)
|
|
1364
1435
|
_ensure_private_dir(base_dir)
|
|
1365
1436
|
_ensure_private_dir(os.path.join(base_dir, "profiles"))
|
|
1437
|
+
backup_root = None
|
|
1438
|
+
old_record = None
|
|
1439
|
+
old_state = None
|
|
1440
|
+
if is_existing and force:
|
|
1441
|
+
old_record = store["get_session"](name)
|
|
1442
|
+
old_state = store["read_session_state"](name)
|
|
1443
|
+
if os.path.exists(session_root):
|
|
1444
|
+
backup_root = tempfile.mkdtemp(prefix=f".{_encode(name)}.import.", dir=os.path.dirname(session_root))
|
|
1445
|
+
os.rmdir(backup_root)
|
|
1446
|
+
os.rename(session_root, backup_root)
|
|
1447
|
+
is_existing = False
|
|
1366
1448
|
_ensure_private_dir(session_root)
|
|
1367
1449
|
_ensure_private_dir(auth_home)
|
|
1368
1450
|
|
|
1369
1451
|
existing_state_before = None
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1452
|
+
try:
|
|
1453
|
+
if is_existing and merge:
|
|
1454
|
+
existing_record = store["get_session"](name) or {}
|
|
1455
|
+
# replace_session resets the state file to defaults, so the
|
|
1456
|
+
# local state must be captured before it runs.
|
|
1457
|
+
existing_state_before = store["read_session_state"](name) or {}
|
|
1458
|
+
bundle_record = {
|
|
1459
|
+
**session_payload,
|
|
1460
|
+
"provider": provider,
|
|
1461
|
+
"enabled": session_payload.get("enabled", True) is not False,
|
|
1462
|
+
"sessionRoot": session_root,
|
|
1463
|
+
"authHome": auth_home,
|
|
1464
|
+
}
|
|
1465
|
+
# Existing values take precedence; bundle fills in missing keys only.
|
|
1466
|
+
merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
|
|
1467
|
+
merged_record["sessionRoot"] = session_root
|
|
1468
|
+
merged_record["authHome"] = auth_home
|
|
1469
|
+
store["replace_session"](name, merged_record)
|
|
1470
|
+
else:
|
|
1471
|
+
session_record = {
|
|
1472
|
+
**session_payload,
|
|
1473
|
+
"provider": provider,
|
|
1474
|
+
"enabled": session_payload.get("enabled", True) is not False,
|
|
1475
|
+
"sessionRoot": session_root,
|
|
1476
|
+
"authHome": auth_home,
|
|
1477
|
+
}
|
|
1478
|
+
store["replace_session"](name, session_record)
|
|
1479
|
+
|
|
1480
|
+
state = (payload.get("states") or {}).get(name)
|
|
1481
|
+
if is_existing and merge:
|
|
1482
|
+
merged_state = {**(state or {}), **{k: v for k, v in (existing_state_before or {}).items() if v is not None}}
|
|
1483
|
+
if merged_state:
|
|
1484
|
+
store["write_session_state"](name, merged_state)
|
|
1485
|
+
elif state is not None:
|
|
1486
|
+
store["write_session_state"](name, state)
|
|
1487
|
+
|
|
1488
|
+
for item in decoded_profiles.get(name, []):
|
|
1489
|
+
dest_path = os.path.join(session_root, item["path"])
|
|
1490
|
+
# In merge mode, skip files that already exist locally.
|
|
1491
|
+
if is_existing and merge and os.path.exists(dest_path):
|
|
1492
|
+
continue
|
|
1493
|
+
_ensure_private_dir(os.path.dirname(dest_path))
|
|
1494
|
+
# Decrypted credentials: 0o600 from creation, no umask window.
|
|
1495
|
+
atomic_write(dest_path, item["content"], mode=0o600)
|
|
1496
|
+
except Exception:
|
|
1497
|
+
_restore_import_backup(name, backup_root, session_root, old_record, old_state)
|
|
1498
|
+
raise
|
|
1499
|
+
finally:
|
|
1500
|
+
if backup_root and os.path.exists(backup_root):
|
|
1501
|
+
remove_tree(backup_root, ignore_errors=True)
|
|
1418
1502
|
|
|
1419
1503
|
return {
|
|
1420
1504
|
"path": file_path,
|
package/src/session_store.py
CHANGED
|
@@ -294,8 +294,8 @@ def create_session_store(base_dir):
|
|
|
294
294
|
continue
|
|
295
295
|
try:
|
|
296
296
|
entry = json.loads(line)
|
|
297
|
-
except json.JSONDecodeError
|
|
298
|
-
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
continue
|
|
299
299
|
if session_name and entry.get("session_name") != session_name:
|
|
300
300
|
continue
|
|
301
301
|
entries.append(entry)
|
package/src/status_source.py
CHANGED
|
@@ -20,10 +20,6 @@ _KEY_VALUE_PATTERNS = [
|
|
|
20
20
|
("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
21
21
|
("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
22
22
|
("credits", re.compile(r"credits?\s*[:=]\s*(\d[\d, ]*(?:\.\d+)?)\s*(?:credits?)?", re.I)),
|
|
23
|
-
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
24
|
-
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
25
|
-
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
26
|
-
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
27
23
|
("usage_pct", re.compile(r"usage\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
28
24
|
("usage_pct", re.compile(r"current\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
29
25
|
("remaining_5h_pct", re.compile(r"5h(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
@@ -32,6 +28,14 @@ _KEY_VALUE_PATTERNS = [
|
|
|
32
28
|
("remaining_week_pct", re.compile(r"remaining\s+week\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
33
29
|
]
|
|
34
30
|
|
|
31
|
+
_CODEX_LIMIT_PERCENT_PATTERNS = [
|
|
32
|
+
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
33
|
+
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
34
|
+
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
35
|
+
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
36
|
+
]
|
|
37
|
+
_KEY_VALUE_PATTERNS.extend(_CODEX_LIMIT_PERCENT_PATTERNS)
|
|
38
|
+
|
|
35
39
|
|
|
36
40
|
def _strip_ansi(text):
|
|
37
41
|
return _ANSI_ESCAPE.sub("", str(text or ""))
|
|
@@ -103,6 +107,27 @@ def _is_zero_credit_balance(value):
|
|
|
103
107
|
return False
|
|
104
108
|
|
|
105
109
|
|
|
110
|
+
def _get_structured_rate_limit_window(rate_limits, duration_mins, fallback_key):
|
|
111
|
+
for key in ("primary", "secondary"):
|
|
112
|
+
window = rate_limits.get(key) or {}
|
|
113
|
+
if window.get("windowDurationMins") == duration_mins:
|
|
114
|
+
return window
|
|
115
|
+
if window.get("window_minutes") == duration_mins:
|
|
116
|
+
return window
|
|
117
|
+
fallback = rate_limits.get(fallback_key) or {}
|
|
118
|
+
if "windowDurationMins" in fallback or "window_minutes" in fallback:
|
|
119
|
+
return {}
|
|
120
|
+
return fallback
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _get_rate_limit_used_percent(window):
|
|
124
|
+
return window.get("used_percent", window.get("usedPercent"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get_rate_limit_reset_at(window):
|
|
128
|
+
return window.get("resets_at", window.get("resetsAt"))
|
|
129
|
+
|
|
130
|
+
|
|
106
131
|
def _extract_structured_rate_limits(record):
|
|
107
132
|
if not isinstance(record, dict):
|
|
108
133
|
return None
|
|
@@ -111,25 +136,27 @@ def _extract_structured_rate_limits(record):
|
|
|
111
136
|
if not isinstance(rate_limits, dict):
|
|
112
137
|
return None
|
|
113
138
|
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
five_hour = _get_structured_rate_limit_window(rate_limits, 300, "primary")
|
|
140
|
+
weekly = _get_structured_rate_limit_window(rate_limits, 10080, "secondary")
|
|
116
141
|
credits = rate_limits.get("credits") or {}
|
|
117
142
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
remaining_5h_pct = None if
|
|
121
|
-
remaining_week_pct = None if
|
|
143
|
+
five_hour_used = _coerce_percentage(_get_rate_limit_used_percent(five_hour))
|
|
144
|
+
weekly_used = _coerce_percentage(_get_rate_limit_used_percent(weekly))
|
|
145
|
+
remaining_5h_pct = None if five_hour_used is None else max(0, 100 - five_hour_used)
|
|
146
|
+
remaining_week_pct = None if weekly_used is None else max(0, 100 - weekly_used)
|
|
122
147
|
|
|
123
148
|
result = {
|
|
124
|
-
"usage_pct":
|
|
149
|
+
"usage_pct": five_hour_used if five_hour_used is not None else weekly_used,
|
|
125
150
|
"remaining_5h_pct": remaining_5h_pct,
|
|
126
151
|
"remaining_week_pct": remaining_week_pct,
|
|
127
152
|
"credits": None,
|
|
128
|
-
"reset_5h_at": _normalize_timestamp_value(
|
|
129
|
-
"reset_week_at": _normalize_timestamp_value(
|
|
153
|
+
"reset_5h_at": _normalize_timestamp_value(_get_rate_limit_reset_at(five_hour)),
|
|
154
|
+
"reset_week_at": _normalize_timestamp_value(_get_rate_limit_reset_at(weekly)),
|
|
130
155
|
"reset_at": None,
|
|
131
156
|
"raw_status_text": json.dumps(rate_limits, sort_keys=True),
|
|
132
157
|
}
|
|
158
|
+
if not five_hour and weekly:
|
|
159
|
+
result["_skip_5h_backfill"] = True
|
|
133
160
|
|
|
134
161
|
balance = credits.get("balance")
|
|
135
162
|
if balance not in (None, "") and not (
|
|
@@ -496,18 +523,10 @@ def extract_named_statuses_from_text(text):
|
|
|
496
523
|
break
|
|
497
524
|
|
|
498
525
|
for line in lines:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if m and "remaining_5h_pct" not in result:
|
|
504
|
-
result["remaining_5h_pct"] = int(m[1])
|
|
505
|
-
m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", line, re.I)
|
|
506
|
-
if m and "remaining_week_pct" not in result:
|
|
507
|
-
result["remaining_week_pct"] = int(m[1])
|
|
508
|
-
m = re.search(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", line, re.I)
|
|
509
|
-
if m and "remaining_week_pct" not in result:
|
|
510
|
-
result["remaining_week_pct"] = int(m[1])
|
|
526
|
+
for field, pattern in _CODEX_LIMIT_PERCENT_PATTERNS:
|
|
527
|
+
m = pattern.search(line)
|
|
528
|
+
if m and field not in result:
|
|
529
|
+
result[field] = int(m[1])
|
|
511
530
|
|
|
512
531
|
if "remaining_5h_pct" in result and "usage_pct" not in result:
|
|
513
532
|
result["usage_pct"] = max(0, 100 - result["remaining_5h_pct"])
|
|
@@ -640,6 +659,8 @@ _STATUS_VALUE_FIELDS = (
|
|
|
640
659
|
|
|
641
660
|
def _backfill_missing_fields(target, source):
|
|
642
661
|
for field in _STATUS_VALUE_FIELDS:
|
|
662
|
+
if target.get("_skip_5h_backfill") and field in ("remaining_5h_pct", "reset_5h_at"):
|
|
663
|
+
continue
|
|
643
664
|
if target.get(field) is None and source.get(field) is not None:
|
|
644
665
|
target[field] = source[field]
|
|
645
666
|
|
|
@@ -694,6 +715,7 @@ def find_latest_status_artifact(
|
|
|
694
715
|
"reset_week_at": candidate["structured"].get("reset_week_at"),
|
|
695
716
|
"reset_at": candidate["structured"].get("reset_at"),
|
|
696
717
|
"raw_status_text": candidate["structured"].get("raw_status_text"),
|
|
718
|
+
"_skip_5h_backfill": candidate["structured"].get("_skip_5h_backfill"),
|
|
697
719
|
}
|
|
698
720
|
if not parsed:
|
|
699
721
|
continue
|
package/src/status_view.py
CHANGED
|
@@ -76,15 +76,15 @@ def _format_credits(value, empty="n/a"):
|
|
|
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
78
|
if small:
|
|
79
|
-
headers = ["SESSION", "STATUS", "OK", "5H", "WEEK", "RESET 5H", "RESET WEEK"]
|
|
79
|
+
headers = ["SESSION", "STATUS", "OK", "5H", "WEEK", "RESETS", "RESET 5H", "RESET WEEK"]
|
|
80
80
|
elif has_provider:
|
|
81
|
-
headers = ["SESSION", "PROV.", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
81
|
+
headers = ["SESSION", "PROV.", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
82
82
|
else:
|
|
83
|
-
headers = ["SESSION", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
83
|
+
headers = ["SESSION", "STATUS", "AUTH", "OK", "5H", "WEEK", "BLOCK", "CR", "RESETS", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
84
84
|
if not rows:
|
|
85
85
|
if small:
|
|
86
|
-
return "SESSION STATUS OK 5H WEEK RESET 5H RESET WEEK\nNo saved sessions yet."
|
|
87
|
-
return "SESSION STATUS OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
|
|
86
|
+
return "SESSION STATUS OK 5H WEEK RESETS RESET 5H RESET WEEK\nNo saved sessions yet."
|
|
87
|
+
return "SESSION STATUS OK 5H WEEK BLOCK CR RESETS RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
|
|
88
88
|
headers = [_style(header, "1", use_color) for header in headers]
|
|
89
89
|
active_rows = [r for r in rows if r.get("enabled", True) is not False]
|
|
90
90
|
priority = recommend_priority_rows(rows)
|
|
@@ -109,12 +109,13 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
109
109
|
auth_color = "32" if auth == "logged" else "31" if auth == "logged out" else "2"
|
|
110
110
|
base.append(_style(auth, auth_color, use_color))
|
|
111
111
|
if r.get("enabled", True) is False:
|
|
112
|
-
usage_columns = [_style("-", "2", use_color)] *
|
|
112
|
+
usage_columns = [_style("-", "2", use_color)] * 6
|
|
113
113
|
else:
|
|
114
114
|
usage_columns = [
|
|
115
115
|
_style_pct(r.get("available_pct"), use_color),
|
|
116
116
|
_style_pct(r.get("remaining_5h_pct"), use_color),
|
|
117
117
|
_style_pct(r.get("remaining_week_pct"), use_color),
|
|
118
|
+
_style(str(r["reset_credits_available"]), "33", use_color) if r.get("reset_credits_available") is not None else _style("-", "2", use_color),
|
|
118
119
|
_style_reset_time(r.get("reset_5h_at"), use_color),
|
|
119
120
|
_style_reset_time(r.get("reset_week_at"), use_color),
|
|
120
121
|
]
|
|
@@ -367,8 +368,15 @@ def _format_status_detail(row, use_color=False):
|
|
|
367
368
|
f"{_style('Week left:', '1', use_color)} {_style_pct(row.get('remaining_week_pct'), use_color)}",
|
|
368
369
|
f"{_style('Block:', '1', use_color)} {_style(_format_blocking_quota(row), '33', use_color)}",
|
|
369
370
|
f"{_style('Credits:', '1', use_color)} {_style(_format_credits(row.get('credits')), '33' if row.get('credits') is not None else '2', use_color)}",
|
|
371
|
+
f"{_style('Bonus resets:', '1', use_color)} {_style(str(row['reset_credits_available']), '33', use_color) if row.get('reset_credits_available') is not None else _style('-', '2', use_color)}",
|
|
372
|
+
f"{_style('Reset expiry:', '1', use_color)} {_style_reset_time(_nearest_reset_expiry(row), use_color)}",
|
|
370
373
|
f"{_style('5h reset:', '1', use_color)} {_style_reset_time(row.get('reset_5h_at'), use_color)}",
|
|
371
374
|
f"{_style('Week reset:', '1', use_color)} {_style_reset_time(row.get('reset_week_at'), use_color)}",
|
|
372
375
|
f"{_style('Updated:', '1', use_color)} {_dim(_format_relative_age(row.get('updated_at')), use_color)}",
|
|
373
376
|
]
|
|
374
377
|
return "\n".join(lines)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _nearest_reset_expiry(row):
|
|
381
|
+
values = [credit.get("expires_at") for credit in row.get("reset_credits") or [] if credit.get("expires_at")]
|
|
382
|
+
return min(values, key=lambda value: _parse_reset_timestamp(value) or float("inf")) if values else None
|