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.
@@ -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": "account/rateLimits/read",
208
- "params": None,
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": "rate_limits_read_failed",
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": status, "response": response}
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")}
@@ -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 FileNotFoundError as error:
927
+ except OSError as error:
919
928
  raise _format_probe_failure(session, spec, error) from error
920
929
  return spec["parser"](output)
921
930
 
@@ -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,
@@ -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)] * 5
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