whatsapp-pi 1.0.21 → 1.0.23

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
@@ -18,14 +18,22 @@ Pi is a powerful agentic AI coding assistant that operates in your terminal. Thi
18
18
  - View ignored numbers (not in allow list) and add them when needed
19
19
  - **Reliable Messaging**: Queue-based message sending with retry logic
20
20
  - **TUI Integration**: Menu-driven interface for managing connections and contacts
21
+ - **Media Support**:
22
+ - **Vision Analysis**: Automatically forwards WhatsApp images to Pi for analysis.
23
+ - **Document Handling**: Downloads and stores documents (PDF, text) for agent access.
21
24
 
22
25
  ## Prerequisites
23
26
 
24
- To enable audio features, you need to install OpenAI Whisper:
27
+ To enable audio transcription features:
25
28
  ```bash
26
29
  python -m pip install -U openai-whisper
27
30
  ```
28
31
 
32
+ To enable PDF reading capabilities (required for the agent to process documents):
33
+ - **Linux**: `sudo apt-get install poppler-utils`
34
+ - **macOS**: `brew install poppler`
35
+ - **Windows**: Install `poppler` (e.g., via Scoop) and add to PATH.
36
+
29
37
  ## Quick Start
30
38
 
31
39
  1. Install the extension:
@@ -108,6 +116,11 @@ See `specs/` directory for detailed feature documentation:
108
116
  - `002-manual-whatsapp-connection/` - Connection management
109
117
  - `003-whatsapp-messaging-refactor/` - Reliable messaging
110
118
  - `004-blocked-numbers-management/` - Block list feature
119
+ - `005-verbose-mode-support/` - Logging and tracing
120
+ - `006-auto-connect-flag/` - Automatic connection support
121
+ - `007-image-recognition/` - Vision analysis integration
122
+ - `008-document-message-support/` - Document handling and storage
123
+ - `009-localize-system-messages/` - US English localization
111
124
 
112
125
  ## Development
113
126
 
@@ -116,7 +129,17 @@ Run tests:
116
129
  npm test
