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 +3 -0
- muse_cli/__main__.py +3 -0
- muse_cli/cli.py +538 -0
- muse_cli/desc0.bin +0 -0
- muse_cli/desc1.bin +0 -0
- muse_cli/gateway.py +336 -0
- muse_cli/routes.json +1410 -0
- muse_cli-0.2.0.dist-info/METADATA +195 -0
- muse_cli-0.2.0.dist-info/RECORD +12 -0
- muse_cli-0.2.0.dist-info/WHEEL +4 -0
- muse_cli-0.2.0.dist-info/entry_points.txt +2 -0
- muse_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
muse_cli/__init__.py
ADDED
muse_cli/__main__.py
ADDED
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
|