@ardennguyen/zalo-agent-cli 1.0.7

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 (52) hide show
  1. package/DISCLAIMER.md +27 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -0
  4. package/package.json +64 -0
  5. package/src/cli.test.js +98 -0
  6. package/src/commands/account.js +178 -0
  7. package/src/commands/auto-reply.js +83 -0
  8. package/src/commands/catalog.js +143 -0
  9. package/src/commands/conv.js +264 -0
  10. package/src/commands/friend.js +384 -0
  11. package/src/commands/group.js +606 -0
  12. package/src/commands/label.js +39 -0
  13. package/src/commands/listen.js +293 -0
  14. package/src/commands/login.js +165 -0
  15. package/src/commands/mcp.js +219 -0
  16. package/src/commands/msg.js +608 -0
  17. package/src/commands/oa-init.js +503 -0
  18. package/src/commands/oa-listen.js +215 -0
  19. package/src/commands/oa.js +657 -0
  20. package/src/commands/poll.js +159 -0
  21. package/src/commands/profile.js +274 -0
  22. package/src/commands/quick-msg.js +55 -0
  23. package/src/commands/reminder.js +195 -0
  24. package/src/core/accounts.js +93 -0
  25. package/src/core/credentials.js +63 -0
  26. package/src/core/oa-client.js +407 -0
  27. package/src/core/zalo-client.js +201 -0
  28. package/src/index.js +95 -0
  29. package/src/mcp/mcp-config.js +81 -0
  30. package/src/mcp/mcp-http-transport.js +84 -0
  31. package/src/mcp/mcp-server.js +40 -0
  32. package/src/mcp/mcp-tools.js +353 -0
  33. package/src/mcp/media-downloader.js +191 -0
  34. package/src/mcp/media-downloader.test.js +45 -0
  35. package/src/mcp/message-buffer.js +127 -0
  36. package/src/mcp/message-buffer.test.js +308 -0
  37. package/src/mcp/notifier.js +141 -0
  38. package/src/mcp/notifier.test.js +269 -0
  39. package/src/mcp/thread-filter.js +79 -0
  40. package/src/mcp/thread-filter.test.js +206 -0
  41. package/src/mcp/thread-name-cache.js +162 -0
  42. package/src/mcp/thread-name-cache.test.js +139 -0
  43. package/src/utils/bank-helpers.js +233 -0
  44. package/src/utils/bank-helpers.test.js +66 -0
  45. package/src/utils/extract-message-text.js +21 -0
  46. package/src/utils/output.js +21 -0
  47. package/src/utils/proxy-helpers.js +14 -0
  48. package/src/utils/proxy-helpers.test.js +36 -0
  49. package/src/utils/qr-display.js +91 -0
  50. package/src/utils/qr-display.test.js +97 -0
  51. package/src/utils/qr-http-server.js +190 -0
  52. package/src/utils/update-check.js +56 -0
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Friend commands — list, find, info, add, accept, remove, block, unblock, last-online, online.
3
+ */
4
+
5
+ import { getApi } from "../core/zalo-client.js";
6
+ import { success, error, info, output } from "../utils/output.js";
7
+
8
+ /** Extract numeric error code from zca-js error message string. */
9
+ function extractErrorCode(msg) {
10
+ const match = String(msg).match(/\((\-?\d+)\)/);
11
+ return match ? Number(match[1]) : null;
12
+ }
13
+
14
+ export function registerFriendCommands(program) {
15
+ const friend = program.command("friend").description("Manage friends and contacts");
16
+
17
+ friend
18
+ .command("list")
19
+ .description("List all friends")
20
+ .action(async () => {
21
+ try {
22
+ const result = await getApi().getAllFriends();
23
+ output(result, program.opts().json, () => {
24
+ const profiles = result?.changed_profiles || result || {};
25
+ const entries = Object.entries(profiles);
26
+ info(`${entries.length} friends`);
27
+ for (const [uid, p] of entries) {
28
+ console.log(` ${uid} ${p.displayName || p.zaloName || "?"}`);
29
+ }
30
+ });
31
+ } catch (e) {
32
+ error(e.message);
33
+ }
34
+ });
35
+
36
+ friend
37
+ .command("online")
38
+ .description("List currently online friends")
39
+ .action(async () => {
40
+ try {
41
+ const result = await getApi().getFriendOnlines();
42
+ output(result, program.opts().json);
43
+ } catch (e) {
44
+ error(e.message);
45
+ }
46
+ });
47
+
48
+ friend
49
+ .command("search <name>")
50
+ .description("Search friends by name (returns thread_id for messaging)")
51
+ .action(async (name) => {
52
+ try {
53
+ const result = await getApi().getAllFriends();
54
+ const friends = Array.isArray(result) ? result : [];
55
+ const query = name.toLowerCase();
56
+ const matches = friends.filter((f) => {
57
+ const dn = (f.displayName || "").toLowerCase();
58
+ const zn = (f.zaloName || "").toLowerCase();
59
+ return dn.includes(query) || zn.includes(query);
60
+ });
61
+ output(matches, program.opts().json, () => {
62
+ if (matches.length === 0) {
63
+ error(`No friends matching "${name}". Use "friend list" to see all.`);
64
+ return;
65
+ }
66
+ info(`${matches.length} friend(s) matching "${name}":`);
67
+ console.log();
68
+ for (const f of matches) {
69
+ const display = f.displayName || f.zaloName || "?";
70
+ console.log(` ${f.userId} ${display}`);
71
+ }
72
+ console.log();
73
+ info("Use the ID above as thread_id for messaging commands.");
74
+ info('Example: zalo-agent msg send <thread_id> "Hello"');
75
+ });
76
+ } catch (e) {
77
+ error(e.message);
78
+ }
79
+ });
80
+
81
+ friend
82
+ .command("find <query>")
83
+ .description("Find user by phone number or Zalo ID")
84
+ .action(async (query) => {
85
+ try {
86
+ const result = await getApi().findUser(query);
87
+ if (!result || (!result.uid && !result?.data?.uid)) {
88
+ error(
89
+ `No Zalo user found for "${query}". User may not exist, has disabled phone search, or phone is not registered on Zalo.`,
90
+ );
91
+ return;
92
+ }
93
+ output(result, program.opts().json, () => {
94
+ const u = result?.uid ? result : result?.data || result;
95
+ info(`User ID: ${u.uid || "?"}`);
96
+ info(`Name: ${u.displayName || u.zaloName || u.display_name || u.zalo_name || "?"}`);
97
+ });
98
+ } catch (e) {
99
+ error(`Find user failed: ${e.message}`);
100
+ }
101
+ });
102
+
103
+ friend
104
+ .command("info <userId>")
105
+ .description("Get user profile information")
106
+ .action(async (userId) => {
107
+ try {
108
+ const result = await getApi().getUserInfo(userId);
109
+ output(result, program.opts().json, () => {
110
+ const profiles = result?.changed_profiles || {};
111
+ const p = profiles[userId] || {};
112
+ info(`Name: ${p.displayName || p.zaloName || "?"}`);
113
+ info(`Phone: ${p.phoneNumber || "?"}`);
114
+ info(`Avatar: ${p.avatar || "?"}`);
115
+ });
116
+ } catch (e) {
117
+ error(e.message);
118
+ }
119
+ });
120
+
121
+ friend
122
+ .command("add <userId>")
123
+ .description("Send a friend request")
124
+ .option("-m, --msg <text>", "Message to include", "")
125
+ .action(async (userId, opts) => {
126
+ try {
127
+ // zca-js API signature: sendFriendRequest(msg, userId)
128
+ const result = await getApi().sendFriendRequest(opts.msg, userId);
129
+ output(result, program.opts().json, () => success(`Friend request sent to ${userId}`));
130
+ } catch (e) {
131
+ // Map Zalo error codes to actionable messages
132
+ const code = e.code || extractErrorCode(e.message);
133
+ const errMap = {
134
+ 225: `Already friends with ${userId}. Use "friend list" to verify.`,
135
+ 215: `User ${userId} may have blocked you or is unreachable.`,
136
+ 222: `User ${userId} already sent you a friend request. Use "friend accept ${userId}" instead.`,
137
+ "-1": `Invalid userId "${userId}". Use "friend find <phone>" to get the correct userId first.`,
138
+ };
139
+ error(errMap[code] || `Friend request failed (code ${code}): ${e.message}`);
140
+ }
141
+ });
142
+
143
+ friend
144
+ .command("accept <userId>")
145
+ .description("Accept a friend request")
146
+ .action(async (userId) => {
147
+ try {
148
+ const result = await getApi().acceptFriendRequest(userId);
149
+ output(result, program.opts().json, () => success(`Accepted friend request from ${userId}`));
150
+ } catch (e) {
151
+ error(`Accept friend request failed for ${userId}: ${e.message}`);
152
+ }
153
+ });
154
+
155
+ friend
156
+ .command("remove <userId>")
157
+ .description("Remove a friend")
158
+ .action(async (userId) => {
159
+ try {
160
+ const result = await getApi().removeFriend(userId);
161
+ output(result, program.opts().json, () => success(`Removed friend ${userId}`));
162
+ } catch (e) {
163
+ error(`Remove friend failed for ${userId}: ${e.message}`);
164
+ }
165
+ });
166
+
167
+ friend
168
+ .command("block <userId>")
169
+ .description("Block a user")
170
+ .action(async (userId) => {
171
+ try {
172
+ const result = await getApi().blockUser(userId);
173
+ output(result, program.opts().json, () => success(`Blocked user ${userId}`));
174
+ } catch (e) {
175
+ error(`Block user failed for ${userId}: ${e.message}`);
176
+ }
177
+ });
178
+
179
+ friend
180
+ .command("unblock <userId>")
181
+ .description("Unblock a user")
182
+ .action(async (userId) => {
183
+ try {
184
+ const result = await getApi().unblockUser(userId);
185
+ output(result, program.opts().json, () => success(`Unblocked user ${userId}`));
186
+ } catch (e) {
187
+ error(`Unblock user failed for ${userId}: ${e.message}`);
188
+ }
189
+ });
190
+
191
+ friend
192
+ .command("last-online <userId>")
193
+ .description("Check when user was last online")
194
+ .action(async (userId) => {
195
+ try {
196
+ const result = await getApi().lastOnline(userId);
197
+ output(result, program.opts().json);
198
+ } catch (e) {
199
+ error(e.message);
200
+ }
201
+ });
202
+
203
+ friend
204
+ .command("find-username <username>")
205
+ .description("Find a user by their Zalo username")
206
+ .action(async (username) => {
207
+ try {
208
+ const result = await getApi().findUserByUsername(username);
209
+ output(result, program.opts().json, () => {
210
+ if (!result) {
211
+ error(`No user found for username "${username}"`);
212
+ return;
213
+ }
214
+ info(`User ID: ${result.uid || "?"}`);
215
+ info(`Name: ${result.displayName || result.zaloName || "?"}`);
216
+ });
217
+ } catch (e) {
218
+ error(`Find username failed: ${e.message}`);
219
+ }
220
+ });
221
+
222
+ friend
223
+ .command("alias <friendId> <alias>")
224
+ .description("Set a nickname (alias) for a friend")
225
+ .action(async (friendId, alias) => {
226
+ try {
227
+ const result = await getApi().changeFriendAlias(alias, friendId);
228
+ output(result, program.opts().json, () => success(`Alias set to "${alias}" for ${friendId}`));
229
+ } catch (e) {
230
+ error(`Set alias failed: ${e.message}`);
231
+ }
232
+ });
233
+
234
+ friend
235
+ .command("alias-list")
236
+ .description("List all friend aliases")
237
+ .option("-c, --count <n>", "Page size", parseInt, 100)
238
+ .option("-p, --page <n>", "Page number", parseInt, 1)
239
+ .action(async (opts) => {
240
+ try {
241
+ const result = await getApi().getAliasList(opts.count, opts.page);
242
+ output(result, program.opts().json, () => {
243
+ const items = result?.items || [];
244
+ info(`${items.length} alias(es)`);
245
+ for (const item of items) {
246
+ console.log(` ${item.userId} ${item.alias}`);
247
+ }
248
+ });
249
+ } catch (e) {
250
+ error(`Get alias list failed: ${e.message}`);
251
+ }
252
+ });
253
+
254
+ friend
255
+ .command("alias-remove <friendId>")
256
+ .description("Remove a friend's alias")
257
+ .action(async (friendId) => {
258
+ try {
259
+ const result = await getApi().removeFriendAlias(friendId);
260
+ output(result, program.opts().json, () => success(`Alias removed for ${friendId}`));
261
+ } catch (e) {
262
+ error(`Remove alias failed: ${e.message}`);
263
+ }
264
+ });
265
+
266
+ friend
267
+ .command("reject <userId>")
268
+ .description("Reject a friend request")
269
+ .action(async (userId) => {
270
+ try {
271
+ const result = await getApi().rejectFriendRequest(userId);
272
+ output(result, program.opts().json, () => success(`Rejected friend request from ${userId}`));
273
+ } catch (e) {
274
+ error(`Reject friend request failed: ${e.message}`);
275
+ }
276
+ });
277
+
278
+ friend
279
+ .command("undo-request <userId>")
280
+ .description("Cancel a sent friend request")
281
+ .action(async (userId) => {
282
+ try {
283
+ const result = await getApi().undoFriendRequest(userId);
284
+ output(result, program.opts().json, () => success(`Friend request to ${userId} cancelled`));
285
+ } catch (e) {
286
+ error(`Undo friend request failed: ${e.message}`);
287
+ }
288
+ });
289
+
290
+ friend
291
+ .command("sent-requests")
292
+ .description("List all sent friend requests")
293
+ .action(async () => {
294
+ try {
295
+ const result = await getApi().getSentFriendRequest();
296
+ output(result, program.opts().json, () => {
297
+ const entries = Object.entries(result || {});
298
+ info(`${entries.length} sent request(s)`);
299
+ for (const [uid, req] of entries) {
300
+ console.log(` ${uid} ${req.displayName || req.zaloName || "?"}`);
301
+ }
302
+ });
303
+ } catch (e) {
304
+ // Code 112 = no friend requests
305
+ if (String(e.message).includes("112")) {
306
+ info("No sent friend requests");
307
+ } else {
308
+ error(`Get sent requests failed: ${e.message}`);
309
+ }
310
+ }
311
+ });
312
+
313
+ friend
314
+ .command("request-status <userId>")
315
+ .description("Check friend request status with a user")
316
+ .action(async (userId) => {
317
+ try {
318
+ const result = await getApi().getFriendRequestStatus(userId);
319
+ output(result, program.opts().json, () => {
320
+ info(`is_friend: ${result.is_friend}`);
321
+ info(`is_requested: ${result.is_requested}`);
322
+ info(`is_requesting: ${result.is_requesting}`);
323
+ });
324
+ } catch (e) {
325
+ error(`Get request status failed: ${e.message}`);
326
+ }
327
+ });
328
+
329
+ friend
330
+ .command("close")
331
+ .description("List close friends")
332
+ .action(async () => {
333
+ try {
334
+ const result = await getApi().getCloseFriends();
335
+ output(result, program.opts().json, () => {
336
+ const friends = Array.isArray(result) ? result : [];
337
+ info(`${friends.length} close friend(s)`);
338
+ for (const f of friends) {
339
+ console.log(` ${f.userId || f.uid || "?"} ${f.displayName || f.zaloName || "?"}`);
340
+ }
341
+ });
342
+ } catch (e) {
343
+ error(`Get close friends failed: ${e.message}`);
344
+ }
345
+ });
346
+
347
+ friend
348
+ .command("recommendations")
349
+ .description("Get friend recommendations and received requests")
350
+ .action(async () => {
351
+ try {
352
+ const result = await getApi().getFriendRecommendations();
353
+ output(result, program.opts().json, () => {
354
+ const items = result?.recommItems || [];
355
+ info(`${items.length} recommendation(s)`);
356
+ for (const item of items) {
357
+ const d = item.dataInfo || {};
358
+ const type = d.recommType === 2 ? "[request]" : "[suggest]";
359
+ console.log(` ${d.userId} ${d.displayName || d.zaloName || "?"} ${type}`);
360
+ }
361
+ });
362
+ } catch (e) {
363
+ error(`Get recommendations failed: ${e.message}`);
364
+ }
365
+ });
366
+
367
+ friend
368
+ .command("find-phones <phones...>")
369
+ .description("Find users by phone numbers")
370
+ .action(async (phones) => {
371
+ try {
372
+ const result = await getApi().getMultiUsersByPhones(phones);
373
+ output(result, program.opts().json, () => {
374
+ const entries = Object.entries(result || {});
375
+ info(`${entries.length} user(s) found`);
376
+ for (const [phone, user] of entries) {
377
+ console.log(` ${phone} ${user.uid || "?"} ${user.displayName || user.zaloName || "?"}`);
378
+ }
379
+ });
380
+ } catch (e) {
381
+ error(`Find by phone failed: ${e.message}`);
382
+ }
383
+ });
384
+ }