plazbot-cli 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +67 -111
  2. package/dist/cli.js +26 -11
  3. package/dist/commands/agent/ai-config.js +264 -63
  4. package/dist/commands/agent/chat.js +16 -11
  5. package/dist/commands/agent/copy.js +49 -13
  6. package/dist/commands/agent/create.js +62 -17
  7. package/dist/commands/agent/delete.js +50 -39
  8. package/dist/commands/agent/enable-widget.js +47 -27
  9. package/dist/commands/agent/export.js +180 -91
  10. package/dist/commands/agent/files.js +155 -73
  11. package/dist/commands/agent/get.js +25 -19
  12. package/dist/commands/agent/index.js +12 -10
  13. package/dist/commands/agent/list.js +20 -11
  14. package/dist/commands/agent/monitor.js +103 -57
  15. package/dist/commands/agent/on-message.js +55 -32
  16. package/dist/commands/agent/set.js +230 -82
  17. package/dist/commands/agent/templates.js +259 -156
  18. package/dist/commands/agent/tools.js +356 -123
  19. package/dist/commands/agent/update.js +69 -28
  20. package/dist/commands/agent/validate.js +72 -40
  21. package/dist/commands/agent/wizard.js +240 -329
  22. package/dist/commands/auth/browser-login.js +37 -31
  23. package/dist/commands/auth/login.js +49 -45
  24. package/dist/commands/auth/logout.js +20 -11
  25. package/dist/commands/auth/status.js +36 -12
  26. package/dist/commands/auth/workspace.js +61 -27
  27. package/dist/commands/portal/add-agent.js +106 -45
  28. package/dist/commands/portal/add-link.js +25 -28
  29. package/dist/commands/portal/clear-links.js +39 -19
  30. package/dist/commands/portal/create.js +66 -37
  31. package/dist/commands/portal/delete.js +40 -45
  32. package/dist/commands/portal/get.js +21 -54
  33. package/dist/commands/portal/index.js +12 -12
  34. package/dist/commands/portal/list.js +34 -52
  35. package/dist/commands/portal/shared.js +92 -0
  36. package/dist/commands/portal/update.js +100 -70
  37. package/dist/commands/whatsapp/broadcast.js +169 -77
  38. package/dist/commands/whatsapp/channels.js +167 -65
  39. package/dist/commands/whatsapp/chat.js +45 -46
  40. package/dist/commands/whatsapp/connect-flow.js +16 -15
  41. package/dist/commands/whatsapp/connect.js +36 -21
  42. package/dist/commands/whatsapp/delete-webhook.js +42 -25
  43. package/dist/commands/whatsapp/index.js +15 -16
  44. package/dist/commands/whatsapp/register-webhook.js +43 -26
  45. package/dist/commands/whatsapp/send-template.js +62 -61
  46. package/dist/commands/whatsapp/send.js +46 -33
  47. package/dist/commands/whatsapp/setup.js +62 -52
  48. package/dist/commands/whatsapp/shared.js +75 -0
  49. package/dist/commands/whatsapp/widget.js +82 -45
  50. package/dist/commands/workers/deploy.js +80 -58
  51. package/dist/commands/workers/index.js +8 -8
  52. package/dist/commands/workers/list.js +42 -20
  53. package/dist/commands/workers/logs.js +139 -36
  54. package/dist/commands/workers/remove.js +45 -47
  55. package/dist/commands/workers/secret.js +159 -83
  56. package/dist/commands/workers/test.js +108 -60
  57. package/dist/schemas/agent.config.schema.json +159 -8
  58. package/dist/studio/api/sseClient.js +1 -1
  59. package/dist/studio/api/studioApi.js +8 -0
  60. package/dist/studio/components/App.js +12 -0
  61. package/dist/studio/components/Footer.js +1 -1
  62. package/dist/studio/index.js +74 -14
  63. package/dist/studio/runOneShot.js +62 -23
  64. package/dist/studio/runRepl.js +5 -5
  65. package/dist/studio/slash/handlers.js +11 -8
  66. package/dist/studio/state/store.js +1 -1
  67. package/dist/utils/agent-errors.js +14 -11
  68. package/dist/utils/agent-patch.js +60 -0
  69. package/dist/utils/banner.js +9 -5
  70. package/dist/utils/confirm.js +31 -0
  71. package/dist/utils/credentials.js +13 -3
  72. package/dist/utils/logger.js +13 -10
  73. package/dist/utils/output.js +78 -0
  74. package/dist/utils/phone.js +22 -0
  75. package/dist/utils/ui.js +14 -11
  76. package/package.json +3 -3
