@mu-cabin/muc-cli 0.2.1 → 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 +49 -6
- package/dist/muc.mjs +814 -23
- 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
|
|
@@ -222,6 +230,19 @@ before using any chat-channel command if you have a real device logged in.**
|
|
|
222
230
|
Sending to anyone other than yourself requires the explicit `--yes-not-self`
|
|
223
231
|
flag — a guard against accidentally messaging real humans during testing.
|
|
224
232
|
|
|
233
|
+
**Message tail (signature)**: by default `muc send` appends a short tail to
|
|
234
|
+
the text it sends (e.g. `"\n—— 来自王璐璐的muc-cli"`) so recipients can
|
|
235
|
+
tell automated traffic from a typed message. The check is idempotent —
|
|
236
|
+
if your text already ends with the resolved tail, it is not appended a
|
|
237
|
+
second time. Customise it with the
|
|
238
|
+
`messageTail` field in `~/.muc/config.json` — set to a non-empty string to
|
|
239
|
+
override, or to `""` to disable entirely. You can also use
|
|
240
|
+
`muc init --message-tail "<custom>"` / `muc init --no-message-tail` to
|
|
241
|
+
write the value, and per-call flags `muc send --tail "<one-off>"` /
|
|
242
|
+
`muc send --no-tail` to override on a single send. Only `muc send`
|
|
243
|
+
(plain-text) is affected; `send-file` / `send-image` / `send-voice` are
|
|
244
|
+
not.
|
|
245
|
+
|
|
225
246
|
```bash
|
|
226
247
|
muc tcp-ping --wait 8 # safe protocol smoke test
|
|
227
248
|
muc send "hello from the CLI" # to-self (default)
|
|
@@ -338,6 +359,30 @@ $ muc emoticons
|
|
|
338
359
|
Both files share the `~/.muc/` parent (mode 700). The CLI creates them on
|
|
339
360
|
first use; you should never need to edit them by hand.
|
|
340
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
|
+
|
|
341
386
|
## What it can't do (yet)
|
|
342
387
|
|
|
343
388
|
- **Richtext messages** (`subType=11`, mixed text + image + @-mention in one
|
|
@@ -347,8 +392,6 @@ first use; you should never need to edit them by hand.
|
|
|
347
392
|
- **Forwarded / merged messages** (`subType=26`).
|
|
348
393
|
- **SSO / scan-to-login.** REST username+password works; the SSO flow is
|
|
349
394
|
not wired up.
|
|
350
|
-
- **Live push streaming.** Use `muc history` for server-side message
|
|
351
|
-
retrieval; there is no long-lived `talk/msg` listener wired up.
|
|
352
395
|
- **Image thumbnails.** The upload step asks the server to make `tn1`/`tn5`
|
|
353
396
|
thumbs, but `muc download` only fetches the original `fileKey`.
|
|
354
397
|
- **End-to-end-encrypted file content.** Not active on the default tenant
|
|
@@ -391,7 +434,7 @@ first use; you should never need to edit them by hand.
|
|
|
391
434
|
| 2 | USAGE |
|
|
392
435
|
| 3 | AUTH_REQUIRED, AUTH_EXPIRED |
|
|
393
436
|
| 4 | NOT_FOUND |
|
|
394
|
-
| 5 | REMOTE_ERROR, NETWORK |
|
|
437
|
+
| 5 | REMOTE_ERROR, NETWORK, TIMEOUT, KICKED |
|
|
395
438
|
| 6 | CONFIG_MISSING |
|
|
396
439
|
| 7 | CONFIG_INVALID |
|
|
397
440
|
|
package/dist/muc.mjs
CHANGED
|
@@ -21301,6 +21301,12 @@ var MucConfigSchema = external_exports.object({
|
|
|
21301
21301
|
imPort: external_exports.number().int().min(1).max(65535),
|
|
21302
21302
|
fileHost: external_exports.string().min(1, "fileHost required"),
|
|
21303
21303
|
filePort: external_exports.number().int().min(1).max(65535),
|
|
21304
|
+
// Optional signature appended to outbound `muc send` text. Tri-state:
|
|
21305
|
+
// undefined → use built-in default (defaultMessageTail in constants.ts)
|
|
21306
|
+
// "" → explicit opt-out (no tail appended)
|
|
21307
|
+
// any other → that literal string (e.g. "\n— bot")
|
|
21308
|
+
// Per-call `--tail` / `--no-tail` flags on `muc send` override this.
|
|
21309
|
+
messageTail: external_exports.string().max(200).optional(),
|
|
21304
21310
|
savedAt: external_exports.number().int().nonnegative(),
|
|
21305
21311
|
schemaVersion: external_exports.literal(CONFIG_SCHEMA_VERSION)
|
|
21306
21312
|
});
|
|
@@ -21397,6 +21403,8 @@ var EXIT_CODES = {
|
|
|
21397
21403
|
NOT_FOUND: 4,
|
|
21398
21404
|
REMOTE_ERROR: 5,
|
|
21399
21405
|
NETWORK: 5,
|
|
21406
|
+
TIMEOUT: 5,
|
|
21407
|
+
KICKED: 5,
|
|
21400
21408
|
CONFIG_MISSING: 6,
|
|
21401
21409
|
CONFIG_INVALID: 7,
|
|
21402
21410
|
INTERNAL: 1
|
|
@@ -21414,6 +21422,9 @@ var JSON_MODE = false;
|
|
|
21414
21422
|
function setJsonMode(v) {
|
|
21415
21423
|
JSON_MODE = v;
|
|
21416
21424
|
}
|
|
21425
|
+
function isJsonMode() {
|
|
21426
|
+
return JSON_MODE;
|
|
21427
|
+
}
|
|
21417
21428
|
function stableStringify(value) {
|
|
21418
21429
|
return JSON.stringify(value, (_k, v) => {
|
|
21419
21430
|
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
@@ -21585,6 +21596,24 @@ function requireSingleTarget(opts, label) {
|
|
|
21585
21596
|
);
|
|
21586
21597
|
}
|
|
21587
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
|
+
}
|
|
21588
21617
|
function define2(program3, spec) {
|
|
21589
21618
|
const cmd = program3.command(spec.name).description(spec.description);
|
|
21590
21619
|
spec.configure?.(cmd);
|
|
@@ -21700,6 +21729,11 @@ var SECURE_SYS_KEY = "mx_app_key";
|
|
|
21700
21729
|
var PWD_DES_IV = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
|
21701
21730
|
var DEFAULT_CHAT_OS = "iOS";
|
|
21702
21731
|
var DEFAULT_REST_PLATFORM = PLATFORM_IOS;
|
|
21732
|
+
function defaultMessageTail(userName) {
|
|
21733
|
+
const name = userName.trim() || "unknown";
|
|
21734
|
+
return `
|
|
21735
|
+
\u2014\u2014 \u6765\u81EA${name}\u7684muc-cli`;
|
|
21736
|
+
}
|
|
21703
21737
|
var DEFAULTS = {
|
|
21704
21738
|
// Hosts/ports for the 东航 deployment. Other tenants override at init.
|
|
21705
21739
|
restHost: "https://muc.ceair.com/",
|
|
@@ -22048,6 +22082,46 @@ function register(program3) {
|
|
|
22048
22082
|
|
|
22049
22083
|
// src/commands/init.ts
|
|
22050
22084
|
import * as readline2 from "node:readline";
|
|
22085
|
+
|
|
22086
|
+
// src/lib/tail.ts
|
|
22087
|
+
function resolveTail(configured, userName, override) {
|
|
22088
|
+
if (override.disable) return "";
|
|
22089
|
+
if (override.custom != null) return override.custom;
|
|
22090
|
+
if (configured === "") return "";
|
|
22091
|
+
if (configured != null) return configured;
|
|
22092
|
+
return defaultMessageTail(userName);
|
|
22093
|
+
}
|
|
22094
|
+
function applyTail(body, tail) {
|
|
22095
|
+
if (!tail) return body;
|
|
22096
|
+
if (body.endsWith(tail)) return body;
|
|
22097
|
+
return body + tail;
|
|
22098
|
+
}
|
|
22099
|
+
function detectFlagConflict(argv2, name) {
|
|
22100
|
+
const valueLong = `--${name}`;
|
|
22101
|
+
const negatedLong = `--no-${name}`;
|
|
22102
|
+
const valueEq = `${valueLong}=`;
|
|
22103
|
+
let sawValueForm = false;
|
|
22104
|
+
let sawNegatedForm = false;
|
|
22105
|
+
for (let i = 0; i < argv2.length; i++) {
|
|
22106
|
+
const a = argv2[i];
|
|
22107
|
+
if (a === "--") break;
|
|
22108
|
+
if (a === valueLong) {
|
|
22109
|
+
sawValueForm = true;
|
|
22110
|
+
i++;
|
|
22111
|
+
continue;
|
|
22112
|
+
}
|
|
22113
|
+
if (a.startsWith(valueEq)) {
|
|
22114
|
+
sawValueForm = true;
|
|
22115
|
+
continue;
|
|
22116
|
+
}
|
|
22117
|
+
if (a === negatedLong) {
|
|
22118
|
+
sawNegatedForm = true;
|
|
22119
|
+
}
|
|
22120
|
+
}
|
|
22121
|
+
return { sawValueForm, sawNegatedForm };
|
|
22122
|
+
}
|
|
22123
|
+
|
|
22124
|
+
// src/commands/init.ts
|
|
22051
22125
|
var FingerprintShape = external_exports.object({
|
|
22052
22126
|
appKey: external_exports.string(),
|
|
22053
22127
|
appId: external_exports.string(),
|
|
@@ -22060,6 +22134,8 @@ var FingerprintShape = external_exports.object({
|
|
|
22060
22134
|
imPort: external_exports.number(),
|
|
22061
22135
|
fileHost: external_exports.string(),
|
|
22062
22136
|
filePort: external_exports.number(),
|
|
22137
|
+
messageTail: external_exports.string(),
|
|
22138
|
+
// summary: "default" / "none" / "custom (N chars)"
|
|
22063
22139
|
savedAt: external_exports.number(),
|
|
22064
22140
|
schemaVersion: external_exports.literal(CONFIG_SCHEMA_VERSION)
|
|
22065
22141
|
});
|
|
@@ -22080,6 +22156,11 @@ function fingerprint2(v) {
|
|
|
22080
22156
|
if (v.length <= 8) return "***";
|
|
22081
22157
|
return `${v.slice(0, 4)}\u2026${v.slice(-4)}`;
|
|
22082
22158
|
}
|
|
22159
|
+
function summarizeTail(t) {
|
|
22160
|
+
if (t === void 0) return "default";
|
|
22161
|
+
if (t === "") return "none";
|
|
22162
|
+
return `custom (${t.length} chars)`;
|
|
22163
|
+
}
|
|
22083
22164
|
function redactConfig(c) {
|
|
22084
22165
|
return {
|
|
22085
22166
|
appKey: fingerprint2(c.appKey),
|
|
@@ -22093,6 +22174,7 @@ function redactConfig(c) {
|
|
|
22093
22174
|
imPort: c.imPort,
|
|
22094
22175
|
fileHost: c.fileHost,
|
|
22095
22176
|
filePort: c.filePort,
|
|
22177
|
+
messageTail: summarizeTail(c.messageTail),
|
|
22096
22178
|
savedAt: c.savedAt,
|
|
22097
22179
|
schemaVersion: c.schemaVersion
|
|
22098
22180
|
};
|
|
@@ -22238,7 +22320,7 @@ function register2(program3) {
|
|
|
22238
22320
|
description: "Bootstrap or update ~/.muc/config.json (tenant config: APP_KEY, APP_ID, signing secrets, hosts/ports). Interactive prompts by default; non-interactive via flags or MUC_* env vars. WARNING: env vars are visible to other processes via /proc/<pid>/environ and shell history \u2014 prefer interactive or flag-based for production.",
|
|
22239
22321
|
output: Output2,
|
|
22240
22322
|
configure: (cmd) => {
|
|
22241
|
-
cmd.option("--check", "verify config presence and validity (exit 0 ok / 6 missing / 7 invalid); does not modify").option("--show", "print the saved config (secrets are always redacted in both human and JSON output; cat ~/.muc/config.json for raw values)").option("--reset", "delete ~/.muc/config.json (requires --yes)").option("--yes", "confirm a destructive action like --reset").option("--no-interactive", "fail rather than prompt for missing fields").option("--app-key <hex>", "tenant APP_KEY (8 lowercase hex)").option("--app-id <hex>", "tenant APP_ID (24 lowercase hex)").option("--sign-secret-rest <s>", "REST signing secret").option("--sign-secret-secure <s>", "secure-API signing secret").option("--pwd-des-key <s>", "password DES key (defaults to APP_KEY)").option("--rest-host <url>", "REST host URL").option("--sso-host <url>", "SSO host URL (optional)").option("--im-host <h>", "IM TCP host").option("--im-port <n>", "IM TCP port").option("--file-host <h>", "file-channel TCP host").option("--file-port <n>", "file-channel TCP port");
|
|
22323
|
+
cmd.option("--check", "verify config presence and validity (exit 0 ok / 6 missing / 7 invalid); does not modify").option("--show", "print the saved config (secrets are always redacted in both human and JSON output; cat ~/.muc/config.json for raw values)").option("--reset", "delete ~/.muc/config.json (requires --yes)").option("--yes", "confirm a destructive action like --reset").option("--no-interactive", "fail rather than prompt for missing fields").option("--app-key <hex>", "tenant APP_KEY (8 lowercase hex)").option("--app-id <hex>", "tenant APP_ID (24 lowercase hex)").option("--sign-secret-rest <s>", "REST signing secret").option("--sign-secret-secure <s>", "secure-API signing secret").option("--pwd-des-key <s>", "password DES key (defaults to APP_KEY)").option("--rest-host <url>", "REST host URL").option("--sso-host <url>", "SSO host URL (optional)").option("--im-host <h>", "IM TCP host").option("--im-port <n>", "IM TCP port").option("--file-host <h>", "file-channel TCP host").option("--file-port <n>", "file-channel TCP port").option("--message-tail <text>", "custom signature appended to outbound `muc send` text (max 200 chars)").option("--no-message-tail", "explicitly disable the message tail (no signature appended)");
|
|
22242
22324
|
},
|
|
22243
22325
|
run: async (cmd) => {
|
|
22244
22326
|
const opts = cmd.opts();
|
|
@@ -22336,6 +22418,18 @@ function register2(program3) {
|
|
|
22336
22418
|
partial[f.key] = parsed;
|
|
22337
22419
|
}
|
|
22338
22420
|
}
|
|
22421
|
+
const mtConflict = detectFlagConflict(process.argv.slice(2), "message-tail");
|
|
22422
|
+
if (mtConflict.sawValueForm && mtConflict.sawNegatedForm) {
|
|
22423
|
+
throw new CliError("USAGE", "--message-tail and --no-message-tail are mutually exclusive");
|
|
22424
|
+
}
|
|
22425
|
+
let messageTail;
|
|
22426
|
+
if (opts.messageTail === false) {
|
|
22427
|
+
messageTail = "";
|
|
22428
|
+
} else if (typeof opts.messageTail === "string") {
|
|
22429
|
+
messageTail = opts.messageTail;
|
|
22430
|
+
} else if (existing && "messageTail" in existing) {
|
|
22431
|
+
messageTail = existing.messageTail;
|
|
22432
|
+
}
|
|
22339
22433
|
const draft = {
|
|
22340
22434
|
appKey: String(partial.appKey ?? ""),
|
|
22341
22435
|
appId: String(partial.appId ?? ""),
|
|
@@ -22348,6 +22442,7 @@ function register2(program3) {
|
|
|
22348
22442
|
imPort: Number(partial.imPort ?? 0),
|
|
22349
22443
|
fileHost: String(partial.fileHost ?? ""),
|
|
22350
22444
|
filePort: Number(partial.filePort ?? 0),
|
|
22445
|
+
messageTail,
|
|
22351
22446
|
savedAt: Date.now(),
|
|
22352
22447
|
schemaVersion: CONFIG_SCHEMA_VERSION
|
|
22353
22448
|
};
|
|
@@ -22763,17 +22858,20 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22763
22858
|
host;
|
|
22764
22859
|
port;
|
|
22765
22860
|
ackTimeoutMs;
|
|
22861
|
+
idleTimeoutMs;
|
|
22766
22862
|
debug;
|
|
22767
22863
|
sock = null;
|
|
22768
22864
|
reader;
|
|
22769
22865
|
sq = 2;
|
|
22770
22866
|
pending = /* @__PURE__ */ new Map();
|
|
22771
22867
|
closed = false;
|
|
22868
|
+
idleTimer = null;
|
|
22772
22869
|
constructor(opts) {
|
|
22773
22870
|
super();
|
|
22774
22871
|
this.host = opts.host;
|
|
22775
22872
|
this.port = opts.port;
|
|
22776
22873
|
this.ackTimeoutMs = opts.ackTimeoutMs ?? 25e3;
|
|
22874
|
+
this.idleTimeoutMs = opts.idleTimeoutMs ?? 75e3;
|
|
22777
22875
|
this.debug = !!opts.debug;
|
|
22778
22876
|
this.reader = new FrameReader((f) => this.handleFrame(f));
|
|
22779
22877
|
}
|
|
@@ -22783,12 +22881,20 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22783
22881
|
const sock = createConnection({ host: this.host, port: this.port });
|
|
22784
22882
|
this.sock = sock;
|
|
22785
22883
|
sock.setKeepAlive(true, 2e4);
|
|
22884
|
+
const errorSink = (_e) => {
|
|
22885
|
+
};
|
|
22886
|
+
this.on("error", errorSink);
|
|
22786
22887
|
const onErr = (e) => {
|
|
22787
22888
|
cleanup();
|
|
22788
22889
|
reject(e);
|
|
22789
22890
|
};
|
|
22891
|
+
const onPreReadyClose = () => {
|
|
22892
|
+
cleanup();
|
|
22893
|
+
reject(new Error("chat socket closed before negotiate-ack"));
|
|
22894
|
+
};
|
|
22790
22895
|
const cleanup = () => {
|
|
22791
22896
|
sock.off("error", onErr);
|
|
22897
|
+
sock.off("close", onPreReadyClose);
|
|
22792
22898
|
this.off("ready", onReady);
|
|
22793
22899
|
};
|
|
22794
22900
|
const onReady = () => {
|
|
@@ -22796,26 +22902,56 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22796
22902
|
resolve2();
|
|
22797
22903
|
};
|
|
22798
22904
|
sock.once("error", onErr);
|
|
22905
|
+
sock.once("close", onPreReadyClose);
|
|
22799
22906
|
this.once("ready", onReady);
|
|
22800
22907
|
sock.on("connect", () => {
|
|
22801
22908
|
if (this.debug) console.error("[wire] tcp connect");
|
|
22802
22909
|
this.emit("connect");
|
|
22910
|
+
this.armIdle();
|
|
22803
22911
|
this.sendNegotiate();
|
|
22804
22912
|
});
|
|
22805
|
-
sock.on("data", (buf) =>
|
|
22806
|
-
|
|
22807
|
-
|
|
22808
|
-
if (this.closed) return;
|
|
22809
|
-
this.closed = true;
|
|
22810
|
-
for (const { reject: reject2, timer } of this.pending.values()) {
|
|
22811
|
-
clearTimeout(timer);
|
|
22812
|
-
reject2(new Error("socket closed"));
|
|
22813
|
-
}
|
|
22814
|
-
this.pending.clear();
|
|
22815
|
-
this.emit("close");
|
|
22913
|
+
sock.on("data", (buf) => {
|
|
22914
|
+
this.armIdle();
|
|
22915
|
+
this.reader.feed(buf);
|
|
22816
22916
|
});
|
|
22917
|
+
sock.on("error", (e) => this.emit("error", e));
|
|
22918
|
+
sock.on("close", () => this.teardown(new Error("socket closed")));
|
|
22817
22919
|
});
|
|
22818
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
|
+
}
|
|
22819
22955
|
sendNegotiate() {
|
|
22820
22956
|
const frame = buildFrame(T.NEGOTIATE, NEGOTIATE_BODY, NEGOTIATE_CRC);
|
|
22821
22957
|
if (this.debug) console.error("[wire] \u2192 NEGOTIATE", frame.toString("hex"));
|
|
@@ -22854,11 +22990,26 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22854
22990
|
if (!this.sock || this.closed) throw new Error("not connected");
|
|
22855
22991
|
this.sock.write(buildFrame(type, payload, crcOverride));
|
|
22856
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
|
+
}
|
|
22857
23007
|
close() {
|
|
22858
23008
|
if (!this.sock || this.closed) return;
|
|
22859
|
-
|
|
23009
|
+
const sock = this.sock;
|
|
23010
|
+
this.teardown(new Error("socket closed by client"));
|
|
22860
23011
|
try {
|
|
22861
|
-
|
|
23012
|
+
sock.end();
|
|
22862
23013
|
} catch {
|
|
22863
23014
|
}
|
|
22864
23015
|
}
|
|
@@ -22868,10 +23019,10 @@ var MucChatSocket = class extends EventEmitter {
|
|
|
22868
23019
|
try {
|
|
22869
23020
|
bodyText = aesDecrypt(f.payload);
|
|
22870
23021
|
} catch (e) {
|
|
22871
|
-
|
|
22872
|
-
|
|
22873
|
-
|
|
22874
|
-
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}`);
|
|
22875
23026
|
}
|
|
22876
23027
|
return;
|
|
22877
23028
|
}
|
|
@@ -23519,7 +23670,7 @@ function register15(program3) {
|
|
|
23519
23670
|
description: "Send a plain-text chat message via the TCP chat channel. VISIBLE TO THE RECIPIENT \u2014 non-self targets require --yes-not-self.",
|
|
23520
23671
|
output: Output15,
|
|
23521
23672
|
configure: (cmd) => {
|
|
23522
|
-
cmd.argument("<text...>", "message text (variadic; words rejoined with spaces)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot", parseChatOs, DEFAULT_CHAT_OS).option("--debug", "forward wire-level debug events to stderr");
|
|
23673
|
+
cmd.argument("<text...>", "message text (variadic; words rejoined with spaces)").option("--peer <uid>", "1:1 chat with a peer uid").option("--team <id>", "group chat with a team id").option("--to-self", "send to your own uid (default if no target given)").option("--yes-not-self", "required to confirm sending to anyone other than yourself").option("--dry-run", "build the frame but do not connect").option("--os <os>", "chat-channel os slot", parseChatOs, DEFAULT_CHAT_OS).option("--tail <text>", "override message tail for this call only").option("--no-tail", "do not append a message tail for this call").option("--debug", "forward wire-level debug events to stderr");
|
|
23523
23674
|
},
|
|
23524
23675
|
run: async (cmd) => {
|
|
23525
23676
|
const text = cmd.args.join(" ").trim();
|
|
@@ -23528,6 +23679,15 @@ function register15(program3) {
|
|
|
23528
23679
|
const cfg = requireConfig();
|
|
23529
23680
|
const cred = requireAuth(loadCredentials());
|
|
23530
23681
|
requireSingleTarget(opts, "send");
|
|
23682
|
+
const conflict = detectFlagConflict(process.argv.slice(2), "tail");
|
|
23683
|
+
if (conflict.sawValueForm && conflict.sawNegatedForm) {
|
|
23684
|
+
throw new CliError("USAGE", "--tail and --no-tail are mutually exclusive");
|
|
23685
|
+
}
|
|
23686
|
+
const override = {};
|
|
23687
|
+
if (opts.tail === false) override.disable = true;
|
|
23688
|
+
else if (typeof opts.tail === "string") override.custom = opts.tail;
|
|
23689
|
+
const tail = resolveTail(cfg.messageTail, cred.name ?? "", override);
|
|
23690
|
+
const finalText = applyTail(text, tail);
|
|
23531
23691
|
let scene;
|
|
23532
23692
|
let toId;
|
|
23533
23693
|
if (opts.team) {
|
|
@@ -23554,7 +23714,7 @@ function register15(program3) {
|
|
|
23554
23714
|
scene,
|
|
23555
23715
|
type: 1,
|
|
23556
23716
|
subType: 1,
|
|
23557
|
-
body:
|
|
23717
|
+
body: finalText,
|
|
23558
23718
|
fName: cred.name,
|
|
23559
23719
|
platform: opts.os === "Mac" || opts.os === "OS X" ? "Mac" : "Windows",
|
|
23560
23720
|
appKey: cfg.appKey
|
|
@@ -25010,8 +25170,634 @@ function register21(program3) {
|
|
|
25010
25170
|
});
|
|
25011
25171
|
}
|
|
25012
25172
|
|
|
25013
|
-
// 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
|
|
25014
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({
|
|
25015
25801
|
version: external_exports.string(),
|
|
25016
25802
|
schema_version: external_exports.literal(1),
|
|
25017
25803
|
commands: external_exports.array(external_exports.object({
|
|
@@ -25020,11 +25806,11 @@ var Output22 = external_exports.object({
|
|
|
25020
25806
|
output_schema: external_exports.unknown()
|
|
25021
25807
|
}))
|
|
25022
25808
|
});
|
|
25023
|
-
function
|
|
25809
|
+
function register27(program3) {
|
|
25024
25810
|
define2(program3, {
|
|
25025
25811
|
name: "manifest",
|
|
25026
25812
|
description: "Emit the full command graph + per-command output JSON Schema. Skill authors should consume this rather than parse --help.",
|
|
25027
|
-
output:
|
|
25813
|
+
output: Output26,
|
|
25028
25814
|
run: async () => ({
|
|
25029
25815
|
version: CLI_VERSION,
|
|
25030
25816
|
schema_version: 1,
|
|
@@ -25071,6 +25857,11 @@ register19(program2);
|
|
|
25071
25857
|
register20(program2);
|
|
25072
25858
|
register21(program2);
|
|
25073
25859
|
register22(program2);
|
|
25860
|
+
register23(program2);
|
|
25861
|
+
register24(program2);
|
|
25862
|
+
register25(program2);
|
|
25863
|
+
register26(program2);
|
|
25864
|
+
register27(program2);
|
|
25074
25865
|
async function main() {
|
|
25075
25866
|
try {
|
|
25076
25867
|
await program2.parseAsync(argv);
|