cdx-manager 0.9.15 → 0.10.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 +75 -3
- package/changelogs/CHANGELOGS_0_10_0.md +43 -0
- package/changelogs/CHANGELOGS_0_9_16.md +19 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +168 -2
- package/src/cli_args.py +1 -0
- package/src/cli_commands.py +513 -4
- package/src/cli_helpers.py +7 -0
- package/src/codex_usage.py +81 -16
- package/src/provider_runtime.py +10 -1
- package/src/session_service.py +10 -0
- package/src/status_view.py +14 -6
package/src/codex_usage.py
CHANGED
|
@@ -65,6 +65,15 @@ def _format_reset_date(unix_seconds):
|
|
|
65
65
|
return f"{MONTH_ABBR[dt.month - 1]} {dt.day} {str(dt.hour).zfill(2)}:{str(dt.minute).zfill(2)}"
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def _format_timestamp(unix_seconds):
|
|
69
|
+
if unix_seconds is None:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
return datetime.fromtimestamp(int(unix_seconds), tz=timezone.utc).astimezone().isoformat()
|
|
73
|
+
except (TypeError, ValueError, OSError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
68
77
|
def _remaining_from_used_percent(value):
|
|
69
78
|
if value is None:
|
|
70
79
|
return None
|
|
@@ -84,7 +93,7 @@ def _get_window(snapshot, duration_mins):
|
|
|
84
93
|
return {}
|
|
85
94
|
|
|
86
95
|
|
|
87
|
-
def normalize_codex_rate_limit_snapshot(snapshot):
|
|
96
|
+
def normalize_codex_rate_limit_snapshot(snapshot, reset_credits=None):
|
|
88
97
|
if not snapshot:
|
|
89
98
|
return None
|
|
90
99
|
|
|
@@ -101,6 +110,23 @@ def normalize_codex_rate_limit_snapshot(snapshot):
|
|
|
101
110
|
|
|
102
111
|
reset_5h_at = _format_reset_date(five_hour.get("resetsAt") or five_hour.get("resets_at"))
|
|
103
112
|
reset_week_at = _format_reset_date(weekly.get("resetsAt") or weekly.get("resets_at"))
|
|
113
|
+
reset_credits = reset_credits or {}
|
|
114
|
+
available_count = reset_credits.get("availableCount", reset_credits.get("available_count"))
|
|
115
|
+
try:
|
|
116
|
+
available_count = max(0, int(available_count)) if available_count is not None else None
|
|
117
|
+
except (TypeError, ValueError):
|
|
118
|
+
available_count = None
|
|
119
|
+
credit_rows = []
|
|
120
|
+
for credit in reset_credits.get("credits") or []:
|
|
121
|
+
if not isinstance(credit, dict) or credit.get("status") not in (None, "available"):
|
|
122
|
+
continue
|
|
123
|
+
credit_rows.append({
|
|
124
|
+
"id": credit.get("id"),
|
|
125
|
+
"title": credit.get("title"),
|
|
126
|
+
"description": credit.get("description"),
|
|
127
|
+
"granted_at": _format_timestamp(credit.get("grantedAt", credit.get("granted_at"))),
|
|
128
|
+
"expires_at": _format_timestamp(credit.get("expiresAt", credit.get("expires_at"))),
|
|
129
|
+
})
|
|
104
130
|
|
|
105
131
|
raw_status_text = json.dumps(snapshot, sort_keys=True)
|
|
106
132
|
return {
|
|
@@ -111,6 +137,8 @@ def normalize_codex_rate_limit_snapshot(snapshot):
|
|
|
111
137
|
weekly.get("usedPercent", weekly.get("used_percent"))
|
|
112
138
|
),
|
|
113
139
|
"credits": credit_balance,
|
|
140
|
+
"reset_credits_available": available_count,
|
|
141
|
+
"reset_credits": credit_rows,
|
|
114
142
|
"reset_5h_at": reset_5h_at,
|
|
115
143
|
"reset_week_at": reset_week_at,
|
|
116
144
|
"reset_at": reset_week_at or reset_5h_at,
|
|
@@ -166,6 +194,32 @@ def fetch_codex_rate_limit_diagnostic(session, timeout=5, popen_factory=None):
|
|
|
166
194
|
|
|
167
195
|
|
|
168
196
|
def _probe_codex_rate_limit_diagnostic(session, auth_home, timeout=5, popen_factory=None):
|
|
197
|
+
diagnostic = _request_codex_app_server(
|
|
198
|
+
auth_home,
|
|
199
|
+
"account/rateLimits/read",
|
|
200
|
+
None,
|
|
201
|
+
"rate_limits_read_failed",
|
|
202
|
+
timeout=timeout,
|
|
203
|
+
popen_factory=popen_factory,
|
|
204
|
+
)
|
|
205
|
+
if not diagnostic["ok"]:
|
|
206
|
+
return diagnostic
|
|
207
|
+
response = diagnostic["response"]
|
|
208
|
+
result = response.get("result") or {}
|
|
209
|
+
by_limit = result.get("rateLimitsByLimitId") or {}
|
|
210
|
+
snapshot = by_limit.get("codex") or result.get("rateLimits")
|
|
211
|
+
status = normalize_codex_rate_limit_snapshot(snapshot, result.get("rateLimitResetCredits"))
|
|
212
|
+
if not status:
|
|
213
|
+
return {
|
|
214
|
+
"ok": False,
|
|
215
|
+
"reason": "missing_rate_limits",
|
|
216
|
+
"status": None,
|
|
217
|
+
"response": response,
|
|
218
|
+
}
|
|
219
|
+
return {"ok": True, "reason": None, "status": status, "response": response}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _request_codex_app_server(auth_home, method, params, failure_reason, timeout=5, popen_factory=None):
|
|
169
223
|
env = os.environ.copy()
|
|
170
224
|
env["CODEX_HOME"] = auth_home
|
|
171
225
|
popen_factory = popen_factory or subprocess.Popen
|
|
@@ -204,29 +258,18 @@ def _probe_codex_rate_limit_diagnostic(session, auth_home, timeout=5, popen_fact
|
|
|
204
258
|
_write_json_line(process, {
|
|
205
259
|
"jsonrpc": "2.0",
|
|
206
260
|
"id": 2,
|
|
207
|
-
"method":
|
|
208
|
-
"params":
|
|
261
|
+
"method": method,
|
|
262
|
+
"params": params,
|
|
209
263
|
})
|
|
210
264
|
response = _read_response(output, 2, timeout)
|
|
211
265
|
if not response or response.get("error"):
|
|
212
266
|
return {
|
|
213
267
|
"ok": False,
|
|
214
|
-
"reason":
|
|
215
|
-
"status": None,
|
|
216
|
-
"response": response,
|
|
217
|
-
}
|
|
218
|
-
result = response.get("result") or {}
|
|
219
|
-
by_limit = result.get("rateLimitsByLimitId") or {}
|
|
220
|
-
snapshot = by_limit.get("codex") or result.get("rateLimits")
|
|
221
|
-
status = normalize_codex_rate_limit_snapshot(snapshot)
|
|
222
|
-
if not status:
|
|
223
|
-
return {
|
|
224
|
-
"ok": False,
|
|
225
|
-
"reason": "missing_rate_limits",
|
|
268
|
+
"reason": failure_reason,
|
|
226
269
|
"status": None,
|
|
227
270
|
"response": response,
|
|
228
271
|
}
|
|
229
|
-
return {"ok": True, "reason": None, "status":
|
|
272
|
+
return {"ok": True, "reason": None, "status": None, "response": response}
|
|
230
273
|
except FileNotFoundError as error:
|
|
231
274
|
return {"ok": False, "reason": "codex_cli_not_found", "status": None, "error": str(error)}
|
|
232
275
|
except (OSError, ValueError, BrokenPipeError) as error:
|
|
@@ -250,3 +293,25 @@ def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
|
250
293
|
popen_factory=popen_factory,
|
|
251
294
|
)
|
|
252
295
|
return diagnostic.get("status") if diagnostic.get("ok") else None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def consume_codex_rate_limit_reset_credit(session, idempotency_key, credit_id=None, timeout=5, popen_factory=None):
|
|
299
|
+
auth_home = session.get("authHome")
|
|
300
|
+
if not auth_home:
|
|
301
|
+
return {"ok": False, "reason": "missing_auth_home", "outcome": None}
|
|
302
|
+
with codex_auth_lock(auth_home) as acquired:
|
|
303
|
+
if not acquired:
|
|
304
|
+
return {"ok": False, "reason": "auth_locked", "outcome": None}
|
|
305
|
+
params = {"idempotencyKey": idempotency_key}
|
|
306
|
+
if credit_id:
|
|
307
|
+
params["creditId"] = credit_id
|
|
308
|
+
diagnostic = _request_codex_app_server(
|
|
309
|
+
auth_home,
|
|
310
|
+
"account/rateLimitResetCredit/consume",
|
|
311
|
+
params,
|
|
312
|
+
"reset_consume_failed",
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
popen_factory=popen_factory,
|
|
315
|
+
)
|
|
316
|
+
result = (diagnostic.get("response") or {}).get("result") or {}
|
|
317
|
+
return {**diagnostic, "outcome": result.get("outcome")}
|
package/src/provider_runtime.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import errno
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
@@ -881,6 +882,14 @@ def _format_probe_failure(session, spec, error):
|
|
|
881
882
|
f"Install {command} and retry cdx add {session['name']}.",
|
|
882
883
|
127,
|
|
883
884
|
)
|
|
885
|
+
if isinstance(error, OSError) and error.errno == errno.ENOEXEC:
|
|
886
|
+
target = getattr(error, "filename", None) or command
|
|
887
|
+
return CdxError(
|
|
888
|
+
f"Failed to check login status for {session['name']}: {target} is not a valid executable "
|
|
889
|
+
f"(corrupted install, wrong architecture, or a script missing its shebang). "
|
|
890
|
+
f"Reinstall {command} and retry.",
|
|
891
|
+
126,
|
|
892
|
+
)
|
|
884
893
|
message = getattr(error, "message", None) or str(error)
|
|
885
894
|
return CdxError(f"Failed to check login status for {session['name']}: {message}")
|
|
886
895
|
|
|
@@ -915,7 +924,7 @@ def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_loca
|
|
|
915
924
|
stdout = result.get("stdout") if isinstance(result, dict) else getattr(result, "stdout", "")
|
|
916
925
|
stderr = result.get("stderr") if isinstance(result, dict) else getattr(result, "stderr", "")
|
|
917
926
|
output = (stdout or "") + (stderr or "")
|
|
918
|
-
except
|
|
927
|
+
except OSError as error:
|
|
919
928
|
raise _format_probe_failure(session, spec, error) from error
|
|
920
929
|
return spec["parser"](output)
|
|
921
930
|
|
package/src/session_service.py
CHANGED
|
@@ -29,6 +29,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
29
29
|
"configs",
|
|
30
30
|
"cp",
|
|
31
31
|
"disable",
|
|
32
|
+
"disk",
|
|
32
33
|
"doctor",
|
|
33
34
|
"enable",
|
|
34
35
|
"export",
|
|
@@ -48,6 +49,7 @@ RESERVED_SESSION_NAMES = {
|
|
|
48
49
|
"power",
|
|
49
50
|
"ready",
|
|
50
51
|
"repair",
|
|
52
|
+
"reset",
|
|
51
53
|
"resume",
|
|
52
54
|
"ren",
|
|
53
55
|
"rename",
|
|
@@ -260,6 +262,8 @@ def _normalize_status_payload(payload=None):
|
|
|
260
262
|
"remaining_5h_pct": _normalize_pct_value(payload.get("remaining_5h_pct")),
|
|
261
263
|
"remaining_week_pct": _normalize_pct_value(payload.get("remaining_week_pct")),
|
|
262
264
|
"credits": payload.get("credits"),
|
|
265
|
+
"reset_credits_available": payload.get("reset_credits_available"),
|
|
266
|
+
"reset_credits": payload.get("reset_credits"),
|
|
263
267
|
"reset_5h_at": payload.get("reset_5h_at"),
|
|
264
268
|
"reset_week_at": payload.get("reset_week_at"),
|
|
265
269
|
"reset_at": payload.get("reset_at") or payload.get("reset_week_at") or payload.get("reset_5h_at"),
|
|
@@ -337,6 +341,8 @@ def _status_has_more_detail(candidate, current):
|
|
|
337
341
|
"remaining_5h_pct",
|
|
338
342
|
"remaining_week_pct",
|
|
339
343
|
"credits",
|
|
344
|
+
"reset_credits_available",
|
|
345
|
+
"reset_credits",
|
|
340
346
|
"reset_5h_at",
|
|
341
347
|
"reset_week_at",
|
|
342
348
|
"reset_at",
|
|
@@ -358,6 +364,8 @@ def _merge_status_payload(current, candidate):
|
|
|
358
364
|
"remaining_5h_pct",
|
|
359
365
|
"remaining_week_pct",
|
|
360
366
|
"credits",
|
|
367
|
+
"reset_credits_available",
|
|
368
|
+
"reset_credits",
|
|
361
369
|
"reset_5h_at",
|
|
362
370
|
"reset_week_at",
|
|
363
371
|
"reset_at",
|
|
@@ -1101,6 +1109,8 @@ def create_session_service(options=None):
|
|
|
1101
1109
|
"remaining_5h_pct": _normalize_pct_value(row_status.get("remaining_5h_pct")) if row_status else None,
|
|
1102
1110
|
"remaining_week_pct": _normalize_pct_value(row_status.get("remaining_week_pct")) if row_status else None,
|
|
1103
1111
|
"credits": row_status.get("credits") if row_status else None,
|
|
1112
|
+
"reset_credits_available": row_status.get("reset_credits_available") if row_status else None,
|
|
1113
|
+
"reset_credits": row_status.get("reset_credits") if row_status else None,
|
|
1104
1114
|
"available_pct": _compute_available_pct(row_status),
|
|
1105
1115
|
"reset_5h_at": row_status.get("reset_5h_at") if row_status else None,
|
|
1106
1116
|
"reset_week_at": row_status.get("reset_week_at") if row_status else None,
|
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
|