cdx-manager 0.5.3 → 0.5.5
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 +4 -4
- package/changelogs/CHANGELOGS_0_5_4.md +49 -0
- package/changelogs/CHANGELOGS_0_5_5.md +48 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +36 -19
- package/src/claude_usage.py +33 -1
- package/src/cli.py +1 -1
- package/src/cli_commands.py +185 -156
- package/src/codex_usage.py +36 -7
- package/src/config.py +4 -0
- package/src/notify.py +25 -1
- package/src/provider_runtime.py +17 -10
- package/src/session_service.py +99 -28
- package/src/status_source.py +22 -20
- package/src/status_view.py +22 -4
package/src/provider_runtime.py
CHANGED
|
@@ -7,6 +7,7 @@ import subprocess
|
|
|
7
7
|
import sys
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
|
|
10
|
+
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
|
|
10
11
|
from .errors import CdxError
|
|
11
12
|
|
|
12
13
|
|
|
@@ -99,10 +100,15 @@ def _wrap_launch_with_transcript(session, spec, capture_transcript=True, env=Non
|
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None):
|
|
103
|
+
if initial_prompt is not None:
|
|
104
|
+
if not isinstance(initial_prompt, str):
|
|
105
|
+
raise CdxError("initial_prompt must be a string.")
|
|
106
|
+
if len(initial_prompt) > 32768:
|
|
107
|
+
raise CdxError("initial_prompt exceeds maximum allowed length.")
|
|
102
108
|
cwd = cwd or os.getcwd()
|
|
103
109
|
env_override = env_override or {}
|
|
104
110
|
env = {**os.environ, **env_override}
|
|
105
|
-
if session["provider"] ==
|
|
111
|
+
if session["provider"] == PROVIDER_CLAUDE:
|
|
106
112
|
args = ["--name", session["name"]]
|
|
107
113
|
if initial_prompt:
|
|
108
114
|
args.append(initial_prompt)
|
|
@@ -130,7 +136,7 @@ def _build_launch_spec(session, cwd=None, env_override=None, initial_prompt=None
|
|
|
130
136
|
|
|
131
137
|
def _build_login_status_spec(session, env_override=None):
|
|
132
138
|
env = {**os.environ, **(env_override or {})}
|
|
133
|
-
if session["provider"] ==
|
|
139
|
+
if session["provider"] == PROVIDER_CLAUDE:
|
|
134
140
|
env.update(_home_env_overrides(_get_auth_home(session)))
|
|
135
141
|
|
|
136
142
|
def parser(output):
|
|
@@ -155,7 +161,7 @@ def _build_login_status_spec(session, env_override=None):
|
|
|
155
161
|
def _build_auth_action_spec(session, action, cwd=None, env_override=None):
|
|
156
162
|
cwd = cwd or os.getcwd()
|
|
157
163
|
env = {**os.environ, **(env_override or {})}
|
|
158
|
-
if session["provider"] ==
|
|
164
|
+
if session["provider"] == PROVIDER_CLAUDE:
|
|
159
165
|
env.update(_home_env_overrides(_get_auth_home(session)))
|
|
160
166
|
return {"command": "claude", "args": ["auth", action],
|
|
161
167
|
"options": {"cwd": cwd, "env": env}, "label": f"claude auth {action}"}
|
|
@@ -184,7 +190,7 @@ def _resolve_command(command, env=None):
|
|
|
184
190
|
def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
185
191
|
spawn_sync = spawn_sync or subprocess.run
|
|
186
192
|
spec = _build_login_status_spec(session, env_override)
|
|
187
|
-
if session.get("provider") ==
|
|
193
|
+
if session.get("provider") == PROVIDER_CODEX:
|
|
188
194
|
auth_path = os.path.join(_get_auth_home(session), "auth.json")
|
|
189
195
|
if os.path.isfile(auth_path):
|
|
190
196
|
return True
|
|
@@ -250,11 +256,12 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
|
250
256
|
spec = _fallback_launch_spec_or_raise(spec, error)
|
|
251
257
|
child = start_child(spec)
|
|
252
258
|
|
|
253
|
-
forwarded_signal =
|
|
259
|
+
forwarded_signal = None
|
|
254
260
|
handlers = []
|
|
255
261
|
|
|
256
262
|
def forward(sig, _frame=None):
|
|
257
|
-
forwarded_signal
|
|
263
|
+
nonlocal forwarded_signal
|
|
264
|
+
forwarded_signal = sig
|
|
258
265
|
try:
|
|
259
266
|
if hasattr(child, "send_signal"):
|
|
260
267
|
child.send_signal(sig)
|
|
@@ -283,7 +290,7 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
|
283
290
|
|
|
284
291
|
try:
|
|
285
292
|
child.wait()
|
|
286
|
-
if forwarded_signal
|
|
293
|
+
if forwarded_signal is None and child.returncode != 0 and _should_retry_without_transcript(spec):
|
|
287
294
|
spec = _fallback_launch_spec_or_raise(spec)
|
|
288
295
|
child = start_child(spec)
|
|
289
296
|
child.wait()
|
|
@@ -301,10 +308,10 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
|
301
308
|
except (OSError, ValueError):
|
|
302
309
|
pass
|
|
303
310
|
|
|
304
|
-
if forwarded_signal
|
|
311
|
+
if forwarded_signal is not None:
|
|
305
312
|
raise CdxError(
|
|
306
|
-
f"{spec['label']} interrupted by {_signal_name(forwarded_signal
|
|
307
|
-
_signal_exit_code(forwarded_signal
|
|
313
|
+
f"{spec['label']} interrupted by {_signal_name(forwarded_signal)} for session {session['name']}",
|
|
314
|
+
_signal_exit_code(forwarded_signal),
|
|
308
315
|
)
|
|
309
316
|
if child.returncode != 0:
|
|
310
317
|
raise CdxError(
|
package/src/session_service.py
CHANGED
|
@@ -8,14 +8,14 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from urllib.parse import quote
|
|
9
9
|
|
|
10
10
|
from .backup_bundle import decode_bundle, encode_bundle
|
|
11
|
-
from .config import get_cdx_home
|
|
11
|
+
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
|
|
12
12
|
from .codex_usage import fetch_codex_rate_limits
|
|
13
13
|
from .errors import CdxError
|
|
14
14
|
from .session_store import create_session_store
|
|
15
15
|
from .status_source import find_latest_status_artifact
|
|
16
16
|
|
|
17
|
-
DEFAULT_PROVIDER =
|
|
18
|
-
ALLOWED_PROVIDERS =
|
|
17
|
+
DEFAULT_PROVIDER = PROVIDER_CODEX
|
|
18
|
+
ALLOWED_PROVIDERS = set(PROVIDERS)
|
|
19
19
|
MAX_SESSION_NAME_LENGTH = 64
|
|
20
20
|
RESERVED_SESSION_NAMES = {
|
|
21
21
|
"add",
|
|
@@ -81,8 +81,15 @@ def _local_now_iso():
|
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
def _safe_relpath(path):
|
|
84
|
-
normalized = str(path or "").replace("\\", "/").
|
|
85
|
-
if
|
|
84
|
+
normalized = os.path.normpath(str(path or "").replace("\\", "/")).replace("\\", "/")
|
|
85
|
+
if (
|
|
86
|
+
not normalized
|
|
87
|
+
or normalized == "."
|
|
88
|
+
or normalized.startswith("/")
|
|
89
|
+
or (len(normalized) > 1 and normalized[1] == ":")
|
|
90
|
+
or normalized == ".."
|
|
91
|
+
or normalized.startswith("../")
|
|
92
|
+
):
|
|
86
93
|
raise CdxError("Bundle contains an unsafe file path.")
|
|
87
94
|
return normalized
|
|
88
95
|
|
|
@@ -253,7 +260,7 @@ def create_session_service(options=None):
|
|
|
253
260
|
|
|
254
261
|
def _get_session_auth_home(name, provider):
|
|
255
262
|
root = _get_session_root(name)
|
|
256
|
-
if provider ==
|
|
263
|
+
if provider == PROVIDER_CLAUDE:
|
|
257
264
|
return os.path.join(root, "claude-home")
|
|
258
265
|
return root
|
|
259
266
|
|
|
@@ -288,22 +295,39 @@ def create_session_service(options=None):
|
|
|
288
295
|
"auth": session.get("auth"),
|
|
289
296
|
}
|
|
290
297
|
|
|
291
|
-
def
|
|
292
|
-
|
|
298
|
+
def _auth_bundle_paths(provider):
|
|
299
|
+
if provider == PROVIDER_CLAUDE:
|
|
300
|
+
return [
|
|
301
|
+
"claude-home/.claude/.credentials.json",
|
|
302
|
+
"claude-home/.claude.json",
|
|
303
|
+
"claude-home/auth.json",
|
|
304
|
+
]
|
|
305
|
+
return [
|
|
306
|
+
"auth.json",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
def _collect_auth_files(session_root, provider, session_name=None, progress_callback=None):
|
|
293
310
|
files = []
|
|
311
|
+
total_bytes = 0
|
|
294
312
|
if not os.path.isdir(session_root):
|
|
295
|
-
return files
|
|
296
|
-
for
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
return {"files": files, "file_count": 0, "bytes": 0}
|
|
314
|
+
for rel_path in _auth_bundle_paths(provider):
|
|
315
|
+
full_path = os.path.join(session_root, rel_path)
|
|
316
|
+
if not os.path.isfile(full_path):
|
|
317
|
+
continue
|
|
318
|
+
with open(full_path, "rb") as handle:
|
|
319
|
+
raw_content = handle.read()
|
|
320
|
+
total_bytes += len(raw_content)
|
|
321
|
+
content = base64.b64encode(raw_content).decode("ascii")
|
|
322
|
+
files.append({"path": rel_path.replace(os.sep, "/"), "data_b64": content})
|
|
323
|
+
if progress_callback:
|
|
324
|
+
progress_callback({
|
|
325
|
+
"event": "profile_progress",
|
|
326
|
+
"session_name": session_name,
|
|
327
|
+
"file_count": len(files),
|
|
328
|
+
"bytes": total_bytes,
|
|
329
|
+
})
|
|
330
|
+
return {"files": files, "file_count": len(files), "bytes": total_bytes}
|
|
307
331
|
|
|
308
332
|
def _resolve_session_subset(session_names):
|
|
309
333
|
if not session_names:
|
|
@@ -326,7 +350,7 @@ def create_session_service(options=None):
|
|
|
326
350
|
_ensure_private_dir(os.path.join(base_dir, "profiles"))
|
|
327
351
|
_ensure_private_dir(session_root)
|
|
328
352
|
_ensure_private_dir(auth_home)
|
|
329
|
-
if normalized_provider ==
|
|
353
|
+
if normalized_provider == PROVIDER_CODEX:
|
|
330
354
|
_seed_codex_auth_from_global(auth_home, env=env)
|
|
331
355
|
now = _local_now_iso()
|
|
332
356
|
session = {
|
|
@@ -553,7 +577,7 @@ def create_session_service(options=None):
|
|
|
553
577
|
source_root = session.get("authHome") or _get_session_auth_home(
|
|
554
578
|
session["name"], session["provider"]
|
|
555
579
|
)
|
|
556
|
-
if session["provider"] ==
|
|
580
|
+
if session["provider"] == PROVIDER_CODEX and codex_status_fetcher:
|
|
557
581
|
live_status = codex_status_fetcher({**session, "authHome": source_root})
|
|
558
582
|
if live_status:
|
|
559
583
|
record_status(session["name"], live_status)
|
|
@@ -561,7 +585,7 @@ def create_session_service(options=None):
|
|
|
561
585
|
|
|
562
586
|
expected_account_email = (
|
|
563
587
|
_read_expected_account_email(source_root)
|
|
564
|
-
if session["provider"] ==
|
|
588
|
+
if session["provider"] == PROVIDER_CODEX
|
|
565
589
|
else None
|
|
566
590
|
)
|
|
567
591
|
artifact = find_latest_status_artifact(
|
|
@@ -570,7 +594,7 @@ def create_session_service(options=None):
|
|
|
570
594
|
expected_account_email=expected_account_email,
|
|
571
595
|
)
|
|
572
596
|
if (
|
|
573
|
-
session["provider"] ==
|
|
597
|
+
session["provider"] == PROVIDER_CODEX
|
|
574
598
|
and not artifact
|
|
575
599
|
and os.path.abspath(base_dir) == os.path.abspath(get_cdx_home(env))
|
|
576
600
|
):
|
|
@@ -620,11 +644,28 @@ def create_session_service(options=None):
|
|
|
620
644
|
raise CdxError(f"Unknown session: {name}")
|
|
621
645
|
return updated
|
|
622
646
|
|
|
623
|
-
def get_status_rows():
|
|
647
|
+
def get_status_rows(progress_callback=None):
|
|
624
648
|
sessions = list_sessions()
|
|
649
|
+
if progress_callback:
|
|
650
|
+
progress_callback({
|
|
651
|
+
"event": "status_started",
|
|
652
|
+
"session_count": len(sessions),
|
|
653
|
+
})
|
|
625
654
|
resolved = []
|
|
626
655
|
for s in sessions:
|
|
656
|
+
if progress_callback:
|
|
657
|
+
progress_callback({
|
|
658
|
+
"event": "session_started",
|
|
659
|
+
"session_name": s["name"],
|
|
660
|
+
"provider": s["provider"],
|
|
661
|
+
})
|
|
627
662
|
status = _resolve_session_status(s)
|
|
663
|
+
if progress_callback:
|
|
664
|
+
progress_callback({
|
|
665
|
+
"event": "session_finished",
|
|
666
|
+
"session_name": s["name"],
|
|
667
|
+
"has_status": bool(status),
|
|
668
|
+
})
|
|
628
669
|
resolved.append({
|
|
629
670
|
**s,
|
|
630
671
|
"lastStatus": status,
|
|
@@ -659,6 +700,12 @@ def create_session_service(options=None):
|
|
|
659
700
|
"reset_week_at": status.get("reset_week_at") if status else None,
|
|
660
701
|
"reset_at": status.get("reset_at") if status else None,
|
|
661
702
|
"updated_at": _to_local_iso(s.get("lastStatusAt")),
|
|
703
|
+
"last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
|
|
704
|
+
})
|
|
705
|
+
if progress_callback:
|
|
706
|
+
progress_callback({
|
|
707
|
+
"event": "status_finished",
|
|
708
|
+
"row_count": len(rows),
|
|
662
709
|
})
|
|
663
710
|
return rows
|
|
664
711
|
|
|
@@ -688,7 +735,7 @@ def create_session_service(options=None):
|
|
|
688
735
|
def get_session_root(name):
|
|
689
736
|
return _get_session_root(name)
|
|
690
737
|
|
|
691
|
-
def export_bundle(file_path, include_auth=False, session_names=None, passphrase=None, force=False):
|
|
738
|
+
def export_bundle(file_path, include_auth=False, session_names=None, passphrase=None, force=False, progress_callback=None):
|
|
692
739
|
if not file_path:
|
|
693
740
|
raise CdxError("Export path is required.")
|
|
694
741
|
if os.path.exists(file_path) and not force:
|
|
@@ -703,16 +750,36 @@ def create_session_service(options=None):
|
|
|
703
750
|
"states": {},
|
|
704
751
|
"profiles": {},
|
|
705
752
|
}
|
|
753
|
+
profile_file_count = 0
|
|
754
|
+
profile_bytes = 0
|
|
755
|
+
if progress_callback:
|
|
756
|
+
progress_callback({
|
|
757
|
+
"event": "export_started",
|
|
758
|
+
"include_auth": bool(include_auth),
|
|
759
|
+
"session_count": len(sessions),
|
|
760
|
+
"session_names": [session["name"] for session in sessions],
|
|
761
|
+
})
|
|
706
762
|
for session in sessions:
|
|
763
|
+
if progress_callback:
|
|
764
|
+
progress_callback({"event": "session_started", "session_name": session["name"]})
|
|
707
765
|
payload["sessions"].append(_build_export_session_record(session))
|
|
708
766
|
state = store["read_session_state"](session["name"])
|
|
709
767
|
if state is not None:
|
|
710
768
|
payload["states"][session["name"]] = state
|
|
711
769
|
if include_auth:
|
|
712
770
|
session_root = session.get("sessionRoot") or _get_session_root(session["name"])
|
|
713
|
-
|
|
714
|
-
|
|
771
|
+
profile = _collect_auth_files(session_root, session["provider"], session["name"], progress_callback)
|
|
772
|
+
payload["profiles"][session["name"]] = profile["files"]
|
|
773
|
+
profile_file_count += profile["file_count"]
|
|
774
|
+
profile_bytes += profile["bytes"]
|
|
775
|
+
if progress_callback:
|
|
776
|
+
progress_callback({"event": "session_finished", "session_name": session["name"]})
|
|
777
|
+
|
|
778
|
+
if progress_callback:
|
|
779
|
+
progress_callback({"event": "encoding_started"})
|
|
715
780
|
bundle_bytes = encode_bundle(payload, include_auth=include_auth, passphrase=passphrase)
|
|
781
|
+
if progress_callback:
|
|
782
|
+
progress_callback({"event": "writing_started", "path": file_path, "bundle_size_bytes": len(bundle_bytes)})
|
|
716
783
|
_ensure_private_dir(os.path.dirname(os.path.abspath(file_path)) or ".")
|
|
717
784
|
with open(file_path, "wb") as handle:
|
|
718
785
|
handle.write(bundle_bytes)
|
|
@@ -725,6 +792,10 @@ def create_session_service(options=None):
|
|
|
725
792
|
"path": file_path,
|
|
726
793
|
"include_auth": include_auth,
|
|
727
794
|
"session_names": [session["name"] for session in sessions],
|
|
795
|
+
"session_count": len(sessions),
|
|
796
|
+
"profile_file_count": profile_file_count if include_auth else None,
|
|
797
|
+
"profile_bytes": profile_bytes if include_auth else None,
|
|
798
|
+
"bundle_size_bytes": len(bundle_bytes),
|
|
728
799
|
}
|
|
729
800
|
|
|
730
801
|
def import_bundle(file_path, passphrase=None, session_names=None, force=False):
|
package/src/status_source.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import re
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
|
|
6
|
+
from .config import PROVIDER_CLAUDE, PROVIDER_CODEX
|
|
6
7
|
|
|
7
8
|
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*m")
|
|
8
9
|
_ANSI_TERMINAL_CONTROL = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]")
|
|
@@ -14,6 +15,23 @@ MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
|
14
15
|
MAX_STATUS_READ_BYTES = 512 * 1024
|
|
15
16
|
MAX_STATUS_CANDIDATE_FILES = 64
|
|
16
17
|
|
|
18
|
+
_KEY_VALUE_PATTERNS = [
|
|
19
|
+
("usage_pct", re.compile(r"usage_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
20
|
+
("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
21
|
+
("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
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
|
+
("usage_pct", re.compile(r"usage\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
28
|
+
("usage_pct", re.compile(r"current\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
29
|
+
("remaining_5h_pct", re.compile(r"5h(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
30
|
+
("remaining_5h_pct", re.compile(r"remaining\s+5h\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
31
|
+
("remaining_week_pct", re.compile(r"week(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
32
|
+
("remaining_week_pct", re.compile(r"remaining\s+week\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
33
|
+
]
|
|
34
|
+
|
|
17
35
|
|
|
18
36
|
def _strip_ansi(text):
|
|
19
37
|
return _ANSI_ESCAPE.sub("", str(text or ""))
|
|
@@ -185,7 +203,7 @@ def _extract_status_blocks_from_text(text, provider=None, source_ref=None, times
|
|
|
185
203
|
index = max(index + 1, end_index)
|
|
186
204
|
return blocks
|
|
187
205
|
|
|
188
|
-
if provider !=
|
|
206
|
+
if provider != PROVIDER_CODEX:
|
|
189
207
|
for block in collect_blocks(
|
|
190
208
|
re.compile(r"^\s*(?:[│|]\s*)?Current session\b", re.I),
|
|
191
209
|
[re.compile(p, re.I) for p in [
|
|
@@ -195,7 +213,7 @@ def _extract_status_blocks_from_text(text, provider=None, source_ref=None, times
|
|
|
195
213
|
):
|
|
196
214
|
items.append({"source_ref": source_ref, "timestamp": timestamp, "text": block})
|
|
197
215
|
|
|
198
|
-
if provider !=
|
|
216
|
+
if provider != PROVIDER_CLAUDE:
|
|
199
217
|
for block in collect_blocks(
|
|
200
218
|
re.compile(r"^\s*(?:[│|]\s*)?5h\s+limit\b", re.I),
|
|
201
219
|
[re.compile(p, re.I) for p in [
|
|
@@ -412,23 +430,7 @@ def extract_named_statuses_from_text(text):
|
|
|
412
430
|
lines = [l.strip() for l in normalized.split("\n") if l.strip()]
|
|
413
431
|
result = {}
|
|
414
432
|
|
|
415
|
-
|
|
416
|
-
("usage_pct", re.compile(r"usage_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
417
|
-
("remaining_5h_pct", re.compile(r"remaining_?5h_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
418
|
-
("remaining_week_pct", re.compile(r"remaining_?week_pct\s*[:=]\s*(\d{1,3})%?", re.I)),
|
|
419
|
-
("credits", re.compile(r"credits?\s*[:=]\s*([\d, ]*\d[\d, ]*)\s*(?:credits?)?", re.I)),
|
|
420
|
-
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
421
|
-
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})%\s*left", re.I)),
|
|
422
|
-
("remaining_5h_pct", re.compile(r"5h\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
423
|
-
("remaining_week_pct", re.compile(r"weekly\s+limit\s*:\s*\[[^\]]*\]\s*(\d{1,3})(?:%|\b)", re.I)),
|
|
424
|
-
("usage_pct", re.compile(r"usage\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
425
|
-
("usage_pct", re.compile(r"current\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
426
|
-
("remaining_5h_pct", re.compile(r"5h(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
427
|
-
("remaining_5h_pct", re.compile(r"remaining\s+5h\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
428
|
-
("remaining_week_pct", re.compile(r"week(?:\s+remaining)?\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
429
|
-
("remaining_week_pct", re.compile(r"remaining\s+week\s*[:=]\s*(\d{1,3})%", re.I)),
|
|
430
|
-
]
|
|
431
|
-
for field, pattern in key_value_patterns:
|
|
433
|
+
for field, pattern in _KEY_VALUE_PATTERNS:
|
|
432
434
|
if field not in result:
|
|
433
435
|
m = pattern.search(normalized)
|
|
434
436
|
if m:
|
|
@@ -620,7 +622,7 @@ def find_latest_status_artifact(root_dir, provider=None, expected_account_email=
|
|
|
620
622
|
|
|
621
623
|
best = None
|
|
622
624
|
for candidate in records:
|
|
623
|
-
if provider ==
|
|
625
|
+
if provider == PROVIDER_CODEX and not _account_matches_expected(
|
|
624
626
|
candidate["text"], expected_account_email
|
|
625
627
|
):
|
|
626
628
|
continue
|
package/src/status_view.py
CHANGED
|
@@ -9,6 +9,9 @@ from .cli_render import (
|
|
|
9
9
|
_style_pct,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
RESET_COUNTDOWN_SAFETY_SECONDS = 60
|
|
13
|
+
PRIORITY_EMPTY_AVAILABLE_THRESHOLD = 5
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
def _format_reset_time(value):
|
|
14
17
|
if not value:
|
|
@@ -27,6 +30,7 @@ def _format_reset_time(value):
|
|
|
27
30
|
if hours_ago < 24:
|
|
28
31
|
return f"passed {hours_ago}h ago"
|
|
29
32
|
return value
|
|
33
|
+
delta_s = delta_s + RESET_COUNTDOWN_SAFETY_SECONDS
|
|
30
34
|
if delta_s < 60:
|
|
31
35
|
return "now"
|
|
32
36
|
if delta_s < 24 * 60 * 60:
|
|
@@ -104,14 +108,28 @@ def _format_status_rows(rows, use_color=False, small=False):
|
|
|
104
108
|
if len(priority) > 1 else "."
|
|
105
109
|
)
|
|
106
110
|
) if priority else "Priority: no usable session status yet."
|
|
111
|
+
current_line = _format_current_session_line(rows)
|
|
107
112
|
return "\n".join([
|
|
108
113
|
_pad_table([headers] + table_rows),
|
|
109
114
|
"",
|
|
110
115
|
_style(priority_line, "1", use_color),
|
|
111
|
-
_style(
|
|
116
|
+
_style(current_line, "2", use_color),
|
|
112
117
|
])
|
|
113
118
|
|
|
114
119
|
|
|
120
|
+
def _format_current_session_line(rows):
|
|
121
|
+
launched = []
|
|
122
|
+
for row in rows:
|
|
123
|
+
timestamp = _parse_reset_timestamp(row.get("last_launched_at"))
|
|
124
|
+
if timestamp is not None:
|
|
125
|
+
launched.append((timestamp, row))
|
|
126
|
+
if not launched:
|
|
127
|
+
return "Current: no launched session known yet."
|
|
128
|
+
timestamp, row = max(launched, key=lambda item: (item[0], item[1].get("session_name") or ""))
|
|
129
|
+
label = _format_relative_age(row.get("last_launched_at"))
|
|
130
|
+
return f"Current: last launched {row['session_name']} ({label})."
|
|
131
|
+
|
|
132
|
+
|
|
115
133
|
def _recommend_priority_sessions(rows):
|
|
116
134
|
if not rows:
|
|
117
135
|
return []
|
|
@@ -120,7 +138,7 @@ def _recommend_priority_sessions(rows):
|
|
|
120
138
|
has_credits = row.get("credits") is not None
|
|
121
139
|
credit_rank = 0 if has_credits else 1
|
|
122
140
|
available = row.get("available_pct")
|
|
123
|
-
usable_now = available is not None and available >
|
|
141
|
+
usable_now = available is not None and available > PRIORITY_EMPTY_AVAILABLE_THRESHOLD
|
|
124
142
|
known_available = available is not None
|
|
125
143
|
reset_timestamp = _priority_reset_timestamp(row)
|
|
126
144
|
reset_is_future = reset_timestamp is not None and reset_timestamp >= _now_timestamp()
|
|
@@ -165,7 +183,7 @@ def _priority_instruction(row, position):
|
|
|
165
183
|
|
|
166
184
|
def _priority_needs_refresh(row):
|
|
167
185
|
available = row.get("available_pct")
|
|
168
|
-
if available is None or available >
|
|
186
|
+
if available is None or available > PRIORITY_EMPTY_AVAILABLE_THRESHOLD:
|
|
169
187
|
return False
|
|
170
188
|
_label, is_past = _priority_reset_info(row)
|
|
171
189
|
return is_past
|
|
@@ -175,7 +193,7 @@ def _priority_reason(row):
|
|
|
175
193
|
available = row.get("available_pct")
|
|
176
194
|
if available is None:
|
|
177
195
|
return "status unknown"
|
|
178
|
-
if available >
|
|
196
|
+
if available > PRIORITY_EMPTY_AVAILABLE_THRESHOLD:
|
|
179
197
|
return f"{_format_pct(available)} OK"
|
|
180
198
|
label, is_past = _priority_reset_info(row)
|
|
181
199
|
if label:
|