plazbot-cli 0.4.0 → 0.5.0

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 +179 -88
  10. package/dist/commands/agent/files.js +155 -73
  11. package/dist/commands/agent/get.js +24 -16
  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 +101 -53
  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 +183 -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 +77 -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 +2 -2
@@ -1,86 +1,87 @@
1
1
  import { Command } from 'commander';
2
- import { Message } from 'plazbot';
3
- import { getStoredCredentials } from '../../utils/credentials.js';
4
2
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
3
+ import { addExamples, addNotes } from '../../utils/help.js';
6
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, parseVariables } from './shared.js';
7
8
  export const sendTemplateCommand = new Command('send-template')
8
- .description('Send a WhatsApp template message with variables')
9
- .requiredOption('-p, --phone <number>', 'Phone number (with country code)')
10
- .requiredOption('-t, --template <name>', 'Template name')
11
- .option('--var <vars...>', 'Body variables (format: name=value)')
12
- .option('--header-var <vars...>', 'Header variables (format: name=value)')
13
- .option('--file-url <url>', 'Attachment file URL')
14
- .option('--file-name <name>', 'Attachment file name')
9
+ .description('Send an approved WhatsApp template message')
10
+ .addOption(phoneOption('Recipient number in international format, e.g. +51912345678'))
11
+ .requiredOption('-t, --template <name>', 'Template name exactly as approved in Meta (e.g. welcome_message)')
12
+ .option('--var <name=value...>', 'Body variables, one or more name=value pairs (e.g. --var name=Anna code=PB-123)')
13
+ .option('--header-var <name=value...>', 'Header text variables, name=value pairs (e.g. --header-var store=Lima)')
14
+ .option('--file-url <url>', 'Public URL of the media for an image/video/document header')
15
+ .option('--file-name <name>', 'File name shown to the recipient for a document header (default: "file")')
16
+ .addOption(jsonOption())
15
17
  .option('--dev', 'Use development environment', false)
