npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 listen tail 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 --json mode emitting a stable envelope; muc manifest --json returns the full command graph + per-command output JSON Schema for agent / script consumers.

Install

npm i -g @mu-cabin/muc-cli
muc --version

Requirements: 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, hosts

What 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-interactive

Environment 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>/environ on Linux, and they land in shell history if you don't prefix the command with a space (and HISTCONTROL=ignorespace is 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.json

Resolution 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 login
  • account is your MUC uid (uppercase ASCII, e.g. WANGLULU8); falls back to $MUC_ACCOUNT or the previously-saved uid if --account is omitted.
  • One of --password / --password-stdin / --token-pwd / $MUC_PASSWORD is required — running muc login with no creds fails fast with AUTH_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 (mode 600): accessToken (≈7 day TTL) and tokenPwd (≈30 day TTL, used for silent re-login with --token-pwd).
  • A stable per-CLI device id is generated at ~/.muc/device-id on 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: 0 success, 1 internal bug, 2 usage, 3 auth required / expired, 4 not found, 5 remote / network error, 6 config missing, 7 config invalid.
  • --help / --version are human escape hatches and bypass the JSON envelope by design. The canonical agent surface is muc 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 historyhistory 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 --osdefaultChatOs 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 directory

download 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 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 --osdefaultChatOs → 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.

0.3.0

2026.04.29

  • Add muc react / muc unreact / muc reactions to add, revoke, and list emoticon reactions on existing messages
  • 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
  • Add muc listen, a long-lived TCP tap with sender / chat / keyword / --at-me filters, one NDJSON line per match
  • Exit code 5 now also covers TIMEOUT and KICKED

0.2.2

2026.04.28

  • 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

0.2.1

2026.04.28

  • Skills unified across macOS / Linux / Windows
  • On Windows, file mode 600 is silently ignored — use NTFS ACLs on shared hosts to protect ~/.muc/

0.2.0

2026.04.27

  • Add muc init: extract appKey / 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 + a send-rich command.
  • 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/tn5 thumbs, but muc download only fetches the original fileKey.
  • 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.json is absent. Run muc init to 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). Run muc init to repair it; --reset --yes to start clean.
  • login failed: code=… — sign or DES is wrong. Double-check the signing secrets and appKey / appId you supplied to muc init against 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, code AUTH_REQUIRED) — the saved credentials file is missing or unreadable. Check ~/.muc/credentials.json and re-run muc login.
  • code: AUTH_EXPIRED (exit 3) — accessToken or tokenPwd was rejected by the server. Re-run muc login --token-pwd (or full muc login if tokenPwd is also stale).
  • EPROTO / TLS errors — corporate proxy or VPN intercepting traffic. muc-cli does 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 --os value collided with the desktop's slot. See Coexistence above: default --os iOS only 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 mode 600. 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_PASSWORD ends up in shell history; prefer --password-stdin or --token-pwd for refreshes.
  • Prefer interactive muc init over env vars. Environment variables leak through shell history and /proc/<pid>/environ to other processes on the same machine. Use the interactive prompt or one-shot flags pulled from a secret manager rather than persistent MUC_* 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 iOS keeps the CLI on the Mobile slot; only your iPhone is ever at risk.
  • tcp-ping is the only chat-channel command that cannot kick anything — use it as a network smoke-test before trying TCP sends.