117
130
  ```
118
131
 
119
- Lint:
120
- ```bash
121
- npm run lint
122
- ```
132
+ ## Implementation Notes
133
+
134
+ ### Recent Feature Updates (2026-04)
135
+
136
+ - **Auto-Connect Support**: Use the `--whatsapp-pi-online` flag to automatically connect to WhatsApp on startup if you have a valid active session.
137
+ - **Vision Analysis**: Images sent via WhatsApp are automatically downloaded and forwarded to the Pi agent as base64, enabling vision-based interactions.
138
+ - **Document Message Support**:
139
+ - WhatsApp documents (PDFs, text files, etc.) are downloaded and saved to `./.pi-data/whatsapp/documents/`.
140
+ - The Pi agent receives a notification with the file path and metadata.
141
+ - **Full US-English Localization**: All user-facing menus, TUI notifications, console logs, and agent communication headers have been localized to `en-US` for a consistent experience.
142
+ - **Intelligent Message Filtering**:
143
+ - **Loop Prevention**: The bot automatically ignores any message ending with the `π` symbol, preventing infinite loops between instances.
144
+ - **Manual Interaction**: Users can now interact with the bot from their own WhatsApp account; the bot will process `fromMe` messages as long as they don't contain the bot's signature.
145
+ - **Storage Management**: All persistent data (auth state, documents, config) is centralized in the `.pi-data/` directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
@@ -44,3 +44,11 @@ export class WhatsAppError extends Error {
44
44
  export function validatePhoneNumber(number: string): boolean {
45
45
  return /^\+[1-9]\d{1,14}$/.test(number);
46
46
  }
47
+
48
+ export interface DocumentMetadata {
49
+ filename: string;
50
+ mimetype: string;
51
+ size: number;
52
+ savedPath: string;
53
+ timestamp: number;
54
+ }
@@ -45,10 +45,10 @@ export class AudioService {
45
45
  return text.trim();
46
46
  }
47
47
 
48
- return '[Transcrição vazia]';
48
+ return '[Empty transcription]';
49
49
  } catch (error) {
50
50
  console.error('[AudioService] Transcription error:', error);
51
- return `[Erro na transcrição: ${error instanceof Error ? error.message : String(error)}]`;
51
+ return `[Transcription error: ${error instanceof Error ? error.message : String(error)}]`;
52
52
  }
53
53
  }
54
54
  }
@@ -1,19 +1,17 @@
1
1
  import { useMultiFileAuthState } from '@whiskeysockets/baileys';
2
- import { join, dirname } from 'path';
3
- import { fileURLToPath } from 'url';
4
- import { rm, readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { readFile, writeFile, mkdir, rm } from 'fs/promises';
4
+ import { homedir } from 'os';
5
5
  import { SessionStatus } from '../models/whatsapp.types.js';
6
6
 
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
-
9
7
  export interface Contact {
10
8
  number: string;
11
9
  name?: string;
12
10
  }
13
11
 
14
12
  export class SessionManager {
15
- // Data is stored in a fixed folder inside the extension project
16
- private readonly baseDir = join(__dirname, '..', '..', '.pi-data');
13
+ // Data is stored in the user's home directory to persist across updates
14
+ private readonly baseDir = join(homedir(), '.pi', 'whatsapp-pi');
17
15
  private readonly authDir = join(this.baseDir, 'auth');
18
16
  private readonly configPath = join(this.baseDir, 'config.json');
19
17
 
@@ -41,7 +41,7 @@ export class WhatsAppService {
41
41
 
42
42
  async start() {
43
43
  if (this.isReconnecting) return;
44
- this.onStatusUpdate?.('| WhatsApp: Connecting...');
44
+ this.onStatusUpdate?.('WhatsApp: Connecting...');
45
45
 
46
46
  const { state, saveCreds } = await this.sessionManager.getAuthState();
47
47
  const { version } = await fetchLatestBaileysVersion();
@@ -76,7 +76,7 @@ export class WhatsAppService {
76
76
  if (qr) {
77
77
  this.sessionManager.setStatus('pairing');
78
78
  this.onQRCode?.(qr);
79
- this.onStatusUpdate?.('| WhatsApp: Pairing...');
79
+ this.onStatusUpdate?.('WhatsApp: Pairing...');
80
80
  }
81
81
 
82
82
  if (connection === 'close') {
@@ -96,26 +96,26 @@ export class WhatsAppService {
96
96
  console.error(`Session invalid or logged out [${statusCode}] - clearing session and forcing re-auth`);
97
97
  await this.sessionManager.clearSession();
98
98
  this.sessionManager.setStatus('logged-out');
99
- this.onStatusUpdate?.('| WhatsApp: Logged out');
99
+ this.onStatusUpdate?.('WhatsApp: Logged out');
100
100
  return;
101
101
  }
102
102
 
103
103
  if (statusCode === DisconnectReason.connectionReplaced) {
104
104
  console.error('Connection replaced - another instance connected');
105
- this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
105
+ this.onStatusUpdate?.('WhatsApp: Conflict (Another Instance)');
106
106
  return;
107
107
  }
108
108
 
109
109
  if (shouldReconnect && !this.isReconnecting) {
110
110
  this.isReconnecting = true;
111
- this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
111
+ this.onStatusUpdate?.('WhatsApp: Reconnecting...');
112
112
  setTimeout(() => {
113
113
  this.isReconnecting = false;
114
114
  this.start();
115
115
  }, 3000);
116
116
  } else if (!shouldReconnect) {
117
117
  this.sessionManager.setStatus('logged-out');
118
- this.onStatusUpdate?.('| WhatsApp: Disconnected');
118
+ this.onStatusUpdate?.('WhatsApp: Disconnected');
119
119
  }
120
120
  } else if (connection === 'open') {
121
121
  if (this.verboseMode) {
@@ -123,7 +123,7 @@ export class WhatsAppService {
123
123
  }
124
124
  this.isReconnecting = false;
125
125
  this.sessionManager.setStatus('connected');
126
- this.onStatusUpdate?.('| WhatsApp: Connected');
126
+ this.onStatusUpdate?.('WhatsApp: Connected');
127
127
  }
128
128
  });
129
129
 
@@ -245,6 +245,6 @@ export class WhatsAppService {
245
245
  this.isReconnecting = false;
246
246
  }
247
247
  await this.sessionManager.setStatus('disconnected');
248
- this.onStatusUpdate?.('| WhatsApp: Disconnected');
248
+ this.onStatusUpdate?.('WhatsApp: Disconnected');
249
249
  }
250
250
  }
@@ -64,7 +64,7 @@ export class MenuHandler {
64
64
 
65
65
  private async manageAllowList(ctx: ExtensionCommandContext) {
66
66
  const list = this.sessionManager.getAllowList();
67
- // Exibe o nome se existir, senão apenas o número
67
+ // Display the name if it exists, otherwise just the number
68
68
  let options = [...list.map(c => `Remove ${c.name ? c.name + ' (' + c.number + ')' : c.number}`), 'Add Number'];
69
69
  if (list.length > 0) {
70
70
  options.push('Clear All');
@@ -90,7 +90,7 @@ export class MenuHandler {
90
90
  }
91
91
  await this.manageAllowList(ctx);
92
92
  } else if (choice?.startsWith('Remove ')) {
93
- // Extrai o número entre parênteses ou o que sobrar depois de "Remove "
93
+ // Extract the number between parentheses or what's left after "Remove "
94
94
  let num = choice.replace('Remove ', '');
95
95
  if (num.includes('(')) {
96
96
  const match = num.match(/\((.*?)\)/);
package/whatsapp-pi.ts CHANGED
@@ -3,6 +3,8 @@ import { SessionManager } from './src/services/session.manager.js';
3
3
  import { WhatsAppService } from './src/services/whatsapp.service.js';
4
4
  import { MenuHandler } from './src/ui/menu.handler.js';
5
5
  import { AudioService } from './src/services/audio.service.js';
6
+ import { join } from 'node:path';
7
+ import { writeFile, mkdir } from 'node:fs/promises';
6
8
 
7
9
  console.log("[WhatsApp-Pi] Extension file loaded by Pi...");
8
10
  export default function (pi: ExtensionAPI) {
@@ -39,7 +41,7 @@ export default function (pi: ExtensionAPI) {
39
41
  if (isVerbose) {
40
42
  console.log('[WhatsApp-Pi] Verbose mode enabled - Baileys trace logs will be shown');
41
43
  }
42
- ctx.ui.setStatus('whatsapp', '| WhatsApp: Disconnected');
44
+ ctx.ui.setStatus('whatsapp', 'WhatsApp: Disconnected');
43
45
  whatsappService.setStatusCallback((status) => {
44
46
  ctx.ui.setStatus('whatsapp', status);
45
47
  });
@@ -67,7 +69,7 @@ export default function (pi: ExtensionAPI) {
67
69
  const shouldConnect = isWhatsappPiOn;
68
70
 
69
71
  if (shouldConnect) {
70
- ctx.ui.setStatus('whatsapp', '| WhatsApp: Auto-connecting...');
72
+ ctx.ui.setStatus('whatsapp', 'WhatsApp: Auto-connecting...');
71
73
 
72
74
  // Retry logic (max 3 attempts, 3s delay)
73
75
  let attempts = 0;
@@ -83,7 +85,7 @@ export default function (pi: ExtensionAPI) {
83
85
  setTimeout(tryConnect, 3000);
84
86
  } else {
85
87
  ctx.ui.notify('WhatsApp: Auto-connect failed after multiple attempts.', 'error');
86
- ctx.ui.setStatus('whatsapp', '| WhatsApp: Connection Failed');
88
+ ctx.ui.setStatus('whatsapp', 'WhatsApp: Connection Failed');
87
89
  }
88
90
  }
89
91
  };
@@ -98,6 +100,17 @@ export default function (pi: ExtensionAPI) {
98
100
  }
99
101
 
100
102
  ctx.ui.notify('WhatsApp: Session reset via /new is now fully supported.', 'info');
103
+
104
+ // Verify pdftotext availability for document support
105
+ try {
106
+ const { code } = await pi.exec('pdftotext', ['-v']);
107
+ if (code !== 0 && code !== 99) { // 99 is a common exit code for -v in some versions
108
+ throw new Error(`pdftotext returned code ${code}`);
109
+ }
110
+ } catch (e) {
111
+ ctx.ui.notify('WhatsApp: pdftotext not found. PDF document support will be limited to storage only.', 'warning');
112
+ console.warn('[WhatsApp-Pi] Warning: pdftotext not found in system PATH.');
113
+ }
101
114
  });
102
115
 
103
116
 
@@ -126,7 +139,7 @@ export default function (pi: ExtensionAPI) {
126
139
  if (msg.message.audioMessage) {
127
140
  console.log(`[WhatsApp-Pi] Transcribing audio from ${pushName}...`);
128
141
  const transcription = await audioService.transcribe(msg.message.audioMessage);
129
- text = `[Áudio Transcrito]: ${transcription}`;
142
+ text = `[Transcribed Audio]: ${transcription}`;
130
143
  } else if (msg.message.imageMessage) {
131
144
  console.log(`[WhatsApp-Pi] Downloading image from ${pushName}...`);
132
145
  try {
@@ -153,7 +166,53 @@ export default function (pi: ExtensionAPI) {
153
166
  } else if (!text) {
154
167
  if (msg.message.videoMessage) text = "[Video]";
155
168
  else if (msg.message.stickerMessage) text = "[Sticker]";
156
- else if (msg.message.documentMessage) text = "[Document]";
169
+ else if (msg.message.documentMessage)
170
+ {
171
+ const doc = msg.message.documentMessage;
172
+ const fileName = doc.fileName || 'unnamed_document';
173
+ const mimeType = doc.mimetype || 'application/octet-stream';
174
+ const fileSize = doc.fileLength ? Number(doc.fileLength) : 0;
175
+
176
+ console.log(`[WhatsApp-Pi] Downloading document from ${pushName}: ${fileName}...`);
177
+
178
+ try {
179
+ const { downloadContentFromMessage } = await import('@whiskeysockets/baileys');
180
+ const stream = await downloadContentFromMessage(doc, 'document');
181
+ let buffer = Buffer.from([]);
182
+ for await (const chunk of stream) {
183
+ buffer = Buffer.concat([buffer, chunk]);
184
+ }
185
+
186
+ // Sanitize filename
187
+ const sanitized = fileName.replace(/[^a-z0-9\._-]/gi, '_');
188
+ const savedFileName = `${Date.now()}_${sanitized}`;
189
+ const relativePath = `./.pi-data/whatsapp/documents/${savedFileName}`;
190
+ const absolutePath = join(process.cwd(), '.pi-data', 'whatsapp', 'documents', savedFileName);
191
+
192
+ // Ensure directory exists (T001 handles it at startup, but let's be safe)
193
+ await mkdir(join(process.cwd(), '.pi-data', 'whatsapp', 'documents'), { recursive: true });
194
+ await writeFile(absolutePath, buffer);
195
+
196
+ console.log(`[WhatsApp-Pi] Document saved to ${relativePath} (${buffer.length} bytes)`);
197
+
198
+ const sizeFormatted = fileSize > 1024 * 1024
199
+ ? `${(fileSize / (1024 * 1024)).toFixed(1)} MB`
200
+ : `${(fileSize / 1024).toFixed(1)} KB`;
201
+
202
+ text = `[Document Received: ${fileName}]\n` +
203
+ `MIME Type: ${mimeType}\n` +
204
+ `Size: ${sizeFormatted}\n` +
205
+ `Location: ${relativePath}`;
206
+
207
+ if (doc.caption) {
208
+ text += `\n\nDescription: ${doc.caption}`;
209
+ }
210
+ } catch (e) {
211
+ console.error(`[WhatsApp-Pi] Failed to download document:`, e);
212
+ text = `[Document: ${fileName} (download failed)]`;
213
+ }
214
+ }
215
+
157
216
  else if (msg.message.contactMessage || msg.message.contactsArrayMessage) text = "[Contact]";
158
217
  else if (msg.message.locationMessage) text = "[Location]";
159
218
  else text = "[Unsupported Message Type]";
@@ -165,11 +224,11 @@ export default function (pi: ExtensionAPI) {
165
224
  // Use a standard delivery for ALL messages to ensure TUI consistency
166
225
  if (imageBuffer && imageMimeType) {
167
226
  pi.sendUserMessage([
168
- { type: "text", text: `Mensagem de ${pushName} (+${sender}): ${text}` },
227
+ { type: "text", text: `Message from ${pushName} (+${sender}): ${text}` },
169
228
  { type: "image", data: imageBuffer.toString('base64'), mimeType: imageMimeType }
170
229
  ], { deliverAs: "followUp" });
171
230
  } else {
172
- pi.sendUserMessage(`Mensagem de ${pushName} (+${sender}): ${text}`, { deliverAs: "followUp" });
231
+ pi.sendUserMessage(`Message from ${pushName} (+${sender}): ${text}`, { deliverAs: "followUp" });
173
232
  }
174
233
 
175
234
  // Handle commands
@@ -178,7 +237,7 @@ export default function (pi: ExtensionAPI) {
178
237
 
179
238
  if (_ctx) {
180
239
  _ctx.compact();
181
- await whatsappService.sendMessage(remoteJid!, "Sessão compactada com sucesso! ✅");
240
+ await whatsappService.sendMessage(remoteJid!, "Session compacted successfully! ✅");
182
241
  }
183
242
  return;
184
243
  }
@@ -187,7 +246,7 @@ export default function (pi: ExtensionAPI) {
187
246
  console.log(`[WhatsApp-Pi] Abort requested by ${pushName}.`);
188
247
  if (_ctx) {
189
248
  _ctx.abort();
190
- await whatsappService.sendMessage(remoteJid!, "Abortado! ✅");
249
+ await whatsappService.sendMessage(remoteJid!, "Aborted! ✅");
191
250
  }
192
251
  return;
193
252
  }