@ardennguyen/zalo-agent-cli 2.2.2 → 2.5.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.
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * MCP tool registrations for Zalo message access, sending, user lookup, and group productivity.
3
- * Registers 21 tools: zalo_get_messages, zalo_get_history, zalo_send_message, zalo_list_threads,
3
+ * Registers 23 tools: zalo_get_messages, zalo_get_history, zalo_send_message, zalo_list_threads,
4
4
  * zalo_search_threads, zalo_mark_read, zalo_view_media, zalo_react, zalo_undo, zalo_get_group_members,
5
5
  * zalo_list_conversations, zalo_coverage, zalo_sync, zalo_sync_status, zalo_search_messages,
6
- * zalo_lookup_user, zalo_forward_message, zalo_delete_message, zalo_reminder, zalo_note, zalo_poll.
6
+ * zalo_lookup_user, zalo_forward_message, zalo_delete_message, zalo_reminder, zalo_note, zalo_poll,
7
+ * zalo_wait_for_message, zalo_label.
7
8
  *
8
9
  * A tool that mirrors a CLI command calls the same code the command does (src/core/cached-message.js,
9
10
  * forward.js, pin.js, group-members.js, recent-conversations.js, src/utils/my-documents.js, urgency.js),
@@ -1490,9 +1491,11 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
1490
1491
  if (self) {
1491
1492
  const info = await api.fetchAccountInfo();
1492
1493
  const p = info?.profile || info || {};
1494
+ const ownId = ownIdOf(api) || p.userId || null;
1495
+ if (p.displayName || p.zaloName) syncJobs?.onSelfName?.(p.displayName || p.zaloName);
1493
1496
  return ok({
1494
1497
  mode: "self",
1495
- ownId: ownIdOf(api) || p.userId || null,
1498
+ ownId,
1496
1499
  displayName: p.displayName || p.zaloName || null,
1497
1500
  zaloName: p.zaloName || null,
1498
1501
  phoneNumber: p.phoneNumber || null,
@@ -2258,4 +2261,232 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
2258
2261
  }
2259
2262
  },
2260
2263
  );
