whatsapp-pi 1.0.63 → 1.0.65

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 CHANGED
@@ -126,6 +126,20 @@ pi -e whatsapp-pi.ts --whatsapp-pi-online
126
126
  - **Send Message** and `send_wa_message` are outbound only.
127
127
  - If you message yourself, WhatsApp may show sent/read ticks, but that does not guarantee Pi will treat it as a trigger.
128
128
 
129
+ ## LLM-Callable Tools
130
+
131
+ The extension registers the following tools that the Pi agent can call:
132
+
133
+ | Tool | Direction | Description |
134
+ | --- | --- | --- |
135
+ | `send_wa_message` | outbound | Send a WhatsApp message to a contact or group (or reply to the last conversation if `jid` is omitted). |
136
+ | `send_reaction` | outbound | React to a WhatsApp message with an emoji. |
137
+ | `list_wa_conversations` | read-only | List recent conversations from the local recents store. Supports `onlyIncoming`, `onlyAllowed`, and `limit`. |
138
+ | `get_wa_conversation_history` | read-only | Get the most recent messages with a given `senderNumber` (accepts `+E164`, raw digits, or a JID). Supports `limit`. |
139
+ | `check_wa_new_messages` | read-only | List conversations whose most recent message is incoming (i.e. waiting for a reply). Supports `sinceTimestamp` (ms epoch). |
140
+
141
+ The three read-only tools query the local recents store at `~/.pi/whatsapp-pi/recents/recents.json`. They never touch the network and do not mark messages as read.
142
+
129
143
  ## WhatsApp Numbers and JIDs
130
144
 
131
145
  - Contacts use phone format in UI: `+5511999999999`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
package/src/i18n.ts CHANGED
@@ -193,7 +193,15 @@ const fallback = {
193
193
  "incoming.media.reactionRemoved": "Removed reaction",
194
194
  "tool.sendReaction.label": "Send WhatsApp Reaction",
195
195
  "tool.sendReaction.description": "React to a WhatsApp message with an emoji",
196
- "tool.sendReaction.error.invalidEmoji": "Invalid emoji provided",
196
+ "tool.sendReaction.error.invalidEmoji": "Invalid emoji",
197
+ "tool.listConversations.label": "List WhatsApp Conversations",
198
+ "tool.listConversations.description": "List recent WhatsApp conversations from the local recents store (sender number, optional name, last message preview, direction, timestamp, allowed flag). Read-only.",
199
+ "tool.getHistory.label": "Get WhatsApp Conversation History",
200
+ "tool.getHistory.description": "Get the most recent messages exchanged with a given WhatsApp sender from the local recents store. Accepts a phone number (+E164 or raw digits) or a JID. Read-only.",
201
+ "tool.checkNew.label": "Check WhatsApp New Messages",
202
+ "tool.checkNew.description": "List recent WhatsApp conversations whose most recent message is incoming (i.e. waiting for a reply). Optionally filter by a `sinceTimestamp` (ms epoch). Read-only.",
203
+ "tool.error.notInitialized": "WhatsApp-Pi recents store not initialized",
204
+ "tool.error.missingSender": "senderNumber is required",
197
205
  "incoming.media.audioTranscribing": "[WhatsApp-Pi] Transcribing audio from {pushName}...",
198
206
  "incoming.media.audioTranscribed": "[Transcribed Audio]: {transcription}",
199
207
  "incoming.media.imageDownloading": "[WhatsApp-Pi] Downloading image from {pushName}...",
package/whatsapp-pi.ts CHANGED
@@ -401,6 +401,112 @@ export default function (pi: ExtensionAPI) {
401
401
  }
402
402
  });
403
403
 
