cdx-manager 0.9.10 → 0.9.11

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.
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env python3
2
+ """Shared helpers for cdx commands: JSON envelopes, formatting, handoff transcripts.
3
+
4
+ Split out of cli_commands.py as the low-level, side-effect-light helper layer
5
+ (no command dispatch). Imported back into cli_commands and used by handlers.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import re
11
+ from datetime import datetime
12
+
13
+ from .cli_render import _dim, _info, _warn
14
+ from .config import PROVIDER_CODEX
15
+ from .provider_runtime import _list_launch_transcript_paths
16
+ from .status_source import _normalize_terminal_transcript
17
+
18
+ API_SCHEMA_VERSION = 1
19
+ HANDOFF_TRANSCRIPT_CHARS = 120000
20
+ HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES = 64
21
+
22
+
23
+ def _local_now_iso():
24
+ return datetime.now().astimezone().isoformat()
25
+
26
+
27
+ def _json_success(action, message, warnings=None, **extra):
28
+ payload = {
29
+ "schema_version": API_SCHEMA_VERSION,
30
+ "ok": True,
31
+ "action": action,
32
+ "message": message,
33
+ "warnings": warnings or [],
34
+ }
35
+ payload.update(extra)
36
+ return payload
37
+
38
+
39
+ def _json_failure(action, code, message, source="cdx", exit_code=1, warnings=None, **extra):
40
+ payload = {
41
+ "schema_version": API_SCHEMA_VERSION,
42
+ "ok": False,
43
+ "action": action,
44
+ "warnings": warnings or [],
45
+ "error": {
46
+ "source": source,
47
+ "code": code,
48
+ "message": message,
49
+ "exit_code": exit_code,
50
+ },
51
+ }
52
+ payload.update(extra)
53
+ return payload
54
+
55
+
56
+ def _write_json(ctx, payload):
57
+ ctx["out"](f"{json.dumps(payload, indent=2)}\n")
58
+
59
+
60
+ def _update_notice_warning(ctx):
61
+ notices = ctx.get("update_notices") or []
62
+ if not notices:
63
+ return None
64
+ warnings = _update_notice_warnings(ctx)
65
+ return warnings[0] if warnings else None
66
+
67
+
68
+ def _update_notice_warnings(ctx):
69
+ warnings = []
70
+ for notice in ctx.get("update_notices") or []:
71
+ tool = notice.get("tool") or "cdx-manager"
72
+ current = notice.get("current_version") or ctx.get("version")
73
+ command = notice.get("update_command") or ("cdx update" if tool == "cdx-manager" else None)
74
+ message = f"Update available: {tool} {notice['latest_version']}"
75
+ if current:
76
+ message = f"{message} (current {current})"
77
+ if command:
78
+ message = f"{message}. Run: {command}"
79
+ warnings.append({
80
+ "code": "update_available" if tool == "cdx-manager" else f"{tool.replace('-', '_')}_update_available",
81
+ "message": message,
82
+ "tool": tool,
83
+ "latest_version": notice["latest_version"],
84
+ "current_version": current,
85
+ "update_command": command,
86
+ "url": notice.get("url"),
87
+ })
88
+ return warnings
89
+
90
+
91
+ def _write_update_notice(ctx):
92
+ warnings = _update_notice_warnings(ctx)
93
+ if not warnings:
94
+ return
95
+ for warning in warnings:
96
+ ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
97
+
98
+
99
+ def _format_bytes(value):
100
+ if value is None:
101
+ return "n/a"
102
+ try:
103
+ amount = float(value)
104
+ except (TypeError, ValueError):
105
+ return str(value)
106
+ units = ["B", "KB", "MB", "GB", "TB"]
107
+ unit = units[0]
108
+ for unit in units:
109
+ if amount < 1024 or unit == units[-1]:
110
+ break
111
+ amount /= 1024
112
+ if unit == "B":
113
+ return f"{int(amount)} B"
114
+ return f"{amount:.1f} {unit}"
115
+
116
+
117
+ def _format_export_report(result):
118
+ lines = [
119
+ f"Path: {result['path']}",
120
+ f"Sessions: {', '.join(result['session_names']) or '-'}",
121
+ f"Auth: {'included and encrypted' if result['include_auth'] else 'not included'}",
122
+ f"Bundle size: {_format_bytes(result.get('bundle_size_bytes'))}",
123
+ ]
124
+ if result.get("include_auth"):
125
+ lines.extend([
126
+ f"Auth files: {result.get('profile_file_count', 0)}",
127
+ f"Auth data: {_format_bytes(result.get('profile_bytes'))}",
128
+ ])
129
+ return "\n".join(lines)
130
+
131
+
132
+ def _make_export_progress(ctx):
133
+ def progress(event):
134
+ kind = event.get("event")
135
+ if kind == "export_started":
136
+ auth = " with auth" if event.get("include_auth") else ""
137
+ message = f"Exporting {event.get('session_count', 0)} session(s){auth}..."
138
+ ctx["out"](f"{_info(message, ctx['use_color'])}\n")
139
+ elif kind == "session_started":
140
+ message = f"Collecting {event.get('session_name')}..."
141
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
142
+ elif kind == "profile_progress":
143
+ message = f" {event.get('session_name')}: {event.get('file_count', 0)} files, {_format_bytes(event.get('bytes'))}"
144
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
145
+ elif kind == "encoding_started":
146
+ ctx["out"](f"{_dim('Encoding and encrypting bundle...', ctx['use_color'])}\n")
147
+ elif kind == "writing_started":
148
+ message = f"Writing {_format_bytes(event.get('bundle_size_bytes'))}..."
149
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
150
+ return progress
151
+
152
+
153
+ def _make_status_progress(ctx):
154
+ progress_state = {"checked": 0, "total": 0}
155
+
156
+ def progress(event):
157
+ kind = event.get("event")
158
+ if kind == "status_started":
159
+ progress_state["checked"] = 0
160
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
161
+ message = f"Resolving status for {event.get('session_count', 0)} session(s)..."
162
+ ctx["out"](f"{_info(message, ctx['use_color'])}\n")
163
+ elif kind == "session_started":
164
+ provider = event.get("provider") or "session"
165
+ message = f"Checking {event.get('session_name')} ({provider})..."
166
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
167
+ elif kind == "session_finished" and not event.get("cache_hit"):
168
+ progress_state["checked"] += 1
169
+ total = progress_state["total"] or progress_state["checked"]
170
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
171
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
172
+ elif kind == "status_finished":
173
+ message = f"Resolved {event.get('row_count', 0)} status row(s)."
174
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
175
+ return progress
176
+
177
+
178
+ def _make_notify_progress(ctx):
179
+ progress_state = {"checked": 0, "total": 0}
180
+
181
+ def progress(event):
182
+ kind = event.get("event")
183
+ if kind == "notify_check_started":
184
+ target = event.get("session_name") or "next ready session"
185
+ ctx["out"](f"{_info(f'Checking notification target: {target}...', ctx['use_color'])}\n")
186
+ elif kind == "status_started":
187
+ progress_state["checked"] = 0
188
+ progress_state["total"] = event.get("check_count", event.get("session_count", 0)) or 0
189
+ message = f"Loading status for {event.get('session_count', 0)} session(s)..."
190
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
191
+ elif kind == "session_started":
192
+ provider = event.get("provider") or "session"
193
+ message = f"Checking {event.get('session_name')} ({provider})..."
194
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
195
+ elif kind == "session_finished" and not event.get("cache_hit"):
196
+ progress_state["checked"] += 1
197
+ total = progress_state["total"] or progress_state["checked"]
198
+ message = f"Checked {event.get('session_name')} ({progress_state['checked']}/{total})."
199
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
200
+ elif kind == "notify_waiting":
201
+ message = f"{event.get('message')}; checking again in {event.get('poll')}s..."
202
+ ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
203
+ return progress
204
+
205
+
206
+ def _latest_launch_transcript_path(session):
207
+ paths = _list_launch_transcript_paths(session)
208
+ if not paths:
209
+ return None
210
+ return max(paths, key=lambda path: (os.path.getmtime(path), path))
211
+
212
+
213
+ def _get_session_home(session):
214
+ return session.get("authHome") or session.get("sessionRoot") or session.get("codexHome", "")
215
+
216
+
217
+ def _safe_stat(path):
218
+ try:
219
+ return os.stat(path)
220
+ except OSError:
221
+ return None
222
+
223
+
224
+ def _sort_recent_paths(paths):
225
+ stats = {path: stat for path, stat in ((_path, _safe_stat(_path)) for _path in set(paths)) if stat}
226
+ return sorted(stats, key=lambda path: (stats[path].st_mtime, path), reverse=True)
227
+
228
+
229
+ def _collect_native_handoff_transcript_paths(session):
230
+ root = _get_session_home(session)
231
+ if not root:
232
+ return []
233
+ candidates = []
234
+ direct = [
235
+ os.path.join(root, "history.jsonl"),
236
+ os.path.join(root, "session_index.jsonl"),
237
+ ]
238
+ candidates.extend(path for path in direct if _safe_stat(path))
239
+
240
+ scan_roots = [
241
+ os.path.join(root, "sessions"),
242
+ os.path.join(root, ".claude", "projects"),
243
+ os.path.join(root, "projects"),
244
+ ]
245
+ skip_dirs = {"cache", "plugins", "skills", "memories", "sqlite", "shell_snapshots", "tmp", "__pycache__"}
246
+ for scan_root in scan_roots:
247
+ if not os.path.isdir(scan_root):
248
+ continue
249
+ for dirpath, dirnames, filenames in os.walk(scan_root):
250
+ dirnames[:] = [name for name in dirnames if not name.startswith(".") and name not in skip_dirs]
251
+ for filename in filenames:
252
+ if filename.endswith(".jsonl") or filename.endswith(".log"):
253
+ candidates.append(os.path.join(dirpath, filename))
254
+ return _sort_recent_paths(candidates)[:HANDOFF_NATIVE_TRANSCRIPT_CANDIDATES]
255
+
256
+
257
+ def _latest_handoff_transcript_path(session):
258
+ launch_path = _latest_launch_transcript_path(session)
259
+ if launch_path:
260
+ return launch_path
261
+ native_paths = _collect_native_handoff_transcript_paths(session)
262
+ return native_paths[0] if native_paths else None
263
+
264
+
265
+ def _collect_handoff_text_fragments(value):
266
+ fragments = []
267
+ if isinstance(value, str):
268
+ text = value.strip()
269
+ if text:
270
+ fragments.append(text)
271
+ elif isinstance(value, list):
272
+ for item in value:
273
+ fragments.extend(_collect_handoff_text_fragments(item))
274
+ elif isinstance(value, dict):
275
+ if isinstance(value.get("text"), str):
276
+ fragments.extend(_collect_handoff_text_fragments(value.get("text")))
277
+ if isinstance(value.get("content"), (str, list, dict)):
278
+ fragments.extend(_collect_handoff_text_fragments(value.get("content")))
279
+ if isinstance(value.get("message"), dict):
280
+ fragments.extend(_collect_handoff_text_fragments(value.get("message")))
281
+ if isinstance(value.get("payload"), dict):
282
+ fragments.extend(_collect_handoff_text_fragments(value.get("payload")))
283
+ return fragments
284
+
285
+
286
+ def _jsonl_record_to_handoff_text(record):
287
+ if not isinstance(record, dict):
288
+ return None
289
+ message = record.get("message") if isinstance(record.get("message"), dict) else {}
290
+ role = (
291
+ message.get("role")
292
+ or record.get("role")
293
+ or record.get("type")
294
+ or record.get("sender")
295
+ or "entry"
296
+ )
297
+ text_sources = []
298
+ for key in ("content", "text", "payload"):
299
+ if key in record:
300
+ text_sources.append(record.get(key))
301
+ for key in ("content", "text"):
302
+ if key in message:
303
+ text_sources.append(message.get(key))
304
+ fragments = []
305
+ for value in text_sources:
306
+ fragments.extend(_collect_handoff_text_fragments(value))
307
+ text = "\n".join(fragment for fragment in fragments if fragment).strip()
308
+ if not text:
309
+ return None
310
+ role = re.sub(r"[^A-Za-z0-9_-]+", "-", str(role).strip()).strip("-") or "entry"
311
+ return f"[{role}]\n{text}"
312
+
313
+
314
+ def _read_jsonl_handoff_transcript(path):
315
+ entries = []
316
+ with open(path, encoding="utf-8", errors="replace") as handle:
317
+ for line in handle:
318
+ line = line.strip()
319
+ if not line:
320
+ continue
321
+ try:
322
+ text = _jsonl_record_to_handoff_text(json.loads(line))
323
+ except json.JSONDecodeError:
324
+ text = None
325
+ if text:
326
+ entries.append(text)
327
+ if entries:
328
+ return "\n\n".join(entries)
329
+ with open(path, encoding="utf-8", errors="replace") as handle:
330
+ return handle.read()
331
+
332
+
333
+ def _read_handoff_transcript(path):
334
+ if path.endswith(".jsonl"):
335
+ content = _read_jsonl_handoff_transcript(path)
336
+ else:
337
+ with open(path, encoding="utf-8", errors="replace") as handle:
338
+ # `script`-captured PTY transcript: strip ANSI/control noise before
339
+ # the char-budget truncation so the tail holds real text, not escapes.
340
+ content = _normalize_terminal_transcript(handle.read())
341
+ if len(content) <= HANDOFF_TRANSCRIPT_CHARS:
342
+ return content, False
343
+ return content[-HANDOFF_TRANSCRIPT_CHARS:], True
344
+
345
+
346
+ def _build_handoff_context(source, target, transcript_path, transcript, truncated=False):
347
+ truncation_note = (
348
+ "Only the tail of the previous transcript is included because the log was large."
349
+ if truncated else
350
+ "The previous transcript is included below."
351
+ )
352
+ return "\n".join([
353
+ "# Shared Context",
354
+ "",
355
+ "## Goal",
356
+ f"Resume the work from `{source['name']}` in `{target['name']}` because the source account has no usage left.",
357
+ "",
358
+ "## Current State",
359
+ f"- Source session: `{source['name']}` ({source['provider']})",
360
+ f"- Target session: `{target['name']}` ({target['provider']})",
361
+ f"- Transcript source: `{transcript_path}`",
362
+ f"- {truncation_note}",
363
+ "",
364
+ "## Previous Session Transcript",
365
+ transcript.rstrip(),
366
+ "",
367
+ "## Next Steps",
368
+ "- Read the transcript above.",
369
+ "- Continue the task from the latest actionable state.",
370
+ "- Preserve the target account authentication; do not copy source credentials.",
371
+ ])
372
+
373
+
374
+ def _handoff_launch_prompt(session, install=None):
375
+ if session.get("provider") == PROVIDER_CODEX:
376
+ context_ref = "$CODEX_HOME/shared-context.md"
377
+ else:
378
+ context_ref = (install or {}).get("target_path") or os.path.join(
379
+ session.get("authHome") or session.get("sessionRoot") or "~",
380
+ "shared-context.md",
381
+ )
382
+ return (
383
+ f"Read {context_ref} first, then resume the previous session "
384
+ "from the latest actionable state. Do not ask me to paste the context again."
385
+ )
package/src/cli_render.py CHANGED
@@ -3,7 +3,6 @@ import re
3
3
  import sys
4
4
  from datetime import datetime, timezone
5
5
 
6
-
7
6
  ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
8
7
 
9
8
 
package/src/cli_view.py CHANGED
@@ -1,5 +1,4 @@
1
- import json
2
-
1
+ from .cli_helpers import _json_success, _write_json
3
2
  from .cli_render import _warn
4
3
  from .errors import CdxError
5
4
  from .logics_view import (
@@ -11,25 +10,7 @@ from .logics_view import (
11
10
  )
12
11
  from .update_check import check_logics_manager_for_update
13
12
 
14
-
15
13
  VIEW_USAGE = "Usage: cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--open] [--no-open]"
16
- API_SCHEMA_VERSION = 1
17
-
18
-
19
- def _json_success(action, message, warnings=None, **extra):
20
- payload = {
21
- "schema_version": API_SCHEMA_VERSION,
22
- "ok": True,
23
- "action": action,
24
- "message": message,
25
- "warnings": warnings or [],
26
- }
27
- payload.update(extra)
28
- return payload
29
-
30
-
31
- def _write_json(ctx, payload):
32
- ctx["out"](f"{json.dumps(payload, indent=2)}\n")
33
14
 
34
15
 
35
16
  def _update_notice_warnings(notices):
@@ -7,7 +7,6 @@ from datetime import datetime
7
7
  from .errors import CdxError
8
8
  from .session_store import _ensure_dir
9
9
 
10
-
11
10
  DEFAULT_CONTEXT_TEMPLATE = """# Shared Context
