cdx-manager 0.5.3 → 0.5.5
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 +4 -4
- package/changelogs/CHANGELOGS_0_5_4.md +49 -0
- package/changelogs/CHANGELOGS_0_5_5.md +48 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/claude_refresh.py +36 -19
- package/src/claude_usage.py +33 -1
- package/src/cli.py +1 -1
- package/src/cli_commands.py +185 -156
- 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 +99 -28
- package/src/status_source.py +22 -20
- package/src/status_view.py +22 -4
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,
|
|
@@ -68,6 +69,76 @@ def _write_json(ctx, payload):
|
|
|
68
69
|
ctx["out"](f"{json.dumps(payload, indent=2)}\n")
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
def _format_bytes(value):
|
|
73
|
+
if value is None:
|
|
74
|
+
return "n/a"
|
|
75
|
+
try:
|
|
76
|
+
amount = float(value)
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
return str(value)
|
|
79
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
80
|
+
unit = units[0]
|
|
81
|
+
for unit in units:
|
|
82
|
+
if amount < 1024 or unit == units[-1]:
|
|
83
|
+
break
|
|
84
|
+
amount /= 1024
|
|
85
|
+
if unit == "B":
|
|
86
|
+
return f"{int(amount)} B"
|
|
87
|
+
return f"{amount:.1f} {unit}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_export_report(result):
|
|
91
|
+
lines = [
|
|
92
|
+
f"Path: {result['path']}",
|
|
93
|
+
f"Sessions: {', '.join(result['session_names']) or '-'}",
|
|
94
|
+
f"Auth: {'included and encrypted' if result['include_auth'] else 'not included'}",
|
|
95
|
+
f"Bundle size: {_format_bytes(result.get('bundle_size_bytes'))}",
|
|
96
|
+
]
|
|
97
|
+
if result.get("include_auth"):
|
|
98
|
+
lines.extend([
|
|
99
|
+
f"Auth files: {result.get('profile_file_count', 0)}",
|
|
100
|
+
f"Auth data: {_format_bytes(result.get('profile_bytes'))}",
|
|
101
|
+
])
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _make_export_progress(ctx):
|
|
106
|
+
def progress(event):
|
|
107
|
+
kind = event.get("event")
|
|
108
|
+
if kind == "export_started":
|
|
109
|
+
auth = " with auth" if event.get("include_auth") else ""
|
|
110
|
+
message = f"Exporting {event.get('session_count', 0)} session(s){auth}..."
|
|
111
|
+
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
112
|
+
elif kind == "session_started":
|
|
113
|
+
message = f"Collecting {event.get('session_name')}..."
|
|
114
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
115
|
+
elif kind == "profile_progress":
|
|
116
|
+
message = f" {event.get('session_name')}: {event.get('file_count', 0)} files, {_format_bytes(event.get('bytes'))}"
|
|
117
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
118
|
+
elif kind == "encoding_started":
|
|
119
|
+
ctx["out"](f"{_dim('Encoding and encrypting bundle...', ctx['use_color'])}\n")
|
|
120
|
+
elif kind == "writing_started":
|
|
121
|
+
message = f"Writing {_format_bytes(event.get('bundle_size_bytes'))}..."
|
|
122
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
123
|
+
return progress
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _make_status_progress(ctx):
|
|
127
|
+
def progress(event):
|
|
128
|
+
kind = event.get("event")
|
|
129
|
+
if kind == "status_started":
|
|
130
|
+
message = f"Resolving status for {event.get('session_count', 0)} session(s)..."
|
|
131
|
+
ctx["out"](f"{_info(message, ctx['use_color'])}\n")
|
|
132
|
+
elif kind == "session_started":
|
|
133
|
+
provider = event.get("provider") or "session"
|
|
134
|
+
message = f"Checking {event.get('session_name')} ({provider})..."
|
|
135
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
136
|
+
elif kind == "status_finished":
|
|
137
|
+
message = f"Resolved {event.get('row_count', 0)} status row(s)."
|
|
138
|
+
ctx["out"](f"{_dim(message, ctx['use_color'])}\n")
|
|
139
|
+
return progress
|
|
140
|
+
|
|
141
|
+
|
|
71
142
|
def _latest_launch_transcript_path(session):
|
|
72
143
|
paths = _list_launch_transcript_paths(session)
|
|
73
144
|
if not paths:
|
|
@@ -112,7 +183,7 @@ def _build_handoff_context(source, target, transcript_path, transcript, truncate
|
|
|
112
183
|
|
|
113
184
|
|
|
114
185
|
def _handoff_launch_prompt(session, install=None):
|
|
115
|
-
if session.get("provider") ==
|
|
186
|
+
if session.get("provider") == PROVIDER_CODEX:
|
|
116
187
|
context_ref = "$CODEX_HOME/shared-context.md"
|
|
117
188
|
else:
|
|
118
189
|
context_ref = (install or {}).get("target_path") or os.path.join(
|
|
@@ -131,11 +202,54 @@ def _parse_json_flag(args):
|
|
|
131
202
|
return json_flag, cleaned
|
|
132
203
|
|
|
133
204
|
|
|
205
|
+
def _parse_flag_args(args, schema, usage, positionals_key=None, max_positionals=0):
|
|
206
|
+
parsed = {spec["key"]: spec.get("default") for spec in schema.values()}
|
|
207
|
+
positionals = []
|
|
208
|
+
index = 0
|
|
209
|
+
while index < len(args):
|
|
210
|
+
arg = args[index]
|
|
211
|
+
if arg in schema:
|
|
212
|
+
spec = schema[arg]
|
|
213
|
+
if spec["type"] == "bool":
|
|
214
|
+
parsed[spec["key"]] = True
|
|
215
|
+
index += 1
|
|
216
|
+
continue
|
|
217
|
+
value, index = _read_option_value(args, index, usage)
|
|
218
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
219
|
+
continue
|
|
220
|
+
if arg.startswith("--") and "=" in arg:
|
|
221
|
+
flag, value = arg.split("=", 1)
|
|
222
|
+
spec = schema.get(flag)
|
|
223
|
+
if not spec or spec["type"] == "bool":
|
|
224
|
+
raise CdxError(usage)
|
|
225
|
+
parsed[spec["key"]] = spec.get("transform", lambda item: item)(value)
|
|
226
|
+
index += 1
|
|
227
|
+
continue
|
|
228
|
+
if arg.startswith("-"):
|
|
229
|
+
raise CdxError(usage)
|
|
230
|
+
positionals.append(arg)
|
|
231
|
+
if len(positionals) > max_positionals:
|
|
232
|
+
raise CdxError(usage)
|
|
233
|
+
index += 1
|
|
234
|
+
|
|
235
|
+
if positionals_key is not None:
|
|
236
|
+
parsed[positionals_key] = positionals
|
|
237
|
+
return parsed
|
|
238
|
+
|
|
239
|
+
|
|
134
240
|
def _parse_add_args(args):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
241
|
+
parsed = _parse_flag_args(
|
|
242
|
+
args,
|
|
243
|
+
{},
|
|
244
|
+
"Usage: cdx add [provider] <name> [--json]",
|
|
245
|
+
positionals_key="values",
|
|
246
|
+
max_positionals=2,
|
|
247
|
+
)
|
|
248
|
+
values = parsed["values"]
|
|
249
|
+
if len(values) == 1:
|
|
250
|
+
return {"provider": PROVIDER_CODEX, "name": values[0]}
|
|
251
|
+
if len(values) == 2:
|
|
252
|
+
return {"provider": values[0], "name": values[1]}
|
|
139
253
|
raise CdxError("Usage: cdx add [provider] <name> [--json]")
|
|
140
254
|
|
|
141
255
|
|
|
@@ -152,19 +266,21 @@ def _parse_rename_args(args):
|
|
|
152
266
|
|
|
153
267
|
|
|
154
268
|
def _parse_remove_args(args):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if
|
|
269
|
+
parsed = _parse_flag_args(args, {
|
|
270
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
271
|
+
}, "Usage: cdx rmv <name> [--force] [--json]", positionals_key="names", max_positionals=1)
|
|
272
|
+
if len(parsed["names"]) != 1:
|
|
159
273
|
raise CdxError("Usage: cdx rmv <name> [--force] [--json]")
|
|
160
|
-
return {"name": names[0], "force": force}
|
|
274
|
+
return {"name": parsed["names"][0], "force": parsed["force"]}
|
|
161
275
|
|
|
162
276
|
|
|
163
277
|
def _parse_toggle_args(args, usage):
|
|
164
|
-
|
|
165
|
-
|
|
278
|
+
parsed = _parse_flag_args(args, {
|
|
279
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
280
|
+
}, usage, positionals_key="names", max_positionals=1)
|
|
281
|
+
if len(parsed["names"]) != 1:
|
|
166
282
|
raise CdxError(usage)
|
|
167
|
-
return {"name":
|
|
283
|
+
return {"name": parsed["names"][0], "json": parsed["json"]}
|
|
168
284
|
|
|
169
285
|
|
|
170
286
|
def _read_option_value(args, index, usage):
|
|
@@ -183,37 +299,12 @@ def _parse_session_names(value):
|
|
|
183
299
|
|
|
184
300
|
|
|
185
301
|
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
|
-
|
|
302
|
+
parsed = _parse_flag_args(args, {
|
|
303
|
+
"--check": {"key": "check", "type": "bool", "default": False},
|
|
304
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
305
|
+
"--yes": {"key": "yes", "type": "bool", "default": False},
|
|
306
|
+
"--version": {"key": "version", "type": "str", "default": None},
|
|
307
|
+
}, UPDATE_USAGE)
|
|
217
308
|
if parsed["check"] and parsed["version"]:
|
|
218
309
|
raise CdxError("Usage: cdx update --check cannot be combined with --version.")
|
|
219
310
|
if parsed["version"] is not None and not parsed["version"].strip():
|
|
@@ -222,52 +313,19 @@ def _parse_update_args(args):
|
|
|
222
313
|
|
|
223
314
|
|
|
224
315
|
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
|
-
|
|
316
|
+
parsed = _parse_flag_args(args, {
|
|
317
|
+
"--include-auth": {"key": "include_auth", "type": "bool", "default": False},
|
|
318
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
319
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
320
|
+
"--sessions": {
|
|
321
|
+
"key": "session_names",
|
|
322
|
+
"type": "str",
|
|
323
|
+
"default": None,
|
|
324
|
+
"transform": _parse_session_names,
|
|
325
|
+
},
|
|
326
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
327
|
+
}, EXPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
328
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
271
329
|
if not parsed["file_path"]:
|
|
272
330
|
raise CdxError(EXPORT_USAGE)
|
|
273
331
|
if parsed["passphrase_env"] and not parsed["include_auth"]:
|
|
@@ -276,47 +334,18 @@ def _parse_export_args(args):
|
|
|
276
334
|
|
|
277
335
|
|
|
278
336
|
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
|
-
|
|
337
|
+
parsed = _parse_flag_args(args, {
|
|
338
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
339
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
340
|
+
"--sessions": {
|
|
341
|
+
"key": "session_names",
|
|
342
|
+
"type": "str",
|
|
343
|
+
"default": None,
|
|
344
|
+
"transform": _parse_session_names,
|
|
345
|
+
},
|
|
346
|
+
"--passphrase-env": {"key": "passphrase_env", "type": "str", "default": None},
|
|
347
|
+
}, IMPORT_USAGE, positionals_key="positionals", max_positionals=1)
|
|
348
|
+
parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
|
|
320
349
|
if not parsed["file_path"]:
|
|
321
350
|
raise CdxError(IMPORT_USAGE)
|
|
322
351
|
return parsed
|
|
@@ -558,21 +587,20 @@ def handle_doctor(rest, ctx):
|
|
|
558
587
|
|
|
559
588
|
|
|
560
589
|
def handle_repair(rest, ctx):
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
raise CdxError(REPAIR_USAGE)
|
|
590
|
+
parsed = _parse_flag_args(rest, {
|
|
591
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
592
|
+
"--dry-run": {"key": "dry_run", "type": "bool", "default": False},
|
|
593
|
+
"--force": {"key": "force", "type": "bool", "default": False},
|
|
594
|
+
}, REPAIR_USAGE)
|
|
595
|
+
dry_run = parsed["dry_run"] or not parsed["force"]
|
|
568
596
|
report = repair_health(
|
|
569
597
|
ctx["service"],
|
|
570
598
|
ctx["service"]["base_dir"],
|
|
571
599
|
env=ctx.get("env"),
|
|
572
600
|
dry_run=dry_run,
|
|
573
|
-
force=force,
|
|
601
|
+
force=parsed["force"],
|
|
574
602
|
)
|
|
575
|
-
if
|
|
603
|
+
if parsed["json"]:
|
|
576
604
|
_write_json(ctx, _json_success("repair", "Collected repair report", report=report))
|
|
577
605
|
else:
|
|
578
606
|
ctx["out"](f"{format_repair_report(report, use_color=ctx['use_color'])}\n")
|
|
@@ -704,22 +732,23 @@ def handle_context(rest, ctx):
|
|
|
704
732
|
|
|
705
733
|
|
|
706
734
|
def handle_status(rest, ctx):
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if
|
|
735
|
+
parsed = _parse_flag_args(rest, {
|
|
736
|
+
"--json": {"key": "json", "type": "bool", "default": False},
|
|
737
|
+
"--small": {"key": "small", "type": "bool", "default": False},
|
|
738
|
+
"-s": {"key": "small", "type": "bool", "default": False},
|
|
739
|
+
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
740
|
+
}, STATUS_USAGE, positionals_key="args", max_positionals=1)
|
|
741
|
+
if parsed["json"] and parsed["small"]:
|
|
714
742
|
raise CdxError(STATUS_USAGE)
|
|
715
|
-
|
|
743
|
+
args = parsed["args"]
|
|
744
|
+
if len(args) == 1 and parsed["small"]:
|
|
716
745
|
raise CdxError(STATUS_USAGE)
|
|
717
746
|
|
|
718
747
|
refresh_result = _refresh_claude_sessions(
|
|
719
748
|
ctx["service"],
|
|
720
749
|
ctx.get("refresh_fn"),
|
|
721
750
|
target_names=args if len(args) == 1 else None,
|
|
722
|
-
force=
|
|
751
|
+
force=parsed["refresh"],
|
|
723
752
|
)
|
|
724
753
|
refresh_errors = [
|
|
725
754
|
{
|
|
@@ -737,19 +766,20 @@ def handle_status(rest, ctx):
|
|
|
737
766
|
for item in refresh_errors
|
|
738
767
|
]
|
|
739
768
|
|
|
740
|
-
|
|
769
|
+
status_progress = None if parsed["json"] else _make_status_progress(ctx)
|
|
770
|
+
rows = ctx["service"]["get_status_rows"](progress_callback=status_progress)
|
|
741
771
|
if len(args) == 0:
|
|
742
|
-
if
|
|
772
|
+
if parsed["json"]:
|
|
743
773
|
_write_json(ctx, _json_success("status", "Collected session status rows", warnings=warnings, rows=rows))
|
|
744
774
|
return 0
|
|
745
|
-
ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=
|
|
775
|
+
ctx["out"](f"{_format_status_rows(rows, use_color=ctx['use_color'], small=parsed['small'])}\n")
|
|
746
776
|
_write_refresh_warnings(refresh_errors, ctx)
|
|
747
777
|
return 0
|
|
748
778
|
|
|
749
779
|
row = next((r for r in rows if r["session_name"] == args[0]), None)
|
|
750
780
|
if not row:
|
|
751
781
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
752
|
-
if
|
|
782
|
+
if parsed["json"]:
|
|
753
783
|
_write_json(ctx, _json_success("status", f"Collected status for {args[0]}", warnings=warnings, session=row))
|
|
754
784
|
return 0
|
|
755
785
|
ctx["out"](f"{_format_status_detail(row, use_color=ctx['use_color'])}\n")
|
|
@@ -781,6 +811,7 @@ def handle_export(rest, ctx):
|
|
|
781
811
|
session_names=parsed["session_names"],
|
|
782
812
|
passphrase=passphrase,
|
|
783
813
|
force=parsed["force"],
|
|
814
|
+
progress_callback=None if parsed["json"] else _make_export_progress(ctx),
|
|
784
815
|
)
|
|
785
816
|
session_count = len(result["session_names"])
|
|
786
817
|
auth_suffix = " with auth" if result["include_auth"] else ""
|
|
@@ -794,6 +825,7 @@ def handle_export(rest, ctx):
|
|
|
794
825
|
_write_json(ctx, payload)
|
|
795
826
|
return 0
|
|
796
827
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
828
|
+
ctx["out"](f"{_format_export_report(result)}\n")
|
|
797
829
|
return 0
|
|
798
830
|
|
|
799
831
|
|
|
@@ -1002,9 +1034,6 @@ def handle_launch(command, ctx, initial_prompt=None):
|
|
|
1002
1034
|
if update_notice.get("url"):
|
|
1003
1035
|
text = f"{text} {update_notice['url']}"
|
|
1004
1036
|
ctx["out"](f"{_warn(text, ctx['use_color'])}\n")
|
|
1005
|
-
if session["provider"] == "codex":
|
|
1006
|
-
if not json_flag:
|
|
1007
|
-
ctx["out"](f"{_dim('Tip: cdx status reads Codex rate limits from the local app-server when available.', ctx['use_color'])}\n")
|
|
1008
1037
|
_run_interactive_provider_command(
|
|
1009
1038
|
session, "launch", spawn=ctx.get("spawn"), env_override=ctx.get("env"),
|
|
1010
1039
|
signal_emitter=ctx.get("signal_emitter"), initial_prompt=initial_prompt,
|
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):
|