cdx-manager 0.2.1
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/LICENSE +21 -0
- package/README.md +243 -0
- package/bin/cdx +20 -0
- package/changelogs/CHANGELOGS_0_1_1.md +98 -0
- package/changelogs/CHANGELOGS_0_2_0.md +68 -0
- package/changelogs/CHANGELOGS_0_2_1.md +29 -0
- package/package.json +44 -0
- package/src/__init__.py +17 -0
- package/src/claude_refresh.py +72 -0
- package/src/claude_usage.py +84 -0
- package/src/cli.py +188 -0
- package/src/cli_commands.py +369 -0
- package/src/cli_render.py +131 -0
- package/src/config.py +8 -0
- package/src/errors.py +4 -0
- package/src/health.py +125 -0
- package/src/notify.py +138 -0
- package/src/provider_runtime.py +290 -0
- package/src/repair.py +121 -0
- package/src/session_service.py +563 -0
- package/src/session_store.py +244 -0
- package/src/status_source.py +572 -0
- package/src/status_view.py +270 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from .cli_render import (
|
|
4
|
+
_dim,
|
|
5
|
+
_format_pct,
|
|
6
|
+
_format_relative_age,
|
|
7
|
+
_pad_table,
|
|
8
|
+
_style,
|
|
9
|
+
_style_pct,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _format_reset_time(value):
|
|
14
|
+
if not value:
|
|
15
|
+
return "-"
|
|
16
|
+
timestamp = _parse_reset_timestamp(value)
|
|
17
|
+
if timestamp is None:
|
|
18
|
+
return value
|
|
19
|
+
delta_s = timestamp - _now_timestamp()
|
|
20
|
+
if delta_s < 0:
|
|
21
|
+
minutes_ago = int(abs(delta_s) // 60)
|
|
22
|
+
if minutes_ago < 1:
|
|
23
|
+
return "passed"
|
|
24
|
+
if minutes_ago < 60:
|
|
25
|
+
return f"passed {minutes_ago}m ago"
|
|
26
|
+
hours_ago = minutes_ago // 60
|
|
27
|
+
if hours_ago < 24:
|
|
28
|
+
return f"passed {hours_ago}h ago"
|
|
29
|
+
return value
|
|
30
|
+
if delta_s < 60:
|
|
31
|
+
return "now"
|
|
32
|
+
if delta_s < 24 * 60 * 60:
|
|
33
|
+
minutes = int(delta_s // 60)
|
|
34
|
+
hours = minutes // 60
|
|
35
|
+
remaining_minutes = minutes % 60
|
|
36
|
+
if hours == 0:
|
|
37
|
+
return f"in {remaining_minutes}m"
|
|
38
|
+
if remaining_minutes == 0:
|
|
39
|
+
return f"in {hours}h"
|
|
40
|
+
return f"in {hours}h {remaining_minutes}m"
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _style_reset_time(value, use_color=False):
|
|
45
|
+
text = _format_reset_time(value)
|
|
46
|
+
if text == "-":
|
|
47
|
+
return _style(text, "2", use_color)
|
|
48
|
+
if text == "now" or text.startswith("in "):
|
|
49
|
+
return _style(text, "32", use_color)
|
|
50
|
+
if text == "passed" or text.startswith("passed "):
|
|
51
|
+
return _style(text, "31", use_color)
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_status_rows(rows, use_color=False, small=False):
|
|
56
|
+
has_provider = len({r["provider"] for r in rows}) > 1 and not small
|
|
57
|
+
if small:
|
|
58
|
+
headers = ["SESSION", "OK", "5H", "WEEK", "RESET 5H", "RESET WEEK"]
|
|
59
|
+
elif has_provider:
|
|
60
|
+
headers = ["SESSION", "PROV.", "OK", "5H", "WEEK", "BLOCK", "CR", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
61
|
+
else:
|
|
62
|
+
headers = ["SESSION", "OK", "5H", "WEEK", "BLOCK", "CR", "RESET 5H", "RESET WEEK", "UPDATED"]
|
|
63
|
+
if not rows:
|
|
64
|
+
if small:
|
|
65
|
+
return "SESSION OK 5H WEEK RESET 5H RESET WEEK\nNo saved sessions yet."
|
|
66
|
+
return "SESSION OK 5H WEEK BLOCK CR RESET 5H RESET WEEK UPDATED\nNo saved sessions yet."
|
|
67
|
+
headers = [_style(header, "1", use_color) for header in headers]
|
|
68
|
+
priority = _recommend_priority_sessions(rows)
|
|
69
|
+
table_rows = []
|
|
70
|
+
for r in priority:
|
|
71
|
+
base = [r["session_name"]]
|
|
72
|
+
if has_provider:
|
|
73
|
+
base.append(r.get("provider") or "n/a")
|
|
74
|
+
usage_columns = [
|
|
75
|
+
_style_pct(r.get("available_pct"), use_color),
|
|
76
|
+
_style_pct(r.get("remaining_5h_pct"), use_color),
|
|
77
|
+
_style_pct(r.get("remaining_week_pct"), use_color),
|
|
78
|
+
_style_reset_time(r.get("reset_5h_at"), use_color),
|
|
79
|
+
_style_reset_time(r.get("reset_week_at"), use_color),
|
|
80
|
+
]
|
|
81
|
+
if small:
|
|
82
|
+
base += usage_columns
|
|
83
|
+
else:
|
|
84
|
+
block = _format_blocking_quota(r)
|
|
85
|
+
credits = str(r["credits"]) if r.get("credits") is not None else "-"
|
|
86
|
+
base += usage_columns[:3] + [
|
|
87
|
+
_style(block, "33" if block not in ("?", "-") else "2", use_color),
|
|
88
|
+
_style(credits, "33" if r.get("credits") is not None else "2", use_color),
|
|
89
|
+
*usage_columns[3:],
|
|
90
|
+
_style(_format_relative_age(r.get("updated_at")), "2", use_color),
|
|
91
|
+
]
|
|
92
|
+
table_rows.append(base)
|
|
93
|
+
priority_line = (
|
|
94
|
+
f"Priority: {_priority_instruction(priority[0], 'first')}"
|
|
95
|
+
+ (
|
|
96
|
+
f", {_priority_instruction(priority[1], 'next')}."
|
|
97
|
+
if len(priority) > 1 else "."
|
|
98
|
+
)
|
|
99
|
+
) if priority else "Priority: no usable session status yet."
|
|
100
|
+
return "\n".join([
|
|
101
|
+
_pad_table([headers] + table_rows),
|
|
102
|
+
"",
|
|
103
|
+
_style(priority_line, "1", use_color),
|
|
104
|
+
_style("Tip: run /status in codex to refresh. Claude sessions auto-refresh; use --refresh to force.", "2", use_color),
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _recommend_priority_sessions(rows):
|
|
109
|
+
if not rows:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
def rank(row):
|
|
113
|
+
has_credits = row.get("credits") is not None
|
|
114
|
+
credit_rank = 0 if has_credits else 1
|
|
115
|
+
available = row.get("available_pct")
|
|
116
|
+
usable_now = available is not None and available > 0
|
|
117
|
+
known_available = available is not None
|
|
118
|
+
reset_timestamp = _priority_reset_timestamp(row)
|
|
119
|
+
reset_is_future = reset_timestamp is not None and reset_timestamp >= _now_timestamp()
|
|
120
|
+
blocked_future = not usable_now and reset_is_future
|
|
121
|
+
reset_is_known = reset_timestamp is not None
|
|
122
|
+
reset_rank = -reset_timestamp if reset_is_known else float("-inf")
|
|
123
|
+
available_rank = available if available is not None else -1
|
|
124
|
+
name_rank = row.get("session_name") or ""
|
|
125
|
+
if usable_now:
|
|
126
|
+
return (3, credit_rank, 1 if known_available else 0, available_rank, reset_rank, name_rank)
|
|
127
|
+
if blocked_future:
|
|
128
|
+
return (2, 1 if reset_is_known else 0, reset_rank, credit_rank, available_rank, name_rank)
|
|
129
|
+
if reset_is_known:
|
|
130
|
+
return (1, reset_rank, credit_rank, 1 if known_available else 0, available_rank, name_rank)
|
|
131
|
+
return (0, credit_rank, 1 if known_available else 0, available_rank, name_rank)
|
|
132
|
+
|
|
133
|
+
return sorted(rows, key=rank, reverse=True)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _format_blocking_quota(row):
|
|
137
|
+
remaining_5h = row.get("remaining_5h_pct")
|
|
138
|
+
remaining_week = row.get("remaining_week_pct")
|
|
139
|
+
if remaining_5h is None and remaining_week is None:
|
|
140
|
+
return "?"
|
|
141
|
+
if remaining_5h is None:
|
|
142
|
+
return "WEEK"
|
|
143
|
+
if remaining_week is None:
|
|
144
|
+
return "5H"
|
|
145
|
+
if remaining_5h < remaining_week:
|
|
146
|
+
return "5H"
|
|
147
|
+
if remaining_week < remaining_5h:
|
|
148
|
+
return "WEEK"
|
|
149
|
+
return "5H+WEEK"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _priority_instruction(row, position):
|
|
153
|
+
action = "refresh" if _priority_needs_refresh(row) else "use"
|
|
154
|
+
if position == "next" and action == "use":
|
|
155
|
+
return f"next {row['session_name']} ({_priority_reason(row)})"
|
|
156
|
+
return f"{action} {row['session_name']} {position} ({_priority_reason(row)})"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _priority_needs_refresh(row):
|
|
160
|
+
available = row.get("available_pct")
|
|
161
|
+
if available is None or available > 0:
|
|
162
|
+
return False
|
|
163
|
+
_label, is_past = _priority_reset_info(row)
|
|
164
|
+
return is_past
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _priority_reason(row):
|
|
168
|
+
available = row.get("available_pct")
|
|
169
|
+
if available is None:
|
|
170
|
+
return "status unknown"
|
|
171
|
+
if available > 0:
|
|
172
|
+
return f"{_format_pct(available)} OK"
|
|
173
|
+
label, is_past = _priority_reset_info(row)
|
|
174
|
+
if label:
|
|
175
|
+
if is_past:
|
|
176
|
+
return f"0% OK, {label} reset passed"
|
|
177
|
+
return f"0% OK, {label} resets first"
|
|
178
|
+
return "0% OK"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _priority_reset_info(row):
|
|
182
|
+
remaining_5h = row.get("remaining_5h_pct")
|
|
183
|
+
remaining_week = row.get("remaining_week_pct")
|
|
184
|
+
candidates = []
|
|
185
|
+
if remaining_5h is not None:
|
|
186
|
+
candidates.append((remaining_5h, "5H", row.get("reset_5h_at")))
|
|
187
|
+
if remaining_week is not None:
|
|
188
|
+
candidates.append((remaining_week, "WEEK", row.get("reset_week_at")))
|
|
189
|
+
if not candidates:
|
|
190
|
+
return None
|
|
191
|
+
lowest_remaining = min(value for value, _label, _reset in candidates)
|
|
192
|
+
blocked = [
|
|
193
|
+
(label, reset)
|
|
194
|
+
for value, label, reset in candidates
|
|
195
|
+
if value == lowest_remaining and reset
|
|
196
|
+
]
|
|
197
|
+
timestamps = [
|
|
198
|
+
(timestamp, label)
|
|
199
|
+
for label, reset in blocked
|
|
200
|
+
for timestamp in [_parse_reset_timestamp(reset)]
|
|
201
|
+
if timestamp is not None
|
|
202
|
+
]
|
|
203
|
+
if timestamps:
|
|
204
|
+
timestamp, label = min(timestamps)
|
|
205
|
+
return label, timestamp < _now_timestamp()
|
|
206
|
+
if blocked:
|
|
207
|
+
return blocked[0][0], False
|
|
208
|
+
return None, False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _priority_reset_timestamp(row):
|
|
212
|
+
remaining_5h = row.get("remaining_5h_pct")
|
|
213
|
+
remaining_week = row.get("remaining_week_pct")
|
|
214
|
+
candidates = []
|
|
215
|
+
if remaining_5h is not None:
|
|
216
|
+
candidates.append((remaining_5h, row.get("reset_5h_at")))
|
|
217
|
+
if remaining_week is not None:
|
|
218
|
+
candidates.append((remaining_week, row.get("reset_week_at")))
|
|
219
|
+
if not candidates:
|
|
220
|
+
return None
|
|
221
|
+
lowest_remaining = min(value for value, _reset in candidates)
|
|
222
|
+
reset_values = [_reset for value, _reset in candidates if value == lowest_remaining]
|
|
223
|
+
timestamps = [
|
|
224
|
+
timestamp
|
|
225
|
+
for timestamp in (_parse_reset_timestamp(reset_value) for reset_value in reset_values)
|
|
226
|
+
if timestamp is not None
|
|
227
|
+
]
|
|
228
|
+
if not timestamps:
|
|
229
|
+
return None
|
|
230
|
+
return min(timestamps)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_reset_timestamp(value):
|
|
234
|
+
if not value:
|
|
235
|
+
return None
|
|
236
|
+
text = str(value).strip()
|
|
237
|
+
try:
|
|
238
|
+
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
239
|
+
if parsed.tzinfo is None:
|
|
240
|
+
parsed = parsed.replace(tzinfo=datetime.now().astimezone().tzinfo)
|
|
241
|
+
return parsed.timestamp()
|
|
242
|
+
except (TypeError, ValueError):
|
|
243
|
+
pass
|
|
244
|
+
try:
|
|
245
|
+
parsed = datetime.strptime(text, "%b %d %H:%M")
|
|
246
|
+
except (TypeError, ValueError):
|
|
247
|
+
return None
|
|
248
|
+
now = datetime.now().astimezone()
|
|
249
|
+
parsed = parsed.replace(year=now.year, tzinfo=now.tzinfo)
|
|
250
|
+
return parsed.timestamp()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _now_timestamp():
|
|
254
|
+
return datetime.now().astimezone().timestamp()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _format_status_detail(row, use_color=False):
|
|
258
|
+
lines = [
|
|
259
|
+
f"{_style('Session:', '1', use_color)} {row['session_name']}",
|
|
260
|
+
f"{_style('Provider:', '1', use_color)} {row.get('provider') or 'n/a'}",
|
|
261
|
+
f"{_style('Available:', '1', use_color)} {_style_pct(row.get('available_pct'), use_color)}",
|
|
262
|
+
f"{_style('5h left:', '1', use_color)} {_style_pct(row.get('remaining_5h_pct'), use_color)}",
|
|
263
|
+
f"{_style('Week left:', '1', use_color)} {_style_pct(row.get('remaining_week_pct'), use_color)}",
|
|
264
|
+
f"{_style('Block:', '1', use_color)} {_style(_format_blocking_quota(row), '33', use_color)}",
|
|
265
|
+
f"{_style('Credits:', '1', use_color)} {_style(row['credits'] if row.get('credits') is not None else 'n/a', '33' if row.get('credits') is not None else '2', use_color)}",
|
|
266
|
+
f"{_style('5h reset:', '1', use_color)} {_style_reset_time(row.get('reset_5h_at'), use_color)}",
|
|
267
|
+
f"{_style('Week reset:', '1', use_color)} {_style_reset_time(row.get('reset_week_at'), use_color)}",
|
|
268
|
+
f"{_style('Updated:', '1', use_color)} {_dim(_format_relative_age(row.get('updated_at')), use_color)}",
|
|
269
|
+
]
|
|
270
|
+
return "\n".join(lines)
|