12
11
 
13
12
  ## Goal
@@ -49,7 +48,7 @@ def get_context_meta_path(base_dir, cwd=None):
49
48
  def read_context(base_dir, cwd=None):
50
49
  path = get_context_path(base_dir, cwd)
51
50
  try:
52
- with open(path, "r", encoding="utf-8") as handle:
51
+ with open(path, encoding="utf-8") as handle:
53
52
  return handle.read()
54
53
  except FileNotFoundError:
55
54
  return ""
@@ -2,7 +2,6 @@ import os
2
2
  import shutil
3
3
  import subprocess
4
4
 
5
-
6
5
  LOGICS_MANAGER_INSTALL_HINT = "Install or update it with: npm install -g @grifhinz/logics-manager"
7
6
 
8
7
 
package/src/notify.py CHANGED
@@ -13,7 +13,6 @@ from .status_view import (
13
13
  _recommend_priority_sessions,
14
14
  )
15
15
 
16
-
17
16
  NOTIFY_USAGE = "Usage: cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] | cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh]"
18
17
 
19
18
 
@@ -1,20 +1,19 @@
1
1
  import json
2
2
  import os
3
3
  import re
4
- import signal
5
4
  import shlex
6
5
  import shutil
6
+ import signal
7
7
  import subprocess
