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.
- package/README.md +1 -1
- package/changelogs/CHANGELOGS_0_9_11.md +44 -0
- package/checksums/release-archives.json +4 -0
- package/package.json +1 -1
- package/pyproject.toml +5 -2
- package/src/backup_bundle.py +0 -1
- package/src/claude_usage.py +19 -3
- package/src/cli.py +27 -9
- package/src/cli_args.py +631 -0
- package/src/cli_commands.py +64 -995
- package/src/cli_helpers.py +385 -0
- package/src/cli_render.py +0 -1
- package/src/cli_view.py +1 -20
- package/src/context_store.py +1 -2
- package/src/logics_view.py +0 -1
- package/src/notify.py +0 -1
- package/src/provider_runtime.py +9 -33
- package/src/run_command.py +1 -1
- package/src/run_registry.py +2 -2
- package/src/run_usage.py +1 -2
- package/src/session_service.py +6 -20
- package/src/session_store.py +2 -2
- package/src/status_source.py +0 -19
- package/src/status_view.py +1 -1
- package/src/update_check.py +1 -2
|
@@ -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
package/src/cli_view.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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):
|
package/src/context_store.py
CHANGED
|
@@ -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,
|
|
51
|
+
with open(path, encoding="utf-8") as handle:
|
|
53
52
|
return handle.read()
|
|
54
53
|
except FileNotFoundError:
|
|
55
54
|
return ""
|
package/src/logics_view.py
CHANGED
package/src/notify.py
CHANGED
package/src/provider_runtime.py
CHANGED
|
@@ -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),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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,
|
|
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,
|
|
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
|
|
package/src/run_command.py
CHANGED
|
@@ -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"],
|
|
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
|
package/src/run_registry.py
CHANGED
|
@@ -14,7 +14,7 @@ def registry_path(base_dir):
|
|
|
14
14
|
|
|
15
15
|
def _read_registry(path):
|
|
16
16
|
try:
|
|
17
|
-
with open(path,
|
|
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,
|
|
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,
|
|
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()
|
package/src/session_service.py
CHANGED
|
@@ -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 .
|
|
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,
|
|
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,
|
|
418
|
+
with open(settings_path, encoding="utf-8") as handle:
|
|
433
419
|
settings = json.load(handle)
|
|
434
420
|
except FileNotFoundError:
|
|
435
421
|
settings = {}
|