openzca 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -8
  2. package/dist/cli.js +202 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,8 @@
1
1
  # openzca
2
2
 
3
- Free and open-source CLI for Zalo, built on [zca-js](https://github.com/nicenathapong/zca-js). Command structure compatible with [zca-cli.dev/docs](https://zca-cli.dev/docs).
3
+ Free and open-source CLI for Zalo, built on [zca-js](https://github.com/RFS-ADRENO/zca-js). Command structure compatible with [zca-cli.dev/docs](https://zca-cli.dev/docs).
4
4
 
5
- ## Integrate with OpenClaw ZaloUser plugin
6
-
7
- Prompt:
8
- ```
9
- help me to enable zalouser openclaw plugin. However, don't install zca cli follow their instruction, please use: npm install -g openzca@latest
10
- ```
5
+ ## Integrate with OpenClaw OpenZalo plugin (including legacy `zalouser`)
11
6
 
12
7
 
13
8
  ## Install
@@ -206,7 +201,10 @@ It also includes stable routing fields for downstream tools:
206
201
 
207
202
  - `threadId`, `targetId`, `conversationId`
208
203
  - `senderId`, `toId`, `chatType`, `msgType`, `timestamp`
204
+ - `mentions` (normalized mention entities: `uid`, `pos`, `len`, `type`, optional `text`)
205
+ - `mentionIds` (flattened mention user IDs)
209
206
  - `metadata.threadId`, `metadata.targetId`, `metadata.senderId`, `metadata.toId`
207
+ - `metadata.mentions`, `metadata.mentionIds`, `metadata.mentionCount`
210
208
  - `quote` and `metadata.quote` when the inbound message is a reply to a previous message
211
209
  - Includes parsed `quote.attach` and extracted `quote.mediaUrls` when attachment URLs are present.
212
210
  - `quoteMediaPath`, `quoteMediaPaths`, `quoteMediaUrl`, `quoteMediaUrls`, `quoteMediaType`, `quoteMediaTypes`
@@ -293,7 +291,7 @@ Supervised mode notes:
293
291
 
294
292
  ## Multi-account profiles
295
293
 
296
- Use `--profile <name>` or set `ZCA_PROFILE=<name>` to switch between accounts. Manage profiles with the `account` commands.
294
+ Use `--profile <name>` or set `OPENZCA_PROFILE=<name>` (or legacy `ZCA_PROFILE=<name>`) to switch between accounts. Manage profiles with the `account` commands.
297
295
 
298
296
  Profile data is stored in `~/.openzca/` (override with `OPENZCA_HOME`):
299
297
 
package/dist/cli.js CHANGED
@@ -164,7 +164,7 @@ async function removeProfile(name) {
164
164
  }
165
165
  async function resolveProfileName(flagProfile) {
166
166
  const db = await ensureProfilesDb();
167
- const picked = flagProfile && flagProfile.trim() || process.env.ZCA_PROFILE && process.env.ZCA_PROFILE.trim() || db.defaultProfile || DEFAULT_PROFILE;
167
+ const picked = flagProfile && flagProfile.trim() || (process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim()) || db.defaultProfile || DEFAULT_PROFILE;
168
168
  if (!db.profiles[picked]) {
169
169
  if (picked === DEFAULT_PROFILE) {
170
170
  await ensureProfile(DEFAULT_PROFILE);
@@ -765,7 +765,7 @@ async function currentProfile(_command) {
765
765
  async function profileForLogin() {
766
766
  const opts = program.opts();
767
767
  const explicit = opts.profile?.trim();
768
- const fromEnv = process.env.ZCA_PROFILE?.trim();
768
+ const fromEnv = process.env.OPENZCA_PROFILE?.trim() || process.env.ZCA_PROFILE?.trim();
769
769
  if (explicit) {
770
770
  await ensureProfile(explicit);
771
771
  return explicit;
@@ -1459,6 +1459,98 @@ function buildReplyMediaAttachedText(params) {
1459
1459
  }
1460
1460
  return lines.join("\n");
1461
1461
  }
1462
+ function parseOptionalInt(value) {
1463
+ const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1464
+ if (!Number.isFinite(numeric)) return void 0;
1465
+ return Math.trunc(numeric);
1466
+ }
1467
+ function buildInboundMention(record, rawText) {
1468
+ const uid = getStringCandidate(record, ["uid", "userId", "user_id", "id"]);
1469
+ if (!uid) return null;
1470
+ const pos = parseOptionalInt(record.pos ?? record.offset ?? record.start ?? record.index);
1471
+ const len = parseOptionalInt(record.len ?? record.length);
1472
+ const type = parseOptionalInt(record.type ?? record.kind);
1473
+ let text = getStringCandidate(record, ["text", "label", "name"]) || (typeof pos === "number" && typeof len === "number" && len > 0 && pos >= 0 && pos < rawText.length ? rawText.slice(pos, Math.min(rawText.length, pos + len)) : "");
1474
+ if (!text.trim()) {
1475
+ text = "";
1476
+ }
1477
+ return {
1478
+ uid,
1479
+ ...typeof pos === "number" ? { pos } : {},
1480
+ ...typeof len === "number" ? { len } : {},
1481
+ ...typeof type === "number" ? { type } : {},
1482
+ ...text ? { text } : {}
1483
+ };
1484
+ }
1485
+ function collectInboundMentions(value, sink, rawText, depth = 0) {
1486
+ if (depth > 6 || sink.size >= 64 || value === null || value === void 0) {
1487
+ return;
1488
+ }
1489
+ if (typeof value === "string") {
1490
+ if (!looksLikeStructuredJsonString(value)) return;
1491
+ try {
1492
+ const parsed = JSON.parse(value);
1493
+ collectInboundMentions(parsed, sink, rawText, depth + 1);
1494
+ } catch {
1495
+ }
1496
+ return;
1497
+ }
1498
+ if (Array.isArray(value)) {
1499
+ for (const item of value) {
1500
+ const mention = asObject(item) ? buildInboundMention(item, rawText) : null;
1501
+ if (mention) {
1502
+ const key = `${mention.uid}|${mention.pos ?? ""}|${mention.len ?? ""}|${mention.type ?? ""}`;
1503
+ sink.set(key, mention);
1504
+ continue;
1505
+ }
1506
+ collectInboundMentions(item, sink, rawText, depth + 1);
1507
+ if (sink.size >= 64) return;
1508
+ }
1509
+ return;
1510
+ }
1511
+ const record = asObject(value);
1512
+ if (!record) return;
1513
+ const direct = buildInboundMention(record, rawText);
1514
+ if (direct) {
1515
+ const key = `${direct.uid}|${direct.pos ?? ""}|${direct.len ?? ""}|${direct.type ?? ""}`;
1516
+ sink.set(key, direct);
1517
+ }
1518
+ const mentionKeys = [
1519
+ "mentions",
1520
+ "mentionInfo",
1521
+ "mention_info",
1522
+ "mentionList",
1523
+ "mention_list",
1524
+ "mention"
1525
+ ];
1526
+ for (const key of mentionKeys) {
1527
+ if (!(key in record)) continue;
1528
+ collectInboundMentions(record[key], sink, rawText, depth + 1);
1529
+ if (sink.size >= 64) return;
1530
+ }
1531
+ for (const nested of Object.values(record)) {
1532
+ collectInboundMentions(nested, sink, rawText, depth + 1);
1533
+ if (sink.size >= 64) return;
1534
+ }
1535
+ }
1536
+ function extractInboundMentions(params) {
1537
+ const sink = /* @__PURE__ */ new Map();
1538
+ const candidates = [
1539
+ params.messageData.mentions,
1540
+ params.messageData.mentionInfo,
1541
+ params.messageData.mention_info,
1542
+ params.messageData.mentionList,
1543
+ params.messageData.mention_list,
1544
+ params.messageData.mention,
1545
+ params.messageData.content,
1546
+ params.parsedContent
1547
+ ];
1548
+ for (const candidate of candidates) {
1549
+ collectInboundMentions(candidate, sink, params.rawText);
1550
+ if (sink.size >= 64) break;
1551
+ }
1552
+ return [...sink.values()];
1553
+ }
1462
1554
  function normalizeFriendLookupRows(value) {
1463
1555
  const queue = [value];
1464
1556
  const rows = [];
@@ -1561,7 +1653,7 @@ program.hook("preAction", (_parent, actionCommand) => {
1561
1653
  argv: process.argv.slice(2),
1562
1654
  cwd: process.cwd(),
1563
1655
  profileFlag: getDebugOptions(actionCommand).profile ?? null,
1564
- envProfile: process.env.ZCA_PROFILE ?? null
1656
+ envProfile: process.env.OPENZCA_PROFILE ?? process.env.ZCA_PROFILE ?? null
1565
1657
  },
1566
1658
  actionCommand
1567
1659
  );
@@ -1990,6 +2082,32 @@ msg.command("undo <msgId> <cliMsgId> <threadId>").option("-g, --group", "Undo in
1990
2082
  }
1991
2083
  )
1992
2084
  );
2085
+ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group", "Edit in group").description("Edit message (compatibility shim: recall old message then resend new text)").action(
2086
+ wrapAction(
2087
+ async (msgId, cliMsgId, threadId, message, opts, command) => {
2088
+ const { api } = await requireApi(command);
2089
+ const type = asThreadType(opts.group);
2090
+ const undoResponse = await api.undo(
2091
+ {
2092
+ msgId,
2093
+ cliMsgId
2094
+ },
2095
+ threadId,
2096
+ type
2097
+ );
2098
+ const sendResponse = await api.sendMessage(message, threadId, type);
2099
+ output(
2100
+ {
2101
+ mode: "undo+send",
2102
+ nativeEditSupported: false,
2103
+ undo: undoResponse,
2104
+ send: sendResponse
2105
+ },
2106
+ false
2107
+ );
2108
+ }
2109
+ )
2110
+ );
1993
2111
  msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeatable)", collectValues, []).option("-g, --group", "Upload in group").description("Upload and send file(s)").action(
1994
2112
  wrapAction(
1995
2113
  async (arg1, arg2, opts, command) => {
@@ -2072,6 +2190,75 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
2072
2190
  }
2073
2191
  )
2074
2192
  );
2193
+ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").description("Pin conversation").action(
2194
+ wrapAction(async (threadId, opts, command) => {
2195
+ const { api } = await requireApi(command);
2196
+ const type = asThreadType(opts.group);
2197
+ const response = await api.setPinnedConversations(true, threadId, type);
2198
+ output(
2199
+ {
2200
+ threadId,
2201
+ threadType: type === ThreadType.Group ? "group" : "user",
2202
+ pinned: true,
2203
+ response
2204
+ },
2205
+ false
2206
+ );
2207
+ })
2208
+ );
2209
+ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation").description("Unpin conversation").action(
2210
+ wrapAction(async (threadId, opts, command) => {
2211
+ const { api } = await requireApi(command);
2212
+ const type = asThreadType(opts.group);
2213
+ const response = await api.setPinnedConversations(false, threadId, type);
2214
+ output(
2215
+ {
2216
+ threadId,
2217
+ threadType: type === ThreadType.Group ? "group" : "user",
2218
+ pinned: false,
2219
+ response
2220
+ },
2221
+ false
2222
+ );
2223
+ })
2224
+ );
2225
+ msg.command("list-pins").option("-j, --json", "JSON output").description("List pinned conversations").action(
2226
+ wrapAction(async (opts, command) => {
2227
+ const { api } = await requireApi(command);
2228
+ const response = await api.getPinConversations();
2229
+ if (opts.json) {
2230
+ output(response, true);
2231
+ return;
2232
+ }
2233
+ output(
2234
+ response.conversations.map((threadId) => ({
2235
+ threadId,
2236
+ pinned: true
2237
+ })),
2238
+ false
2239
+ );
2240
+ })
2241
+ );
2242
+ msg.command("member-info <userId>").option("-j, --json", "JSON output").description("Get member/user profile info").action(
2243
+ wrapAction(async (userId, opts, command) => {
2244
+ const { api } = await requireApi(command);
2245
+ const response = await api.getUserInfo(userId);
2246
+ if (opts.json) {
2247
+ output(response, true);
2248
+ return;
2249
+ }
2250
+ const profiles = response.changed_profiles ?? {};
2251
+ const matchedProfile = profiles[userId] ?? profiles[`${userId}_0`] ?? Object.values(profiles)[0] ?? null;
2252
+ output(
2253
+ {
2254
+ userId,
2255
+ found: Boolean(matchedProfile),
2256
+ profile: matchedProfile
2257
+ },
2258
+ false
2259
+ );
2260
+ })
2261
+ );
2075
2262
  var group = program.command("group").description("Group management");
2076
2263
  group.command("list").option("-j, --json", "JSON output").description("List groups").action(
2077
2264
  wrapAction(async (opts, command) => {
@@ -2791,6 +2978,12 @@ ${replyContextText}` : replyContextText;
2791
2978
  const senderNameForMetadata = message.type === ThreadType.Group ? senderDisplayName : void 0;
2792
2979
  const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
2793
2980
  const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
2981
+ const mentions = extractInboundMentions({
2982
+ messageData,
2983
+ parsedContent,
2984
+ rawText
2985
+ });
2986
+ const mentionIds = mentions.map((item) => item.uid);
2794
2987
  const timestamp = toEpochSeconds(message.data.ts);
2795
2988
  const payload = {
2796
2989
  threadId: message.threadId,
@@ -2816,6 +3009,8 @@ ${replyContextText}` : replyContextText;
2816
3009
  mediaType,
2817
3010
  mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
2818
3011
  mediaKind: mediaKind ?? void 0,
3012
+ mentions: mentions.length > 0 ? mentions : void 0,
3013
+ mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
2819
3014
  metadata: {
2820
3015
  isGroup: message.type === ThreadType.Group,
2821
3016
  chatType,
@@ -2842,7 +3037,10 @@ ${replyContextText}` : replyContextText;
2842
3037
  mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0,
2843
3038
  mediaType,
2844
3039
  mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
2845
- mediaKind: mediaKind ?? void 0
3040
+ mediaKind: mediaKind ?? void 0,
3041
+ mentions: mentions.length > 0 ? mentions : void 0,
3042
+ mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
3043
+ mentionCount: mentions.length > 0 ? mentions.length : void 0
2846
3044
  },
2847
3045
  // Backward-compatible convenience fields.
2848
3046
  chatType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {