fluxy-bot 0.15.1 → 0.15.2

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": "fluxy-bot",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
package/shared/config.ts CHANGED
@@ -3,10 +3,10 @@ import { paths, DATA_DIR } from './paths.js';
3
3
 
4
4
  export interface ChannelConfig {
5
5
  enabled: boolean;
6
- /** 'shared' = user's own number, 'dedicated' = Fluxy has its own number */
7
- mode: 'shared' | 'dedicated';
8
- /** The human/owner's phone number (used for role resolution in dedicated mode) */
9
- humanPhone?: string;
6
+ /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer support */
7
+ mode: 'channel' | 'business';
8
+ /** Phone numbers with admin access (owner, secretary, etc.) business mode only */
9
+ admins?: string[];
10
10
  }
11
11
 
12
12
  export interface BotConfig {
@@ -3,10 +3,16 @@
3
3
  *
4
4
  * Responsibilities:
5
5
  * - Manages channel providers (WhatsApp, Telegram, etc.)
6
- * - Resolves sender role (human vs customer)
6
+ * - Resolves sender role (admin vs customer) based on mode
7
7
  * - Routes inbound messages to the agent with appropriate system prompt
8
- * - Routes outbound messages from agent/API back to channels
9
- * - Manages parallel agent instances for customer conversations
8
+ * - Routes agent responses back to channels
9
+ * - Manages parallel agent instances for customer conversations (business mode)
10
+ *
11
+ * Modes:
12
+ * - channel: Just talk to me. Only self-chat (fromMe=true) triggers the agent.
13
+ * All other messages are ignored — it's the user's personal WhatsApp.
14
+ * - business: Fluxy has its own number. Numbers in the admins array get the main
15
+ * system prompt. Everyone else gets the customer support prompt.
10
16
  */
11
17
 
12
18
  import fs from 'fs';
@@ -30,7 +36,6 @@ interface ChannelManagerOpts {
30
36
  interface ActiveAgentQuery {
31
37
  sender: string;
32
38
  channel: ChannelType;
33
- abortController?: AbortController;
34
39
  }
35
40
 
36
41
  export class ChannelManager {
@@ -47,7 +52,7 @@ export class ChannelManager {
47
52
  /** Initialize channels based on config */
48
53
  async init(): Promise<void> {
49
54
  const config = loadConfig();
50
- const channelConfigs = (config as any).channels as Record<string, ChannelConfig> | undefined;
55
+ const channelConfigs = config.channels;
51
56
 
52
57
  if (!channelConfigs?.whatsapp?.enabled) {
53
58
  log.info('[channels] WhatsApp not enabled — skipping');
@@ -75,7 +80,6 @@ export class ChannelManager {
75
80
  async connectWhatsApp(): Promise<void> {
76
81
  let provider = this.providers.get('whatsapp');
77
82
  if (!provider) {
78
- // Create provider on-demand if not initialized
79
83
  const whatsapp = new WhatsAppChannel(
80
84
  (sender, senderName, text, fromMe) => this.handleInboundMessage('whatsapp', sender, senderName, text, fromMe),
81
85
  (status) => this.handleStatusChange(status),
@@ -148,27 +152,10 @@ export class ChannelManager {
148
152
  }
149
153
  }
150
154
 
151
- /** Resolve sender role based on channel config */
152
- private resolveRole(channel: ChannelType, sender: string, fromMe: boolean): SenderRole {
155
+ /** Get the channel config, re-reading from disk each time */
156
+ private getChannelConfig(channel: ChannelType): ChannelConfig | undefined {
153
157
  const config = loadConfig();
154
- const channelConfigs = (config as any).channels as Record<string, ChannelConfig> | undefined;
155
- const channelConfig = channelConfigs?.[channel];
156
-
157
- if (!channelConfig) return 'customer';
158
-
159
- if (channelConfig.mode === 'shared') {
160
- // Shared number mode: fromMe messages are from the human
161
- return fromMe ? 'human' : 'customer';
162
- }
163
-
164
- // Dedicated number mode: check if sender matches human's phone
165
- if (channelConfig.humanPhone) {
166
- const senderPhone = sender.replace(/@.*/, '').replace(/[^0-9]/g, '');
167
- const humanPhone = channelConfig.humanPhone.replace(/[^0-9]/g, '');
168
- if (senderPhone === humanPhone) return 'human';
169
- }
170
-
171
- return 'customer';
158
+ return config.channels?.[channel];
172
159
  }
173
160
 
174
161
  /** Handle an incoming message from any channel */
@@ -179,7 +166,34 @@ export class ChannelManager {
179
166
  text: string,
180
167
  fromMe: boolean,
181
168
  ) {
182
- const role = this.resolveRole(channel, sender, fromMe);
169
+ const channelConfig = this.getChannelConfig(channel);
170
+ if (!channelConfig) return;
171
+
172
+ const mode = channelConfig.mode || 'channel';
173
+
174
+ // ── Channel mode: only respond to self-chat (fromMe=true) ──
175
+ if (mode === 'channel') {
176
+ if (!fromMe) {
177
+ // Ignore messages from other people — this is the user's personal WhatsApp
178
+ return;
179
+ }
180
+
181
+ const message: InboundMessage = {
182
+ channel,
183
+ sender: sender.replace(/@.*/, ''),
184
+ senderName,
185
+ role: 'admin',
186
+ text,
187
+ rawSender: sender,
188
+ };
189
+
190
+ log.info(`[channels] Channel mode | self-chat | "${text.slice(0, 60)}"`);
191
+ await this.handleAdminMessage(message);
192
+ return;
193
+ }
194
+
195
+ // ── Business mode: resolve role based on admins array ──
196
+ const role = this.resolveBusinessRole(channelConfig, sender, fromMe);
183
197
 
184
198
  const message: InboundMessage = {
185
199
  channel,
@@ -190,31 +204,47 @@ export class ChannelManager {
190
204
  rawSender: sender,
191
205
  };
192
206
 
193
- log.info(`[channels] Inbound ${channel} | ${message.sender} | role=${role} | "${text.slice(0, 60)}"`);
207
+ log.info(`[channels] Business mode | ${message.sender} | role=${role} | "${text.slice(0, 60)}"`);
194
208
 
195
- if (role === 'human') {
196
- await this.handleHumanMessage(message);
209
+ if (role === 'admin') {
210
+ await this.handleAdminMessage(message);
197
211
  } else {
198
212
  await this.handleCustomerMessage(message);
199
213
  }
200
214
  }
201
215
 
202
- /** Handle message from the human/ownermirrors to chat conversation */
203
- private async handleHumanMessage(msg: InboundMessage) {
216
+ /** Resolve role in business modecheck admins array */
217
+ private resolveBusinessRole(config: ChannelConfig, sender: string, fromMe: boolean): SenderRole {
218
+ // fromMe is always admin (the number Fluxy is connected with)
219
+ if (fromMe) return 'admin';
220
+
221
+ // Check admins array
222
+ if (config.admins?.length) {
223
+ const senderPhone = sender.replace(/@.*/, '').replace(/[^0-9]/g, '');
224
+ for (const admin of config.admins) {
225
+ const adminPhone = admin.replace(/[^0-9]/g, '');
226
+ if (senderPhone === adminPhone || senderPhone.endsWith(adminPhone) || adminPhone.endsWith(senderPhone)) {
227
+ return 'admin';
228
+ }
229
+ }
230
+ }
231
+
232
+ return 'customer';
233
+ }
234
+
235
+ /** Handle message from an admin — mirrors to chat conversation, uses main system prompt */
236
+ private async handleAdminMessage(msg: InboundMessage) {
204
237
  const { workerApi, broadcastFluxy, getModel } = this.opts;
205
238
  const model = getModel();
206
239
 
207
- // Get or create the human's WhatsApp conversation (mirrored with chat)
240
+ // Get or create conversation (shared with chat for mirroring)
208
241
  let convId: string | undefined;
209
242
  try {
210
243
  const ctx = await workerApi('/api/context/current');
211
244
  if (ctx.conversationId) {
212
245
  convId = ctx.conversationId;
213
246
  } else {
214
- const conv = await workerApi('/api/conversations', 'POST', {
215
- title: `WhatsApp`,
216
- model,
217
- });
247
+ const conv = await workerApi('/api/conversations', 'POST', { title: 'WhatsApp', model });
218
248
  convId = conv.id;
219
249
  await workerApi('/api/context/set', 'POST', { conversationId: convId });
220
250
  }
@@ -240,7 +270,7 @@ export class ChannelManager {
240
270
  message: { role: 'user', content: msg.text, timestamp: new Date().toISOString() },
241
271
  });
242
272
 
243
- // Fetch agent/user names and recent messages
273
+ // Fetch names and recent messages
244
274
  let botName = 'Fluxy', humanName = 'Human';
245
275
  let recentMessages: RecentMessage[] = [];
246
276
  try {
@@ -261,8 +291,8 @@ export class ChannelManager {
261
291
  }
262
292
  } catch {}
263
293
 
264
- // Run agent with main system prompt (same as chat)
265
- const channelContext = `[WhatsApp | ${msg.sender} | human]\n`;
294
+ // Channel context — tells the agent this is a WhatsApp message, respond naturally
295
+ const channelContext = `[WhatsApp | ${msg.sender} | admin]\n`;
266
296
 
267
297
  startFluxyAgentQuery(
268
298
  convId,
@@ -270,7 +300,7 @@ export class ChannelManager {
270
300
  model,
271
301
  (type, eventData) => {
272
302
  if (type === 'bot:response' && eventData.content) {
273
- // Send response back via WhatsApp
303
+ // Send agent's response back via WhatsApp
274
304
  this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
275
305
  log.warn(`[channels] Failed to send WhatsApp reply: ${err.message}`);
276
306
  });
@@ -305,7 +335,6 @@ export class ChannelManager {
305
335
 
306
336
  // Check concurrent limit
307
337
  if (this.activeAgents.size >= MAX_CONCURRENT_AGENTS && !this.activeAgents.has(agentKey)) {
308
- // Queue the message
309
338
  log.info(`[channels] Max concurrent agents reached — queuing message from ${msg.sender}`);
310
339
  this.messageQueue.push(msg);
311
340
  return;
@@ -317,7 +346,7 @@ export class ChannelManager {
317
346
  // Load support system prompt from skill
318
347
  const supportPrompt = this.loadSupportPrompt();
319
348
 
320
- // Fetch agent/user names
349
+ // Fetch agent name
321
350
  let botName = 'Fluxy', humanName = 'Human';
322
351
  try {
323
352
  const status = await workerApi('/api/onboard/status');
@@ -325,7 +354,6 @@ export class ChannelManager {
325
354
  humanName = status.userName || 'Human';
326
355
  } catch {}
327
356
 
328
- // Build channel context
329
357
  const channelContext = `[WhatsApp | ${msg.sender} | customer${msg.senderName ? ` | ${msg.senderName}` : ''}]\n`;
330
358
  const convId = `channel-${agentKey}-${Date.now()}`;
331
359
 
@@ -337,7 +365,6 @@ export class ChannelManager {
337
365
  model,
338
366
  (type, eventData) => {
339
367
  if (type === 'bot:response' && eventData.content) {
340
- // Send response back via WhatsApp
341
368
  this.sendMessage(msg.channel, msg.rawSender, eventData.content).catch((err) => {
342
369
  log.warn(`[channels] Failed to send customer reply: ${err.message}`);
343
370
  });
@@ -345,12 +372,7 @@ export class ChannelManager {
345
372
 
346
373
  if (type === 'bot:done') {
347
374
  this.activeAgents.delete(agentKey);
348
-
349
- if (eventData.usedFileTools) {
350
- this.opts.restartBackend();
351
- }
352
-
353
- // Process queued messages
375
+ if (eventData.usedFileTools) this.opts.restartBackend();
354
376
  this.processQueue();
355
377
  }
356
378
  },
@@ -364,7 +386,6 @@ export class ChannelManager {
364
386
 
365
387
  /** Load customer-facing system prompt from skills */
366
388
  private loadSupportPrompt(): string | undefined {
367
- // Look for SUPPORT.md in any skill directory
368
389
  const skillsDir = path.join(WORKSPACE_DIR, 'skills');
369
390
  try {
370
391
  for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
@@ -3,19 +3,19 @@
3
3
  */
4
4
 
5
5
  export type ChannelType = 'whatsapp' | 'telegram';
6
- export type SenderRole = 'human' | 'customer';
6
+ export type SenderRole = 'admin' | 'customer';
7
7
 
8
8
  export interface ChannelConfig {
9
9
  enabled: boolean;
10
- /** 'shared' = user's own number, 'dedicated' = Fluxy has its own number */
11
- mode: 'shared' | 'dedicated';
12
- /** The human/owner's phone number (used for role resolution in dedicated mode) */
13
- humanPhone?: string;
10
+ /** 'channel' = just talk to me (self-chat only), 'business' = admin/customer support */
11
+ mode: 'channel' | 'business';
12
+ /** Phone numbers with admin access (owner, secretary, etc.) business mode only */
13
+ admins?: string[];
14
14
  }
15
15
 
16
16
  export interface InboundMessage {
17
17
  channel: ChannelType;
18
- /** Sender identifier (phone number / JID) */
18
+ /** Sender identifier (phone number) */
19
19
  sender: string;
20
20
  /** Sender display name if available */
21
21
  senderName?: string;
@@ -23,7 +23,7 @@ export interface InboundMessage {
23
23
  role: SenderRole;
24
24
  /** Message text content */
25
25
  text: string;
26
- /** Raw sender JID (channel-specific format) */
26
+ /** Raw sender JID (channel-specific format, used for replies) */
27
27
  rawSender: string;
28
28
  }
29
29
 
@@ -30,13 +30,18 @@ export class WhatsAppChannel implements ChannelProvider {
30
30
 
31
31
  private sock: WASocket | null = null;
32
32
  private connected = false;
33
- private qrData: string | null = null; // raw QR string data
34
- private qrSvg: string | null = null; // SVG rendered QR
33
+ private qrData: string | null = null;
34
+ private qrSvg: string | null = null;
35
35
  private onMessage: OnWhatsAppMessage;
36
36
  private onStatusChange: (status: ChannelStatus) => void;
37
37
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
38
38
  private intentionalDisconnect = false;
39
39
 
40
+ /** Maps LID JIDs to phone JIDs (WhatsApp uses LIDs internally for self-chat) */
41
+ private lidToPhoneMap = new Map<string, string>();
42
+ /** Our own phone JID (number@s.whatsapp.net) */
43
+ private ownPhoneJid: string | null = null;
44
+
40
45
  constructor(
41
46
  onMessage: OnWhatsAppMessage,
42
47
  onStatusChange: (status: ChannelStatus) => void,
@@ -110,6 +115,42 @@ export class WhatsAppChannel implements ChannelProvider {
110
115
 
111
116
  // ── Internal ──
112
117
 
118
+ /** Translate a JID from LID format to phone format if possible */
119
+ private translateJid(jid: string): string {
120
+ // If it's already a phone JID, return as-is
121
+ if (jid.endsWith('@s.whatsapp.net')) return jid;
122
+
123
+ // Check LID map
124
+ const mapped = this.lidToPhoneMap.get(jid);
125
+ if (mapped) return mapped;
126
+
127
+ // If it's a LID JID and we know our own phone, and this looks like a self-chat LID
128
+ // LID JIDs typically end with @lid or have a long numeric format
129
+ if (this.ownPhoneJid && (jid.includes('@lid') || jid.match(/^\d{15,}@/))) {
130
+ log.info(`[whatsapp] Unmapped LID ${jid} — assuming self-chat, using own phone JID`);
131
+ this.lidToPhoneMap.set(jid, this.ownPhoneJid);
132
+ return this.ownPhoneJid;
133
+ }
134
+
135
+ return jid;
136
+ }
137
+
138
+ /** Build the LID-to-phone mapping from sock.user */
139
+ private buildLidMap() {
140
+ if (!this.sock?.user) return;
141
+
142
+ const user = this.sock.user;
143
+ // user.id is "phone:device@s.whatsapp.net" — extract phone
144
+ const phone = user.id.split(':')[0];
145
+ this.ownPhoneJid = `${phone}@s.whatsapp.net`;
146
+
147
+ // user.lid (if available) is the LID JID
148
+ if ((user as any).lid) {
149
+ this.lidToPhoneMap.set((user as any).lid, this.ownPhoneJid);
150
+ log.info(`[whatsapp] LID map: ${(user as any).lid} → ${this.ownPhoneJid}`);
151
+ }
152
+ }
153
+
113
154
  private async connectInternal(): Promise<void> {
114
155
  // Ensure auth directory exists
115
156
  fs.mkdirSync(AUTH_DIR, { recursive: true });
@@ -164,6 +205,7 @@ export class WhatsAppChannel implements ChannelProvider {
164
205
  this.connected = true;
165
206
  this.qrData = null;
166
207
  this.qrSvg = null;
208
+ this.buildLidMap();
167
209
  log.ok(`[whatsapp] Connected as ${sock.user?.id}`);
168
210
  this.emitStatus();
169
211
  }
@@ -207,10 +249,13 @@ export class WhatsAppChannel implements ChannelProvider {
207
249
  if (!text) continue;
208
250
 
209
251
  const fromMe = msg.key.fromMe || false;
210
- const sender = msg.key.remoteJid || '';
252
+ const rawSender = msg.key.remoteJid || '';
253
+
254
+ // Translate LID JIDs to phone JIDs
255
+ const sender = this.translateJid(rawSender);
211
256
  const pushName = msg.pushName || undefined;
212
257
 
213
- log.info(`[whatsapp] Message from ${sender} (fromMe=${fromMe}): ${text.slice(0, 80)}`);
258
+ log.info(`[whatsapp] Message from ${sender} (raw=${rawSender}, fromMe=${fromMe}): ${text.slice(0, 80)}`);
214
259
 
215
260
  this.onMessage(sender, pushName, text, fromMe);
216
261
  }
@@ -429,7 +429,7 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
429
429
  // Enable WhatsApp in config
430
430
  const cfg = loadConfig();
431
431
  if (!cfg.channels) cfg.channels = {};
432
- if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'shared' };
432
+ if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'channel' };
433
433
  cfg.channels.whatsapp.enabled = true;
434
434
  saveConfig(cfg);
435
435
 
@@ -480,7 +480,7 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
480
480
  return;
481
481
  }
482
482
 
483
- // POST /api/channels/whatsapp/configure — set mode + human phone
483
+ // POST /api/channels/whatsapp/configure — set mode + admins
484
484
  if (req.method === 'POST' && channelPath === '/api/channels/whatsapp/configure') {
485
485
  let body = '';
486
486
  req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
@@ -489,9 +489,9 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
489
489
  const data = JSON.parse(body);
490
490
  const cfg = loadConfig();
491
491
  if (!cfg.channels) cfg.channels = {};
492
- if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'shared' };
492
+ if (!cfg.channels.whatsapp) cfg.channels.whatsapp = { enabled: true, mode: 'channel' };
493
493
  if (data.mode) cfg.channels.whatsapp.mode = data.mode;
494
- if (data.humanPhone !== undefined) cfg.channels.whatsapp.humanPhone = data.humanPhone;
494
+ if (data.admins !== undefined) cfg.channels.whatsapp.admins = data.admins;
495
495
  saveConfig(cfg);
496
496
  res.writeHead(200);
497
497
  res.end(JSON.stringify({ ok: true, config: cfg.channels.whatsapp }));
@@ -206,6 +206,14 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
206
206
 
207
207
  You can communicate through messaging channels beyond the chat bubble. Currently supported: **WhatsApp**.
208
208
 
209
+ ### CRITICAL: How WhatsApp Responses Work
210
+
211
+ **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 as if you're talking to the person.
212
+
213
+ **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.
214
+
215
+ **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.
216
+
209
217
  ### Channel Config
210
218
 
211
219
  Your channel configuration is injected below (if any channels are configured). It comes from `~/.fluxy/config.json` — a file OUTSIDE your workspace that the supervisor manages.
@@ -220,28 +228,41 @@ Hi, I'd like to schedule an appointment.
220
228
 
221
229
  The format is: `[Channel | phone | role | name (optional)]`
222
230
 
223
- - **role=human**: This is YOUR human (the owner). Use your normal personality, full capabilities, main system prompt.
231
+ - **role=admin**: This is your human or an authorized admin. Use your normal personality, full capabilities, main system prompt.
224
232
  - **role=customer**: This is someone else messaging. You're in **support mode** — follow the instructions from your active skill's SUPPORT.md.
225
233
 
234
+ ### WhatsApp Modes
235
+
236
+ **Channel Mode** (default): Your human's own WhatsApp number. Only self-chat triggers you — messages from other people are completely ignored. This is "just talk to me" mode.
237
+
238
+ **Business Mode**: Fluxy has its own dedicated number. Numbers in the `admins` array get admin access (main system prompt). Everyone else is a customer (support prompt).
239
+
226
240
  ### Setting Up WhatsApp
227
241
 
228
242
  When your human asks to configure WhatsApp:
229
243
  1. Start the connection: `curl -s -X POST http://localhost:3000/api/channels/whatsapp/connect`
230
244
  2. Tell them to open the QR page: `http://localhost:3000/api/channels/whatsapp/qr-page` (or create a dashboard page that embeds it)
231
245
  3. They scan the QR with their WhatsApp app
232
- 4. Once connected, configure the mode:
233
- - **Shared number** (default): Your human's own WhatsApp. Messages they send to themselves trigger you.
234
- - **Dedicated number**: A separate phone/SIM for you. Configure with: `curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure -H "Content-Type: application/json" -d '{"mode":"dedicated","humanPhone":"+1234567890"}'`
246
+ 4. The default mode is **channel** (self-chat only)
247
+
248
+ To switch to **business mode** with admin numbers:
249
+ ```bash
250
+ curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure \
251
+ -H "Content-Type: application/json" \
252
+ -d '{"mode":"business","admins":["+17865551234","+5511999887766"]}'
253
+ ```
235
254
 
236
- ### Sending Messages
255
+ ### Sending Proactive Messages
237
256
 
238
- To send a WhatsApp message (during pulse, cron, or any time):
257
+ To INITIATE a WhatsApp message (during pulse, cron, or when you want to reach out first):
239
258
  ```bash
240
259
  curl -s -X POST http://localhost:3000/api/channels/send \
241
260
  -H "Content-Type: application/json" \
242
261
  -d '{"channel":"whatsapp","to":"5511999888777","text":"Your appointment is confirmed for tomorrow at 2pm."}'
243
262
  ```
244
263
 
264
+ **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.
265
+
245
266
  ### Customer Conversation Logs
246
267
 
247
268
  When you finish a conversation with a **customer** via WhatsApp, save a summary to `whatsapp/{phone}.md`:
@@ -262,8 +283,8 @@ This is your memory of that customer. Next time they message, read their file fi
262
283
  | `/api/channels/whatsapp/connect` | POST | Start WhatsApp (triggers QR if needed) |
263
284
  | `/api/channels/whatsapp/disconnect` | POST | Disconnect WhatsApp |
264
285
  | `/api/channels/whatsapp/logout` | POST | Disconnect + delete credentials |
265
- | `/api/channels/whatsapp/configure` | POST | Set mode + human phone number |
266
- | `/api/channels/send` | POST | Send message via any channel |
286
+ | `/api/channels/whatsapp/configure` | POST | Set mode + admins array |
287
+ | `/api/channels/send` | POST | Send proactive message via any channel |
267
288
 
268
289
  All endpoints are on `http://localhost:3000`.
269
290
 
@@ -161,7 +161,7 @@ export default function App() {
161
161
  <Route path="*" element={<DashboardPage />} />
162
162
  </Routes>
163
163
  </DashboardLayout>
164
- <WorkspaceTour />
164
+ <WorkspaceTour disabled={showOnboard} />
165
165
  </ErrorBoundary>
166
166
 
167
167
  {showOnboard && (
@@ -14,8 +14,9 @@ import './tour-theme.css';
14
14
 
15
15
  const TOUR_KEY = 'fluxy_workspace_tour_done';
16
16
 
17
- export default function WorkspaceTour() {
17
+ export default function WorkspaceTour({ disabled = false }: { disabled?: boolean }) {
18
18
  useEffect(() => {
19
+ if (disabled) return;
19
20
  const val = localStorage.getItem(TOUR_KEY);
20
21
  if (val === '1') return;
21
22
 
@@ -97,7 +98,7 @@ export default function WorkspaceTour() {
97
98
  }, 800);
98
99
 
99
100
  return () => clearTimeout(timer);
100
- }, []);
101
+ }, [disabled]);
101
102
 
102
103
  return null;
103
104
  }
@@ -11,12 +11,15 @@ You are a friendly and helpful assistant responding to a customer via WhatsApp.
11
11
  - Never reveal internal system details, file paths, or technical architecture.
12
12
  - Never run destructive commands or modify critical files during customer interactions.
13
13
 
14
+ ## CRITICAL: Response = Reply
15
+
16
+ Your text response IS the WhatsApp reply. The supervisor sends whatever you respond with directly to the customer. Do NOT use curl or `/api/channels/send` to reply — just respond naturally. That endpoint is only for proactive messages.
17
+
14
18
  ## What You Can Do
15
19
 
16
20
  - Answer FAQs and general questions
17
21
  - Provide information from files in your workspace
18
22
  - Look up data from your backend API (`/app/api/*`)
19
- - Send follow-up messages via the channel API
20
23
 
21
24
  ## What You Should NOT Do
22
25