whatsapp-pi 1.0.14 → 1.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whatsapp-pi",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "description": "WhatsApp integration extension for Pi",
6
6
  "main": "whatsapp-pi.ts",
@@ -21,6 +21,8 @@ export class SessionManager {
21
21
  private allowList: Contact[] = [];
22
22
  private blockList: Contact[] = [];
23
23
  private ignoredNumbers: Contact[] = [];
24
+ private openaiKey: string = '';
25
+ private visionModel: string = 'gpt-4o';
24
26
 
25
27
  public async ensureInitialized() {
26
28
  try {
@@ -54,6 +56,8 @@ export class SessionManager {
54
56
  this.blockList = (config.blockList || []).map(cleanContact).filter(Boolean) as Contact[];
55
57
  this.ignoredNumbers = (config.ignoredNumbers || []).map(cleanContact).filter(Boolean) as Contact[];
56
58
  this.status = config.status || 'logged-out';
59
+ this.openaiKey = config.openaiKey || '';
60
+ this.visionModel = config.visionModel || 'gpt-4o';
57
61
  } catch (error) {
58
62
  // File not found is fine
59
63
  }
@@ -65,7 +69,9 @@ export class SessionManager {
65
69
  allowList: this.allowList,
66
70
  blockList: this.blockList,
67
71
  ignoredNumbers: this.ignoredNumbers,
68
- status: this.status
72
+ status: this.status,
73
+ openaiKey: this.openaiKey,
74
+ visionModel: this.visionModel
69
75
  };
70
76
  await writeFile(this.configPath, JSON.stringify(config, null, 2));
71
77
  } catch (error) {
@@ -195,6 +201,24 @@ export class SessionManager {
195
201
  await this.saveConfig();
196
202
  }
197
203
 
204
+ getOpenaiKey(): string {
205
+ return this.openaiKey;
206
+ }
207
+
208
+ async setOpenaiKey(key: string) {
209
+ this.openaiKey = key;
210
+ await this.saveConfig();
211
+ }
212
+
213
+ getVisionModel(): string {
214
+ return this.visionModel;
215
+ }
216
+
217
+ async setVisionModel(model: string) {
218
+ this.visionModel = model;
219
+ await this.saveConfig();
220
+ }
221
+
198
222
  getAuthDir(): string {
199
223
  return this.authDir;
200
224
  }
@@ -31,6 +31,7 @@ export class MenuHandler {
31
31
 
32
32
  options.push('Allowed Numbers');
33
33
  options.push('Blocked Numbers');
34
+ options.push('Vision Settings');
34
35
  options.push('Back');
35
36
 
36
37
  const choice = await ctx.ui.select(`WhatsApp (Status: ${status})`, options);
@@ -66,6 +67,9 @@ export class MenuHandler {
66
67
  case 'Blocked Numbers':
67
68
  await this.manageBlockList(ctx);
68
69
  break;
70
+ case 'Vision Settings':
71
+ await this.manageVisionSettings(ctx);
72
+ break;
69
73
  }
70
74
  }
71
75
 
@@ -158,4 +162,33 @@ export class MenuHandler {
158
162
  await this.manageBlockList(ctx);
159
163
  }
160
164
  }
165
+
166
+ private async manageVisionSettings(ctx: ExtensionCommandContext) {
167
+ const key = this.sessionManager.getOpenaiKey();
168
+ const model = this.sessionManager.getVisionModel();
169
+
170
+ const options = [
171
+ `API Key: ${key ? '********' : '(Not Set)'}`,
172
+ `Vision Model: ${model}`,
173
+ 'Back'
174
+ ];
175
+
176
+ const choice = await ctx.ui.select('Vision Settings', options);
177
+
178
+ if (choice?.startsWith('API Key:')) {
179
+ const newKey = await ctx.ui.input('Enter OpenAI API Key (leave empty to clear):');
180
+ await this.sessionManager.setOpenaiKey(newKey || '');
181
+ ctx.ui.notify('OpenAI Key updated', 'info');
182
+ await this.manageVisionSettings(ctx);
183
+ } else if (choice?.startsWith('Vision Model:')) {
184
+ const newModel = await ctx.ui.input('Enter Vision Model (e.g., gpt-4o, gpt-4o-mini):', model);
185
+ if (newModel) {
186
+ await this.sessionManager.setVisionModel(newModel);
187
+ ctx.ui.notify('Vision model updated', 'info');
188
+ }
189
+ await this.manageVisionSettings(ctx);
190
+ } else {
191
+ await this.handleCommand(ctx);
192
+ }
193
+ }
161
194
  }
package/whatsapp-pi.ts CHANGED
@@ -13,8 +13,8 @@ export default function (pi: ExtensionAPI) {
13
13
  default: false
14
14
  });
15
15
 
