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.
- package/README.md +67 -111
- package/dist/cli.js +26 -11
- package/dist/commands/agent/ai-config.js +264 -63
- package/dist/commands/agent/chat.js +16 -11
- package/dist/commands/agent/copy.js +49 -13
- package/dist/commands/agent/create.js +62 -17
- package/dist/commands/agent/delete.js +50 -39
- package/dist/commands/agent/enable-widget.js +47 -27
- package/dist/commands/agent/export.js +180 -91
- package/dist/commands/agent/files.js +155 -73
- package/dist/commands/agent/get.js +25 -19
- package/dist/commands/agent/index.js +12 -10
- package/dist/commands/agent/list.js +20 -11
- package/dist/commands/agent/monitor.js +103 -57
- package/dist/commands/agent/on-message.js +55 -32
- package/dist/commands/agent/set.js +230 -82
- package/dist/commands/agent/templates.js +259 -156
- package/dist/commands/agent/tools.js +356 -123
- package/dist/commands/agent/update.js +69 -28
- package/dist/commands/agent/validate.js +72 -40
- package/dist/commands/agent/wizard.js +240 -329
- package/dist/commands/auth/browser-login.js +37 -31
- package/dist/commands/auth/login.js +49 -45
- package/dist/commands/auth/logout.js +20 -11
- package/dist/commands/auth/status.js +36 -12
- package/dist/commands/auth/workspace.js +61 -27
- package/dist/commands/portal/add-agent.js +106 -45
- package/dist/commands/portal/add-link.js +25 -28
- package/dist/commands/portal/clear-links.js +39 -19
- package/dist/commands/portal/create.js +66 -37
- package/dist/commands/portal/delete.js +40 -45
- package/dist/commands/portal/get.js +21 -54
- package/dist/commands/portal/index.js +12 -12
- package/dist/commands/portal/list.js +34 -52
- package/dist/commands/portal/shared.js +92 -0
- package/dist/commands/portal/update.js +100 -70
- package/dist/commands/whatsapp/broadcast.js +169 -77
- package/dist/commands/whatsapp/channels.js +167 -65
- package/dist/commands/whatsapp/chat.js +45 -46
- package/dist/commands/whatsapp/connect-flow.js +16 -15
- package/dist/commands/whatsapp/connect.js +36 -21
- package/dist/commands/whatsapp/delete-webhook.js +42 -25
- package/dist/commands/whatsapp/index.js +15 -16
- package/dist/commands/whatsapp/register-webhook.js +43 -26
- package/dist/commands/whatsapp/send-template.js +62 -61
- package/dist/commands/whatsapp/send.js +46 -33
- package/dist/commands/whatsapp/setup.js +62 -52
- package/dist/commands/whatsapp/shared.js +75 -0
- package/dist/commands/whatsapp/widget.js +82 -45
- package/dist/commands/workers/deploy.js +80 -58
- package/dist/commands/workers/index.js +8 -8
- package/dist/commands/workers/list.js +42 -20
- package/dist/commands/workers/logs.js +139 -36
- package/dist/commands/workers/remove.js +45 -47
- package/dist/commands/workers/secret.js +159 -83
- package/dist/commands/workers/test.js +108 -60
- package/dist/schemas/agent.config.schema.json +159 -8
- package/dist/studio/api/sseClient.js +1 -1
- package/dist/studio/api/studioApi.js +8 -0
- package/dist/studio/components/App.js +12 -0
- package/dist/studio/components/Footer.js +1 -1
- package/dist/studio/index.js +74 -14
- package/dist/studio/runOneShot.js +62 -23
- package/dist/studio/runRepl.js +5 -5
- package/dist/studio/slash/handlers.js +11 -8
- package/dist/studio/state/store.js +1 -1
- package/dist/utils/agent-errors.js +14 -11
- package/dist/utils/agent-patch.js +60 -0
- package/dist/utils/banner.js +9 -5
- package/dist/utils/confirm.js +31 -0
- package/dist/utils/credentials.js +13 -3
- package/dist/utils/logger.js +13 -10
- package/dist/utils/output.js +78 -0
- package/dist/utils/phone.js +22 -0
- package/dist/utils/ui.js +14 -11
- package/package.json +3 -3
|
@@ -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
|
|
9
|
-
.
|
|
10
|
-
.requiredOption('-t, --template <name>', 'Template name')
|
|
11
|
-
.option('--var <
|
|
12
|
-
.option('--header-var <
|
|
13
|
-
.option('--file-url <url>', '
|
|
14
|
-
.option('--file-name <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
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
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 (
|
|
40
|
-
|
|
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 (
|
|
43
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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: '
|
|
75
|
-
command: 'plazbot whatsapp send-template -p 51912345678 -t
|
|
76
|
-
{ description: '
|
|
77
|
-
command: 'plazbot whatsapp send-template -p 51912345678 -t
|
|
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
|
|
9
|
-
.
|
|
10
|
-
.
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const spinner = createSpinner('Sending...');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
message: options.message,
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
logger.error(message);
|
|
38
|
-
process.exit(1);
|
|
44
|
+
fail(error);
|
|
39
45
|
}
|
|
40
46
|
});
|
|
41
47
|
addExamples(sendMessageCommand, [
|
|
42
|
-
{ description: 'Send a quick
|
|
43
|
-
command: 'plazbot whatsapp send-message -
|
|
44
|
-
{ description: '
|
|
45
|
-
command: 'plazbot whatsapp send-message -
|
|
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 {
|
|
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
|
|
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('
|
|
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: '
|
|
79
|
+
message: 'Which number do you want to use?',
|
|
78
80
|
choices: [
|
|
79
|
-
...numbers.map(n => ({ name: `${integrationLabel(n)}${n.isActive ? '' : chalk.hex('#EF5350')(' (
|
|
81
|
+
...numbers.map(n => ({ name: `${integrationLabel(n)}${n.isActive ? '' : chalk.hex('#EF5350')(' (inactive)')}`, value: n.id })),
|
|
80
82
|
new inquirer.Separator(),
|
|
81
|
-
{ name: '
|
|
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('
|
|
91
|
+
const act = createSpinner('Activating the number...').start();
|
|
90
92
|
if (await activateIntegration(creds, selected.id, opts.dev))
|
|
91
|
-
act.succeed('
|
|
93
|
+
act.succeed('Number active');
|
|
92
94
|
else
|
|
93
|
-
act.warn('
|
|
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: '
|
|
103
|
+
message: 'How do you want to connect WhatsApp?',
|
|
102
104
|
choices: [
|
|
103
|
-
{ name: `WhatsApp API ${theme.muted('—
|
|
104
|
-
{ name: `WhatsApp Business ${theme.muted('—
|
|
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: '
|
|
114
|
-
validate: (v) =>
|
|
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)
|
|
124
|
+
const digits = normalizePhone(String(phone));
|
|
117
125
|
const target = number.cellphoneNumber || '';
|
|
118
126
|
console.log();
|
|
119
|
-
console.log(`
|
|
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('
|
|
122
|
-
console.log(theme.muted('
|
|
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('
|
|
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('
|
|
134
|
-
console.log(theme.muted('
|
|
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(`
|
|
138
|
-
const { reply } = await inquirer.prompt([{ type: 'confirm', name: 'reply', message: '
|
|
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('
|
|
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: '
|
|
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 || '
|
|
150
|
-
sendSpinner.succeed('
|
|
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(`
|
|
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
|
|
170
|
+
message: 'Webhook URL to receive incoming messages (Enter to skip):',
|
|
163
171
|
default: number.webhookUrl || '',
|
|
164
|
-
validate: (v) => !v || /^https:\/\/\S+$/.test(v) || '
|
|
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(
|
|
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('
|
|
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
|
|
183
|
+
spinner.succeed(`Webhook registered: ${url}`);
|
|
176
184
|
return url;
|
|
177
185
|
}
|
|
178
186
|
catch (error) {
|
|
179
|
-
spinner.fail(`
|
|
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: "
|
|
203
|
+
` message: "Hello from my app",`,
|
|
196
204
|
`})`,
|
|
197
205
|
];
|
|
198
|
-
console.log(`\n${chalk.bold('
|
|
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('
|
|
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('
|
|
207
|
-
.option('--no-browser', '
|
|
208
|
-
.option('--dev', '
|
|
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
|
-
|
|
212
|
-
|
|
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, '
|
|
223
|
+
step(2, 'Your WhatsApp number');
|
|
215
224
|
const number = await chooseNumber(creds, options);
|
|
216
225
|
if (!number) {
|
|
217
|
-
console.log(theme.muted('\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, '
|
|
229
|
+
step(3, 'Round-trip test');
|
|
221
230
|
const to = await firstMessageTest(creds, number, options.dev);
|
|
222
|
-
step(4, 'Webhook (
|
|
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
|
|
238
|
+
console.log(theme.muted('\n Cancelled.\n'));
|
|
230
239
|
process.exit(130);
|
|
231
240
|
}
|
|
232
|
-
|
|
233
|
-
process.exit(1);
|
|
241
|
+
fail(error);
|
|
234
242
|
}
|
|
235
243
|
});
|
|
236
244
|
addExamples(whatsappSetupCommand, [
|
|
237
|
-
{ description: '
|
|
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
|
-
'
|
|
241
|
-
'
|
|
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
|
+
}
|