muse-cli 0.2.0__py3-none-any.whl

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.
muse_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """muse-cli: talk to your personal muse.ai AI agent from the terminal."""
2
+
3
+ __version__ = "0.2.0"
muse_cli/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
muse_cli/cli.py ADDED
@@ -0,0 +1,538 @@
1
+ """muse-cli: CLI for your personal muse.ai agent. No browser needed (after cookie export).
2
+
3
+ Setup:
4
+ 1. Log in to https://muse.ai/ in Chrome (Auth profile).
5
+ 2. muse-cli auth export # saves session cookies locally (chmod 600)
6
+
7
+ Then: muse-cli status | muse-cli threads | muse-cli history | muse-cli send "hello" | ...
8
+ """
9
+ import argparse
10
+ import json
11
+ import os
12
+ import sys
13
+ import time
14
+
15
+ from . import __version__
16
+ from .gateway import Gateway, AuthError, GatewayError, load_cookies
17
+
18
+ CONFIG_DIR = os.path.expanduser("~/.config/muse-cli")
19
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
20
+ COOKIES_FILE = os.path.join(CONFIG_DIR, "cookies.txt")
21
+
22
+
23
+ def load_config():
24
+ import secrets
25
+ cfg = {}
26
+ if os.path.exists(CONFIG_FILE):
27
+ cfg = json.load(open(CONFIG_FILE))
28
+ changed = False
29
+ cfg.setdefault("cookies_file", COOKIES_FILE)
30
+ if "vm_id" not in cfg and os.environ.get("MUSE_VM_ID"):
31
+ cfg["vm_id"] = os.environ["MUSE_VM_ID"]
32
+ if "node_id" not in cfg:
33
+ if os.environ.get("MUSE_NODE_ID"):
34
+ cfg["node_id"] = os.environ["MUSE_NODE_ID"]
35
+ else:
36
+ cfg["node_id"] = secrets.token_hex(8)
37
+ changed = True
38
+ if changed:
39
+ os.makedirs(CONFIG_DIR, exist_ok=True)
40
+ json.dump(cfg, open(CONFIG_FILE, "w"), indent=2)
41
+ return cfg
42
+
43
+
44
+ def connect(cfg):
45
+ if not os.path.exists(cfg["cookies_file"]):
46
+ raise AuthError(f"no cookies at {cfg['cookies_file']}; log in to https://muse.ai/ "
47
+ "in Chrome, then run `muse-cli auth export`")
48
+ cookies = load_cookies(cfg["cookies_file"])
49
+ if not cookies.strip():
50
+ raise AuthError(f"cookies file {cfg['cookies_file']} is empty; run `muse-cli auth export`")
51
+ return Gateway(cookies, vm_id=cfg.get("vm_id"))
52
+
53
+
54
+ def out(obj):
55
+ print(json.dumps(obj, indent=2, ensure_ascii=False))
56
+
57
+
58
+ def _browser_run(argv):
59
+ """Run agent-browser, parsing its JSON envelope. NOTE: it exits 0 even
60
+ on failure, reporting {"success": false, "error": ...} on stdout."""
61
+ import subprocess
62
+ r = subprocess.run(argv, capture_output=True, text=True)
63
+ if r.returncode != 0:
64
+ raise RuntimeError((r.stderr or r.stdout)[:200] or f"exit {r.returncode}")
65
+ try:
66
+ doc = json.loads(r.stdout)
67
+ except json.JSONDecodeError:
68
+ raise RuntimeError((r.stdout or r.stderr)[:200] or "empty output")
69
+ if isinstance(doc, dict) and doc.get("success") is False:
70
+ raise RuntimeError(str(doc.get("error") or "unknown error")[:200])
71
+ return doc.get("data", {}) if isinstance(doc, dict) else doc
72
+
73
+
74
+ def _browser_cookies(headed):
75
+ """Read the cookie jar via agent-browser. Headed auto-connect attaches
76
+ to the user's real Chrome; plain mode uses a fresh browser (no login)."""
77
+ base = ["agent-browser"] + (["--headed", "--auto-connect"] if headed else [])
78
+ last_err = "unknown error"
79
+ for _ in range(3):
80
+ try:
81
+ if headed:
82
+ # Cookies follow the active tab: focus a muse.ai tab first,
83
+ # else the export comes back empty even when logged in.
84
+ data = _browser_run(base + ["tab", "list", "--json"])
85
+ tabs = data.get("tabs", []) if isinstance(data, dict) else []
86
+ muse_tabs = [t for t in tabs
87
+ if isinstance(t, dict) and "muse.ai" in (t.get("url") or "")
88
+ and (t.get("id") or t.get("tabId"))]
89
+ if not muse_tabs:
90
+ return None, "no muse.ai tab open in Chrome"
91
+ _browser_run(base + ["tab", muse_tabs[0].get("id") or muse_tabs[0]["tabId"]])
92
+ data = _browser_run(base + ["cookies", "get", "--json"])
93
+ jar = data.get("cookies", []) if isinstance(data, dict) else []
94
+ return [c for c in jar if "muse.ai" in c.get("domain", "")], None
95
+ except RuntimeError as e:
96
+ last_err = str(e)
97
+ time.sleep(2)
98
+ return None, last_err
99
+
100
+
101
+ def cmd_auth_export(_args):
102
+ os.makedirs(CONFIG_DIR, exist_ok=True)
103
+ jar, err = _browser_cookies(headed=True)
104
+ if jar is None and err != "no muse.ai tab open in Chrome":
105
+ # Headed attach failed (no Chrome, old agent-browser, ...): a plain
106
+ # browser shares no login, so this is a last resort at best.
107
+ jar, err = _browser_cookies(headed=False)
108
+ if jar is None:
109
+ print(f"cookie export failed: {err}", file=sys.stderr)
110
+ print("is Chrome running with muse.ai open?", file=sys.stderr)
111
+ print("alternative: export cookies by hand, see README Setup.", file=sys.stderr)
112
+ sys.exit(1)
113
+ # Never clobber a working login with an empty or logged-out jar.
114
+ if not any(c["name"] == "hatch_sess" for c in jar):
115
+ print("refusing to overwrite cookies: no hatch_sess in export "
116
+ "(are you logged in to muse.ai?). Existing file left intact.",
117
+ file=sys.stderr)
118
+ sys.exit(1)
119
+ lines = ["# Netscape HTTP Cookie File"]
120
+ for c in jar:
121
+ dom = c["domain"]
122
+ lines.append("\t".join([
123
+ dom, "TRUE" if dom.startswith(".") else "FALSE", c.get("path", "/"),
124
+ "TRUE" if c.get("secure") else "FALSE",
125
+ str(int(c.get("expires", 0) or 0)), c["name"], c["value"],
126
+ ]))
127
+ if os.path.exists(COOKIES_FILE):
128
+ os.replace(COOKIES_FILE, COOKIES_FILE + ".bak")
129
+ with open(COOKIES_FILE, "w") as fh:
130
+ fh.write("\n".join(lines) + "\n")
131
+ os.chmod(COOKIES_FILE, 0o600)
132
+ print(f"saved {len(jar)} muse.ai cookies to {COOKIES_FILE}")
133
+
134
+
135
+ def cmd_status(_args):
136
+ gw = connect(load_config())
137
+ try:
138
+ sess = gw.call_json("sessions.list")
139
+ unread = gw.call_json("chat.unread_count")
140
+ ident = gw.call_json("identity")
141
+ out({"vm_id": gw.vm_id, "sessions": len(sess.get("sessions", [])),
142
+ "unread": unread, "identity": ident})
143
+ finally:
144
+ gw.close()
145
+
146
+
147
+ def cmd_threads(args):
148
+ gw = connect(load_config())
149
+ try:
150
+ d = gw.call_json("sessions.list")
151
+ rows = [{
152
+ "session_id": s.get("session_id"),
153
+ "title": s.get("title"),
154
+ "thread": s.get("is_thread"),
155
+ "pinned": s.get("pinned"),
156
+ "archived": s.get("archived"),
157
+ "updated": time.strftime("%Y-%m-%d %H:%M", time.localtime(s.get("updated_at_ms", 0) / 1000)),
158
+ } for s in d.get("sessions", [])]
159
+ if args.archived is False:
160
+ rows = [r for r in rows if not r["archived"]]
161
+ out(rows)
162
+ finally:
163
+ gw.close()
164
+
165
+
166
+ def fmt_event(e):
167
+ p = e.get("payload", {}) if isinstance(e.get("payload"), dict) else {}
168
+ role = p.get("role") or e.get("event_name")
169
+ text = p.get("display_text") or p.get("content") or ""
170
+ return {"seq": e.get("seq"), "role": role,
171
+ "message_id": p.get("message_id") or e.get("message_id"),
172
+ "text": text}
173
+
174
+
175
+ def cmd_history(args):
176
+ gw = connect(load_config())
177
+ try:
178
+ params = {"limit": args.limit}
179
+ if args.thread:
180
+ params["session_id"] = args.thread
181
+ d = gw.call_json("chat.history", body=params)
182
+ events = d.get("chat_events", [])
183
+ if args.raw:
184
+ out(d)
185
+ return
186
+ out([fmt_event(e) for e in events if (e.get("payload", {}) or {}).get("display_text")
187
+ or (e.get("payload", {}) or {}).get("content")])
188
+ finally:
189
+ gw.close()
190
+
191
+
192
+ def is_reply(ev, baseline):
193
+ """True for a genuine assistant reply (not a proactive push).
194
+
195
+ In history, genuine replies are message.assistant events with an empty
196
+ reply_to_message_id; proactive pushes (Telegram drafts, background task
197
+ updates) are self-referential there.
198
+ """
199
+ if ev.get("event_name") != "message.assistant":
200
+ return False
201
+ if (ev.get("seq") or 0) <= baseline:
202
+ return False
203
+ p = ev.get("payload", {}) if isinstance(ev.get("payload"), dict) else {}
204
+ if (ev.get("reply_to_message_id") or p.get("reply_to_message_id")):
205
+ return False
206
+ if not (p.get("display_text") or p.get("content")):
207
+ return False
208
+ if "display_text_ready" in p and not p["display_text_ready"]:
209
+ return False
210
+ return True
211
+
212
+
213
+ def cmd_send(args):
214
+ cfg = load_config()
215
+ gw = connect(cfg)
216
+ try:
217
+ params = {"items": [{"type": "text", "text": args.text}],
218
+ "node_id": cfg["node_id"], "capabilities": {}}
219
+ if args.thread:
220
+ params["session_id"] = args.thread
221
+ # chat.history without session_id reads the main chat; with it, the
222
+ # thread. Either way the scope matches where the reply will land.
223
+ scope = {"session_id": args.thread} if args.thread else {}
224
+ baseline = 0
225
+ try:
226
+ h = gw.call_json("chat.history", body={"limit": 1, **scope})
227
+ evs = h.get("chat_events", [])
228
+ if evs:
229
+ baseline = max(e.get("seq", 0) for e in evs)
230
+ except (GatewayError, TimeoutError):
231
+ pass
232
+ stream_sid = gw._open("chat.stream", body=params)
233
+ if not args.wait:
234
+ out({"sent": True, "stream": stream_sid,
235
+ "note": "fire-and-forget; check `muse-cli history` for the reply"})
236
+ return
237
+
238
+ # The reply is picked up by polling history, not by watching the
239
+ # live stream: threaded replies never arrive as live events, live
240
+ # chat events use delta.* shapes (not message.*), and only the
241
+ # history shape carries the reply_to discriminator that tells
242
+ # genuine replies apart from proactive pushes. Sequential unary
243
+ # calls also mean a single frame consumer: no Noise races, ever.
244
+ reply, deadline = None, time.time() + args.wait
245
+ while time.time() < deadline and reply is None:
246
+ try:
247
+ h = gw.call_json("chat.history", body={"limit": 10, **scope})
248
+ except (GatewayError, TimeoutError):
249
+ time.sleep(4)
250
+ continue
251
+ for ev in sorted(h.get("chat_events", []), key=lambda e: e.get("seq", 0)):
252
+ if is_reply(ev, baseline):
253
+ reply = fmt_event(ev)
254
+ break
255
+ if reply is None:
256
+ time.sleep(4)
257
+ result = {"sent": True, "stream": stream_sid}
258
+ if reply:
259
+ result["reply"] = reply
260
+ else:
261
+ result["note"] = f"no assistant reply within {args.wait}s; check `muse-cli history`"
262
+ out(result)
263
+ finally:
264
+ gw.close()
265
+
266
+
267
+ def cmd_watch(args):
268
+ gw = connect(load_config())
269
+ try:
270
+ recs = gw.subscribe_json("chat.subscribe", body={"capabilities": {}},
271
+ max_records=args.n, idle_timeout=8,
272
+ overall_timeout=args.timeout)
273
+ for ev in recs:
274
+ et = ev.get("event", ev.get("type"))
275
+ if et in ("agent.status",) and not args.all:
276
+ continue
277
+ p = ev.get("payload", {}) if isinstance(ev.get("payload"), dict) else {}
278
+ row = {"event": et, "seq": ev.get("seq")}
279
+ for k in ("display_text", "content", "text", "activity_text", "status"):
280
+ if p.get(k):
281
+ row[k] = str(p[k])[:300]
282
+ print(json.dumps(row, ensure_ascii=False), flush=True)
283
+ finally:
284
+ gw.close()
285
+
286
+
287
+ def cmd_feed(args):
288
+ gw = connect(load_config())
289
+ try:
290
+ d = gw.call_json("feed.list", query={"limit": args.limit} if args.limit else None)
291
+ if args.raw:
292
+ out(d); return
293
+ rows = []
294
+ for day in d.get("days", []):
295
+ for ed in day.get("editions", []):
296
+ for u in ed.get("units", []):
297
+ rows.append({"id": u.get("unit_id"),
298
+ "title": u.get("title"),
299
+ "date": day.get("local_date"),
300
+ "edition": ed.get("kind")})
301
+ out(rows[:args.limit] if args.limit else rows)
302
+ finally:
303
+ gw.close()
304
+
305
+
306
+ def cmd_feed_status(_args):
307
+ gw = connect(load_config())
308
+ try:
309
+ out(gw.call_json("feed.status"))
310
+ finally:
311
+ gw.close()
312
+
313
+
314
+ def cmd_feed_react(args):
315
+ gw = connect(load_config())
316
+ try:
317
+ out(gw.call_json("feed.unit.reaction", path_params={"unit_id": args.unit},
318
+ body={"reaction": args.reaction}))
319
+ finally:
320
+ gw.close()
321
+
322
+
323
+ def cmd_feed_prompt(_args):
324
+ gw = connect(load_config())
325
+ try:
326
+ out(gw.call_json("feed.prompt.get"))
327
+ finally:
328
+ gw.close()
329
+
330
+
331
+ def cmd_goals(_args):
332
+ gw = connect(load_config())
333
+ try:
334
+ d = gw.call_json("goals.list")
335
+ out([{"id": g.get("id") or g.get("goal_id"), "title": g.get("title"),
336
+ "status": g.get("status"), "subtitle": g.get("subtitle")}
337
+ for g in d.get("goals", [])])
338
+ finally:
339
+ gw.close()
340
+
341
+
342
+ def cmd_goal(args):
343
+ gw = connect(load_config())
344
+ try:
345
+ out(gw.call_json("goals.get", path_params={"id": args.id}))
346
+ finally:
347
+ gw.close()
348
+
349
+
350
+ def cmd_ideas(_args):
351
+ gw = connect(load_config())
352
+ try:
353
+ d = gw.call_json("api.idea-cards.list")
354
+ rows = []
355
+ for s in d.get("sections", []):
356
+ for c in s.get("cards", s.get("ideas", [])):
357
+ rows.append({"id": c.get("id") or c.get("ideaCardId"),
358
+ "title": c.get("title"), "section": s.get("title")})
359
+ out(rows)
360
+ finally:
361
+ gw.close()
362
+
363
+
364
+ def cmd_idea(args):
365
+ gw = connect(load_config())
366
+ try:
367
+ out(gw.call_json("api.idea-cards.detail", path_params={"ideaCardId": args.id}))
368
+ finally:
369
+ gw.close()
370
+
371
+
372
+ def cmd_idea_exec(args):
373
+ gw = connect(load_config())
374
+ try:
375
+ body = {"ideaCardId": args.id, "mode": "full"}
376
+ if args.thread:
377
+ body["session_id"] = args.thread
378
+ out(gw.call_json("api.idea-cards.execute", path_params={"ideaCardId": args.id}, body=body))
379
+ finally:
380
+ gw.close()
381
+
382
+
383
+ def cmd_unread(_args):
384
+ gw = connect(load_config())
385
+ try:
386
+ out(gw.call_json("chat.unread_count"))
387
+ finally:
388
+ gw.close()
389
+
390
+
391
+ def cmd_seen(args):
392
+ gw = connect(load_config())
393
+ try:
394
+ out(gw.call_json("chat.mark_seen", path_params={"thread_id": args.thread}))
395
+ finally:
396
+ gw.close()
397
+
398
+
399
+ def cmd_session_start(args):
400
+ gw = connect(load_config())
401
+ try:
402
+ params = {"origin": "fresh", "lifecycle": "persistent"}
403
+ if args.title:
404
+ params["title"] = args.title
405
+ out(gw.call_json("session.start",
406
+ body={"method": "/api/session/start", "params": params}))
407
+ finally:
408
+ gw.close()
409
+
410
+
411
+ def cmd_session_op(kind):
412
+ def run(args):
413
+ gw = connect(load_config())
414
+ try:
415
+ if kind == "rename":
416
+ body = {"session_id": args.id, "title": args.title}
417
+ else:
418
+ body = {"method": f"/api/session/{kind}", "session_id": args.id}
419
+ out(gw.call_json(f"session.{kind}", body=body))
420
+ finally:
421
+ gw.close()
422
+ return run
423
+
424
+
425
+ def cmd_wake(_args):
426
+ cfg = load_config()
427
+ from .gateway import _hatch_headers, fetch_access_token, fetch_session_info
428
+ from curl_cffi import requests as rq
429
+ cookies = load_cookies(cfg["cookies_file"])
430
+ at = fetch_access_token(cookies)
431
+ vm_id = cfg.get("vm_id") or fetch_session_info(cookies)["vm_id"]
432
+ resp = rq.post("https://muse.ai/api/hatch/vm/wake", headers=_hatch_headers(cookies, at),
433
+ json={"vm_id": vm_id, "retry_count": 0},
434
+ impersonate="chrome", timeout=15)
435
+ out({"status": resp.status_code, "body": resp.json() if resp.text else None})
436
+
437
+
438
+ def cmd_raw(args):
439
+ from .gateway import ROUTES
440
+ if args.method not in ROUTES:
441
+ print(f"unknown method '{args.method}' (see routes.json for the {len(ROUTES)} known methods)",
442
+ file=sys.stderr)
443
+ sys.exit(2)
444
+ try:
445
+ body = json.loads(args.body) if args.body else None
446
+ except json.JSONDecodeError as e:
447
+ print(f"invalid --body JSON: {e}", file=sys.stderr)
448
+ sys.exit(2)
449
+ pp = {}
450
+ for kv in (args.param or []):
451
+ if "=" not in kv:
452
+ print(f"bad --param '{kv}': expected k=v", file=sys.stderr)
453
+ sys.exit(2)
454
+ k, v = kv.split("=", 1)
455
+ pp[k] = v
456
+ gw = connect(load_config())
457
+ try:
458
+ data = gw.request(args.method, path_params=pp or None, body=body,
459
+ query=None, timeout=args.timeout)
460
+ try:
461
+ out(json.loads(data) if data else {})
462
+ except json.JSONDecodeError:
463
+ print(data.decode("utf-8", "replace"))
464
+ finally:
465
+ gw.close()
466
+
467
+
468
+ def main():
469
+ ap = argparse.ArgumentParser(prog="muse-cli", description="CLI for your personal muse.ai agent")
470
+ ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
471
+ sub = ap.add_subparsers(dest="cmd", required=True)
472
+
473
+ p = sub.add_parser("auth", help="auth helpers"); a = p.add_subparsers(dest="op", required=True)
474
+ a.add_parser("export", help="export Chrome session cookies for the CLI").set_defaults(fn=cmd_auth_export)
475
+
476
+ sub.add_parser("status", help="VM, session count, unread, identity").set_defaults(fn=cmd_status)
477
+ p = sub.add_parser("threads", help="list chats and side chats")
478
+ p.add_argument("--all", dest="archived", action="store_true", help="include archived")
479
+ p.set_defaults(fn=cmd_threads)
480
+ p = sub.add_parser("history", help="read chat messages")
481
+ p.add_argument("--thread", default=None); p.add_argument("--limit", type=int, default=10)
482
+ p.add_argument("--raw", action="store_true"); p.set_defaults(fn=cmd_history)
483
+ p = sub.add_parser("send", help="send the agent a message")
484
+ p.add_argument("text"); p.add_argument("--thread", default=None)
485
+ p.add_argument("--wait", type=int, default=90, help="seconds to wait for reply (0 = don't)")
486
+ p.set_defaults(fn=cmd_send)
487
+ p = sub.add_parser("watch", help="tail live agent events")
488
+ p.add_argument("--timeout", type=int, default=60); p.add_argument("--n", type=int, default=50)
489
+ p.add_argument("--all", action="store_true"); p.set_defaults(fn=cmd_watch)
490
+ p = sub.add_parser("feed", help="list feed units")
491
+ p.add_argument("--limit", type=int, default=None); p.add_argument("--raw", action="store_true")
492
+ p.set_defaults(fn=cmd_feed)
493
+ sub.add_parser("feed-status", help="feed refresh status").set_defaults(fn=cmd_feed_status)
494
+ p = sub.add_parser("feed-react", help="react to a feed unit")
495
+ p.add_argument("unit"); p.add_argument("reaction", nargs="?", default="love")
496
+ p.set_defaults(fn=cmd_feed_react)
497
+ sub.add_parser("feed-prompt", help="show feed prompt").set_defaults(fn=cmd_feed_prompt)
498
+ sub.add_parser("goals", help="list goals").set_defaults(fn=cmd_goals)
499
+ p = sub.add_parser("goal", help="show one goal"); p.add_argument("id"); p.set_defaults(fn=cmd_goal)
500
+ sub.add_parser("ideas", help="list idea cards").set_defaults(fn=cmd_ideas)
501
+ p = sub.add_parser("idea", help="show one idea"); p.add_argument("id"); p.set_defaults(fn=cmd_idea)
502
+ p = sub.add_parser("idea-exec", help="execute an idea (agent acts on it)")
503
+ p.add_argument("id"); p.add_argument("--thread", default=None); p.set_defaults(fn=cmd_idea_exec)
504
+ sub.add_parser("unread", help="unread counts").set_defaults(fn=cmd_unread)
505
+ p = sub.add_parser("seen", help="mark a thread seen"); p.add_argument("thread")
506
+ p.set_defaults(fn=cmd_seen)
507
+ p = sub.add_parser("session-start", help="start a new side chat")
508
+ p.add_argument("--title", default=None); p.set_defaults(fn=cmd_session_start)
509
+ for kind, help_text in [("rename", "rename a session"), ("pin", "pin a session"),
510
+ ("unpin", "unpin a session"), ("archive", "archive a session"),
511
+ ("unarchive", "unarchive a session"), ("delete", "delete a session")]:
512
+ p = sub.add_parser(f"session-{kind}", help=help_text)
513
+ p.add_argument("id")
514
+ if kind == "rename":
515
+ p.add_argument("title")
516
+ p.set_defaults(fn=cmd_session_op(kind))
517
+ sub.add_parser("wake", help="request a VM wake").set_defaults(fn=cmd_wake)
518
+ p = sub.add_parser("raw", help="call any gateway method (escape hatch)")
519
+ p.add_argument("method"); p.add_argument("--body", default=None)
520
+ p.add_argument("--param", action="append", default=[], help="path param k=v (repeatable)")
521
+ p.add_argument("--timeout", type=int, default=30); p.set_defaults(fn=cmd_raw)
522
+
523
+ args = ap.parse_args()
524
+ try:
525
+ args.fn(args)
526
+ except AuthError as e:
527
+ print(f"auth error: {e}", file=sys.stderr)
528
+ sys.exit(2)
529
+ except GatewayError as e:
530
+ print(f"gateway error: {e}", file=sys.stderr)
531
+ sys.exit(3)
532
+ except TimeoutError as e:
533
+ print(f"timeout: {e}", file=sys.stderr)
534
+ sys.exit(4)
535
+
536
+
537
+ if __name__ == "__main__":
538
+ main()
muse_cli/desc0.bin ADDED
Binary file
muse_cli/desc1.bin ADDED
Binary file