8
8
  import sys
9
9
  import uuid
10
- import base64
11
10
  from datetime import datetime, timezone
12
11
 
12
+ from .claude_usage import _clean_oauth_token, _decode_jwt_claims
13
13
  from .codex_usage import codex_auth_lock
14
14
  from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
15
15
  from .errors import CdxError
16
16
 
17
-
18
17
  LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
19
18
  REASONING_EFFORT_VALUES = {"minimal", "low", "medium", "high", "xhigh"}
20
19
  RTK_PROMPT = (
@@ -90,15 +89,6 @@ def _anthropic_credentials_path(auth_home):
90
89
  return os.path.join(auth_home, "credentials", f"{_anthropic_profile_name()}.json")
91
90
 
92
91
 
93
- def _clean_oauth_token(token):
94
- if not token:
95
- return None
96
- text = re.sub(r"\x1b\[[0-9;?]*[ -/]*[@-~]", "", str(token)).strip()
97
- if not text or text.startswith("<") or any(ord(ch) < 32 or ord(ch) == 127 for ch in text):
98
- return None
99
- return text
100
-
101
-
102
92
  def _claude_env(base_env, auth_home):
103
93
  env = {**base_env, **_home_env_overrides(auth_home)}
104
94
  env.pop("CLAUDE_CONFIG_DIR", None)
@@ -109,7 +99,7 @@ def _claude_env(base_env, auth_home):
109
99
 
110
100
  def _read_anthropic_oauth_token(auth_home):
111
101
  try:
112
- with open(_anthropic_credentials_path(auth_home), "r", encoding="utf-8") as handle:
102
+ with open(_anthropic_credentials_path(auth_home), encoding="utf-8") as handle:
113
103
  credentials = json.load(handle)
114
104
  except (FileNotFoundError, OSError, json.JSONDecodeError):
115
105
  return None
@@ -119,7 +109,7 @@ def _read_anthropic_oauth_token(auth_home):
119
109
 
120
110
  def _has_local_claude_auth(auth_home):
121
111
  try:
122
- with open(os.path.join(auth_home, ".claude", ".credentials.json"), "r", encoding="utf-8") as handle:
112
+ with open(os.path.join(auth_home, ".claude", ".credentials.json"), encoding="utf-8") as handle:
123
113
  credentials = json.load(handle)
124
114
  except (FileNotFoundError, OSError, json.JSONDecodeError):
125
115
  return False
@@ -135,7 +125,7 @@ def _read_claude_launch_oauth_token(auth_home):
135
125
 
136
126
  def _has_local_codex_auth(auth_home):
137
127
  try:
138
- with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
128
+ with open(os.path.join(auth_home, "auth.json"), encoding="utf-8") as handle:
139
129
  auth = json.load(handle)
140
130
  except (FileNotFoundError, OSError, json.JSONDecodeError):
141
131
  return False
@@ -150,23 +140,9 @@ def _has_local_codex_auth(auth_home):
150
140
  )
