twinclaw 1.1.2 → 1.1.4
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 +40 -0
- package/dist/core/gateway-cli.js +22 -1
- package/dist/core/onboarding.js +73 -8
- package/dist/core/simplified-onboarding.js +1 -1
- package/dist/index.js +17 -5
- 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 = `
|
|
@@ -12,6 +14,7 @@ Commands:
|
|
|
12
14
|
channels <subcmd> Manage messaging channels (e.g. login)
|
|
13
15
|
gateway <subcmd> Manage the TwinClaw background service daemon
|
|
14
16
|
logs [--follow] Stream daily structured memory logs from the daemon
|
|
17
|
+
update Check for and install the latest TwinClaw version
|
|
15
18
|
--onboard Run the interactive AI persona-building session
|
|
16
19
|
|
|
17
20
|
Options:
|
|
@@ -98,6 +101,7 @@ export function handleUnknownCommand(argv) {
|
|
|
98
101
|
'channels',
|
|
99
102
|
'gateway',
|
|
100
103
|
'logs',
|
|
104
|
+
'update',
|
|
101
105
|
'--onboard',
|
|
102
106
|
'--help',
|
|
103
107
|
'-h',
|
|
@@ -111,3 +115,39 @@ export function handleUnknownCommand(argv) {
|
|
|
111
115
|
process.exitCode = 1;
|
|
112
116
|
return true;
|
|
113
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Handle the `update` command.
|
|
120
|
+
* Checks for and installs the latest TwinClaw version from npm.
|
|
121
|
+
* Returns `true` when the command was recognized and handled.
|
|
122
|
+
*/
|
|
123
|
+
export function handleUpdateCli(argv) {
|
|
124
|
+
if (argv[0] !== 'update')
|
|
125
|
+
return false;
|
|
126
|
+
console.log('[TwinClaw] Checking for updates...');
|
|
127
|
+
try {
|
|
128
|
+
// Get current version
|
|
129
|
+
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
|
|
130
|
+
const currentVersion = packageJson.version;
|
|
131
|
+
console.log(`[TwinClaw] Current version: ${currentVersion}`);
|
|
132
|
+
// Fetch latest version from npm
|
|
133
|
+
const latestVersion = execSync('npm view twinclaw version', { encoding: 'utf8' }).trim();
|
|
134
|
+
console.log(`[TwinClaw] Latest version: ${latestVersion}`);
|
|
135
|
+
if (currentVersion === latestVersion) {
|
|
136
|
+
console.log('[TwinClaw] You are already on the latest version.');
|
|
137
|
+
process.exitCode = 0;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
console.log('[TwinClaw] Update available! Installing...');
|
|
141
|
+
// Install latest version globally (--yes to skip prompts)
|
|
142
|
+
execSync('npm install -g twinclaw@latest --yes', { stdio: 'inherit' });
|
|
143
|
+
console.log(`[TwinClaw] Successfully updated to version ${latestVersion}`);
|
|
144
|
+
console.log('[TwinClaw] Please restart TwinClaw to use the new version.');
|
|
145
|
+
process.exitCode = 0;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
console.error(`[TwinClaw] Update failed: ${message}`);
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
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
|
@@ -70,7 +70,7 @@ const WIZARD_SECTIONS = [
|
|
|
70
70
|
{
|
|
71
71
|
id: 'models',
|
|
72
72
|
label: 'Intelligence & Models',
|
|
73
|
-
fields: ['OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
|
|
73
|
+
fields: ['PRIMARY_MODEL', 'OPENROUTER_API_KEY', 'MODAL_API_KEY', 'GEMINI_API_KEY', 'GROQ_API_KEY'],
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
id: 'messaging',
|
|
@@ -105,6 +105,18 @@ const ONBOARD_FIELDS = [
|
|
|
105
105
|
required: true,
|
|
106
106
|
secret: true,
|
|
107
107
|
},
|
|
108
|
+
{
|
|
109
|
+
key: 'PRIMARY_MODEL',
|
|
110
|
+
label: 'Primary Model',
|
|
111
|
+
hint: 'Required. Model to use as primary (e.g., openai/gpt-4o, anthropic/claude-sonnet-4-20250514).',
|
|
112
|
+
required: true,
|
|
113
|
+
validator: (value) => {
|
|
114
|
+
if (!value.includes('/')) {
|
|
115
|
+
return 'PRIMARY_MODEL must be in format "provider/model" (e.g., openai/gpt-4o).';
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
108
120
|
{
|
|
109
121
|
key: 'OPENROUTER_API_KEY',
|
|
110
122
|
label: 'OpenRouter API Key',
|
|
@@ -229,18 +241,52 @@ class ReadlinePrompter {
|
|
|
229
241
|
}
|
|
230
242
|
async prompt(question, options = {}) {
|
|
231
243
|
const isSecret = options.secret === true;
|
|
244
|
+
// For secret prompts, use a simpler approach without MutedWriter complexity
|
|
232
245
|
if (isSecret) {
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
this.#pendingReject = reject;
|
|
248
|
+
// Write question to stdout directly
|
|
249
|
+
process.stdout.write(question);
|
|
250
|
+
// Use raw mode to prevent echo
|
|
251
|
+
const originalRaw = process.stdin.isRaw;
|
|
252
|
+
process.stdin.setRawMode?.(true);
|
|
253
|
+
let input = '';
|
|
254
|
+
const onData = (chunk) => {
|
|
255
|
+
const char = chunk.toString();
|
|
256
|
+
if (char === '\r' || char === '\n') {
|
|
257
|
+
// Enter pressed - finish input
|
|
258
|
+
process.stdin.removeListener('data', onData);
|
|
259
|
+
process.stdin.setRawMode?.(originalRaw);
|
|
260
|
+
process.stdout.write('\n');
|
|
261
|
+
this.#pendingReject = null;
|
|
262
|
+
resolve(input.trim());
|
|
263
|
+
}
|
|
264
|
+
else if (char === '\u0003') {
|
|
265
|
+
// Ctrl+C
|
|
266
|
+
process.stdin.removeListener('data', onData);
|
|
267
|
+
process.stdin.setRawMode?.(originalRaw);
|
|
268
|
+
reject(new OnboardingCancelledError());
|
|
269
|
+
}
|
|
270
|
+
else if (char === '\u007f') {
|
|
271
|
+
// Backspace
|
|
272
|
+
if (input.length > 0) {
|
|
273
|
+
input = input.slice(0, -1);
|
|
274
|
+
process.stdout.write('\b \b');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (char.length === 1 && char.charCodeAt(0) >= 32) {
|
|
278
|
+
// Printable character
|
|
279
|
+
input += char;
|
|
280
|
+
process.stdout.write('*');
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
process.stdin.on('data', onData);
|
|
284
|
+
});
|
|
235
285
|
}
|
|
236
286
|
return new Promise((resolve, reject) => {
|
|
237
287
|
this.#pendingReject = reject;
|
|
238
|
-
this.#rl.question(
|
|
288
|
+
this.#rl.question(question, (answer) => {
|
|
239
289
|
this.#pendingReject = null;
|
|
240
|
-
this.#writer.muted = false;
|
|
241
|
-
if (isSecret) {
|
|
242
|
-
process.stdout.write('\n');
|
|
243
|
-
}
|
|
244
290
|
resolve(answer.trim());
|
|
245
291
|
});
|
|
246
292
|
});
|
|
@@ -278,6 +324,8 @@ function readConfigValue(config, key) {
|
|
|
278
324
|
return config.integration.embeddingProvider ?? '';
|
|
279
325
|
case 'API_PORT':
|
|
280
326
|
return String(config.runtime.apiPort ?? 3100);
|
|
327
|
+
case 'PRIMARY_MODEL':
|
|
328
|
+
return config.models.primaryModel ?? '';
|
|
281
329
|
}
|
|
282
330
|
}
|
|
283
331
|
function applyConfigValue(config, key, value) {
|
|
@@ -303,9 +351,23 @@ function applyConfigValue(config, key, value) {
|
|
|
303
351
|
case 'TELEGRAM_USER_ID':
|
|
304
352
|
config.messaging.telegram.userId =
|
|
305
353
|
value.trim().length === 0 ? null : Number.parseInt(value.trim(), 10);
|
|
354
|
+
// Also add to allowlist so user can message themselves
|
|
355
|
+
if (config.messaging.telegram.userId !== null) {
|
|
356
|
+
const userIdStr = String(config.messaging.telegram.userId);
|
|
357
|
+
if (!config.messaging.telegram.allowFrom.includes(userIdStr)) {
|
|
358
|
+
config.messaging.telegram.allowFrom.push(userIdStr);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
306
361
|
break;
|
|
307
362
|
case 'WHATSAPP_PHONE_NUMBER':
|
|
308
363
|
config.messaging.whatsapp.phoneNumber = value;
|
|
364
|
+
// Also add to allowlist so user can message themselves
|
|
365
|
+
if (value && value.trim().length > 0) {
|
|
366
|
+
const normalizedPhone = value.replace(/\D/g, ''); // Remove non-digits
|
|
367
|
+
if (!config.messaging.whatsapp.allowFrom.includes(normalizedPhone)) {
|
|
368
|
+
config.messaging.whatsapp.allowFrom.push(normalizedPhone);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
309
371
|
break;
|
|
310
372
|
case 'EMBEDDING_PROVIDER':
|
|
311
373
|
config.integration.embeddingProvider =
|
|
@@ -314,6 +376,9 @@ function applyConfigValue(config, key, value) {
|
|
|
314
376
|
case 'API_PORT':
|
|
315
377
|
config.runtime.apiPort = Number.parseInt(value, 10);
|
|
316
378
|
break;
|
|
379
|
+
case 'PRIMARY_MODEL':
|
|
380
|
+
config.models.primaryModel = value;
|
|
381
|
+
break;
|
|
317
382
|
}
|
|
318
383
|
}
|
|
319
384
|
function applyUpdates(config, updates) {
|
|
@@ -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,9 +1,9 @@
|
|
|
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
|
-
import { handleDoctorCli, handleHelpCli, handleUnknownCommand } from './core/cli.js';
|
|
6
|
+
import { handleDoctorCli, handleHelpCli, handleUnknownCommand, handleUpdateCli } from './core/cli.js';
|
|
7
7
|
import { handleSelfImproveCli } from './core/self-improve-cli.js';
|
|
8
8
|
import { HeartbeatService } from './core/heartbeat.js';
|
|
9
9
|
import { Dispatcher } from './interfaces/dispatcher.js';
|
|
@@ -58,6 +58,9 @@ if (await handleSelfImproveCli(process.argv.slice(2))) {
|
|
|
58
58
|
if (handleDoctorCli(process.argv.slice(2))) {
|
|
59
59
|
process.exit(process.exitCode ?? 0);
|
|
60
60
|
}
|
|
61
|
+
if (handleUpdateCli(process.argv.slice(2))) {
|
|
62
|
+
process.exit(process.exitCode ?? 0);
|
|
63
|
+
}
|
|
61
64
|
if (handleSecretVaultCli(process.argv.slice(2), secretVault)) {
|
|
62
65
|
process.exit(process.exitCode ?? 0);
|
|
63
66
|
}
|
|
@@ -108,7 +111,7 @@ async function tryAutoSetup() {
|
|
|
108
111
|
catch (error) {
|
|
109
112
|
console.log("\n[TwinClaw] Welcome! Critical configuration missing.");
|
|
110
113
|
console.log("Starting the interactive setup wizard to configure your agent.\n");
|
|
111
|
-
await
|
|
114
|
+
await runOnboarding();
|
|
112
115
|
await runSetupWizard();
|
|
113
116
|
console.log("\n[TwinClaw] Setup complete. Initializing Gateway...\n");
|
|
114
117
|
}
|
|
@@ -261,15 +264,24 @@ if (telegramBotToken || whatsappPhoneNumber) {
|
|
|
261
264
|
}
|
|
262
265
|
}, heartbeat.scheduler);
|
|
263
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');
|
|
264
272
|
dispatcher = new Dispatcher(telegramHandler, whatsappHandler, sttService, ttsService, gateway, queueService, {
|
|
265
273
|
pairingService,
|
|
266
274
|
telegram: {
|
|
267
|
-
dmPolicy:
|
|
275
|
+
dmPolicy: telegramDmPolicy,
|
|
276
|
+
groupPolicy: telegramGroupPolicy,
|
|
268
277
|
allowFrom: telegramAllowFrom,
|
|
278
|
+
groupAllowFrom: telegramAllowFrom, // Use same allowlist for groups by default
|
|
269
279
|
},
|
|
270
280
|
whatsapp: {
|
|
271
|
-
dmPolicy:
|
|
281
|
+
dmPolicy: whatsappDmPolicy,
|
|
282
|
+
groupPolicy: whatsappGroupPolicy,
|
|
272
283
|
allowFrom: whatsappAllowFrom,
|
|
284
|
+
groupAllowFrom: whatsappAllowFrom, // Use same allowlist for groups by default
|
|
273
285
|
},
|
|
274
286
|
});
|
|
275
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
|
};
|