@plosson/agentio 0.4.2 → 0.4.3

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.
@@ -0,0 +1,853 @@
1
+ import { Command } from 'commander';
2
+ import { setCredentials, getCredentials } from '../auth/token-store';
3
+ import { setProfile, resolveProfile, removeProfile } from '../config/config-manager';
4
+ import { CliError, handleError } from '../utils/errors';
5
+ import { readStdin, prompt, confirm } from '../utils/stdin';
6
+ import { getGatewayClient, isGatewayAvailable } from '../gateway/client';
7
+ import {
8
+ printInboxMessageList,
9
+ printInboxMessage,
10
+ printInboxStats,
11
+ printInboxAckResult,
12
+ printInboxReplyResult,
13
+ printOutboxMessageList,
14
+ printOutboxMessage,
15
+ printOutboxSendResult,
16
+ printWhatsAppGroupList,
17
+ printWhatsAppGroup,
18
+ printWhatsAppGroupCreated,
19
+ printWhatsAppGroupInvite,
20
+ printWhatsAppGroupJoined,
21
+ printWhatsAppGroupLeft,
22
+ printWhatsAppParticipantsResult,
23
+ } from '../utils/output';
24
+ import type { WhatsAppCredentials } from '../types/whatsapp';
25
+ import qrcode from 'qrcode-terminal';
26
+
27
+ /**
28
+ * Run the WhatsApp pairing flow - polls for QR code until connected
29
+ */
30
+ async function runPairingFlow(profileName: string): Promise<boolean> {
31
+ const client = await getGatewayClient();
32
+ let lastQr = '';
33
+
34
+ console.log('\nWaiting for QR code... (Ctrl+C to cancel)\n');
35
+
36
+ while (true) {
37
+ const result = await client.whatsappPair(profileName);
38
+
39
+ switch (result.status) {
40
+ case 'connected':
41
+ console.log(`\nConnected to WhatsApp!`);
42
+ if (result.phoneNumber) {
43
+ console.log(`Phone: ${result.phoneNumber}`);
44
+ }
45
+ if (result.displayName) {
46
+ console.log(`Name: ${result.displayName}`);
47
+ }
48
+ return true;
49
+
50
+ case 'waiting_qr':
51
+ if (result.qrCode && result.qrCode !== lastQr) {
52
+ lastQr = result.qrCode;
53
+ console.clear();
54
+ console.log('\nScan this QR code with WhatsApp on your phone:\n');
55
+ console.log('1. Open WhatsApp on your phone');
56
+ console.log('2. Tap Menu or Settings');
57
+ console.log('3. Tap "Linked Devices"');
58
+ console.log('4. Tap "Link a Device"');
59
+ console.log('5. Point your phone at this screen\n');
60
+ qrcode.generate(result.qrCode, { small: true });
61
+ console.log('\nWaiting for scan...');
62
+ }
63
+ break;
64
+
65
+ case 'connecting':
66
+ console.error(`Status: ${result.message || 'Connecting...'}`);
67
+ break;
68
+
69
+ case 'not_configured':
70
+ throw new CliError('CONFIG_ERROR', result.message || 'WhatsApp not configured');
71
+ }
72
+
73
+ await new Promise(resolve => setTimeout(resolve, 2000));
74
+ }
75
+ }
76
+
77
+ export function registerWhatsAppCommands(program: Command): void {
78
+ const whatsapp = program
79
+ .command('whatsapp')
80
+ .description('WhatsApp operations (requires gateway)');
81
+
82
+ // Profile management
83
+ const profile = whatsapp.command('profile').description('Manage WhatsApp profiles');
84
+
85
+ profile
86
+ .command('add')
87
+ .description('Add a new WhatsApp profile and pair via QR code')
88
+ .option('--profile <name>', 'Profile name')
89
+ .action(async (options) => {
90
+ try {
91
+ console.error('\nWhatsApp Profile Setup\n');
92
+
93
+ const profileName = options.profile || await prompt('? Profile name: ');
94
+
95
+ if (!profileName) {
96
+ throw new CliError('INVALID_PARAMS', 'Profile name is required');
97
+ }
98
+
99
+ // Check if profile already exists
100
+ const existing = await getCredentials<WhatsAppCredentials>('whatsapp', profileName);
101
+ if (existing?.paired) {
102
+ const overwrite = await confirm(`Profile "${profileName}" already exists and is paired. Overwrite?`);
103
+ if (!overwrite) {
104
+ console.log('Cancelled');
105
+ return;
106
+ }
107
+ }
108
+
109
+ // Create initial credentials (not yet paired)
110
+ const credentials: WhatsAppCredentials = {
111
+ paired: false,
112
+ };
113
+
114
+ await setProfile('whatsapp', profileName);
115
+ await setCredentials('whatsapp', profileName, credentials);
116
+
117
+ console.log(`Profile "${profileName}" created.`);
118
+
119
+ // Check if gateway is running
120
+ const gatewayRunning = await isGatewayAvailable();
121
+ if (!gatewayRunning) {
122
+ console.log('\nGateway is not running.');
123
+ console.log('Start the gateway first, then run this command again:');
124
+ console.log(' agentio gateway start');
125
+ console.log(` agentio whatsapp profile add --profile ${profileName}`);
126
+ return;
127
+ }
128
+
129
+ // Gateway is running - proceed with pairing
130
+ await runPairingFlow(profileName);
131
+
132
+ console.log(`\nProfile "${profileName}" is ready to use.`);
133
+ console.log(`Try: agentio whatsapp inbox pull --profile ${profileName}`);
134
+ } catch (error) {
135
+ handleError(error);
136
+ }
137
+ });
138
+
139
+ profile
140
+ .command('list')
141
+ .description('List WhatsApp profiles')
142
+ .action(async () => {
143
+ try {
144
+ const { profiles } = await import('../config/config-manager').then(m => m.listProfiles('whatsapp').then(r => r[0]));
145
+
146
+ if (profiles.length === 0) {
147
+ console.log('No WhatsApp profiles configured');
148
+ console.log('Run: agentio whatsapp profile add');
149
+ return;
150
+ }
151
+
152
+ console.log(`WhatsApp profiles (${profiles.length}):\n`);
153
+
154
+ for (const name of profiles) {
155
+ const creds = await getCredentials<WhatsAppCredentials>('whatsapp', name);
156
+ const status = creds?.paired ? 'paired' : 'not paired';
157
+ const phone = creds?.phoneNumber ? ` (${creds.phoneNumber})` : '';
158
+ const displayName = creds?.displayName ? ` - ${creds.displayName}` : '';
159
+ console.log(` ${name}${phone}${displayName} [${status}]`);
160
+ }
161
+ } catch (error) {
162
+ handleError(error);
163
+ }
164
+ });
165
+
166
+ profile
167
+ .command('remove')
168
+ .description('Remove a WhatsApp profile')
169
+ .option('--profile <name>', 'Profile name to remove')
170
+ .action(async (options) => {
171
+ try {
172
+ const profileResult = await resolveProfile('whatsapp', options.profile);
173
+ if (!profileResult.profile) {
174
+ if (profileResult.error === 'none') {
175
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured');
176
+ }
177
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
178
+ }
179
+
180
+ const profileName = profileResult.profile;
181
+ const confirmed = await confirm(`Remove profile "${profileName}"? This will delete the session.`);
182
+
183
+ if (!confirmed) {
184
+ console.log('Cancelled');
185
+ return;
186
+ }
187
+
188
+ await removeProfile('whatsapp', profileName);
189
+ // Note: Auth state in gateway DB will be cleaned up when gateway restarts
190
+ // or we could add a direct cleanup call here
191
+
192
+ console.log(`Profile "${profileName}" removed`);
193
+ console.log('Note: Restart the gateway to fully disconnect this session.');
194
+ } catch (error) {
195
+ handleError(error);
196
+ }
197
+ });
198
+
199
+ // Inbox subcommands (requires gateway)
200
+ const inbox = whatsapp.command('inbox').description('Inbox operations (requires gateway)');
201
+
202
+ inbox
203
+ .command('pull')
204
+ .description('Get pending messages from inbox')
205
+ .option('--profile <name>', 'Profile name')
206
+ .option('--limit <n>', 'Maximum messages to retrieve', '50')
207
+ .option('--status <status>', 'Filter by status: pending or done', 'pending')
208
+ .option('--conversation <id>', 'Filter by conversation/group (name or JID)')
209
+ .action(async (options) => {
210
+ try {
211
+ const profileResult = await resolveProfile('whatsapp', options.profile);
212
+ if (!profileResult.profile) {
213
+ if (profileResult.error === 'none') {
214
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
215
+ }
216
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
217
+ }
218
+
219
+ const client = await getGatewayClient();
220
+
221
+ // Resolve conversation name to JID if needed
222
+ let conversationId = options.conversation;
223
+ if (conversationId && !conversationId.includes('@')) {
224
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, conversationId);
225
+ if (resolved.groupId) {
226
+ conversationId = resolved.groupId;
227
+ }
228
+ // If not found as group, keep original (might be a phone number)
229
+ }
230
+
231
+ const messages = await client.inboxPull({
232
+ service: 'whatsapp',
233
+ profile: profileResult.profile,
234
+ conversationId,
235
+ limit: parseInt(options.limit, 10),
236
+ status: options.status as 'pending' | 'done',
237
+ });
238
+ printInboxMessageList(messages);
239
+ } catch (error) {
240
+ handleError(error);
241
+ }
242
+ });
243
+
244
+ inbox
245
+ .command('get')
246
+ .description('Get a specific inbox message')
247
+ .argument('<id>', 'Message ID')
248
+ .action(async (id: string) => {
249
+ try {
250
+ const client = await getGatewayClient();
251
+ const message = await client.inboxGet(id);
252
+ if (!message) {
253
+ throw new CliError('NOT_FOUND', `Message not found: ${id}`);
254
+ }
255
+ printInboxMessage(message);
256
+ } catch (error) {
257
+ handleError(error);
258
+ }
259
+ });
260
+
261
+ inbox
262
+ .command('ack')
263
+ .description('Mark a message as done')
264
+ .argument('<id>', 'Message ID')
265
+ .action(async (id: string) => {
266
+ try {
267
+ const client = await getGatewayClient();
268
+ const success = await client.inboxAck(id);
269
+ printInboxAckResult(success, id);
270
+ } catch (error) {
271
+ handleError(error);
272
+ }
273
+ });
274
+
275
+ inbox
276
+ .command('reply')
277
+ .description('Reply to an inbox message')
278
+ .argument('<id>', 'Message ID to reply to')
279
+ .argument('[message]', 'Reply text (or pipe via stdin)')
280
+ .action(async (id: string, message: string | undefined) => {
281
+ try {
282
+ let text = message;
283
+ if (!text) {
284
+ text = await readStdin() || undefined;
285
+ }
286
+ if (!text) {
287
+ throw new CliError('INVALID_PARAMS', 'Message is required. Provide as argument or pipe via stdin.');
288
+ }
289
+
290
+ const client = await getGatewayClient();
291
+ const result = await client.inboxReply(id, text);
292
+ printInboxReplyResult(result);
293
+ } catch (error) {
294
+ handleError(error);
295
+ }
296
+ });
297
+
298
+ inbox
299
+ .command('stats')
300
+ .description('Get inbox statistics')
301
+ .option('--profile <name>', 'Profile name')
302
+ .action(async (options) => {
303
+ try {
304
+ const profileResult = await resolveProfile('whatsapp', options.profile);
305
+ const client = await getGatewayClient();
306
+ const stats = await client.inboxStats({
307
+ service: 'whatsapp',
308
+ profile: profileResult.profile ?? undefined,
309
+ });
310
+ printInboxStats(stats);
311
+ } catch (error) {
312
+ handleError(error);
313
+ }
314
+ });
315
+
316
+ // Outbox subcommands (requires gateway)
317
+ const outbox = whatsapp.command('outbox').description('Outbox operations (requires gateway)');
318
+
319
+ outbox
320
+ .command('send')
321
+ .description('Queue a message for sending')
322
+ .option('--profile <name>', 'Profile name')
323
+ .option('--to <phone>', 'Destination phone number (with country code, e.g., +1234567890)')
324
+ .option('--group <name>', 'Destination group (name or JID)')
325
+ .option('--attachment <path>', 'Path to file attachment (image, video, audio, or document)')
326
+ .option('--type <type>', 'Media type: image, video, audio, document (auto-detected if not specified)')
327
+ .argument('[message]', 'Message text or caption (or pipe via stdin)')
328
+ .action(async (message: string | undefined, options) => {
329
+ try {
330
+ const profileResult = await resolveProfile('whatsapp', options.profile);
331
+ if (!profileResult.profile) {
332
+ if (profileResult.error === 'none') {
333
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
334
+ }
335
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
336
+ }
337
+
338
+ if (!options.to && !options.group) {
339
+ throw new CliError('INVALID_PARAMS', 'Destination required. Use --to <phone> or --group <name>');
340
+ }
341
+
342
+ if (options.to && options.group) {
343
+ throw new CliError('INVALID_PARAMS', 'Use either --to or --group, not both.');
344
+ }
345
+
346
+ let text = message;
347
+ if (!text) {
348
+ text = await readStdin() || undefined;
349
+ }
350
+
351
+ // Validate: need either text or attachment
352
+ if (!text && !options.attachment) {
353
+ throw new CliError('INVALID_PARAMS', 'Message or attachment is required.');
354
+ }
355
+
356
+ // Auto-detect media type from file extension if not specified
357
+ let mediaType = options.type as 'image' | 'video' | 'audio' | 'document' | undefined;
358
+ if (options.attachment && !mediaType) {
359
+ const ext = options.attachment.toLowerCase().split('.').pop();
360
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext || '')) {
361
+ mediaType = 'image';
362
+ } else if (['mp4', 'mov', 'avi', 'mkv', 'webm'].includes(ext || '')) {
363
+ mediaType = 'video';
364
+ } else if (['mp3', 'ogg', 'wav', 'm4a', 'opus'].includes(ext || '')) {
365
+ mediaType = 'audio';
366
+ } else {
367
+ mediaType = 'document';
368
+ }
369
+ }
370
+
371
+ const client = await getGatewayClient();
372
+
373
+ // Resolve destination
374
+ let conversationId = options.to;
375
+ if (options.group) {
376
+ // Resolve group name to JID
377
+ if (options.group.includes('@g.us')) {
378
+ conversationId = options.group;
379
+ } else {
380
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, options.group);
381
+ if (!resolved.groupId) {
382
+ throw new CliError('NOT_FOUND', `Group not found: ${options.group}`, 'Run: agentio whatsapp group list');
383
+ }
384
+ conversationId = resolved.groupId;
385
+ console.error(`Resolved group "${options.group}" to ${resolved.groupId}`);
386
+ }
387
+ }
388
+
389
+ const result = await client.outboxSend({
390
+ service: 'whatsapp',
391
+ profile: profileResult.profile,
392
+ conversationId,
393
+ content: text,
394
+ mediaPath: options.attachment,
395
+ mediaType,
396
+ });
397
+ printOutboxSendResult(result);
398
+ } catch (error) {
399
+ handleError(error);
400
+ }
401
+ });
402
+
403
+ outbox
404
+ .command('status')
405
+ .description('Check send status of a message')
406
+ .argument('<id>', 'Outbox message ID')
407
+ .action(async (id: string) => {
408
+ try {
409
+ const client = await getGatewayClient();
410
+ const message = await client.outboxStatus(id);
411
+ if (!message) {
412
+ throw new CliError('NOT_FOUND', `Message not found: ${id}`);
413
+ }
414
+ printOutboxMessage(message);
415
+ } catch (error) {
416
+ handleError(error);
417
+ }
418
+ });
419
+
420
+ outbox
421
+ .command('list')
422
+ .description('List outbox messages')
423
+ .option('--profile <name>', 'Profile name')
424
+ .option('--status <status>', 'Filter by status: pending, sending, sent, or failed')
425
+ .option('--limit <n>', 'Maximum messages to retrieve', '50')
426
+ .action(async (options) => {
427
+ try {
428
+ const profileResult = await resolveProfile('whatsapp', options.profile);
429
+ const client = await getGatewayClient();
430
+ const messages = await client.outboxList({
431
+ service: 'whatsapp',
432
+ profile: profileResult.profile ?? undefined,
433
+ status: options.status as 'pending' | 'sending' | 'sent' | 'failed' | undefined,
434
+ limit: parseInt(options.limit, 10),
435
+ });
436
+ printOutboxMessageList(messages);
437
+ } catch (error) {
438
+ handleError(error);
439
+ }
440
+ });
441
+
442
+ // Group subcommands
443
+ const group = whatsapp.command('group').description('Group management (requires gateway)');
444
+
445
+ group
446
+ .command('list')
447
+ .description('List all groups')
448
+ .option('--profile <name>', 'Profile name')
449
+ .action(async (options) => {
450
+ try {
451
+ const profileResult = await resolveProfile('whatsapp', options.profile);
452
+ if (!profileResult.profile) {
453
+ if (profileResult.error === 'none') {
454
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
455
+ }
456
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
457
+ }
458
+
459
+ const client = await getGatewayClient();
460
+ const groups = await client.whatsappGroupList(profileResult.profile);
461
+ printWhatsAppGroupList(groups);
462
+ } catch (error) {
463
+ handleError(error);
464
+ }
465
+ });
466
+
467
+ group
468
+ .command('get')
469
+ .description('Get group details')
470
+ .argument('<id>', 'Group ID or name')
471
+ .option('--profile <name>', 'Profile name')
472
+ .action(async (id: string, options) => {
473
+ try {
474
+ const profileResult = await resolveProfile('whatsapp', options.profile);
475
+ if (!profileResult.profile) {
476
+ if (profileResult.error === 'none') {
477
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
478
+ }
479
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
480
+ }
481
+
482
+ const client = await getGatewayClient();
483
+
484
+ // Resolve name to ID if needed
485
+ let groupId = id;
486
+ if (!id.includes('@g.us')) {
487
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
488
+ if (!resolved.groupId) {
489
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
490
+ }
491
+ groupId = resolved.groupId;
492
+ }
493
+
494
+ const group = await client.whatsappGroupGet(profileResult.profile, groupId);
495
+ printWhatsAppGroup(group);
496
+ } catch (error) {
497
+ handleError(error);
498
+ }
499
+ });
500
+
501
+ group
502
+ .command('create')
503
+ .description('Create a new group')
504
+ .argument('<name>', 'Group name')
505
+ .option('--profile <name>', 'Profile name')
506
+ .option('--participants <phones...>', 'Participant phone numbers')
507
+ .option('--picture <path>', 'Path to group profile picture')
508
+ .action(async (name: string, options) => {
509
+ try {
510
+ const profileResult = await resolveProfile('whatsapp', options.profile);
511
+ if (!profileResult.profile) {
512
+ if (profileResult.error === 'none') {
513
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
514
+ }
515
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
516
+ }
517
+
518
+ if (!options.participants || options.participants.length === 0) {
519
+ throw new CliError('INVALID_PARAMS', 'At least one participant is required. Use --participants <phone...>');
520
+ }
521
+
522
+ const client = await getGatewayClient();
523
+ const group = await client.whatsappGroupCreate(
524
+ profileResult.profile,
525
+ name,
526
+ options.participants,
527
+ options.picture
528
+ );
529
+ printWhatsAppGroupCreated(group);
530
+ } catch (error) {
531
+ handleError(error);
532
+ }
533
+ });
534
+
535
+ group
536
+ .command('update')
537
+ .description('Update group info')
538
+ .argument('<id>', 'Group ID or name')
539
+ .option('--profile <name>', 'Profile name')
540
+ .option('--name <name>', 'New group name')
541
+ .option('--description <text>', 'New group description')
542
+ .option('--picture <path>', 'Path to new group profile picture')
543
+ .action(async (id: string, options) => {
544
+ try {
545
+ const profileResult = await resolveProfile('whatsapp', options.profile);
546
+ if (!profileResult.profile) {
547
+ if (profileResult.error === 'none') {
548
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
549
+ }
550
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
551
+ }
552
+
553
+ if (!options.name && options.description === undefined && !options.picture) {
554
+ throw new CliError('INVALID_PARAMS', 'Provide --name, --description, or --picture to update.');
555
+ }
556
+
557
+ const client = await getGatewayClient();
558
+
559
+ // Resolve name to ID if needed
560
+ let groupId = id;
561
+ if (!id.includes('@g.us')) {
562
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
563
+ if (!resolved.groupId) {
564
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
565
+ }
566
+ groupId = resolved.groupId;
567
+ }
568
+
569
+ await client.whatsappGroupUpdate(profileResult.profile, groupId, {
570
+ subject: options.name,
571
+ description: options.description,
572
+ picture: options.picture,
573
+ });
574
+ console.log('Group updated');
575
+ } catch (error) {
576
+ handleError(error);
577
+ }
578
+ });
579
+
580
+ group
581
+ .command('add')
582
+ .description('Add participants to group')
583
+ .argument('<id>', 'Group ID or name')
584
+ .argument('<phones...>', 'Phone numbers to add')
585
+ .option('--profile <name>', 'Profile name')
586
+ .action(async (id: string, phones: string[], options) => {
587
+ try {
588
+ const profileResult = await resolveProfile('whatsapp', options.profile);
589
+ if (!profileResult.profile) {
590
+ if (profileResult.error === 'none') {
591
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
592
+ }
593
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
594
+ }
595
+
596
+ const client = await getGatewayClient();
597
+
598
+ // Resolve name to ID if needed
599
+ let groupId = id;
600
+ if (!id.includes('@g.us')) {
601
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
602
+ if (!resolved.groupId) {
603
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
604
+ }
605
+ groupId = resolved.groupId;
606
+ }
607
+
608
+ const result = await client.whatsappGroupParticipants(
609
+ profileResult.profile,
610
+ groupId,
611
+ phones,
612
+ 'add'
613
+ );
614
+ if (result.results) {
615
+ printWhatsAppParticipantsResult('added', result.results);
616
+ } else {
617
+ console.log('Participants added');
618
+ }
619
+ } catch (error) {
620
+ handleError(error);
621
+ }
622
+ });
623
+
624
+ group
625
+ .command('remove')
626
+ .description('Remove participants from group')
627
+ .argument('<id>', 'Group ID or name')
628
+ .argument('<phones...>', 'Phone numbers to remove')
629
+ .option('--profile <name>', 'Profile name')
630
+ .action(async (id: string, phones: string[], options) => {
631
+ try {
632
+ const profileResult = await resolveProfile('whatsapp', options.profile);
633
+ if (!profileResult.profile) {
634
+ if (profileResult.error === 'none') {
635
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
636
+ }
637
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
638
+ }
639
+
640
+ const client = await getGatewayClient();
641
+
642
+ // Resolve name to ID if needed
643
+ let groupId = id;
644
+ if (!id.includes('@g.us')) {
645
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
646
+ if (!resolved.groupId) {
647
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
648
+ }
649
+ groupId = resolved.groupId;
650
+ }
651
+
652
+ const result = await client.whatsappGroupParticipants(
653
+ profileResult.profile,
654
+ groupId,
655
+ phones,
656
+ 'remove'
657
+ );
658
+ if (result.results) {
659
+ printWhatsAppParticipantsResult('removed', result.results);
660
+ } else {
661
+ console.log('Participants removed');
662
+ }
663
+ } catch (error) {
664
+ handleError(error);
665
+ }
666
+ });
667
+
668
+ group
669
+ .command('promote')
670
+ .description('Promote participants to admin')
671
+ .argument('<id>', 'Group ID or name')
672
+ .argument('<phones...>', 'Phone numbers to promote')
673
+ .option('--profile <name>', 'Profile name')
674
+ .action(async (id: string, phones: string[], options) => {
675
+ try {
676
+ const profileResult = await resolveProfile('whatsapp', options.profile);
677
+ if (!profileResult.profile) {
678
+ if (profileResult.error === 'none') {
679
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
680
+ }
681
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
682
+ }
683
+
684
+ const client = await getGatewayClient();
685
+
686
+ // Resolve name to ID if needed
687
+ let groupId = id;
688
+ if (!id.includes('@g.us')) {
689
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
690
+ if (!resolved.groupId) {
691
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
692
+ }
693
+ groupId = resolved.groupId;
694
+ }
695
+
696
+ const result = await client.whatsappGroupParticipants(
697
+ profileResult.profile,
698
+ groupId,
699
+ phones,
700
+ 'promote'
701
+ );
702
+ if (result.results) {
703
+ printWhatsAppParticipantsResult('promoted', result.results);
704
+ } else {
705
+ console.log('Participants promoted to admin');
706
+ }
707
+ } catch (error) {
708
+ handleError(error);
709
+ }
710
+ });
711
+
712
+ group
713
+ .command('demote')
714
+ .description('Demote admins to regular participants')
715
+ .argument('<id>', 'Group ID or name')
716
+ .argument('<phones...>', 'Phone numbers to demote')
717
+ .option('--profile <name>', 'Profile name')
718
+ .action(async (id: string, phones: string[], options) => {
719
+ try {
720
+ const profileResult = await resolveProfile('whatsapp', options.profile);
721
+ if (!profileResult.profile) {
722
+ if (profileResult.error === 'none') {
723
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
724
+ }
725
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
726
+ }
727
+
728
+ const client = await getGatewayClient();
729
+
730
+ // Resolve name to ID if needed
731
+ let groupId = id;
732
+ if (!id.includes('@g.us')) {
733
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
734
+ if (!resolved.groupId) {
735
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
736
+ }
737
+ groupId = resolved.groupId;
738
+ }
739
+
740
+ const result = await client.whatsappGroupParticipants(
741
+ profileResult.profile,
742
+ groupId,
743
+ phones,
744
+ 'demote'
745
+ );
746
+ if (result.results) {
747
+ printWhatsAppParticipantsResult('demoted', result.results);
748
+ } else {
749
+ console.log('Admins demoted to regular participants');
750
+ }
751
+ } catch (error) {
752
+ handleError(error);
753
+ }
754
+ });
755
+
756
+ group
757
+ .command('leave')
758
+ .description('Leave a group')
759
+ .argument('<id>', 'Group ID or name')
760
+ .option('--profile <name>', 'Profile name')
761
+ .action(async (id: string, options) => {
762
+ try {
763
+ const profileResult = await resolveProfile('whatsapp', options.profile);
764
+ if (!profileResult.profile) {
765
+ if (profileResult.error === 'none') {
766
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
767
+ }
768
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
769
+ }
770
+
771
+ const client = await getGatewayClient();
772
+
773
+ // Resolve name to ID if needed
774
+ let groupId = id;
775
+ if (!id.includes('@g.us')) {
776
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
777
+ if (!resolved.groupId) {
778
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
779
+ }
780
+ groupId = resolved.groupId;
781
+ }
782
+
783
+ // Confirm before leaving
784
+ const confirmed = await confirm(`Leave group ${id}?`);
785
+ if (!confirmed) {
786
+ console.log('Cancelled');
787
+ return;
788
+ }
789
+
790
+ await client.whatsappGroupLeave(profileResult.profile, groupId);
791
+ printWhatsAppGroupLeft(groupId);
792
+ } catch (error) {
793
+ handleError(error);
794
+ }
795
+ });
796
+
797
+ group
798
+ .command('invite')
799
+ .description('Get group invite link')
800
+ .argument('<id>', 'Group ID or name')
801
+ .option('--profile <name>', 'Profile name')
802
+ .action(async (id: string, options) => {
803
+ try {
804
+ const profileResult = await resolveProfile('whatsapp', options.profile);
805
+ if (!profileResult.profile) {
806
+ if (profileResult.error === 'none') {
807
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
808
+ }
809
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
810
+ }
811
+
812
+ const client = await getGatewayClient();
813
+
814
+ // Resolve name to ID if needed
815
+ let groupId = id;
816
+ if (!id.includes('@g.us')) {
817
+ const resolved = await client.whatsappGroupResolve(profileResult.profile, id);
818
+ if (!resolved.groupId) {
819
+ throw new CliError('NOT_FOUND', `Group not found: ${id}`);
820
+ }
821
+ groupId = resolved.groupId;
822
+ }
823
+
824
+ const result = await client.whatsappGroupInvite(profileResult.profile, groupId);
825
+ printWhatsAppGroupInvite(result);
826
+ } catch (error) {
827
+ handleError(error);
828
+ }
829
+ });
830
+
831
+ group
832
+ .command('join')
833
+ .description('Join group via invite code or link')
834
+ .argument('<code>', 'Invite code or full link (https://chat.whatsapp.com/...)')
835
+ .option('--profile <name>', 'Profile name')
836
+ .action(async (code: string, options) => {
837
+ try {
838
+ const profileResult = await resolveProfile('whatsapp', options.profile);
839
+ if (!profileResult.profile) {
840
+ if (profileResult.error === 'none') {
841
+ throw new CliError('PROFILE_NOT_FOUND', 'No WhatsApp profiles configured', 'Run: agentio whatsapp profile add');
842
+ }
843
+ throw new CliError('INVALID_PARAMS', 'Multiple profiles exist. Use --profile to specify one.');
844
+ }
845
+
846
+ const client = await getGatewayClient();
847
+ const groupId = await client.whatsappGroupJoin(profileResult.profile, code);
848
+ printWhatsAppGroupJoined(groupId);
849
+ } catch (error) {
850
+ handleError(error);
851
+ }
852
+ });
853
+ }