151
141
 
152
142
 
153
- def _decode_jwt_claims(token):
154
- if not token or "." not in str(token):
155
- return {}
156
- parts = str(token).split(".")
157
- if len(parts) < 2:
158
- return {}
159
- padding = "=" * (-len(parts[1]) % 4)
160
- try:
161
- decoded = base64.urlsafe_b64decode(parts[1] + padding)
162
- return json.loads(decoded.decode("utf-8"))
163
- except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
164
- return {}
165
-
166
-
167
143
  def _read_codex_account_email(auth_home):
168
144
  try:
169
- with open(os.path.join(auth_home, "auth.json"), "r", encoding="utf-8") as handle:
145
+ with open(os.path.join(auth_home, "auth.json"), encoding="utf-8") as handle:
170
146
  auth = json.load(handle)
171
147
  except (FileNotFoundError, OSError, json.JSONDecodeError):
172
148
  return None
@@ -211,7 +187,7 @@ def codex_auth_diagnostic(session, spawn_sync=None, env_override=None):
211
187
  def _read_claude_account_email(auth_home):
212
188
  config_path = os.path.join(auth_home, ".claude.json")
213
189
  try:
214
- with open(config_path, "r", encoding="utf-8") as handle:
190
+ with open(config_path, encoding="utf-8") as handle:
215
191
  config = json.load(handle)
