@mu-cabin/muc-cli 0.2.2 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +36 -6
  2. package/dist/muc.mjs +730 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,11 +13,14 @@ with real devices; see [Coexistence](#coexistence) below before sending.
13
13
 
14
14
  A single command (`muc`) packaging:
15
15
 
16
- - **Send** — 1:1 and group text, files, images, and voice clips.
16
+ - **Send** — 1:1 and group text, files, images, and voice clips. Add and
17
+ remove emoticon reactions on existing messages.
17
18
  - **Read** — server-side chat history, profile lookups, group lists, sticker
18
- packs, and directory search (people, departments, apps, service numbers).
19
+ packs, directory search (people, departments, apps, service numbers), and
20
+ long-lived `listen` tail of inbound messages with sender/keyword filters.
19
21
  - **Manage** — login / token refresh, withdraw a message, clear a session,
20
- download a chat-attached file.
22
+ download a chat-attached file, wait for a human emoticon reaction before
23
+ proceeding (`wait-react`).
21
24
  - **Self-describe** — every command has a `--json` mode emitting a stable
22
25
  envelope; `muc manifest --json` returns the full command graph + per-command
23
26
  output JSON Schema for agent / script consumers.
@@ -186,6 +189,9 @@ MUC_ACCOUNT=WANGLULU8 MUC_PASSWORD='…' muc login
186
189
  | `muc emoticons` | Sticker packages installed on your account. |
187
190
  | `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. |
188
191
  | `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. |
192
+ | `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. |
193
+ | `muc reactions --mid <MID>` | List all emoticon reactions on a single message. Read-only. |
194
+ | `muc unreact --emoticon-id <id> [target]` | Revoke one of your own reactions (id from `muc react` or `muc reactions`). |
189
195
  | `muc manifest` | Print the full command graph + per-command output JSON Schema. Pair with `--json` for agents. |
190
196
 
191
197
  `history` flags: scope with one of `--peer UID`, `--team TEAM_ID`,
@@ -214,6 +220,8 @@ muc history --team 9009876543210000 --before 2026-04-20 --json | jq .
214
220
  | `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). |
215
221
  | `muc withdraw --mid <MID> [target]` | Recall a sent message. Original gets replaced server-side with "您撤回了一条消息". Target flag must match the original message's chat. |
216
222
  | `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. |
223
+ | `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. |
224
+ | `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`. |
217
225
 
218
226
  `--os` defaults to `iOS`. Valid values: `Windows`, `OS X`, `Mac`, `iOS`,
219
227
  `Android`. **`--os` does not provide coexistence — read the section below
@@ -351,6 +359,30 @@ $ muc emoticons
351
359
  Both files share the `~/.muc/` parent (mode 700). The CLI creates them on
352
360
  first use; you should never need to edit them by hand.
353
361
 