404
+ // Register list_wa_conversations tool (LLM-callable, read-only)
405
+ pi.registerTool({
406
+ name: "list_wa_conversations",
407
+ label: t("tool.listConversations.label"),
408
+ description: t("tool.listConversations.description"),
409
+ promptSnippet: "list_wa_conversations({onlyIncoming?, onlyAllowed?, limit?}) - List recent WhatsApp conversations from the local recents store. Read-only; safe to call any time.",
410
+ parameters: Type.Object({
411
+ onlyIncoming: Type.Optional(Type.Boolean({ description: "Only return conversations whose last message is incoming (waiting for a reply)." })),
412
+ onlyAllowed: Type.Optional(Type.Boolean({ description: "Only return conversations from senders/groups currently in the allow list." })),
413
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Maximum number of conversations to return (default 20)." }))
414
+ }),
415
+ async execute(_toolCallId, params) {
416
+ try {
417
+ const conversations = await recentsService.getRecentConversations();
418
+ let filtered = conversations;
419
+ if (params.onlyIncoming) {
420
+ filtered = filtered.filter(c => c.lastMessageDirection === 'incoming');
421
+ }
422
+ if (params.onlyAllowed) {
423
+ filtered = filtered.filter(c => c.isAllowed);
424
+ }
425
+ const limit = typeof params.limit === 'number' ? params.limit : 20;
426
+ filtered = filtered.slice(0, limit);
427
+ return {
428
+ isError: false,
429
+ details: undefined,
430
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, count: filtered.length, conversations: filtered }) }]
431
+ };
432
+ } catch (error) {
433
+ return {
434
+ isError: true,
435
+ details: undefined,
436
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }) }]
437
+ };
438
+ }
439
+ }
440
+ });
441
+
442
+ // Register get_wa_conversation_history tool (LLM-callable, read-only)
443
+ pi.registerTool({
444
+ name: "get_wa_conversation_history",
445
+ label: t("tool.getHistory.label"),
446
+ description: t("tool.getHistory.description"),
447
+ promptSnippet: "get_wa_conversation_history({senderNumber, limit?}) - Get the most recent messages with a sender. `senderNumber` accepts +E164 (e.g. +14155551212), raw digits, or a JID (e.g. 14155551212@s.whatsapp.net, 120363012345@g.us). Read-only.",
448
+ parameters: Type.Object({
449
+ senderNumber: Type.String({ description: "Phone number (+E164 or raw digits) or WhatsApp JID of the conversation." }),
450
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 20, description: "Maximum number of messages to return (default 20)." }))
451
+ }),
452
+ async execute(_toolCallId, params) {
453
+ if (!params.senderNumber || !params.senderNumber.trim()) {
454
+ return {
455
+ isError: true,
456
+ details: undefined,
457
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: t("tool.error.missingSender") }) }]
458
+ };
459
+ }
460
+ try {
461
+ const messages = await recentsService.getConversationHistory(params.senderNumber);
462
+ const limit = typeof params.limit === 'number' ? params.limit : 20;
463
+ const sliced = messages.slice(-limit);
464
+ return {
465
+ isError: false,
466
+ details: undefined,
467
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, count: sliced.length, messages: sliced }) }]
468
+ };
469
+ } catch (error) {
470
+ return {
471
+ isError: true,
472
+ details: undefined,
473
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }) }]
474
+ };
475
+ }
476
+ }
477
+ });
478
+
479
+ // Register check_wa_new_messages tool (LLM-callable, read-only)
480
+ pi.registerTool({
481
+ name: "check_wa_new_messages",
482
+ label: t("tool.checkNew.label"),
483
+ description: t("tool.checkNew.description"),
484
+ promptSnippet: "check_wa_new_messages({sinceTimestamp?}) - List conversations whose most recent message is incoming (i.e. waiting for a reply). Optional `sinceTimestamp` (ms epoch) filters to messages newer than that. Read-only.",
485
+ parameters: Type.Object({
486
+ sinceTimestamp: Type.Optional(Type.Integer({ minimum: 0, description: "Only include conversations whose last incoming message timestamp is strictly greater than this (ms since epoch)." }))
487
+ }),
488
+ async execute(_toolCallId, params) {
489
+ try {
490
+ const conversations = await recentsService.getRecentConversations();
491
+ const since = typeof params.sinceTimestamp === 'number' ? params.sinceTimestamp : 0;
492
+ const pending = conversations.filter(c =>
493
+ c.lastMessageDirection === 'incoming' && c.lastMessageTime > since
494
+ );
495
+ return {
496
+ isError: false,
497
+ details: undefined,
498
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, count: pending.length, conversations: pending }) }]
499
+ };
500
+ } catch (error) {
501
+ return {
502
+ isError: true,
503
+ details: undefined,
504
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }) }]
505
+ };
506
+ }
507
+ }
508
+ });
509
+
404
510
  // Suppress automatic message_end reply when tool already sent
405
511
  // This is checked by the message_end handler below
406
512