216
192
  except (FileNotFoundError, OSError, json.JSONDecodeError):
217
193
  return None
@@ -685,7 +661,7 @@ def _combine_headless_transcript(paths):
685
661
  continue
686
662
  transcript.write(f"--- {label} ---\n")
687
663
  try:
688
- with open(path, "r", encoding="utf-8", errors="replace") as handle:
664
+ with open(path, encoding="utf-8", errors="replace") as handle:
689
665
  content = handle.read()
690
666
  except OSError:
691
667
  content = ""
@@ -907,7 +883,7 @@ def _probe_provider_auth(session, spawn_sync=None, env_override=None, trust_loca
907
883
  stderr = result.get("stderr") if isinstance(result, dict) else getattr(result, "stderr", "")
908
884
  output = (stdout or "") + (stderr or "")
909
885
  except FileNotFoundError as error:
910
- raise _format_probe_failure(session, spec, error)
886
+ raise _format_probe_failure(session, spec, error) from error
911
887
  return spec["parser"](output)
912
888
 
913
889
 
@@ -8,7 +8,7 @@ def read_run_prompt(parsed):
8
8
  if parsed.get("prompt") is not None:
9
9
  return parsed["prompt"]
10
10
  try:
11
- with open(parsed["prompt_file"], "r", encoding="utf-8") as handle:
11
+ with open(parsed["prompt_file"], encoding="utf-8") as handle:
12
12
  return handle.read()
13
13
  except OSError as error:
14
14
  raise CdxError(f"Unable to read prompt file: {parsed['prompt_file']}") from error
@@ -14,7 +14,7 @@ def registry_path(base_dir):
14
14
 
15
15
  def _read_registry(path):
16
16
  try:
17
- with open(path, "r", encoding="utf-8") as handle:
17
+ with open(path, encoding="utf-8") as handle:
18
18
  data = json.load(handle)
19
19
  except FileNotFoundError:
20
20
  return {"schema_version": 1, "runs": []}
@@ -158,7 +158,7 @@ def read_text_file(path, limit=120000):
158
158
  if not path:
159
159
  return ""
160
160
  try:
161
- with open(path, "r", encoding="utf-8", errors="replace") as handle:
161
+ with open(path, encoding="utf-8", errors="replace") as handle:
162
162
  return handle.read(limit)
163
163
  except OSError:
164
164
  return ""
package/src/run_usage.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import json
2
2
 
3
-
4
3
  USAGE_KEYS = ("input_tokens", "output_tokens", "reasoning_tokens", "total_tokens")
5
4
  SUPPORTED_PROVIDERS = {"claude", "codex"}
6
5
 
@@ -15,7 +14,7 @@ def extract_run_usage(provider, stdout_path):
15
14
  if provider not in SUPPORTED_PROVIDERS:
16
15
  return empty_usage()
17
16
  try:
18
- with open(stdout_path, "r", encoding="utf-8", errors="replace") as handle:
17
+ with open(stdout_path, encoding="utf-8", errors="replace") as handle:
19
18
  text = handle.read()
20
19
  except OSError:
21
20
  return empty_usage()
@@ -1,7 +1,7 @@
1
+ import base64
2
+ import json
1
3
  import os
2
4
  import shutil
3
- import json
4
- import base64
5
5
  import sys
6
6
  import tempfile
7
7
  import uuid
@@ -10,8 +10,9 @@ from datetime import datetime, timezone
10
10
  from urllib.parse import quote
11
11
 
12
12
  from .backup_bundle import decode_bundle, encode_bundle
13
- from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
13
+ from .claude_usage import _decode_jwt_claims
14
14
  from .codex_usage import fetch_codex_rate_limits
15
+ from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDERS, get_cdx_home
15
16
  from .errors import CdxError
16
17
  from .fs_utils import remove_tree
17
18
  from .session_store import create_session_store
@@ -389,25 +390,10 @@ def _is_low_confidence_status_source(status):
389
390
  return "/sessions/" in source_ref and "/rollout" in source_ref
390
391
 
391
392
 
392
- def _decode_jwt_claims(token):
393
- if not token or "." not in str(token):
394
- return {}
395
- parts = str(token).split(".")
396
- if len(parts) < 2:
397
- return {}
398
- payload = parts[1]
399
- padding = "=" * (-len(payload) % 4)
400
- try:
401
- decoded = base64.urlsafe_b64decode(payload + padding)
402
- return json.loads(decoded.decode("utf-8"))
403
- except (ValueError, json.JSONDecodeError, UnicodeDecodeError):
404
- return {}
405
-
406
-
407
393
  def _read_expected_account_email(auth_home):
408
394
  auth_path = os.path.join(auth_home, "auth.json")
409
395
  try:
410
- with open(auth_path, "r", encoding="utf-8") as handle:
396
+ with open(auth_path, encoding="utf-8") as handle:
411
397
  auth = json.load(handle)
412
398
  except (FileNotFoundError, OSError, json.JSONDecodeError):
413
399
  return None
@@ -429,7 +415,7 @@ def _ensure_claude_attribution_disabled(auth_home):
429
415
  settings_path = os.path.join(settings_dir, "settings.json")
430
416
  try:
431
417
  os.makedirs(settings_dir, exist_ok=True)
432
- with open(settings_path, "r", encoding="utf-8") as handle:
418
+ with open(settings_path, encoding="utf-8") as handle:
433
419
  settings = json.load(handle)
434
420
  except FileNotFoundError:
435
421
  settings = {}