16
18
  .action(async (options) => {
17
19
  try {
18
- const credentials = await getStoredCredentials();
19
- const messageClient = new Message({
20
- workspaceId: credentials.workspace,
21
- apiKey: credentials.apiKey,
22
- zone: credentials.zone,
23
- ...(options.dev && { customUrl: "http://localhost:5090" })
24
- });
25
- // Parsear variables
26
- const variablesBody = parseVariables(options.var);
27
- const variablesHeader = parseVariables(options.headerVar);
28
- // Archivo adjunto
29
- const file = options.fileUrl ? {
30
- fileUrl: options.fileUrl,
31
- fileName: options.fileName || 'file',
32
- } : undefined;
33
- logger.title('Sending WhatsApp template');
34
- logger.label('Destination', options.phone);
35
- logger.label('Template', options.template);
36
- if (variablesBody.length > 0) {
37
- logger.label('Body variables', variablesBody.map(v => `${v.variable}=${v.value}`).join(', '));
20
+ const phone = normalizePhone(options.phone);
21
+ const template = String(options.template || '').trim();
22
+ if (!template) {
23
+ throw new CliError('Template name is required.', 'missing_template', 'Pass it with -t, --template <name>.');
38
24
  }
39
- if (variablesHeader.length > 0) {
40
- logger.label('Header variables', variablesHeader.map(v => `${v.variable}=${v.value}`).join(', '));
25
+ if (options.fileName && !options.fileUrl) {
26
+ throw new CliError('--file-name needs --file-url.', 'missing_file_url', 'Pass the media URL with --file-url <url>.');
41
27
  }
42
- if (file) {
43
- logger.label('File', file.fileUrl);
28
+ if (options.fileUrl && !/^https?:\/\/\S+$/i.test(options.fileUrl)) {
29
+ throw new CliError(`Invalid --file-url "${options.fileUrl}".`, 'invalid_file_url', 'Use a public http(s) URL.');
44
30
  }
45
- const spinner = createSpinner('Sending...');
46
- spinner.start();
47
- const params = {
48
- to: options.phone,
49
- template: options.template,
50
- };
31
+ const variablesBody = parseVariables(options.var, '--var');
32
+ const variablesHeader = parseVariables(options.headerVar, '--header-var');
33
+ const file = options.fileUrl ? { fileUrl: options.fileUrl, fileName: options.fileName || 'file' } : undefined;
34
+ const messageClient = await getMessageClient(options.dev);
35
+ if (!isJsonMode()) {
36
+ logger.title('Sending WhatsApp template');
37
+ logger.label('To', `+${phone}`);
38
+ logger.label('Template', template);
39
+ if (variablesBody.length > 0)
40
+ logger.label('Body variables', variablesBody.map(v => `${v.variable}=${v.value}`).join(', '));
41
+ if (variablesHeader.length > 0)
42
+ logger.label('Header variables', variablesHeader.map(v => `${v.variable}=${v.value}`).join(', '));
43
+ if (file)
44
+ logger.label('File', `${file.fileUrl} (${file.fileName})`);
45
+ }
46
+ const params = { to: phone, template };
51
47
  if (variablesBody.length > 0)
52
48
  params.variablesBody = variablesBody;
53
49
  if (variablesHeader.length > 0)
54
50
  params.variablesHeader = variablesHeader;
55
51
  if (file)
56
52
  params.file = file;
57
- const response = await messageClient.onConversation(params);
58
- spinner.succeed('Template sent successfully');
59
- if (options.dev) {
60
- logger.warning('Environment: development');
53
+ const spinner = createSpinner('Sending...').start();
54
+ let response;
55
+ try {
56
+ response = await messageClient.onConversation(params);
57
+ ensureApiSuccess(response, 'The template was not sent', "Check the template name and its variables in the panel (Templates), and that it's approved.");
58
+ }
59
+ catch (error) {
60
+ spinner.stop();
61
+ throw error;
62
+ }
63
+ spinner.succeed('Template sent');
64
+ if (isJsonMode()) {
65
+ printJson({ phone, template, status: 'sent', ...(response?.messageId && { messageId: response.messageId }) });
61
66
  }
62
67
  }
63
68
  catch (error) {
64
- const message = error?.message || 'Unknown error while sending the template';
65
- logger.error(message);
66
- process.exit(1);
69
+ fail(error);
67
70
  }
68
71
  });
69
72
  addExamples(sendTemplateCommand, [
70
73
  { description: 'Send a template with no variables',
71
- command: 'plazbot whatsapp send-template -p 51912345678 -t welcome_message' },
74
+ command: 'plazbot whatsapp send-template -p +51912345678 -t welcome_message' },
72
75
  { description: 'Send a template with body variables',
73
- command: 'plazbot whatsapp send-template -p 51912345678 -t order_status --var name=Anna code=PB-123' },
74
- { description: 'Send a template with an attached PDF',
75
- command: 'plazbot whatsapp send-template -p 51912345678 -t invoice_template --file-url https://files.example.com/inv.pdf --file-name invoice.pdf' },
76
- { description: 'Send a template with header and body variables',
77
- command: 'plazbot whatsapp send-template -p 51912345678 -t promo --header-var image=https://img.example.com/banner.png --var amount=20' },
76
+ command: 'plazbot whatsapp send-template -p +51912345678 -t order_status --var name=Anna code=PB-123' },
77
+ { description: 'Header text variable plus body variable',
78
+ command: 'plazbot whatsapp send-template -p +51912345678 -t promo --header-var store=Lima --var amount=20' },
79
+ { description: 'Template with a document header',
80
+ command: 'plazbot whatsapp send-template -p +51912345678 -t invoice --file-url https://files.example.com/inv.pdf --file-name invoice.pdf' },
81
+ ]);
82
+ addNotes(sendTemplateCommand, [
83
+ 'Templates can be sent any time, including to new contacts and outside the 24h window.',
84
+ 'Variables use name=value; the value may contain "=" and spaces (quote it: --var "name=Anna Lopez"). Empty values are rejected.',
85
+ '--header-var fills placeholders of a text header. For an image/video/document header use --file-url (and --file-name for documents).',
86
+ '--var and --header-var take several pairs; put them after the other options or repeat the flag.',
78
87
  ]);
79
- function parseVariables(vars) {
80
- if (!vars)
81
- return [];
82
- return vars.map(v => {
83
- const [variable, ...rest] = v.split('=');
84
- return { variable: variable.trim(), value: rest.join('=').trim() };
85
- }).filter(v => v.variable && v.value);
86
- }
@@ -1,46 +1,59 @@
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';
6
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';
8
+ const WINDOW_HINT = "Free-form text only works within 24h of the contact's last message. Outside that window send an approved template: plazbot whatsapp send-template -p <number> -t <template>";
7
9
  export const sendMessageCommand = new Command('send-message')
8
- .description('Send a direct WhatsApp message')
9
- .requiredOption('-t, --to <phone>', 'Phone number (with country code, e.g.: 51912345678)')
10
- .requiredOption('-m, --message <text>', 'Message to send')
10
+ .description('Send a free-form WhatsApp text message to a contact')
11
+ .addOption(phoneOption('Recipient number in international format, e.g. +51912345678'))
12
+ .addOption(new Option('-t, --to <phone>', 'Alias of --phone').hideHelp())
13
+ .requiredOption('-m, --message <text>', 'Text to send (quote it if it has spaces)')
14
+ .addOption(jsonOption())
11
15
  .option('--dev', 'Use development environment', false)
12
16
  .action(async (options) => {
13
17
  try {
14
- const credentials = await getStoredCredentials();
15
- const messageClient = new Message({
16
- workspaceId: credentials.workspace,
17
- apiKey: credentials.apiKey,
18
- zone: credentials.zone,
19
- ...(options.dev && { customUrl: "http://localhost:5090" })
20
- });
21
- logger.title('Sending WhatsApp message');
22
- logger.label('Destination', options.to);
23
- logger.label('Message', options.message);
24
- const spinner = createSpinner('Sending...');
25
- spinner.start();
26
- const response = await messageClient.onWhatsappMessage({
27
- message: options.message,
28
- to: options.to
29
- });
30
- spinner.succeed('Message sent successfully');
31
- if (options.dev) {
32
- logger.warning('Environment: development');
18
+ const phone = normalizePhone(options.phone ?? options.to);
19
+ if (!options.message || !options.message.trim()) {
20
+ throw new CliError('Message is empty.', 'missing_message', 'Pass the text with -m, --message "Hello".');
21
+ }
22
+ const messageClient = await getMessageClient(options.dev);
23
+ if (!isJsonMode()) {
24
+ logger.title('Sending WhatsApp message');
25
+ logger.label('To', `+${phone}`);
26
+ logger.label('Message', options.message);
27
+ }
28
+ const spinner = createSpinner('Sending...').start();
29
+ let response;
30
+ try {
31
+ response = await messageClient.onWhatsappMessage({ message: options.message, to: phone });
32
+ ensureApiSuccess(response, 'The message was not sent', WINDOW_HINT);
33
+ }
34
+ catch (error) {
35
+ spinner.stop();
36
+ throw error;
37
+ }
38
+ spinner.succeed('Message sent');
39
+ if (isJsonMode()) {
40
+ printJson({ phone, status: 'sent', ...(response?.messageId && { messageId: response.messageId }) });
33
41
  }
34
42
  }
35
43
  catch (error) {
36
- const message = error?.message || 'Unknown error while sending the message';
37
- logger.error(message);
38
- process.exit(1);
44
+ fail(error);
39
45
  }
40
46
  });
41
47
  addExamples(sendMessageCommand, [
42
- { description: 'Send a quick WhatsApp message',
43
- command: 'plazbot whatsapp send-message -t 51912345678 -m "Hello from Plazbot"' },
44
- { description: 'Send through the development backend',
45
- command: 'plazbot whatsapp send-message -t 51912345678 -m "Hi" --dev' },
48
+ { description: 'Send a quick text message',
49
+ command: 'plazbot whatsapp send-message -p +51912345678 -m "Hello from Plazbot"' },
50
+ { description: 'Any international format works (spaces, 00 prefix)',
51
+ command: 'plazbot whatsapp send-message -p "+51 912 345 678" -m "Your order is on its way"' },
52
+ { description: 'Machine-readable result for scripts',
53
+ command: 'plazbot whatsapp send-message -p +51912345678 -m "Hi" --json' },
54
+ ]);
55
+ addNotes(sendMessageCommand, [
56
+ "WhatsApp only allows free-form text within 24 hours of the contact's last message (the 24h window).",
57
+ 'Outside the window, or to message a new contact first, use an approved template: plazbot whatsapp send-template.',
58
+ 'The contact must already exist in the workspace (it is created when they write to your number).',
46
59
  ]);
@@ -4,7 +4,9 @@ import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import { apiBaseUrl, getStoredCredentials } from '../../utils/credentials.js';
6
6
  import { createSpinner, theme } from '../../utils/ui.js';
7
- import { logger } from '../../utils/logger.js';
7
+ import { fail } from '../../utils/output.js';
8
+ import { requireInteractive } from '../../utils/confirm.js';
9
+ import { normalizePhone } from '../../utils/phone.js';
8
10
  import { addExamples, addNotes } from '../../utils/help.js';
9
11
  import { runBrowserLogin } from '../auth/browser-login.js';
10
12
  import { activateIntegration, apiHeaders, connectNewNumber, fetchIntegrations, integrationLabel, whatsappNumbers, } from './connect-flow.js';
@@ -61,12 +63,12 @@ async function ensureSession(opts) {
61
63
  return creds;
62
64
  }
63
65
  catch {
64
- console.log(theme.muted(' No tienes una sesión activa. Vamos a iniciarla desde el navegador.'));
66
+ console.log(theme.muted(' No active session. Let\'s sign in from the browser.'));
65
67
  return runBrowserLogin({ dev: opts.dev, browser: opts.browser });
66
68
  }
67
69
  }
68
70
  async function chooseNumber(creds, opts) {
69
- const spinner = createSpinner('Buscando números conectados...').start();
71
+ const spinner = createSpinner('Looking for connected numbers...').start();
70
72
  const numbers = whatsappNumbers(await fetchIntegrations(creds, opts.dev)).filter(n => !n.isUnofficialWsp);
71
73
  spinner.stop();
72
74
  let choice = 'new';
@@ -74,11 +76,11 @@ async function chooseNumber(creds, opts) {
74
76
  const answer = await inquirer.prompt([{
75
77
  type: 'list',
76
78
  name: 'choice',
77
- message: '¿Qué número quieres usar?',
79
+ message: 'Which number do you want to use?',
78
80
  choices: [
79
- ...numbers.map(n => ({ name: `${integrationLabel(n)}${n.isActive ? '' : chalk.hex('#EF5350')(' (inactivo)')}`, value: n.id })),
81
+ ...numbers.map(n => ({ name: `${integrationLabel(n)}${n.isActive ? '' : chalk.hex('#EF5350')(' (inactive)')}`, value: n.id })),
80
82
  new inquirer.Separator(),
81
- { name: 'Conectar un número nuevo', value: 'new' },
83
+ { name: 'Connect a new number', value: 'new' },
82
84
  ],
83
85
  }]);
84
86
  choice = answer.choice;
@@ -86,11 +88,11 @@ async function chooseNumber(creds, opts) {
86
88
  if (choice !== 'new') {
87
89
  const selected = numbers.find(n => n.id === choice);
88
90
  if (!selected.isActive) {
89
- const act = createSpinner('Activando el número...').start();
91
+ const act = createSpinner('Activating the number...').start();
90
92
  if (await activateIntegration(creds, selected.id, opts.dev))
91
- act.succeed('Número activo');
93
+ act.succeed('Number active');
92
94
  else
93
- act.warn('No se pudo activar. Actívalo en el panel → Integraciones.');
95
+ act.warn('Could not activate it. Activate it in the panel → Integrations.');
94
96
  }
95
97
  console.log(` ${chalk.green('✓')} ${integrationLabel(selected)}`);
96
98
  return selected;
@@ -98,10 +100,10 @@ async function chooseNumber(creds, opts) {
98
100
  const { kind } = await inquirer.prompt([{
99
101
  type: 'list',
100
102
  name: 'kind',
101
- message: '¿Cómo quieres conectar WhatsApp?',
103
+ message: 'How do you want to connect WhatsApp?',
102
104
  choices: [
103
- { name: `WhatsApp API ${theme.muted('— número nuevo o dedicado, ideal para enviar a escala')}`, value: 'api' },
104
- { name: `WhatsApp Business ${theme.muted('— tu app actual en el celular (coexistencia, se vincula con QR)')}`, value: 'business' },
105
+ { name: `WhatsApp API ${theme.muted('— new or dedicated number, best for sending at scale')}`, value: 'api' },
106
+ { name: `WhatsApp Business ${theme.muted('— your current phone app (coexistence, linked by QR)')}`, value: 'business' },
105
107
  ],
106
108
  }]);
107
109
  return connectNewNumber(creds, { business: kind === 'business', dev: opts.dev, openInBrowser: opts.browser });
@@ -110,19 +112,25 @@ async function firstMessageTest(creds, number, dev) {
110
112
  const { phone } = await inquirer.prompt([{
111
113
  type: 'input',
112
114
  name: 'phone',
113
- message: '¿Desde qué celular vas a escribir? (con código de país, p. ej. 51912345678)',
114
- validate: (v) => /^\d{8,15}$/.test(v.replace(/\D/g, '')) || 'Escribe solo el número con código de país.',
115
+ message: 'Which phone will you write from? (international format, e.g. +51912345678)',
116
+ validate: (v) => { try {
117
+ normalizePhone(v);
118
+ return true;
119
+ }
120
+ catch {
121
+ return 'Enter the number with country code, e.g. +51912345678.';
122
+ } },
115
123
  }]);
116
- const digits = String(phone).replace(/\D/g, '');
124
+ const digits = normalizePhone(String(phone));
117
125
  const target = number.cellphoneNumber || '';
118
126
  console.log();
119
- console.log(` Desde ese celular envía ${chalk.bold('"hola"')} por WhatsApp a ${chalk.bold(`+${target}`)}`);
127
+ console.log(` From that phone, send ${chalk.bold('"hello"')} on WhatsApp to ${chalk.bold(`+${target}`)}`);
120
128
  if (target)
121
- console.log(` ${theme.muted('Atajo:')} ${chalk.underline(`https://wa.me/${target}?text=hola`)}`);
122
- console.log(theme.muted(' Tu mensaje crea el contacto y abre la ventana de 24 h en la que WhatsApp permite responder con texto libre.'));
129
+ console.log(` ${theme.muted('Shortcut:')} ${chalk.underline(`https://wa.me/${target}?text=hello`)}`);
130
+ console.log(theme.muted(' Your message creates the contact and opens the 24h window in which WhatsApp allows free-form replies.'));
123
131
  console.log();
124
132
  const since = Date.now() - 60_000;
125
- const spinner = createSpinner('Esperando tu mensaje... (Ctrl+C para cancelar)').start();
133
+ const spinner = createSpinner('Waiting for your message... (Ctrl+C to cancel)').start();
126
134
  const deadline = Date.now() + 5 * 60_000;
127
135
  let contact = null;
128
136
  while (Date.now() < deadline && !contact) {
@@ -130,28 +138,28 @@ async function firstMessageTest(creds, number, dev) {
130
138
  contact = await findRecentContact(creds, dev, phoneVariants(digits), since, number.codeCellphoneNumberId);
131
139
  }
132
140
  if (!contact) {
133
- spinner.warn('No recibimos tu mensaje en 5 minutos.');
134
- console.log(theme.muted(' Revisa que el número esté activo y vuelve a ejecutar plazbot whatsapp setup.'));
141
+ spinner.warn('Your message did not arrive within 5 minutes.');
142
+ console.log(theme.muted(' Check that the number is active and run plazbot whatsapp setup again.'));
135
143
  return null;
136
144
  }
137
- spinner.succeed(`Recibimos tu mensaje${contact.lastMessage ? `: "${contact.lastMessage}"` : ''}`);
138
- const { reply } = await inquirer.prompt([{ type: 'confirm', name: 'reply', message: '¿Te respondemos desde la API?', default: true }]);
145
+ spinner.succeed(`Message received${contact.lastMessage ? `: "${contact.lastMessage}"` : ''}`);
146
+ const { reply } = await inquirer.prompt([{ type: 'confirm', name: 'reply', message: 'Reply to you from the API?', default: true }]);
139
147
  if (!reply)
140
148
  return contact.internalWhatsappNumber;
141
- const sendSpinner = createSpinner('Enviando respuesta...').start();
149
+ const sendSpinner = createSpinner('Sending reply...').start();
142
150
  try {
143
151
  const res = await axios.post(`${apiBaseUrl(creds.zone, dev)}/api/message`, {
144
152
  workspaceId: creds.workspace,
145
153
  recipientPhone: contact.internalWhatsappNumber,
146
- content: '¡Funciona! 🎉 Este mensaje lo envió tu código a través de la API de Plazbot.',
154
+ content: 'It works! 🎉 This message was sent by your code through the Plazbot API.',
147
155
  }, { headers: apiHeaders(creds) });
148
156
  if (res.data?.success === false)
149
- throw new Error(res.data?.message || 'La API rechazó el envío.');
150
- sendSpinner.succeed('Mensaje enviado. Revisa tu WhatsApp.');
157
+ throw new Error(res.data?.message || 'The API rejected the message.');
158
+ sendSpinner.succeed('Message sent. Check your WhatsApp.');
151
159
  }
152
160
  catch (error) {
153
161
  const msg = axios.isAxiosError(error) ? (error.response?.data?.message || error.message) : error.message;
154
- sendSpinner.fail(`No se pudo enviar: ${msg}`);
162
+ sendSpinner.fail(`Could not send it: ${msg}`);
155
163
  }
156
164
  return contact.internalWhatsappNumber;
157
165
  }
@@ -159,24 +167,24 @@ async function webhookStep(creds, number, dev) {
159
167
  const { url } = await inquirer.prompt([{
160
168
  type: 'input',
161
169
  name: 'url',
162
- message: 'URL de tu webhook para recibir los mensajes entrantes (Enter para omitir):',
170
+ message: 'Webhook URL to receive incoming messages (Enter to skip):',
163
171
  default: number.webhookUrl || '',
164
- validate: (v) => !v || /^https:\/\/\S+$/.test(v) || 'Debe ser una URL https://',
172
+ validate: (v) => !v || /^https:\/\/\S+$/.test(v) || 'It must be an https:// URL',
165
173
  }]);
166
174
  if (!url) {
167
- console.log(theme.muted(' Omitido. Puedes registrarlo después con: plazbot whatsapp register-webhook'));
175
+ console.log(theme.muted(` Skipped. Register it later with: plazbot whatsapp register-webhook -p +${number.cellphoneNumber || '51912345678'} -u <url>`));
168
176
  return null;
169
177
  }
170
- const spinner = createSpinner('Registrando webhook...').start();
178
+ const spinner = createSpinner('Registering webhook...').start();
171
179
  try {
172
180
  const res = await axios.post(`${apiBaseUrl(creds.zone, dev)}/api/workspace/${creds.workspace}/whatsapp/numbers/${number.cellphoneNumber}/webhook`, { webhook: url }, { headers: apiHeaders(creds) });
173
181
  if (res.data?.success === false)
174
182
  throw new Error(res.data?.message);
175
- spinner.succeed(`Webhook registrado: ${url}`);
183
+ spinner.succeed(`Webhook registered: ${url}`);
176
184
  return url;
177
185
  }
178
186
  catch (error) {
179
- spinner.fail(`No se pudo registrar el webhook${error instanceof Error && error.message ? `: ${error.message}` : ''}`);
187
+ spinner.fail(`Could not register the webhook${error instanceof Error && error.message ? `: ${error.message}` : ''}`);
180
188
  return null;
181
189
  }
182
190
  }
@@ -192,51 +200,53 @@ function printSnippet(creds, to) {
192
200
  ``,
193
201
  `await bot.message.onWhatsappMessage({`,
194
202
  ` to: "${to || '51912345678'}",`,
195
- ` message: "Hola desde mi app",`,
203
+ ` message: "Hello from my app",`,
196
204
  `})`,
197
205
  ];
198
- console.log(`\n${chalk.bold('Listo. Usa esto en tu app')} ${theme.muted('(npm i plazbot)')}\n`);
206
+ console.log(`\n${chalk.bold('Done. Use this in your app')} ${theme.muted('(npm i plazbot)')}\n`);
199
207
  code.forEach(l => console.log(' ' + chalk.hex('#9CDCFE')(l)));
200
208
  console.log();
201
- console.log(theme.muted(' Para tu servidor crea una API key en el panel → Configuración → Developer y guárdala en PLAZBOT_KEY.'));
209
+ console.log(theme.muted(' For your server, create an API key in the panel → Settings → Developer and store it in PLAZBOT_KEY.'));
202
210
  console.log(theme.muted(' Docs: https://developers.plazbot.com/sdk'));
203
211
  console.log();
204
212
  }
205
213
  export const whatsappSetupCommand = new Command('setup')
206
- .description('Asistente: conecta WhatsApp y envía/recibe tu primer mensaje')
207
- .option('--no-browser', 'No abrir el navegador automáticamente')
208
- .option('--dev', 'Usar el entorno de desarrollo local', false)
214
+ .description('Guided setup: connect WhatsApp and send/receive your first message')
215
+ .option('--no-browser', 'Print links instead of opening the browser')
216
+ .option('--dev', 'Use development environment', false)
209
217
  .action(async (options) => {
210
218
  try {
211
- console.log(`\n${chalk.bold('WhatsApp · configuración inicial')}`);
212
- step(1, 'Tu cuenta');
219
+ requireInteractive('The WhatsApp setup wizard', 'Use the individual commands instead: plazbot whatsapp connect, send-message, register-webhook.');
220
+ console.log(`\n${chalk.bold('WhatsApp · first-time setup')}`);
221
+ step(1, 'Your account');
213
222
  const creds = await ensureSession(options);
214
- step(2, 'Tu número de WhatsApp');
223
+ step(2, 'Your WhatsApp number');
215
224
  const number = await chooseNumber(creds, options);
216
225
  if (!number) {
217
- console.log(theme.muted('\n Ejecuta plazbot whatsapp setup de nuevo cuando termines el registro en Meta.\n'));
226
+ console.log(theme.muted('\n Run plazbot whatsapp setup again when you finish the Meta sign-up.\n'));
218
227
  process.exit(1);
219
228
  }
220
- step(3, 'Prueba de ida y vuelta');
229
+ step(3, 'Round-trip test');
221
230
  const to = await firstMessageTest(creds, number, options.dev);
222
- step(4, 'Webhook (opcional)');
231
+ step(4, 'Webhook (optional)');
223
232
  await webhookStep(creds, number, options.dev);
224
233
  printSnippet(creds, to);
225
234
  }
226
235
  catch (error) {
227
236
  // Ctrl+C durante un prompt de inquirer
228
237
  if (error instanceof Error && error.name === 'ExitPromptError') {
229
- console.log(theme.muted('\n Cancelado.\n'));
238
+ console.log(theme.muted('\n Cancelled.\n'));
230
239
  process.exit(130);
231
240
  }
232
- logger.error(error instanceof Error ? error.message : 'Error desconocido');
233
- process.exit(1);
241
+ fail(error);
234
242
  }
235
243
  });
236
244
  addExamples(whatsappSetupCommand, [
237
- { description: 'Configurar WhatsApp paso a paso', command: 'plazbot whatsapp setup' },
245
+ { description: 'Set up WhatsApp step by step', command: 'plazbot whatsapp setup' },
246
+ { description: 'Print links instead of opening the browser', command: 'plazbot whatsapp setup --no-browser' },
238
247
  ]);
239
248
  addNotes(whatsappSetupCommand, [
240
- 'La prueba usa tu propio celular: le escribes "hola" a tu número y la API te responde.',
241
- 'Para escribir primero a un cliente nuevo necesitas una plantilla aprobada (plazbot whatsapp send-template).',
249
+ 'The test uses your own phone: you text "hello" to your number and the API replies.',
250
+ 'To message a new customer first you need an approved template (plazbot whatsapp send-template).',
251
+ 'Needs an interactive terminal.',
242
252
  ]);
@@ -0,0 +1,75 @@
1
+ // Helpers shared by the WhatsApp commands (local to this folder).
2
+ import { Agent, Message } from 'plazbot';
3
+ import { getStoredCredentials } from '../../utils/credentials.js';
4
+ import { CliError } from '../../utils/output.js';
5
+ import { patchAgent } from '../../utils/agent-patch.js';
6
+ const DEV_URL = 'http://localhost:5090';
7
+ export async function getMessageClient(dev) {
8
+ const credentials = await getStoredCredentials();
9
+ return new Message({
10
+ workspaceId: credentials.workspace,
11
+ apiKey: credentials.apiKey,
12
+ zone: credentials.zone,
13
+ ...(dev && { customUrl: DEV_URL }),
14
+ });
15
+ }
16
+ export async function getAgentClient(dev) {
17
+ const credentials = await getStoredCredentials();
18
+ const agent = new Agent({
19
+ workspaceId: credentials.workspace,
20
+ apiKey: credentials.apiKey,
21
+ zone: credentials.zone,
22
+ ...(dev && { customUrl: DEV_URL }),
23
+ });
24
+ return { agent, credentials };
25
+ }
26
+ /**
27
+ * The SDK only throws on non-2xx; the API may still answer 200 with success:false
28
+ * (e.g. outside the 24h window). Surface that message instead of reporting success.
29
+ */
30
+ export function ensureApiSuccess(response, fallback, hint) {
31
+ if (response && response.success === false) {
32
+ const errors = Array.isArray(response.errors) ? response.errors : [];
33
+ const details = errors.map(e => (typeof e === 'string' ? e : JSON.stringify(e))).join('; ');
34
+ const message = String(response.message || fallback).replace(/\.?$/, '.');
35
+ throw new CliError(details ? `${message} ${details}` : message, 'api_rejected', hint);
36
+ }
37
+ }
38
+ /**
39
+ * Parses `name=value` pairs from --var / --header-var. Strict: a pair without "=",
40
+ * without a name or with an empty value is an error (it is never dropped silently).
41
+ * The value may contain "=" (only the first one splits).
42
+ */
43
+ export function parseVariables(vars, flag) {
44
+ if (!vars)
45
+ return [];
46
+ return vars.map(raw => {
47
+ const idx = raw.indexOf('=');
48
+ const variable = idx >= 0 ? raw.slice(0, idx).trim() : '';
49
+ const value = idx >= 0 ? raw.slice(idx + 1).trim() : '';
50
+ if (idx < 0 || !variable) {
51
+ throw new CliError(`Invalid ${flag} "${raw}".`, 'invalid_variable', `Use the format ${flag} name=value (e.g. ${flag} name=Anna).`);
52
+ }
53
+ if (!value) {
54
+ throw new CliError(`Variable "${variable}" in ${flag} has an empty value.`, 'empty_variable', 'WhatsApp rejects empty template variables. Pass a value, e.g. name=Anna.');
55
+ }
56
+ return { variable, value };
57
+ });
58
+ }
59
+ /** Digits only, to compare numbers stored in different formats (+51 9..., 519...). */
60
+ export function phoneDigits(value) {
61
+ return String(value ?? '').replace(/\D/g, '');
62
+ }
63
+ /** Full agent document (getAgentById answers {agent}, {data} or the agent itself). */
64
+ export async function fetchFullAgent(agent, agentId) {
65
+ const response = await agent.getAgentById({ id: agentId });
66
+ const current = response?.agent || response?.data || response;
67
+ if (!current || !current.name) {
68
+ throw new CliError(`Agent ${agentId} not found in the active workspace.`, 'not_found', "Check the ID with 'plazbot agent list'.");
69
+ }
70
+ return current;
71
+ }
72
+ /** Changes only the given fields of the agent (see utils/agent-patch.ts). */
73
+ export async function updateAgentMerged(credentials, dev, agentId, current, patch) {
74
+ await patchAgent({ apiKey: credentials.apiKey, workspace: credentials.workspace, zone: credentials.zone, dev }, agentId, patch, current);
75
+ }