16
- pi.registerFlag("whatsapp-pi-off", {
17
- description: "Disable WhatsApp-Pi on startup",
16
+ pi.registerFlag("whatsapp-pi-online", {
17
+ description: "Enable WhatsApp-Pi on startup",
18
18
  type: "boolean",
19
19
  default: false
20
20
  });
@@ -120,13 +120,38 @@ export default function (pi: ExtensionAPI) {
120
120
  }
121
121
 
122
122
  // Handle media types
123
+ let imageBuffer: Buffer | undefined;
124
+ let imageMimeType: string | undefined;
125
+
123
126
  if (msg.message.audioMessage) {
124
127
  console.log(`[WhatsApp-Pi] Transcribing audio from ${pushName}...`);
125
128
  const transcription = await audioService.transcribe(msg.message.audioMessage);
126
129
  text = `[Áudio Transcrito]: ${transcription}`;
130
+ } else if (msg.message.imageMessage) {
131
+ console.log(`[WhatsApp-Pi] Downloading image from ${pushName}...`);
132
+ try {
133
+ const { downloadContentFromMessage } = await import('@whiskeysockets/baileys');
134
+ const stream = await downloadContentFromMessage(msg.message.imageMessage, 'image');
135
+ let buffer = Buffer.from([]);
136
+ for await (const chunk of stream) {
137
+ buffer = Buffer.concat([buffer, chunk]);
138
+ }
139
+ imageBuffer = buffer;
140
+
141
+ // Normalize mime type for Cloud Code Assist / Gemini
142
+ let rawMime = msg.message.imageMessage.mimetype || 'image/jpeg';
143
+ imageMimeType = rawMime.toLowerCase().split(';')[0].trim();
144
+ if (imageMimeType === 'image/jpg') imageMimeType = 'image/jpeg';
145
+
146
+ console.log(`[WhatsApp-Pi] Image downloaded. MIME: ${imageMimeType} (original: ${rawMime}), Size: ${imageBuffer.length} bytes`);
147
+
148
+ text = msg.message.imageMessage.caption || "[Image]";
149
+ } catch (e) {
150
+ console.error(`[WhatsApp-Pi] Failed to download image:`, e);
151
+ text = "[Image (download failed)]";
152
+ }
127
153
  } else if (!text) {
128
- if (msg.message.imageMessage) text = "[Image]";
129
- else if (msg.message.videoMessage) text = "[Video]";
154
+ if (msg.message.videoMessage) text = "[Video]";
130
155
  else if (msg.message.stickerMessage) text = "[Sticker]";
131
156
  else if (msg.message.documentMessage) text = "[Document]";
132
157
  else if (msg.message.contactMessage || msg.message.contactsArrayMessage) text = "[Contact]";
@@ -137,6 +162,57 @@ export default function (pi: ExtensionAPI) {
137
162
  // Always log to console so it appears in the TUI log pane
138
163
  console.log(`[WhatsApp-Pi] ${pushName} (+${sender}): ${text}`);
139
164
 
165
+ // Handle image with dedicated vision model if configured
166
+ if (imageBuffer && imageMimeType && sessionManager.getOpenaiKey()) {
167
+ console.log(`[WhatsApp-Pi] Using dedicated vision model (${sessionManager.getVisionModel()}) for image analysis...`);
168
+ try {
169
+ const openAiKey = sessionManager.getOpenaiKey();
170
+ const visionModel = sessionManager.getVisionModel();
171
+
172
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ "Authorization": `Bearer ${openAiKey}`
177
+ },
178
+ body: JSON.stringify({
179
+ model: visionModel,
180
+ messages: [
181
+ {
182
+ role: "user",
183
+ content: [
184
+ { type: "text", text: "Descreva esta imagem em detalhes. Se houver texto, transcreva-o." },
185
+ {
186
+ type: "image_url",
187
+ image_url: {
188
+ url: `data:${imageMimeType};base64,${imageBuffer.toString('base64')}`
189
+ }
190
+ }
191
+ ]
192
+ }
193
+ ],
194
+ max_tokens: 500
195
+ })
196
+ });
197
+
198
+ if (response.ok) {
199
+ const data = await response.json() as any;
200
+ const description = data.choices[0].message.content;
201
+ text = `${text}\n\n[Análise da Imagem por ${visionModel}]:\n${description}`;
202
+ // Clear buffer/mime so we don't send the image again to Pi
203
+ imageBuffer = undefined;
204
+ imageMimeType = undefined;
205
+ } else {
206
+ const error = await response.text();
207
+ console.error(`[WhatsApp-Pi] OpenAI Vision API error:`, error);
208
+ text = `${text}\n\n[Erro na análise da imagem: API retornou ${response.status}]`;
209
+ }
210
+ } catch (e) {
211
+ console.error(`[WhatsApp-Pi] Failed to analyze image with OpenAI:`, e);
212
+ text = `${text}\n\n[Erro ao processar imagem com OpenAI]`;
213
+ }
214
+ }
215
+
140
216
  // Handle commands
141
217
  if (text.trim().toLowerCase().startsWith('/compact')) {
142
218
  console.log(`[WhatsApp-Pi] Session compact requested by ${pushName}.`);
@@ -158,7 +234,14 @@ export default function (pi: ExtensionAPI) {
158
234
  }
159
235
 
160
236
  // Use a standard delivery for ALL messages to ensure TUI consistency
161
- pi.sendUserMessage(`Mensagem de ${pushName} (+${sender}): ${text}`, { deliverAs: "followUp" });
237
+ if (imageBuffer && imageMimeType) {
238
+ pi.sendUserMessage([
239
+ { type: "text", text: `Mensagem de ${pushName} (+${sender}): ${text}` },
240
+ { type: "image", data: imageBuffer.toString('base64'), mimeType: imageMimeType }
241
+ ], { deliverAs: "followUp" });
242
+ } else {
243
+ pi.sendUserMessage(`Mensagem de ${pushName} (+${sender}): ${text}`, { deliverAs: "followUp" });
244
+ }
162
245
  });
163
246
 
164
247
  // Register commands