twinclaw 1.1.3 → 1.1.5
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/dist/core/cli.js +6 -4
- package/dist/core/gateway-cli.js +22 -1
- package/dist/core/onboarding.js +74 -10
- package/dist/core/simplified-onboarding.js +1 -1
- package/dist/index.js +13 -4
- package/dist/interfaces/dispatcher.js +43 -4
- package/dist/interfaces/telegram_handler.js +8 -2
- package/dist/interfaces/whatsapp_handler.js +3 -0
- package/package.json +1 -1
package/dist/core/cli.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
1
3
|
import { runDoctorChecks, formatDoctorReport } from './doctor.js';
|
|
2
4
|
// ── Help text ────────────────────────────────────────────────────────────────
|
|
3
5
|
const HELP_TEXT = `
|
|
@@ -121,11 +123,11 @@ export function handleUnknownCommand(argv) {
|
|
|
121
123
|
export function handleUpdateCli(argv) {
|
|
122
124
|
if (argv[0] !== 'update')
|
|
123
125
|
return false;
|
|
124
|
-
const { execSync } = require('child_process');
|
|
125
126
|
console.log('[TwinClaw] Checking for updates...');
|
|
126
127
|
try {
|
|
127
128
|
// Get current version
|
|
128
|
-
const
|
|
129
|
+
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
|
|
130
|
+
const currentVersion = packageJson.version;
|
|
129
131
|
console.log(`[TwinClaw] Current version: ${currentVersion}`);
|
|
130
132
|
// Fetch latest version from npm
|
|
131
133
|
const latestVersion = execSync('npm view twinclaw version', { encoding: 'utf8' }).trim();
|
|
@@ -136,8 +138,8 @@ export function handleUpdateCli(argv) {
|
|
|
136
138
|
return true;
|
|
137
139
|
}
|
|
138
140
|
console.log('[TwinClaw] Update available! Installing...');
|
|
139
|
-
// Install latest version globally
|
|
140
|
-
execSync('npm install -g twinclaw@latest', { stdio: 'inherit' });
|
|
141
|
+
// Install latest version globally (--yes to skip prompts)
|
|
142
|
+
execSync('npm install -g twinclaw@latest --yes', { stdio: 'inherit' });
|
|
141
143
|
console.log(`[TwinClaw] Successfully updated to version ${latestVersion}`);
|
|
142
144
|
console.log('[TwinClaw] Please restart TwinClaw to use the new version.');
|
|
143
145
|
process.exitCode = 0;
|
package/dist/core/gateway-cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const GATEWAY_COMMANDS = new Set([
|
|
|
11
11
|
'status',
|
|
12
12
|
'uninstall',
|
|
13
13
|
'tailscale',
|
|
14
|
+
'run',
|
|
14
15
|
]);
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -54,8 +55,10 @@ export function parseGatewayArgs(argv) {
|
|
|
54
55
|
if (argv[0] !== 'gateway') {
|
|
55
56
|
throw new Error('Gateway parser expects argv beginning with "gateway".');
|
|
56
57
|
}
|
|
58
|
+
// If no subcommand provided, default to 'run' (interactive mode)
|
|
59
|
+
const command = argv[1] || 'run';
|
|
57
60
|
const parsed = {
|
|
58
|
-
command: ensureCommand(
|
|
61
|
+
command: ensureCommand(command),
|
|
59
62
|
asJson: false,
|
|
60
63
|
deep: false,
|
|
61
64
|
};
|
|
@@ -311,6 +314,24 @@ export async function handleGatewayCli(argv) {
|
|
|
311
314
|
case 'tailscale':
|
|
312
315
|
console.log(buildTailscaleInstructions(context).trimStart());
|
|
313
316
|
break;
|
|
317
|
+
case 'run': {
|
|
318
|
+
// Run TwinClaw in foreground (interactive mode)
|
|
319
|
+
console.log('[TwinClaw] Starting TwinClaw in interactive mode...');
|
|
320
|
+
// Spawn the main application
|
|
321
|
+
const { spawn } = require('node:child_process');
|
|
322
|
+
const child = spawn(context.nodePath, [context.entryScript], {
|
|
323
|
+
stdio: 'inherit',
|
|
324
|
+
env: { ...process.env },
|
|
325
|
+
});
|
|
326
|
+
// Wait for the child to exit
|
|
327
|
+
await new Promise((resolve) => {
|
|
328
|
+
child.on('exit', (code) => {
|
|
329
|
+
process.exitCode = process.exitCode ?? code ?? 0;
|
|
330
|
+
resolve();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
catch (error) {
|
package/dist/core/onboarding.js
CHANGED
|
@@ -8,7 +8,6 @@ import { createSession, saveMessage } from '../services/db.js';
|
|
|
8
8
|
import { indexConversationTurn, retrieveMemoryContext } from '../services/semantic-memory.js';
|
|
9
9
|
import { ModelRouter } from '../services/model-router.js';
|
|
10
10
|
import { logThought } from '../utils/logger.js';
|
|
11
|
-
import { runSimplifiedOnboarding } from './simplified-onboarding.js';
|
|
12
11
|
const rl = readline.createInterface({
|
|
13
12
|
input: process.stdin,
|
|
14
13
|
output: process.stdout,
|
|
@@ -70,7 +69,7 @@ const WIZARD_SECTIONS = [
|
|
|
70
69
|
{
|
|
71
70
|
id: 'models',
|
|
72
71
|
label: 'Intelligence & Models',
|
|
73
|
-
fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
|
|
72
|
+
fields: ['PRIMARY_MODEL', 'OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
|
|
74
73
|
},
|
|
75
74
|
{
|
|
76
75
|
id: 'messaging',
|
|
@@ -105,6 +104,18 @@ const ONBOARD_FIELDS = [
|
|
|
105
104
|
required: true,
|
|
106
105
|
secret: true,
|
|
107
106
|
},
|
|
107
|
+
{
|
|
108
|
+
key: 'PRIMARY_MODEL',
|
|
109
|
+
label: 'Primary Model',
|
|
110
|
+
hint: 'Required. Model to use as primary (e.g., openai/gpt-4o, anthropic/claude-sonnet-4-20250514).',
|
|
111
|
+
required: true,
|
|
112
|
+
validator: (value) => {
|
|
113
|
+
if (!value.includes('/')) {
|
|
114
|
+
return 'PRIMARY_MODEL must be in format "provider/model" (e.g., openai/gpt-4o).';
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
},
|
|
118
|
+
},
|
|
108
119
|
{
|
|
109
120
|
key: 'OPENROUTER_API_KEY',
|
|
110
121
|
label: 'OpenRouter API Key',
|
|
@@ -229,18 +240,52 @@ class ReadlinePrompter {
|
|
|
229
240
|
}
|
|
230
241
|
async prompt(question, options = {}) {
|
|
231
242
|
const isSecret = options.secret === true;
|
|
243
|
+
// For secret prompts, use a simpler approach without MutedWriter complexity
|
|
232
244
|
if (isSecret) {
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
this.#pendingReject = reject;
|
|
247
|
+
// Write question to stdout directly
|
|
248
|
+
process.stdout.write(question);
|
|
249
|
+
// Use raw mode to prevent echo
|
|
250
|
+
const originalRaw = process.stdin.isRaw;
|
|
251
|
+
process.stdin.setRawMode?.(true);
|
|
252
|
+
let input = '';
|
|
253
|
+
const onData = (chunk) => {
|
|
254
|
+
const char = chunk.toString();
|
|
255
|
+
if (char === '\r' || char === '\n') {
|
|
256
|
+
// Enter pressed - finish input
|
|
257
|
+
process.stdin.removeListener('data', onData);
|
|
258
|
+
process.stdin.setRawMode?.(originalRaw);
|
|
259
|
+
process.stdout.write('\n');
|
|
260
|
+
this.#pendingReject = null;
|
|
261
|
+
resolve(input.trim());
|
|
262
|
+
}
|
|
263
|
+
else if (char === '\u0003') {
|
|
264
|
+
// Ctrl+C
|
|
265
|
+
process.stdin.removeListener('data', onData);
|
|
266
|
+
process.stdin.setRawMode?.(originalRaw);
|
|
267
|
+
reject(new OnboardingCancelledError());
|
|
268
|
+
}
|
|
269
|
+
else if (char === '\u007f') {
|
|
270
|
+
// Backspace
|
|
271
|
+
if (input.length > 0) {
|
|
272
|
+
input = input.slice(0, -1);
|
|
273
|
+
process.stdout.write('\b \b');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (char.length === 1 && char.charCodeAt(0) >= 32) {
|
|
277
|
+
// Printable character
|
|
278
|
+
input += char;
|
|
279
|
+
process.stdout.write('*');
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
process.stdin.on('data', onData);
|
|
283
|
+
});
|
|
235
284
|
}
|
|
236
285
|
return new Promise((resolve, reject) => {
|
|
237
286
|
this.#pendingReject = reject;
|
|
238
|
-
this.#rl.question(
|
|
287
|
+
this.#rl.question(question, (answer) => {
|
|
239
288
|
this.#pendingReject = null;
|
|
240
|
-
this.#writer.muted = false;
|
|
241
|
-
if (isSecret) {
|
|
242
|
-
process.stdout.write('\n');
|
|
243
|
-
}
|
|
244
289
|
resolve(answer.trim());
|
|
245
290
|
});
|
|
246
291
|
});
|
|
@@ -278,6 +323,8 @@ function readConfigValue(config, key) {
|
|
|
278
323
|
return config.integration.embeddingProvider ?? '';
|
|
279
324
|
case 'API_PORT':
|
|
280
325
|
return String(config.runtime.apiPort ?? 3100);
|
|
326
|
+
case 'PRIMARY_MODEL':
|
|
327
|
+
return config.models.primaryModel ?? '';
|
|
281
328
|
}
|
|
282
329
|
}
|
|
283
330
|
function applyConfigValue(config, key, value) {
|
|
@@ -303,9 +350,23 @@ function applyConfigValue(config, key, value) {
|
|
|
303
350
|
case 'TELEGRAM_USER_ID':
|
|
304
351
|
config.messaging.telegram.userId =
|
|
305
352
|
value.trim().length === 0 ? null : Number.parseInt(value.trim(), 10);
|
|
353
|
+
// Also add to allowlist so user can message themselves
|
|
354
|
+
if (config.messaging.telegram.userId !== null) {
|
|
355
|
+
const userIdStr = String(config.messaging.telegram.userId);
|
|
356
|
+
if (!config.messaging.telegram.allowFrom.includes(userIdStr)) {
|
|
357
|
+
config.messaging.telegram.allowFrom.push(userIdStr);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
306
360
|
break;
|
|
307
361
|
case 'WHATSAPP_PHONE_NUMBER':
|
|
308
362
|
config.messaging.whatsapp.phoneNumber = value;
|
|
363
|
+
// Also add to allowlist so user can message themselves
|
|
364
|
+
if (value && value.trim().length > 0) {
|
|
365
|
+
const normalizedPhone = value.replace(/\D/g, ''); // Remove non-digits
|
|
366
|
+
if (!config.messaging.whatsapp.allowFrom.includes(normalizedPhone)) {
|
|
367
|
+
config.messaging.whatsapp.allowFrom.push(normalizedPhone);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
309
370
|
break;
|
|
310
371
|
case 'EMBEDDING_PROVIDER':
|
|
311
372
|
config.integration.embeddingProvider =
|
|
@@ -314,6 +375,9 @@ function applyConfigValue(config, key, value) {
|
|
|
314
375
|
case 'API_PORT':
|
|
315
376
|
config.runtime.apiPort = Number.parseInt(value, 10);
|
|
316
377
|
break;
|
|
378
|
+
case 'PRIMARY_MODEL':
|
|
379
|
+
config.models.primaryModel = value;
|
|
380
|
+
break;
|
|
317
381
|
}
|
|
318
382
|
}
|
|
319
383
|
function applyUpdates(config, updates) {
|
|
@@ -665,7 +729,7 @@ export async function handleOnboardCli(argv) {
|
|
|
665
729
|
if (argv[0] !== 'onboard') {
|
|
666
730
|
return false;
|
|
667
731
|
}
|
|
668
|
-
await
|
|
732
|
+
await runOnboarding();
|
|
669
733
|
return true;
|
|
670
734
|
}
|
|
671
735
|
export { runSimplifiedOnboarding } from './simplified-onboarding.js';
|
|
@@ -283,7 +283,7 @@ Multi-Modal Hooks | Proactive Memory | Voice-First
|
|
|
283
283
|
: [];
|
|
284
284
|
}
|
|
285
285
|
config.messaging = config.messaging || {
|
|
286
|
-
telegram: { enabled: false, botToken: '', userId: null, dmPolicy: '
|
|
286
|
+
telegram: { enabled: false, botToken: '', userId: null, dmPolicy: 'allowlist', groupPolicy: 'allowlist', allowFrom: [] },
|
|
287
287
|
whatsapp: { enabled: false, phoneNumber: '', dmPolicy: 'allowlist', groupPolicy: 'allowlist', allowFrom: [] },
|
|
288
288
|
voice: { groqApiKey: '' },
|
|
289
289
|
inbound: { enabled: false, debounceMs: 0 },
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
|
-
import { handleOnboardCli, runOnboarding, runSetupWizard,
|
|
4
|
+
import { handleOnboardCli, runOnboarding, runSetupWizard, startBasicREPL } from './core/onboarding.js';
|
|
5
5
|
import { Gateway } from './core/gateway.js';
|
|
6
6
|
import { handleDoctorCli, handleHelpCli, handleUnknownCommand, handleUpdateCli } from './core/cli.js';
|
|
7
7
|
import { handleSelfImproveCli } from './core/self-improve-cli.js';
|
|
@@ -111,7 +111,7 @@ async function tryAutoSetup() {
|
|
|
111
111
|
catch (error) {
|
|
112
112
|
console.log("\n[TwinClaw] Welcome! Critical configuration missing.");
|
|
113
113
|
console.log("Starting the interactive setup wizard to configure your agent.\n");
|
|
114
|
-
await
|
|
114
|
+
await runOnboarding();
|
|
115
115
|
await runSetupWizard();
|
|
116
116
|
console.log("\n[TwinClaw] Setup complete. Initializing Gateway...\n");
|
|
117
117
|
}
|
|
@@ -264,15 +264,24 @@ if (telegramBotToken || whatsappPhoneNumber) {
|
|
|
264
264
|
}
|
|
265
265
|
}, heartbeat.scheduler);
|
|
266
266
|
queueService.start();
|
|
267
|
+
// Read from environment variables or default to 'allowlist'
|
|
268
|
+
const telegramDmPolicy = (process.env.TELEGRAM_DM_POLICY || 'allowlist');
|
|
269
|
+
const whatsappDmPolicy = (process.env.WHATSAPP_DM_POLICY || 'allowlist');
|
|
270
|
+
const telegramGroupPolicy = (process.env.TELEGRAM_GROUP_POLICY || 'allowlist');
|
|
271
|
+
const whatsappGroupPolicy = (process.env.WHATSAPP_GROUP_POLICY || 'allowlist');
|
|
267
272
|
dispatcher = new Dispatcher(telegramHandler, whatsappHandler, sttService, ttsService, gateway, queueService, {
|
|
268
273
|
pairingService,
|
|
269
274
|
telegram: {
|
|
270
|
-
dmPolicy:
|
|
275
|
+
dmPolicy: telegramDmPolicy,
|
|
276
|
+
groupPolicy: telegramGroupPolicy,
|
|
271
277
|
allowFrom: telegramAllowFrom,
|
|
278
|
+
groupAllowFrom: telegramAllowFrom, // Use same allowlist for groups by default
|
|
272
279
|
},
|
|
273
280
|
whatsapp: {
|
|
274
|
-
dmPolicy:
|
|
281
|
+
dmPolicy: whatsappDmPolicy,
|
|
282
|
+
groupPolicy: whatsappGroupPolicy,
|
|
275
283
|
allowFrom: whatsappAllowFrom,
|
|
284
|
+
groupAllowFrom: whatsappAllowFrom, // Use same allowlist for groups by default
|
|
276
285
|
},
|
|
277
286
|
});
|
|
278
287
|
void logThought('[TwinClaw] Messaging dispatcher and persistent queue initialized.');
|
|
@@ -6,8 +6,17 @@ import { EmbeddedBlockChunker } from '../services/block-chunker.js';
|
|
|
6
6
|
import { getConfigValue } from '../config/config-loader.js';
|
|
7
7
|
const DEFAULT_ACCESS_CONFIG = {
|
|
8
8
|
dmPolicy: 'allowlist',
|
|
9
|
+
groupPolicy: 'allowlist',
|
|
9
10
|
allowFrom: [],
|
|
10
11
|
};
|
|
12
|
+
function buildGroupRejectionMessage(channel) {
|
|
13
|
+
return (`[TwinClaw] Your message from a group was not processed because group messages are not allowed for ${channel}.\n` +
|
|
14
|
+
`Please contact the bot owner to request access.`);
|
|
15
|
+
}
|
|
16
|
+
function buildAllowlistRejectionMessage(channel) {
|
|
17
|
+
return (`[TwinClaw] Your message was not processed because you are not on the allowed list for ${channel}.\n` +
|
|
18
|
+
`Please contact the bot owner to request access.`);
|
|
19
|
+
}
|
|
11
20
|
function buildPairingChallenge(channel, code) {
|
|
12
21
|
return (`[TwinClaw] Pairing required before I can process your messages on ${channel}.\n` +
|
|
13
22
|
`Run: twinclaw pairing approve ${channel} ${code}`);
|
|
@@ -113,12 +122,18 @@ export class Dispatcher {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
#resolveAccessConfig(channel, config) {
|
|
116
|
-
|
|
125
|
+
// Default to 'allowlist' - never default to 'pairing' for security
|
|
126
|
+
const dmPolicy = config?.dmPolicy === 'pairing' ? 'pairing' : 'allowlist';
|
|
127
|
+
const groupPolicy = config?.groupPolicy ?? 'allowlist';
|
|
117
128
|
const allowFrom = [...new Set((config?.allowFrom ?? DEFAULT_ACCESS_CONFIG.allowFrom)
|
|
118
129
|
.map((senderId) => normalizePairingSenderId(channel, senderId))
|
|
119
130
|
.filter((senderId) => senderId.length > 0))];
|
|
131
|
+
const groupAllowFrom = [...new Set((config?.groupAllowFrom ?? [])
|
|
132
|
+
.map((senderId) => normalizePairingSenderId(channel, senderId))
|
|
133
|
+
.filter((senderId) => senderId.length > 0))];
|
|
120
134
|
this.#pairingService.seedAllowFrom(channel, allowFrom);
|
|
121
|
-
|
|
135
|
+
this.#pairingService.seedAllowFrom(channel, groupAllowFrom);
|
|
136
|
+
return { dmPolicy, groupPolicy, allowFrom, groupAllowFrom };
|
|
122
137
|
}
|
|
123
138
|
#authorizeSender(message) {
|
|
124
139
|
const channel = message.platform;
|
|
@@ -127,12 +142,36 @@ export class Dispatcher {
|
|
|
127
142
|
return { allowed: false };
|
|
128
143
|
}
|
|
129
144
|
const config = this.#accessConfig[message.platform];
|
|
130
|
-
const
|
|
145
|
+
const isGroupChat = message.isGroupChat ?? false;
|
|
146
|
+
// Determine which allowlist to use based on group/dm
|
|
147
|
+
const allowlist = isGroupChat
|
|
148
|
+
? new Set(config.groupAllowFrom ?? [])
|
|
149
|
+
: new Set(config.allowFrom);
|
|
150
|
+
// Check if sender is already allowed
|
|
131
151
|
if (allowlist.has(normalizedSenderId) || this.#pairingService.isApproved(channel, normalizedSenderId)) {
|
|
132
152
|
return { allowed: true };
|
|
133
153
|
}
|
|
154
|
+
// Apply group policy if this is a group chat
|
|
155
|
+
if (isGroupChat) {
|
|
156
|
+
if (config.groupPolicy === 'open') {
|
|
157
|
+
return { allowed: true };
|
|
158
|
+
}
|
|
159
|
+
if (config.groupPolicy === 'denylist') {
|
|
160
|
+
// Check if sender is on denylist (for future implementation)
|
|
161
|
+
return { allowed: true }; // Allow by default for now, denylist check can be added later
|
|
162
|
+
}
|
|
163
|
+
// groupPolicy === 'allowlist' - sender not in groupAllowFrom, reject
|
|
164
|
+
return {
|
|
165
|
+
allowed: false,
|
|
166
|
+
challengeText: buildGroupRejectionMessage(channel),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// DM policy check for non-group chats
|
|
134
170
|
if (config.dmPolicy !== 'pairing') {
|
|
135
|
-
return {
|
|
171
|
+
return {
|
|
172
|
+
allowed: false,
|
|
173
|
+
challengeText: buildAllowlistRejectionMessage(channel),
|
|
174
|
+
};
|
|
136
175
|
}
|
|
137
176
|
const request = this.#pairingService.requestPairing(channel, normalizedSenderId);
|
|
138
177
|
if (request.status === 'created' && request.request) {
|
|
@@ -34,10 +34,13 @@ export class TelegramHandler {
|
|
|
34
34
|
if (!msg.from)
|
|
35
35
|
return;
|
|
36
36
|
await this.#applyRateLimit();
|
|
37
|
+
// Detect if this is a group chat (group or supergroup)
|
|
38
|
+
const isGroupChat = msg.chat.type === 'group' || msg.chat.type === 'supergroup';
|
|
37
39
|
const base = {
|
|
38
40
|
platform: 'telegram',
|
|
39
41
|
senderId: String(msg.from.id),
|
|
40
42
|
chatId: msg.chat.id,
|
|
43
|
+
isGroupChat,
|
|
41
44
|
text: msg.text,
|
|
42
45
|
rawPayload: msg,
|
|
43
46
|
};
|
|
@@ -54,10 +57,13 @@ export class TelegramHandler {
|
|
|
54
57
|
}
|
|
55
58
|
catch (err) {
|
|
56
59
|
console.error('[TelegramHandler] Failed to download voice note:', err);
|
|
60
|
+
// Continue to process text below instead of returning
|
|
57
61
|
}
|
|
58
|
-
return;
|
|
59
62
|
}
|
|
60
|
-
|
|
63
|
+
// Process text - this runs whether voice succeeded or failed
|
|
64
|
+
if (msg.text || base.text) {
|
|
65
|
+
await this.onMessage?.({ ...base, text: msg.text || base.text });
|
|
66
|
+
}
|
|
61
67
|
});
|
|
62
68
|
this.#bot.on('polling_error', (err) => {
|
|
63
69
|
console.error('[TelegramHandler] Polling error:', err.message);
|
|
@@ -116,10 +116,13 @@ export class WhatsAppHandler {
|
|
|
116
116
|
// Incoming messages
|
|
117
117
|
this.#client.on('message', async (msg) => {
|
|
118
118
|
await this.#applyRateLimit();
|
|
119
|
+
// Detect if this is a group chat (group chats have @g.us in the ID)
|
|
120
|
+
const isGroupChat = msg.from.includes('@g.us');
|
|
119
121
|
const base = {
|
|
120
122
|
platform: 'whatsapp',
|
|
121
123
|
senderId: msg.from,
|
|
122
124
|
chatId: msg.from,
|
|
125
|
+
isGroupChat,
|
|
123
126
|
text: msg.body,
|
|
124
127
|
rawPayload: msg,
|
|
125
128
|
};
|