2264
+
2265
+ // --- zalo_wait_for_message ---
2266
+ server.registerTool(
2267
+ "zalo_wait_for_message",
2268
+ {
2269
+ title: "Wait for Incoming Zalo Message",
2270
+ description:
2271
+ "Long-poll the live MCP message buffer for the next incoming watched message without manual polling. " +
2272
+ "Returns immediately if a buffered message newer than `since` is already waiting; otherwise waits up to " +
2273
+ "`timeoutSec` seconds (default 55, max 300) for a new message to arrive. Mirrors GET /events/wait for " +
2274
+ "remote HTTPS/OAuth2 MCP clients.",
2275
+ inputSchema: z.object({
2276
+ threadId: z
2277
+ .string()
2278
+ .min(1)
2279
+ .optional()
2280
+ .describe("Optional thread ID to wait on (omit to wait for a message in any watched thread)"),
2281
+ since: z
2282
+ .number()
2283
+ .int()
2284
+ .min(0)
2285
+ .optional()
2286
+ .describe(
2287
+ "Optional buffer cursor (`_cursor` from a prior message or `cursor` from a prior wait). " +
2288
+ "If a buffered message with `_cursor > since` already exists, returns immediately.",
2289
+ ),
2290
+ timeoutSec: z
2291
+ .number()
2292
+ .int()
2293
+ .min(1)
2294
+ .max(300)
2295
+ .default(55)
2296
+ .describe("Maximum seconds to wait before returning `status: 'timeout'` (default 55, max 300)"),
2297
+ }),
2298
+ },
2299
+ async ({ threadId, since, timeoutSec = 55 }) => {
2300
+ try {
2301
+ if (typeof buffer?.waitForMessage !== "function") {
2302
+ return err("Live message buffer does not support waitForMessage in this context.");
2303
+ }
2304
+ const cleanThreadId = typeof threadId === "string" && threadId.trim() ? threadId.trim() : null;
2305
+ const clampedSec = Math.max(1, Math.min(300, Number(timeoutSec) || 55));
2306
+ const msg = await buffer.waitForMessage({
2307
+ threadId: cleanThreadId,
2308
+ since: typeof since === "number" && Number.isFinite(since) ? since : undefined,
2309
+ timeoutMs: clampedSec * 1000,
2310
+ });
2311
+ if (msg) {
2312
+ const tid = String(msg.threadId || "");
2313
+ let threadName = nameCache?.get(tid)?.name || null;
2314
+ if (!threadName && tid) {
2315
+ try {
2316
+ threadName = getDisplayName(tid) || null;
2317
+ } catch {
2318
+ threadName = null;
2319
+ }
2320
+ }
2321
+ return ok({
2322
+ status: "message",
2323
+ message: {
2324
+ ...msg,
2325
+ ...(threadName ? { threadName } : {}),
2326
+ },
2327
+ cursor: msg._cursor,
2328
+ });
2329
+ }
2330
+ return ok({
2331
+ status: "timeout",
2332
+ message: null,
2333
+ cursor: Math.max(
2334
+ typeof since === "number" && Number.isFinite(since) ? since : 0,
2335
+ Number(buffer._globalCursor) || 0,
2336
+ typeof buffer.readCursor === "function" ? buffer.readCursor() : 0,
2337
+ ),
2338
+ });
2339
+ } catch (e) {
2340
+ console.error("[mcp-tools] zalo_wait_for_message error:", e.message);
2341
+ return err(e.message);
2342
+ }
2343
+ },
2344
+ );
2345
+
2346
+ // --- zalo_label ---
2347
+ server.registerTool(
2348
+ "zalo_label",
2349
+ {
2350
+ title: "Manage Zalo Conversation Labels",
2351
+ description:
2352
+ "List, assign, unassign, or update Zalo conversation/contact labels (tags such as 'Nội bộ', 'Khách hàng', 'Đối tác'). " +
2353
+ "Actions: 'list' (returns all labels with their IDs, text, colors, conversations array, and version), " +
2354
+ "'assign' (adds threadId or threadIds to a label identified by labelId or labelText), " +
2355
+ "'unassign' (removes threadId or threadIds from a label identified by labelId or labelText), " +
2356
+ "'update' (raw update with full labelData array and version, matching 'label update').",
2357
+ inputSchema: z.object({
2358
+ action: z
2359
+ .enum(["list", "assign", "unassign", "update"])
2360
+ .describe("Label operation to perform: 'list', 'assign', 'unassign', or 'update'"),
2361
+ labelId: z
2362
+ .number()
2363
+ .int()
2364
+ .optional()
2365
+ .describe("Numeric label ID (for 'assign' or 'unassign'; optional if labelText is provided)"),
2366
+ labelText: z
2367
+ .string()
2368
+ .min(1)
2369
+ .optional()
2370
+ .describe(
2371
+ "Label name/text, e.g. 'Nội bộ', 'Khách hàng', 'Đối tác' (case-insensitive match for 'assign' or 'unassign')",
2372
+ ),
2373
+ threadId: z.string().min(1).optional().describe("Single thread ID to assign or unassign"),
2374
+ threadIds: z
2375
+ .array(z.string().min(1))
2376
+ .optional()
2377
+ .describe("Multiple thread IDs to assign or unassign in one call"),
2378
+ labelData: z
2379
+ .array(z.any())
2380
+ .optional()
2381
+ .describe("Full labelData array from action='list' (required for raw action='update')"),
2382
+ version: z
2383
+ .number()
2384
+ .int()
2385
+ .optional()
2386
+ .describe("Label version integer from action='list' (required for raw action='update')"),
2387
+ }),
2388
+ },
2389
+ async ({ action, labelId, labelText, threadId, threadIds, labelData, version }) => {
2390
+ try {
2391
+ if (action === "list") {
2392
+ const res = await api.getLabels();
2393
+ return ok({
2394
+ action: "list",
2395
+ labelData: Array.isArray(res?.labelData) ? res.labelData : [],
2396
+ version: res?.version ?? 0,
2397
+ lastUpdateTime: res?.lastUpdateTime ?? null,
2398
+ });
2399
+ }
2400
+
2401
+ if (action === "update") {
2402
+ if (!Array.isArray(labelData) || typeof version !== "number" || !Number.isFinite(version)) {
2403
+ return err("labelData (array) and version (number) are required for action='update'.");
2404
+ }
2405
+ const updated = await api.updateLabels({
2406
+ labelData,
2407
+ version: Number(version),
2408
+ });
2409
+ return ok({
2410
+ action: "update",
2411
+ success: true,
2412
+ version: updated?.version ?? version,
2413
+ labelData: updated?.labelData ?? labelData,
2414
+ result: updated,
2415
+ });
2416
+ }
2417
+
2418
+ if (action === "assign" || action === "unassign") {
2419
+ const targets = [
2420
+ ...new Set(
2421
+ [
2422
+ ...(threadId ? [String(threadId).trim()] : []),
2423
+ ...(Array.isArray(threadIds) ? threadIds.map((t) => String(t).trim()) : []),
2424
+ ].filter(Boolean),
2425
+ ),
2426
+ ];
2427
+ if (targets.length === 0) {
2428
+ return err(`threadId (or threadIds) is required for action='${action}'.`);
2429
+ }
2430
+ if (labelId === undefined && (!labelText || !labelText.trim())) {
2431
+ return err(`labelId or labelText is required for action='${action}'.`);
2432
+ }
2433
+
2434
+ const current = await api.getLabels();
2435
+ const list = Array.isArray(current?.labelData) ? current.labelData : [];
2436
+ const needle = labelText ? labelText.trim().toLowerCase() : null;
2437
+ const matched =
2438
+ labelId !== undefined
2439
+ ? list.find((l) => Number(l.id) === Number(labelId))
2440
+ : list.find(
2441
+ (l) =>
2442
+ String(l.text ?? "")
2443
+ .trim()
2444
+ .toLowerCase() === needle ||
2445
+ String(l.textKey ?? "")
2446
+ .trim()
2447
+ .toLowerCase() === needle,
2448
+ );
2449
+ if (!matched) {
2450
+ const available = list.map((l) => `${l.id}:${l.text}`).join(", ") || "none";
2451
+ return err(
2452
+ `Label not found (${labelId !== undefined ? `labelId=${labelId}` : `labelText="${labelText}"`}). Available labels: ${available}`,
2453
+ );
2454
+ }
2455
+
2456
+ const targetSet = new Set(targets);
2457
+ let updatedConvList = [];
2458
+ const nextLabelData = list.map((lbl) => {
2459
+ if (Number(lbl.id) !== Number(matched.id)) return lbl;
2460
+ const existing = Array.isArray(lbl.conversations) ? lbl.conversations.map(String) : [];
2461
+ const nextConvs =
2462
+ action === "assign"
2463
+ ? [...new Set([...existing, ...targets])]
2464
+ : existing.filter((c) => !targetSet.has(String(c)));
2465
+ updatedConvList = nextConvs;
2466
+ return { ...lbl, conversations: nextConvs };
2467
+ });
2468
+ const nextVersion = Number(current?.version ?? 0);
2469
+ const updated = await api.updateLabels({
2470
+ labelData: nextLabelData,
2471
+ version: nextVersion,
2472
+ });
2473
+ return ok({
2474
+ action,
2475
+ success: true,
2476
+ labelId: matched.id,
2477
+ labelText: matched.text,
2478
+ threadIds: targets,
2479
+ conversations: updatedConvList,
2480
+ version: updated?.version ?? nextVersion,
2481
+ result: updated,
2482
+ });
2483
+ }
2484
+
2485
+ return err(`Unsupported label action: ${action}`);
2486
+ } catch (e) {
2487
+ console.error("[mcp-tools] zalo_label error:", e.message);
2488
+ return err(e.message);
2489
+ }
2490
+ },
2491
+ );
2261
2492
  }