muse-cli 0.3.4__tar.gz → 0.3.6__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.
@@ -5,6 +5,7 @@ __pycache__/
5
5
  *.egg-info/
6
6
  dist/
7
7
  build/
8
+ uv.lock
8
9
 
9
10
  # Local secrets and state (never commit)
10
11
  cookies.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: muse-cli
3
- Version: 0.3.4
3
+ Version: 0.3.6
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
@@ -46,7 +46,7 @@ Talk to your personal muse.ai AI agent from the terminal.
46
46
  [![Downloads](https://img.shields.io/pepy/dt/muse-cli?style=for-the-badge)](https://pepy.tech/project/muse-cli)
47
47
  [![License: MIT](https://img.shields.io/github/license/nikships/muse-cli?style=for-the-badge)](https://github.com/nikships/muse-cli/blob/main/LICENSE)
48
48
  [![GitHub stars](https://img.shields.io/github/stars/nikships/muse-cli?style=for-the-badge)](https://github.com/nikships/muse-cli/stargazers)
49
- [![Website](https://img.shields.io/badge/website-live-3fb950?style=for-the-badge)](https://muse-cli-site.web.app)
49
+ [![Website](https://img.shields.io/badge/website-live-3fb950?style=for-the-badge)](https://muse-cli-web.web.app)
50
50
 
51
51
  </div>
52
52
 
@@ -84,7 +84,7 @@ to `~/.agents/skills/muse-cli`:
84
84
  curl -fsSL https://raw.githubusercontent.com/nikships/muse-cli/main/install.sh | bash
85
85
  ```
86
86
 
87
- Upgrade with `muse-cli update`. In a terminal it also checks PyPI once a day and, when a newer release exists, prints that command on stderr. It does not upgrade itself. Set `MUSE_NO_UPDATE_CHECK=1` to silence the notice. The check stays quiet when output is piped or `CI` is set. Remove with `uv tool uninstall muse-cli`.
87
+ Upgrade with `muse-cli update`. It also refreshes the agent skill if you installed one (the skill ships inside the package, so it always matches the CLI). `muse-cli update --skill` refreshes or installs just the skill, with no package upgrade, which is handy after `pip` or `pipx` installs. In a terminal it also checks PyPI once a day and, when a newer release exists, prints that command on stderr. It does not upgrade itself. Set `MUSE_NO_UPDATE_CHECK=1` to silence the notice. The check stays quiet when output is piped or `CI` is set. Remove with `uv tool uninstall muse-cli`.
88
88
 
89
89
  Logging in is a separate one-time step. Chrome shares its cookies after you
90
90
  turn on remote debugging. Follow [Log in once](#log-in-once) before running
@@ -209,10 +209,12 @@ muse-cli history --limit 3 --download ./out # save files from these messag
209
209
  muse-cli send "make a cat video" --wait 300 --download ./out
210
210
  muse-cli watch --thread <session-id> # tail one side chat
211
211
 
212
+ muse-cli unread # unread counts
212
213
  muse-cli feed --limit 5
214
+ muse-cli feed-status # also: feed-prompt
213
215
  muse-cli feed-react <unit-id> love
214
- muse-cli goals
215
- muse-cli ideas
216
+ muse-cli goals # one goal: muse-cli goal <id>
217
+ muse-cli ideas # one idea: muse-cli idea <id>
216
218
  muse-cli idea-exec <idea-id> # agent acts on the idea
217
219
 
218
220
  muse-cli session-start --title "trip planning" # new side chat
@@ -268,17 +270,26 @@ uv run python -m unittest discover -s tests # unit tests (no network)
268
270
  muse-cli/
269
271
  assets/ README artwork (generated with Muse Image)
270
272
  docs/PROTOCOL.md protocol reference for re-derivation
273
+ scripts/ release version bump (used by CI)
274
+ site/ landing page (Firebase Hosting)
271
275
  skills/muse-cli/ agent skill
272
276
  src/muse_cli/
273
277
  cli.py argument parsing and all commands
278
+ chat.py interactive chat (the default command)
274
279
  gateway.py gateway client (auth, Noise transport, subscriptions)
280
+ update.py daily update notice and `muse-cli update`
281
+ skill.py installs and refreshes the bundled agent skill
275
282
  routes.json 258 gateway methods extracted from the web client
276
283
  desc0.bin protobuf descriptors for the wire framing
277
284
  desc1.bin
285
+ tests/ unit tests (no network)
278
286
  install.sh CLI + skill installer
279
287
  pyproject.toml
280
288
  ```
281
289
 
290
+ See [AGENTS.md](https://github.com/nikships/muse-cli/blob/main/AGENTS.md) for
291
+ release flow and contributor notes. Merging to `main` publishes a release.
292
+
282
293
  ## Setup notes
283
294
 
284
295
  - Login is the [Log in once](#log-in-once) section above. Cookies live at
@@ -9,7 +9,7 @@ Talk to your personal muse.ai AI agent from the terminal.
9
9
  [![Downloads](https://img.shields.io/pepy/dt/muse-cli?style=for-the-badge)](https://pepy.tech/project/muse-cli)
10
10
  [![License: MIT](https://img.shields.io/github/license/nikships/muse-cli?style=for-the-badge)](https://github.com/nikships/muse-cli/blob/main/LICENSE)
11
11
  [![GitHub stars](https://img.shields.io/github/stars/nikships/muse-cli?style=for-the-badge)](https://github.com/nikships/muse-cli/stargazers)
12
- [![Website](https://img.shields.io/badge/website-live-3fb950?style=for-the-badge)](https://muse-cli-site.web.app)
12
+ [![Website](https://img.shields.io/badge/website-live-3fb950?style=for-the-badge)](https://muse-cli-web.web.app)
13
13
 
14
14
  </div>
15
15
 
@@ -47,7 +47,7 @@ to `~/.agents/skills/muse-cli`:
47
47
  curl -fsSL https://raw.githubusercontent.com/nikships/muse-cli/main/install.sh | bash
48
48
  ```
49
49
 
50
- Upgrade with `muse-cli update`. In a terminal it also checks PyPI once a day and, when a newer release exists, prints that command on stderr. It does not upgrade itself. Set `MUSE_NO_UPDATE_CHECK=1` to silence the notice. The check stays quiet when output is piped or `CI` is set. Remove with `uv tool uninstall muse-cli`.
50
+ Upgrade with `muse-cli update`. It also refreshes the agent skill if you installed one (the skill ships inside the package, so it always matches the CLI). `muse-cli update --skill` refreshes or installs just the skill, with no package upgrade, which is handy after `pip` or `pipx` installs. In a terminal it also checks PyPI once a day and, when a newer release exists, prints that command on stderr. It does not upgrade itself. Set `MUSE_NO_UPDATE_CHECK=1` to silence the notice. The check stays quiet when output is piped or `CI` is set. Remove with `uv tool uninstall muse-cli`.
51
51
 
52
52
  Logging in is a separate one-time step. Chrome shares its cookies after you
53
53
  turn on remote debugging. Follow [Log in once](#log-in-once) before running
@@ -172,10 +172,12 @@ muse-cli history --limit 3 --download ./out # save files from these messag
172
172
  muse-cli send "make a cat video" --wait 300 --download ./out
173
173
  muse-cli watch --thread <session-id> # tail one side chat
174
174
 
175
+ muse-cli unread # unread counts
175
176
  muse-cli feed --limit 5
177
+ muse-cli feed-status # also: feed-prompt
176
178
  muse-cli feed-react <unit-id> love
177
- muse-cli goals
178
- muse-cli ideas
179
+ muse-cli goals # one goal: muse-cli goal <id>
180
+ muse-cli ideas # one idea: muse-cli idea <id>
179
181
  muse-cli idea-exec <idea-id> # agent acts on the idea
180
182
 
181
183
  muse-cli session-start --title "trip planning" # new side chat
@@ -231,17 +233,26 @@ uv run python -m unittest discover -s tests # unit tests (no network)
231
233
  muse-cli/
232
234
  assets/ README artwork (generated with Muse Image)
233
235
  docs/PROTOCOL.md protocol reference for re-derivation
236
+ scripts/ release version bump (used by CI)
237
+ site/ landing page (Firebase Hosting)
234
238
  skills/muse-cli/ agent skill
235
239
  src/muse_cli/
236
240
  cli.py argument parsing and all commands
241
+ chat.py interactive chat (the default command)
237
242
  gateway.py gateway client (auth, Noise transport, subscriptions)
243
+ update.py daily update notice and `muse-cli update`
244
+ skill.py installs and refreshes the bundled agent skill
238
245
  routes.json 258 gateway methods extracted from the web client
239
246
  desc0.bin protobuf descriptors for the wire framing
240
247
  desc1.bin
248
+ tests/ unit tests (no network)
241
249
  install.sh CLI + skill installer
242
250
  pyproject.toml
243
251
  ```
244
252
 
253
+ See [AGENTS.md](https://github.com/nikships/muse-cli/blob/main/AGENTS.md) for
254
+ release flow and contributor notes. Merging to `main` publishes a release.
255
+
245
256
  ## Setup notes
246
257
 
247
258
  - Login is the [Log in once](#log-in-once) section above. Cookies live at
@@ -1,14 +1,14 @@
1
1
  # Protocol notes
2
2
 
3
- How `muse` talks to the muse.ai personal gateway. Derived from the web app's
4
- own client bundle and verified live. Internal APIs are unversioned and can
5
- change without notice; if calls start failing, re-derive from a fresh bundle
6
- (HAR capture + the `derive-client` workflow).
3
+ How `muse-cli` talks to the muse.ai personal gateway. Derived from the web
4
+ app's own client bundle and verified live. Internal APIs are unversioned and
5
+ can change without notice; if calls start failing, re-derive from a fresh
6
+ bundle (for example a HAR capture of the web app).
7
7
 
8
8
  ## Layers
9
9
 
10
10
  ```
11
- muse CLI
11
+ muse-cli
12
12
  │ HTTPS (browser TLS fingerprint via curl-impersonated requests)
13
13
  ▼
14
14
  muse.ai cookies → POST /api/auth/check → access_token
@@ -75,6 +75,9 @@ path = "src/muse_cli/__init__.py"
75
75
  [tool.hatch.build.targets.wheel]
76
76
  packages = ["src/muse_cli"]
77
77
 
78
+ [tool.hatch.build.targets.wheel.force-include]
79
+ "skills/muse-cli/SKILL.md" = "muse_cli/SKILL.md"
80
+
78
81
  [tool.hatch.build.targets.sdist]
79
82
  include = [
80
83
  "src/muse_cli",
@@ -11,7 +11,8 @@ fails and report it.
11
11
 
12
12
  The `muse-cli` package (on PyPI) talks to the muse.ai personal gateway
13
13
  directly. No browser needed after the one-time cookie export. Every command
14
- prints JSON.
14
+ prints JSON except the interactive chat (`muse-cli` with no arguments, or
15
+ `muse-cli chat`), which is a human REPL. Agents should never run it.
15
16
 
16
17
  ## 1. Install
17
18
 
@@ -143,9 +144,9 @@ muse-cli files --limit 20 # images/videos the agen
143
144
  muse-cli download <seq|message-id|sandbox://...> -o DIR # save them locally
144
145
  muse-cli history --limit 3 --download DIR # save files from these messages
145
146
  muse-cli send "make an image of X" --wait 300 --download DIR
146
- muse-cli feed --limit 5
147
- muse-cli goals
148
- muse-cli ideas
147
+ muse-cli feed --limit 5 # also: feed-status, feed-prompt
148
+ muse-cli goals # one goal: muse-cli goal <id>
149
+ muse-cli ideas # one idea: muse-cli idea <id>
149
150
  muse-cli unread
150
151
  muse-cli seen <thread-id>
151
152
  ```
@@ -173,6 +174,9 @@ only for methods with no wrapper.
173
174
  than the wait: the message still landed, confirm with `history`. The reply
174
175
  is matched as a genuine answer, not background chatter.
175
176
  - `send --wait 0` is fire-and-forget (no polling).
177
+ - Running `muse-cli` with no command opens the interactive chat and blocks on
178
+ stdin. From an agent, always pass an explicit command such as `status` or
179
+ `send`.
176
180
  - `history` without `--thread` reads the main chat only.
177
181
  - Agent media shows up as `sandbox://workspace/...` paths, often in a
178
182
  separate `delta.presentation` event after the text reply. `download` looks
@@ -184,6 +188,7 @@ only for methods with no wrapper.
184
188
  - Respect rate limits. Writes (send, react, execute, session ops) act as the
185
189
  user in their agent: announce them before running, never loop them.
186
190
  - A stderr line `A new release of muse-cli is available` is an upgrade
187
- notice, not a failed command. Upgrade with `muse-cli update`. The check
191
+ notice, not a failed command. Upgrade with `muse-cli update`, which also refreshes this skill
192
+ (`muse-cli update --skill` refreshes only the skill). The check
188
193
  runs at most once a day, only in a terminal, and never changes the install
189
194
  by itself. `MUSE_NO_UPDATE_CHECK=1` silences it.
@@ -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.4"
3
+ __version__ = "0.3.6"
@@ -15,6 +15,7 @@ import re
15
15
  import shutil
16
16
  import sys
17
17
  import time
18
+ import urllib.parse
18
19
 
19
20
  from . import __version__
20
21
  from .gateway import Gateway, AuthError, GatewayError, load_cookies
@@ -29,7 +30,8 @@ def load_config():
29
30
  import secrets
30
31
  cfg = {}
31
32
  if os.path.exists(CONFIG_FILE):
32
- cfg = json.load(open(CONFIG_FILE))
33
+ with open(CONFIG_FILE) as fh:
34
+ cfg = json.load(fh)
33
35
  changed = False
34
36
  cfg.setdefault("cookies_file", COOKIES_FILE)
35
37
  if "vm_id" not in cfg and os.environ.get("MUSE_VM_ID"):
@@ -42,7 +44,8 @@ def load_config():
42
44
  changed = True
43
45
  if changed:
44
46
  os.makedirs(CONFIG_DIR, exist_ok=True)
45
- json.dump(cfg, open(CONFIG_FILE, "w"), indent=2)
47
+ with open(CONFIG_FILE, "w") as fh:
48
+ json.dump(cfg, fh, indent=2)
46
49
  return cfg
47
50
 
48
51
 
@@ -284,9 +287,11 @@ def cmd_auth_export(_args):
284
287
  ]))
285
288
  if os.path.exists(COOKIES_FILE):
286
289
  os.replace(COOKIES_FILE, COOKIES_FILE + ".bak")
287
- with open(COOKIES_FILE, "w") as fh:
290
+ # Create the file at 0600 so the cookies are never readable by others,
291
+ # not even between the write and a later chmod.
292
+ fd = os.open(COOKIES_FILE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
293
+ with os.fdopen(fd, "w") as fh:
288
294
  fh.write("\n".join(lines) + "\n")
289
- os.chmod(COOKIES_FILE, 0o600)
290
295
  print(f"saved {len(jar)} muse.ai cookies to {COOKIES_FILE}")
291
296
 
292
297
 
@@ -405,7 +410,9 @@ def event_resources(ev):
405
410
 
406
411
 
407
412
  def _local_name(path, directory):
408
- name = os.path.basename(path.split("?", 1)[0].rstrip("/")) or "download"
413
+ name = os.path.basename(urllib.parse.unquote(path.split("?", 1)[0]).rstrip("/"))
414
+ if name in ("", ".", ".."):
415
+ name = "download"
409
416
  return os.path.join(directory, name)
410
417
 
411
418
 
@@ -897,7 +904,10 @@ def main():
897
904
  p.add_argument("title")
898
905
  p.set_defaults(fn=cmd_session_op(kind))
899
906
  sub.add_parser("wake", help="request a VM wake").set_defaults(fn=cmd_wake)
900
- sub.add_parser("update", help="upgrade this install to the latest release").set_defaults(fn=cmd_update)
907
+ p = sub.add_parser("update", help="upgrade this install and its agent skill to the latest release")
908
+ p.add_argument("--skill", action="store_true",
909
+ help="only install or refresh the agent skill (no package upgrade)")
910
+ p.set_defaults(fn=cmd_update)
901
911
  p = sub.add_parser("raw", help="call any gateway method (escape hatch)")
902
912
  p.add_argument("method"); p.add_argument("--body", default=None)
903
913
  p.add_argument("--param", action="append", default=[], help="path param k=v (repeatable)")
@@ -0,0 +1,62 @@
1
+ """Install and refresh the muse-cli agent skill (SKILL.md).
2
+
3
+ The skill ships inside the wheel, so it always matches the installed version.
4
+ `install.sh` puts it in the same default location.
5
+ """
6
+ import os
7
+ import tempfile
8
+
9
+ HERE = os.path.dirname(os.path.abspath(__file__))
10
+
11
+
12
+ def skill_path():
13
+ """Where coding agents look for the skill. MUSE_CLI_SKILLS matches install.sh."""
14
+ root = os.environ.get("MUSE_CLI_SKILLS") or os.path.expanduser("~/.agents/skills")
15
+ return os.path.join(root, "muse-cli", "SKILL.md")
16
+
17
+
18
+ def bundled_skill():
19
+ """Path of the SKILL.md that belongs to this version, or None."""
20
+ candidates = (
21
+ os.path.join(HERE, "SKILL.md"), # built wheel
22
+ os.path.join(HERE, "..", "..", "skills", "muse-cli", "SKILL.md"), # source checkout
23
+ )
24
+ for path in candidates:
25
+ if os.path.isfile(path):
26
+ return os.path.normpath(path)
27
+ return None
28
+
29
+
30
+ def sync_skill(create=False):
31
+ """Copy the bundled skill over the installed one.
32
+
33
+ Returns (status, path). Status is one of "updated", "installed", "current",
34
+ "absent" (not installed and create is False) or "no-source".
35
+ """
36
+ src = bundled_skill()
37
+ dest = skill_path()
38
+ if src is None:
39
+ return "no-source", dest
40
+ with open(src, "rb") as fh:
41
+ data = fh.read()
42
+ exists = os.path.exists(dest)
43
+ if exists:
44
+ with open(dest, "rb") as fh:
45
+ if fh.read() == data:
46
+ return "current", dest
47
+ elif not create:
48
+ return "absent", dest
49
+ directory = os.path.dirname(dest)
50
+ os.makedirs(directory, exist_ok=True)
51
+ mode = os.stat(dest).st_mode & 0o777 if exists else 0o644
52
+ fd, tmp = tempfile.mkstemp(dir=directory, prefix=".SKILL.md.")
53
+ try:
54
+ with os.fdopen(fd, "wb") as fh:
55
+ fh.write(data)
56
+ os.chmod(tmp, mode)
57
+ os.replace(tmp, dest)
58
+ except BaseException:
59
+ if os.path.exists(tmp):
60
+ os.unlink(tmp)
61
+ raise
62
+ return ("updated" if exists else "installed"), dest
@@ -13,6 +13,7 @@ import time
13
13
  import urllib.request
14
14
 
15
15
  from . import __version__
16
+ from .skill import skill_path
16
17
 
17
18
  CHECK_INTERVAL = 24 * 60 * 60
18
19
  FAIL_BACKOFF = 60 * 60
@@ -139,7 +140,27 @@ def finish_update_check(holder):
139
140
  )
140
141
 
141
142
 
142
- def cmd_update(_args):
143
+ def sync_skill_cmd():
144
+ """Install or refresh the agent skill. Returns an exit code."""
145
+ from .skill import sync_skill
146
+ try:
147
+ status, path = sync_skill(create=True)
148
+ except OSError as e:
149
+ print(f"could not write the skill: {e}", file=sys.stderr)
150
+ return 1
151
+ messages = {
152
+ "updated": f"skill updated: {path}",
153
+ "installed": f"skill installed: {path}",
154
+ "current": f"skill already up to date: {path}",
155
+ "no-source": "this install has no bundled skill; run install.sh to fetch it",
156
+ }
157
+ print(messages[status], file=sys.stderr)
158
+ return 1 if status == "no-source" else 0
159
+
160
+
161
+ def cmd_update(args):
162
+ if args.skill:
163
+ raise SystemExit(sync_skill_cmd())
143
164
  argv = upgrade_argv()
144
165
  if argv[0] != sys.executable and shutil.which(argv[0]) is None:
145
166
  print(f"{argv[0]} is not on PATH.", file=sys.stderr)
@@ -147,4 +168,11 @@ def cmd_update(_args):
147
168
  sys.exit(1)
148
169
  print(f"running: {upgrade_command()}", file=sys.stderr)
149
170
  import subprocess
150
- raise SystemExit(subprocess.call(argv))
171
+ code = subprocess.call(argv)
172
+ if code == 0 and os.path.exists(skill_path()):
173
+ # The upgrade replaced this package on disk, so refresh the skill from
174
+ # a new interpreter that loads the new version's bundled copy.
175
+ refreshed = subprocess.call([sys.executable, "-m", "muse_cli", "update", "--skill"])
176
+ if refreshed != 0:
177
+ print("skill not refreshed; run `muse-cli update --skill`", file=sys.stderr)
178
+ raise SystemExit(code)
File without changes
File without changes