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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: muse-cli
3
- Version: 0.3.6
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", title?}}`.
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
- - `api.idea-cards.execute`: `{ideaCardId, mode: "full"}` plus path param.
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`.
@@ -1,3 +1,3 @@
1
1
  """muse-cli: talk to your personal muse.ai AI agent from the terminal."""
2
2
 
3
- __version__ = "0.3.6"
3
+ __version__ = "0.3.7"
@@ -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
- params = {"origin": "fresh", "lifecycle": "persistent"}
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; give them a moment, then save every file newer
575
- # than the baseline.
576
- time.sleep(min(3, max(0, deadline - time.time())))
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
- params = {"origin": "fresh", "lifecycle": "persistent"}
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"); p.add_argument("--thread", default=None); p.set_defaults(fn=cmd_idea_exec)
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