@mu-cabin/muc-cli 0.5.2 → 0.5.3
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.
- package/README.md +2 -1
- package/REFERENCE.md +787 -0
- package/dist/muc.mjs +266 -15
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -280,7 +280,8 @@ packages/muc/ (everything-cli 工作区中的一个包)
|
|
|
280
280
|
├── scripts/build.mjs esbuild 打包脚本(→ dist/muc.mjs)
|
|
281
281
|
├── bin/muc tsx 入口(零构建直接跑 src/)
|
|
282
282
|
├── README.md 本文件
|
|
283
|
-
|
|
283
|
+
├── REFERENCE.md 完整命令 / 协议参考
|
|
284
|
+
└── NATIVE_API.md MUC 原生协议全景 / CLI 实现状态
|
|
284
285
|
```
|
|
285
286
|
|
|
286
287
|
> 协议研究文档与抓包脚本保留在内部源码研究仓库,未随本包导入;下文"协议抓包 /
|
package/REFERENCE.md
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
# muc-cli
|
|
2
|
+
|
|
3
|
+
Terminal client for 东航 MUC IM (the `com.meicloud.im.dh` desktop app, also
|
|
4
|
+
called MUC.app). Drives the same REST surface the desktop talks to, plus the
|
|
5
|
+
binary chat protocol on raw TCP 8101 for sending messages, withdrawing, and
|
|
6
|
+
syncing sessions.
|
|
7
|
+
|
|
8
|
+
REST commands always coexist with the running desktop and phone — the server
|
|
9
|
+
allows concurrent `accessToken`s. Chat-channel commands share online slots
|
|
10
|
+
with real devices; see [Coexistence](#coexistence) below before sending.
|
|
11
|
+
|
|
12
|
+
## What it is
|
|
13
|
+
|
|
14
|
+
A single command (`muc`) packaging:
|
|
15
|
+
|
|
16
|
+
- **Send** — 1:1 and group text, files, images, and voice clips. Add and
|
|
17
|
+
remove emoticon reactions on existing messages.
|
|
18
|
+
- **Read** — server-side chat history, profile lookups, group lists, sticker
|
|
19
|
+
packs, directory search (people, departments, apps, service numbers), and
|
|
20
|
+
long-lived `listen` tail of inbound messages with sender/keyword filters.
|
|
21
|
+
- **Manage** — login / token refresh, withdraw a message, clear a session,
|
|
22
|
+
download a chat-attached file, wait for a human emoticon reaction before
|
|
23
|
+
proceeding (`wait-react`).
|
|
24
|
+
- **Personal daemon** — foreground `daemon-start` process for the user's own
|
|
25
|
+
account. It owns one chat socket, records raw `daemon-events`, and lets
|
|
26
|
+
ordinary chat-channel commands share that socket instead of opening a second
|
|
27
|
+
TCP login.
|
|
28
|
+
- **Auto-reply** — long-running `assistantd` daemon plus named `assistant`s
|
|
29
|
+
with their own backend (Claude Code / Codex / Gemini / Cursor / Antigravity), persona prompt,
|
|
30
|
+
scope rules, optional slash-command claims, and a memory log. A
|
|
31
|
+
structural signature tail on every outbound stops bot-vs-bot reply loops.
|
|
32
|
+
This is separate from the personal daemon; ordinary commands do not borrow
|
|
33
|
+
a dedicated assistant account's socket.
|
|
34
|
+
- **Self-describe** — every command has a `--json` mode emitting a stable
|
|
35
|
+
envelope; `muc manifest --json` returns the full command graph + per-command
|
|
36
|
+
output JSON Schema for agent / script consumers.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm i -g @mu-cabin/muc-cli
|
|
42
|
+
muc --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requirements: **Node ≥ 18**. macOS, Linux, and Windows are all supported.
|
|
46
|
+
DES-CBC password encryption is `crypto-js` (pure JS) — no
|
|
47
|
+
`--openssl-legacy-provider` flag needed.
|
|
48
|
+
|
|
49
|
+
`~/.muc/credentials.json` and `~/.muc/config.json` are written with mode
|
|
50
|
+
`600` and the parent dir with mode `700`. POSIX honors that; on Windows the
|
|
51
|
+
modes are silently ignored — protect the files with NTFS ACLs if you're on a
|
|
52
|
+
shared host.
|
|
53
|
+
|
|
54
|
+
## First-time setup
|
|
55
|
+
|
|
56
|
+
Before you can `login`, run `muc init` to seed the tenant configuration. The
|
|
57
|
+
CLI is generic across MUC deployments — your MUC admin provides the
|
|
58
|
+
tenant-specific values.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
muc init # interactive: walks through APP_KEY, APP_ID, signing secrets, hosts
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
What `muc init` collects:
|
|
65
|
+
|
|
66
|
+
| Field | Source | Notes |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `appKey` | admin | 8 hex chars |
|
|
69
|
+
| `appId` | admin | 24 hex chars |
|
|
70
|
+
| `signSecretRest` | admin | secret for the regular-API signing scheme |
|
|
71
|
+
| `signSecretSecure` | admin | secret for the `/contacts/secure/...` signing scheme |
|
|
72
|
+
| `pwdDesKey` | admin (optional) | DES-CBC key for password encryption; defaults to `appKey` |
|
|
73
|
+
| `restHost` | default `https://muc.ceair.com/` | press enter to accept |
|
|
74
|
+
| `ssoHost` | default `https://store.ceair.com/` | reserved (advanced) |
|
|
75
|
+
| `imHost` / `imPort` | default `muc.ceair.com` / `8101` | TCP chat channel |
|
|
76
|
+
| `fileHost` / `filePort` | default `muc.ceair.com` / `6101` | TCP file channel |
|
|
77
|
+
|
|
78
|
+
Built-in defaults are provided **for hosts and ports only** — never for
|
|
79
|
+
`appKey`, `appId`, or signing secrets.
|
|
80
|
+
|
|
81
|
+
### Non-interactive (CI, scripts)
|
|
82
|
+
|
|
83
|
+
You can supply values via flags or env vars:
|
|
84
|
+
|
|
85
|
+
Get the four secrets from your MUC admin first; the snippets below use
|
|
86
|
+
placeholder names — substitute the real values your admin gives you.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Flag form
|
|
90
|
+
muc init --no-interactive \
|
|
91
|
+
--app-key "$APP_KEY" \
|
|
92
|
+
--app-id "$APP_ID" \
|
|
93
|
+
--sign-secret-rest "$SIGN_SECRET_REST" \
|
|
94
|
+
--sign-secret-secure "$SIGN_SECRET_SECURE" \
|
|
95
|
+
--json
|
|
96
|
+
|
|
97
|
+
# Env-var form
|
|
98
|
+
MUC_APP_KEY="$APP_KEY" \
|
|
99
|
+
MUC_APP_ID="$APP_ID" \
|
|
100
|
+
MUC_SIGN_SECRET_REST="$SIGN_SECRET_REST" \
|
|
101
|
+
MUC_SIGN_SECRET_SECURE="$SIGN_SECRET_SECURE" \
|
|
102
|
+
muc init --no-interactive
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Environment variables: `MUC_APP_KEY`, `MUC_APP_ID`, `MUC_SIGN_SECRET_REST`,
|
|
106
|
+
`MUC_SIGN_SECRET_SECURE`, `MUC_PWD_DES_KEY`, `MUC_REST_HOST`, `MUC_SSO_HOST`,
|
|
107
|
+
`MUC_IM_HOST`, `MUC_IM_PORT`, `MUC_FILE_HOST`, `MUC_FILE_PORT`.
|
|
108
|
+
|
|
109
|
+
> **Env-var leakage warning.** Values exported into the environment are
|
|
110
|
+
> visible to other processes on the same machine via `/proc/<pid>/environ`
|
|
111
|
+
> on Linux, and they land in shell history if you don't prefix the command
|
|
112
|
+
> with a space (and `HISTCONTROL=ignorespace` is set). For production setups
|
|
113
|
+
> prefer the **interactive** prompt or **flag-based** invocation — you can
|
|
114
|
+
> protect the flag values with secret-manager retrieval that never touches
|
|
115
|
+
> the env.
|
|
116
|
+
|
|
117
|
+
### Inspect, repair, reset
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
muc init --check # validate; exit 0 ok, 6 missing, 7 invalid
|
|
121
|
+
muc init --check --json # same, machine-readable
|
|
122
|
+
muc init --show # print config (secrets always redacted)
|
|
123
|
+
muc init --show --json # JSON envelope — secrets are also redacted here
|
|
124
|
+
# (read ~/.muc/config.json directly for raw values)
|
|
125
|
+
muc init --reset --yes # delete ~/.muc/config.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Resolution order per field: CLI flag → env var → existing saved value → built-in
|
|
129
|
+
default (hosts/ports only) → interactive prompt (TTY) → fail.
|
|
130
|
+
|
|
131
|
+
## Login
|
|
132
|
+
|
|
133
|
+
`muc login` is **non-interactive on the credential side** — it never prompts
|
|
134
|
+
for a password. Pass it via flag, stdin, env var, or `--token-pwd` (reuse
|
|
135
|
+
the saved tokenPwd).
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Plaintext flag (avoid; lands in shell history)
|
|
139
|
+
muc login --account WANGLULU8 --password '…'
|
|
140
|
+
|
|
141
|
+
# Safer: read password from stdin, no flag value
|
|
142
|
+
printf '%s' "$MY_PWD" | muc login --account WANGLULU8 --password-stdin
|
|
143
|
+
|
|
144
|
+
# Reuse saved tokenPwd (no password needed; ~30 day TTL)
|
|
145
|
+
muc login --token-pwd
|
|
146
|
+
|
|
147
|
+
# Env-var form
|
|
148
|
+
MUC_ACCOUNT=WANGLULU8 MUC_PASSWORD='…' muc login
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `account` is your MUC uid (uppercase ASCII, e.g. `WANGLULU8`); falls back
|
|
152
|
+
to `$MUC_ACCOUNT` or the previously-saved uid if `--account` is omitted.
|
|
153
|
+
- One of `--password` / `--password-stdin` / `--token-pwd` / `$MUC_PASSWORD`
|
|
154
|
+
is required — running `muc login` with no creds fails fast with
|
|
155
|
+
`AUTH_REQUIRED` (exit 3).
|
|
156
|
+
- The password is encrypted client-side (DES-CBC) before being sent — same
|
|
157
|
+
scheme the desktop client uses.
|
|
158
|
+
- A successful login saves to `~/.muc/credentials.json` (mode `600`):
|
|
159
|
+
`accessToken` (short-lived; the server doesn't publish a fixed TTL —
|
|
160
|
+
expect anywhere from hours to days) and `tokenPwd` (a longer-lived
|
|
161
|
+
refresh token used for silent re-login with `--token-pwd`).
|
|
162
|
+
- On any `61004` (access token expired/invalid), the CLI mirrors the
|
|
163
|
+
desktop's `mucReLogin` recovery: it first probes `/emp/get` (which
|
|
164
|
+
rotates `accessToken` server-side without burning the `tokenPwd`); only
|
|
165
|
+
if that probe also fails does it fall back to the `tokenPwd → /emp/login`
|
|
166
|
+
refresh.
|
|
167
|
+
- If a command fails with `code: AUTH_EXPIRED` and `tokenPwd rejected`
|
|
168
|
+
(server `61018`), the saved `tokenPwd` is gone — re-run
|
|
169
|
+
`muc login --account <UID>` with your real password to mint fresh
|
|
170
|
+
credentials.
|
|
171
|
+
- A stable per-CLI device id is generated at `~/.muc/device-id` on first
|
|
172
|
+
run. It is intentionally **different** from the desktop's machine-id so
|
|
173
|
+
REST sessions stay independent.
|
|
174
|
+
|
|
175
|
+
## Output contract
|
|
176
|
+
|
|
177
|
+
- **Default:** human-readable plain text on stdout, errors on stderr in the
|
|
178
|
+
form `error: <msg>` / `hint: <hint>` / `code: <code> (exit <n>)`.
|
|
179
|
+
- **`--json`** (anywhere on the command line): every command emits a single
|
|
180
|
+
stable JSON envelope on stdout — `{ok:true, schema_version:1, data}` or
|
|
181
|
+
`{ok:false, schema_version:1, code, msg, hint?}` — and stderr stays
|
|
182
|
+
silent. Agents and scripts should always pass `--json`.
|
|
183
|
+
- **Exit codes** are typed and identical in both modes: `0` success, `1`
|
|
184
|
+
internal bug, `2` usage, `3` auth required / expired, `4` not found, `5`
|
|
185
|
+
remote / network error, `6` config missing, `7` config invalid.
|
|
186
|
+
- `--help` / `--version` are human escape hatches and bypass the JSON
|
|
187
|
+
envelope by design. The canonical agent surface is `muc manifest --json`.
|
|
188
|
+
|
|
189
|
+
## Commands
|
|
190
|
+
|
|
191
|
+
### Setup
|
|
192
|
+
|
|
193
|
+
| Command | What it does |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `muc init` | Seed / inspect / reset tenant config (`~/.muc/config.json`). Run before `login`. See [First-time setup](#first-time-setup). |
|
|
196
|
+
| `muc login` | Authenticate; persists `accessToken` + `tokenPwd` to `~/.muc/credentials.json`. See [Login](#login). |
|
|
197
|
+
| `muc assistant-init` / `muc assistant-login` | Same as `init` / `login` but for the assistant daemon's **dedicated account** at `~/.muc/assistant/` — see [Dedicated account for the daemon](#dedicated-account-for-the-daemon). |
|
|
198
|
+
| `muc whoami [--assistant]` | Show the saved account + token freshness. `--assistant` inspects `~/.muc/assistant/` instead. |
|
|
199
|
+
|
|
200
|
+
### REST commands (no impact on the desktop app)
|
|
201
|
+
|
|
202
|
+
| Command | What it does |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `muc login` | Authenticate, persist `accessToken` + `tokenPwd`. |
|
|
205
|
+
| `muc whoami` | Show the saved profile + token freshness. Local only — does not read tenant config. |
|
|
206
|
+
| `muc search <keyword>` | Org / employee directory search (12 results max, server-side). |
|
|
207
|
+
| `muc search-dept <keyword>` | Department directory search (with employee count + path). |
|
|
208
|
+
| `muc search-app <keyword>` | Search the user's MUC widgets/apps drawer. |
|
|
209
|
+
| `muc search-sno <keyword>` | Search service-numbers (服务号); shows `★` for already-subscribed. |
|
|
210
|
+
| `muc whois <UID>` | Employee info + ext info (`secureApi`). Renders `cn / id / mail / departmentName / positionName / empStatusText`. `--no-ext` skips the slower `getExtInfo` call. |
|
|
211
|
+
| `muc set-signature [text]` | Update your personal signature (个人签名). ≤50 chars; pass `""` or `--clear` to clear. Read back via `muc whois <self-uid> --json` at `data.ext.sign` (the value lives on `getExtInfo`, not `empInfo`). Other personal-info fields (name / empId / dept / mobile / email) are HR-sourced and have no client-side write API. |
|
|
212
|
+
| `muc groups` | Starred groups (flat list keyed by `team_id`) + department groups (keyed by `groupId / deptName`). |
|
|
213
|
+
| `muc default-emojis` | Built-in default emoji names bundled by the desktop client. These can be used in text messages; `data.reactions` is the supported message-reaction subset. |
|
|
214
|
+
| `muc history --peer UID \| --team TEAM_ID \| --to-self` | Server-side chat history via the roaming-messages endpoint. Stateless: each call fetches from the server. Default window: 30 days, 50 messages newest-first. |
|
|
215
|
+
| `muc download --file-key <key> [--out <path>]` | Download a chat-attached file from the V5 file server (TCP 6101). **Read-only** — opens a fresh per-request session, no chat login, doesn't kick anyone. Pass `--mid <mid>` instead of `--file-key` plus `--peer/--team/--to-self` to look the file up via roaming history. Verifies md5 on completion. |
|
|
216
|
+
| `muc react --mid <MID> [--emoji <name>] [target]` | Add a default message reaction emoji to an existing message. **Visible.** Defaults to `[点赞]` and `--to-self`; non-self chats need `--yes-not-self`. By default assumes the original message was sent by the current user; pass `--src-sender-id <UID>` (and `--src-sender-app <appkey>`) to react to someone else's message. |
|
|
217
|
+
| `muc reactions --mid <MID>` | List all emoticon reactions on a single message. Read-only. |
|
|
218
|
+
| `muc unreact --emoticon-id <id> [target]` | Revoke one of your own reactions (id from `muc react` or `muc reactions`). |
|
|
219
|
+
| `muc manifest` | Print the full command graph + per-command output JSON Schema. Pair with `--json` for agents. |
|
|
220
|
+
|
|
221
|
+
`history` flags: scope with one of `--peer UID`, `--team TEAM_ID`,
|
|
222
|
+
`--sid <UID1|UID2 or teamId>`, or `--to-self`. `--limit N` (default 50),
|
|
223
|
+
`--days N` (window from now, default 30), `--before <ts|ISO>` to fetch
|
|
224
|
+
older than a moment, `--sub-type N` to filter to one type (e.g. `1` text,
|
|
225
|
+
`7` file, `8` image), `--desc` for newest-first (default: oldest at top).
|
|
226
|
+
The fetched `timestamp` is normalized to ms.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
muc history --peer YANWENYU --limit 100
|
|
230
|
+
muc history --to-self --days 7
|
|
231
|
+
muc history --team 9009876543210000 --before 2026-04-20 --json | jq .
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Chat-channel commands (TCP 8101) — see [Coexistence](#coexistence)
|
|
235
|
+
|
|
236
|
+
| Command | What it does |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `muc tcp-ping [--wait N] [--debug]` | TCP connect + NEGOTIATE only. Smoke test, no login frame, **zero** kick risk. |
|
|
239
|
+
| `muc sync [--os OS]` | After chat-login, run `syncDone` / `syncSession` / `syncWithdraws`. Read-only. |
|
|
240
|
+
| `muc search-team <keyword> [--enrich]` | Search groups you belong to via the chat channel; `--enrich` follows up with `team/get_team` per id to print names + member counts. |
|
|
241
|
+
| `muc team-members <TEAM_ID> [--profiles]` | List members of a joined group via `team/get_team_members`; `--profiles` enriches each UID with org directory fields. |
|
|
242
|
+
| `muc send (--to-self \| --peer UID \| --team TEAM_ID) <text> [--yes-not-self]` | Send a plain-text message. **Visible to the recipient.** |
|
|
243
|
+
| `muc send-file <file> [target] [--yes-not-self]` | Upload a file (TCP 6101) and broadcast as `MESSAGE_CHAT_FILE` (subType=7). Returns persistent `fileKey` + `mid`. **Visible.** Uses both 6101 (upload) and 8101 (broadcast) — costs the chosen `--os` slot for the broadcast. |
|
|
244
|
+
| `muc send-image <file> [target] [--width N --height N]` | Same as `send-file` but as `MESSAGE_CHAT_IMAGE` (subType=8). Width/height auto-probed from PNG/JPEG/GIF/BMP headers; pass `--width/--height` to override. |
|
|
245
|
+
| `muc send-voice <file> --duration <ms> [target]` | Same as `send-file` but as `MESSAGE_CHAT_AUDIO` (subType=9). `--duration <ms>` is required (the desktop renders the bubble's clip length from this metadata, not from the file). |
|
|
246
|
+
| `muc withdraw --mid <MID> [target]` | Recall a sent message. Original gets replaced server-side with "您撤回了一条消息". Target flag must match the original message's chat. |
|
|
247
|
+
| `muc clear-session [target]` | Delete a session from your client-side history (syncs to all your devices). **Doesn't delete server-side history** — `history` still returns the messages. |
|
|
248
|
+
| `muc pin (--to-self \| --peer UID \| --team TEAM_ID \| --sid sId)` | Pin a session to the top of the chat list (`user/setCacheField` → `sticky_on_top_id`). Multi-device. **Not** the same as starred groups (`muc groups`). |
|
|
249
|
+
| `muc unpin [target]` | Unpin a session (same targets as `pin`). |
|
|
250
|
+
| `muc pinned` | List currently pinned session sIds (and best-effort kind: team / p2p / self). |
|
|
251
|
+
| `muc wait-react --mid <MID> [--from-uid UID] [--emoji <name>] [--timeout 300]` | Block until someone reacts with a default reaction emoji to `--mid`, then exit. Useful as a "human-in-the-loop approval" gate after `muc send`. Exits with `TIMEOUT` (exit 5) if no matching reaction arrives within `--timeout` seconds. |
|
|
252
|
+
| `muc listen [--from-uid UID] [--team TEAM] [--peer UID] [--contains TEXT] [--regex …] [--at-me] [--max N]` | Long-lived TCP tap that emits one **NDJSON line per matching inbound message** (in `--json` mode each line is the standard envelope; in human mode it's a one-line summary). Supports repeatable filters (OR semantics within each kind, AND across kinds), `--at-me` to restrict to group messages where another user @-mentions you (implies excluding self), `--include-self` to include your own sends, `--include-reactions` to also surface type=3 sub=300/301 frames, `--max N` to exit after N matches. Stop with `^C`. |
|
|
253
|
+
|
|
254
|
+
`--os` resolves as **per-call `--os` → `defaultChatOs` in `~/.muc/config.json`
|
|
255
|
+
→ built-in `iOS`**. Valid values: `Windows`, `OS X`, `Mac`, `iOS`, `Android`.
|
|
256
|
+
Set the per-machine default once with `muc init --default-os <os>` (or the
|
|
257
|
+
`MUC_DEFAULT_OS` env var, or the interactive prompt during `muc init`). Useful
|
|
258
|
+
if your real device is an iPhone — flip the CLI default to `windows` so
|
|
259
|
+
chat-channel commands park on the PC slot instead of kicking your phone.
|
|
260
|
+
**`--os` / `defaultChatOs` does not provide coexistence — read the section
|
|
261
|
+
below before using any chat-channel command if you have a real device
|
|
262
|
+
logged in.**
|
|
263
|
+
|
|
264
|
+
Sending to anyone other than yourself requires the explicit `--yes-not-self`
|
|
265
|
+
flag — a guard against accidentally messaging real humans during testing.
|
|
266
|
+
|
|
267
|
+
**Personal daemon routing.** When `muc daemon-start` is running for the same
|
|
268
|
+
uid, every ordinary chat-channel command above (`send`, `send-file`,
|
|
269
|
+
`send-image`, `send-voice`, `withdraw`, `clear-session`, `pin`, `unpin`,
|
|
270
|
+
`pinned`, `sync`,
|
|
271
|
+
`search-team`, `team-members`, `wait-react`, `listen`) routes its TCP work
|
|
272
|
+
through the personal daemon's already-open socket. Dedicated `assistantd`
|
|
273
|
+
is not borrowed by ordinary commands; it is only probed to avoid stealing a
|
|
274
|
+
same-uid shared-profile assistant slot.
|
|
275
|
+
|
|
276
|
+
Behavioural consequences while a daemon is running:
|
|
277
|
+
|
|
278
|
+
- `--os` is **ignored** by these commands; the slot is fixed by whatever
|
|
279
|
+
`--os` the daemon was started with. Passing an `--os` value that disagrees
|
|
280
|
+
with the daemon's slot is rejected up front with `code:"USAGE"` (so the
|
|
281
|
+
user can't ask the CLI to do something that would kick the daemon).
|
|
282
|
+
- `muc listen --include-offline` is rejected with `code:"USAGE"` — the
|
|
283
|
+
daemon already drained the offline batch at its own startup, so there
|
|
284
|
+
are no offMsg frames left for `listen` to surface. Use `muc daemon-events`
|
|
285
|
+
for raw replay, or stop the daemon and rerun without it if you need a
|
|
286
|
+
direct offline flush.
|
|
287
|
+
- `--json` output now carries a `route` field: `"daemon"` when the call was
|
|
288
|
+
forwarded through the daemon's IPC socket, `"direct"` when the CLI opened
|
|
289
|
+
its own TCP login. Routed ordinary commands also include
|
|
290
|
+
`daemon_kind:"personal"` for compatibility-friendly disambiguation.
|
|
291
|
+
- If the daemon is up but **not connected** (kicked, auth-expired, or
|
|
292
|
+
network-error parked), commands fail fast with `code:"DAEMON_UNAVAILABLE"`
|
|
293
|
+
(exit 5) rather than silently taking the slot the daemon is trying to
|
|
294
|
+
reclaim. Inspect with `muc daemon-status --json`; resolve the pause or stop
|
|
295
|
+
the daemon (`muc daemon-stop`) before sending directly.
|
|
296
|
+
- If `assistantd` is explicitly running on the personal shared profile,
|
|
297
|
+
direct ordinary TCP is blocked with `code:"DAEMON_CONFLICT"` so it cannot
|
|
298
|
+
steal the assistant's slot. Stop `assistantd` or run the personal daemon.
|
|
299
|
+
|
|
300
|
+
### Personal daemon
|
|
301
|
+
|
|
302
|
+
Foreground-only v1 daemon for the user's own `~/.muc` account. It records raw
|
|
303
|
+
chat-channel events for 90 days by default (`MUC_DAEMON_HISTORY_DAYS`
|
|
304
|
+
overrides retention) and exposes replay/tail by stable cursor.
|
|
305
|
+
|
|
306
|
+
| Command | What it does |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `muc daemon-start [--os OS] [--on-kicked POLICY] [--debug]` | Start the personal daemon in the foreground. Holds one chat socket for ordinary commands. `POLICY` is `pause`, `immediate`, or a duration such as `5m`; default `3m`. |
|
|
309
|
+
| `muc daemon-status` | Show `kind:"personal"`, running status, uid, os, pid, socket, and retained history window. |
|
|
310
|
+
| `muc daemon-stop` | Send `SIGTERM` to the personal daemon pid, or clean stale socket state. |
|
|
311
|
+
| `muc daemon-events [--since CURSOR_OR_TIME] [--tail] [--if-missing error|earliest|latest]` | Replay raw daemon events and optionally continue live. `--since` accepts a cursor id, ISO timestamp, epoch milliseconds, or `now`. |
|
|
312
|
+
|
|
313
|
+
`daemon-events` emits one envelope per NDJSON line. Event ids are ordered as
|
|
314
|
+
`<epoch_ms>-<sequence>`; delivery is at-least-once, so consumers should
|
|
315
|
+
deduplicate by event `id` and, for messages, by MUC `mid`.
|
|
316
|
+
|
|
317
|
+
### Assistant daemon (auto-reply)
|
|
318
|
+
|
|
319
|
+
A long-lived daemon (`assistantd`) plus a registry of named **assistants**
|
|
320
|
+
that auto-reply to inbound DMs and group `@me` mentions. The assistant daemon
|
|
321
|
+
owns one TCP-8101 slot for assistant replies only. Ordinary commands route
|
|
322
|
+
through `muc daemon-start`, not through a dedicated assistant account.
|
|
323
|
+
|
|
324
|
+
#### Daemon lifecycle
|
|
325
|
+
|
|
326
|
+
| Command | What it does |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `muc assistantd-start [--os OS] [--on-kicked POLICY] [--debug]` | Start the assistant daemon in the foreground. Holds the chat slot for the chosen `--os` (priority: `--os` → the active profile's `defaultChatOs` → built-in `iOS`). `POLICY` is shared with the personal daemon and defaults to `3m`. With a [dedicated account](#dedicated-account-for-the-daemon) provisioned, that profile defaults `defaultChatOs` to `Windows` (PC slot). Without a dedicated profile it now fails unless `muc init --allow-assistant-shared-profile` explicitly opted into shared-profile mode. v1 has no launchd integration — wrap it in `tmux` / `screen` / `nohup` if you want it persistent. |
|
|
329
|
+
| `muc assistantd-status` | Probe the daemon: running? slot? attached assistants? last paused-state message? |
|
|
330
|
+
| `muc assistantd-stop` | Send `SIGTERM` to the daemon (graceful shutdown). |
|
|
331
|
+
| `muc assistantd-resume` | Manual resume for `--on-kicked pause`, auth expiry, or exhausted network retries is reserved for v1.1. Today: `assistantd-stop` then `assistantd-start` again. |
|
|
332
|
+
|
|
333
|
+
For both daemon types, `--on-kicked` accepts `pause`, `immediate`, a plain
|
|
334
|
+
number of seconds, or a duration ending in `s`, `m`, `h`, or `d` (maximum
|
|
335
|
+
`7d`). The default is `3m`. Immediate and delayed recovery open a new login
|
|
336
|
+
after the selected delay, so they may kick the other device if it is still
|
|
337
|
+
online. `daemon-status` / `assistantd-status` expose the resolved policy as
|
|
338
|
+
`on_kicked`.
|
|
339
|
+
|
|
340
|
+
If the daemon parks itself (`paused_kicked` with policy `pause`,
|
|
341
|
+
`paused_auth` when `accessToken` / `tokenPwd` are stale, or
|
|
342
|
+
`paused_network` after a dropped link), the process keeps running but stops
|
|
343
|
+
handling events. The corresponding status command shows the `status` and
|
|
344
|
+
`message`; chat-channel CLI calls fail fast with `code:"DAEMON_UNAVAILABLE"`
|
|
345
|
+
(exit 5) until the daemon recovers or you stop it.
|
|
346
|
+
|
|
347
|
+
#### Dedicated account for the daemon
|
|
348
|
+
|
|
349
|
+
By default `assistantd` requires a dedicated profile under `~/.muc/assistant/`.
|
|
350
|
+
Shared-profile assistant mode is an explicit advanced opt-in:
|
|
351
|
+
`muc init --allow-assistant-shared-profile`. In shared mode it authenticates
|
|
352
|
+
as the personal `~/.muc` account and is mutually exclusive with
|
|
353
|
+
`muc daemon-start` for that uid.
|
|
354
|
+
|
|
355
|
+
- **Chat slot.** A daemon and a real device on the **same** slot kick
|
|
356
|
+
each other (`auth/kicked`, "same account login"). The CLI's built-in
|
|
357
|
+
default slot is `iOS` (Mobile) — so an iPhone login knocks a shared daemon
|
|
358
|
+
offline.
|
|
359
|
+
- **REST `tokenPwd`.** A fresh `muc login` rotates the account's refresh
|
|
360
|
+
token; other devices on that account get logged out when their token
|
|
361
|
+
next refreshes.
|
|
362
|
+
|
|
363
|
+
The fix is to give the daemon its **own account**, stored under
|
|
364
|
+
`~/.muc/assistant/` (next to its pid / socket / logs) and completely
|
|
365
|
+
separate from your personal `~/.muc/`:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
muc assistant-init # tenant config → ~/.muc/assistant/config.json
|
|
369
|
+
# (tenant secrets/hosts default to whatever your
|
|
370
|
+
# personal `muc init` saved; defaultChatOs → Windows)
|
|
371
|
+
muc assistant-login --account <BOT_UID> # → ~/.muc/assistant/credentials.json
|
|
372
|
+
# (--platform defaults to mac)
|
|
373
|
+
muc assistantd-start # picks up the dedicated profile
|
|
374
|
+
muc whoami --assistant # confirm which account it's on
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Once provisioned, `muc assistantd-start` reads the dedicated profile
|
|
378
|
+
automatically (every REST call the daemon makes follows it too). The
|
|
379
|
+
daemon parks on the **PC slot** (`Windows`), so it coexists with your
|
|
380
|
+
personal account on a real iPhone (Mobile slot) on the same machine — at
|
|
381
|
+
the cost of conflicting with the MUC **desktop app** (also PC slot; don't
|
|
382
|
+
run it). Remove the profile (`rm -rf ~/.muc/assistant/config.json
|
|
383
|
+
~/.muc/assistant/credentials.json`) only if you intend to use the explicit
|
|
384
|
+
shared-profile mode.
|
|
385
|
+
|
|
386
|
+
#### Assistant CRUD
|
|
387
|
+
|
|
388
|
+
| Command | What it does |
|
|
389
|
+
|---|---|
|
|
390
|
+
| `muc assistant-new <name> [--template <id>] [--backend claude-code\|codex\|gemini\|cursor\|antigravity] [--model <id>] [--reasoning-effort <level>] [--commands /a,/b] [--description TEXT]` | Scaffold `~/.muc/assistants/<name>/` with `config.json`, a persona `system-prompt.md` (template substitutes `<SCOPE_DESCRIPTION>` from `--description`), and an empty `memory.jsonl`. **`--template <id>`** seeds the scaffold from a starter template (run `muc assistant-templates` to list); supplies the persona prompt, default `backend` / `model` / `reasoning_effort` / `commands`, and copies any `bin/` helpers into `~/.muc/assistants/<name>/bin/`. Explicit `--backend` / `--model` / `--reasoning-effort` / `--commands` flags still override the template's defaults. **`--reasoning-effort <level>`** is codex-only (`minimal` \| `low` \| `medium` \| `high` \| `xhigh`) and forwards to `codex -c model_reasoning_effort=<level>`; `xhigh` is model-dependent (codex falls back to `high` on models that don't support it). Other backends ignore the field. Omit the flag (and don't set `reasoning_effort` in the template) to let codex use its built-in default. After a template-based create the JSON output includes `setup_notes` (host-side prereqs the template can't auto-do, e.g. `glab auth login`) and `bin_files`. Backend defaults to `claude-code`; `codex` requires the `codex` CLI on PATH (logged in via `codex login` or `OPENAI_API_KEY`); `gemini` requires the `gemini` CLI on PATH (logged in via `gemini` interactive first-run, or `GEMINI_API_KEY` / `GOOGLE_API_KEY`); `cursor` requires the `cursor-agent` CLI on PATH (logged in via `cursor-agent login` OAuth, or `CURSOR_API_KEY`); `antigravity` requires the `agy` CLI on PATH (logged in via `agy` interactive Google-OAuth first-run) — its `--model` is advisory only (the active model is whatever you select in agy's own picker; defaults to `"Gemini 3.5 Flash (High)"`). `--commands` claims a comma-separated set of slash-commands (e.g. `--commands /img,/draw`); each must match `^/[a-z][a-z0-9-]{0,20}$`, must be globally unique across assistants, and `/help` is reserved for the daemon. Omit (or pass `""`, and don't use `--template`) to mark this assistant as a fallback that fires on messages with no `/cmd` match. The flag only takes bare names — to attach a per-command `desc` / `usage` (rendered in `/help`), edit each entry in `~/.muc/assistants/<name>/config.json` after creation: `{ "name": "/img", "desc": "AI 图像生成", "usage": "/img <prompt>" }`. |
|
|
391
|
+
| `muc assistant-templates` | List starter templates available to `assistant-new --template <id>`. Two roots are searched: bundled (ships with muc-cli) and `~/.muc/templates/<id>/` (user-managed); when an id exists in both, **user wins**. Each row reports `id`, `source` (`bundled` \| `user`), `description`, default `backend` / `model` / `reasoning_effort` / `commands`, declared `placeholders`, host-side `prereqs`, whether the template ships a `bin/` helper directory, and the absolute `path`. To author a template, drop a directory under `~/.muc/templates/<your-id>/` containing `template.json` (with `id` matching `^[a-z][a-z0-9-]{0,40}$`), `system-prompt.md`, optional `setup-notes.md`, and an optional `bin/`. To override a bundled template without forking, copy it under `~/.muc/templates/<same-id>/` and edit in place. |
|
|
392
|
+
| `muc assistant-list` | List registered assistants with their backend, priority, claimed `commands` (legacy name array) plus `command_specs` (rich `{name,desc?,usage?}` array), `is_fallback` flag, scope, and whether the daemon currently has them attached. |
|
|
393
|
+
| `muc assistant-start <name>` | Tell the running daemon to start routing matching events to this assistant. |
|
|
394
|
+
| `muc assistant-stop [name]` | Detach a single assistant; omit the name to detach **all**. Scaffolds stay on disk. |
|
|
395
|
+
| `muc assistant-edit <name>` | Open this assistant's persona `system-prompt.md` in `$EDITOR` / `$VISUAL` / `vi`. |
|
|
396
|
+
| `muc assistant-memory <name> [--tid <tid>] [--all] [--clear]` | Inspect the per-thread memory shards the backend sees. Default action: list every thread with its entry count. `--tid <tid>` (e.g. `team_g:<groupId>` or `p2p:<UID>`) shows one thread's journal; `--all` is the legacy cross-thread dump (debug only). `--clear` truncates — combine with `--tid` for one thread or `--all` for everything. Storage: `~/.muc/assistants/<name>/threads/<safe-tid>.jsonl`. |
|
|
397
|
+
| `muc assistant-delete <name>` | Detach (if the daemon's up) and remove the scaffold (config, prompt, memory). |
|
|
398
|
+
|
|
399
|
+
#### Starter templates
|
|
400
|
+
|
|
401
|
+
`muc assistant-templates` lists every template available to `assistant-new --template <id>`. A template is a directory with this layout:
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
<root>/<id>/
|
|
405
|
+
template.json # id, description, default backend / model / reasoning_effort (codex only) / commands, declared placeholders, host-side prereqs
|
|
406
|
+
system-prompt.md # persona prompt (with <PLACEHOLDER> substitutions)
|
|
407
|
+
setup-notes.md # optional — printed verbatim after `assistant-new` finishes
|
|
408
|
+
bin/ # optional — copied to ~/.muc/assistants/<name>/bin/ and chmod +x
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Two roots are searched user-first:
|
|
412
|
+
|
|
413
|
+
- `~/.muc/templates/<id>/` — user-managed; sharable by `tar` / `cp`; no muc release needed.
|
|
414
|
+
- bundled (ships with muc-cli) — listed with `source: "bundled"`.
|
|
415
|
+
|
|
416
|
+
Same id in both → user wins. To override a bundled template without forking, copy it to `~/.muc/templates/<same-id>/` and edit in place.
|
|
417
|
+
|
|
418
|
+
`<ASSISTANT_NAME>` / `<USER_NAME>` / `<MUC_ASSISTANT_DIR>` / `<MUC_ASSISTANT_BIN_DIR>` placeholders in `system-prompt.md` and `setup-notes.md` are substituted at create-time. At runtime, the daemon also exports `MUC_ASSISTANT_NAME` / `MUC_ASSISTANT_DIR` / `MUC_ASSISTANT_BIN_DIR` to the backend process, so a prompt can reference its own bundled scripts as `$MUC_ASSISTANT_BIN_DIR/<helper>` without absolute paths.
|
|
419
|
+
|
|
420
|
+
Bundled today: **`code-handler`** — GitLab code assistant. A single `/code [<gitlab-url>] [<desc>]` command covers review / implement / refine / explain / Q&A; the assistant decides the action from the URL kind and description, and the URL can be omitted when it's resolvable from the conversation context. Authorization is keyed off GitLab project membership rather than a config-file allowlist; commit author is taken from the trigger's MUC UID. Prereqs: `glab auth login` against your internal GitLab. Run `muc assistant-templates --json` for the full list and per-template `prereqs`.
|
|
421
|
+
|
|
422
|
+
#### Per-assistant config — `~/.muc/assistants/<name>/config.json`
|
|
423
|
+
|
|
424
|
+
Hand-edit, or seeded by `assistant-new`:
|
|
425
|
+
|
|
426
|
+
| Field | Type | Default | Notes |
|
|
427
|
+
|---|---|---|---|
|
|
428
|
+
| `name` | string | (arg) | `^[a-zA-Z0-9_-]{1,40}$` — also the directory name |
|
|
429
|
+
| `backend` | enum | `claude-code` | `claude-code` requires the `claude` CLI on PATH; `codex` requires the `codex` CLI on PATH (logged in via `codex login` or `OPENAI_API_KEY`) — defaults to `gpt-5.6-sol`; `gpt-5.6-terra`, `gpt-5.6-luna`, and older model ids remain selectable through the `model` field; `gemini` requires the `gemini` CLI on PATH (logged in via `gemini` interactive first-run, or `GEMINI_API_KEY` / `GOOGLE_API_KEY`) — defaults to `auto-gemini-3` (lets gemini auto-route between gemini-3-pro and gemini-3-flash, falling back to gemini-2.5-pro if no preview access), override via the `model` field; `cursor` requires the `cursor-agent` CLI on PATH (logged in via `cursor-agent login` OAuth, or `CURSOR_API_KEY`) — defaults to `auto` (Cursor's router), override via the `model` field; `antigravity` requires the `agy` CLI on PATH (logged in via `agy` interactive Google-OAuth first-run) — the `model` field is advisory only (used for the disclosed label; agy runs whatever model is selected in its own picker, defaulting to `"Gemini 3.5 Flash (High)"`) |
|
|
430
|
+
| `model` | string | — | optional backend-specific model override |
|
|
431
|
+
| `reasoning_effort` | enum | — | codex backend only — `minimal` \| `low` \| `medium` \| `high` \| `xhigh`. Forwarded as `codex -c model_reasoning_effort=<level>`. `xhigh` is model-dependent (codex falls back to `high` on models that don't support it). Other backends ignore the field. Omit to let codex use its built-in default. Templates seed this from `template.json`'s `reasoning_effort`. |
|
|
432
|
+
| `scope.scenes` | `("p2p"\|"team_g")[]` | — | restrict to DMs and / or group chats |
|
|
433
|
+
| `scope.tids` | `string[]` | — | only these `team_id`s match (groups only — DMs are filtered via `scope.from_uids`) |
|
|
434
|
+
| `scope.from_uids` | `string[]` | — | only messages from these senders match |
|
|
435
|
+
| `scope.keywords` | `string[]` | — | substring-match on body text |
|
|
436
|
+
| `commands` | `Array<string \| {name,desc?,usage?}>` | `[]` | slash-commands this assistant claims. Each entry is either a bare name (`"/img"`) or a rich spec (`{ "name":"/img", "desc":"AI 图像生成", "usage":"/img <prompt>" }`); both shapes coexist in one array. `name` matches `^/[a-z][a-z0-9-]{0,20}$`, is globally unique across assistants, and `/help` is reserved for the daemon. `desc`/`usage` ≤ 200 chars each — surfaced in the daemon's `/help` menu (defaults: `usage` falls back to `name`; `desc` falls back to `help_text`, then `<name>(<backend>[/<model>])`). Empty list = fallback assistant for its scope. |
|
|
437
|
+
| `priority` | int | `100` | lower wins when multiple assistants tie for the same command or fallback bucket; ties are logged to `~/.muc/assistant/conflicts.log` |
|
|
438
|
+
| `memory_window` | int (0–500) | `30` | rolling history entries kept per thread |
|
|
439
|
+
| `bootstrap_history` | bool | `true` | seed memory from server-side roaming history on first event in a thread |
|
|
440
|
+
| `reply_timeout_ms` | int (≥5s) | `120000` | backend invocation timeout |
|
|
441
|
+
| `help_text` | string | built-in fallback | sent verbatim when the backend returns `{"action":"skip","blocks":[]}` |
|
|
442
|
+
| `restrict.shell_grants` | `string[]` | — | optional opt-in tightening. **Absent** ⇒ permissive-by-default: every backend runs in its sandbox-but-permissive mode (writes inside the per-turn cwd, network on, native muc-verb deny still loads). **Set** ⇒ revert to a per-prefix allow-list backends enforce natively where possible (`claude --allowed-tools "Bash(<prefix>*)"`, cursor `cli.json` `permissions.allow`, gemini `--allowed-tools ShellTool(<prefix>)`). codex has no native flag — entries surface as a prompt-side advisory only. Templates seed this from `template.json`'s `restrict.shell_grants`; placeholders (`<MUC_ASSISTANT_BIN_DIR>` etc.) are pre-substituted at `assistant-new` time. |
|
|
443
|
+
|
|
444
|
+
> **Per-turn workspace + repo cache.** Every dispatch gets a private dir
|
|
445
|
+
> `~/.muc/assistants/<name>/workspace/turn-<msgId>/` that the daemon hands
|
|
446
|
+
> the backend as `cwd` and exports as `MUC_ASSISTANT_REPO_CACHE`'s parent
|
|
447
|
+
> (`MUC_ASSISTANT_REPO_CACHE=<turn-cwd>/.cache/repos`). Template-bundled
|
|
448
|
+
> helper scripts (e.g. `prep-repo`) honor that var so all writes — bare-
|
|
449
|
+
> caches, worktrees, scratch files — stay inside the turn dir and are
|
|
450
|
+
> wiped on cleanup. There is no separate `writable_roots` config to
|
|
451
|
+
> maintain anymore.
|
|
452
|
+
|
|
453
|
+
#### Routing & dispatch
|
|
454
|
+
|
|
455
|
+
1. Inbound message passes the trigger gate (DMs always; groups only on `@me`; never self; only `kind:"text"`).
|
|
456
|
+
2. Per-assistant scope filter (scenes / tids / from_uids / keywords) narrows the eligible set.
|
|
457
|
+
3. **`/help`** is daemon-owned. Renders a per-thread menu of every claimed `command` from in-scope active assistants — one line per command, formatted as `• <usage> — <desc>` (with `usage`/`desc` pulled from each command's spec, falling back to `name` and `help_text` / `<name>(<backend>)` respectively). Includes a `• 其他消息 → <name>` line if a fallback is in scope. Reachable even when no assistants are active. Sent as a richtext bubble with a real `@<asker>` mention so the response is clickable in the group.
|
|
458
|
+
4. **`/<cmd>`** dispatches to the assistant that registered it (lower `priority` wins on ties). A `/cmd` that no in-scope assistant owns triggers the daemon's `unknown command <cmd> — type /help…` reply. A bare `/` or malformed `/CMD` (uppercase, leading digit, etc.) gets the same treatment — never silently flows into a fallback.
|
|
459
|
+
5. **No `/` prefix** falls back to the in-scope assistant whose `commands` list is empty (lowest `priority` wins). If no fallback is in scope, the daemon sends `no_route_hint` so the user knows there's nothing listening.
|
|
460
|
+
6. Backend returns a JSON envelope `{"action":"reply"|"skip","blocks":[…]}`. `action:"skip"` ⇒ daemon sends `help_text` (or the built-in fallback) to the thread. `action:"reply"` ⇒ daemon ships the blocks straight to the thread and appends a `\n\n— <assistant> · <model>` signature tail to every reply. (No self-DM mirror — use `muc assistant-ui` / `muc assistant-log` to audit outbound traffic.)
|
|
461
|
+
7. The signature tail is also the **loop-stop**: any inbound whose last paragraph matches that exact shape is dropped before routing, so two assistants on opposite ends can't ping-pong.
|
|
462
|
+
8. Outbound failures (server reject or socket throw) emit IPC `outbound_failed` and fire a macOS notification.
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
# v1: foreground only; wrap in tmux/screen for persistence.
|
|
466
|
+
muc assistantd-start --os iOS # default Mobile slot
|
|
467
|
+
muc assistant-new desk-bot --description "out-of-hours triage in IT 值班群"
|
|
468
|
+
$EDITOR ~/.muc/assistants/desk-bot/config.json # add scope.tids etc.
|
|
469
|
+
muc assistant-edit desk-bot # tune the persona
|
|
470
|
+
muc assistant-start desk-bot # attach to running daemon
|
|
471
|
+
muc assistant-list --json | jq '.data.assistants[] | {name,active,commands,scope}'
|
|
472
|
+
|
|
473
|
+
# Specialist with a slash-command claim (paired with a fallback chatter):
|
|
474
|
+
muc assistant-new img-gen --commands /img,/draw --description "image generation"
|
|
475
|
+
muc assistant-new chatter --description "general triage in IT 值班群"
|
|
476
|
+
muc assistant-start img-gen
|
|
477
|
+
muc assistant-start chatter
|
|
478
|
+
# /img cat → img-gen ; /help → daemon menu ; anything else → chatter
|
|
479
|
+
|
|
480
|
+
# Inspect / unwind
|
|
481
|
+
muc assistantd-status --json
|
|
482
|
+
muc assistant-memory desk-bot --clear
|
|
483
|
+
muc assistant-stop desk-bot
|
|
484
|
+
muc assistantd-stop
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Message tail (signature)**: by default `muc send` appends a short tail to
|
|
488
|
+
the text it sends (e.g. `"\n—— 来自王璐璐的muc-cli"`) so recipients can
|
|
489
|
+
tell automated traffic from a typed message. The check is idempotent —
|
|
490
|
+
if your text already ends with the resolved tail, it is not appended a
|
|
491
|
+
second time. Customise it with the
|
|
492
|
+
`messageTail` field in `~/.muc/config.json` — set to a non-empty string to
|
|
493
|
+
override, or to `""` to disable entirely. You can also use
|
|
494
|
+
`muc init --message-tail "<custom>"` / `muc init --no-message-tail` to
|
|
495
|
+
write the value, and per-call flags `muc send --tail "<one-off>"` /
|
|
496
|
+
`muc send --no-tail` to override on a single send. Only `muc send`
|
|
497
|
+
(plain-text) is affected; `send-file` / `send-image` / `send-voice` are
|
|
498
|
+
not.
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
muc tcp-ping --wait 8 # safe protocol smoke test
|
|
502
|
+
muc send "hello from the CLI" # to-self (default)
|
|
503
|
+
muc send --peer ZHANG_W2 "ping" --yes-not-self
|
|
504
|
+
muc send --team 9009876543210000 "team message" --yes-not-self
|
|
505
|
+
|
|
506
|
+
muc send-file ./report.pdf --to-self # self test
|
|
507
|
+
muc send-image ./screenshot.png --peer ZHANG_W2 --yes-not-self # auto width/height
|
|
508
|
+
muc send-voice ./clip.amr --duration 3500 --to-self # 3.5 s clip
|
|
509
|
+
|
|
510
|
+
muc download --file-key /chat/m/WANGLULU8/.../<md5> --out ./got.bin # by explicit key
|
|
511
|
+
muc download --mid 69ef1ae07e5462c465c05e00 --to-self # look up body via roaming
|
|
512
|
+
muc download --mid <MID> --peer YANWENYU --out ./inbox/ # save into a directory
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`download` is the only file-channel command that **doesn't** open a chat
|
|
516
|
+
session — it speaks only port 6101 (and a one-time bucket lookup on first
|
|
517
|
+
call). Run it from any environment without disturbing your desktop or phone.
|
|
518
|
+
|
|
519
|
+
## Coexistence
|
|
520
|
+
|
|
521
|
+
**REST commands always coexist** with a running desktop and phone — the
|
|
522
|
+
server allows concurrent `accessToken`s. `whoami`, `search`, `search-dept`,
|
|
523
|
+
`search-app`, `search-sno`, `history`, `whois`, `set-signature`, `groups`, `default-emojis`, and
|
|
524
|
+
`download` are REST-only (or 6101-only, in `download`'s case) and safe to
|
|
525
|
+
run anytime.
|
|
526
|
+
|
|
527
|
+
**Chat-channel commands** (`sync`, `search-team`, `team-members`, `send`, `send-file`,
|
|
528
|
+
`send-image`, `send-voice`, `withdraw`, `clear-session`, `pin`, `unpin`, `pinned`) open a TCP-8101
|
|
529
|
+
session and share a per-user **two-slot** model with real devices:
|
|
530
|
+
|
|
531
|
+
| Slot | `--os` values |
|
|
532
|
+
|---|---|
|
|
533
|
+
| PC | `Windows`, `"OS X"`, `Mac` |
|
|
534
|
+
| Mobile | `iOS` (default), `Android` |
|
|
535
|
+
|
|
536
|
+
A second login in the same slot kicks the previous client off the realtime
|
|
537
|
+
channel — the IM panel goes offline, but tokens stay valid (reopen to
|
|
538
|
+
recover). The Mac desktop signs in as `"OS X"` (PC slot); the iPhone signs
|
|
539
|
+
in as `iOS` (Mobile slot).
|
|
540
|
+
|
|
541
|
+
The CLI defaults to `--os iOS` so it coexists with a Mac desktop. If your
|
|
542
|
+
iPhone is also signed in, both slots are taken and any TCP command will
|
|
543
|
+
kick one of them — fall back to REST-only commands, or run
|
|
544
|
+
`muc init --default-os windows` to flip the per-machine default to the PC
|
|
545
|
+
slot (which kicks the desktop instead of the phone).
|
|
546
|
+
|
|
547
|
+
`tcp-ping` only does TCP connect + NEGOTIATE (no `auth/login`), so it can't
|
|
548
|
+
kick anything. Run it first to confirm the wire protocol is happy on your
|
|
549
|
+
network.
|
|
550
|
+
|
|
551
|
+
If you do get kicked, reopen the desktop's IM panel or relaunch the mobile
|
|
552
|
+
app to recover — tokens stay valid.
|
|
553
|
+
|
|
554
|
+
## Examples
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
$ printf '%s' "$MY_PWD" | muc login --account WANGLULU8 --password-stdin
|
|
558
|
+
uid: WANGLULU8
|
|
559
|
+
empId: 5394483846707200
|
|
560
|
+
name: 王璐璐
|
|
561
|
+
accessToken: TV5l…u9kQ (redacted; use --json for raw)
|
|
562
|
+
accessTokenExpireAt: 1745678340000
|
|
563
|
+
tokenPwdRefreshed: true
|
|
564
|
+
tokenPwdExpireAt: 1748097540000
|
|
565
|
+
|
|
566
|
+
$ muc whoami
|
|
567
|
+
uid: WANGLULU8
|
|
568
|
+
empId: 5394483846707200
|
|
569
|
+
name: 王璐璐
|
|
570
|
+
deviceId: 8e413b4141…
|
|
571
|
+
deviceName: WANGLULU8-mbp.local (muc-cli)
|
|
572
|
+
accessToken: TV5l…u9kQ (redacted; use --json for raw)
|
|
573
|
+
accessTokenExpireAt: 1745678340000
|
|
574
|
+
tokenPwdSet: true
|
|
575
|
+
tokenPwdExpireAt: 1748097540000
|
|
576
|
+
savedAt: 1745073540000
|
|
577
|
+
credentialsPath: ~/.muc/credentials.json
|
|
578
|
+
|
|
579
|
+
$ muc search 张
|
|
580
|
+
ZHANG_W2 张伟 飞行部 / 机长室
|
|
581
|
+
ZHANGSAN03 张三 地面服务部
|
|
582
|
+
…
|
|
583
|
+
|
|
584
|
+
$ muc whois ZHANG_W2
|
|
585
|
+
uid ZHANG_W2
|
|
586
|
+
name 张伟
|
|
587
|
+
empId 4892173049281024
|
|
588
|
+
mobile 1380013…
|
|
589
|
+
email zhang.wei@ceair.com
|
|
590
|
+
dept 飞行部 / 机长室
|
|
591
|
+
title 机长
|
|
592
|
+
|
|
593
|
+
$ muc groups
|
|
594
|
+
starred:
|
|
595
|
+
[常用] (3)
|
|
596
|
+
9001234567890123 IT 值班群
|
|
597
|
+
9001234567890124 运行联络
|
|
598
|
+
9001234567890125 部门通知
|
|
599
|
+
dept groups: 1
|
|
600
|
+
9009876543210000 飞行部 / 机长室
|
|
601
|
+
|
|
602
|
+
$ muc default-emojis
|
|
603
|
+
emojis:
|
|
604
|
+
- [+1]
|
|
605
|
+
- [DONE]
|
|
606
|
+
- [GET]
|
|
607
|
+
...
|
|
608
|
+
reactions:
|
|
609
|
+
- [+1]
|
|
610
|
+
- [收到]
|
|
611
|
+
- [加油]
|
|
612
|
+
- [OK]
|
|
613
|
+
- [点赞]
|
|
614
|
+
- [鼓掌]
|
|
615
|
+
- [DONE]
|
|
616
|
+
defaultReaction: [点赞]
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Files & paths
|
|
620
|
+
|
|
621
|
+
| Path | Purpose | Mode |
|
|
622
|
+
|---|---|---|
|
|
623
|
+
| `~/.muc/config.json` | tenant config: APP_KEY, APP_ID, signing secrets, hosts/ports | 600 |
|
|
624
|
+
| `~/.muc/credentials.json` | accessToken, tokenPwd, profile | 600 |
|
|
625
|
+
| `~/.muc/device-id` | stable per-CLI deviceId (sha256 hex) | 600 |
|
|
626
|
+
| `~/.muc/remember.salt` | salt for the optional `--remember` password blob | 600 |
|
|
627
|
+
| `~/.muc/runtime/daemon.sock`, `daemon.pid`, `daemon-state.json`, `daemon-events-*.ndjson` | personal daemon IPC, pid/state, and raw event history | — |
|
|
628
|
+
| `~/.muc/assistant/config.json`, `credentials.json`, `device-id`, … | the daemon's [dedicated account](#dedicated-account-for-the-daemon), when provisioned — same file set as above, scoped to one profile dir | 600 |
|
|
629
|
+
| `~/.muc/assistant/` (other files) | daemon runtime: `daemon.pid`, `assistant.sock`, `state.json`, `inbox.ndjson`, `events-*.ndjson`, `conflicts.log` | — |
|
|
630
|
+
| `~/.muc/assistants/<name>/` | per-assistant persona: `config.json`, `system-prompt.md`, `threads/`, `workspace/` | — |
|
|
631
|
+
|
|
632
|
+
Everything shares the `~/.muc/` parent (mode 700). The CLI creates these on
|
|
633
|
+
first use; you should never need to edit the `config.json` / `credentials.json`
|
|
634
|
+
files by hand.
|
|
635
|
+
|
|
636
|
+
## Changelog
|
|
637
|
+
|
|
638
|
+
#### Unreleased
|
|
639
|
+
- Add the personal daemon command family: `muc daemon-start`, `daemon-status`, `daemon-stop`, and `daemon-events`. Ordinary chat-channel commands now route only through the personal daemon and report `daemon_kind:"personal"` when routed.
|
|
640
|
+
- Dedicated `assistantd` is no longer borrowed by ordinary commands. Without a dedicated assistant profile, `assistantd-start` now requires explicit `muc init --allow-assistant-shared-profile`; same-uid shared-profile assistantd conflicts with the personal daemon / direct ordinary TCP via `DAEMON_CONFLICT`.
|
|
641
|
+
- Add raw event cursors (`<epoch_ms>-<sequence>`), 90-day personal daemon event retention, and `CURSOR_NOT_FOUND` handling for missing retained cursors.
|
|
642
|
+
|
|
643
|
+
#### `0.5.0`
|
|
644
|
+
`2026.05.11`
|
|
645
|
+
- Add `muc assistant-init` / `muc assistant-login` and `muc whoami --assistant`: the assistant daemon can now authenticate as a **dedicated account** stored under `~/.muc/assistant/` (config.json, credentials.json, device-id, refresh logs), fully separate from the personal `~/.muc/` account every other command uses. `muc assistantd-start` picks it up automatically when present; the dedicated profile defaults `defaultChatOs` to `Windows` (PC slot) so the daemon coexists with a personal account on a real iPhone (Mobile slot). `assistant-init` seeds tenant secrets/hosts from your personal `muc init`; `assistant-login` defaults `--platform` to `mac`. Older builds kept shared-profile behavior when no profile existed; current builds require explicit opt-in.
|
|
646
|
+
- Internals: `config.json` / `credentials.json` / `device-id` / `remember.salt` / `runtime/refresh-log.ndjson` paths now resolve through a process-wide *profile root* (default `~/.muc`) instead of being hardcoded. `configPath` / `credentialsPath` / `configDir` are now functions.
|
|
647
|
+
|
|
648
|
+
#### `0.4.1`
|
|
649
|
+
`2026.05.03`
|
|
650
|
+
- Redesign assistant routing around globally-unique slash-commands (`assistant-new --commands /img,/draw`) plus a `commands:[]` fallback bucket; `/help` is now a daemon-owned per-thread menu reachable even when no assistants are active. Drop the draft / 👍 / DONE approval flow, `auto_send`, `cooldown_ms`, `draft_ttl_ms`, and `scope.default` — replies ship straight to the thread.
|
|
651
|
+
- Loop-stop is now structural: every reply carries a `\n\n— <assistant> · <model>` signature tail, and any inbound whose last paragraph matches that shape is dropped before routing. The `__SKIP__` literal is gone — backends return `{"action":"skip","blocks":[]}` instead.
|
|
652
|
+
- Outbound send failures emit IPC `outbound_failed` and fire a macOS notification. The monitor (`muc assistant-ui`) and `muc assistant-log` surface the daemon-owned routing replies (`help_menu_sent` / `unknown_command_sent` / `no_route_hint_sent`) under the synthetic `daemon` owner.
|
|
653
|
+
- Existing `~/.muc/assistants/<name>/config.json` files are migrated in place on read: removed fields stripped, `commands: []` injected.
|
|
654
|
+
|
|
655
|
+
#### `0.4.0`
|
|
656
|
+
`2026.04.30`
|
|
657
|
+
- Add `muc assistantd-start` / `assistantd-status` / `assistantd-stop` (foreground daemon, holds one TCP slot, owns the chat socket for the rest of the CLI via IPC)
|
|
658
|
+
- Add `muc assistant-new` / `assistant-list` / `assistant-start` / `assistant-stop` / `assistant-edit` / `assistant-memory` / `assistant-delete` to manage named auto-reply assistants under `~/.muc/assistants/<name>/`
|
|
659
|
+
- Pluggable backends (`claude-code`, `codex`, `gemini`, `cursor`); per-assistant scope rules, priority, persona overlay on a bundled safety-rails prompt, rolling memory window
|
|
660
|
+
- Other chat-channel commands (`send`, `send-file`, `send-image`, `send-voice`, `withdraw`, `sync`, `wait-react`, `listen`) now route through the daemon when it's running; `--json` output gains `route: "daemon" \| "direct"`. Conflicting `--os` and `listen --include-offline` are rejected up front; a parked daemon yields `code:"DAEMON_UNAVAILABLE"`
|
|
661
|
+
|
|
662
|
+
#### `0.3.1`
|
|
663
|
+
`2026.04.29`
|
|
664
|
+
- Add `defaultChatOs` to `~/.muc/config.json`: per-machine fallback for the chat-channel `--os` slot. Configure via `muc init --default-os <windows|osx|mac|ios|android>`, the `MUC_DEFAULT_OS` env var, or the interactive prompt during `muc init`. Resolution order: per-call `--os` → `defaultChatOs` → built-in `iOS`. Use this when your real device is on the Mobile slot (e.g. iPhone) so chat-channel commands park on PC instead of kicking your phone.
|
|
665
|
+
|
|
666
|
+
#### `0.3.0`
|
|
667
|
+
`2026.04.29`
|
|
668
|
+
- Add `muc react` / `muc unreact` / `muc reactions` to add, revoke, and list emoticon reactions on existing messages
|
|
669
|
+
- Add `muc wait-react` to block until a human reacts to a given mid; useful as a "human-in-the-loop approval" gate after `muc send`
|
|
670
|
+
- Add `muc listen`, a long-lived TCP tap with sender / chat / keyword / `--at-me` filters, one NDJSON line per match
|
|
671
|
+
- Exit code `5` now also covers `TIMEOUT` and `KICKED`
|
|
672
|
+
|
|
673
|
+
#### `0.2.2`
|
|
674
|
+
`2026.04.28`
|
|
675
|
+
- `muc send` plain-text now appends an idempotent signature tail by default; override via `--tail` / `--no-tail`, `muc init --message-tail`, or `messageTail` in `~/.muc/config.json`
|
|
676
|
+
|
|
677
|
+
#### `0.2.1`
|
|
678
|
+
`2026.04.28`
|
|
679
|
+
- Skills unified across macOS / Linux / Windows
|
|
680
|
+
- On Windows, file mode `600` is silently ignored — use NTFS ACLs on shared hosts to protect `~/.muc/`
|
|
681
|
+
|
|
682
|
+
#### `0.2.0`
|
|
683
|
+
`2026.04.27`
|
|
684
|
+
- Add `muc init`: extract `appKey` / `appId` / signing secrets out of source code, write to `~/.muc/config.json` (mode 600)
|
|
685
|
+
- Companion subcommands: `muc init --check` / `--show` / `--reset`
|
|
686
|
+
- License changed to MIT; package published to npm with public access
|
|
687
|
+
|
|
688
|
+
## What it can't do (yet)
|
|
689
|
+
|
|
690
|
+
- **Richtext messages** (`subType=11`, mixed text + image + @-mention in one
|
|
691
|
+
bubble). The image segments share the file-channel upload path so the
|
|
692
|
+
bulk of the work is already there; just needs a body-builder + a
|
|
693
|
+
`send-rich` command.
|
|
694
|
+
- **Forwarded / merged messages** (`subType=26`).
|
|
695
|
+
- **SSO / scan-to-login.** REST username+password works; the SSO flow is
|
|
696
|
+
not wired up.
|
|
697
|
+
- **Image thumbnails.** The upload step asks the server to make `tn1`/`tn5`
|
|
698
|
+
thumbs, but `muc download` only fetches the original `fileKey`.
|
|
699
|
+
- **End-to-end-encrypted file content.** Not active on the default tenant
|
|
700
|
+
and the CLI doesn't implement it.
|
|
701
|
+
|
|
702
|
+
## Troubleshooting
|
|
703
|
+
|
|
704
|
+
- `code: CONFIG_MISSING` (exit 6) — `~/.muc/config.json` is absent. Run
|
|
705
|
+
`muc init` to create it. The CLI cannot guess your tenant's signing
|
|
706
|
+
secrets.
|
|
707
|
+
- `code: CONFIG_INVALID` (exit 7) — the file exists but failed schema
|
|
708
|
+
validation (corruption, manual edit gone wrong, or a stale schema after
|
|
709
|
+
upgrade). Run `muc init` to repair it; `--reset --yes` to start clean.
|
|
710
|
+
- `login failed: code=…` — sign or DES is wrong. Double-check the signing
|
|
711
|
+
secrets and `appKey` / `appId` you supplied to `muc init` against the
|
|
712
|
+
values your MUC admin provided. `muc init --show` (secrets redacted) lets
|
|
713
|
+
you confirm what's currently saved; `muc init` (interactive) lets you
|
|
714
|
+
re-enter the values.
|
|
715
|
+
- `error: not logged in` (exit 3, code `AUTH_REQUIRED`) — the saved
|
|
716
|
+
credentials file is missing or unreadable. Check `~/.muc/credentials.json`
|
|
717
|
+
and re-run `muc login`.
|
|
718
|
+
- `code: AUTH_EXPIRED` (exit 3) — accessToken or tokenPwd was rejected by
|
|
719
|
+
the server. On the personal profile (`~/.muc`), re-run `muc login --token-pwd`
|
|
720
|
+
(or full `muc login` if `tokenPwd` is also stale). On the dedicated
|
|
721
|
+
assistant profile (`~/.muc/assistant/config.json` present — the daemon
|
|
722
|
+
account), substitute `muc assistant-login --token-pwd` /
|
|
723
|
+
`muc assistant-login` — `muc login` only touches the personal credentials.
|
|
724
|
+
- `EPROTO` / TLS errors — corporate proxy or VPN intercepting traffic.
|
|
725
|
+
`muc-cli` does not pin certificates, so adding the proxy CA to your Node
|
|
726
|
+
trust store should suffice (`NODE_EXTRA_CA_CERTS=/path/to/ca.pem`).
|
|
727
|
+
- A TCP send kicks your desktop off the IM panel — the CLI's `--os` value
|
|
728
|
+
collided with the desktop's slot. See [Coexistence](#coexistence) above:
|
|
729
|
+
default `--os iOS` only risks the Mobile slot (your iPhone), not the
|
|
730
|
+
desktop. If both desktop and phone are online, drop to REST-only
|
|
731
|
+
commands.
|
|
732
|
+
|
|
733
|
+
### 5.9 Gemini CLI headless timeout
|
|
734
|
+
|
|
735
|
+
If a `backend: "gemini"` assistant times out with empty stdout/stderr,
|
|
736
|
+
the current adapter is already passing `--skip-trust`, closing stdin,
|
|
737
|
+
and killing the full process tree on timeout. Round-5 live probes against
|
|
738
|
+
Gemini CLI `0.40.0` still found an environment-specific failure mode
|
|
739
|
+
where Node `child_process.spawn()` never receives JSON output even after
|
|
740
|
+
testing:
|
|
741
|
+
|
|
742
|
+
- `stdin.end(payload)` vs `write()+destroy()`
|
|
743
|
+
- `detached: true` vs `false`
|
|
744
|
+
- explicit `TERM` / `CI` / `GEMINI_*` / `GOOGLE_*` / `NO_COLOR` env sync
|
|
745
|
+
- `mkdtemp()` dir mode `0700` vs `0755`
|
|
746
|
+
- `stdio: ['pipe','inherit','inherit']`
|
|
747
|
+
- `GEMINI_CLI_TRUST_WORKSPACE=true`
|
|
748
|
+
|
|
749
|
+
If the same `gemini ... --output-format json` command works in your
|
|
750
|
+
interactive terminal but not when spawned from Node, treat it as an
|
|
751
|
+
external Gemini CLI runtime issue rather than a muc prompt/JSON bug. The
|
|
752
|
+
daemon now fails fast with a timeout instead of hanging indefinitely; use
|
|
753
|
+
`claude-code` or `codex` for unattended runs on hosts where Gemini only
|
|
754
|
+
works interactively.
|
|
755
|
+
|
|
756
|
+
### Exit-code summary
|
|
757
|
+
|
|
758
|
+
| Exit | Code |
|
|
759
|
+
|---|---|
|
|
760
|
+
| 0 | success |
|
|
761
|
+
| 1 | INTERNAL |
|
|
762
|
+
| 2 | USAGE |
|
|
763
|
+
| 3 | AUTH_REQUIRED, AUTH_EXPIRED |
|
|
764
|
+
| 4 | NOT_FOUND |
|
|
765
|
+
| 5 | REMOTE_ERROR, NETWORK, TIMEOUT, KICKED, DAEMON_UNAVAILABLE |
|
|
766
|
+
| 6 | CONFIG_MISSING |
|
|
767
|
+
| 7 | CONFIG_INVALID |
|
|
768
|
+
|
|
769
|
+
## Security
|
|
770
|
+
|
|
771
|
+
- Both files in `~/.muc/` are written with mode `600`. **Don't** check them
|
|
772
|
+
into git or copy them to shared machines.
|
|
773
|
+
- **Don't share `~/.muc/config.json`** — it contains your tenant's signing
|
|
774
|
+
secrets (`signSecretRest`, `signSecretSecure`, `pwdDesKey`, `appKey`).
|
|
775
|
+
Anyone with these can impersonate API calls against your MUC tenant.
|
|
776
|
+
- `MUC_PASSWORD` ends up in shell history; prefer `--password-stdin` or
|
|
777
|
+
`--token-pwd` for refreshes.
|
|
778
|
+
- **Prefer interactive `muc init` over env vars.** Environment variables
|
|
779
|
+
leak through shell history and `/proc/<pid>/environ` to other processes
|
|
780
|
+
on the same machine. Use the interactive prompt or one-shot flags pulled
|
|
781
|
+
from a secret manager rather than persistent `MUC_*` exports.
|
|
782
|
+
- TCP login over 8101 registers a device session in one of two slots per
|
|
783
|
+
user (PC or Mobile). A second login in the same slot kicks the first.
|
|
784
|
+
Default `--os iOS` keeps the CLI on the Mobile slot; only your iPhone is
|
|
785
|
+
ever at risk.
|
|
786
|
+
- `tcp-ping` is the only chat-channel command that cannot kick anything —
|
|
787
|
+
use it as a network smoke-test before trying TCP sends.
|
package/dist/muc.mjs
CHANGED
|
@@ -27506,6 +27506,254 @@ function register23(program3) {
|
|
|
27506
27506
|
});
|
|
27507
27507
|
}
|
|
27508
27508
|
|
|
27509
|
+
// src/lib/sticky.ts
|
|
27510
|
+
var STICKY_CACHE_KEY = "sticky_on_top_id";
|
|
27511
|
+
function stickyCacheFrom() {
|
|
27512
|
+
return process.platform === "win32" ? "WIN" : "MAC";
|
|
27513
|
+
}
|
|
27514
|
+
var STICKY_LIST_PROBE_SID = "__muc_cli_sticky_list_probe__";
|
|
27515
|
+
function buildStickyCacheFieldData(args) {
|
|
27516
|
+
return {
|
|
27517
|
+
from: args.from ?? stickyCacheFrom(),
|
|
27518
|
+
key: [STICKY_CACHE_KEY],
|
|
27519
|
+
subkey: null,
|
|
27520
|
+
value: [args.sId],
|
|
27521
|
+
op: args.op
|
|
27522
|
+
};
|
|
27523
|
+
}
|
|
27524
|
+
function extractStickyIds(frame) {
|
|
27525
|
+
if (!frame || typeof frame !== "object") return [];
|
|
27526
|
+
const f = frame;
|
|
27527
|
+
if (f.err) return [];
|
|
27528
|
+
const candidates = [f.respond, f.data, f];
|
|
27529
|
+
for (const c of candidates) {
|
|
27530
|
+
if (!c || typeof c !== "object" || Array.isArray(c)) continue;
|
|
27531
|
+
const ids = c.sticky_on_top_id;
|
|
27532
|
+
if (Array.isArray(ids)) {
|
|
27533
|
+
return ids.map((x) => String(x)).filter((s) => s && s !== STICKY_LIST_PROBE_SID);
|
|
27534
|
+
}
|
|
27535
|
+
}
|
|
27536
|
+
return [];
|
|
27537
|
+
}
|
|
27538
|
+
function classifySid(sId, selfUid) {
|
|
27539
|
+
if (!sId) return { kind: "unknown" };
|
|
27540
|
+
if (sId.includes("|")) {
|
|
27541
|
+
const parts = sId.split("|");
|
|
27542
|
+
if (parts.length === 2) {
|
|
27543
|
+
const self2 = selfUid.toUpperCase();
|
|
27544
|
+
const a = parts[0].toUpperCase();
|
|
27545
|
+
const b = parts[1].toUpperCase();
|
|
27546
|
+
if (a === self2 && b === self2) return { kind: "self" };
|
|
27547
|
+
const peer = a === self2 ? parts[1] : b === self2 ? parts[0] : parts[1];
|
|
27548
|
+
return { kind: "p2p", peer };
|
|
27549
|
+
}
|
|
27550
|
+
}
|
|
27551
|
+
if (/^\d{10,}$/.test(sId)) return { kind: "team", team_id: sId };
|
|
27552
|
+
return { kind: "unknown" };
|
|
27553
|
+
}
|
|
27554
|
+
function stickySessionRows(ids, selfUid) {
|
|
27555
|
+
return ids.map((sId) => ({ sId, ...classifySid(sId, selfUid) }));
|
|
27556
|
+
}
|
|
27557
|
+
|
|
27558
|
+
// src/commands/pin.ts
|
|
27559
|
+
var SessionRow = external_exports.object({
|
|
27560
|
+
sId: external_exports.string(),
|
|
27561
|
+
kind: external_exports.enum(["team", "p2p", "self", "unknown"]),
|
|
27562
|
+
peer: external_exports.string().optional(),
|
|
27563
|
+
team_id: external_exports.string().optional()
|
|
27564
|
+
});
|
|
27565
|
+
var MutateOutput = external_exports.object({
|
|
27566
|
+
action: external_exports.enum(["pin", "unpin"]),
|
|
27567
|
+
scene: external_exports.enum(["p2p", "team_g"]),
|
|
27568
|
+
toId: external_exports.string(),
|
|
27569
|
+
sId: external_exports.string(),
|
|
27570
|
+
dryRun: external_exports.boolean(),
|
|
27571
|
+
sticky_ids: external_exports.array(external_exports.string()),
|
|
27572
|
+
sessions: external_exports.array(SessionRow),
|
|
27573
|
+
frame: external_exports.unknown().optional(),
|
|
27574
|
+
raw: external_exports.unknown().optional(),
|
|
27575
|
+
route: external_exports.enum(["daemon", "direct"]).optional(),
|
|
27576
|
+
daemon_kind: external_exports.literal("personal").optional()
|
|
27577
|
+
});
|
|
27578
|
+
var ListOutput = external_exports.object({
|
|
27579
|
+
sticky_ids: external_exports.array(external_exports.string()),
|
|
27580
|
+
sessions: external_exports.array(SessionRow),
|
|
27581
|
+
count: external_exports.number(),
|
|
27582
|
+
dryRun: external_exports.boolean().optional(),
|
|
27583
|
+
frame: external_exports.unknown().optional(),
|
|
27584
|
+
raw: external_exports.unknown().optional(),
|
|
27585
|
+
route: external_exports.enum(["daemon", "direct"]).optional(),
|
|
27586
|
+
daemon_kind: external_exports.literal("personal").optional()
|
|
27587
|
+
});
|
|
27588
|
+
function resolveTarget(opts, selfUid, label) {
|
|
27589
|
+
requireSingleTarget(
|
|
27590
|
+
{ peer: opts.peer, team: opts.team, sid: opts.sid, toSelf: opts.toSelf },
|
|
27591
|
+
label
|
|
27592
|
+
);
|
|
27593
|
+
if (opts.sid) {
|
|
27594
|
+
const sId2 = opts.sid;
|
|
27595
|
+
if (sId2.includes("|")) {
|
|
27596
|
+
return { scene: "p2p", toId: selfUid, sId: sId2 };
|
|
27597
|
+
}
|
|
27598
|
+
return { scene: "team_g", toId: sId2, sId: sId2 };
|
|
27599
|
+
}
|
|
27600
|
+
let scene;
|
|
27601
|
+
let toId;
|
|
27602
|
+
if (opts.team) {
|
|
27603
|
+
scene = "team_g";
|
|
27604
|
+
toId = opts.team;
|
|
27605
|
+
} else if (opts.toSelf || !opts.peer && !opts.team) {
|
|
27606
|
+
scene = "p2p";
|
|
27607
|
+
toId = selfUid;
|
|
27608
|
+
} else {
|
|
27609
|
+
scene = "p2p";
|
|
27610
|
+
toId = opts.peer.toUpperCase();
|
|
27611
|
+
}
|
|
27612
|
+
const sId = buildSId(scene, selfUid, toId);
|
|
27613
|
+
return { scene, toId, sId };
|
|
27614
|
+
}
|
|
27615
|
+
async function tcpUserSetCacheField(args) {
|
|
27616
|
+
const { data, debug, daemon, cmd, credUid } = args;
|
|
27617
|
+
let os = args.os;
|
|
27618
|
+
const daemonDisabled = daemonRoutingDisabled({ daemon });
|
|
27619
|
+
if (daemonDisabled) await assertNoAssistantSlotConflict(credUid);
|
|
27620
|
+
const daemonRoute = daemonDisabled ? null : await probeDaemonRoute(credUid);
|
|
27621
|
+
if (daemonRoute) {
|
|
27622
|
+
os = assertNoSlotConflict({
|
|
27623
|
+
route: daemonRoute,
|
|
27624
|
+
userOs: args.optsOs ?? os,
|
|
27625
|
+
userExplicit: userExplicitOs(cmd)
|
|
27626
|
+
});
|
|
27627
|
+
const res = await tcpSendViaDaemon({ sid: "user", cid: "setCacheField", data });
|
|
27628
|
+
return { res, route: "daemon", daemon_kind: "personal", os };
|
|
27629
|
+
}
|
|
27630
|
+
const cred = requireAuth(loadCredentials());
|
|
27631
|
+
const session = await openChatSession(cred, { os, debug });
|
|
27632
|
+
try {
|
|
27633
|
+
const res = await session.socket.send("user", "setCacheField", data);
|
|
27634
|
+
return { res, route: "direct", os };
|
|
27635
|
+
} finally {
|
|
27636
|
+
session.socket.close();
|
|
27637
|
+
}
|
|
27638
|
+
}
|
|
27639
|
+
function registerPin(program3) {
|
|
27640
|
+
define2(program3, {
|
|
27641
|
+
name: "pin",
|
|
27642
|
+
description: "Pin a chat session to the top of the conversation list (TCP user/setCacheField sticky_on_top_id, op=1). Multi-device via user cache. Will kick a session in the same online-slot.",
|
|
27643
|
+
output: MutateOutput,
|
|
27644
|
+
configure: (cmd) => {
|
|
27645
|
+
cmd.option("--peer <uid>", "1:1 session with this uid").option("--team <id>", "group session (team id)").option("--to-self", "self-chat session (default if no target)").option("--sid <sId>", "raw session id (teamId or UID|UID); overrides --peer/--team/--to-self").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--no-daemon", "bypass any running personal daemon (also: MUC_NO_DAEMON=1)").option("--debug", "forward wire-level debug events to stderr");
|
|
27646
|
+
},
|
|
27647
|
+
run: async (cmd) => mutateSticky(cmd, "pin")
|
|
27648
|
+
});
|
|
27649
|
+
}
|
|
27650
|
+
function registerUnpin(program3) {
|
|
27651
|
+
define2(program3, {
|
|
27652
|
+
name: "unpin",
|
|
27653
|
+
description: "Remove a session from the pin list (TCP user/setCacheField sticky_on_top_id, op=2). Will kick a session in the same online-slot.",
|
|
27654
|
+
output: MutateOutput,
|
|
27655
|
+
configure: (cmd) => {
|
|
27656
|
+
cmd.option("--peer <uid>", "1:1 session with this uid").option("--team <id>", "group session (team id)").option("--to-self", "self-chat session (default if no target)").option("--sid <sId>", "raw session id (teamId or UID|UID); overrides --peer/--team/--to-self").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--no-daemon", "bypass any running personal daemon (also: MUC_NO_DAEMON=1)").option("--debug", "forward wire-level debug events to stderr");
|
|
27657
|
+
},
|
|
27658
|
+
run: async (cmd) => mutateSticky(cmd, "unpin")
|
|
27659
|
+
});
|
|
27660
|
+
}
|
|
27661
|
+
async function mutateSticky(cmd, action) {
|
|
27662
|
+
const opts = cmd.opts();
|
|
27663
|
+
const cfg = requireConfig();
|
|
27664
|
+
const cred = requireAuth(loadCredentials());
|
|
27665
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
27666
|
+
const { scene, toId, sId } = resolveTarget(opts, cred.uid, action);
|
|
27667
|
+
const op = action === "pin" ? 1 : 2;
|
|
27668
|
+
const data = buildStickyCacheFieldData({ sId, op });
|
|
27669
|
+
const frame = { sid: "user", cid: "setCacheField", data };
|
|
27670
|
+
if (opts.dryRun) {
|
|
27671
|
+
return {
|
|
27672
|
+
action,
|
|
27673
|
+
scene,
|
|
27674
|
+
toId,
|
|
27675
|
+
sId,
|
|
27676
|
+
dryRun: true,
|
|
27677
|
+
sticky_ids: [],
|
|
27678
|
+
sessions: [],
|
|
27679
|
+
frame
|
|
27680
|
+
};
|
|
27681
|
+
}
|
|
27682
|
+
const { res, route: route2, daemon_kind } = await tcpUserSetCacheField({
|
|
27683
|
+
data,
|
|
27684
|
+
os,
|
|
27685
|
+
optsOs: opts.os,
|
|
27686
|
+
debug: opts.debug,
|
|
27687
|
+
daemon: opts.daemon,
|
|
27688
|
+
cmd,
|
|
27689
|
+
credUid: cred.uid
|
|
27690
|
+
});
|
|
27691
|
+
if (res?.err) {
|
|
27692
|
+
throw new CliError("REMOTE_ERROR", `${action} rejected: ${JSON.stringify(res).slice(0, 400)}`);
|
|
27693
|
+
}
|
|
27694
|
+
const sticky_ids = extractStickyIds(res);
|
|
27695
|
+
return {
|
|
27696
|
+
action,
|
|
27697
|
+
scene,
|
|
27698
|
+
toId,
|
|
27699
|
+
sId,
|
|
27700
|
+
dryRun: false,
|
|
27701
|
+
sticky_ids,
|
|
27702
|
+
sessions: stickySessionRows(sticky_ids, cred.uid),
|
|
27703
|
+
raw: res,
|
|
27704
|
+
route: route2,
|
|
27705
|
+
daemon_kind
|
|
27706
|
+
};
|
|
27707
|
+
}
|
|
27708
|
+
function registerPinned(program3) {
|
|
27709
|
+
define2(program3, {
|
|
27710
|
+
name: "pinned",
|
|
27711
|
+
description: "List pinned session sIds from user cache (TCP user/setCacheField no-op delete probe \u2192 sticky_on_top_id). Will kick a session in the same online-slot.",
|
|
27712
|
+
output: ListOutput,
|
|
27713
|
+
configure: (cmd) => {
|
|
27714
|
+
cmd.option("--dry-run", "build the probe frame but do not connect").option("--os <os>", "chat-channel os slot (default: cfg.defaultChatOs ?? iOS)", parseChatOs).option("--no-daemon", "bypass any running personal daemon (also: MUC_NO_DAEMON=1)").option("--debug", "forward wire-level debug events to stderr");
|
|
27715
|
+
},
|
|
27716
|
+
run: async (cmd) => {
|
|
27717
|
+
const opts = cmd.opts();
|
|
27718
|
+
const cfg = requireConfig();
|
|
27719
|
+
const cred = requireAuth(loadCredentials());
|
|
27720
|
+
const os = resolveChatOs(opts.os, cfg);
|
|
27721
|
+
const data = buildStickyCacheFieldData({ sId: STICKY_LIST_PROBE_SID, op: 2 });
|
|
27722
|
+
const frame = { sid: "user", cid: "setCacheField", data };
|
|
27723
|
+
if (opts.dryRun) {
|
|
27724
|
+
return { sticky_ids: [], sessions: [], count: 0, dryRun: true, frame };
|
|
27725
|
+
}
|
|
27726
|
+
const { res, route: route2, daemon_kind } = await tcpUserSetCacheField({
|
|
27727
|
+
data,
|
|
27728
|
+
os,
|
|
27729
|
+
optsOs: opts.os,
|
|
27730
|
+
debug: opts.debug,
|
|
27731
|
+
daemon: opts.daemon,
|
|
27732
|
+
cmd,
|
|
27733
|
+
credUid: cred.uid
|
|
27734
|
+
});
|
|
27735
|
+
if (res?.err) {
|
|
27736
|
+
throw new CliError("REMOTE_ERROR", `pinned list rejected: ${JSON.stringify(res).slice(0, 400)}`);
|
|
27737
|
+
}
|
|
27738
|
+
const sticky_ids = extractStickyIds(res);
|
|
27739
|
+
return {
|
|
27740
|
+
sticky_ids,
|
|
27741
|
+
sessions: stickySessionRows(sticky_ids, cred.uid),
|
|
27742
|
+
count: sticky_ids.length,
|
|
27743
|
+
dryRun: false,
|
|
27744
|
+
raw: res,
|
|
27745
|
+
route: route2,
|
|
27746
|
+
daemon_kind
|
|
27747
|
+
};
|
|
27748
|
+
}
|
|
27749
|
+
});
|
|
27750
|
+
}
|
|
27751
|
+
function register24(program3) {
|
|
27752
|
+
registerPin(program3);
|
|
27753
|
+
registerUnpin(program3);
|
|
27754
|
+
registerPinned(program3);
|
|
27755
|
+
}
|
|
27756
|
+
|
|
27509
27757
|
// src/lib/reactions.ts
|
|
27510
27758
|
async function saveReply(cfg, args) {
|
|
27511
27759
|
const { ctx, stickerId, senderName, isFromMe, selfAppKey } = args;
|
|
@@ -27572,7 +27820,7 @@ var Output24 = external_exports.object({
|
|
|
27572
27820
|
emoticonId: external_exports.string().optional(),
|
|
27573
27821
|
raw: external_exports.unknown()
|
|
27574
27822
|
});
|
|
27575
|
-
function
|
|
27823
|
+
function register25(program3) {
|
|
27576
27824
|
define2(program3, {
|
|
27577
27825
|
name: "react",
|
|
27578
27826
|
description: "Add a default reaction emoji to a message (POST emoticon/reply/save). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
|
|
@@ -27669,7 +27917,7 @@ var Output25 = external_exports.object({
|
|
|
27669
27917
|
reactions: external_exports.array(external_exports.unknown()),
|
|
27670
27918
|
raw: external_exports.unknown()
|
|
27671
27919
|
});
|
|
27672
|
-
function
|
|
27920
|
+
function register26(program3) {
|
|
27673
27921
|
define2(program3, {
|
|
27674
27922
|
name: "reactions",
|
|
27675
27923
|
description: "List default emoji reactions on a message (GET emoticon/reply/list?mid=\u2026).",
|
|
@@ -27712,7 +27960,7 @@ var Output26 = external_exports.object({
|
|
|
27712
27960
|
emoticonId: external_exports.string(),
|
|
27713
27961
|
raw: external_exports.unknown()
|
|
27714
27962
|
});
|
|
27715
|
-
function
|
|
27963
|
+
function register27(program3) {
|
|
27716
27964
|
define2(program3, {
|
|
27717
27965
|
name: "unreact",
|
|
27718
27966
|
description: "Revoke one of your default emoji reactions (POST emoticon/reply/revocation). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
|
|
@@ -27785,7 +28033,7 @@ var Output27 = external_exports.object({
|
|
|
27785
28033
|
route: external_exports.enum(["daemon", "direct"]).optional(),
|
|
27786
28034
|
daemon_kind: external_exports.literal("personal").optional()
|
|
27787
28035
|
});
|
|
27788
|
-
function
|
|
28036
|
+
function register28(program3) {
|
|
27789
28037
|
define2(program3, {
|
|
27790
28038
|
name: "wait-react",
|
|
27791
28039
|
description: "Block until someone adds a default emoji reaction (subType=300) to --mid. Consumes a TCP slot \u2014 defaults to --os iOS (Mobile slot).",
|
|
@@ -27998,7 +28246,7 @@ var STREAM_SCHEMA = {
|
|
|
27998
28246
|
}
|
|
27999
28247
|
]
|
|
28000
28248
|
};
|
|
28001
|
-
function
|
|
28249
|
+
function register29(program3) {
|
|
28002
28250
|
defineStream(program3, {
|
|
28003
28251
|
name: "listen",
|
|
28004
28252
|
description: "Tail incoming chat messages, filtered by sender / team / peer / keyword / regex / @-mention. Emit one line per match \u2014 NDJSON envelope under `--json`, human summary otherwise. Consumes a TCP slot.",
|
|
@@ -28773,13 +29021,14 @@ var ALLOWED_TCP_CALLS = /* @__PURE__ */ new Set([
|
|
|
28773
29021
|
"team/search_own_teams",
|
|
28774
29022
|
"team/get_team",
|
|
28775
29023
|
"team/get_team_members",
|
|
28776
|
-
"user/delSession"
|
|
29024
|
+
"user/delSession",
|
|
29025
|
+
"user/setCacheField"
|
|
28777
29026
|
]);
|
|
28778
29027
|
function isAllowedTcpCall(sid, cid) {
|
|
28779
29028
|
return ALLOWED_TCP_CALLS.has(`${sid}/${cid}`);
|
|
28780
29029
|
}
|
|
28781
29030
|
function shouldAuditTcpCall(sid, cid) {
|
|
28782
|
-
return sid === "talk" && cid === "sendMsg" || sid === "user" && cid === "delSession";
|
|
29031
|
+
return sid === "talk" && cid === "sendMsg" || sid === "user" && (cid === "delSession" || cid === "setCacheField");
|
|
28783
29032
|
}
|
|
28784
29033
|
function readState(paths2) {
|
|
28785
29034
|
if (!existsSync10(paths2.state)) return null;
|
|
@@ -28965,7 +29214,7 @@ var STREAM_SCHEMA2 = {
|
|
|
28965
29214
|
}
|
|
28966
29215
|
]
|
|
28967
29216
|
};
|
|
28968
|
-
function
|
|
29217
|
+
function register30(program3) {
|
|
28969
29218
|
define2(program3, {
|
|
28970
29219
|
name: "daemon-start",
|
|
28971
29220
|
description: "Start the personal-account daemon in the foreground. Holds the chat socket for ordinary commands and writes raw daemon events.",
|
|
@@ -31040,7 +31289,7 @@ var KNOWN_CLAUDE_MODELS = /* @__PURE__ */ new Set([
|
|
|
31040
31289
|
"claude-haiku-4-5",
|
|
31041
31290
|
"claude-haiku-4-5-20251001"
|
|
31042
31291
|
]);
|
|
31043
|
-
var
|
|
31292
|
+
var ListOutput2 = external_exports.object({
|
|
31044
31293
|
count: external_exports.number(),
|
|
31045
31294
|
assistants: external_exports.array(external_exports.object({
|
|
31046
31295
|
name: external_exports.string(),
|
|
@@ -31086,7 +31335,7 @@ var LogOutput = external_exports.object({
|
|
|
31086
31335
|
since_ms: external_exports.number(),
|
|
31087
31336
|
rows: external_exports.array(LogRowSchema)
|
|
31088
31337
|
});
|
|
31089
|
-
function
|
|
31338
|
+
function register31(program3) {
|
|
31090
31339
|
define2(program3, {
|
|
31091
31340
|
name: "assistant-new",
|
|
31092
31341
|
description: "Create a new assistant scaffold under ~/.muc/assistants/<name>/. Edit the system-prompt.md afterwards.",
|
|
@@ -31187,7 +31436,7 @@ function register30(program3) {
|
|
|
31187
31436
|
define2(program3, {
|
|
31188
31437
|
name: "assistant-list",
|
|
31189
31438
|
description: "List registered assistants and which are currently attached to the daemon.",
|
|
31190
|
-
output:
|
|
31439
|
+
output: ListOutput2,
|
|
31191
31440
|
run: async () => {
|
|
31192
31441
|
const all = listAssistants();
|
|
31193
31442
|
const active = await fetchActiveSet();
|
|
@@ -35305,7 +35554,8 @@ var ALLOWED_TCP_CALLS2 = /* @__PURE__ */ new Set([
|
|
|
35305
35554
|
"team/search_own_teams",
|
|
35306
35555
|
"team/get_team",
|
|
35307
35556
|
"team/get_team_members",
|
|
35308
|
-
"user/delSession"
|
|
35557
|
+
"user/delSession",
|
|
35558
|
+
"user/setCacheField"
|
|
35309
35559
|
]);
|
|
35310
35560
|
function isAllowedTcpCall2(sid, cid) {
|
|
35311
35561
|
return ALLOWED_TCP_CALLS2.has(`${sid}/${cid}`);
|
|
@@ -35784,7 +36034,7 @@ var StatusOutput2 = external_exports.object({
|
|
|
35784
36034
|
});
|
|
35785
36035
|
var StopOutput2 = external_exports.object({ stopped: external_exports.boolean(), pid: external_exports.number().optional() });
|
|
35786
36036
|
var ResumeOutput = external_exports.object({ resumed: external_exports.boolean() });
|
|
35787
|
-
function
|
|
36037
|
+
function register32(program3) {
|
|
35788
36038
|
define2(program3, {
|
|
35789
36039
|
name: "assistantd-start",
|
|
35790
36040
|
description: "Start the assistant daemon in the foreground. Holds the chat socket on the chosen --os slot. v1 has no launchd integration; rerun under tmux/screen if you want it persistent.",
|
|
@@ -36865,7 +37115,7 @@ var StartOutput3 = external_exports.object({
|
|
|
36865
37115
|
// honest while leaving the long-running HTTP server in the background.
|
|
36866
37116
|
closed: external_exports.boolean()
|
|
36867
37117
|
});
|
|
36868
|
-
function
|
|
37118
|
+
function register33(program3) {
|
|
36869
37119
|
define2(program3, {
|
|
36870
37120
|
name: "assistant-ui",
|
|
36871
37121
|
description: "Start a local HTTP server (default 127.0.0.1:7821) that serves a read-only monitor for the running assistant daemon. Foreground; Ctrl-C to stop.",
|
|
@@ -37241,7 +37491,7 @@ var Output28 = external_exports.object({
|
|
|
37241
37491
|
output_schema: external_exports.unknown()
|
|
37242
37492
|
}))
|
|
37243
37493
|
});
|
|
37244
|
-
function
|
|
37494
|
+
function register34(program3) {
|
|
37245
37495
|
define2(program3, {
|
|
37246
37496
|
name: "manifest",
|
|
37247
37497
|
description: "Emit the full command graph + per-command output JSON Schema. Skill authors should consume this rather than parse --help.",
|
|
@@ -37305,6 +37555,7 @@ register30(program2);
|
|
|
37305
37555
|
register31(program2);
|
|
37306
37556
|
register32(program2);
|
|
37307
37557
|
register33(program2);
|
|
37558
|
+
register34(program2);
|
|
37308
37559
|
async function main() {
|
|
37309
37560
|
try {
|
|
37310
37561
|
await program2.parseAsync(argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mu-cabin/muc-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Terminal client for MUC IM (com.meicloud.im.dh).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist/muc.mjs",
|
|
12
12
|
"dist/templates",
|
|
13
13
|
"README.md",
|
|
14
|
+
"REFERENCE.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
|
16
17
|
"engines": {
|