bloby-bot 0.24.4 → 0.25.1

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": "bloby-bot",
3
- "version": "0.24.4",
3
+ "version": "0.25.1",
4
4
  "releaseNotes": [
5
5
  "1. new stuff",
6
6
  "2. ",
package/shared/config.ts CHANGED
@@ -3,8 +3,8 @@ import { paths, DATA_DIR } from './paths.js';
3
3
 
4
4
  export interface ChannelConfig {
5
5
  enabled: boolean;
6
- /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer mode */
7
- mode: 'channel' | 'business';
6
+ /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer mode, 'assistant' = personal assistant in conversations */
7
+ mode: 'channel' | 'business' | 'assistant';
8
8
  /** Phone numbers with admin access (owner, secretary, etc.) — business mode only */
9
9
  admins?: string[];
10
10
  /** Active skill for customer-facing mode (folder name in workspace/skills/) */
@@ -13,6 +13,9 @@
13
13
  * All other messages are ignored — it's the user's personal WhatsApp.
14
14
  * - business: Bloby has its own number. Numbers in the admins array get the main
15
15
  * system prompt. Everyone else gets the customer support prompt.
16
+ * - assistant: Personal assistant in conversations. Self-chat = admin channel.
17
+ * Others' messages stored for context. Only responds when the owner
18
+ * triggers with @botname in someone else's chat.
16
19
  */
17
20
 
18
21
  import fs from 'fs';
@@ -240,6 +243,11 @@ export class ChannelManager {
240
243
 
241
244
  // ── Internal ──
242
245
 
246
+ /** Format a bot reply with the agent's name prefix (for admin & assistant messages, NOT customer) */
247
+ private formatBotReply(text: string, botName: string): string {
248
+ return `🤖 *${botName}:*\n\n${text}`;
249
+ }
250
+
243
251
  private handleStatusChange(status: ChannelStatus) {
244
252
  for (const listener of this.statusListeners) {
245
253
  listener(status);
@@ -275,6 +283,21 @@ export class ChannelManager {
275
283
  // ── Business mode: filter outgoing (except self-chat) ──
276
284
  if (mode === 'business' && fromMe && !isSelfChat) return;
277
285
 
286
+ // ── Assistant mode ──
287
+ // Self-chat: falls through to debounce (processed as admin)
288
+ // Others' messages or my untriggered messages: store for context, don't invoke
289
+ // My messages with @botname trigger: falls through to debounce → agent
290
+ if (mode === 'assistant' && !(fromMe && isSelfChat)) {
291
+ // Store every message for context (both mine and theirs)
292
+ this.storeAssistantContext(channel, sender, senderName, text, fromMe);
293
+
294
+ // Only continue if it's me AND the message contains the trigger
295
+ const botName = loadConfig().username || 'bloby';
296
+ const triggerPattern = new RegExp(`(?:^|\\n)\\s*@${botName}[:\\s]`, 'i');
297
+ if (!fromMe || !triggerPattern.test(text)) return;
298
+ // Falls through to debounce → flushDebounce → handleAssistantMessage
299
+ }
300
+
278
301
  // Debounce: accumulate rapid messages from the same sender
279
302
  const debounceKey = `${channel}:${sender}`;
280
303
  const existing = this.debounceBuffers.get(debounceKey);
@@ -319,8 +342,8 @@ export class ChannelManager {
319
342
  const mode = channelConfig.mode || 'channel';
320
343
 
321
344
  // Route based on mode and role
322
- if (mode === 'channel' || (mode === 'business' && fromMe && isSelfChat)) {
323
- // Admin (self-chat in either mode)
345
+ if (mode === 'channel' || (mode === 'business' && fromMe && isSelfChat) || (mode === 'assistant' && fromMe && isSelfChat)) {
346
+ // Admin (self-chat in any mode)
324
347
  const message: InboundMessage = {
325
348
  channel,
326
349
  sender: sender.replace(/@.*/, ''),
@@ -331,12 +354,30 @@ export class ChannelManager {
331
354
  attachments: attachments.length > 0 ? attachments : undefined,
332
355
  };
333
356
 
334
- const modeLabel = mode === 'channel' ? 'Channel mode | self-chat' : 'Business mode | self-chat | admin';
357
+ const modeLabel = mode === 'channel' ? 'Channel mode | self-chat'
358
+ : mode === 'assistant' ? 'Assistant mode | self-chat | admin'
359
+ : 'Business mode | self-chat | admin';
335
360
  log.info(`[channels] ${modeLabel} | "${combinedText.slice(0, 60)}"`);
336
361
  await this.handleAdminMessage(message);
337
362
  return;
338
363
  }
339
364
 
365
+ // Assistant mode — triggered message in someone else's chat
366
+ if (mode === 'assistant') {
367
+ const message: InboundMessage = {
368
+ channel,
369
+ sender: sender.replace(/@.*/, ''),
370
+ senderName,
371
+ role: 'assistant',
372
+ text: combinedText,
373
+ rawSender: sender,
374
+ attachments: attachments.length > 0 ? attachments : undefined,
375
+ };
376
+ log.info(`[channels] Assistant mode | triggered in chat with ${message.sender} | "${combinedText.slice(0, 60)}"`);
377
+ await this.handleAssistantMessage(message, channelConfig);
378
+ return;
379
+ }
380
+
340
381
  // Business mode — incoming message
341
382
  const role = this.resolveBusinessRole(channelConfig, sender);
342
383
 
@@ -462,7 +503,7 @@ export class ChannelManager {
462
503
 
463
504
  // Agent paused to use a tool — send accumulated text as an intermediate WhatsApp message
464
505
  if (type === 'bot:tool' && waChunkBuf.trim()) {
465
- this.sendMessage(msg.channel, msg.rawSender, waChunkBuf.trim()).catch((err) => {
506
+ this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(waChunkBuf.trim(), botName)).catch((err) => {
466
507
  log.warn(`[channels] Failed to send WhatsApp chunk: ${err.message}`);
467
508
  });
468
509
  waChunkBuf = '';
@@ -472,7 +513,7 @@ export class ChannelManager {
472
513
  // Send remaining text
473
514
  const remaining = waChunkBuf.trim();
474
515
  if (remaining) {
475
- this.sendMessage(msg.channel, msg.rawSender, remaining).catch((err) => {
516
+ this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(remaining, botName)).catch((err) => {
476
517
  log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
477
518
  });
478
519
  waChunkBuf = '';
@@ -637,6 +678,171 @@ export class ChannelManager {
637
678
  );
638
679
  }
639
680
 
681
+ /** Store a message in the assistant context buffer (for conversation history when triggered) */
682
+ private storeAssistantContext(
683
+ channel: ChannelType,
684
+ sender: string,
685
+ senderName: string | undefined,
686
+ text: string,
687
+ fromMe: boolean,
688
+ ) {
689
+ // Normalize key to phone number (strip @lid / @s.whatsapp.net) — must match handleAssistantMessage's agentKey
690
+ const phone = sender.replace(/@.*/, '');
691
+ const bufferKey = `${channel}:${phone}`;
692
+ let buffer = this.customerBuffers.get(bufferKey);
693
+ if (!buffer) {
694
+ buffer = [];
695
+ this.customerBuffers.set(bufferKey, buffer);
696
+ }
697
+ const label = fromMe ? 'me' : (senderName || phone);
698
+ buffer.push({ role: 'user', content: `[${label}]: ${text}` });
699
+ if (buffer.length > MAX_BUFFER_MESSAGES) {
700
+ buffer.splice(0, buffer.length - MAX_BUFFER_MESSAGES);
701
+ }
702
+ log.info(`[channels] Assistant context stored: ${bufferKey} | ${buffer.length} msgs | [${label}]: "${text.slice(0, 60)}"`);
703
+ }
704
+
705
+ /** Handle a triggered assistant message — runs one-shot agent with conversation context */
706
+ private async handleAssistantMessage(msg: InboundMessage, channelConfig: ChannelConfig) {
707
+ const agentKey = `${msg.channel}:${msg.sender}`;
708
+
709
+ // Check concurrent limit
710
+ if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
711
+ log.info(`[channels] Max concurrent agents reached — queuing assistant message for ${msg.sender}`);
712
+ this.messageQueue.push(msg);
713
+ return;
714
+ }
715
+
716
+ const { workerApi, getModel } = this.opts;
717
+ const model = getModel();
718
+
719
+ // Extract command after trigger: "Ok.\n\n@bloby: do X" → "do X"
720
+ const config = loadConfig();
721
+ const botName = config.username || 'bloby';
722
+ const triggerRegex = new RegExp(`@${botName}[:\\s]+`, 'i');
723
+ const triggerMatch = msg.text.match(triggerRegex);
724
+ let cleanText = msg.text;
725
+ if (triggerMatch && triggerMatch.index !== undefined) {
726
+ cleanText = msg.text.slice(triggerMatch.index + triggerMatch[0].length).trim();
727
+ }
728
+
729
+ // Load SCRIPT.md from configured skill
730
+ const scriptPrompt = this.loadActiveScript(channelConfig);
731
+
732
+ // Fetch agent name
733
+ let agentBotName = 'Bloby', humanName = 'Human';
734
+ try {
735
+ const status = await workerApi('/api/onboard/status');
736
+ agentBotName = status.agentName || 'Bloby';
737
+ humanName = status.userName || 'Human';
738
+ } catch {}
739
+
740
+ // Get conversation buffer (already populated by storeAssistantContext)
741
+ let buffer = this.customerBuffers.get(agentKey);
742
+ if (!buffer) {
743
+ buffer = [];
744
+ this.customerBuffers.set(agentKey, buffer);
745
+ }
746
+
747
+ log.info(`[channels] Assistant trigger: agentKey=${agentKey} | buffer=${buffer.length} msgs | cleanText="${cleanText.slice(0, 80)}"`);
748
+ if (buffer.length > 0) {
749
+ log.info(`[channels] Assistant context preview: ${buffer.slice(-5).map((m) => `[${m.role}] ${m.content.slice(0, 50)}`).join(' | ')}`);
750
+ }
751
+
752
+ // All buffered messages are context for the agent
753
+ const recentMessages: RecentMessage[] = buffer.map((m) => ({
754
+ role: m.role,
755
+ content: m.content,
756
+ }));
757
+
758
+ // Load per-contact memory from the skill's customer_data directory
759
+ let contactMemory = '';
760
+ try {
761
+ const customerDataDir = this.getSkillCustomerDataDir(channelConfig);
762
+ if (customerDataDir) {
763
+ const memoryPath = path.join(WORKSPACE_DIR, customerDataDir, `${msg.sender}.md`);
764
+ if (fs.existsSync(memoryPath)) {
765
+ contactMemory = fs.readFileSync(memoryPath, 'utf-8').trim();
766
+ }
767
+ }
768
+ } catch {}
769
+
770
+ // Build enriched script with contact memory
771
+ let enrichedScript = scriptPrompt;
772
+ if (contactMemory && enrichedScript) {
773
+ enrichedScript += `\n\n---\n# Contact History (${msg.sender})\n\n${contactMemory}`;
774
+ }
775
+
776
+ const channelContext = `[WhatsApp | ${msg.sender} | assistant${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
777
+
778
+ // Convert inbound attachments to agent format
779
+ const agentAttachments: AgentAttachment[] | undefined = msg.attachments?.map((att) => ({
780
+ type: 'image' as const,
781
+ name: `whatsapp_image.${att.mediaType.split('/')[1] || 'jpg'}`,
782
+ mediaType: att.mediaType,
783
+ data: att.data,
784
+ }));
785
+
786
+ // Stable convId per contact
787
+ const convId = `channel-${agentKey}`;
788
+
789
+ this.activeAgents.set(agentKey, { sender: msg.sender, channel: msg.channel });
790
+
791
+ // Show "typing..." while the agent processes
792
+ this.startTyping(msg.channel, msg.rawSender);
793
+
794
+ // Track text chunks for WhatsApp
795
+ let waChunkBuf = '';
796
+
797
+ startBlobyAgentQuery(
798
+ convId,
799
+ channelContext + cleanText,
800
+ model,
801
+ (type, eventData) => {
802
+ // Accumulate text tokens
803
+ if (type === 'bot:token' && eventData.token) {
804
+ waChunkBuf += eventData.token;
805
+ }
806
+
807
+ // Agent paused to use a tool — send accumulated text as intermediate message
808
+ if (type === 'bot:tool' && waChunkBuf.trim()) {
809
+ this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(waChunkBuf.trim(), agentBotName)).catch((err) => {
810
+ log.warn(`[channels] Failed to send assistant chunk: ${err.message}`);
811
+ });
812
+ waChunkBuf = '';
813
+ }
814
+
815
+ if (type === 'bot:response' && eventData.content) {
816
+ // Add response to buffer for continuity across triggers
817
+ buffer!.push({ role: 'assistant', content: eventData.content });
818
+ if (buffer!.length > MAX_BUFFER_MESSAGES) {
819
+ buffer!.splice(0, buffer!.length - MAX_BUFFER_MESSAGES);
820
+ }
821
+
822
+ // Send remaining text
823
+ const remaining = waChunkBuf.trim();
824
+ if (remaining) {
825
+ this.sendMessage(msg.channel, msg.rawSender, this.formatBotReply(remaining, agentBotName)).catch((err) => {
826
+ log.warn(`[channels] Failed to send assistant reply: ${err.message}`);
827
+ });
828
+ waChunkBuf = '';
829
+ }
830
+ }
831
+
832
+ if (type === 'bot:done') {
833
+ this.activeAgents.delete(agentKey);
834
+ if (eventData.usedFileTools) this.opts.restartBackend();
835
+ this.processQueue();
836
+ }
837
+ },
838
+ agentAttachments,
839
+ undefined,
840
+ { botName: agentBotName, humanName },
841
+ recentMessages,
842
+ enrichedScript,
843
+ );
844
+ }
845
+
640
846
  /** Transcribe audio via the existing whisper endpoint */
641
847
  private async transcribeAudio(audioBase64: string): Promise<string | null> {
642
848
  try {
@@ -3,12 +3,12 @@
3
3
  */
4
4
 
5
5
  export type ChannelType = 'whatsapp' | 'telegram';
6
- export type SenderRole = 'admin' | 'customer';
6
+ export type SenderRole = 'admin' | 'customer' | 'assistant';
7
7
 
8
8
  export interface ChannelConfig {
9
9
  enabled: boolean;
10
- /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer mode */
11
- mode: 'channel' | 'business';
10
+ /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer mode, 'assistant' = personal assistant in conversations */
11
+ mode: 'channel' | 'business' | 'assistant';
12
12
  /** Phone numbers with admin access (owner, secretary, etc.) — business mode only */
13
13
  admins?: string[];
14
14
  /** Active skill for customer-facing mode (folder name in workspace/skills/) */
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "whatsapp",
3
+ "version": "2.0.0",
4
+ "description": "WhatsApp channel via Baileys. QR auth, messaging, voice transcription, channel and business modes.",
5
+ "skills": "./"
6
+ }
@@ -0,0 +1,227 @@
1
+ # WhatsApp
2
+
3
+ ## What This Is
4
+
5
+ Gives your agent a WhatsApp number. Connect via QR code, send and receive messages, handle voice notes, and switch between personal (channel) and business modes. Built on Baileys — no Meta Business API needed.
6
+
7
+ ## Dependencies
8
+
9
+ None.
10
+
11
+ ---
12
+
13
+ ## How Responses Work
14
+
15
+ **Your text response IS the WhatsApp reply.** When you receive a message tagged with `[WhatsApp | ...]`, the supervisor takes whatever you respond with and sends it directly to WhatsApp. You do NOT need to use curl or `/api/channels/send` to reply — just respond normally.
16
+
17
+ **Do NOT use `/api/channels/send` to reply to incoming WhatsApp messages.** That endpoint is ONLY for proactive messages (during pulse, cron, or when you want to initiate a conversation). If you use it to reply, the person will get duplicate messages.
18
+
19
+ **Adjust your style for WhatsApp:** Keep messages shorter and more conversational than chat. No markdown headers, no code blocks unless asked. Think texting, not email.
20
+
21
+ ---
22
+
23
+ ## How Messages Arrive
24
+
25
+ When a message arrives via WhatsApp, the supervisor wraps it with context:
26
+
27
+ ```
28
+ [WhatsApp | 5511999888777 | customer | Alice]
29
+ Hi, I'd like to schedule an appointment.
30
+ ```
31
+
32
+ The format is: `[WhatsApp | phone | role | name (optional)]`
33
+
34
+ - **role=admin**: This is your human or an authorized admin. Use your normal personality, full capabilities, main system prompt.
35
+ - **role=customer**: This is someone else messaging. Follow the instructions from the active skill's SCRIPT.md (loaded as your system prompt for that conversation).
36
+ - **role=assistant**: Your human triggered you with `@botname:` inside a conversation with this person. You have the full conversation history as context. Execute the task and respond concisely — your reply goes directly into that chat. The SCRIPT.md from the active skill is loaded as your system prompt.
37
+
38
+ ---
39
+
40
+ ## Channel Config
41
+
42
+ Your channel configuration is injected into your context (if any channels are configured). It comes from `~/.bloby/config.json` — a file OUTSIDE your workspace that the supervisor manages.
43
+
44
+ ---
45
+
46
+ ## Modes
47
+
48
+ **Channel Mode** (default): Your human's own WhatsApp number. Only self-chat (messages your human sends to themselves) triggers you — messages from other people are completely ignored. This is "just talk to me" mode.
49
+
50
+ **Business Mode**: Bloby has its own dedicated WhatsApp number. Numbers in the `admins` array get admin access (main system prompt). Everyone else is a customer and gets the support prompt from the active skill's SCRIPT.md.
51
+
52
+ **Assistant Mode**: Your personal assistant inside your own conversations. Self-chat works as a normal admin channel. When other people message you, their messages are silently stored for context. When YOU type `@botname:` followed by a command in someone's chat, the agent activates with full conversation context and responds in that chat. The trigger uses the bot's configured name (from `config.json` `username` field) and is case-insensitive. Nobody else can trigger the agent — only you (the account owner). Uses the active skill's SCRIPT.md for the system prompt and `customer_data/` for per-contact memory.
53
+
54
+ ---
55
+
56
+ ## Setup
57
+
58
+ ### 1. Connect WhatsApp
59
+
60
+ When your human asks to configure WhatsApp:
61
+
62
+ 1. Start the connection:
63
+ ```bash
64
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/connect
65
+ ```
66
+
67
+ 2. Tell them to open the QR page: `/api/channels/whatsapp/qr-page`
68
+ (Send this as a relative URL — their browser is already on the correct domain. Don't mention the URL until you are actually starting the connection)
69
+
70
+ 3. They scan the QR with their WhatsApp app
71
+
72
+ 4. The default mode is **channel** (self-chat only)
73
+
74
+ If the QR page doesn't load, make sure you initiated the connection first (step 1).
75
+
76
+ **On mobile?** The QR page also offers a "Link with phone number instead" option. The user enters their phone number, gets an 8-character code, and types it into WhatsApp (Settings > Linked Devices > Link a Device > "Link with phone number instead"). No camera needed.
77
+
78
+ ### 2. Choose a Mode
79
+
80
+ **Channel mode** (default) — personal assistant. Only self-chat triggers the agent:
81
+
82
+ ```bash
83
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
84
+ -H "Content-Type: application/json" \
85
+ -d '{"mode":"channel"}'
86
+ ```
87
+
88
+ **Business mode** — customer-facing. The agent responds to incoming messages from customers using a skill's SCRIPT.md. Admin numbers get full agent access:
89
+
90
+ ```bash
91
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
92
+ -H "Content-Type: application/json" \
93
+ -d '{"mode":"business","admins":["ADMIN_PHONE_1","ADMIN_PHONE_2"],"skill":"SKILL_FOLDER_NAME"}'
94
+ ```
95
+
96
+ Replace `ADMIN_PHONE_1` with the human's phone number (digits only, with country code, e.g. `5511999887766`). Replace `SKILL_FOLDER_NAME` with the skill that should handle customer conversations (e.g. `whatsapp-clinic-secretary`).
97
+
98
+ **Assistant mode** — personal assistant in your conversations. Self-chat works normally. Other people's messages are stored silently. Type `@botname: <command>` in any chat to trigger the agent:
99
+
100
+ ```bash
101
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
102
+ -H "Content-Type: application/json" \
103
+ -d '{"mode":"assistant","skill":"SKILL_FOLDER_NAME"}'
104
+ ```
105
+
106
+ The trigger uses the bot's name from `config.json` `username` field (e.g. if username is "bloby", trigger is `@bloby:`). Only the account owner can trigger — other people's messages are context only.
107
+
108
+ ### 3. Verify
109
+
110
+ ```bash
111
+ curl -s http://localhost:7400/api/channels/status
112
+ ```
113
+
114
+ Expected: `"channel":"whatsapp","connected":true`
115
+
116
+ ---
117
+
118
+ ## Business Mode — Active Skill
119
+
120
+ Only ONE skill can be active for customer-facing mode at a time. The active skill is set in the channel config (`channels.whatsapp.skill`). When your human asks to switch skills:
121
+
122
+ ```bash
123
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
124
+ -H "Content-Type: application/json" -d '{"skill":"whatsapp-clinic"}'
125
+ ```
126
+
127
+ The active skill should have:
128
+ - `SCRIPT.md` — the customer-facing system prompt (loaded automatically for customer conversations)
129
+ - Optionally a `customer_data/` directory — per-customer memory files (named by phone number, e.g. `5511999887766.md`)
130
+
131
+ ---
132
+
133
+ ## Sending Proactive Messages
134
+
135
+ To INITIATE a WhatsApp message (during pulse, cron, or when you want to reach out first):
136
+
137
+ ```bash
138
+ curl -s -X POST http://localhost:7400/api/channels/send \
139
+ -H "Content-Type: application/json" \
140
+ -d '{"channel":"whatsapp","to":"5511999888777","text":"Your appointment is confirmed for tomorrow at 2pm."}'
141
+ ```
142
+
143
+ Phone number format: digits with country code (e.g. `5511999887766`). The system normalizes to WhatsApp JID format automatically.
144
+
145
+ **Remember:** This is ONLY for starting new conversations or sending unprompted messages. When replying to an incoming message, just respond normally — the supervisor handles delivery.
146
+
147
+ ---
148
+
149
+ ## Customer Conversation Logs
150
+
151
+ When you finish a conversation with a **customer** via WhatsApp, save a summary to `whatsapp/{phone}.md`:
152
+ - Key details from the conversation
153
+ - Outcome (appointment scheduled, question answered, etc.)
154
+ - Any follow-ups needed
155
+ - Timestamp
156
+
157
+ This is your memory of that customer. Next time they message, read their file first.
158
+
159
+ ---
160
+
161
+ ## Voice Notes
162
+
163
+ Voice messages are automatically transcribed via Whisper and delivered as text. No extra setup needed if Whisper is configured on the supervisor.
164
+
165
+ ## Typing Indicator
166
+
167
+ The agent automatically shows "typing..." to the recipient while composing a response. This is handled by the supervisor — no action needed from you.
168
+
169
+ ## Message Buffering (Business Mode)
170
+
171
+ In business mode, rapid messages from the same customer are debounced (4-second window) and delivered together. The system maintains a 30-message conversation buffer per customer.
172
+
173
+ ## Concurrent Conversations (Business Mode)
174
+
175
+ Up to 5 customer conversations can run in parallel. Additional messages queue automatically.
176
+
177
+ ---
178
+
179
+ ## Account Management
180
+
181
+ **Disconnect** (keep credentials for later):
182
+ ```bash
183
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/disconnect
184
+ ```
185
+
186
+ **Logout** (delete credentials, requires new QR scan):
187
+ ```bash
188
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/logout
189
+ ```
190
+
191
+ **Switch accounts** (relink): Use the "Relink" button on the QR page, or logout + connect again.
192
+
193
+ ---
194
+
195
+ ## Human Interaction
196
+
197
+ - The human must scan the QR code with their phone — this cannot be automated
198
+ - If WhatsApp disconnects (phone lost, account switched), the human needs to re-scan
199
+ - In business mode, explain to the human that admin numbers get full agent access while all other numbers get the customer-facing skill
200
+ - If the human asks about privacy: credentials are stored locally at `~/.bloby/channels/whatsapp/auth/`, never sent to external servers
201
+
202
+ ---
203
+
204
+ ## API Reference
205
+
206
+ | Endpoint | Method | Purpose |
207
+ |----------|--------|---------|
208
+ | `/api/channels/status` | GET | List all channel statuses |
209
+ | `/api/channels/whatsapp/qr` | GET | Get current QR code SVG |
210
+ | `/api/channels/whatsapp/qr-page` | GET | Standalone QR scanning page |
211
+ | `/api/channels/whatsapp/connect` | POST | Start WhatsApp (triggers QR if needed) |
212
+ | `/api/channels/whatsapp/disconnect` | POST | Disconnect WhatsApp |
213
+ | `/api/channels/whatsapp/logout` | POST | Disconnect + delete credentials |
214
+ | `/api/channels/whatsapp/configure` | POST | Set mode + admins + skill |
215
+ | `/api/channels/whatsapp/pairing-code` | POST | Get 8-char pairing code (mobile linking) |
216
+ | `/api/channels/send` | POST | Send proactive message via channel |
217
+
218
+ All endpoints use `http://localhost:7400` for internal API calls (curl from your terminal). For URLs shown to your human, use relative paths (e.g. `/api/channels/whatsapp/qr-page`) — their browser is already on the correct domain.
219
+
220
+ ---
221
+
222
+ ## Technical Notes
223
+
224
+ - Baileys is a reverse-engineering of WhatsApp Web. It can break if WhatsApp changes their protocol. Reconnection is automatic on network drops.
225
+ - If you get error 401 (loggedOut), credentials were invalidated — the human needs to re-scan QR.
226
+ - If you get error 440 (connectionReplaced), another device/instance took over — do NOT auto-reconnect, ask the human.
227
+ - LID (Local ID) vs phone number: WhatsApp uses internal IDs. The system translates them automatically — use phone numbers in all your API calls.
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "whatsapp",
3
+ "version": "2.0.0",
4
+ "type": "skill",
5
+ "bloby_human": "Bruno Bertapeli",
6
+ "bloby": "bloby-bruno",
7
+ "author": "newbot-official",
8
+ "description": "WhatsApp channel for your agent via Baileys. QR auth, messaging, voice transcription, channel and business modes.",
9
+ "depends": [],
10
+ "env_keys": [],
11
+ "has_telemetry": false,
12
+ "size": "12KB",
13
+ "contains_binaries": false,
14
+ "tags": ["whatsapp", "channel", "messaging"]
15
+ }