@mu-cabin/muc-cli
v0.3.1
Published
Terminal client for MUC IM (com.meicloud.im.dh).
Readme
muc-cli
Terminal client for 东航 MUC IM (the com.meicloud.im.dh desktop app, also
called MUC.app). Drives the same REST surface the desktop talks to, plus the
binary chat protocol on raw TCP 8101 for sending messages, withdrawing, and
syncing sessions.
REST commands always coexist with the running desktop and phone — the server
allows concurrent accessTokens. Chat-channel commands share online slots
with real devices; see Coexistence below before sending.
What it is
A single command (muc) packaging:
- Send — 1:1 and group text, files, images, and voice clips. Add and remove emoticon reactions on existing messages.
- Read — server-side chat history, profile lookups, group lists, sticker
packs, directory search (people, departments, apps, service numbers), and
long-lived
listentail of inbound messages with sender/keyword filters. - Manage — login / token refresh, withdraw a message, clear a session,
download a chat-attached file, wait for a human emoticon reaction before
proceeding (
wait-react). - Self-describe — every command has a
--jsonmode emitting a stable envelope;muc manifest --jsonreturns the full command graph + per-command output JSON Schema for agent / script consumers.
Install
npm i -g @mu-cabin/muc-cli
muc --versionRequirements: Node ≥ 18. macOS, Linux, and Windows are all supported.
DES-CBC password encryption is crypto-js (pure JS) — no
--openssl-legacy-provider flag needed.
~/.muc/credentials.json and ~/.muc/config.json are written with mode
600 and the parent dir with mode 700. POSIX honors that; on Windows the
modes are silently ignored — protect the files with NTFS ACLs if you're on a
shared host.
First-time setup
Before you can login, run muc init to seed the tenant configuration. The
CLI is generic across MUC deployments — your MUC admin provides the
tenant-specific values.
muc init # interactive: walks through APP_KEY, APP_ID, signing secrets, hostsWhat muc init collects:
| Field | Source | Notes |
|---|---|---|
| appKey | admin | 8 hex chars |
| appId | admin | 24 hex chars |
| signSecretRest | admin | secret for the regular-API signing scheme |
| signSecretSecure | admin | secret for the /contacts/secure/... signing scheme |
| pwdDesKey | admin (optional) | DES-CBC key for password encryption; defaults to appKey |
| restHost | default https://muc.ceair.com/ | press enter to accept |
| ssoHost | default https://store.ceair.com/ | reserved (advanced) |
| imHost / imPort | default muc.ceair.com / 8101 | TCP chat channel |
| fileHost / filePort | default muc.ceair.com / 6101 | TCP file channel |
Built-in defaults are provided for hosts and ports only — never for
appKey, appId, or signing secrets.
Non-interactive (CI, scripts)
You can supply values via flags or env vars:
Get the four secrets from your MUC admin first; the snippets below use placeholder names — substitute the real values your admin gives you.
# Flag form
muc init --no-interactive \
--app-key "$APP_KEY" \
--app-id "$APP_ID" \
--sign-secret-rest "$SIGN_SECRET_REST" \
--sign-secret-secure "$SIGN_SECRET_SECURE" \
--json
# Env-var form
MUC_APP_KEY="$APP_KEY" \
MUC_APP_ID="$APP_ID" \
MUC_SIGN_SECRET_REST="$SIGN_SECRET_REST" \
MUC_SIGN_SECRET_SECURE="$SIGN_SECRET_SECURE" \
muc init --no-interactiveEnvironment variables: MUC_APP_KEY, MUC_APP_ID, MUC_SIGN_SECRET_REST,
MUC_SIGN_SECRET_SECURE, MUC_PWD_DES_KEY, MUC_REST_HOST, MUC_SSO_HOST,
MUC_IM_HOST, MUC_IM_PORT, MUC_FILE_HOST, MUC_FILE_PORT.
Env-var leakage warning. Values exported into the environment are visible to other processes on the same machine via
/proc/<pid>/environon Linux, and they land in shell history if you don't prefix the command with a space (andHISTCONTROL=ignorespaceis set). For production setups prefer the interactive prompt or flag-based invocation — you can protect the flag values with secret-manager retrieval that never touches the env.
Inspect, repair, reset
muc init --check # validate; exit 0 ok, 6 missing, 7 invalid
muc init --check --json # same, machine-readable
muc init --show # print config (secrets always redacted)
muc init --show --json # JSON envelope — secrets are also redacted here
# (read ~/.muc/config.json directly for raw values)
muc init --reset --yes # delete ~/.muc/config.jsonResolution order per field: CLI flag → env var → existing saved value → built-in default (hosts/ports only) → interactive prompt (TTY) → fail.
Login
muc login is non-interactive on the credential side — it never prompts
for a password. Pass it via flag, stdin, env var, or --token-pwd (reuse
the saved tokenPwd).
# Plaintext flag (avoid; lands in shell history)
muc login --account WANGLULU8 --password '…'
# Safer: read password from stdin, no flag value
printf '%s' "$MY_PWD" | muc login --account WANGLULU8 --password-stdin
# Reuse saved tokenPwd (no password needed; ~30 day TTL)
muc login --token-pwd
# Env-var form
MUC_ACCOUNT=WANGLULU8 MUC_PASSWORD='…' muc loginaccountis your MUC uid (uppercase ASCII, e.g.WANGLULU8); falls back to$MUC_ACCOUNTor the previously-saved uid if--accountis omitted.- One of
--password/--password-stdin/--token-pwd/$MUC_PASSWORDis required — runningmuc loginwith no creds fails fast withAUTH_REQUIRED(exit 3). - The password is encrypted client-side (DES-CBC) before being sent — same scheme the desktop client uses.
- A successful login saves to
~/.muc/credentials.json(mode600):accessToken(≈7 day TTL) andtokenPwd(≈30 day TTL, used for silent re-login with--token-pwd). - A stable per-CLI device id is generated at
~/.muc/device-idon first run. It is intentionally different from the desktop's machine-id so REST sessions stay independent.
Output contract
- Default: human-readable plain text on stdout, errors on stderr in the
form
error: <msg>/hint: <hint>/code: <code> (exit <n>). --json(anywhere on the command line): every command emits a single stable JSON envelope on stdout —{ok:true, schema_version:1, data}or{ok:false, schema_version:1, code, msg, hint?}— and stderr stays silent. Agents and scripts should always pass--json.- Exit codes are typed and identical in both modes:
0success,1internal bug,2usage,3auth required / expired,4not found,5remote / network error,6config missing,7config invalid. --help/--versionare human escape hatches and bypass the JSON envelope by design. The canonical agent surface ismuc manifest --json.
Commands
Setup
| Command | What it does |
|---|---|
| muc init | Seed / inspect / reset tenant config (~/.muc/config.json). Run before login. See First-time setup. |
REST commands (no impact on the desktop app)
| Command | What it does |
|---|---|
| muc login | Authenticate, persist accessToken + tokenPwd. |
| muc whoami | Show the saved profile + token freshness. Local only — does not read tenant config. |
| muc search <keyword> | Org / employee directory search (12 results max, server-side). |
| muc search-dept <keyword> | Department directory search (with employee count + path). |
| muc search-app <keyword> | Search the user's MUC widgets/apps drawer. |
| muc search-sno <keyword> | Search service-numbers (服务号); shows ★ for already-subscribed. |
| muc whois <UID> | Employee info + ext info (secureApi). Renders cn / id / mail / departmentName / positionName / empStatusText. --no-ext skips the slower getExtInfo call. |
| muc groups | Starred groups (flat list keyed by team_id) + department groups (keyed by groupId / deptName). |
| muc emoticons | Sticker packages installed on your account. |
| 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. |
| 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. |
| muc react --mid <MID> --sticker-id <id> [target] | Add an emoticon reaction to an existing message. Visible. Defaults to --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. |
| muc reactions --mid <MID> | List all emoticon reactions on a single message. Read-only. |
| muc unreact --emoticon-id <id> [target] | Revoke one of your own reactions (id from muc react or muc reactions). |
| muc manifest | Print the full command graph + per-command output JSON Schema. Pair with --json for agents. |
history flags: scope with one of --peer UID, --team TEAM_ID,
--sid <UID1|UID2 or teamId>, or --to-self. --limit N (default 50),
--days N (window from now, default 30), --before <ts|ISO> to fetch
older than a moment, --sub-type N to filter to one type (e.g. 1 text,
7 file, 8 image), --desc for newest-first (default: oldest at top).
The fetched timestamp is normalized to ms.
muc history --peer YANWENYU --limit 100
muc history --to-self --days 7
muc history --team 9009876543210000 --before 2026-04-20 --json | jq .Chat-channel commands (TCP 8101) — see Coexistence
| Command | What it does |
|---|---|
| muc tcp-ping [--wait N] [--debug] | TCP connect + NEGOTIATE only. Smoke test, no login frame, zero kick risk. |
| muc sync [--os OS] | After chat-login, run syncDone / syncSession / syncWithdraws. Read-only. |
| 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. |
| muc send (--to-self \| --peer UID \| --team TEAM_ID) <text> [--yes-not-self] | Send a plain-text message. Visible to the recipient. |
| 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. |
| 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. |
| 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). |
| muc withdraw --mid <MID> [target] | Recall a sent message. Original gets replaced server-side with "您撤回了一条消息". Target flag must match the original message's chat. |
| 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. |
| muc wait-react --mid <MID> [--from-uid UID] [--sticker-id ID] [--timeout 300] | Block until someone reacts with an emoticon 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. |
| 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. |
--os resolves as per-call --os → defaultChatOs in ~/.muc/config.json
→ built-in iOS. Valid values: Windows, OS X, Mac, iOS, Android.
Set the per-machine default once with muc init --default-os <os> (or the
MUC_DEFAULT_OS env var, or the interactive prompt during muc init). Useful
if your real device is an iPhone — flip the CLI default to windows so
chat-channel commands park on the PC slot instead of kicking your phone.
--os / defaultChatOs does not provide coexistence — read the section
below before using any chat-channel command if you have a real device
logged in.
Sending to anyone other than yourself requires the explicit --yes-not-self
flag — a guard against accidentally messaging real humans during testing.
Message tail (signature): by default muc send appends a short tail to
the text it sends (e.g. "\n—— 来自王璐璐的muc-cli") so recipients can
tell automated traffic from a typed message. The check is idempotent —
if your text already ends with the resolved tail, it is not appended a
second time. Customise it with the
messageTail field in ~/.muc/config.json — set to a non-empty string to
override, or to "" to disable entirely. You can also use
muc init --message-tail "<custom>" / muc init --no-message-tail to
write the value, and per-call flags muc send --tail "<one-off>" /
muc send --no-tail to override on a single send. Only muc send
(plain-text) is affected; send-file / send-image / send-voice are
not.
muc tcp-ping --wait 8 # safe protocol smoke test
muc send "hello from the CLI" # to-self (default)
muc send --peer ZHANG_W2 "ping" --yes-not-self
muc send --team 9009876543210000 "team message" --yes-not-self
muc send-file ./report.pdf --to-self # self test
muc send-image ./screenshot.png --peer ZHANG_W2 --yes-not-self # auto width/height
muc send-voice ./clip.amr --duration 3500 --to-self # 3.5 s clip
muc download --file-key /chat/m/WANGLULU8/.../<md5> --out ./got.bin # by explicit key
muc download --mid 69ef1ae07e5462c465c05e00 --to-self # look up body via roaming
muc download --mid <MID> --peer YANWENYU --out ./inbox/ # save into a directorydownload is the only file-channel command that doesn't open a chat
session — it speaks only port 6101 (and a one-time bucket lookup on first
call). Run it from any environment without disturbing your desktop or phone.
Coexistence
REST commands always coexist with a running desktop and phone — the
server allows concurrent accessTokens. whoami, search, search-dept,
search-app, search-sno, history, whois, groups, emoticons, and
download are REST-only (or 6101-only, in download's case) and safe to
run anytime.
Chat-channel commands (sync, search-team, send, send-file,
send-image, send-voice, withdraw, clear-session) open a TCP-8101
session and share a per-user two-slot model with real devices:
| Slot | --os values |
|---|---|
| PC | Windows, "OS X", Mac |
| Mobile | iOS (default), Android |
A second login in the same slot kicks the previous client off the realtime
channel — the IM panel goes offline, but tokens stay valid (reopen to
recover). The Mac desktop signs in as "OS X" (PC slot); the iPhone signs
in as iOS (Mobile slot).
The CLI defaults to --os iOS so it coexists with a Mac desktop. If your
iPhone is also signed in, both slots are taken and any TCP command will
kick one of them — fall back to REST-only commands, or run
muc init --default-os windows to flip the per-machine default to the PC
slot (which kicks the desktop instead of the phone).
tcp-ping only does TCP connect + NEGOTIATE (no auth/login), so it can't
kick anything. Run it first to confirm the wire protocol is happy on your
network.
If you do get kicked, reopen the desktop's IM panel or relaunch the mobile app to recover — tokens stay valid.
Examples
$ printf '%s' "$MY_PWD" | muc login --account WANGLULU8 --password-stdin
uid: WANGLULU8
empId: 5394483846707200
name: 王璐璐
accessToken: TV5l…u9kQ (redacted; use --json for raw)
accessTokenExpireAt: 1745678340000
tokenPwdRefreshed: true
tokenPwdExpireAt: 1748097540000
$ muc whoami
uid: WANGLULU8
empId: 5394483846707200
name: 王璐璐
deviceId: 8e413b4141…
deviceName: WANGLULU8-mbp.local (muc-cli)
accessToken: TV5l…u9kQ (redacted; use --json for raw)
accessTokenExpireAt: 1745678340000
tokenPwdSet: true
tokenPwdExpireAt: 1748097540000
savedAt: 1745073540000
credentialsPath: ~/.muc/credentials.json
$ muc search 张
ZHANG_W2 张伟 飞行部 / 机长室
ZHANGSAN03 张三 地面服务部
…
$ muc whois ZHANG_W2
uid ZHANG_W2
name 张伟
empId 4892173049281024
mobile 1380013…
email [email protected]
dept 飞行部 / 机长室
title 机长
$ muc groups
starred:
[常用] (3)
9001234567890123 IT 值班群
9001234567890124 运行联络
9001234567890125 部门通知
dept groups: 1
9009876543210000 飞行部 / 机长室
$ muc emoticons
1001 默认表情包 (32)
1042 航空主题 (18)Files & paths
| Path | Purpose | Mode |
|---|---|---|
| ~/.muc/config.json | tenant config: APP_KEY, APP_ID, signing secrets, hosts/ports | 600 |
| ~/.muc/credentials.json | accessToken, tokenPwd, profile | 600 |
| ~/.muc/device-id | stable per-CLI deviceId (sha256 hex) | 600 |
Both files share the ~/.muc/ parent (mode 700). The CLI creates them on
first use; you should never need to edit them by hand.
Changelog
0.3.1
2026.04.29
- Add
defaultChatOsto~/.muc/config.json: per-machine fallback for the chat-channel--osslot. Configure viamuc init --default-os <windows|osx|mac|ios|android>, theMUC_DEFAULT_OSenv var, or the interactive prompt duringmuc init. Resolution order: per-call--os→defaultChatOs→ built-iniOS. 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.
0.3.0
2026.04.29
- Add
muc react/muc unreact/muc reactionsto add, revoke, and list emoticon reactions on existing messages - Add
muc wait-reactto block until a human reacts to a given mid; useful as a "human-in-the-loop approval" gate aftermuc send - Add
muc listen, a long-lived TCP tap with sender / chat / keyword /--at-mefilters, one NDJSON line per match - Exit code
5now also coversTIMEOUTandKICKED
0.2.2
2026.04.28
muc sendplain-text now appends an idempotent signature tail by default; override via--tail/--no-tail,muc init --message-tail, ormessageTailin~/.muc/config.json
0.2.1
2026.04.28
- Skills unified across macOS / Linux / Windows
- On Windows, file mode
600is silently ignored — use NTFS ACLs on shared hosts to protect~/.muc/
0.2.0
2026.04.27
- Add
muc init: extractappKey/appId/ signing secrets out of source code, write to~/.muc/config.json(mode 600) - Companion subcommands:
muc init --check/--show/--reset - License changed to MIT; package published to npm with public access
What it can't do (yet)
- Richtext messages (
subType=11, mixed text + image + @-mention in one bubble). The image segments share the file-channel upload path so the bulk of the work is already there; just needs a body-builder + asend-richcommand. - Forwarded / merged messages (
subType=26). - SSO / scan-to-login. REST username+password works; the SSO flow is not wired up.
- Image thumbnails. The upload step asks the server to make
tn1/tn5thumbs, butmuc downloadonly fetches the originalfileKey. - End-to-end-encrypted file content. Not active on the default tenant and the CLI doesn't implement it.
Troubleshooting
code: CONFIG_MISSING(exit 6) —~/.muc/config.jsonis absent. Runmuc initto create it. The CLI cannot guess your tenant's signing secrets.code: CONFIG_INVALID(exit 7) — the file exists but failed schema validation (corruption, manual edit gone wrong, or a stale schema after upgrade). Runmuc initto repair it;--reset --yesto start clean.login failed: code=…— sign or DES is wrong. Double-check the signing secrets andappKey/appIdyou supplied tomuc initagainst the values your MUC admin provided.muc init --show(secrets redacted) lets you confirm what's currently saved;muc init(interactive) lets you re-enter the values.error: not logged in(exit 3, codeAUTH_REQUIRED) — the saved credentials file is missing or unreadable. Check~/.muc/credentials.jsonand re-runmuc login.code: AUTH_EXPIRED(exit 3) — accessToken or tokenPwd was rejected by the server. Re-runmuc login --token-pwd(or fullmuc loginiftokenPwdis also stale).EPROTO/ TLS errors — corporate proxy or VPN intercepting traffic.muc-clidoes not pin certificates, so adding the proxy CA to your Node trust store should suffice (NODE_EXTRA_CA_CERTS=/path/to/ca.pem).- A TCP send kicks your desktop off the IM panel — the CLI's
--osvalue collided with the desktop's slot. See Coexistence above: default--os iOSonly risks the Mobile slot (your iPhone), not the desktop. If both desktop and phone are online, drop to REST-only commands.
Exit-code summary
| Exit | Code | |---|---| | 0 | success | | 1 | INTERNAL | | 2 | USAGE | | 3 | AUTH_REQUIRED, AUTH_EXPIRED | | 4 | NOT_FOUND | | 5 | REMOTE_ERROR, NETWORK, TIMEOUT, KICKED | | 6 | CONFIG_MISSING | | 7 | CONFIG_INVALID |
Security
- Both files in
~/.muc/are written with mode600. Don't check them into git or copy them to shared machines. - Don't share
~/.muc/config.json— it contains your tenant's signing secrets (signSecretRest,signSecretSecure,pwdDesKey,appKey). Anyone with these can impersonate API calls against your MUC tenant. MUC_PASSWORDends up in shell history; prefer--password-stdinor--token-pwdfor refreshes.- Prefer interactive
muc initover env vars. Environment variables leak through shell history and/proc/<pid>/environto other processes on the same machine. Use the interactive prompt or one-shot flags pulled from a secret manager rather than persistentMUC_*exports. - TCP login over 8101 registers a device session in one of two slots per
user (PC or Mobile). A second login in the same slot kicks the first.
Default
--os iOSkeeps the CLI on the Mobile slot; only your iPhone is ever at risk. tcp-pingis the only chat-channel command that cannot kick anything — use it as a network smoke-test before trying TCP sends.
