whatsapp-pi 1.0.9 → 1.0.11

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
@@ -1,98 +1,98 @@
1
- <p align="center">
2
- <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/WhatsApp.svg" alt="WhatsApp Logo" width="100">
3
- </p>
4
-
5
- # WhatsApp-Pi
6
-
7
- A WhatsApp integration extension for the **[Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent)**.
8
-
9
- Pi is a powerful agentic AI coding assistant that operates in your terminal. This extension allows you to chat and pair-program with your Pi agent directly through WhatsApp, featuring message filtering, allow-listing, and reliable message delivery.
10
-
11
- ## Features
12
-
13
- - **Manual WhatsApp Connection**: QR code-based authentication with session persistence
14
- - **Allow List**: Control which numbers can interact with Pi
15
- - Add contacts with optional names for easy identification
16
- - View ignored numbers (not in allow list) and add them when needed
17
- - **Reliable Messaging**: Queue-based message sending with retry logic
18
- - **TUI Integration**: Menu-driven interface for managing connections and contacts
19
-
20
- ## Quick Start
21
-
22
- 1. Install the extension:
23
- ```bash
24
- pi install npm:whatsapp-pi
25
- ```
26
-
27
- 2. Start Pi (the extension will load automatically once installed):
28
- ```bash
29
- pi
30
- ```
31
-
32
- To automatically connect to WhatsApp on startup (if you are already authenticated):
33
- ```bash
34
- pi -w
35
- # or
36
- pi --whatsapp
37
- ```
38
-
39
- 3. Use the menu to connect WhatsApp and manage allowed/blocked numbers
40
-
41
- ## Development / Testing
42
-
43
- If you are developing or testing the extension locally:
44
-
45
- 1. Install dependencies:
46
- ```bash
47
- npm install
48
- ```
49
-
50
- 2. Run the extension:
51
- ```bash
52
- pi -e whatsapp-pi.ts
53
- ```
54
-
55
- For verbose mode (shows Baileys trace logs for debugging):
56
- ```bash
57
- pi -e whatsapp-pi.ts -v
58
- # or
59
- pi -e whatsapp-pi.ts --verbose
60
- ```
61
-
62
- ## Commands
63
-
64
- - `/whatsapp` - Open the WhatsApp management menu
65
- - **Allow Numbers**: Manage contacts that can interact with Pi
66
- - **Blocked Numbers**: View ignored numbers (not in allow list) and add them to allow list
67
-
68
- ## Project Structure
69
-
70
- ```
71
- src/
72
- ├── models/ # Type definitions
73
- ├── services/ # Core services (WhatsApp, Session, MessageSender)
74
- └── ui/ # Menu handlers
75
-
76
- specs/ # Feature specifications
77
- tests/ # Unit and integration tests
78
- ```
79
-
80
- ## Documentation
81
-
82
- See `specs/` directory for detailed feature documentation:
83
- - `001-whatsapp-tui-integration/` - TUI menu system
84
- - `002-manual-whatsapp-connection/` - Connection management
85
- - `003-whatsapp-messaging-refactor/` - Reliable messaging
86
- - `004-blocked-numbers-management/` - Block list feature
87
-
88
- ## Development
89
-
90
- Run tests:
91
- ```bash
92
- npm test
93
- ```
94
-
95
- Lint:
96
- ```bash
97
- npm run lint
98
- ```
1
+ <p align="center">
2
+ <img src="https://upload.wikimedia.org/wikipedia/commons/6/6b/WhatsApp.svg" alt="WhatsApp Logo" width="100">
3
+ </p>
4
+
5
+ # WhatsApp-Pi
6
+
7
+ A WhatsApp integration extension for the **[Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent)**.
8
+
9
+ Pi is a powerful agentic AI coding assistant that operates in your terminal. This extension allows you to chat and pair-program with your Pi agent directly through WhatsApp, featuring message filtering, allow-listing, and reliable message delivery.
10
+
11
+ ## Features
12
+
13
+ - **Manual WhatsApp Connection**: QR code-based authentication with session persistence
14
+ - **Allow List**: Control which numbers can interact with Pi
15
+ - Add contacts with optional names for easy identification
16
+ - View ignored numbers (not in allow list) and add them when needed
17
+ - **Reliable Messaging**: Queue-based message sending with retry logic
18
+ - **TUI Integration**: Menu-driven interface for managing connections and contacts
19
+
20
+ ## Quick Start
21
+
22
+ 1. Install the extension:
23
+ ```bash
24
+ pi install npm:whatsapp-pi
25
+ ```
26
+
27
+ 2. Start Pi (the extension will load automatically once installed):
28
+ ```bash
29
+ pi
30
+ ```
31
+
32
+ To automatically connect to WhatsApp on startup (if you are already authenticated):
33
+ ```bash
34
+ pi -w
35
+ # or
36
+ pi --whatsapp
37
+ ```
38
+
39
+ 3. Use the menu to connect WhatsApp and manage allowed/blocked numbers
40
+
41
+ ## Development / Testing
42
+
43
+ If you are developing or testing the extension locally:
44
+
45
+ 1. Install dependencies:
46
+ ```bash
47
+ npm install
48
+ ```
49
+
50
+ 2. Run the extension:
51
+ ```bash
52
+ pi -e whatsapp-pi.ts
53
+ ```
54
+
55
+ For verbose mode (shows Baileys trace logs for debugging):
56
+ ```bash
57
+ pi -e whatsapp-pi.ts -v
58
+ # or
59
+ pi -e whatsapp-pi.ts --verbose
60
+ ```
61
+
62
+ ## Commands
63
+
64
+ - `/whatsapp` - Open the WhatsApp management menu
65
+ - **Allow Numbers**: Manage contacts that can interact with Pi
66
+ - **Blocked Numbers**: View ignored numbers (not in allow list) and add them to allow list
67
+
68
+ ## Project Structure
69
+
70
+ ```
71
+ src/
72
+ ├── models/ # Type definitions
73
+ ├── services/ # Core services (WhatsApp, Session, MessageSender)
74
+ └── ui/ # Menu handlers
75
+
76
+ specs/ # Feature specifications
77
+ tests/ # Unit and integration tests
78
+ ```
79
+
80
+ ## Documentation
81
+
82
+ See `specs/` directory for detailed feature documentation:
83
+ - `001-whatsapp-tui-integration/` - TUI menu system
84
+ - `002-manual-whatsapp-connection/` - Connection management
85
+ - `003-whatsapp-messaging-refactor/` - Reliable messaging
86
+ - `004-blocked-numbers-management/` - Block list feature
87
+
88
+ ## Development
89
+
90
+ Run tests:
91
+ ```bash
92
+ npm test
93
+ ```
94
+
95
+ Lint:
96
+ ```bash
97
+ npm run lint
98
+ ```
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "whatsapp-pi",
3
- "version": "1.0.9",
4
- "type": "module",
5
- "description": "WhatsApp integration extension for Pi",
6
- "main": "whatsapp-pi.ts",
7
- "files": [
8
- "whatsapp-pi.ts",
9
- "src"
10
- ],
11
- "keywords": [
12
- "pi",
13
- "pi-package",
14
- "pi-extension",
15
- "whatsapp",
16
- "baileys",
17
- "agent"
18
- ],
19
- "author": "Rapha",
20
- "license": "MIT",
21
- "scripts": {
22
- "test": "vitest run",
23
- "typecheck": "tsc --noEmit"
24
- },
25
- "dependencies": {
26
- "@whiskeysockets/baileys": "^6.11.0",
27
- "pino": "^10.3.1",
28
- "qrcode-terminal": "^0.12.0"
29
- },
30
- "devDependencies": {
31
- "@mariozechner/pi-coding-agent": "latest",
32
- "@types/node": "^20.11.0",
33
- "@types/qrcode-terminal": "^0.12.2",
34
- "ts-node": "^10.9.2",
35
- "tsx": "^4.7.0",
36
- "typescript": "^5.3.0",
37
- "vitest": "^1.2.0"
38
- },
39
- "pi": {
40
- "extensions": [
41
- "./whatsapp-pi.ts"
42
- ],
43
- "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/WhatsApp.svg/512px-WhatsApp.svg.png"
44
- }
45
- }
1
+ {
2
+ "name": "whatsapp-pi",
3
+ "version": "1.0.11",
4
+ "type": "module",
5
+ "description": "WhatsApp integration extension for Pi",
6
+ "main": "whatsapp-pi.ts",
7
+ "files": [
8
+ "whatsapp-pi.ts",
9
+ "src"
10
+ ],
11
+ "keywords": [
12
+ "pi",
13
+ "pi-package",
14
+ "pi-extension",
15
+ "whatsapp",
16
+ "baileys",
17
+ "agent"
18
+ ],
19
+ "author": "Rapha",
20
+ "license": "MIT",
21
+ "scripts": {
22
+ "test": "vitest run",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@whiskeysockets/baileys": "^6.11.0",
27
+ "pino": "^10.3.1",
28
+ "qrcode-terminal": "^0.12.0"
29
+ },
30
+ "devDependencies": {
31
+ "@mariozechner/pi-coding-agent": "latest",
32
+ "@types/node": "^20.11.0",
33
+ "@types/qrcode-terminal": "^0.12.2",
34
+ "ts-node": "^10.9.2",
35
+ "tsx": "^4.7.0",
36
+ "typescript": "^5.3.0",
37
+ "vitest": "^1.2.0"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./whatsapp-pi.ts"
42
+ ],
43
+ "image": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/WhatsApp.svg/512px-WhatsApp.svg.png"
44
+ }
45
+ }
@@ -1,46 +1,46 @@
1
- export type SessionStatus = 'logged-out' | 'pairing' | 'connected' | 'disconnected';
2
-
3
- export interface WhatsAppSession {
4
- id: string;
5
- status: SessionStatus;
6
- credentialsPath: string;
7
- }
8
-
9
- export interface AllowList {
10
- numbers: string[];
11
- }
12
-
13
- export interface IncomingMessage {
14
- id: string;
15
- remoteJid: string;
16
- pushName?: string;
17
- text?: string;
18
- timestamp: number;
19
- }
20
-
21
- export interface MessageRequest {
22
- recipientJid: string;
23
- text: string;
24
- options?: {
25
- maxRetries?: number;
26
- priority?: 'high' | 'normal';
27
- };
28
- }
29
-
30
- export interface MessageResult {
31
- success: boolean;
32
- messageId?: string;
33
- error?: string;
34
- attempts: number;
35
- }
36
-
37
- export class WhatsAppError extends Error {
38
- constructor(public code: string, message: string) {
39
- super(message);
40
- this.name = 'WhatsAppError';
41
- }
42
- }
43
-
44
- export function validatePhoneNumber(number: string): boolean {
45
- return /^\+[1-9]\d{1,14}$/.test(number);
46
- }
1
+ export type SessionStatus = 'logged-out' | 'pairing' | 'connected' | 'disconnected';
2
+
3
+ export interface WhatsAppSession {
4
+ id: string;
5
+ status: SessionStatus;
6
+ credentialsPath: string;
7
+ }
8
+
9
+ export interface AllowList {
10
+ numbers: string[];
11
+ }
12
+
13
+ export interface IncomingMessage {
14
+ id: string;
15
+ remoteJid: string;
16
+ pushName?: string;
17
+ text?: string;
18
+ timestamp: number;
19
+ }
20
+
21
+ export interface MessageRequest {
22
+ recipientJid: string;
23
+ text: string;
24
+ options?: {
25
+ maxRetries?: number;
26
+ priority?: 'high' | 'normal';
27
+ };
28
+ }
29
+
30
+ export interface MessageResult {
31
+ success: boolean;
32
+ messageId?: string;
33
+ error?: string;
34
+ attempts: number;
35
+ }
36
+
37
+ export class WhatsAppError extends Error {
38
+ constructor(public code: string, message: string) {
39
+ super(message);
40
+ this.name = 'WhatsAppError';
41
+ }
42
+ }
43
+
44
+ export function validatePhoneNumber(number: string): boolean {
45
+ return /^\+[1-9]\d{1,14}$/.test(number);
46
+ }
@@ -1,53 +1,53 @@
1
- import { downloadContentFromMessage } from '@whiskeysockets/baileys';
2
- import { exec } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import { writeFile, mkdir } from 'node:fs/promises';
5
- import { join } from 'node:path';
6
- import { existsSync } from 'node:fs';
7
-
8
- const execAsync = promisify(exec);
9
-
10
- export class AudioService {
11
- private readonly mediaDir = '/home/opc/.pi/whatsapp-medias';
12
- private readonly whisperPath = '/home/opc/.local/bin/whisper';
13
-
14
- constructor() {
15
- if (!existsSync(this.mediaDir)) {
16
- mkdir(this.mediaDir, { recursive: true }).catch(() => {});
17
- }
18
- }
19
-
20
- async transcribe(audioMessage: any): Promise<string> {
21
- try {
22
- const filename = `audio_${Date.now()}`;
23
- const inputPath = join(this.mediaDir, `${filename}.ogg`);
24
-
25
- // Download audio content
26
- const stream = await downloadContentFromMessage(audioMessage, 'audio');
27
- let buffer = Buffer.from([]);
28
- for await (const chunk of stream) {
29
- buffer = Buffer.concat([buffer, chunk]);
30
- }
31
-
32
- await writeFile(inputPath, buffer);
33
-
34
- // Transcribe using Whisper
35
- // Using small model for better accuracy
36
- const command = `${this.whisperPath} "${inputPath}" --model small --language pt --output_format txt --output_dir "${this.mediaDir}" --fp16 False`;
37
-
38
- await execAsync(command);
39
-
40
- const txtPath = join(this.mediaDir, `${filename}.txt`);
41
- if (existsSync(txtPath)) {
42
- const fs = await import('node:fs/promises');
43
- const text = await fs.readFile(txtPath, 'utf8');
44
- return text.trim();
45
- }
46
-
47
- return '[Transcrição vazia]';
48
- } catch (error) {
49
- console.error('[AudioService] Transcription error:', error);
50
- return `[Erro na transcrição: ${error instanceof Error ? error.message : String(error)}]`;
51
- }
52
- }
53
- }
1
+ import { downloadContentFromMessage } from '@whiskeysockets/baileys';
2
+ import { exec } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { writeFile, mkdir } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { existsSync } from 'node:fs';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ export class AudioService {
11
+ private readonly mediaDir = '/home/opc/.pi/whatsapp-medias';
12
+ private readonly whisperPath = '/home/opc/.local/bin/whisper';
13
+
14
+ constructor() {
15
+ if (!existsSync(this.mediaDir)) {
16
+ mkdir(this.mediaDir, { recursive: true }).catch(() => {});
17
+ }
18
+ }
19
+
20
+ async transcribe(audioMessage: any): Promise<string> {
21
+ try {
22
+ const filename = `audio_${Date.now()}`;
23
+ const inputPath = join(this.mediaDir, `${filename}.ogg`);
24
+
25
+ // Download audio content
26
+ const stream = await downloadContentFromMessage(audioMessage, 'audio');
27
+ let buffer = Buffer.from([]);
28
+ for await (const chunk of stream) {
29
+ buffer = Buffer.concat([buffer, chunk]);
30
+ }
31
+
32
+ await writeFile(inputPath, buffer);
33
+
34
+ // Transcribe using Whisper
35
+ // Using small model for better accuracy
36
+ const command = `${this.whisperPath} "${inputPath}" --model small --language pt --output_format txt --output_dir "${this.mediaDir}" --fp16 False`;
37
+
38
+ await execAsync(command);
39
+
40
+ const txtPath = join(this.mediaDir, `${filename}.txt`);
41
+ if (existsSync(txtPath)) {
42
+ const fs = await import('node:fs/promises');
43
+ const text = await fs.readFile(txtPath, 'utf8');
44
+ return text.trim();
45
+ }
46
+
47
+ return '[Transcrição vazia]';
48
+ } catch (error) {
49
+ console.error('[AudioService] Transcription error:', error);
50
+ return `[Erro na transcrição: ${error instanceof Error ? error.message : String(error)}]`;
51
+ }
52
+ }
53
+ }
@@ -1,93 +1,93 @@
1
- import { WhatsAppService } from './whatsapp.service.js';
2
- import { MessageRequest, MessageResult, WhatsAppError } from '../models/whatsapp.types.js';
3
-
4
- export class MessageSender {
5
- private whatsappService: WhatsAppService;
6
-
7
- constructor(whatsappService: WhatsAppService) {
8
- this.whatsappService = whatsappService;
9
- }
10
-
11
- /**
12
- * Pauses execution for the specified time.
13
- * @param ms Milliseconds to sleep.
14
- */
15
- private async sleep(ms: number) {
16
- return new Promise(resolve => setTimeout(resolve, ms));
17
- }
18
-
19
- /**
20
- * Waits for the WhatsApp connection to be active.
21
- * @param timeoutMs Maximum time to wait in milliseconds.
22
- * @throws {WhatsAppError} If connection is not established within timeout.
23
- */
24
- private async waitIfOffline(timeoutMs: number = 30000): Promise<void> {
25
- const start = Date.now();
26
- while (this.whatsappService.getStatus() !== 'connected') {
27
- if (Date.now() - start > timeoutMs) {
28
- throw new WhatsAppError('TIMEOUT', 'Timed out waiting for WhatsApp connection');
29
- }
30
- await this.sleep(1000);
31
- }
32
- }
33
-
34
- /**
35
- * Sends a message with retry logic and connection awareness.
36
- * @param request The message recipient and content.
37
- * @returns Promise resolving to a result object indicating success or failure.
38
- */
39
- public async send(request: MessageRequest): Promise<MessageResult> {
40
- const maxRetries = request.options?.maxRetries ?? 3;
41
- let attempts = 0;
42
- let lastError: any = null;
43
-
44
- while (attempts < maxRetries) {
45
- attempts++;
46
- try {
47
- // 1. Ensure we are online
48
- await this.waitIfOffline();
49
-
50
- // 2. Get active socket
51
- const socket = this.whatsappService.getSocket();
52
- if (!socket) {
53
- throw new WhatsAppError('SOCKET_NOT_INIT', 'WhatsApp socket not initialized');
54
- }
55
-
56
- // 3. Send the message
57
- // Note: Branding π is applied here to ensure consistency
58
- const response = await socket.sendMessage(request.recipientJid, {
59
- text: `${request.text} π`
60
- });
61
-
62
- return {
63
- success: true,
64
- messageId: response.key.id,
65
- attempts
66
- };
67
- } catch (error: any) {
68
- lastError = error;
69
- console.error(`[MessageSender] Attempt ${attempts} failed for ${request.recipientJid}: ${error.message}`);
70
-
71
- // Specific handling for non-retryable errors
72
- if (error.code === 'TIMEOUT') {
73
- break;
74
- }
75
-
76
- // 4. Backoff before retry
77
- if (attempts < maxRetries) {
78
- const backoff = Math.pow(2, attempts) * 1000;
79
- if (this.whatsappService.isVerbose()) {
80
- console.log(`[MessageSender] Retrying in ${backoff}ms...`);
81
- }
82
- await this.sleep(backoff);
83
- }
84
- }
85
- }
86
-
87
- return {
88
- success: false,
89
- error: lastError?.message || 'Unknown error',
90
- attempts
91
- };
92
- }
93
- }
1
+ import { WhatsAppService } from './whatsapp.service.js';
2
+ import { MessageRequest, MessageResult, WhatsAppError } from '../models/whatsapp.types.js';
3
+
4
+ export class MessageSender {
5
+ private whatsappService: WhatsAppService;
6
+
7
+ constructor(whatsappService: WhatsAppService) {
8
+ this.whatsappService = whatsappService;
9
+ }
10
+
11
+ /**
12
+ * Pauses execution for the specified time.
13
+ * @param ms Milliseconds to sleep.
14
+ */
15
+ private async sleep(ms: number) {
16
+ return new Promise(resolve => setTimeout(resolve, ms));
17
+ }
18
+
19
+ /**
20
+ * Waits for the WhatsApp connection to be active.
21
+ * @param timeoutMs Maximum time to wait in milliseconds.
22
+ * @throws {WhatsAppError} If connection is not established within timeout.
23
+ */
24
+ private async waitIfOffline(timeoutMs: number = 30000): Promise<void> {
25
+ const start = Date.now();
26
+ while (this.whatsappService.getStatus() !== 'connected') {
27
+ if (Date.now() - start > timeoutMs) {
28
+ throw new WhatsAppError('TIMEOUT', 'Timed out waiting for WhatsApp connection');
29
+ }
30
+ await this.sleep(1000);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Sends a message with retry logic and connection awareness.
36
+ * @param request The message recipient and content.
37
+ * @returns Promise resolving to a result object indicating success or failure.
38
+ */
39
+ public async send(request: MessageRequest): Promise<MessageResult> {
40
+ const maxRetries = request.options?.maxRetries ?? 3;
41
+ let attempts = 0;
42
+ let lastError: any = null;
43
+
44
+ while (attempts < maxRetries) {
45
+ attempts++;
46
+ try {
47
+ // 1. Ensure we are online
48
+ await this.waitIfOffline();
49
+
50
+ // 2. Get active socket
51
+ const socket = this.whatsappService.getSocket();
52
+ if (!socket) {
53
+ throw new WhatsAppError('SOCKET_NOT_INIT', 'WhatsApp socket not initialized');
54
+ }
55
+
56
+ // 3. Send the message
57
+ // Note: Branding π is applied here to ensure consistency
58
+ const response = await socket.sendMessage(request.recipientJid, {
59
+ text: `${request.text} π`
60
+ });
61
+
62
+ return {
63
+ success: true,
64
+ messageId: response.key.id,
65
+ attempts
66
+ };
67
+ } catch (error: any) {
68
+ lastError = error;
69
+ console.error(`[MessageSender] Attempt ${attempts} failed for ${request.recipientJid}: ${error.message}`);
70
+
71
+ // Specific handling for non-retryable errors
72
+ if (error.code === 'TIMEOUT') {
73
+ break;
74
+ }
75
+
76
+ // 4. Backoff before retry
77
+ if (attempts < maxRetries) {
78
+ const backoff = Math.pow(2, attempts) * 1000;
79
+ if (this.whatsappService.isVerbose()) {
80
+ console.log(`[MessageSender] Retrying in ${backoff}ms...`);
81
+ }
82
+ await this.sleep(backoff);
83
+ }
84
+ }
85
+ }
86
+
87
+ return {
88
+ success: false,
89
+ error: lastError?.message || 'Unknown error',
90
+ attempts
91
+ };
92
+ }
93
+ }