cdx-manager 0.5.3 → 0.5.4
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 +2 -2
- package/changelogs/CHANGELOGS_0_5_4.md +49 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +36 -19
- package/src/cli.py +1 -1
- package/src/cli_commands.py +112 -153
- 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 +17 -10
- package/src/status_source.py +22 -20
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -126,7 +126,7 @@ For a specific version:
|
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
|
|
129
|
-
CDX_VERSION=v0.5.
|
|
129
|
+
CDX_VERSION=v0.5.4 sh install.sh
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
From source:
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog (`0.5.3 -> 0.5.4`)
|
|
2
|
+
|
|
3
|
+
Release date: 2026-05-23
|
|
4
|
+
|
|
5
|
+
## Major Highlights
|
|
6
|
+
|
|
7
|
+
- Added a Logics ADR for the code quality, security, performance, and test coverage improvements completed after `0.5.3`.
|
|
8
|
+
- Hardened bundle import path handling and launch prompt validation without changing CLI contracts or on-disk formats.
|
|
9
|
+
- Improved status parsing and provider handling internals for safer future maintenance.
|
|
10
|
+
- Added structured Codex app-server probe diagnostics while keeping the existing status API behavior compatible.
|
|
11
|
+
|
|
12
|
+
## Security and Runtime Hardening
|
|
13
|
+
|
|
14
|
+
- Hardened bundle relative path validation against traversal, absolute paths, and Windows drive paths.
|
|
15
|
+
- Validated provider launch `initial_prompt` type and length before it is passed into subprocess arguments.
|
|
16
|
+
- Replaced mutable signal-capture state with explicit `nonlocal` signal forwarding.
|
|
17
|
+
- Added Linux `notify-send` support with backend timeout handling and shell-free notification arguments.
|
|
18
|
+
|
|
19
|
+
## Status and Provider Internals
|
|
20
|
+
|
|
21
|
+
- Moved status extraction regex compilation to module-level constants.
|
|
22
|
+
- Added shared provider constants for Codex and Claude identity checks.
|
|
23
|
+
- Improved Claude status refresh handling for sync and async refresh functions without per-thread event loops.
|
|
24
|
+
- Added `fetch_codex_rate_limit_diagnostic()` to expose structured failure reasons such as missing auth home, initialize failure, app-server read failure, missing rate limits, and missing Codex CLI.
|
|
25
|
+
|
|
26
|
+
## CLI Parser Maintenance
|
|
27
|
+
|
|
28
|
+
- Factorized more CLI flag parsing through a shared flat-flag parser.
|
|
29
|
+
- Added `--flag=value` support where appropriate, including `cdx notify --poll=<seconds>`.
|
|
30
|
+
- Preserved existing usage errors and JSON/non-JSON output contracts.
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
- Added `logics/architecture/adr_001_code_quality_and_security_improvements.md` documenting the security, test, quality, and performance decisions.
|
|
35
|
+
- Updated package metadata, CLI version output, README badge, and pinned installer example to `v0.5.4`.
|
|
36
|
+
|
|
37
|
+
## Validation and Regression Coverage
|
|
38
|
+
|
|
39
|
+
- Added direct session-store rollback tests for add, remove, rename, and replace failure paths.
|
|
40
|
+
- Added status-source tests for key-value, Codex block, Claude block, artifact selection, reset timestamp parsing, and safe relative paths.
|
|
41
|
+
- Added notification tests for macOS, Linux, backend fallback, timeout, and argument handling.
|
|
42
|
+
- Added CLI parser tests for update, status, repair, export/import subsets, corrupt bundles, and clean behavior with and without logs.
|
|
43
|
+
- Added Codex app-server diagnostic tests for success and failure cases.
|
|
44
|
+
|
|
45
|
+
## Validation and Regression Evidence
|
|
46
|
+
|
|
47
|
+
- `npm test`
|
|
48
|
+
- `npm run lint`
|
|
49
|
+
- `python logics/skills/logics.py lint --require-status`
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/claude_refresh.py
CHANGED
|
@@ -3,6 +3,7 @@ import threading
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
|
|
5
5
|
from .claude_usage import refresh_claude_session_status
|
|
6
|
+
from .config import PROVIDER_CLAUDE
|
|
6
7
|
from .errors import CdxError
|
|
7
8
|
|
|
8
9
|
CLAUDE_REFRESH_TTL_SECONDS = 10 * 60
|
|
@@ -35,7 +36,7 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
35
36
|
sessions = service["list_sessions"]()
|
|
36
37
|
claude_sessions = [
|
|
37
38
|
s for s in sessions
|
|
38
|
-
if s["provider"] ==
|
|
39
|
+
if s["provider"] == PROVIDER_CLAUDE
|
|
39
40
|
and s.get("enabled", True) is not False
|
|
40
41
|
and (not target_names or s["name"] in target_names)
|
|
41
42
|
and (force or _is_stale(s, ttl_seconds=ttl_seconds))
|
|
@@ -45,25 +46,41 @@ def _refresh_claude_sessions(service, refresh_fn=None, target_names=None, force=
|
|
|
45
46
|
|
|
46
47
|
errors = []
|
|
47
48
|
results = {}
|
|
48
|
-
threads = []
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
if inspect.iscoroutinefunction(refresh_fn):
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
async def fetch_all():
|
|
54
|
+
async def fetch_async(s):
|
|
55
|
+
try:
|
|
56
|
+
usage = await refresh_fn(s)
|
|
57
|
+
if usage:
|
|
58
|
+
results[s["name"]] = usage
|
|
59
|
+
except Exception as e:
|
|
60
|
+
errors.append({"session": s["name"], "error": e})
|
|
61
|
+
|
|
62
|
+
await asyncio.gather(*(fetch_async(s) for s in claude_sessions))
|
|
63
|
+
|
|
64
|
+
asyncio.run(fetch_all())
|
|
65
|
+
else:
|
|
66
|
+
threads = []
|
|
67
|
+
|
|
68
|
+
def fetch(s):
|
|
69
|
+
try:
|
|
70
|
+
usage = refresh_fn(s)
|
|
71
|
+
if inspect.isawaitable(usage):
|
|
72
|
+
raise CdxError("Claude refresh function returned an awaitable from a sync callable.")
|
|
73
|
+
if usage:
|
|
74
|
+
results[s["name"]] = usage
|
|
75
|
+
except Exception as e:
|
|
76
|
+
errors.append({"session": s["name"], "error": e})
|
|
77
|
+
|
|
78
|
+
for s in claude_sessions:
|
|
79
|
+
t = threading.Thread(target=fetch, args=(s,), daemon=True)
|
|
80
|
+
threads.append(t)
|
|
81
|
+
t.start()
|
|
82
|
+
for t in threads:
|
|
83
|
+
t.join(timeout=10)
|
|
67
84
|
|
|
68
85
|
for name, usage in results.items():
|
|
69
86
|
try:
|
package/src/cli.py
CHANGED
package/src/cli_commands.py
CHANGED
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
|
|
7
7
|
|
|
8
8
|
from .claude_refresh import _refresh_claude_sessions
|
|
9
9
|
from .cli_render import _dim, _info, _success, _warn
|
|
10
|
+
from .config import PROVIDER_CODEX
|
|
10
11
|
from .context_store import (
|
|
11
12
|
clear_context,
|
|
12
13
|
edit_context,
|
|
@@ -112,7 +113,7 @@ def _build_handoff_context(source, target, transcript_path, transcript, truncate
|
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
def _handoff_launch_prompt(session, install=None):
|
|
115
|
-
if session.get("provider") ==
|
|
116
|
+
if session.get("provider") == PROVIDER_CODEX:
|
|
116
117
|
context_ref = "$CODEX_HOME/shared-context.md"
|
|
117
118
|
else:
|
|
118
119
|
context_ref = (install or {}).get("target_path") or os.path.join(
|
|
@@ -131,11 +132,54 @@ def _parse_json_flag(args):
|
|
|
131
132
|
return json_flag, cleaned
|
|
132
133
|
|
|
133
134
|
|
|
135
|
+
def _parse_flag_args(args, schema, usage, positionals_key=None, max_positionals=0):
|
|
136
|
+
parsed = {spec["key"]: spec.get("default") for spec in schema.values()}
|
|
137
|
+
positionals = []
|
|
138
|
+
index = 0
|
|
139
|
+
while index < len(args):
|
|
140
|
+
arg = args[index]
|
|
141
|
+
if arg in schema:
|
|
142
|
+
spec = schema[arg]
|
|
143
|
+
if spec["type"] == "bool":
|
|
144
|
+
parsed[spec["key"]] = True
|
|
145
|
+
index += 1
|
|
146
|
+
continue
|
|
147
|
+
value, index = _read_option_value(args, index, usage)
|
|
148
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
149
|
+
continue
|
|
150
|
+
if arg.startswith("--") and "=" in arg:
|
|
151
|
+
flag, value = arg.split("=", 1)
|
|
152
|
+
spec = schema.get(flag)
|
|
153
|
+
if not spec or spec["type"] == "bool":
|
|
154
|
+
raise CdxError(usage)
|
|
155
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
156
|
+
index += 1
|
|
157
|
+
continue
|
|
158
|
+
if arg.startswith("-"):
|
|
159
|
+
raise CdxError(usage)
|
|
160
|
+
positionals.append(arg)
|
|
161
|
+
if len(positionals) > max_positionals:
|
|
162
|
+
raise CdxError(usage)
|
|
163
|
+
index += 1
|
|
164
|
+
|
|
165
|
+
if positionals_key is not None:
|
|
166
|
+
parsed[positionals_key] = positionals
|
|
167
|
+
return parsed
|
|
168
|
+
|
|
169
|
+
|
|
134
170
|
def _parse_add_args(args):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
parsed = _parse_flag_args(
|
|
172
|
+
args,
|
|
173
|
+
{},
|
|
174
|
+
"Usage: cdx add [provider] <name> [--json]",
|
|
175
|
+
positionals_key="values",
|
|
176
|
+
max_positionals=2,
|
|
177
|
+
)
|
|
178
|
+
values = parsed["values"]
|
|
179
|
+
if len(values) == 1:
|
|
180
|
+
return {"provider": PROVIDER_CODEX, "name": values[0]}
|
|
181
|
+
if len(values) == 2:
|
|
182
|
+
return {"provider": values[0], "name": values[1]}
|
|
139
183
|
raise CdxError("Usage: cdx add [provider] <name> [--json]")
|
|
140
184
|
|
|
141
185
|
|
|
@@ -152,19 +196,21 @@ def _parse_rename_args(args):
|
|
|
152
196
|
|
|
153
197
|
|
|
154
198
|
def _parse_remove_args(args):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if
|
|
199
|
+
parsed = _parse_flag_args(args, {
|
|
200
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
201
|
+
}, "Usage: cdx rmv <name> [--force] [--json]", positionals_key="names", max_positionals=1)
|
|
202
|
+
if len(parsed["names"]) != 1:
|
|
159
203
|
raise CdxError("Usage: cdx rmv <name> [--force] [--json]")
|
|
160
|
-
return {"name": names[0], "force": force}
|
|
204
|
+
return {"name": parsed["names"][0], "force": parsed["force"]}
|
|
161
205
|
|
|
162
206
|
|
|
163
207
|
def _parse_toggle_args(args, usage):
|
|
164
|
-
|
|
165
|
-
|
|
208
|
+
parsed = _parse_flag_args(args, {
|
|
209
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
210
|
+
}, usage, positionals_key="names", max_positionals=1)
|
|
211
|
+
if len(parsed["names"]) != 1:
|
|
166
212
|
raise CdxError(usage)
|
|
167
|
-
return {"name":
|
|
213
|
+
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
168
214
|
|
|
169
215
|
|
|
170
216
|
def _read_option_value(args, index, usage):
|
|
@@ -183,37 +229,12 @@ def _parse_session_names(value):
|
|
|
183
229
|
|
|
184
230
|
|
|
185
231
|
def _parse_update_args(args):
|
|
186
|
-
parsed = {
|
|
187
|
-
"check": False,
|
|
188
|
-
"json": False,
|
|
189
|
-
"yes": False,
|
|
190
|
-
"version": None,
|
|
191
|
-
}
|
|
192
|
-
index = 0
|
|
193
|
-
while index < len(args):
|
|
194
|
-
arg = args[index]
|
|
195
|
-
if arg == "--check":
|
|
196
|
-
parsed["check"] = True
|
|
197
|
-
index += 1
|
|
198
|
-
continue
|
|
199
|
-
if arg == "--json":
|
|
200
|
-
parsed["json"] = True
|
|
201
|
-
index += 1
|
|
202
|
-
continue
|
|
203
|
-
if arg == "--yes":
|
|
204
|
-
parsed["yes"] = True
|
|
205
|
-
index += 1
|
|
206
|
-
continue
|
|
207
|
-
if arg == "--version":
|
|
208
|
-
value, index = _read_option_value(args, index, UPDATE_USAGE)
|
|
209
|
-
parsed["version"] = value
|
|
210
|
-
continue
|
|
211
|
-
if arg.startswith("--version="):
|
|
212
|
-
parsed["version"] = arg.split("=", 1)[1]
|
|
213
|
-
index += 1
|
|
214
|
-
continue
|
|
215
|
-
raise CdxError(UPDATE_USAGE)
|
|
216
|
-
|
|
232
|
+
parsed = _parse_flag_args(args, {
|
|
233
|
+
"--check": {"key": "check", "type": "bool", "default": False},
|
|
234
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
235
|
+
"--yes": {"key": "yes", "type": "bool", "default": False},
|
|
236
|
+
"--version": {"key": "version", "type": "str", "default": None},
|
|
237
|
+
}, UPDATE_USAGE)
|
|
217
238
|
if parsed["check"] and parsed["version"]:
|
|
218
239
|
raise CdxError("Usage: cdx update --check cannot be combined with --version.")
|
|
219
240
|
if parsed["version"] is not None and not parsed["version"].strip():
|
|
@@ -222,52 +243,19 @@ def _parse_update_args(args):
|
|
|
222
243
|
|
|
223
244
|
|
|
224
245
|
def _parse_export_args(args):
|
|
225
|
-
parsed = {
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
index += 1
|
|
239
|
-
continue
|
|
240
|
-
if arg == "--force":
|
|
241
|
-
parsed["force"] = True
|
|
242
|
-
index += 1
|
|
243
|
-
continue
|
|
244
|
-
if arg == "--json":
|
|
245
|
-
parsed["json"] = True
|
|
246
|
-
index += 1
|
|
247
|
-
continue
|
|
248
|
-
if arg == "--sessions":
|
|
249
|
-
value, index = _read_option_value(args, index, EXPORT_USAGE)
|
|
250
|
-
parsed["session_names"] = _parse_session_names(value)
|
|
251
|
-
continue
|
|
252
|
-
if arg.startswith("--sessions="):
|
|
253
|
-
parsed["session_names"] = _parse_session_names(arg.split("=", 1)[1])
|
|
254
|
-
index += 1
|
|
255
|
-
continue
|
|
256
|
-
if arg == "--passphrase-env":
|
|
257
|
-
value, index = _read_option_value(args, index, EXPORT_USAGE)
|
|
258
|
-
parsed["passphrase_env"] = value
|
|
259
|
-
continue
|
|
260
|
-
if arg.startswith("--passphrase-env="):
|
|
261
|
-
parsed["passphrase_env"] = arg.split("=", 1)[1]
|
|
262
|
-
index += 1
|
|
263
|
-
continue
|
|
264
|
-
if arg.startswith("-"):
|
|
265
|
-
raise CdxError(EXPORT_USAGE)
|
|
266
|
-
if parsed["file_path"] is not None:
|
|
267
|
-
raise CdxError(EXPORT_USAGE)
|
|
268
|
-
parsed["file_path"] = arg
|
|
269
|
-
index += 1
|
|
270
|
-
|
|
246
|
+
parsed = _parse_flag_args(args, {
|
|
247
|
+
"--include-auth": {"key": "include_auth", "type": "bool", "default": False},
|
|
248
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
249
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
250
|
+
"--sessions": {
|
|
251
|
+
"key": "session_names",
|
|
252
|
+
"type": "str",
|
|
253
|
+
"default": None,
|
|
254
|
+
"transform": _parse_session_names,
|
|
255
|
+
},
|
|
256
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
257
|
+
}, EXPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
258
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
271
259
|
if not parsed["file_path"]:
|
|
272
260
|
raise CdxError(EXPORT_USAGE)
|
|
273
261
|
if parsed["passphrase_env"] and not parsed["include_auth"]:
|
|
@@ -276,47 +264,18 @@ def _parse_export_args(args):
|
|
|
276
264
|
|
|
277
265
|
|
|
278
266
|
def _parse_import_args(args):
|
|
279
|
-
parsed = {
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
index += 1
|
|
292
|
-
continue
|
|
293
|
-
if arg == "--json":
|
|
294
|
-
parsed["json"] = True
|
|
295
|
-
index += 1
|
|
296
|
-
continue
|
|
297
|
-
if arg == "--sessions":
|
|
298
|
-
value, index = _read_option_value(args, index, IMPORT_USAGE)
|
|
299
|
-
parsed["session_names"] = _parse_session_names(value)
|
|
300
|
-
continue
|
|
301
|
-
if arg.startswith("--sessions="):
|
|
302
|
-
parsed["session_names"] = _parse_session_names(arg.split("=", 1)[1])
|
|
303
|
-
index += 1
|
|
304
|
-
continue
|
|
305
|
-
if arg == "--passphrase-env":
|
|
306
|
-
value, index = _read_option_value(args, index, IMPORT_USAGE)
|
|
307
|
-
parsed["passphrase_env"] = value
|
|
308
|
-
continue
|
|
309
|
-
if arg.startswith("--passphrase-env="):
|
|
310
|
-
parsed["passphrase_env"] = arg.split("=", 1)[1]
|
|
311
|
-
index += 1
|
|
312
|
-
continue
|
|
313
|
-
if arg.startswith("-"):
|
|
314
|
-
raise CdxError(IMPORT_USAGE)
|
|
315
|
-
if parsed["file_path"] is not None:
|
|
316
|
-
raise CdxError(IMPORT_USAGE)
|
|
317
|
-
parsed["file_path"] = arg
|
|
318
|
-
index += 1
|
|
319
|
-
|
|
267
|
+
parsed = _parse_flag_args(args, {
|
|
268
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
269
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
270
|
+
"--sessions": {
|
|
271
|
+
"key": "session_names",
|
|
272
|
+
"type": "str",
|
|
273
|
+
"default": None,
|
|
274
|
+
"transform": _parse_session_names,
|
|
275
|
+
},
|
|
276
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
277
|
+
}, IMPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
278
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
320
279
|
if not parsed["file_path"]:
|
|
321
280
|
raise CdxError(IMPORT_USAGE)
|
|
322
281
|
return parsed
|
|
@@ -558,21 +517,20 @@ def handle_doctor(rest, ctx):
|
|
|
558
517
|
|
|
559
518
|
|
|
560
519
|
def handle_repair(rest, ctx):
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
raise CdxError(REPAIR_USAGE)
|
|
520
|
+
parsed = _parse_flag_args(rest, {
|
|
521
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
522
|
+
"--dry-run": {"key": "dry_run", "type": "bool", "default": False},
|
|
523
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
524
|
+
}, REPAIR_USAGE)
|
|
525
|
+
dry_run = parsed["dry_run"] or not parsed["force"]
|
|
568
526
|
report = repair_health(
|
|
569
527
|
ctx["service"],
|
|
570
528
|
ctx["service"]["base_dir"],
|
|
571
529
|
env=ctx.get("env"),
|
|
572
530
|
dry_run=dry_run,
|
|
573
|
-
force=force,
|
|
531
|
+
force=parsed["force"],
|
|
574
532
|
)
|
|
575
|
-
if
|
|
533
|
+
if parsed["json"]:
|
|
576
534
|
_write_json(ctx, _json_success("repair", "Collected repair report", report=report))
|
|
577
535
|
else:
|
|
578
536
|
ctx["out"](f"{format_repair_report(report, use_color=ctx['use_color'])}\n")
|
|
@@ -704,22 +662,23 @@ def handle_context(rest, ctx):
|
|
|
704
662
|
|
|
705
663
|
|
|
706
664
|
def handle_status(rest, ctx):
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if
|
|
665
|
+
parsed = _parse_flag_args(rest, {
|
|
666
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
667
|
+
"--small": {"key": "small", "type": "bool", "default": False},
|
|
668
|
+
"-s": {"key": "small", "type": "bool", "default": False},
|
|
669
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
670
|
+
}, STATUS_USAGE, positionals_key="args", max_positionals=1)
|
|
671
|
+
if parsed["json"] and parsed["small"]:
|
|
714
672
|
raise CdxError(STATUS_USAGE)
|
|
715
|
-
|
|
673
|
+
args = parsed["args"]
|
|
674
|
+
if len(args) == 1 and parsed["small"]:
|
|
716
675
|
raise CdxError(STATUS_USAGE)
|
|
717
676
|
|
|
718
677
|
refresh_result = _refresh_claude_sessions(
|
|
719
678
|
ctx["service"],
|
|
720
679
|
ctx.get("refresh_fn"),
|
|
721
680
|
target_names=args if len(args) == 1 else None,
|
|
722
|
-
force=
|
|
681
|
+
force=parsed["refresh"],
|
|
723
682
|
)
|
|
724
683
|
refresh_errors = [
|
|
725
684
|
{
|
|
@@ -739,17 +698,17 @@ def handle_status(rest, ctx):
|
|
|
739
698
|
|
|
740
699
|
rows = ctx["service"]["get_status_rows"]()
|
|
741
700
|
if len(args) == 0:
|
|
742
|
-
if
|
|
701
|
+
if parsed["json"]:
|
|
743
702
|
_write_json(ctx, _json_success("status", "Collected session status rows", warnings=warnings, rows=rows))
|
|
744
703
|
return 0
|
|
745
|
-
ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=
|
|
704
|
+
ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=parsed['small'])}\n")
|
|
746
705
|
_write_refresh_warnings(refresh_errors, ctx)
|
|
747
706
|
return 0
|
|
748
707
|
|
|
749
708
|
row = next((r for r in rows if r["session_name"] == args[0]), None)
|
|
750
709
|
if not row:
|
|
751
710
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
752
|
-
if
|
|
711
|
+
if parsed["json"]:
|
|
753
712
|
_write_json(ctx, _json_success("status", f"Collected status for {args[0]}", warnings=warnings, session=row))
|
|
754
713
|
return 0
|
|
755
714
|
ctx["out"](f"{_format_status_detail(row, use_color=ctx['use_color'])}\n")
|
|
@@ -1002,7 +961,7 @@ def handle_launch(command, ctx, initial_prompt=None):
|
|
|
1002
961
|
if update_notice.get("url"):
|
|
1003
962
|
text = f"{text} {update_notice['url']}"
|
|
1004
963
|
ctx["out"](f"{_warn(text, ctx['use_color'])}\n")
|
|
1005
|
-
if session["provider"] ==
|
|
964
|
+
if session["provider"] == PROVIDER_CODEX:
|
|
1006
965
|
if not json_flag:
|
|
1007
966
|
ctx["out"](f"{_dim('Tip: cdx status reads Codex rate limits from the local app-server when available.', ctx['use_color'])}\n")
|
|
1008
967
|
_run_interactive_provider_command(
|
package/src/codex_usage.py
CHANGED
|
@@ -106,10 +106,10 @@ def _write_json_line(process, payload):
|
|
|
106
106
|
process.stdin.flush()
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def
|
|
109
|
+
def fetch_codex_rate_limit_diagnostic(session, timeout=5, popen_factory=None):
|
|
110
110
|
auth_home = session.get("authHome")
|
|
111
111
|
if not auth_home:
|
|
112
|
-
return None
|
|
112
|
+
return {"ok": False, "reason": "missing_auth_home", "status": None}
|
|
113
113
|
|
|
114
114
|
env = os.environ.copy()
|
|
115
115
|
env["CODEX_HOME"] = auth_home
|
|
@@ -139,7 +139,12 @@ def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
|
139
139
|
})
|
|
140
140
|
initialized = _read_response(output, 1, timeout)
|
|
141
141
|
if not initialized or initialized.get("error"):
|
|
142
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
"ok": False,
|
|
144
|
+
"reason": "initialize_failed",
|
|
145
|
+
"status": None,
|
|
146
|
+
"response": initialized,
|
|
147
|
+
}
|
|
143
148
|
|
|
144
149
|
_write_json_line(process, {
|
|
145
150
|
"jsonrpc": "2.0",
|
|
@@ -149,13 +154,28 @@ def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
|
149
154
|
})
|
|
150
155
|
response = _read_response(output, 2, timeout)
|
|
151
156
|
if not response or response.get("error"):
|
|
152
|
-
return
|
|
157
|
+
return {
|
|
158
|
+
"ok": False,
|
|
159
|
+
"reason": "rate_limits_read_failed",
|
|
160
|
+
"status": None,
|
|
161
|
+
"response": response,
|
|
162
|
+
}
|
|
153
163
|
result = response.get("result") or {}
|
|
154
164
|
by_limit = result.get("rateLimitsByLimitId") or {}
|
|
155
165
|
snapshot = by_limit.get("codex") or result.get("rateLimits")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
status = normalize_codex_rate_limit_snapshot(snapshot)
|
|
167
|
+
if not status:
|
|
168
|
+
return {
|
|
169
|
+
"ok": False,
|
|
170
|
+
"reason": "missing_rate_limits",
|
|
171
|
+
"status": None,
|
|
172
|
+
"response": response,
|
|
173
|
+
}
|
|
174
|
+
return {"ok": True, "reason": None, "status": status, "response": response}
|
|
175
|
+
except FileNotFoundError as error:
|
|
176
|
+
return {"ok": False, "reason": "codex_cli_not_found", "status": None, "error": str(error)}
|
|
177
|
+
except (OSError, ValueError, BrokenPipeError) as error:
|
|
178
|
+
return {"ok": False, "reason": "probe_failed", "status": None, "error": str(error)}
|
|
159
179
|
finally:
|
|
160
180
|
if process is not None:
|
|
161
181
|
try:
|
|
@@ -166,3 +186,12 @@ def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
|
166
186
|
process.kill()
|
|
167
187
|
except OSError:
|
|
168
188
|
pass
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def fetch_codex_rate_limits(session, timeout=5, popen_factory=None):
|
|
192
|
+
diagnostic = fetch_codex_rate_limit_diagnostic(
|
|
193
|
+
session,
|
|
194
|
+
timeout=timeout,
|
|
195
|
+
popen_factory=popen_factory,
|
|
196
|
+
)
|
|
197
|
+
return diagnostic.get("status") if diagnostic.get("ok") else None
|
package/src/config.py
CHANGED
package/src/notify.py
CHANGED
|
@@ -29,6 +29,13 @@ def parse_notify_args(args):
|
|
|
29
29
|
raise CdxError("--poll must be a number of seconds") from error
|
|
30
30
|
i += 2
|
|
31
31
|
continue
|
|
32
|
+
if arg.startswith("--poll="):
|
|
33
|
+
try:
|
|
34
|
+
poll = max(1, int(arg.split("=", 1)[1]))
|
|
35
|
+
except ValueError as error:
|
|
36
|
+
raise CdxError("--poll must be a number of seconds") from error
|
|
37
|
+
i += 1
|
|
38
|
+
continue
|
|
32
39
|
if arg.startswith("-"):
|
|
33
40
|
raise CdxError("Usage: cdx notify <name> --at-reset [--poll seconds] [--once] | cdx notify --next-ready [--poll seconds] [--once]")
|
|
34
41
|
cleaned.append(arg)
|
|
@@ -121,7 +128,24 @@ def send_desktop_notification(title, message, spawn_sync=None, env=None):
|
|
|
121
128
|
_send_windows_notification(title, message, spawn_sync, env)
|
|
122
129
|
elif shutil_which("osascript", env):
|
|
123
130
|
script = f'display notification "{_escape_applescript(message)}" with title "{_escape_applescript(title)}"'
|
|
124
|
-
|
|
131
|
+
_run_notification_command(
|
|
132
|
+
["osascript", "-e", script],
|
|
133
|
+
spawn_sync,
|
|
134
|
+
env,
|
|
135
|
+
)
|
|
136
|
+
elif shutil_which("notify-send", env):
|
|
137
|
+
_run_notification_command(
|
|
138
|
+
["notify-send", str(title), str(message)],
|
|
139
|
+
spawn_sync,
|
|
140
|
+
env,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _run_notification_command(argv, spawn_sync, env):
|
|
145
|
+
try:
|
|
146
|
+
spawn_sync(argv, env=env, capture_output=True, text=True, timeout=5)
|
|
147
|
+
except (FileNotFoundError, OSError):
|
|
148
|
+
pass
|
|
125
149
|
|
|
126
150
|
|
|
127
151
|
def _send_windows_notification(title, message, spawn_sync, env):
|
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
|
|
|
@@ -326,7 +333,7 @@ def create_session_service(options=None):
|
|
|
326
333
|
_ensure_private_dir(os.path.join(base_dir, "profiles"))
|
|
327
334
|
_ensure_private_dir(session_root)
|
|
328
335
|
_ensure_private_dir(auth_home)
|
|
329
|
-
if normalized_provider ==
|
|
336
|
+
if normalized_provider == PROVIDER_CODEX:
|
|
330
337
|
_seed_codex_auth_from_global(auth_home, env=env)
|
|
331
338
|
now = _local_now_iso()
|
|
332
339
|
session = {
|
|
@@ -553,7 +560,7 @@ def create_session_service(options=None):
|
|
|
553
560
|
source_root = session.get("authHome") or _get_session_auth_home(
|
|
554
561
|
session["name"], session["provider"]
|
|
555
562
|
)
|
|
556
|
-
if session["provider"] ==
|
|
563
|
+
if session["provider"] == PROVIDER_CODEX and codex_status_fetcher:
|
|
557
564
|
live_status = codex_status_fetcher({**session, "authHome": source_root})
|
|
558
565
|
if live_status:
|
|
559
566
|
record_status(session["name"], live_status)
|
|
@@ -561,7 +568,7 @@ def create_session_service(options=None):
|
|
|
561
568
|
|
|
562
569
|
expected_account_email = (
|
|
563
570
|
_read_expected_account_email(source_root)
|
|
564
|
-
if session["provider"] ==
|
|
571
|
+
if session["provider"] == PROVIDER_CODEX
|
|
565
572
|
else None
|
|
566
573
|
)
|
|
567
574
|
artifact = find_latest_status_artifact(
|
|
@@ -570,7 +577,7 @@ def create_session_service(options=None):
|
|
|
570
577
|
expected_account_email=expected_account_email,
|
|
571
578
|
)
|
|
572
579
|
if (
|
|
573
|
-
session["provider"] ==
|
|
580
|
+
session["provider"] == PROVIDER_CODEX
|
|
574
581
|
and not artifact
|
|
575
582
|
and os.path.abspath(base_dir) == os.path.abspath(get_cdx_home(env))
|
|
576
583
|
):
|
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
|