362
+ ## Changelog
363
+
364
+ #### `0.3.0`
365
+ `2026.04.29`
366
+ - Add `muc react` / `muc unreact` / `muc reactions` to add, revoke, and list emoticon reactions on existing messages
367
+ - 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`
368
+ - Add `muc listen`, a long-lived TCP tap with sender / chat / keyword / `--at-me` filters, one NDJSON line per match
369
+ - Exit code `5` now also covers `TIMEOUT` and `KICKED`
370
+
371
+ #### `0.2.2`
372
+ `2026.04.28`
373
+ - `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`
374
+
375
+ #### `0.2.1`
376
+ `2026.04.28`
377
+ - Skills unified across macOS / Linux / Windows
378
+ - On Windows, file mode `600` is silently ignored — use NTFS ACLs on shared hosts to protect `~/.muc/`
379
+
380
+ #### `0.2.0`
381
+ `2026.04.27`
382
+ - Add `muc init`: extract `appKey` / `appId` / signing secrets out of source code, write to `~/.muc/config.json` (mode 600)
383
+ - Companion subcommands: `muc init --check` / `--show` / `--reset`
384
+ - License changed to MIT; package published to npm with public access
385
+
354
386
  ## What it can't do (yet)
355
387
 
356
388
  - **Richtext messages** (`subType=11`, mixed text + image + @-mention in one
@@ -360,8 +392,6 @@ first use; you should never need to edit them by hand.
360
392
  - **Forwarded / merged messages** (`subType=26`).
361
393
  - **SSO / scan-to-login.** REST username+password works; the SSO flow is
362
394
  not wired up.
363
- - **Live push streaming.** Use `muc history` for server-side message
364
- retrieval; there is no long-lived `talk/msg` listener wired up.
365
395
  - **Image thumbnails.** The upload step asks the server to make `tn1`/`tn5`
366
396
  thumbs, but `muc download` only fetches the original `fileKey`.
367
397
  - **End-to-end-encrypted file content.** Not active on the default tenant
@@ -404,7 +434,7 @@ first use; you should never need to edit them by hand.
404
434
  | 2 | USAGE |
405
435
  | 3 | AUTH_REQUIRED, AUTH_EXPIRED |
406
436
  | 4 | NOT_FOUND |
407
- | 5 | REMOTE_ERROR, NETWORK |
437
+ | 5 | REMOTE_ERROR, NETWORK, TIMEOUT, KICKED |
408
438
  | 6 | CONFIG_MISSING |
409
439
  | 7 | CONFIG_INVALID |
410
440
 
package/dist/muc.mjs CHANGED
@@ -21403,6 +21403,8 @@ var EXIT_CODES = {
21403
21403
  NOT_FOUND: 4,
21404
21404
  REMOTE_ERROR: 5,
21405
21405
  NETWORK: 5,
21406
+ TIMEOUT: 5,
21407
+ KICKED: 5,
21406
21408
  CONFIG_MISSING: 6,
21407
21409
  CONFIG_INVALID: 7,
21408
21410
  INTERNAL: 1
@@ -21420,6 +21422,9 @@ var JSON_MODE = false;
21420
21422
  function setJsonMode(v) {
21421
21423
  JSON_MODE = v;
21422
21424
  }
21425
+ function isJsonMode() {
21426
+ return JSON_MODE;
21427
+ }
21423
21428
  function stableStringify(value) {
21424
21429
  return JSON.stringify(value, (_k, v) => {
21425
21430
  if (v && typeof v === "object" && !Array.isArray(v)) {
@@ -21591,6 +21596,24 @@ function requireSingleTarget(opts, label) {
21591
21596
  );
21592
21597
  }
21593
21598
  }
21599
+ function defineStream(program3, spec) {
21600
+ const cmd = program3.command(spec.name).description(spec.description);
21601
+ spec.configure?.(cmd);
21602
+ cmd.action(async () => {
21603
+ try {
21604
+ await spec.run(cmd);
21605
+ } catch (e) {
21606
+ if (e instanceof CliError) fail(e.code, e.message, e.hint);
21607
+ const cls = classifyError(e);
21608
+ fail(cls.code, cls.msg, cls.hint);
21609
+ }
21610
+ });
21611
+ REGISTRY.push({
21612
+ name: spec.name,
21613
+ description: spec.description,
21614
+ output_schema: spec.streamSchema ?? { type: "object", description: "NDJSON-stream command \u2014 one JSON object per stdout line" }
21615
+ });
21616
+ }
21594
21617
  function define2(program3, spec) {
21595
21618
  const cmd = program3.command(spec.name).description(spec.description);
21596
21619
  spec.configure?.(cmd);
@@ -22835,17 +22858,20 @@ var MucChatSocket = class extends EventEmitter {
22835
22858
  host;
22836
22859
  port;
22837
22860
  ackTimeoutMs;
22861
+ idleTimeoutMs;
22838
22862
  debug;
22839
22863
  sock = null;
22840
22864
  reader;
22841
22865
  sq = 2;
22842
22866
  pending = /* @__PURE__ */ new Map();
22843
22867
  closed = false;
22868
+ idleTimer = null;
22844
22869
  constructor(opts) {
22845
22870
  super();
22846
22871
  this.host = opts.host;
22847
22872
  this.port = opts.port;
22848
22873
  this.ackTimeoutMs = opts.ackTimeoutMs ?? 25e3;
22874
+ this.idleTimeoutMs = opts.idleTimeoutMs ?? 75e3;
22849
22875
  this.debug = !!opts.debug;
22850
22876
  this.reader = new FrameReader((f) => this.handleFrame(f));
22851
22877
  }
@@ -22855,12 +22881,20 @@ var MucChatSocket = class extends EventEmitter {
22855
22881
  const sock = createConnection({ host: this.host, port: this.port });
22856
22882
  this.sock = sock;
22857
22883
  sock.setKeepAlive(true, 2e4);
22884
+ const errorSink = (_e) => {
22885
+ };
22886
+ this.on("error", errorSink);
22858
22887
  const onErr = (e) => {
22859
22888
  cleanup();
22860
22889
  reject(e);
22861
22890
  };
22891
+ const onPreReadyClose = () => {
22892
+ cleanup();
22893
+ reject(new Error("chat socket closed before negotiate-ack"));
22894
+ };
22862
22895
  const cleanup = () => {
22863
22896
  sock.off("error", onErr);
22897
+ sock.off("close", onPreReadyClose);
22864
22898
  this.off("ready", onReady);
22865
22899
  };
22866
22900
  const onReady = () => {
@@ -22868,26 +22902,56 @@ var MucChatSocket = class extends EventEmitter {
22868
22902
  resolve2();
22869
22903
  };
22870
22904
  sock.once("error", onErr);
22905
+ sock.once("close", onPreReadyClose);
22871
22906
  this.once("ready", onReady);
22872
22907
  sock.on("connect", () => {
22873
22908
  if (this.debug) console.error("[wire] tcp connect");
22874
22909
  this.emit("connect");
22910
+ this.armIdle();
22875
22911
  this.sendNegotiate();
22876
22912
  });
22877
- sock.on("data", (buf) => this.reader.feed(buf));
22878
- sock.on("error", (e) => this.emit("error", e));
22879
- sock.on("close", () => {
22880
- if (this.closed) return;
22881
- this.closed = true;
22882
- for (const { reject: reject2, timer } of this.pending.values()) {
22883
- clearTimeout(timer);
22884
- reject2(new Error("socket closed"));
22885
- }
22886
- this.pending.clear();
22887
- this.emit("close");
22913
+ sock.on("data", (buf) => {
22914
+ this.armIdle();
22915
+ this.reader.feed(buf);
22888
22916
  });
22917
+ sock.on("error", (e) => this.emit("error", e));
22918
+ sock.on("close", () => this.teardown(new Error("socket closed")));
22889
22919
  });
22890
22920
  }
22921
+ /**
22922
+ * (Re)arm the inactivity watchdog. Called every time inbound bytes land
22923
+ * (data callback) or any active confirmation. If no traffic arrives for
22924
+ * `idleTimeoutMs`, we synthesize a teardown so commands don't hang on a
22925
+ * half-open socket the kernel hasn't surfaced yet.
22926
+ */
22927
+ armIdle() {
22928
+ if (this.closed) return;
22929
+ if (this.idleTimer) clearTimeout(this.idleTimer);
22930
+ this.idleTimer = setTimeout(() => {
22931
+ if (this.closed) return;
22932
+ this.emit("error", new Error(`chat socket idle for ${Math.round(this.idleTimeoutMs / 1e3)}s \u2014 tearing down`));
22933
+ try {
22934
+ this.sock?.destroy();
22935
+ } catch {
22936
+ }
22937
+ this.teardown(new Error("idle timeout"));
22938
+ }, this.idleTimeoutMs);
22939
+ }
22940
+ /** Single owner of close-side cleanup: clears idle timer, rejects all pending sends, emits 'close' once. */
22941
+ teardown(reason) {
22942
+ if (this.closed) return;
22943
+ this.closed = true;
22944
+ if (this.idleTimer) {
22945
+ clearTimeout(this.idleTimer);
22946
+ this.idleTimer = null;
22947
+ }
22948
+ for (const { reject, timer } of this.pending.values()) {
22949
+ clearTimeout(timer);
22950
+ reject(reason);
22951
+ }
22952
+ this.pending.clear();
22953
+ this.emit("close");
22954
+ }
22891
22955
  sendNegotiate() {
22892
22956
  const frame = buildFrame(T.NEGOTIATE, NEGOTIATE_BODY, NEGOTIATE_CRC);
22893
22957
  if (this.debug) console.error("[wire] \u2192 NEGOTIATE", frame.toString("hex"));
@@ -22926,11 +22990,26 @@ var MucChatSocket = class extends EventEmitter {
22926
22990
  if (!this.sock || this.closed) throw new Error("not connected");
22927
22991
  this.sock.write(buildFrame(type, payload, crcOverride));
22928
22992
  }
22993
+ /**
22994
+ * Send an unsolicited frame that *re-uses* a sq from a server-pushed frame
22995
+ * (rather than allocating a new one via nextSq). This is how the desktop
22996
+ * SDK ACKs inbound `talk/msg` (message-server/modules/message.js:961-984):
22997
+ * it echoes the original frame's sq back with `{sid, cid, data:{tids}}`.
22998
+ * Fire-and-forget — no pending registration, no response expected.
22999
+ */
23000
+ sendAck(sid, cid, sq, data = {}) {
23001
+ if (!this.sock || this.closed) throw new Error("not connected");
23002
+ const obj = { sid, cid, sq, data };
23003
+ const payload = aesEncrypt(JSON.stringify(obj));
23004
+ if (this.debug) console.error(`[wire] \u2192 ACK ${sid}/${cid} sq=${sq}`);
23005
+ this.sock.write(buildFrame(T.AES_JSON, payload));
23006
+ }
22929
23007
  close() {
22930
23008
  if (!this.sock || this.closed) return;
22931
- this.closed = true;
23009
+ const sock = this.sock;
23010
+ this.teardown(new Error("socket closed by client"));
22932
23011
  try {
22933
- this.sock.end();
23012
+ sock.end();
22934
23013
  } catch {
22935
23014
  }
22936
23015
  }
@@ -22940,10 +23019,10 @@ var MucChatSocket = class extends EventEmitter {
22940
23019
  try {
22941
23020
  bodyText = aesDecrypt(f.payload);
22942
23021
  } catch (e) {
22943
- if (this.listenerCount("error") > 0) {
22944
- this.emit("error", new Error(`aes decrypt failed (type=${f.type} len=${f.payload.length}): ${e.message}`));
22945
- } else {
22946
- console.error(`[wire] aes decrypt failed (type=${f.type} len=${f.payload.length}): ${e.message}`);
23022
+ const err = new Error(`aes decrypt failed (type=${f.type} len=${f.payload.length}): ${e.message}`);
23023
+ this.emit("error", err);
23024
+ if (this.listenerCount("error") <= 1) {
23025
+ console.error(`[wire] ${err.message}`);
22947
23026
  }
22948
23027
  return;
22949
23028
  }
@@ -25091,8 +25170,634 @@ function register21(program3) {
25091
25170
  });
25092
25171
  }
25093
25172
 
25094
- // src/commands/manifest.ts
25173
+ // src/lib/reactions.ts
25174
+ async function saveReply(cfg, args) {
25175
+ const { ctx, stickerId, senderName, isFromMe, selfAppKey } = args;
25176
+ const isGroup = ctx.scene === "team_g";
25177
+ const targetApp = ctx.targetApp ?? selfAppKey;
25178
+ const body = {
25179
+ sid: ctx.sid,
25180
+ mid: ctx.mid,
25181
+ appkey: selfAppKey,
25182
+ toId: isGroup ? ctx.sid : isFromMe ? ctx.peerUid : ctx.srcSenderId,
25183
+ toAppKey: isGroup ? targetApp : isFromMe ? targetApp : ctx.srcSenderApp || ctx.peerApp,
25184
+ groupId: isGroup ? ctx.sid : void 0,
25185
+ stickerId,
25186
+ senderName,
25187
+ srcSenderId: ctx.srcSenderId,
25188
+ srcSenderApp: ctx.srcSenderApp
25189
+ };
25190
+ return await request({
25191
+ method: "POST",
25192
+ auth: true,
25193
+ url: `${apiUrls(cfg).IMM}/emoticon/reply/save`,
25194
+ body,
25195
+ headers: { "Content-Type": "application/json" }
25196
+ });
25197
+ }
25198
+ async function listReplies(cfg, mid) {
25199
+ return await request({
25200
+ method: "GET",
25201
+ auth: true,
25202
+ url: `${apiUrls(cfg).IMM}/emoticon/reply/list`,
25203
+ qs: { mid }
25204
+ });
25205
+ }
25206
+ async function revokeReply(cfg, args) {
25207
+ const { emoticonId, ctx, isFromMe, selfAppKey } = args;
25208
+ const targetApp = ctx.targetApp ?? selfAppKey;
25209
+ const body = {
25210
+ emoticonId,
25211
+ appKey: selfAppKey
25212
+ };
25213
+ if (ctx.scene === "p2p") {
25214
+ body.toId = isFromMe ? ctx.peerUid : ctx.srcSenderId;
25215
+ body.toAppKey = isFromMe ? targetApp : ctx.srcSenderApp || ctx.peerApp;
25216
+ } else {
25217
+ body.groupId = ctx.sid;
25218
+ body.toAppKey = targetApp;
25219
+ }
25220
+ return await request({
25221
+ method: "POST",
25222
+ auth: true,
25223
+ url: `${apiUrls(cfg).IMM}/emoticon/reply/revocation`,
25224
+ body,
25225
+ headers: { "Content-Type": "application/json" }
25226
+ });
25227
+ }
25228
+
25229
+ // src/commands/react.ts
25095
25230
  var Output22 = external_exports.object({
25231
+ scene: external_exports.enum(["p2p", "team_g"]),
25232
+ sId: external_exports.string(),
25233
+ mid: external_exports.string(),
25234
+ stickerId: external_exports.string(),
25235
+ emoticonId: external_exports.string().optional(),
25236
+ raw: external_exports.unknown()
25237
+ });
25238
+ function register22(program3) {
25239
+ define2(program3, {
25240
+ name: "react",
25241
+ description: "Add an emoticon reaction to a message (POST emoticon/reply/save). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
25242
+ output: Output22,
25243
+ configure: (cmd) => {
25244
+ cmd.requiredOption("--mid <mid>", "message id to react to").requiredOption("--sticker-id <id>", "emoticon/sticker id (run `muc emoticons` to list packages)").option("--peer <uid>", "1:1 chat with a peer uid (the OTHER side of the conversation)").option("--team <id>", "group chat with a team id").option("--to-self", "react in your self-chat (default if no target)").option("--src-sender-id <uid>", "UID of the ORIGINAL message sender (defaults to current user \u2014 i.e. reacting to your own message)").option("--src-sender-app <appkey>", "appKey of the original message sender (defaults to your app key)").option("--target-app-key <appkey>", "original message `app_key` field \u2014 distinct from --src-sender-app (`fApp`); only matters in cross-tenant reacts. Defaults to your app key.").option("--sender-name <name>", "override displayed sender name (defaults to your saved name)").option("--yes-not-self", "required to confirm reacting in a non-self conversation");
25245
+ },
25246
+ run: async (cmd) => {
25247
+ const opts = cmd.opts();
25248
+ const cfg = requireConfig();
25249
+ const cred = requireAuth(loadCredentials());
25250
+ requireSingleTarget(opts, "react");
25251
+ let scene;
25252
+ let peerUid;
25253
+ if (opts.team) {
25254
+ scene = "team_g";
25255
+ } else if (opts.toSelf || !opts.peer && !opts.team) {
25256
+ scene = "p2p";
25257
+ peerUid = cred.uid;
25258
+ } else {
25259
+ scene = "p2p";
25260
+ peerUid = opts.peer.toUpperCase();
25261
+ }
25262
+ const isSelf = scene === "p2p" && peerUid === cred.uid;
25263
+ if (!isSelf && !opts.yesNotSelf) {
25264
+ throw new CliError(
25265
+ "USAGE",
25266
+ `refusing to react in ${scene}:${opts.team ?? peerUid} without --yes-not-self`,
25267
+ "add --yes-not-self to confirm \u2014 reactions are visible to all participants"
25268
+ );
25269
+ }
25270
+ const srcSenderId = (opts.srcSenderId ?? cred.uid).toUpperCase();
25271
+ const srcSenderApp = opts.srcSenderApp ?? cfg.appKey;
25272
+ const isFromMe = srcSenderId === cred.uid && srcSenderApp === cfg.appKey;
25273
+ const sid = scene === "team_g" ? opts.team : buildSId("p2p", cred.uid, peerUid);
25274
+ const ctx = {
25275
+ scene,
25276
+ sid,
25277
+ mid: opts.mid,
25278
+ srcSenderId,
25279
+ srcSenderApp,
25280
+ peerUid,
25281
+ peerApp: cfg.appKey,
25282
+ targetApp: opts.targetAppKey
25283
+ };
25284
+ const res = await saveReply(cfg, {
25285
+ ctx,
25286
+ stickerId: opts.stickerId,
25287
+ senderName: opts.senderName ?? cred.name ?? cred.uid,
25288
+ isFromMe,
25289
+ selfAppKey: cfg.appKey
25290
+ });
25291
+ if (res.code !== 0) {
25292
+ throw new CliError("REMOTE_ERROR", `react rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
25293
+ }
25294
+ const data = res.data ?? {};
25295
+ const emoticonId = pickEmoticonId(data);
25296
+ return {
25297
+ scene,
25298
+ sId: sid,
25299
+ mid: opts.mid,
25300
+ stickerId: opts.stickerId,
25301
+ emoticonId,
25302
+ raw: res
25303
+ };
25304
+ }
25305
+ });
25306
+ }
25307
+ function pickEmoticonId(data) {
25308
+ if (!data || typeof data !== "object") return void 0;
25309
+ const d = data;
25310
+ for (const k of ["emoticonId", "id", "replyId", "commentId"]) {
25311
+ const v = d[k];
25312
+ if (typeof v === "string" && v.length > 0) return v;
25313
+ if (typeof v === "number") return String(v);
25314
+ }
25315
+ return void 0;
25316
+ }
25317
+
25318
+ // src/commands/reactions.ts
25319
+ var Output23 = external_exports.object({
25320
+ mid: external_exports.string(),
25321
+ count: external_exports.number(),
25322
+ reactions: external_exports.array(external_exports.unknown()),
25323
+ raw: external_exports.unknown()
25324
+ });
25325
+ function register23(program3) {
25326
+ define2(program3, {
25327
+ name: "reactions",
25328
+ description: "List emoticon reactions on a message (GET emoticon/reply/list?mid=\u2026).",
25329
+ output: Output23,
25330
+ configure: (cmd) => {
25331
+ cmd.requiredOption("--mid <mid>", "message id to inspect");
25332
+ },
25333
+ run: async (cmd) => {
25334
+ const { mid } = cmd.opts();
25335
+ const cfg = requireConfig();
25336
+ requireAuth(loadCredentials());
25337
+ const res = await listReplies(cfg, mid);
25338
+ if (res.code !== 0) {
25339
+ throw new CliError("REMOTE_ERROR", `reactions list rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
25340
+ }
25341
+ const reactions = extractList(res.data);
25342
+ return {
25343
+ mid,
25344
+ count: reactions.length,
25345
+ reactions,
25346
+ raw: res
25347
+ };
25348
+ }
25349
+ });
25350
+ }
25351
+ function extractList(data) {
25352
+ if (Array.isArray(data)) return data;
25353
+ if (data && typeof data === "object") {
25354
+ for (const k of ["list", "reactions", "replies", "records", "items"]) {
25355
+ const v = data[k];
25356
+ if (Array.isArray(v)) return v;
25357
+ }
25358
+ }
25359
+ return [];
25360
+ }
25361
+
25362
+ // src/commands/unreact.ts
25363
+ var Output24 = external_exports.object({
25364
+ scene: external_exports.enum(["p2p", "team_g"]),
25365
+ emoticonId: external_exports.string(),
25366
+ raw: external_exports.unknown()
25367
+ });
25368
+ function register24(program3) {
25369
+ define2(program3, {
25370
+ name: "unreact",
25371
+ description: "Revoke one of your emoticon reactions (POST emoticon/reply/revocation). VISIBLE TO THE TARGET CHAT \u2014 non-self targets require --yes-not-self.",
25372
+ output: Output24,
25373
+ configure: (cmd) => {
25374
+ cmd.requiredOption("--emoticon-id <id>", "reaction id (returned by `muc react` or seen in `muc reactions --mid \u2026`)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "revoke in your self-chat (default if no target)").option("--src-sender-id <uid>", "UID of the original message sender (defaults to current user)").option("--src-sender-app <appkey>", "appKey of the original message sender (defaults to your app key)").option("--target-app-key <appkey>", "original message `app_key` field \u2014 distinct from --src-sender-app (`fApp`); only matters in cross-tenant revokes. Defaults to your app key.").option("--yes-not-self", "required to confirm revoking in a non-self conversation");
25375
+ },
25376
+ run: async (cmd) => {
25377
+ const opts = cmd.opts();
25378
+ const cfg = requireConfig();
25379
+ const cred = requireAuth(loadCredentials());
25380
+ requireSingleTarget(opts, "unreact");
25381
+ let scene;
25382
+ let peerUid;
25383
+ if (opts.team) {
25384
+ scene = "team_g";
25385
+ } else if (opts.toSelf || !opts.peer && !opts.team) {
25386
+ scene = "p2p";
25387
+ peerUid = cred.uid;
25388
+ } else {
25389
+ scene = "p2p";
25390
+ peerUid = opts.peer.toUpperCase();
25391
+ }
25392
+ const isSelf = scene === "p2p" && peerUid === cred.uid;
25393
+ if (!isSelf && !opts.yesNotSelf) {
25394
+ throw new CliError(
25395
+ "USAGE",
25396
+ `refusing to revoke reaction in ${scene}:${opts.team ?? peerUid} without --yes-not-self`,
25397
+ "add --yes-not-self to confirm"
25398
+ );
25399
+ }
25400
+ const srcSenderId = (opts.srcSenderId ?? cred.uid).toUpperCase();
25401
+ const srcSenderApp = opts.srcSenderApp ?? cfg.appKey;
25402
+ const isFromMe = srcSenderId === cred.uid && srcSenderApp === cfg.appKey;
25403
+ const sid = scene === "team_g" ? opts.team : buildSId("p2p", cred.uid, peerUid);
25404
+ const ctx = {
25405
+ scene,
25406
+ sid,
25407
+ // mid not needed for revoke (server identifies by emoticonId)
25408
+ mid: "",
25409
+ srcSenderId,
25410
+ srcSenderApp,
25411
+ peerUid,
25412
+ peerApp: cfg.appKey,
25413
+ targetApp: opts.targetAppKey
25414
+ };
25415
+ const res = await revokeReply(cfg, {
25416
+ emoticonId: opts.emoticonId,
25417
+ ctx,
25418
+ isFromMe,
25419
+ selfAppKey: cfg.appKey
25420
+ });
25421
+ if (res.code !== 0) {
25422
+ throw new CliError("REMOTE_ERROR", `unreact rejected: code=${res.code} msg=${res.msg ?? ""}`.trim());
25423
+ }
25424
+ return {
25425
+ scene,
25426
+ emoticonId: opts.emoticonId,
25427
+ raw: res
25428
+ };
25429
+ }
25430
+ });
25431
+ }
25432
+
25433
+ // src/commands/wait-react.ts
25434
+ var Output25 = external_exports.object({
25435
+ matched: external_exports.literal(true),
25436
+ mid: external_exports.string(),
25437
+ reaction: external_exports.unknown()
25438
+ });
25439
+ function register25(program3) {
25440
+ define2(program3, {
25441
+ name: "wait-react",
25442
+ description: "Block until someone adds an emoticon reaction (subType=300) to --mid. Consumes a TCP slot \u2014 defaults to --os iOS (Mobile slot).",
25443
+ output: Output25,
25444
+ configure: (cmd) => {
25445
+ cmd.requiredOption("--mid <mid>", "message id to wait for a reaction on").option("--from-uid <uid>", "only consider reactions from this UID (case-insensitive match against fId of the type=3 notification)").option("--sticker-id <id>", "only consider reactions whose stickerId matches").option("--timeout <seconds>", "give up after this many seconds (default 300)", parsePosInt("--timeout"), 300).option("--os <os>", "chat-channel os slot", parseChatOs, DEFAULT_CHAT_OS).option("--debug", "forward wire-level debug events to stderr");
25446
+ },
25447
+ run: async (cmd) => {
25448
+ const opts = cmd.opts();
25449
+ const cred = requireAuth(loadCredentials());
25450
+ const wantedFrom = opts.fromUid?.toUpperCase();
25451
+ const session = await openChatSession(cred, { os: opts.os, debug: opts.debug });
25452
+ const { socket } = session;
25453
+ try {
25454
+ return await new Promise((resolve2, reject) => {
25455
+ const timer = setTimeout(() => {
25456
+ reject(new CliError(
25457
+ "TIMEOUT",
25458
+ `no matching reaction on mid=${opts.mid} after ${opts.timeout}s`
25459
+ ));
25460
+ }, opts.timeout * 1e3);
25461
+ const cleanup = () => clearTimeout(timer);
25462
+ socket.on("kicked", (payload) => {
25463
+ cleanup();
25464
+ reject(new CliError(
25465
+ "KICKED",
25466
+ `chat session kicked: ${JSON.stringify(payload).slice(0, 200)}`,
25467
+ "another login claimed the same device slot \u2014 see HANDOFF.md slot model"
25468
+ ));
25469
+ });
25470
+ socket.on("error", (e) => {
25471
+ cleanup();
25472
+ reject(new CliError("NETWORK", `chat socket error: ${e.message}`));
25473
+ });
25474
+ socket.on("close", () => {
25475
+ cleanup();
25476
+ reject(new CliError("NETWORK", "chat socket closed before a matching reaction arrived"));
25477
+ });
25478
+ socket.on("push", (frame) => {
25479
+ ackInboundIfNeeded(socket, frame);
25480
+ if (frame.sid !== "talk") return;
25481
+ if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
25482
+ const msgs = frame.data?.msgs ?? [];
25483
+ for (const m of msgs) {
25484
+ if (m?.type !== 3 || m?.subType !== 300) continue;
25485
+ const body = m.body ?? {};
25486
+ const refMid = String(body.mid ?? body.refMid ?? "");
25487
+ if (refMid !== opts.mid) continue;
25488
+ if (wantedFrom && String(m.fId ?? "").toUpperCase() !== wantedFrom) continue;
25489
+ if (opts.stickerId) {
25490
+ const sid = String(body.stickerId ?? "");
25491
+ if (sid !== opts.stickerId) continue;
25492
+ }
25493
+ cleanup();
25494
+ resolve2({ matched: true, mid: opts.mid, reaction: m });
25495
+ return;
25496
+ }
25497
+ });
25498
+ socket.send("sync", "syncDone", { timestamp: 0 }).catch(() => {
25499
+ });
25500
+ });
25501
+ } finally {
25502
+ socket.close();
25503
+ }
25504
+ }
25505
+ });
25506
+ }
25507
+ function ackInboundIfNeeded(socket, frame) {
25508
+ if (frame.sid !== "talk") return;
25509
+ if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
25510
+ const sqRaw = frame.sq;
25511
+ const sq = typeof sqRaw === "number" ? sqRaw : Number.parseInt(String(sqRaw ?? ""), 10);
25512
+ if (!Number.isFinite(sq)) return;
25513
+ const tids = Array.isArray(frame.data?.tids) ? frame.data.tids : [];
25514
+ if (tids.length === 0) return;
25515
+ try {
25516
+ socket.sendAck(frame.sid, frame.cid, sq, { tids });
25517
+ } catch {
25518
+ }
25519
+ }
25520
+
25521
+ // src/commands/listen.ts
25522
+ var MESSAGE_DATA_SCHEMA = {
25523
+ type: "object",
25524
+ properties: {
25525
+ mid: { type: "string" },
25526
+ sId: { type: "string" },
25527
+ fId: { type: "string" },
25528
+ fApp: { type: "string" },
25529
+ toId: { type: "string" },
25530
+ scene: { type: "string", enum: ["p2p", "team_g"] },
25531
+ type: { type: "number" },
25532
+ subType: { type: "number" },
25533
+ timestamp: { type: "number" },
25534
+ timestamp_u: { type: "number" },
25535
+ body: {}
25536
+ }
25537
+ };
25538
+ var STREAM_SCHEMA = {
25539
+ description: "NDJSON stream of envelopes. Under --json each stdout line is one JSON object: either {ok:true, schema_version, data:<message>} for a match, or {ok:false, schema_version, code, msg, hint?} for an in-band error (KICKED / NETWORK). Without --json each line is a one-line human summary (NOT JSON). Use --json for any pipe consumer.",
25540
+ oneOf: [
25541
+ {
25542
+ type: "object",
25543
+ required: ["ok", "schema_version", "data"],
25544
+ properties: {
25545
+ ok: { const: true },
25546
+ schema_version: { type: "number" },
25547
+ data: MESSAGE_DATA_SCHEMA
25548
+ }
25549
+ },
25550
+ {
25551
+ type: "object",
25552
+ required: ["ok", "schema_version", "code", "msg"],
25553
+ properties: {
25554
+ ok: { const: false },
25555
+ schema_version: { type: "number" },
25556
+ // The streaming `push` path emits KICKED / NETWORK in-band; the
25557
+ // wrapping `defineStream` action handler may also fail() with any
25558
+ // CliError code raised before the loop starts (USAGE for bad
25559
+ // --regex/--type, AUTH_REQUIRED/AUTH_EXPIRED from the credential
25560
+ // gate, CONFIG_MISSING/CONFIG_INVALID from `requireConfig`,
25561
+ // REMOTE_ERROR from the chat-login refresh path, INTERNAL for
25562
+ // unexpected throws). Advertise all of them.
25563
+ code: {
25564
+ type: "string",
25565
+ enum: [
25566
+ "KICKED",
25567
+ "NETWORK",
25568
+ "TIMEOUT",
25569
+ "USAGE",
25570
+ "AUTH_REQUIRED",
25571
+ "AUTH_EXPIRED",
25572
+ "NOT_FOUND",
25573
+ "REMOTE_ERROR",
25574
+ "CONFIG_MISSING",
25575
+ "CONFIG_INVALID",
25576
+ "INTERNAL"
25577
+ ]
25578
+ },
25579
+ msg: { type: "string" },
25580
+ hint: { type: "string" }
25581
+ }
25582
+ }
25583
+ ]
25584
+ };
25585
+ function register26(program3) {
25586
+ defineStream(program3, {
25587
+ name: "listen",
25588
+ 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.",
25589
+ streamSchema: STREAM_SCHEMA,
25590
+ configure: (cmd) => {
25591
+ cmd.option("--from-uid <uid>", "only emit messages from this UID (repeat for OR; matched against fId, case-insensitive)", collect, []).option("--team <id>", "only emit messages in this team (repeat for OR)", collect, []).option("--peer <uid>", "only emit p2p messages with this peer UID (repeat for OR)", collect, []).option("--contains <text>", "only emit messages whose text contains this substring, case-insensitive (repeat for OR)", collect, []).option("--regex <pattern>", "only emit messages whose text matches this RE2-flavoured regex (repeat for OR)", collect, []).option("--type <n>", "only emit messages of this `type` field (default 1; repeat for OR)", collectInt("--type"), []).option("--subtype <n>", "only emit messages of this `subType` field (repeat for OR; default = any)", collectInt("--subtype"), []).option("--max <n>", "exit after emitting this many matching lines", parsePosInt("--max")).option("--at-me", "only emit group messages that @-mention you (mirrors the desktop SDK isAtMe: scene=team_g, fId\u2260self, atIds includes self uid). Implies excluding self.").option("--include-self", "do not filter out messages whose fId is your own UID").option("--include-reactions", "also emit type=3 sub=300/301 (reaction add/remove) frames").option("--include-offline", "also emit messages flushed via cid=offMsg on connect (default: only post-connect msg)").option("--os <os>", "chat-channel os slot", parseChatOs, DEFAULT_CHAT_OS).option("--debug", "forward wire-level debug events to stderr");
25592
+ },
25593
+ run: async (cmd) => {
25594
+ const opts = cmd.opts();
25595
+ const cred = requireAuth(loadCredentials());
25596
+ const wantedFromUids = new Set(opts.fromUid.map((s) => s.toUpperCase()));
25597
+ const wantedTeams = new Set(opts.team.map((s) => s));
25598
+ const wantedPeers = new Set(opts.peer.map((s) => s.toUpperCase()));
25599
+ const containsTerms = opts.contains.map((s) => s.toLowerCase());
25600
+ const regexes = opts.regex.map((p) => {
25601
+ try {
25602
+ return new RegExp(p);
25603
+ } catch (e) {
25604
+ throw new CliError("USAGE", `invalid --regex "${p}": ${e.message}`);
25605
+ }
25606
+ });
25607
+ const subtypeImpliesReactions = opts.subtype.some((s) => s === 300 || s === 301);
25608
+ const wantedTypes = opts.type.length ? new Set(opts.type) : /* @__PURE__ */ new Set([1, ...opts.includeReactions || subtypeImpliesReactions ? [3] : []]);
25609
+ const wantedSubtypes = opts.subtype.length ? new Set(opts.subtype) : null;
25610
+ const includeReactions = !!opts.includeReactions;
25611
+ const max = opts.max;
25612
+ const atMeOnly = !!opts.atMe;
25613
+ if (atMeOnly && opts.includeSelf) {
25614
+ throw new CliError("USAGE", "--at-me and --include-self are mutually exclusive (--at-me only matches messages from other users)");
25615
+ }
25616
+ const includeSelf = !!opts.includeSelf;
25617
+ const selfUidUpper = cred.uid.toUpperCase();
25618
+ const selfAppKey = requireConfig().appKey;
25619
+ const session = await openChatSession(cred, { os: opts.os, debug: opts.debug });
25620
+ const { socket } = session;
25621
+ const json = isJsonMode();
25622
+ let matched = 0;
25623
+ let exited = false;
25624
+ const closeAndExit = (code) => {
25625
+ if (exited) return;
25626
+ exited = true;
25627
+ try {
25628
+ socket.close();
25629
+ } catch {
25630
+ }
25631
+ process.exit(code);
25632
+ };
25633
+ const onSigint = () => closeAndExit(0);
25634
+ process.on("SIGINT", onSigint);
25635
+ process.on("SIGTERM", onSigint);
25636
+ socket.on("kicked", (payload) => {
25637
+ emitErr(
25638
+ "KICKED",
25639
+ `chat session kicked: ${JSON.stringify(payload).slice(0, 200)}`,
25640
+ "another login claimed the same device slot \u2014 see HANDOFF.md slot model"
25641
+ );
25642
+ closeAndExit(5);
25643
+ });
25644
+ socket.on("error", (e) => {
25645
+ emitErr("NETWORK", `chat socket error: ${e.message}`);
25646
+ closeAndExit(5);
25647
+ });
25648
+ socket.on("close", () => {
25649
+ if (!exited) {
25650
+ emitErr("NETWORK", "chat socket closed unexpectedly");
25651
+ closeAndExit(5);
25652
+ }
25653
+ });
25654
+ socket.on("push", (frame) => {
25655
+ ackInboundIfNeeded2(socket, frame);
25656
+ if (frame.sid !== "talk") return;
25657
+ if (frame.cid !== "msg" && !(opts.includeOffline && frame.cid === "offMsg")) return;
25658
+ const msgs = frame.data?.msgs ?? [];
25659
+ for (const m of msgs) {
25660
+ if (!matches(m)) continue;
25661
+ emitMatch(m);
25662
+ matched++;
25663
+ if (max && matched >= max) {
25664
+ closeAndExit(0);
25665
+ return;
25666
+ }
25667
+ }
25668
+ });
25669
+ socket.send("sync", "syncDone", { timestamp: 0 }).catch(() => {
25670
+ });
25671
+ function matches(m) {
25672
+ if (!m || typeof m !== "object") return false;
25673
+ if (!wantedTypes.has(m.type)) {
25674
+ return false;
25675
+ }
25676
+ if (wantedSubtypes && !wantedSubtypes.has(m.subType)) return false;
25677
+ if (m.type === 3 && (m.subType === 300 || m.subType === 301) && !includeReactions && !opts.subtype.length && !opts.type.length) {
25678
+ return false;
25679
+ }
25680
+ if (!includeSelf && String(m.fId ?? "").toUpperCase() === selfUidUpper) {
25681
+ return false;
25682
+ }
25683
+ if (atMeOnly) {
25684
+ if (m.scene !== "team_g") return false;
25685
+ if (String(m.fId ?? "").toUpperCase() === selfUidUpper) return false;
25686
+ const atIds = Array.isArray(m.atIds) ? m.atIds : [];
25687
+ const idx = atIds.findIndex((id) => String(id ?? "").toUpperCase() === selfUidUpper);
25688
+ if (idx < 0) return false;
25689
+ const atAppkeys = Array.isArray(m.atAppkeys) ? m.atAppkeys : void 0;
25690
+ if (atAppkeys && selfAppKey) {
25691
+ const includesSelfApp = atAppkeys.includes(selfAppKey);
25692
+ const slotEmpty = !atAppkeys[idx];
25693
+ if (!includesSelfApp && !slotEmpty) return false;
25694
+ }
25695
+ }
25696
+ if (wantedFromUids.size && !wantedFromUids.has(String(m.fId ?? "").toUpperCase())) return false;
25697
+ if (wantedTeams.size) {
25698
+ if (m.scene !== "team_g") return false;
25699
+ if (!wantedTeams.has(String(m.toId ?? ""))) return false;
25700
+ }
25701
+ if (wantedPeers.size) {
25702
+ if (m.scene !== "p2p") return false;
25703
+ const fromId = String(m.fId ?? "").toUpperCase();
25704
+ const toId = String(m.toId ?? "").toUpperCase();
25705
+ const peer = fromId === cred.uid.toUpperCase() ? toId : fromId;
25706
+ if (!wantedPeers.has(peer)) return false;
25707
+ }
25708
+ if (containsTerms.length || regexes.length) {
25709
+ const text = extractText(m).toLowerCase();
25710
+ if (containsTerms.length && !containsTerms.some((t) => text.includes(t))) return false;
25711
+ if (regexes.length && !regexes.some((r) => r.test(text))) return false;
25712
+ }
25713
+ return true;
25714
+ }
25715
+ function emitMatch(m) {
25716
+ const out = {
25717
+ mid: String(m.mid ?? ""),
25718
+ sId: String(m.sId ?? ""),
25719
+ fId: String(m.fId ?? ""),
25720
+ fApp: String(m.fApp ?? ""),
25721
+ toId: String(m.toId ?? ""),
25722
+ scene: m.scene,
25723
+ type: m.type,
25724
+ subType: m.subType,
25725
+ timestamp: m.timestamp,
25726
+ timestamp_u: m.timestamp_u,
25727
+ body: m.body
25728
+ };
25729
+ if (json) {
25730
+ process.stdout.write(JSON.stringify({ ok: true, schema_version: SCHEMA_VERSION, data: out }) + "\n");
25731
+ } else {
25732
+ process.stdout.write(formatHuman(out) + "\n");
25733
+ }
25734
+ }
25735
+ function emitErr(code, msg, hint) {
25736
+ if (json) {
25737
+ process.stdout.write(JSON.stringify({ ok: false, schema_version: SCHEMA_VERSION, code, msg, hint }) + "\n");
25738
+ } else {
25739
+ process.stderr.write(`error: ${msg}
25740
+ `);
25741
+ if (hint) process.stderr.write(` hint: ${hint}
25742
+ `);
25743
+ process.stderr.write(` code: ${code}
25744
+ `);
25745
+ }
25746
+ }
25747
+ await new Promise(() => {
25748
+ });
25749
+ }
25750
+ });
25751
+ }
25752
+ function collect(value, prev) {
25753
+ return [...prev, value];
25754
+ }
25755
+ function collectInt(label) {
25756
+ return (raw, prev) => {
25757
+ if (!/^\d+$/.test(raw)) throw new CliError("USAGE", `${label} must be a non-negative integer (got ${raw})`);
25758
+ return [...prev, Number(raw)];
25759
+ };
25760
+ }
25761
+ function ackInboundIfNeeded2(socket, frame) {
25762
+ if (frame.sid !== "talk") return;
25763
+ if (frame.cid !== "msg" && frame.cid !== "offMsg") return;
25764
+ const sqRaw = frame.sq;
25765
+ const sq = typeof sqRaw === "number" ? sqRaw : Number.parseInt(String(sqRaw ?? ""), 10);
25766
+ if (!Number.isFinite(sq)) return;
25767
+ const tids = Array.isArray(frame.data?.tids) ? frame.data.tids : [];
25768
+ if (tids.length === 0) return;
25769
+ try {
25770
+ socket.sendAck(frame.sid, frame.cid, sq, { tids });
25771
+ } catch {
25772
+ }
25773
+ }
25774
+ function extractText(m) {
25775
+ const body = m?.body;
25776
+ if (typeof body === "string") return body;
25777
+ if (Array.isArray(body)) {
25778
+ return body.map((seg) => typeof seg?.content === "string" ? seg.content : typeof seg?.text === "string" ? seg.text : "").join(" ");
25779
+ }
25780
+ if (body && typeof body === "object") {
25781
+ const candidates = ["text", "content", "fName", "name"];
25782
+ for (const k of candidates) {
25783
+ if (typeof body[k] === "string") return body[k];
25784
+ }
25785
+ }
25786
+ return "";
25787
+ }
25788
+ function formatHuman(m) {
25789
+ const ts = typeof m.timestamp === "number" ? new Date(m.timestamp * 1e3).toISOString() : "?";
25790
+ const where = m.scene === "team_g" ? `team:${m.toId}` : `p2p:${m.toId}`;
25791
+ const text = (() => {
25792
+ if (typeof m.body === "string") return m.body;
25793
+ if (m.body == null) return "";
25794
+ return JSON.stringify(m.body).slice(0, 200);
25795
+ })();
25796
+ return `${ts} ${m.fId} \u2192 ${where} (type=${m.type}/${m.subType}): ${text}`;
25797
+ }
25798
+
25799
+ // src/commands/manifest.ts
25800
+ var Output26 = external_exports.object({
25096
25801
  version: external_exports.string(),
25097
25802
  schema_version: external_exports.literal(1),
25098
25803
  commands: external_exports.array(external_exports.object({
@@ -25101,11 +25806,11 @@ var Output22 = external_exports.object({
25101
25806
  output_schema: external_exports.unknown()
25102
25807
  }))
25103
25808
  });
25104
- function register22(program3) {
25809
+ function register27(program3) {
25105
25810
  define2(program3, {
25106
25811
  name: "manifest",
25107
25812
  description: "Emit the full command graph + per-command output JSON Schema. Skill authors should consume this rather than parse --help.",
25108
- output: Output22,
25813
+ output: Output26,
25109
25814
  run: async () => ({
25110
25815
  version: CLI_VERSION,
25111
25816
  schema_version: 1,
@@ -25152,6 +25857,11 @@ register19(program2);
25152
25857
  register20(program2);
25153
25858
  register21(program2);
25154
25859
  register22(program2);
25860
+ register23(program2);
25861
+ register24(program2);
25862
+ register25(program2);
25863
+ register26(program2);
25864
+ register27(program2);
25155
25865
  async function main() {
25156
25866
  try {
25157
25867
  await program2.parseAsync(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mu-cabin/muc-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal client for MUC IM (com.meicloud.im.dh).",
5
5
  "license": "MIT",
6
6
  "type": "module",