wazap-mcp 0.9.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/auth-state.js +116 -0
- package/dist/banner.js +7 -0
- package/dist/cli.js +372 -0
- package/dist/config.js +138 -0
- package/dist/connect.js +222 -0
- package/dist/doctor.js +114 -0
- package/dist/errors.js +48 -0
- package/dist/ids.js +50 -0
- package/dist/index.js +88 -0
- package/dist/lock.js +42 -0
- package/dist/logger.js +16 -0
- package/dist/messages.js +281 -0
- package/dist/ratelimit.js +35 -0
- package/dist/server.js +133 -0
- package/dist/settings.js +74 -0
- package/dist/tools.js +591 -0
- package/dist/wa-types.js +2 -0
- package/dist/whatsapp.js +1514 -0
- package/package.json +60 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { asWazapError, ERROR_GUIDE } from "./errors.js";
|
|
3
|
+
function tool(def) {
|
|
4
|
+
return { ...def, handler: def.handler };
|
|
5
|
+
}
|
|
6
|
+
function ok(text, structured, extra = []) {
|
|
7
|
+
return { content: [{ type: "text", text }, ...extra], structuredContent: structured };
|
|
8
|
+
}
|
|
9
|
+
function synced(result, rest) {
|
|
10
|
+
return { ...rest, sync: result.sync };
|
|
11
|
+
}
|
|
12
|
+
export function toolError(err) {
|
|
13
|
+
const payload = { error: err.code, message: err.message };
|
|
14
|
+
if (err.fix)
|
|
15
|
+
payload.fix = err.fix;
|
|
16
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload, isError: true };
|
|
17
|
+
}
|
|
18
|
+
const READ_ONLY_HINTS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
19
|
+
const WRITE_HINTS = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
|
|
20
|
+
const chatId = z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('Chat id as returned by another tool ("<digits>@s.whatsapp.net" or "<id>@g.us"), or a phone number in international format');
|
|
24
|
+
const messageId = z
|
|
25
|
+
.string()
|
|
26
|
+
.min(5)
|
|
27
|
+
.describe('Message id from read_messages / search_messages / get_message, e.g. "false_4072...@s.whatsapp.net_3EB0..."');
|
|
28
|
+
const GUIDE = `# wazap — WhatsApp for your AI agent
|
|
29
|
+
|
|
30
|
+
Read/write access to the user's linked WhatsApp account: chats, messages, media,
|
|
31
|
+
contacts and groups. Call get_status first if anything looks wrong.
|
|
32
|
+
|
|
33
|
+
## Identifiers
|
|
34
|
+
- chat_id — individual: \`<digits>@s.whatsapp.net\`; group: \`<id>@g.us\`. A phone
|
|
35
|
+
number in international format (+40722123456 or 40722123456) also works. Pass
|
|
36
|
+
ids back exactly as a tool returned them.
|
|
37
|
+
- message_id — the full id from read_messages / search_messages. Needed for
|
|
38
|
+
get_message, download_media, react_to_message, edit_message, forward_message,
|
|
39
|
+
delete_message, and the reply_to of send_message.
|
|
40
|
+
|
|
41
|
+
## Workflows
|
|
42
|
+
- Catch up: get_recent_messages(hours) for everything, or list_chats(filter:"unread")
|
|
43
|
+
then read_messages(chat_id).
|
|
44
|
+
- Go back further: read_messages(chat_id, before: <oldest message_id you have>).
|
|
45
|
+
- Find a person: search_contacts → get_contact.
|
|
46
|
+
- Find something said: search_messages(query[, chat_id]).
|
|
47
|
+
- Send: send_message / send_media / send_poll / send_location. These are REAL
|
|
48
|
+
messages from the user's own account and there is no undo. Confirm the
|
|
49
|
+
recipient and the wording with the user before sending anything sensitive.
|
|
50
|
+
- Media: a message with has_media=true → download_media(message_id).
|
|
51
|
+
- Groups: get_group_info before manage_group; most actions need admin rights.
|
|
52
|
+
|
|
53
|
+
## Message shape
|
|
54
|
+
Every message has non-empty \`text\`: media and system messages carry a
|
|
55
|
+
placeholder like "[image] caption", "[voice message]", "[deleted]", "[poll] question".
|
|
56
|
+
\`timestamp\` is ISO 8601 with the machine's UTC offset, \`age\` is human-readable.
|
|
57
|
+
|
|
58
|
+
## Errors
|
|
59
|
+
Every failure returns \`{ error, message, fix }\`. What to do per code:
|
|
60
|
+
${Object.keys(ERROR_GUIDE).map((code) => `- **${code}** — ${ERROR_GUIDE[code]}`).join("\n")}
|
|
61
|
+
`;
|
|
62
|
+
const TOOLS = [
|
|
63
|
+
tool({
|
|
64
|
+
name: "learn",
|
|
65
|
+
title: "Learn how to use the WhatsApp tools",
|
|
66
|
+
description: `Read this FIRST, before any other WhatsApp tool. Returns the guide to the tools,
|
|
67
|
+
the id formats, the recommended workflows, the message shape and every error
|
|
68
|
+
code with what to do about it. Takes no arguments and never touches WhatsApp.`,
|
|
69
|
+
schema: {},
|
|
70
|
+
write: false,
|
|
71
|
+
handler: async () => ok(GUIDE, { guide: GUIDE }),
|
|
72
|
+
}),
|
|
73
|
+
tool({
|
|
74
|
+
name: "get_status",
|
|
75
|
+
title: "Get the WhatsApp connection status",
|
|
76
|
+
description: `Check the session: connection status ("connected" means the tools work,
|
|
77
|
+
"not_linked" means the user must run \`npx wazap-mcp login\`), whether the initial
|
|
78
|
+
history sync has finished, which account is linked, when a message last arrived,
|
|
79
|
+
and the versions and data directory in use.
|
|
80
|
+
|
|
81
|
+
Call this whenever another tool reports NOT_CONNECTED, NOT_LINKED or
|
|
82
|
+
SYNC_IN_PROGRESS, or to confirm which account you are about to send from.`,
|
|
83
|
+
schema: {},
|
|
84
|
+
write: false,
|
|
85
|
+
handler: async (_args, wa) => {
|
|
86
|
+
const s = wa.getStatus();
|
|
87
|
+
const account = s.account ? `${s.account.name || "(no name)"} (${s.account.number})` : "none";
|
|
88
|
+
const text = [
|
|
89
|
+
`# WhatsApp: ${s.status} (sync: ${s.sync})`,
|
|
90
|
+
`- **account**: ${account}`,
|
|
91
|
+
`- **last message received**: ${s.last_message_received_at ?? "never"}`,
|
|
92
|
+
`- **data dir**: ${s.data_dir} · **read-only**: ${s.read_only} · **rate limit**: ${s.rate_limit}/min`,
|
|
93
|
+
`- **versions**: wazap ${s.wazap_version}, baileys ${s.baileys_version}`,
|
|
94
|
+
s.last_error ? `- **last error**: ${s.last_error}` : null,
|
|
95
|
+
s.hint ? `- **hint**: ${s.hint}` : null,
|
|
96
|
+
]
|
|
97
|
+
.filter((line) => line !== null)
|
|
98
|
+
.join("\n");
|
|
99
|
+
return ok(text, s);
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
tool({
|
|
103
|
+
name: "list_chats",
|
|
104
|
+
title: "List WhatsApp chats",
|
|
105
|
+
description: `List conversations, most recently active first. Use it to discover the chat_id
|
|
106
|
+
values the other tools need.
|
|
107
|
+
|
|
108
|
+
Each chat has: chat_id, name, type, unread_count, last_message {text, timestamp,
|
|
109
|
+
from_me}, archived, pinned, muted_until, and left (groups you are no longer in).`,
|
|
110
|
+
schema: {
|
|
111
|
+
filter: z
|
|
112
|
+
.enum(["all", "unread", "groups", "individual", "archived"])
|
|
113
|
+
.default("all")
|
|
114
|
+
.describe('Which chats to list; "all" (default) excludes archived ones'),
|
|
115
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Maximum number of chats (1-100)"),
|
|
116
|
+
},
|
|
117
|
+
write: false,
|
|
118
|
+
handler: async ({ filter, limit }, wa) => {
|
|
119
|
+
const result = await wa.listChats(filter, limit);
|
|
120
|
+
return ok(renderChats(result.data, filter), synced(result, { filter, count: result.data.length, chats: result.data }));
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
tool({
|
|
124
|
+
name: "read_messages",
|
|
125
|
+
title: "Read messages from a WhatsApp chat",
|
|
126
|
+
description: `Read messages from one chat, oldest to newest.
|
|
127
|
+
|
|
128
|
+
Without \`before\` you get the most recent messages. Pass \`before\` (the oldest
|
|
129
|
+
message_id you already have) to page further back; wazap asks the phone for
|
|
130
|
+
older history when the local store runs out, which takes a few seconds.`,
|
|
131
|
+
schema: {
|
|
132
|
+
chat_id: chatId,
|
|
133
|
+
limit: z.number().int().min(1).max(200).default(20).describe("Maximum number of messages (1-200)"),
|
|
134
|
+
before: messageId.optional().describe("Return the messages immediately older than this message_id"),
|
|
135
|
+
},
|
|
136
|
+
write: false,
|
|
137
|
+
handler: async ({ chat_id, limit, before }, wa) => {
|
|
138
|
+
const result = await wa.readMessages(chat_id, limit, before);
|
|
139
|
+
return ok(renderMessages(`Messages in ${chat_id}`, result.data), synced(result, { chat_id, count: result.data.length, messages: result.data }));
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
tool({
|
|
143
|
+
name: "get_recent_messages",
|
|
144
|
+
title: "Get every WhatsApp conversation from the last N hours",
|
|
145
|
+
description: `Everything that happened recently, grouped by chat. This is the catch-up tool:
|
|
146
|
+
one call instead of list_chats plus a read_messages per chat.`,
|
|
147
|
+
schema: {
|
|
148
|
+
hours: z.number().int().min(1).max(168).default(24).describe("Look-back window in hours (1-168)"),
|
|
149
|
+
filter: z
|
|
150
|
+
.enum(["all", "unread", "groups", "individual"])
|
|
151
|
+
.default("all")
|
|
152
|
+
.describe("Restrict to unread chats, groups, or one-to-one chats"),
|
|
153
|
+
},
|
|
154
|
+
write: false,
|
|
155
|
+
handler: async ({ hours, filter }, wa) => {
|
|
156
|
+
const result = await wa.getRecentMessages(hours, filter);
|
|
157
|
+
const messageCount = result.data.reduce((n, c) => n + c.messages.length, 0);
|
|
158
|
+
return ok(renderConversations(result.data, hours), synced(result, {
|
|
159
|
+
hours,
|
|
160
|
+
filter,
|
|
161
|
+
conversation_count: result.data.length,
|
|
162
|
+
message_count: messageCount,
|
|
163
|
+
conversations: result.data,
|
|
164
|
+
}));
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
tool({
|
|
168
|
+
name: "search_messages",
|
|
169
|
+
title: "Search WhatsApp messages",
|
|
170
|
+
description: `Case-insensitive text search over the messages wazap holds locally — all chats,
|
|
171
|
+
or one chat. It cannot reach messages the phone never synced to this device.`,
|
|
172
|
+
schema: {
|
|
173
|
+
query: z.string().min(1).describe("Text to search for"),
|
|
174
|
+
chat_id: chatId.optional().describe("Restrict the search to this chat"),
|
|
175
|
+
limit: z.number().int().min(1).max(50).default(20).describe("Maximum number of results (1-50)"),
|
|
176
|
+
},
|
|
177
|
+
write: false,
|
|
178
|
+
handler: async ({ query, chat_id, limit }, wa) => {
|
|
179
|
+
const result = await wa.searchMessages(query, chat_id, limit);
|
|
180
|
+
return ok(renderMessages(`Search results for "${query}"`, result.data), synced(result, { query, chat_id: chat_id ?? null, count: result.data.length, messages: result.data }));
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
tool({
|
|
184
|
+
name: "get_message",
|
|
185
|
+
title: "Get one WhatsApp message in full",
|
|
186
|
+
description: `The complete message behind a message_id, including the quoted message it
|
|
187
|
+
replies to, its reactions, and its media metadata. Use it after search_messages
|
|
188
|
+
or read_messages when you need the context around a single message.`,
|
|
189
|
+
schema: { message_id: messageId },
|
|
190
|
+
write: false,
|
|
191
|
+
handler: async ({ message_id }, wa) => {
|
|
192
|
+
const message = await wa.getMessage(message_id);
|
|
193
|
+
return ok(renderMessages("Message", [message]), message);
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
tool({
|
|
197
|
+
name: "search_contacts",
|
|
198
|
+
title: "Search WhatsApp contacts",
|
|
199
|
+
description: `Find contacts by name or phone number (substring match on the name, digit match
|
|
200
|
+
on the number). Returns contact_id values usable as chat_id.`,
|
|
201
|
+
schema: {
|
|
202
|
+
query: z.string().min(2).describe("Name fragment or phone number (at least 2 characters)"),
|
|
203
|
+
limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of results (1-50)"),
|
|
204
|
+
},
|
|
205
|
+
write: false,
|
|
206
|
+
handler: async ({ query, limit }, wa) => {
|
|
207
|
+
const contacts = await wa.searchContacts(query, limit);
|
|
208
|
+
return ok(renderContacts(query, contacts), { query, count: contacts.length, contacts });
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
tool({
|
|
212
|
+
name: "get_contact",
|
|
213
|
+
title: "Get WhatsApp contact details",
|
|
214
|
+
description: `Full details for one contact: name, number, about text, profile picture URL,
|
|
215
|
+
whether they are a saved contact, a business, or blocked.`,
|
|
216
|
+
schema: {
|
|
217
|
+
contact_id: chatId.describe('Contact id from search_contacts / list_chats, or a phone number'),
|
|
218
|
+
},
|
|
219
|
+
write: false,
|
|
220
|
+
handler: async ({ contact_id }, wa) => {
|
|
221
|
+
const c = await wa.getContact(contact_id);
|
|
222
|
+
const text = [
|
|
223
|
+
`# ${c.name}`,
|
|
224
|
+
`- **contact_id**: \`${c.contact_id}\``,
|
|
225
|
+
c.number ? `- **number**: ${c.number}` : null,
|
|
226
|
+
c.about ? `- **about**: ${c.about}` : null,
|
|
227
|
+
c.profile_pic_url ? `- **profile picture**: ${c.profile_pic_url}` : null,
|
|
228
|
+
`- **saved**: ${c.is_my_contact} · **business**: ${c.is_business} · **blocked**: ${c.is_blocked}`,
|
|
229
|
+
]
|
|
230
|
+
.filter((line) => line !== null)
|
|
231
|
+
.join("\n");
|
|
232
|
+
return ok(text, c);
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
tool({
|
|
236
|
+
name: "get_group_info",
|
|
237
|
+
title: "Get WhatsApp group info",
|
|
238
|
+
description: `Details of a group: name, description, owner, creation date, whether only admins
|
|
239
|
+
may post, whether the linked account is an admin, and the participant list (up
|
|
240
|
+
to 500; participant_count is always the true total). The invite link is included
|
|
241
|
+
only when the linked account is an admin.
|
|
242
|
+
|
|
243
|
+
Call this before manage_group: most group actions need admin rights.`,
|
|
244
|
+
schema: { group_id: chatId.describe('Group chat id ("<id>@g.us")') },
|
|
245
|
+
write: false,
|
|
246
|
+
handler: async ({ group_id }, wa) => {
|
|
247
|
+
const info = await wa.getGroupInfo(group_id);
|
|
248
|
+
const text = [
|
|
249
|
+
`# ${info.name} (${info.participant_count} participants)`,
|
|
250
|
+
`- **chat_id**: \`${info.chat_id}\``,
|
|
251
|
+
info.description ? `- **description**: ${info.description}` : null,
|
|
252
|
+
info.owner ? `- **owner**: ${info.owner}` : null,
|
|
253
|
+
info.created_at ? `- **created**: ${info.created_at}` : null,
|
|
254
|
+
`- **admins only can post**: ${info.announcement_only} · **you are admin**: ${info.i_am_admin}`,
|
|
255
|
+
info.invite_link ? `- **invite link**: ${info.invite_link}` : null,
|
|
256
|
+
"",
|
|
257
|
+
"## Participants",
|
|
258
|
+
...info.participants.map((p) => `- ${p.name}${p.is_admin ? " (admin)" : ""} — \`${p.contact_id}\``),
|
|
259
|
+
]
|
|
260
|
+
.filter((line) => line !== null)
|
|
261
|
+
.join("\n");
|
|
262
|
+
return ok(text, info);
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
tool({
|
|
266
|
+
name: "download_media",
|
|
267
|
+
title: "Download media from a WhatsApp message",
|
|
268
|
+
description: `Download the photo/video/audio/document attached to a message and save it to
|
|
269
|
+
disk on the machine running wazap. Images of 1 MB or less are also returned
|
|
270
|
+
inline so you can look at them.
|
|
271
|
+
|
|
272
|
+
Fails with MEDIA_UNAVAILABLE when WhatsApp has expired the file.`,
|
|
273
|
+
schema: {
|
|
274
|
+
message_id: messageId.describe("A message with has_media=true"),
|
|
275
|
+
save_to: z.string().min(1).optional().describe("Absolute directory to save into (default: <data-dir>/media)"),
|
|
276
|
+
},
|
|
277
|
+
write: false,
|
|
278
|
+
handler: async ({ message_id, save_to }, wa) => {
|
|
279
|
+
const media = await wa.downloadMedia(message_id, save_to);
|
|
280
|
+
const { inline_base64, ...structured } = media;
|
|
281
|
+
const extra = inline_base64
|
|
282
|
+
? [{ type: "image", data: inline_base64, mimeType: media.mime }]
|
|
283
|
+
: [];
|
|
284
|
+
const text = `Saved ${media.mime} (${Math.round(media.size / 1024)} KB) to:\n${media.path}` +
|
|
285
|
+
(inline_base64 ? "\n(image attached inline)" : "");
|
|
286
|
+
return ok(text, structured, extra);
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
tool({
|
|
290
|
+
name: "send_message",
|
|
291
|
+
title: "Send a WhatsApp text message",
|
|
292
|
+
description: `Send a text message. This is a REAL message from the user's own account and
|
|
293
|
+
there is no undo — confirm the recipient and the wording with the user before
|
|
294
|
+
sending anything sensitive.`,
|
|
295
|
+
schema: {
|
|
296
|
+
chat_id: chatId,
|
|
297
|
+
text: z.string().min(1).max(65536).describe("The message text"),
|
|
298
|
+
reply_to: messageId.optional().describe("Quote-reply to this message"),
|
|
299
|
+
mention_ids: z
|
|
300
|
+
.array(z.string().min(1))
|
|
301
|
+
.max(50)
|
|
302
|
+
.optional()
|
|
303
|
+
.describe("Chat ids to @-mention; include their names in the text yourself"),
|
|
304
|
+
},
|
|
305
|
+
write: true,
|
|
306
|
+
handler: async ({ chat_id, text, reply_to, mention_ids }, wa) => {
|
|
307
|
+
const sent = await wa.sendMessage(chat_id, text, reply_to, mention_ids);
|
|
308
|
+
return ok(`Sent to ${sent.chat_id} at ${sent.timestamp} (message_id: ${sent.message_id}):\n> ${sent.text}`, sent);
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
tool({
|
|
312
|
+
name: "send_media",
|
|
313
|
+
title: "Send a WhatsApp media message",
|
|
314
|
+
description: `Send an image, video, audio file or document, from a local path on the machine
|
|
315
|
+
running wazap or from a public URL. Exactly one of file_path / url. Maximum
|
|
316
|
+
100 MB.`,
|
|
317
|
+
schema: {
|
|
318
|
+
chat_id: chatId,
|
|
319
|
+
file_path: z.string().min(1).optional().describe("Absolute path of a local file to send"),
|
|
320
|
+
url: z.string().url().optional().describe("Public http(s) URL to fetch and send"),
|
|
321
|
+
caption: z.string().max(1024).optional().describe("Text shown under the media"),
|
|
322
|
+
as_document: z.boolean().default(false).describe("Send as a plain document instead of rendered media"),
|
|
323
|
+
as_voice: z.boolean().default(false).describe("Send an audio file as a voice note (push-to-talk)"),
|
|
324
|
+
},
|
|
325
|
+
write: true,
|
|
326
|
+
handler: async ({ chat_id, file_path, url, caption, as_document, as_voice }, wa) => {
|
|
327
|
+
const sent = await wa.sendMedia(chat_id, { file_path, url }, { caption, asDocument: as_document, asVoice: as_voice });
|
|
328
|
+
return ok(`Media sent to ${sent.chat_id} at ${sent.timestamp} (message_id: ${sent.message_id})`, sent);
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
tool({
|
|
332
|
+
name: "send_poll",
|
|
333
|
+
title: "Send a WhatsApp poll",
|
|
334
|
+
description: `Send a poll to a chat. Participants vote in WhatsApp; wazap cannot read the
|
|
335
|
+
votes back, so ask the user to report the outcome.`,
|
|
336
|
+
schema: {
|
|
337
|
+
chat_id: chatId,
|
|
338
|
+
question: z.string().min(1).max(255).describe("The poll question"),
|
|
339
|
+
options: z.array(z.string().min(1).max(100)).min(2).max(12).describe("Answer options (2-12)"),
|
|
340
|
+
multi_select: z.boolean().default(false).describe("Allow voters to pick more than one option"),
|
|
341
|
+
},
|
|
342
|
+
write: true,
|
|
343
|
+
handler: async ({ chat_id, question, options, multi_select }, wa) => {
|
|
344
|
+
const sent = await wa.sendPoll(chat_id, question, options, multi_select);
|
|
345
|
+
return ok(`Poll sent to ${sent.chat_id} (message_id: ${sent.message_id}):\n> ${question}`, sent);
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
348
|
+
tool({
|
|
349
|
+
name: "send_location",
|
|
350
|
+
title: "Send a WhatsApp location",
|
|
351
|
+
description: "Send a map pin to a chat, optionally labelled with a place name and address.",
|
|
352
|
+
schema: {
|
|
353
|
+
chat_id: chatId,
|
|
354
|
+
latitude: z.number().min(-90).max(90).describe("Latitude in decimal degrees"),
|
|
355
|
+
longitude: z.number().min(-180).max(180).describe("Longitude in decimal degrees"),
|
|
356
|
+
name: z.string().max(255).optional().describe("Place name shown on the pin"),
|
|
357
|
+
address: z.string().max(500).optional().describe("Street address shown under the name"),
|
|
358
|
+
},
|
|
359
|
+
write: true,
|
|
360
|
+
handler: async ({ chat_id, latitude, longitude, name, address }, wa) => {
|
|
361
|
+
const sent = await wa.sendLocation(chat_id, latitude, longitude, name, address);
|
|
362
|
+
return ok(`Location sent to ${sent.chat_id} (message_id: ${sent.message_id})`, sent);
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
tool({
|
|
366
|
+
name: "edit_message",
|
|
367
|
+
title: "Edit a WhatsApp message you sent",
|
|
368
|
+
description: `Replace the text of a message the linked account sent. WhatsApp only allows
|
|
369
|
+
this within 15 minutes of sending; after that send a correction instead.`,
|
|
370
|
+
schema: {
|
|
371
|
+
message_id: messageId.describe("A message the linked account sent"),
|
|
372
|
+
text: z.string().min(1).max(65536).describe("The replacement text"),
|
|
373
|
+
},
|
|
374
|
+
write: true,
|
|
375
|
+
handler: async ({ message_id, text }, wa) => {
|
|
376
|
+
const sent = await wa.editMessage(message_id, text);
|
|
377
|
+
return ok(`Edited ${message_id}:\n> ${sent.text}`, sent);
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
tool({
|
|
381
|
+
name: "react_to_message",
|
|
382
|
+
title: "React to a WhatsApp message",
|
|
383
|
+
description: 'Add an emoji reaction to a message, or pass an empty string to remove your reaction.',
|
|
384
|
+
schema: {
|
|
385
|
+
message_id: messageId,
|
|
386
|
+
emoji: z.string().max(8).describe('A single emoji such as "👍", or "" to remove your reaction'),
|
|
387
|
+
},
|
|
388
|
+
write: true,
|
|
389
|
+
handler: async ({ message_id, emoji }, wa) => {
|
|
390
|
+
const result = await wa.reactToMessage(message_id, emoji);
|
|
391
|
+
const text = emoji ? `Reacted ${emoji} to ${message_id}` : `Removed the reaction from ${message_id}`;
|
|
392
|
+
return ok(text, result);
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
tool({
|
|
396
|
+
name: "forward_message",
|
|
397
|
+
title: "Forward a WhatsApp message",
|
|
398
|
+
description: "Forward an existing message to another chat. The recipient sees it marked as forwarded.",
|
|
399
|
+
schema: { message_id: messageId, to_chat_id: chatId.describe("Destination chat") },
|
|
400
|
+
write: true,
|
|
401
|
+
handler: async ({ message_id, to_chat_id }, wa) => {
|
|
402
|
+
const sent = await wa.forwardMessage(message_id, to_chat_id);
|
|
403
|
+
return ok(`Forwarded ${message_id} to ${sent.chat_id} (message_id: ${sent.message_id})`, sent);
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
tool({
|
|
407
|
+
name: "delete_message",
|
|
408
|
+
title: "Delete a WhatsApp message",
|
|
409
|
+
description: `Retract a message. DESTRUCTIVE and visible to everyone in the chat — confirm
|
|
410
|
+
with the user first. Only works on messages the linked account sent, and only
|
|
411
|
+
within 2 days of sending.`,
|
|
412
|
+
schema: {
|
|
413
|
+
message_id: messageId,
|
|
414
|
+
for_everyone: z.boolean().default(false).describe("Retract for all participants (WhatsApp supports no other kind of delete here)"),
|
|
415
|
+
},
|
|
416
|
+
write: true,
|
|
417
|
+
destructive: true,
|
|
418
|
+
handler: async ({ message_id, for_everyone }, wa) => {
|
|
419
|
+
const result = await wa.deleteMessage(message_id, for_everyone);
|
|
420
|
+
return ok(`Deleted ${message_id} for everyone`, result);
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
tool({
|
|
424
|
+
name: "manage_chat",
|
|
425
|
+
title: "Manage a WhatsApp chat",
|
|
426
|
+
description: `Change the state of a chat: archive/unarchive, pin/unpin, mute/unmute
|
|
427
|
+
(mute_hours defaults to 8), mark_read (sends read receipts) or mark_unread.`,
|
|
428
|
+
schema: {
|
|
429
|
+
chat_id: chatId,
|
|
430
|
+
action: z
|
|
431
|
+
.enum(["archive", "unarchive", "pin", "unpin", "mute", "unmute", "mark_read", "mark_unread"])
|
|
432
|
+
.describe("What to do with the chat"),
|
|
433
|
+
mute_hours: z.number().int().min(1).max(720).optional().describe('Hours to mute, default 8; only used by "mute"'),
|
|
434
|
+
},
|
|
435
|
+
write: true,
|
|
436
|
+
handler: async ({ chat_id, action, mute_hours }, wa) => {
|
|
437
|
+
const result = await wa.manageChat(chat_id, action, mute_hours);
|
|
438
|
+
return ok(result.applied, result);
|
|
439
|
+
},
|
|
440
|
+
}),
|
|
441
|
+
tool({
|
|
442
|
+
name: "create_group",
|
|
443
|
+
title: "Create a WhatsApp group",
|
|
444
|
+
description: `Create a group with the given name and participants; the linked account becomes
|
|
445
|
+
the owner. Each participant comes back with a status: ok, invite_needed (their
|
|
446
|
+
privacy settings require an invite link) or failed.`,
|
|
447
|
+
schema: {
|
|
448
|
+
name: z.string().min(1).max(100).describe("Group name"),
|
|
449
|
+
participant_ids: z.array(z.string().min(1)).min(1).max(256).describe("Chat ids or phone numbers to add (1-256)"),
|
|
450
|
+
},
|
|
451
|
+
write: true,
|
|
452
|
+
handler: async ({ name, participant_ids }, wa) => {
|
|
453
|
+
const result = await wa.createGroup(name, participant_ids);
|
|
454
|
+
const text = [`Group "${name}" created: ${result.chat_id}`, ...renderParticipants(result.participants)].join("\n");
|
|
455
|
+
return ok(text, { name, ...result });
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
tool({
|
|
459
|
+
name: "manage_group",
|
|
460
|
+
title: "Manage a WhatsApp group",
|
|
461
|
+
description: `Administer a group. Actions:
|
|
462
|
+
- add / remove / promote / demote — need participant_ids; each participant
|
|
463
|
+
comes back with status ok, invite_needed or failed
|
|
464
|
+
- leave — DESTRUCTIVE, rejoining needs an invite
|
|
465
|
+
- set_subject / set_description — need value
|
|
466
|
+
- get_invite_link / revoke_invite_link
|
|
467
|
+
|
|
468
|
+
Everything except leave requires the linked account to be a group admin; call
|
|
469
|
+
get_group_info first to check.`,
|
|
470
|
+
schema: {
|
|
471
|
+
group_id: chatId.describe('Group chat id ("<id>@g.us")'),
|
|
472
|
+
action: z
|
|
473
|
+
.enum([
|
|
474
|
+
"add",
|
|
475
|
+
"remove",
|
|
476
|
+
"promote",
|
|
477
|
+
"demote",
|
|
478
|
+
"leave",
|
|
479
|
+
"set_subject",
|
|
480
|
+
"set_description",
|
|
481
|
+
"get_invite_link",
|
|
482
|
+
"revoke_invite_link",
|
|
483
|
+
])
|
|
484
|
+
.describe("Group action to perform"),
|
|
485
|
+
participant_ids: z.array(z.string().min(1)).max(256).optional().describe("Targets of add/remove/promote/demote"),
|
|
486
|
+
value: z.string().max(2048).optional().describe("New subject or description"),
|
|
487
|
+
},
|
|
488
|
+
write: true,
|
|
489
|
+
destructive: true,
|
|
490
|
+
handler: async ({ group_id, action, participant_ids, value }, wa) => {
|
|
491
|
+
const result = await wa.manageGroup(group_id, action, participant_ids, value);
|
|
492
|
+
const text = [result.applied, ...renderParticipants(result.participants ?? [])].join("\n");
|
|
493
|
+
return ok(text, result);
|
|
494
|
+
},
|
|
495
|
+
}),
|
|
496
|
+
];
|
|
497
|
+
export const TOOL_NAMES = TOOLS.map((t) => t.name);
|
|
498
|
+
export function registerTools(server, wa, opts) {
|
|
499
|
+
for (const def of TOOLS) {
|
|
500
|
+
if (def.write && !opts.allowWrite)
|
|
501
|
+
continue;
|
|
502
|
+
server.registerTool(def.name, {
|
|
503
|
+
title: def.title,
|
|
504
|
+
description: def.description,
|
|
505
|
+
inputSchema: def.schema,
|
|
506
|
+
annotations: def.write
|
|
507
|
+
? { ...WRITE_HINTS, destructiveHint: def.destructive === true }
|
|
508
|
+
: { ...READ_ONLY_HINTS, openWorldHint: def.name !== "learn" },
|
|
509
|
+
}, async (args) => {
|
|
510
|
+
try {
|
|
511
|
+
if (def.write)
|
|
512
|
+
opts.limiter.take();
|
|
513
|
+
return await def.handler(args, wa);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
return toolError(asWazapError(err));
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function renderChats(chats, filter) {
|
|
522
|
+
if (chats.length === 0)
|
|
523
|
+
return `No chats found (filter: ${filter}).`;
|
|
524
|
+
const lines = [`# WhatsApp chats — ${filter} (${chats.length})`, ""];
|
|
525
|
+
for (const c of chats) {
|
|
526
|
+
const flags = [
|
|
527
|
+
c.type === "group" ? "group" : null,
|
|
528
|
+
c.unread_count > 0 ? `${c.unread_count} unread` : null,
|
|
529
|
+
c.pinned ? "pinned" : null,
|
|
530
|
+
c.muted_until ? "muted" : null,
|
|
531
|
+
c.archived ? "archived" : null,
|
|
532
|
+
c.left ? "left" : null,
|
|
533
|
+
].filter(Boolean);
|
|
534
|
+
lines.push(`## ${c.name}${flags.length ? ` [${flags.join(", ")}]` : ""}`);
|
|
535
|
+
lines.push(`- **chat_id**: \`${c.chat_id}\``);
|
|
536
|
+
if (c.last_message) {
|
|
537
|
+
lines.push(`- **last**: ${c.last_message.from_me ? "me: " : ""}${truncate(c.last_message.text, 160)} (${c.last_message.timestamp})`);
|
|
538
|
+
}
|
|
539
|
+
lines.push("");
|
|
540
|
+
}
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
function renderMessages(title, messages) {
|
|
544
|
+
if (messages.length === 0)
|
|
545
|
+
return `${title}: no messages found.`;
|
|
546
|
+
const lines = [`# ${title} (${messages.length})`, ""];
|
|
547
|
+
for (const m of messages) {
|
|
548
|
+
const tags = [
|
|
549
|
+
m.type !== "text" ? m.type : null,
|
|
550
|
+
m.forwarded ? "forwarded" : null,
|
|
551
|
+
m.edited ? "edited" : null,
|
|
552
|
+
m.quoted ? "reply" : null,
|
|
553
|
+
m.reactions?.length ? m.reactions.map((r) => r.emoji).join("") : null,
|
|
554
|
+
].filter(Boolean);
|
|
555
|
+
lines.push(`- **${m.from_me ? "me" : m.sender.name}** · ${m.age}${tags.length ? ` [${tags.join(", ")}]` : ""} · id: \`${m.message_id}\``);
|
|
556
|
+
if (m.quoted)
|
|
557
|
+
lines.push(` > ${truncate(m.quoted.text, 160)}`);
|
|
558
|
+
lines.push(` ${truncate(m.text, 500)}`);
|
|
559
|
+
}
|
|
560
|
+
return lines.join("\n");
|
|
561
|
+
}
|
|
562
|
+
function renderConversations(conversations, hours) {
|
|
563
|
+
if (conversations.length === 0)
|
|
564
|
+
return `No WhatsApp conversations in the last ${hours}h.`;
|
|
565
|
+
const total = conversations.reduce((n, c) => n + c.messages.length, 0);
|
|
566
|
+
const lines = [`# WhatsApp · last ${hours}h (${conversations.length} chats, ${total} messages)`, ""];
|
|
567
|
+
for (const c of conversations) {
|
|
568
|
+
lines.push(`## ${c.chat_name}${c.type === "group" ? " [group]" : ""} — \`${c.chat_id}\``);
|
|
569
|
+
for (const m of c.messages) {
|
|
570
|
+
lines.push(`- [${m.timestamp}] ${m.from_me ? "me" : m.sender.name}: ${truncate(m.text, 500)}`);
|
|
571
|
+
}
|
|
572
|
+
lines.push("");
|
|
573
|
+
}
|
|
574
|
+
return lines.join("\n");
|
|
575
|
+
}
|
|
576
|
+
function renderContacts(query, contacts) {
|
|
577
|
+
if (contacts.length === 0)
|
|
578
|
+
return `No contacts matching "${query}".`;
|
|
579
|
+
const lines = [`# Contacts matching "${query}" (${contacts.length})`, ""];
|
|
580
|
+
for (const c of contacts) {
|
|
581
|
+
const flags = [c.is_my_contact ? "saved" : null, c.is_business ? "business" : null].filter(Boolean);
|
|
582
|
+
lines.push(`- **${c.name}**${flags.length ? ` [${flags.join(", ")}]` : ""} — \`${c.contact_id}\`${c.number ? ` (${c.number})` : ""}`);
|
|
583
|
+
}
|
|
584
|
+
return lines.join("\n");
|
|
585
|
+
}
|
|
586
|
+
function renderParticipants(participants) {
|
|
587
|
+
return participants.map((p) => `- ${p.id}: ${p.status}${p.reason ? ` (${p.reason})` : ""}`);
|
|
588
|
+
}
|
|
589
|
+
function truncate(text, max) {
|
|
590
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
591
|
+
}
|
package/dist/wa-types.js
ADDED