@@ -1,75 +1,74 @@
1
1
  import { Command } from 'commander';
2
- import { Message } from 'plazbot';
3
- import { getStoredCredentials } from '../../utils/credentials.js';
4
- import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
2
+ import { addExamples, addNotes } from '../../utils/help.js';
6
3
  import { createSpinner } from '../../utils/ui.js';
4
+ import { fail, describeError } from '../../utils/output.js';
5
+ import { requireInteractive } from '../../utils/confirm.js';
6
+ import { phoneOption, normalizePhone } from '../../utils/phone.js';
7
+ import { ensureApiSuccess, getMessageClient } from './shared.js';
7
8
  import readline from 'readline';
8
9
  import chalk from 'chalk';
10
+ const green = chalk.hex('#25D366');
9
11
  export const whatsappChatCommand = new Command('chat')
10
- .description('Interactive direct chat over WhatsApp')
11
- .argument('<phone>', 'Destination phone number (with country code)')
12
+ .description('Send messages to a WhatsApp number from an interactive prompt (send-only)')
13
+ .addOption(phoneOption('Recipient number in international format, e.g. +51912345678'))
14
+ .argument('[phone]', 'Recipient number (legacy positional form of --phone)')
12
15
  .option('--dev', 'Use development environment', false)
13
- .action(async (phone, options) => {
16
+ .action(async (positional, options) => {
14
17
  try {
15
- const credentials = await getStoredCredentials();
16
- const messageClient = new Message({
17
- workspaceId: credentials.workspace,
18
- apiKey: credentials.apiKey,
19
- zone: credentials.zone,
20
- ...(options.dev && { customUrl: "http://localhost:5090" })
21
- });
22
- const rl = readline.createInterface({
23
- input: process.stdin,
24
- output: process.stdout,
25
- });
26
- console.clear();
18
+ const phone = normalizePhone(options.phone ?? positional);
19
+ requireInteractive('WhatsApp chat', `Send a single message instead: plazbot whatsapp send-message -p +${phone} -m "Hello"`);
20
+ const messageClient = await getMessageClient(options.dev);
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
+ const line = (text) => green(' │') + text.padEnd(60) + green('│');
27
23
  console.log();
28
- console.log(chalk.hex('#25D366')(' ┌' + '─'.repeat(50) + '┐'));
29
- console.log(chalk.hex('#25D366')(' │') + chalk.bold(` WhatsApp Chat`).padEnd(60) + chalk.hex('#25D366')('│'));
30
- console.log(chalk.hex('#25D366')(' │') + chalk.gray(` Destination: ${phone}`).padEnd(60) + chalk.hex('#25D366')('│'));
31
- console.log(chalk.hex('#25D366')(' │') + chalk.gray(' /exit to quit').padEnd(60) + chalk.hex('#25D366')('│'));
32
- console.log(chalk.hex('#25D366')(' └' + '─'.repeat(50) + '┘'));
24
+ console.log(green(' ┌' + '─'.repeat(50) + '┐'));
25
+ console.log(line(chalk.bold(' WhatsApp chat (send-only)')));
26
+ console.log(line(chalk.gray(` To: +${phone}`)));
27
+ console.log(line(chalk.gray(' Type /exit to quit')));
28
+ console.log(green(' └' + '─'.repeat(50) + '┘'));
29
+ console.log(chalk.gray(" Replies from the contact are not shown here; see them in the Plazbot inbox."));
30
+ console.log(chalk.gray(" Free-form text only works within 24h of the contact's last message."));
33
31
  console.log();
32
+ rl.on('close', () => process.exit(0));
34
33
  const ask = () => {
35
- rl.question(chalk.hex('#25D366')(' > '), async (message) => {
36
- if (!message.trim()) {
37
- ask();
38
- return;
39
- }
40
- if (message.toLowerCase() === '/exit') {
34
+ rl.question(green(' > '), async (message) => {
35
+ const text = message.trim();
36
+ if (!text)
37
+ return ask();
38
+ if (text.toLowerCase() === '/exit' || text.toLowerCase() === '/quit') {
41
39
  console.log(chalk.gray('\n Chat ended.\n'));
42
40
  rl.close();
43
41
  return;
44
42
  }
43
+ const spinner = createSpinner('Sending...').start();
45
44
  try {
46
- const spinner = createSpinner('Sending...');
47
- spinner.start();
48
- await messageClient.onWhatsappMessage({
49
- message,
50
- to: phone,
51
- });
45
+ const response = await messageClient.onWhatsappMessage({ message: text, to: phone });
46
+ ensureApiSuccess(response, 'The message was not sent', 'Outside the 24h window use: plazbot whatsapp send-template');
52
47
  spinner.succeed('Sent');
53
- ask();
54
48
  }
55
49
  catch (error) {
56
- const msg = error instanceof Error ? error.message : 'Error';
57
- console.log(chalk.hex('#EF5350')(` ✖ ${msg}`));
58
- ask();
50
+ const { message: msg, hint } = describeError(error);
51
+ spinner.fail(msg);
52
+ if (hint)
53
+ console.log(chalk.gray(` ${hint}`));
59
54
  }
55
+ ask();
60
56
  });
61
57
  };
62
58
  ask();
63
59
  }
64
60
  catch (error) {
65
- const message = error instanceof Error ? error.message : 'Unknown error';
66
- logger.error(message);
67
- process.exit(1);
61
+ fail(error);
68
62
  }
69
63
  });
70
64
  addExamples(whatsappChatCommand, [
71
- { description: 'Open an interactive chat with a WhatsApp number',
65
+ { description: 'Open a send-only chat with a number',
66
+ command: 'plazbot whatsapp chat -p +51912345678' },
67
+ { description: 'Legacy positional form (still supported)',
72
68
  command: 'plazbot whatsapp chat 51912345678' },
73
- { description: 'Chat through the development backend',
74
- command: 'plazbot whatsapp chat 51912345678 --dev' },
69
+ ]);
70
+ addNotes(whatsappChatCommand, [
71
+ 'Send-only: incoming replies are not shown in the terminal. Read them in the Plazbot inbox or via your webhook.',
72
+ "WhatsApp only allows free-form text within 24 hours of the contact's last message; outside it use send-template.",
73
+ 'Needs an interactive terminal. For scripts use plazbot whatsapp send-message.',
75
74
  ]);
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { apiBaseUrl } from '../../utils/credentials.js';
5
5
  import { openBrowser } from '../../utils/open-browser.js';
6
+ import { CliError } from '../../utils/output.js';
6
7
  /**
7
8
  * Lógica compartida de conexión de números de WhatsApp (connect y setup).
8
9
  * Mismo flujo que el panel: link de onboarding co.plzb.link → el usuario completa
@@ -33,9 +34,9 @@ export async function fetchIntegrations(creds, dev = false) {
33
34
  }
34
35
  catch (error) {
35
36
  if (axios.isAxiosError(error) && error.response?.status === 401) {
36
- throw new Error("Tu sesión no es válida o fue revocada. Ejecuta 'plazbot login'.");
37
+ throw new CliError('Your session is invalid or was revoked.', 'unauthorized', "Run 'plazbot login' to sign in again.");
37
38
  }
38
- throw new Error('No se pudieron cargar las integraciones del workspace.');
39
+ throw new CliError('Could not load the workspace integrations.', 'api_error', 'Try again in a few seconds.');
39
40
  }
40
41
  }
41
42
  export function whatsappNumbers(all) {
@@ -67,13 +68,13 @@ export async function getOnboardingLink(creds, business, dev = false) {
67
68
  }
68
69
  catch (error) {
69
70
  if (axios.isAxiosError(error) && error.response?.status === 401) {
70
- throw new Error("Tu sesión no es válida o fue revocada. Ejecuta 'plazbot login'.");
71
+ throw new CliError('Your session is invalid or was revoked.', 'unauthorized', "Run 'plazbot login' to sign in again.");
71
72
  }
72
73
  link = null;
73
74
  }
74
75
  }
75
76
  if (!link?.token)
76
- throw new Error('No se pudo obtener el link de conexión.');
77
+ throw new CliError('Could not get the connection link.', 'api_error', 'Try again, or connect the number from the panel → Integrations.');
77
78
  return { url: link.shortUrl || `${ONBOARDING_SHORT_DOMAIN}/${link.token}`, expiresAt: link.expiresAt };
78
79
  }
79
80
  export async function activateIntegration(creds, integrationId, dev = false) {
@@ -97,15 +98,15 @@ export async function connectNewNumber(creds, opts) {
97
98
  const opened = opts.openInBrowser !== false && openBrowser(url);
98
99
  const border = chalk.hex('#25D366');
99
100
  console.log();
100
- console.log(border(' ┃ ') + chalk.bold(opened ? 'Abrimos el registro de Meta en tu navegador.' : 'Abre este link en tu navegador:'));
101
+ console.log(border(' ┃ ') + chalk.bold(opened ? 'We opened the Meta sign-up in your browser.' : 'Open this link in your browser:'));
101
102
  console.log(border(' ┃ ') + chalk.hex('#22d3ee').underline(url));
102
103
  console.log(border(' ┃ ') + chalk.gray(opts.business
103
- ? 'Inicia sesión con Facebook y vincula tu app WhatsApp Business escaneando el QR.'
104
- : 'Inicia sesión con Facebook, elige o crea tu cuenta de WhatsApp Business y verifica el número.'));
104
+ ? 'Sign in with Facebook and link your WhatsApp Business app by scanning the QR code.'
105
+ : 'Sign in with Facebook, choose or create your WhatsApp Business account and verify the number.'));
105
106
  if (expiresAt)
106
- console.log(border(' ┃ ') + chalk.gray(`El link vence el ${new Date(expiresAt).toLocaleDateString('es-ES', { day: 'numeric', month: 'long' })}.`));
107
+ console.log(border(' ┃ ') + chalk.gray(`The link expires on ${new Date(expiresAt).toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}.`));
107
108
  console.log();
108
- const spinner = ora({ text: chalk.gray('Esperando la conexión... (Ctrl+C para cancelar)'), spinner: 'dots', color: 'cyan' }).start();
109
+ const spinner = ora({ text: chalk.gray('Waiting for the connection... (Ctrl+C to cancel)'), spinner: 'dots', color: 'cyan' }).start();
109
110
  const intervalMs = 5000;
110
111
  const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1000);
111
112
  const startedAt = Date.now();
@@ -115,14 +116,14 @@ export async function connectNewNumber(creds, opts) {
115
116
  const fresh = whatsappNumbers(await fetchIntegrations(creds, dev)).filter(ig => !prevIds.has(ig.id));
116
117
  if (fresh.length > 0) {
117
118
  const detected = fresh[0];
118
- spinner.succeed(chalk.hex('#4ade80')(`Número detectado: ${integrationLabel(detected)}`));
119
- const act = ora({ text: chalk.gray('Activando el número...'), spinner: 'dots', color: 'green' }).start();
119
+ spinner.succeed(chalk.hex('#4ade80')(`Number detected: ${integrationLabel(detected)}`));
120
+ const act = ora({ text: chalk.gray('Activating the number...'), spinner: 'dots', color: 'green' }).start();
120
121
  if (await activateIntegration(creds, detected.id, dev)) {
121
- act.succeed(chalk.hex('#4ade80')('Número activo'));
122
+ act.succeed(chalk.hex('#4ade80')('Number active'));
122
123
  detected.isActive = 1;
123
124
  }
124
125
  else {
125
- act.warn(chalk.hex('#FFA726')('No se pudo activar automáticamente. Actívalo en el panel → Integraciones.'));
126
+ act.warn(chalk.hex('#FFA726')('Could not activate it automatically. Activate it in the panel → Integrations.'));
126
127
  }
127
128
  return detected;
128
129
  }
@@ -130,8 +131,8 @@ export async function connectNewNumber(creds, opts) {
130
131
  catch {
131
132
  // error puntual: seguir esperando
132
133
  }
133
- spinner.text = chalk.gray(`Esperando la conexión... ${Math.floor((Date.now() - startedAt) / 1000)}s (Ctrl+C para cancelar)`);
134
+ spinner.text = chalk.gray(`Waiting for the connection... ${Math.floor((Date.now() - startedAt) / 1000)}s (Ctrl+C to cancel)`);
134
135
  }
135
- spinner.warn(chalk.hex('#FFA726')('No detectamos una conexión nueva a tiempo.'));
136
+ spinner.warn(chalk.hex('#FFA726')('No new connection was detected in time.'));
136
137
  return null;
137
138
  }
@@ -2,26 +2,31 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { getStoredCredentials } from '../../utils/credentials.js';
5
- import { logger } from '../../utils/logger.js';
6
- import { addExamples } from '../../utils/help.js';
5
+ import { addExamples, addNotes } from '../../utils/help.js';
7
6
  import { theme, section } from '../../utils/ui.js';
7
+ import { fail } from '../../utils/output.js';
8
8
  import { connectNewNumber, fetchIntegrations, integrationLabel, whatsappNumbers } from './connect-flow.js';
9
9
  export const whatsappConnectCommand = new Command('connect')
10
- .description('Conecta un número de WhatsApp al workspace')
11
- .option('--business', 'Conectar tu app WhatsApp Business actual (coexistencia)', false)
12
- .option('--no-browser', 'No abrir el navegador automáticamente')
13
- .option('--dev', 'Usar el entorno de desarrollo local', false)
10
+ .description('Connect a WhatsApp number to the workspace (Meta sign-up in the browser)')
11
+ .option('--business', 'Connect your existing WhatsApp Business app (coexistence, linked by QR)', false)
12
+ .option('--no-browser', 'Print the link instead of opening the browser')
13
+ .option('--dev', 'Use development environment', false)
14
14
  .action(async (options) => {
15
15
  try {
16
16
  const credentials = await getStoredCredentials();
17
- console.log(section(options.business ? 'Conectar WhatsApp Business' : 'Conectar WhatsApp'));
18
- const loadSpinner = ora({ text: chalk.gray('Cargando números conectados...'), spinner: 'dots', color: 'green' }).start();
19
- const existing = whatsappNumbers(await fetchIntegrations(credentials, options.dev));
20
- loadSpinner.stop();
17
+ console.log(section(options.business ? 'Connect WhatsApp Business' : 'Connect WhatsApp'));
18
+ const loadSpinner = ora({ text: chalk.gray('Loading connected numbers...'), spinner: 'dots', color: 'green' }).start();
19
+ let existing;
20
+ try {
21
+ existing = whatsappNumbers(await fetchIntegrations(credentials, options.dev));
22
+ }
23
+ finally {
24
+ loadSpinner.stop();
25
+ }
21
26
  if (existing.length > 0) {
22
- console.log(theme.muted(` ${existing.length} número(s) ya conectado(s):`));
27
+ console.log(theme.muted(` ${existing.length} number(s) already connected:`));
23
28
  existing.forEach(ig => {
24
- const status = ig.isActive ? chalk.hex('#66BB6A')('activo') : chalk.hex('#EF5350')('inactivo');
29
+ const status = ig.isActive ? chalk.hex('#66BB6A')('active') : chalk.hex('#EF5350')('inactive');
25
30
  console.log(` ${theme.muted('•')} ${integrationLabel(ig)} ${status}`);
26
31
  });
27
32
  }
@@ -32,22 +37,32 @@ export const whatsappConnectCommand = new Command('connect')
32
37
  });
33
38
  console.log();
34
39
  if (detected) {
35
- console.log(theme.success(` WhatsApp conectado: ${integrationLabel(detected)}`));
40
+ console.log(theme.success(` WhatsApp connected: ${integrationLabel(detected)}`));
41
+ if (!detected.isActive) {
42
+ console.log(theme.muted(' The number is not active yet. Activate it in the panel → Integrations.'));
43
+ }
44
+ console.log();
45
+ console.log(theme.muted(' Next step:'));
46
+ console.log(` ${chalk.cyan('plazbot whatsapp setup')} ${theme.muted('Send and receive your first message')}`);
36
47
  console.log();
37
- console.log(theme.muted(' Siguiente paso:'));
38
- console.log(` ${chalk.cyan('plazbot whatsapp setup')} ${theme.muted('Prueba enviar y recibir tu primer mensaje')}`);
39
48
  }
40
49
  else {
41
- console.log(theme.muted(' Puedes intentarlo otra vez con: plazbot whatsapp connect'));
50
+ console.log(theme.muted(' Try again with: plazbot whatsapp connect'));
51
+ console.log();
52
+ process.exit(1);
42
53
  }
43
- console.log();
44
54
  }
45
55
  catch (error) {
46
- logger.error(error instanceof Error ? error.message : 'Error desconocido');
47
- process.exit(1);
56
+ fail(error);
48
57
  }
49
58
  });
50
59
  addExamples(whatsappConnectCommand, [
51
- { description: 'Conectar un número con WhatsApp API', command: 'plazbot whatsapp connect' },
52
- { description: 'Conectar tu app WhatsApp Business actual', command: 'plazbot whatsapp connect --business' },
60
+ { description: 'Connect a number with the WhatsApp API', command: 'plazbot whatsapp connect' },
61
+ { description: 'Connect your existing WhatsApp Business app', command: 'plazbot whatsapp connect --business' },
62
+ { description: 'Print the link (e.g. to open it on another device)', command: 'plazbot whatsapp connect --no-browser' },
63
+ ]);
64
+ addNotes(whatsappConnectCommand, [
65
+ 'The CLI waits up to 10 minutes for the new number to appear, then activates it.',
66
+ 'If automatic activation fails, activate the number in the panel → Integrations.',
67
+ 'The connection link is reused while it is valid and regenerated when it expires.',
53
68
  ]);
@@ -1,34 +1,51 @@
1
- import { Command } from 'commander';
2
- import { Message } from 'plazbot';
3
- import { getStoredCredentials } from '../../utils/credentials.js';
1
+ import { Command, Option } from 'commander';
4
2
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
3
+ import { addExamples, addNotes } from '../../utils/help.js';
4
+ import { createSpinner } from '../../utils/ui.js';
5
+ import { jsonOption, isJsonMode, printJson, fail } from '../../utils/output.js';
6
+ import { addYesOption, confirmAction } from '../../utils/confirm.js';
7
+ import { phoneOption, normalizePhone } from '../../utils/phone.js';
8
+ import { ensureApiSuccess, getMessageClient } from './shared.js';
6
9
  export const deleteWebhookCommand = new Command('delete-webhook')
7
- .description('Delete the WhatsApp webhook')
8
- .requiredOption('-n, --number <phone>', 'WhatsApp number (with country code, e.g.: 51912345678)')
9
- .option('--dev', 'Use development environment', false)
10
- .action(async (options) => {
10
+ .description('Remove the incoming-messages webhook from a WhatsApp number')
11
+ .addOption(phoneOption('Your connected WhatsApp number in international format, e.g. +51912345678'))
12
+ .addOption(new Option('-n, --number <phone>', 'Alias of --phone').hideHelp())
13
+ .addOption(jsonOption())
14
+ .option('--dev', 'Use development environment', false);
15
+ addYesOption(deleteWebhookCommand);
16
+ deleteWebhookCommand.action(async (options) => {
11
17
  try {
12
- const credentials = await getStoredCredentials();
13
- const messageClient = new Message({
14
- workspaceId: credentials.workspace,
15
- apiKey: credentials.apiKey,
16
- zone: credentials.zone,
17
- ...(options.dev && { customUrl: "http://localhost:5090" })
18
- });
19
- logger.title('Deleting WhatsApp webhook');
20
- logger.label('Number', options.number);
21
- await messageClient.deleteWebhook({
22
- number: options.number
23
- });
24
- logger.success('Webhook deleted successfully');
18
+ const phone = normalizePhone(options.phone ?? options.number);
19
+ if (!(await confirmAction(`Remove the webhook from +${phone}? Your server will stop receiving its messages.`, options))) {
20
+ logger.dim('Aborted.');
21
+ process.exit(1);
22
+ }
23
+ const messageClient = await getMessageClient(options.dev);
24
+ const spinner = createSpinner(`Removing webhook from +${phone}...`).start();
25
+ try {
26
+ const response = await messageClient.deleteWebhook({ number: phone });
27
+ ensureApiSuccess(response, 'The webhook was not removed');
28
+ }
29
+ catch (error) {
30
+ spinner.stop();
31
+ throw error;
32
+ }
33
+ spinner.succeed(`Webhook removed from +${phone}`);
34
+ if (isJsonMode())
35
+ printJson({ phone, deleted: true });
25
36
  }
26
37
  catch (error) {
27
- logger.error(error.message);
28
- process.exit(1);
38
+ fail(error);
29
39
  }
30
40
  });
31
41
  addExamples(deleteWebhookCommand, [
32
- { description: 'Remove the webhook from a WhatsApp number',
33
- command: 'plazbot whatsapp delete-webhook -n 51912345678' },
42
+ { description: 'Remove the webhook (asks for confirmation)',
43
+ command: 'plazbot whatsapp delete-webhook -p +51912345678' },
44
+ { description: 'Remove it without prompting (scripts and CI)',
45
+ command: 'plazbot whatsapp delete-webhook -p +51912345678 --yes' },
46
+ { description: 'Machine-readable output',
47
+ command: 'plazbot whatsapp delete-webhook -p +51912345678 --yes --json' },
48
+ ]);
49
+ addNotes(deleteWebhookCommand, [
50
+ 'Only the forwarding to your URL is removed; the number stays connected to the workspace.',
34
51
  ]);
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { addExamples } from '../../utils/help.js';
2
+ import { addExamples, addNotes } from '../../utils/help.js';
3
3
  import { sendMessageCommand } from './send.js';
4
4
  import { sendTemplateCommand } from './send-template.js';
5
5
  import { registerWebhookCommand } from './register-webhook.js';
@@ -11,29 +11,28 @@ import { whatsappSetupCommand } from './setup.js';
11
11
  import { widgetCommand } from './widget.js';
12
12
  import { channelsCommand, assignCommand } from './channels.js';
13
13
  export const whatsappCommands = new Command('whatsapp')
14
- .description('WhatsApp commands: messages, templates, broadcast and more')
14
+ .description('WhatsApp: connect numbers, send messages and templates, broadcast, webhooks')
15
15
  .addCommand(whatsappSetupCommand)
16
16
  .addCommand(whatsappConnectCommand)
17
17
  .addCommand(sendMessageCommand)
18
18
  .addCommand(sendTemplateCommand)
19
- .addCommand(registerWebhookCommand)
20
- .addCommand(deleteWebhookCommand)
21
19
  .addCommand(broadcastCommand)
22
20
  .addCommand(whatsappChatCommand)
23
- .addCommand(widgetCommand)
21
+ .addCommand(registerWebhookCommand)
22
+ .addCommand(deleteWebhookCommand)
24
23
  .addCommand(channelsCommand)
25
- .addCommand(assignCommand);
24
+ .addCommand(assignCommand)
25
+ .addCommand(widgetCommand);
26
26
  addExamples(whatsappCommands, [
27
- { description: 'Asistente: conectar WhatsApp y enviar tu primer mensaje',
27
+ { description: 'Guided setup: connect WhatsApp and send your first message',
28
28
  command: 'plazbot whatsapp setup' },
29
- { description: 'Connect a WhatsApp number to the workspace',
30
- command: 'plazbot whatsapp connect' },
31
- { description: 'Send a quick message',
32
- command: 'plazbot whatsapp send-message -t 51912345678 -m "Hello"' },
29
+ { description: 'Send a text message (within the 24h window)',
30
+ command: 'plazbot whatsapp send-message -p +51912345678 -m "Hello"' },
33
31
  { description: 'Send a template with variables',
34
- command: 'plazbot whatsapp send-template -p 51912345678 -t welcome --var name=Anna' },
35
- { description: 'Open an interactive chat',
36
- command: 'plazbot whatsapp chat 51912345678' },
37
- { description: 'Assign a number to an agent',
38
- command: 'plazbot whatsapp assign 51912345678 agt_AbcDef123' },
32
+ command: 'plazbot whatsapp send-template -p +51912345678 -t welcome --var name=Anna' },
33
+ { description: 'Make an agent answer a number',
34
+ command: 'plazbot whatsapp assign -p +51912345678 -a age_AbcDef123' },
35
+ ]);
36
+ addNotes(whatsappCommands, [
37
+ 'Every command takes the number as -p, --phone in international format (+51912345678, "+51 912 345 678" or 51912345678).',
39
38
  ]);
@@ -1,37 +1,54 @@
1
- import { Command } from 'commander';
2
- import { Message } from 'plazbot';
3
- import { getStoredCredentials } from '../../utils/credentials.js';
1
+ import { Command, Option } from 'commander';
4
2
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
3
+ import { addExamples, addNotes } from '../../utils/help.js';
4
+ import { createSpinner } from '../../utils/ui.js';
5
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
6
+ import { phoneOption, normalizePhone } from '../../utils/phone.js';
7
+ import { ensureApiSuccess, getMessageClient } from './shared.js';
6
8
  export const registerWebhookCommand = new Command('register-webhook')
7
- .description('Register a webhook for WhatsApp')
8
- .requiredOption('-n, --number <phone>', 'WhatsApp number (with country code, e.g.: 51912345678)')
9
- .requiredOption('-u, --url <webhook>', 'Webhook URL')
9
+ .description('Register the webhook URL that receives incoming messages for a WhatsApp number')
10
+ .addOption(phoneOption('Your connected WhatsApp number in international format, e.g. +51912345678'))
11
+ .addOption(new Option('-n, --number <phone>', 'Alias of --phone').hideHelp())
12
+ .requiredOption('-u, --url <url>', 'Public https URL that will receive the incoming messages')
13
+ .addOption(jsonOption())
10
14
  .option('--dev', 'Use development environment', false)
11
15
  .action(async (options) => {
12
16
  try {
13
- const credentials = await getStoredCredentials();
14
- const messageClient = new Message({
15
- workspaceId: credentials.workspace,
16
- apiKey: credentials.apiKey,
17
- zone: credentials.zone,
18
- ...(options.dev && { customUrl: "http://localhost:5090" })
19
- });
20
- logger.title('Registering WhatsApp webhook');
21
- logger.label('Number', options.number);
22
- logger.label('URL', options.url);
23
- await messageClient.registerWebhook({
24
- number: options.number,
25
- webhookUrl: options.url
26
- });
27
- logger.success('Webhook registered successfully');
17
+ const phone = normalizePhone(options.phone ?? options.number);
18
+ const url = String(options.url || '').trim();
19
+ if (!/^https?:\/\/\S+$/i.test(url)) {
20
+ throw new CliError(`Invalid webhook URL "${options.url}".`, 'invalid_url', 'Use a public URL, e.g. -u https://api.example.com/webhooks/whatsapp');
21
+ }
22
+ const messageClient = await getMessageClient(options.dev);
23
+ if (!isJsonMode()) {
24
+ logger.title('Registering WhatsApp webhook');
25
+ logger.label('Number', `+${phone}`);
26
+ logger.label('URL', url);
27
+ }
28
+ const spinner = createSpinner('Registering...').start();
29
+ try {
30
+ const response = await messageClient.registerWebhook({ number: phone, webhookUrl: url });
31
+ ensureApiSuccess(response, 'The webhook was not registered', "Check that the number is connected to this workspace ('plazbot whatsapp connect').");
32
+ }
33
+ catch (error) {
34
+ spinner.stop();
35
+ throw error;
36
+ }
37
+ spinner.succeed('Webhook registered');
38
+ if (isJsonMode())
39
+ printJson({ phone, url, registered: true });
28
40
  }
29
41
  catch (error) {
30
- logger.error(error.message);
31
- process.exit(1);
42
+ fail(error);
32
43
  }
33
44
  });
34
45
  addExamples(registerWebhookCommand, [
35
- { description: 'Register an inbound webhook for a WhatsApp number',
36
- command: 'plazbot whatsapp register-webhook -n 51912345678 -u https://api.example.com/webhooks/whatsapp' },
46
+ { description: 'Register an incoming-messages webhook for a number',
47
+ command: 'plazbot whatsapp register-webhook -p +51912345678 -u https://api.example.com/webhooks/whatsapp' },
48
+ { description: 'Same, with machine-readable output',
49
+ command: 'plazbot whatsapp register-webhook -p +51912345678 -u https://api.example.com/webhooks/whatsapp --json' },
50
+ ]);
51
+ addNotes(registerWebhookCommand, [
52
+ 'The number must be connected to the active workspace (see plazbot whatsapp connect).',
53
+ 'Registering again replaces the previous URL. Remove it with plazbot whatsapp delete-webhook.',
37
54
  ]);