@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.
- package/README.md +36 -6
- package/dist/muc.mjs +730 -20
- 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,
|
|
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) =>
|
|
22878
|
-
|
|
22879
|
-
|
|
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
|
-
|
|
23009
|
+
const sock = this.sock;
|
|
23010
|
+
this.teardown(new Error("socket closed by client"));
|
|
22932
23011
|
try {
|
|
22933
|
-
|
|
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
|
-
|
|
22944
|
-
|
|
22945
|
-
|
|
22946
|
-
console.error(`[wire]
|
|
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/
|
|
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
|
|
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:
|
|
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);
|