muse-cli 0.3.3__tar.gz → 0.3.5__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.3 → muse_cli-0.3.5}/.gitignore +1 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/PKG-INFO +15 -4
- {muse_cli-0.3.3 → muse_cli-0.3.5}/README.md +14 -3
- {muse_cli-0.3.3 → muse_cli-0.3.5}/docs/PROTOCOL.md +5 -5
- {muse_cli-0.3.3 → muse_cli-0.3.5}/pyproject.toml +3 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/skills/muse-cli/SKILL.md +10 -5
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/__init__.py +1 -1
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/chat.py +20 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/cli.py +16 -6
- muse_cli-0.3.5/src/muse_cli/skill.py +62 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/update.py +30 -2
- {muse_cli-0.3.3 → muse_cli-0.3.5}/LICENSE +0 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/__main__.py +0 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/desc0.bin +0 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/desc1.bin +0 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/gateway.py +0 -0
- {muse_cli-0.3.3 → muse_cli-0.3.5}/src/muse_cli/routes.json +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.5
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
4
|
-
own client bundle and verified live. Internal APIs are unversioned and
|
|
5
|
-
change without notice; if calls start failing, re-derive from a fresh
|
|
6
|
-
(HAR capture
|
|
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
|
|
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
|
|
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.
|
|
@@ -6,6 +6,13 @@ reader. Picks up the conversation you used last. Type /help for commands.
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import time
|
|
9
|
+
# Enable GNU readline / libedit for line editing, history, and proper
|
|
10
|
+
# CJK/wide-character deletion (wcwidth). It is a standard library C extension,
|
|
11
|
+
# but unavailable on native Windows or stripped/minimal Python installations.
|
|
12
|
+
try:
|
|
13
|
+
import readline
|
|
14
|
+
except ImportError:
|
|
15
|
+
readline = None
|
|
9
16
|
|
|
10
17
|
from .cli import CONFIG_DIR, cmd_auth_export, connect, fmt_event, load_config
|
|
11
18
|
from .gateway import AuthError, GatewayError
|
|
@@ -14,6 +21,7 @@ WAIT = 300 # seconds to wait for the first reply
|
|
|
14
21
|
QUIET = 10 # seconds of silence after a reply before taking the next message
|
|
15
22
|
POLL = 1 # seconds between history reads while waiting for a reply
|
|
16
23
|
STATE = os.path.join(CONFIG_DIR, "chat.json")
|
|
24
|
+
HISTFILE = os.path.join(CONFIG_DIR, "chat_history")
|
|
17
25
|
|
|
18
26
|
HELP = """Commands:
|
|
19
27
|
/chats list your conversations
|
|
@@ -214,6 +222,11 @@ def start():
|
|
|
214
222
|
|
|
215
223
|
|
|
216
224
|
def cmd_chat(_args):
|
|
225
|
+
if readline is not None:
|
|
226
|
+
try:
|
|
227
|
+
readline.read_history_file(HISTFILE)
|
|
228
|
+
except OSError:
|
|
229
|
+
pass
|
|
217
230
|
chat = start()
|
|
218
231
|
print(f"Muse chat, in: {chat.st['title']}. Type /help for commands, /exit to quit.")
|
|
219
232
|
try:
|
|
@@ -255,5 +268,12 @@ def cmd_chat(_args):
|
|
|
255
268
|
except KeyboardInterrupt:
|
|
256
269
|
pass
|
|
257
270
|
finally:
|
|
271
|
+
if readline is not None:
|
|
272
|
+
try:
|
|
273
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
274
|
+
readline.set_history_length(1000)
|
|
275
|
+
readline.write_history_file(HISTFILE)
|
|
276
|
+
except OSError:
|
|
277
|
+
pass
|
|
258
278
|
chat.gw.close()
|
|
259
279
|
print("\nBye.")
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("/"))
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|