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
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
@@ -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 '[
|
|
48
|
+
return '[Empty transcription]';
|
|
49
49
|
} catch (error) {
|
|
50
50
|
console.error('[AudioService] Transcription error:', error);
|
|
51
|
-
return `[
|
|
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
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
16
|
-
private readonly baseDir = join(
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
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?.('
|
|
248
|
+
this.onStatusUpdate?.('WhatsApp: Disconnected');
|
|
249
249
|
}
|
|
250
250
|
}
|
package/src/ui/menu.handler.ts
CHANGED
|
@@ -64,7 +64,7 @@ export class MenuHandler {
|
|
|
64
64
|
|
|
65
65
|
private async manageAllowList(ctx: ExtensionCommandContext) {
|
|
66
66
|
const list = this.sessionManager.getAllowList();
|
|
67
|
-
//
|
|
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
|
-
//
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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 = `[
|
|
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)
|
|
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: `
|
|
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(`
|
|
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!, "
|
|
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!, "
|
|
249
|
+
await whatsappService.sendMessage(remoteJid!, "Aborted! ✅");
|
|
191
250
|
}
|
|
192
251
|
return;
|
|
193
252
|
}
|