muse-cli 0.3.6__tar.gz → 0.3.7__tar.gz
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-0.3.6 → muse_cli-0.3.7}/PKG-INFO +3 -3
- {muse_cli-0.3.6 → muse_cli-0.3.7}/README.md +2 -2
- {muse_cli-0.3.6 → muse_cli-0.3.7}/docs/PROTOCOL.md +10 -2
- {muse_cli-0.3.6 → muse_cli-0.3.7}/skills/muse-cli/SKILL.md +7 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/__init__.py +1 -1
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/chat.py +2 -6
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/cli.py +63 -12
- {muse_cli-0.3.6 → muse_cli-0.3.7}/.gitignore +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/LICENSE +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/pyproject.toml +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/__main__.py +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/desc0.bin +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/desc1.bin +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/gateway.py +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/routes.json +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/skill.py +0 -0
- {muse_cli-0.3.6 → muse_cli-0.3.7}/src/muse_cli/update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: muse-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Command-line client for your personal muse.ai AI agent: chat, automate, and manage side chats, feed, goals, ideas, and sessions from the terminal. No browser needed.
|
|
5
5
|
Project-URL: Homepage, https://github.com/nikships/muse-cli
|
|
6
6
|
Project-URL: Documentation, https://github.com/nikships/muse-cli#readme
|
|
@@ -206,13 +206,13 @@ muse-cli watch --timeout 60 # tail live agent events
|
|
|
206
206
|
muse-cli files --limit 20 # images/videos in recent messages
|
|
207
207
|
muse-cli download <seq|message-id|sandbox://...> -o ./out # save agent-made files
|
|
208
208
|
muse-cli history --limit 3 --download ./out # save files from these messages
|
|
209
|
-
muse-cli send "make a cat video" --wait 300 --download ./out
|
|
209
|
+
muse-cli send "make a cat video" --wait 300 --download ./out # waits for the chat to go quiet (15 s), at most --wait
|
|
210
210
|
muse-cli watch --thread <session-id> # tail one side chat
|
|
211
211
|
|
|
212
212
|
muse-cli unread # unread counts
|
|
213
213
|
muse-cli feed --limit 5
|
|
214
214
|
muse-cli feed-status # also: feed-prompt
|
|
215
|
-
muse-cli feed-react <unit-id> love
|
|
215
|
+
muse-cli feed-react <unit-id> love # or like, dislike
|
|
216
216
|
muse-cli goals # one goal: muse-cli goal <id>
|
|
217
217
|
muse-cli ideas # one idea: muse-cli idea <id>
|
|
218
218
|
muse-cli idea-exec <idea-id> # agent acts on the idea
|
|
@@ -169,13 +169,13 @@ muse-cli watch --timeout 60 # tail live agent events
|
|
|
169
169
|
muse-cli files --limit 20 # images/videos in recent messages
|
|
170
170
|
muse-cli download <seq|message-id|sandbox://...> -o ./out # save agent-made files
|
|
171
171
|
muse-cli history --limit 3 --download ./out # save files from these messages
|
|
172
|
-
muse-cli send "make a cat video" --wait 300 --download ./out
|
|
172
|
+
muse-cli send "make a cat video" --wait 300 --download ./out # waits for the chat to go quiet (15 s), at most --wait
|
|
173
173
|
muse-cli watch --thread <session-id> # tail one side chat
|
|
174
174
|
|
|
175
175
|
muse-cli unread # unread counts
|
|
176
176
|
muse-cli feed --limit 5
|
|
177
177
|
muse-cli feed-status # also: feed-prompt
|
|
178
|
-
muse-cli feed-react <unit-id> love
|
|
178
|
+
muse-cli feed-react <unit-id> love # or like, dislike
|
|
179
179
|
muse-cli goals # one goal: muse-cli goal <id>
|
|
180
180
|
muse-cli ideas # one idea: muse-cli idea <id>
|
|
181
181
|
muse-cli idea-exec <idea-id> # agent acts on the idea
|
|
@@ -54,6 +54,9 @@ chat / feed / goals / ideas / sessions / ...
|
|
|
54
54
|
- Genuine assistant replies have an **empty `reply_to_message_id`**; proactive
|
|
55
55
|
pushes (Telegram drafts, background task updates) are self-referential
|
|
56
56
|
there. That is how `send` tells a reply apart from background chatter.
|
|
57
|
+
- Generated media arrives as a **separate** assistant event after the text
|
|
58
|
+
reply, ~20 s later for an image. `send --download` therefore waits until
|
|
59
|
+
the history seq has been quiet for `SETTLE` seconds before collecting files.
|
|
57
60
|
- Live `chat.subscribe` events use **different shapes** (`delta.text_append`
|
|
58
61
|
with `message_seq` + text chunks, `delta.message_done` with the full
|
|
59
62
|
transcript) and only cover main-chat traffic: threaded replies never arrive
|
|
@@ -76,10 +79,15 @@ chat / feed / goals / ideas / sessions / ...
|
|
|
76
79
|
tunnel it returns the file (body chunks). `Gateway.download_file()` wraps
|
|
77
80
|
this. `artifacts.fetch_url` is unrelated (needs `project_dir`).
|
|
78
81
|
- `session.start`: `{method: "/api/session/start", params: {origin: "fresh",
|
|
79
|
-
lifecycle: "persistent"
|
|
82
|
+
lifecycle: "persistent"}}`. A `title` param is accepted but ignored (the
|
|
83
|
+
chat stays untitled), so `start_session` follows up with `session.rename`.
|
|
80
84
|
- `session.rename`: flat `{session_id, title}`. pin/unpin/archive/unarchive/
|
|
81
85
|
delete: `{method: "/api/session/<op>", session_id}`.
|
|
82
|
-
- `
|
|
86
|
+
- `feed.unit.reaction`: `{reaction}` must be `like`, `love` or `dislike` (the
|
|
87
|
+
server's 400 lists them).
|
|
88
|
+
- `api.idea-cards.execute`: `{ideaCardId, mode: "full"}` plus path param. A
|
|
89
|
+
`session_id` in the body did not move the run into that side chat; the
|
|
90
|
+
user message and the agent's reply landed in the main chat.
|
|
83
91
|
- POSTs to muse.ai need browser `Sec-Fetch-*` headers or they return 403.
|
|
84
92
|
- `auth export` reads cookies from the user's own Google Chrome through
|
|
85
93
|
agent-browser `--auto-connect`. Chrome 144+ shares that profile only after
|
|
@@ -174,6 +174,13 @@ only for methods with no wrapper.
|
|
|
174
174
|
than the wait: the message still landed, confirm with `history`. The reply
|
|
175
175
|
is matched as a genuine answer, not background chatter.
|
|
176
176
|
- `send --wait 0` is fire-and-forget (no polling).
|
|
177
|
+
- `send --download DIR` keeps waiting after the reply until the thread has been
|
|
178
|
+
quiet for 15 s (so it always takes at least that long), because generated
|
|
179
|
+
images and videos arrive as separate messages after the text. The wait stops
|
|
180
|
+
at `--wait`; if that comes first the result has a `download_note` and files
|
|
181
|
+
may be missing.
|
|
182
|
+
- `idea-exec --thread` may be ignored by the server; the run can land in the
|
|
183
|
+
main chat.
|
|
177
184
|
- Running `muse-cli` with no command opens the interactive chat and blocks on
|
|
178
185
|
stdin. From an agent, always pass an explicit command such as `status` or
|
|
179
186
|
`send`.
|
|
@@ -14,7 +14,7 @@ try:
|
|
|
14
14
|
except ImportError:
|
|
15
15
|
readline = None
|
|
16
16
|
|
|
17
|
-
from .cli import CONFIG_DIR, cmd_auth_export, connect, fmt_event, load_config
|
|
17
|
+
from .cli import CONFIG_DIR, cmd_auth_export, connect, fmt_event, load_config, start_session
|
|
18
18
|
from .gateway import AuthError, GatewayError
|
|
19
19
|
|
|
20
20
|
WAIT = 300 # seconds to wait for the first reply
|
|
@@ -168,11 +168,7 @@ class Chat:
|
|
|
168
168
|
print("No conversation with that number. Type /chats to see them.")
|
|
169
169
|
|
|
170
170
|
def new_chat(self, title):
|
|
171
|
-
|
|
172
|
-
if title:
|
|
173
|
-
params["title"] = title
|
|
174
|
-
d = self.gw.call_json("session.start",
|
|
175
|
-
body={"method": "/api/session/start", "params": params})
|
|
171
|
+
d = start_session(self.gw, title)
|
|
176
172
|
self.switch(d.get("session_id") or d.get("id"), title or "new conversation")
|
|
177
173
|
|
|
178
174
|
def reconnect(self):
|
|
@@ -522,6 +522,11 @@ def is_reply(ev, baseline):
|
|
|
522
522
|
# unary call; at 4 s a finished reply sat unshown for up to 4 s.
|
|
523
523
|
POLL = 1
|
|
524
524
|
|
|
525
|
+
# With --download, how long the thread must stay quiet after the reply before
|
|
526
|
+
# send collects files. Generated media arrives as separate events well after
|
|
527
|
+
# the text (an image took ~20 s), so a short fixed sleep missed it.
|
|
528
|
+
SETTLE = 15
|
|
529
|
+
|
|
525
530
|
|
|
526
531
|
def cmd_send(args):
|
|
527
532
|
cfg = load_config()
|
|
@@ -571,9 +576,26 @@ def cmd_send(args):
|
|
|
571
576
|
result = {"sent": True, "stream": stream_sid}
|
|
572
577
|
if reply and args.download:
|
|
573
578
|
# Media often lands as separate presentation events after the
|
|
574
|
-
# text reply;
|
|
575
|
-
# than the baseline.
|
|
576
|
-
|
|
579
|
+
# text reply; wait for the thread to go quiet, then save every
|
|
580
|
+
# file newer than the baseline.
|
|
581
|
+
last_seq, quiet_since, settled = reply["seq"] or 0, time.time(), False
|
|
582
|
+
while time.time() < deadline:
|
|
583
|
+
if time.time() - quiet_since >= SETTLE:
|
|
584
|
+
settled = True
|
|
585
|
+
break
|
|
586
|
+
time.sleep(POLL)
|
|
587
|
+
try:
|
|
588
|
+
h = gw.call_json("chat.history", body={"limit": 20, **scope})
|
|
589
|
+
except (GatewayError, TimeoutError):
|
|
590
|
+
continue
|
|
591
|
+
except AuthError:
|
|
592
|
+
break
|
|
593
|
+
seq = max([e.get("seq") or 0 for e in h.get("chat_events", [])] or [0])
|
|
594
|
+
if seq > last_seq:
|
|
595
|
+
last_seq, quiet_since = seq, time.time()
|
|
596
|
+
if not settled:
|
|
597
|
+
result["download_note"] = ("stopped waiting before the thread went quiet; "
|
|
598
|
+
"files may be missing, check `muse-cli history --download`")
|
|
577
599
|
try:
|
|
578
600
|
h = gw.call_json("chat.history", body={"limit": 20, **scope})
|
|
579
601
|
res = []
|
|
@@ -583,7 +605,7 @@ def cmd_send(args):
|
|
|
583
605
|
if r["path"] not in [x["path"] for x in res]:
|
|
584
606
|
res.append(r)
|
|
585
607
|
result["downloads"] = save_resources(gw, res, args.download)
|
|
586
|
-
except (GatewayError, TimeoutError) as e:
|
|
608
|
+
except (AuthError, GatewayError, TimeoutError) as e:
|
|
587
609
|
result["download_error"] = str(e)
|
|
588
610
|
if reply:
|
|
589
611
|
result["reply"] = reply
|
|
@@ -738,14 +760,27 @@ def cmd_seen(args):
|
|
|
738
760
|
gw.close()
|
|
739
761
|
|
|
740
762
|
|
|
763
|
+
def start_session(gw, title=None):
|
|
764
|
+
res = gw.call_json("session.start",
|
|
765
|
+
body={"method": "/api/session/start",
|
|
766
|
+
"params": {"origin": "fresh", "lifecycle": "persistent"}})
|
|
767
|
+
# The server ignores a title on start, so name the chat afterwards.
|
|
768
|
+
sid = res.get("session_id") or res.get("id")
|
|
769
|
+
if title and sid:
|
|
770
|
+
# The chat already exists, so a failed rename must not hide its id.
|
|
771
|
+
try:
|
|
772
|
+
gw.call_json("session.rename", body={"session_id": sid, "title": title})
|
|
773
|
+
res["title"] = title
|
|
774
|
+
except (GatewayError, TimeoutError) as e:
|
|
775
|
+
res["title_error"] = str(e)
|
|
776
|
+
print(f"could not set the chat title: {e}", file=sys.stderr)
|
|
777
|
+
return res
|
|
778
|
+
|
|
779
|
+
|
|
741
780
|
def cmd_session_start(args):
|
|
742
781
|
gw = connect(load_config())
|
|
743
782
|
try:
|
|
744
|
-
|
|
745
|
-
if args.title:
|
|
746
|
-
params["title"] = args.title
|
|
747
|
-
out(gw.call_json("session.start",
|
|
748
|
-
body={"method": "/api/session/start", "params": params}))
|
|
783
|
+
out(start_session(gw, args.title))
|
|
749
784
|
finally:
|
|
750
785
|
gw.close()
|
|
751
786
|
|
|
@@ -860,7 +895,8 @@ def main():
|
|
|
860
895
|
p.add_argument("text"); p.add_argument("--thread", default=None)
|
|
861
896
|
p.add_argument("--wait", type=int, default=90, help="seconds to wait for reply (0 = don't)")
|
|
862
897
|
p.add_argument("--download", metavar="DIR", default=None,
|
|
863
|
-
help="save files the reply produced into DIR (needs --wait)"
|
|
898
|
+
help="save files the reply produced into DIR (needs --wait); waits until the "
|
|
899
|
+
"chat is quiet for 15 s, at most --wait")
|
|
864
900
|
p.set_defaults(fn=cmd_send)
|
|
865
901
|
p = sub.add_parser("files", help="list files (images/videos) in recent messages")
|
|
866
902
|
p.add_argument("--thread", default=None); p.add_argument("--limit", type=int, default=20)
|
|
@@ -881,7 +917,8 @@ def main():
|
|
|
881
917
|
p.set_defaults(fn=cmd_feed)
|
|
882
918
|
sub.add_parser("feed-status", help="feed refresh status").set_defaults(fn=cmd_feed_status)
|
|
883
919
|
p = sub.add_parser("feed-react", help="react to a feed unit")
|
|
884
|
-
p.add_argument("unit"); p.add_argument("reaction", nargs="?", default="love"
|
|
920
|
+
p.add_argument("unit"); p.add_argument("reaction", nargs="?", default="love",
|
|
921
|
+
choices=["like", "love", "dislike"], help="default: love")
|
|
885
922
|
p.set_defaults(fn=cmd_feed_react)
|
|
886
923
|
sub.add_parser("feed-prompt", help="show feed prompt").set_defaults(fn=cmd_feed_prompt)
|
|
887
924
|
sub.add_parser("goals", help="list goals").set_defaults(fn=cmd_goals)
|
|
@@ -889,7 +926,10 @@ def main():
|
|
|
889
926
|
sub.add_parser("ideas", help="list idea cards").set_defaults(fn=cmd_ideas)
|
|
890
927
|
p = sub.add_parser("idea", help="show one idea"); p.add_argument("id"); p.set_defaults(fn=cmd_idea)
|
|
891
928
|
p = sub.add_parser("idea-exec", help="execute an idea (agent acts on it)")
|
|
892
|
-
p.add_argument("id")
|
|
929
|
+
p.add_argument("id")
|
|
930
|
+
p.add_argument("--thread", default=None,
|
|
931
|
+
help="side chat to run it in (the server may ignore this and use the main chat)")
|
|
932
|
+
p.set_defaults(fn=cmd_idea_exec)
|
|
893
933
|
sub.add_parser("unread", help="unread counts").set_defaults(fn=cmd_unread)
|
|
894
934
|
p = sub.add_parser("seen", help="mark a thread seen"); p.add_argument("thread")
|
|
895
935
|
p.set_defaults(fn=cmd_seen)
|
|
@@ -922,6 +962,9 @@ def main():
|
|
|
922
962
|
try:
|
|
923
963
|
try:
|
|
924
964
|
args.fn(args)
|
|
965
|
+
# Flush here: with a block-buffered pipe, a closed reader only
|
|
966
|
+
# surfaces at flush time, which would otherwise be after this handler.
|
|
967
|
+
sys.stdout.flush()
|
|
925
968
|
except AuthError as e:
|
|
926
969
|
print(f"auth error: {e}", file=sys.stderr)
|
|
927
970
|
sys.exit(2)
|
|
@@ -931,6 +974,14 @@ def main():
|
|
|
931
974
|
except TimeoutError as e:
|
|
932
975
|
print(f"timeout: {e}", file=sys.stderr)
|
|
933
976
|
sys.exit(4)
|
|
977
|
+
except BrokenPipeError:
|
|
978
|
+
# The reader (`head`, `jq -e`) closed early. Point stdout at devnull
|
|
979
|
+
# so the interpreter's exit-time flush does not raise again.
|
|
980
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
981
|
+
sys.exit(141)
|
|
982
|
+
except KeyboardInterrupt:
|
|
983
|
+
print(file=sys.stderr)
|
|
984
|
+
sys.exit(130)
|
|
934
985
|
finally:
|
|
935
986
|
finish_update_check(notice)
|
|
936
987
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|