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.
- 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 +179 -88
- package/dist/commands/agent/files.js +155 -73
- package/dist/commands/agent/get.js +24 -16
- package/dist/commands/agent/index.js +12 -10
- package/dist/commands/agent/list.js +20 -11
- package/dist/commands/agent/monitor.js +101 -53
- 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 +183 -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 +77 -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 +2 -2
|
@@ -1,100 +1,192 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import
|
|
3
|
-
import { getStoredCredentials } from '../../utils/credentials.js';
|
|
2
|
+
import fs from 'fs/promises';
|
|
4
3
|
import { logger } from '../../utils/logger.js';
|
|
5
|
-
import { addExamples } from '../../utils/help.js';
|
|
4
|
+
import { addExamples, addNotes } from '../../utils/help.js';
|
|
6
5
|
import { theme, section, progressBar } from '../../utils/ui.js';
|
|
7
|
-
import
|
|
6
|
+
import { jsonOption, isJsonMode, printJson, fail, CliError, describeError } from '../../utils/output.js';
|
|
7
|
+
import { addYesOption, confirmAction } from '../../utils/confirm.js';
|
|
8
|
+
import { normalizePhone } from '../../utils/phone.js';
|
|
9
|
+
import { ensureApiSuccess, getMessageClient, parseVariables } from './shared.js';
|
|
10
|
+
/**
|
|
11
|
+
* Plain list of numbers: one per line, or separated by commas/semicolons.
|
|
12
|
+
* Ignores blank lines, `#` comments and a first header line that isn't a number.
|
|
13
|
+
*/
|
|
14
|
+
export function parsePhoneList(content) {
|
|
15
|
+
const phones = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const invalid = [];
|
|
18
|
+
let duplicates = 0;
|
|
19
|
+
let header;
|
|
20
|
+
let first = true;
|
|
21
|
+
content.replace(/^/, '').split(/\r?\n/).forEach((rawLine, idx) => {
|
|
22
|
+
const line = rawLine.trim();
|
|
23
|
+
if (!line || line.startsWith('#'))
|
|
24
|
+
return;
|
|
25
|
+
for (const piece of line.split(/[,;\t]/)) {
|
|
26
|
+
const entry = piece.trim().replace(/^["']|["']$/g, '').trim();
|
|
27
|
+
if (!entry)
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
const phone = normalizePhone(entry);
|
|
31
|
+
if (seen.has(phone))
|
|
32
|
+
duplicates++;
|
|
33
|
+
else {
|
|
34
|
+
seen.add(phone);
|
|
35
|
+
phones.push(phone);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (first && /[a-z]/i.test(entry)) {
|
|
40
|
+
header = line; // e.g. "phone" or "phone,name": skip the whole line
|
|
41
|
+
first = false;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
invalid.push({ input: entry, line: idx + 1, error: describeError(error).message });
|
|
45
|
+
}
|
|
46
|
+
first = false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return { phones, invalid, duplicates, header };
|
|
50
|
+
}
|
|
8
51
|
export const broadcastCommand = new Command('broadcast')
|
|
9
|
-
.description('
|
|
10
|
-
.requiredOption('-t, --template <name>', 'Template name')
|
|
11
|
-
.requiredOption('--phones <file>', '
|
|
12
|
-
.option('--var <
|
|
13
|
-
.option('--delay <ms>', '
|
|
14
|
-
.option('--
|
|
15
|
-
.
|
|
52
|
+
.description('Send an approved WhatsApp template to a list of numbers')
|
|
53
|
+
.requiredOption('-t, --template <name>', 'Template name exactly as approved in Meta')
|
|
54
|
+
.requiredOption('--phones <file>', 'Text file with the numbers: one per line (commas or semicolons also work)')
|
|
55
|
+
.option('--var <name=value...>', 'Body variables sent to every recipient, name=value pairs')
|
|
56
|
+
.option('--delay <ms>', 'Pause between messages in milliseconds', '1000')
|
|
57
|
+
.option('--dry-run', 'Validate the list and show what would be sent, without sending', false)
|
|
58
|
+
.addOption(jsonOption())
|
|
59
|
+
.option('--dev', 'Use development environment', false);
|
|
60
|
+
addYesOption(broadcastCommand);
|
|
61
|
+
broadcastCommand.action(async (options) => {
|
|
16
62
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let
|
|
63
|
+
const template = String(options.template || '').trim();
|
|
64
|
+
if (!template)
|
|
65
|
+
throw new CliError('Template name is required.', 'missing_template', 'Pass it with -t, --template <name>.');
|
|
66
|
+
const delay = Number(options.delay);
|
|
67
|
+
if (!Number.isFinite(delay) || delay < 0) {
|
|
68
|
+
throw new CliError(`Invalid --delay "${options.delay}".`, 'invalid_delay', 'Use a number of milliseconds, e.g. --delay 1000.');
|
|
69
|
+
}
|
|
70
|
+
const variablesBody = parseVariables(options.var, '--var');
|
|
71
|
+
let content;
|
|
26
72
|
try {
|
|
27
|
-
|
|
28
|
-
phones = content.split('\n')
|
|
29
|
-
.map(line => line.trim())
|
|
30
|
-
.filter(line => line && !line.startsWith('#'));
|
|
73
|
+
content = await fs.readFile(options.phones, 'utf-8');
|
|
31
74
|
}
|
|
32
75
|
catch {
|
|
33
|
-
throw new
|
|
76
|
+
throw new CliError(`Could not read file: ${options.phones}`, 'file_not_found', 'Check the path to your list of numbers.');
|
|
77
|
+
}
|
|
78
|
+
const list = parsePhoneList(content);
|
|
79
|
+
const json = isJsonMode();
|
|
80
|
+
if (!json) {
|
|
81
|
+
console.log(section(options.dryRun ? 'WhatsApp Broadcast (dry run)' : 'WhatsApp Broadcast'));
|
|
82
|
+
logger.label('Template', template);
|
|
83
|
+
logger.label('Recipients', String(list.phones.length));
|
|
84
|
+
if (variablesBody.length > 0)
|
|
85
|
+
logger.label('Variables', variablesBody.map(v => `${v.variable}=${v.value}`).join(', '));
|
|
86
|
+
logger.label('Delay', `${delay}ms`);
|
|
87
|
+
if (list.header)
|
|
88
|
+
logger.dim(`Skipped header line: "${list.header}"`);
|
|
89
|
+
if (list.duplicates > 0)
|
|
90
|
+
logger.dim(`Skipped ${list.duplicates} duplicate number(s).`);
|
|
91
|
+
if (list.invalid.length > 0) {
|
|
92
|
+
logger.warning(`${list.invalid.length} invalid entr${list.invalid.length === 1 ? 'y' : 'ies'} (not sent):`);
|
|
93
|
+
list.invalid.forEach(i => console.log(theme.error(` ✖ line ${i.line}: "${i.input}"`)));
|
|
94
|
+
}
|
|
95
|
+
console.log();
|
|
34
96
|
}
|
|
35
|
-
if (phones.length === 0) {
|
|
36
|
-
throw new
|
|
97
|
+
if (list.phones.length === 0) {
|
|
98
|
+
throw new CliError('The file has no valid phone numbers.', 'no_recipients', 'Put one number per line in international format, e.g. +51912345678.');
|
|
37
99
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
if (options.dryRun) {
|
|
101
|
+
if (json) {
|
|
102
|
+
printJson({
|
|
103
|
+
dryRun: true, template, variables: variablesBody,
|
|
104
|
+
recipients: list.phones,
|
|
105
|
+
summary: { total: list.phones.length, invalid: list.invalid, duplicates: list.duplicates },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
list.phones.forEach(p => console.log(` ${theme.muted('•')} +${p}`));
|
|
110
|
+
logger.success(`Dry run: ${list.phones.length} message(s) would be sent with template "${template}". Nothing was sent.`);
|
|
111
|
+
}
|
|
112
|
+
if (list.invalid.length > 0)
|
|
113
|
+
process.exit(1);
|
|
114
|
+
return;
|
|
46
115
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
116
|
+
if (!(await confirmAction(`Send template "${template}" to ${list.phones.length} number(s)?`, options))) {
|
|
117
|
+
logger.dim('Aborted.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const messageClient = await getMessageClient(options.dev);
|
|
121
|
+
const results = [];
|
|
122
|
+
const tty = !json && process.stdout.isTTY;
|
|
123
|
+
const total = list.phones.length;
|
|
124
|
+
const draw = (done) => {
|
|
125
|
+
if (tty)
|
|
126
|
+
process.stdout.write(`\r\x1b[2K ${progressBar(done, total)}`);
|
|
127
|
+
};
|
|
128
|
+
draw(0);
|
|
129
|
+
for (let i = 0; i < total; i++) {
|
|
130
|
+
const phone = list.phones[i];
|
|
53
131
|
try {
|
|
54
|
-
const params = {
|
|
55
|
-
to: phone,
|
|
56
|
-
template: options.template,
|
|
57
|
-
};
|
|
132
|
+
const params = { to: phone, template };
|
|
58
133
|
if (variablesBody.length > 0)
|
|
59
134
|
params.variablesBody = variablesBody;
|
|
60
|
-
await messageClient.onConversation(params);
|
|
61
|
-
sent
|
|
62
|
-
|
|
63
|
-
catch {
|
|
64
|
-
failed++;
|
|
65
|
-
console.log(theme.error(` ✖ Failed: ${phone}`));
|
|
135
|
+
const response = await messageClient.onConversation(params);
|
|
136
|
+
ensureApiSuccess(response, 'Not sent');
|
|
137
|
+
results.push({ phone, status: 'sent' });
|
|
66
138
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
139
|
+
catch (error) {
|
|
140
|
+
const msg = describeError(error).message;
|
|
141
|
+
results.push({ phone, status: 'failed', error: msg });
|
|
142
|
+
if (!json) {
|
|
143
|
+
if (tty)
|
|
144
|
+
process.stdout.write('\r\x1b[2K');
|
|
145
|
+
console.log(theme.error(` ✖ +${phone}: ${msg}`));
|
|
146
|
+
}
|
|
72
147
|
}
|
|
148
|
+
draw(i + 1);
|
|
149
|
+
if (i < total - 1 && delay > 0)
|
|
150
|
+
await new Promise(r => setTimeout(r, delay));
|
|
73
151
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
152
|
+
if (tty)
|
|
153
|
+
process.stdout.write('\n');
|
|
154
|
+
const sent = results.filter(r => r.status === 'sent').length;
|
|
155
|
+
const failed = total - sent;
|
|
156
|
+
if (json) {
|
|
157
|
+
printJson({
|
|
158
|
+
template, results,
|
|
159
|
+
summary: { total, sent, failed, invalid: list.invalid, duplicates: list.duplicates },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(section('Result'));
|
|
164
|
+
logger.label('Sent', theme.success(String(sent)));
|
|
165
|
+
logger.label('Failed', failed > 0 ? theme.error(String(failed)) : '0');
|
|
166
|
+
if (list.invalid.length > 0)
|
|
167
|
+
logger.label('Invalid (skipped)', theme.error(String(list.invalid.length)));
|
|
168
|
+
logger.label('Total', String(total));
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
if (failed > 0 || list.invalid.length > 0)
|
|
172
|
+
process.exit(1);
|
|
78
173
|
}
|
|
79
174
|
catch (error) {
|
|
80
|
-
|
|
81
|
-
logger.error(message);
|
|
82
|
-
process.exit(1);
|
|
175
|
+
fail(error);
|
|
83
176
|
}
|
|
84
177
|
});
|
|
85
178
|
addExamples(broadcastCommand, [
|
|
86
|
-
{ description: '
|
|
87
|
-
command: 'plazbot whatsapp broadcast -t welcome --phones ./
|
|
88
|
-
{ description: '
|
|
89
|
-
command: 'plazbot whatsapp broadcast -t
|
|
90
|
-
{ description: '
|
|
91
|
-
command: 'plazbot whatsapp broadcast -t
|
|
179
|
+
{ description: 'Check the list first: validates numbers, sends nothing',
|
|
180
|
+
command: 'plazbot whatsapp broadcast -t welcome --phones ./numbers.txt --dry-run' },
|
|
181
|
+
{ description: 'Send a template to every number (asks for confirmation)',
|
|
182
|
+
command: 'plazbot whatsapp broadcast -t welcome --phones ./numbers.txt' },
|
|
183
|
+
{ description: 'Same body variables for everyone, 3s between messages',
|
|
184
|
+
command: 'plazbot whatsapp broadcast -t promo --phones ./numbers.txt --var amount=20 code=PROMO20 --delay 3000' },
|
|
185
|
+
{ description: 'Unattended run with per-number results as JSON',
|
|
186
|
+
command: 'plazbot whatsapp broadcast -t reminder --phones ./numbers.txt --yes --json > results.json' },
|
|
187
|
+
]);
|
|
188
|
+
addNotes(broadcastCommand, [
|
|
189
|
+
'The file is a plain list, e.g. "+51912345678" on each line (commas or semicolons also work). Blank lines, # comments and a header line are ignored; duplicates are sent once.',
|
|
190
|
+
'Invalid numbers are listed and skipped. The command exits with code 1 if any number was invalid or any send failed.',
|
|
191
|
+
'Variables are the same for every recipient. Templates can be sent outside the 24h window.',
|
|
92
192
|
]);
|
|
93
|
-
function parseVariables(vars) {
|
|
94
|
-
if (!vars)
|
|
95
|
-
return [];
|
|
96
|
-
return vars.map(v => {
|
|
97
|
-
const [variable, ...rest] = v.split('=');
|
|
98
|
-
return { variable: variable.trim(), value: rest.join('=').trim() };
|
|
99
|
-
}).filter(v => v.variable && v.value);
|
|
100
|
-
}
|
|
@@ -1,92 +1,194 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { Agent } 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, createTable, theme, section, statusBadge } from '../../utils/ui.js';
|
|
5
|
+
import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
|
|
6
|
+
import { addYesOption, confirmAction } from '../../utils/confirm.js';
|
|
7
|
+
import { phoneOption, normalizePhone } from '../../utils/phone.js';
|
|
8
|
+
import { fetchFullAgent, getAgentClient, phoneDigits, updateAgentMerged } from './shared.js';
|
|
9
|
+
const agentIdOf = (a) => String(a?.id || a?._id || '');
|
|
10
|
+
const whatsappKeys = (a) => (Array.isArray(a?.channels) ? a.channels : [])
|
|
11
|
+
.filter((c) => c?.channel === 'whatsapp' && c?.key)
|
|
12
|
+
.map((c) => String(c.key));
|
|
13
|
+
const hasNumber = (a, phone) => whatsappKeys(a).some(k => phoneDigits(k) === phone);
|
|
14
|
+
// ── channels ──────────────────────────────────────────────────────────────────
|
|
7
15
|
export const channelsCommand = new Command('channels')
|
|
8
|
-
.description('List agents
|
|
16
|
+
.description('List agents that answer on a WhatsApp number')
|
|
17
|
+
.addOption(jsonOption())
|
|
9
18
|
.option('--dev', 'Use development environment', false)
|
|
10
19
|
.action(async (options) => {
|
|
11
20
|
try {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const { agent } = await getAgentClient(options.dev);
|
|
22
|
+
const spinner = createSpinner('Loading channels...').start();
|
|
23
|
+
let agents;
|
|
24
|
+
try {
|
|
25
|
+
agents = (await agent.getAgents()) || [];
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
spinner.stop();
|
|
29
|
+
}
|
|
30
|
+
const rows = agents
|
|
31
|
+
.filter(a => whatsappKeys(a).length > 0)
|
|
32
|
+
.map(a => ({
|
|
33
|
+
agentId: agentIdOf(a),
|
|
34
|
+
name: String(a.name || ''),
|
|
35
|
+
phones: whatsappKeys(a).map(phoneDigits),
|
|
36
|
+
enabled: a.enable !== false,
|
|
37
|
+
}));
|
|
38
|
+
if (isJsonMode()) {
|
|
39
|
+
printJson(rows);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
23
42
|
console.log(section('WhatsApp Channels'));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
console.log(theme.muted('
|
|
27
|
-
console.log(theme.muted(' Use the wizard: plazbot agent create\n'));
|
|
43
|
+
if (rows.length === 0) {
|
|
44
|
+
console.log(theme.muted('\n No agents have a WhatsApp number assigned yet.'));
|
|
45
|
+
console.log(theme.muted(' Assign one: plazbot whatsapp assign -p +51912345678 -a <agentId>\n'));
|
|
28
46
|
return;
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
console.log(createTable(['Agent ID', 'Name', 'WhatsApp Number', 'Status'], rows));
|
|
48
|
+
console.log(createTable(['Agent ID', 'Name', 'WhatsApp number', 'Status'], rows.map(r => [r.agentId, r.name, r.phones.map(p => `+${p}`).join(', '), statusBadge(r.enabled)])));
|
|
49
|
+
// Same number on several agents: warn, only one should answer.
|
|
50
|
+
const owners = new Map();
|
|
51
|
+
rows.forEach(r => r.phones.forEach(p => owners.set(p, [...(owners.get(p) || []), r.name || r.agentId])));
|
|
52
|
+
for (const [p, names] of owners) {
|
|
53
|
+
if (names.length > 1) {
|
|
54
|
+
logger.warning(`+${p} is assigned to ${names.length} agents (${names.join(', ')}). Fix it with: plazbot whatsapp assign -p +${p} -a <agentId>`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
58
|
catch (error) {
|
|
42
|
-
|
|
43
|
-
logger.error(message);
|
|
44
|
-
process.exit(1);
|
|
59
|
+
fail(error);
|
|
45
60
|
}
|
|
46
61
|
});
|
|
47
62
|
addExamples(channelsCommand, [
|
|
48
|
-
{ description: 'List agents
|
|
63
|
+
{ description: 'List agents with a WhatsApp number assigned',
|
|
49
64
|
command: 'plazbot whatsapp channels' },
|
|
65
|
+
{ description: 'Machine-readable list',
|
|
66
|
+
command: 'plazbot whatsapp channels --json' },
|
|
50
67
|
]);
|
|
51
68
|
export const assignCommand = new Command('assign')
|
|
52
|
-
.description('
|
|
53
|
-
.
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
.
|
|
69
|
+
.description('Make an agent answer a WhatsApp number (moves it from any other agent)')
|
|
70
|
+
.addOption(phoneOption('WhatsApp number in international format, e.g. +51912345678'))
|
|
71
|
+
.option('-a, --agent <agentId>', 'Agent that will answer the number (e.g. age_AbcDef123)')
|
|
72
|
+
.argument('[phone]', 'Legacy positional form of --phone')
|
|
73
|
+
.argument('[agentId]', 'Legacy positional form of --agent')
|
|
74
|
+
.addOption(jsonOption())
|
|
75
|
+
.option('--dev', 'Use development environment', false);
|
|
76
|
+
addYesOption(assignCommand);
|
|
77
|
+
assignCommand.action(async (posPhone, posAgent, options) => {
|
|
57
78
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
// `assign -p <phone> <agentId>`: the first positional is the agent.
|
|
80
|
+
if (options.phone && posPhone && !posAgent) {
|
|
81
|
+
posAgent = posPhone;
|
|
82
|
+
posPhone = undefined;
|
|
83
|
+
}
|
|
84
|
+
const phone = normalizePhone(options.phone ?? posPhone);
|
|
85
|
+
const agentId = String(options.agent ?? posAgent ?? '').trim();
|
|
86
|
+
if (!agentId) {
|
|
87
|
+
throw new CliError('Missing agent ID.', 'missing_agent', "Pass it with -a, --agent <agentId> (see 'plazbot agent list').");
|
|
88
|
+
}
|
|
89
|
+
const { agent, credentials } = await getAgentClient(options.dev);
|
|
90
|
+
const spinner = createSpinner('Loading agents...').start();
|
|
91
|
+
let target;
|
|
92
|
+
let others;
|
|
93
|
+
try {
|
|
94
|
+
target = await fetchFullAgent(agent, agentId);
|
|
95
|
+
const all = (await agent.getAgents()) || [];
|
|
96
|
+
others = all.filter(a => agentIdOf(a) && agentIdOf(a) !== agentId && hasNumber(a, phone));
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
spinner.stop();
|
|
100
|
+
}
|
|
101
|
+
const targetName = String(target.name || agentId);
|
|
102
|
+
const alreadyOnTarget = hasNumber(target, phone);
|
|
103
|
+
const removedFrom = others.map(a => ({ id: agentIdOf(a), name: String(a.name || '') }));
|
|
104
|
+
if (alreadyOnTarget && others.length === 0) {
|
|
105
|
+
if (isJsonMode()) {
|
|
106
|
+
printJson({ phone, agentId, agentName: targetName, assigned: true, changed: false, removedFrom: [] });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
logger.success(`+${phone} is already assigned to "${targetName}". Nothing to change.`);
|
|
110
|
+
}
|
|
74
111
|
return;
|
|
75
112
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
if (others.length > 0) {
|
|
114
|
+
if (!isJsonMode()) {
|
|
115
|
+
console.log(theme.muted(`\n +${phone} is currently assigned to:`));
|
|
116
|
+
removedFrom.forEach(a => console.log(` ${theme.muted('•')} ${a.name || a.id} ${theme.muted(`(${a.id})`)}`));
|
|
117
|
+
console.log();
|
|
118
|
+
}
|
|
119
|
+
const question = `Move +${phone} to "${targetName}" and remove it from ${others.length} other agent${others.length > 1 ? 's' : ''}?`;
|
|
120
|
+
if (!(await confirmAction(question, options))) {
|
|
121
|
+
logger.dim('Aborted.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 1) Assign to the target first, so the number is never left without an agent.
|
|
126
|
+
if (!alreadyOnTarget) {
|
|
127
|
+
const s = createSpinner(`Assigning +${phone} to "${targetName}"...`).start();
|
|
128
|
+
try {
|
|
129
|
+
const channels = [
|
|
130
|
+
...(Array.isArray(target.channels) ? target.channels : []).map((c) => ({
|
|
131
|
+
channel: c.channel || '', key: c.key || '', multianswer: c.multianswer || false,
|
|
132
|
+
})),
|
|
133
|
+
{ channel: 'whatsapp', key: phone, multianswer: false },
|
|
134
|
+
];
|
|
135
|
+
await updateAgentMerged(credentials, options.dev, agentId, target, { channels });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
s.stop();
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
s.succeed(`+${phone} assigned to "${targetName}"`);
|
|
142
|
+
}
|
|
143
|
+
// 2) Remove the number from every other agent (full document, only channels change).
|
|
144
|
+
const failures = [];
|
|
145
|
+
for (const other of removedFrom) {
|
|
146
|
+
const s = createSpinner(`Removing +${phone} from "${other.name || other.id}"...`).start();
|
|
147
|
+
try {
|
|
148
|
+
const full = await fetchFullAgent(agent, other.id);
|
|
149
|
+
const channels = (Array.isArray(full.channels) ? full.channels : [])
|
|
150
|
+
.filter((c) => !(c?.channel === 'whatsapp' && phoneDigits(c?.key) === phone));
|
|
151
|
+
await updateAgentMerged(credentials, options.dev, other.id, full, { channels });
|
|
152
|
+
s.succeed(`Removed +${phone} from "${other.name || other.id}"`);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
156
|
+
s.fail(`Could not remove +${phone} from "${other.name || other.id}": ${msg}`);
|
|
157
|
+
failures.push({ ...other, error: msg });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const moved = removedFrom.filter(a => !failures.some(f => f.id === a.id));
|
|
161
|
+
if (isJsonMode()) {
|
|
162
|
+
printJson({
|
|
163
|
+
phone, agentId, agentName: targetName, assigned: true, changed: true,
|
|
164
|
+
removedFrom: moved,
|
|
165
|
+
...(failures.length > 0 && { failed: failures }),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else if (moved.length > 0) {
|
|
169
|
+
logger.success(`+${phone} now answered by "${targetName}" (moved from ${moved.map(a => a.name || a.id).join(', ')}).`);
|
|
170
|
+
}
|
|
171
|
+
if (failures.length > 0) {
|
|
172
|
+
if (!isJsonMode()) {
|
|
173
|
+
logger.warning(`+${phone} is still assigned to ${failures.length} other agent(s). Remove it in the panel or re-run this command.`);
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
82
177
|
}
|
|
83
178
|
catch (error) {
|
|
84
|
-
|
|
85
|
-
logger.error(message);
|
|
86
|
-
process.exit(1);
|
|
179
|
+
fail(error);
|
|
87
180
|
}
|
|
88
181
|
});
|
|
89
182
|
addExamples(assignCommand, [
|
|
90
|
-
{ description: '
|
|
91
|
-
command: 'plazbot whatsapp assign 51912345678
|
|
183
|
+
{ description: 'Make an agent answer a number (moves it from any other agent)',
|
|
184
|
+
command: 'plazbot whatsapp assign -p +51912345678 -a age_AbcDef123' },
|
|
185
|
+
{ description: 'Skip the confirmation when the number moves between agents',
|
|
186
|
+
command: 'plazbot whatsapp assign -p +51912345678 -a age_AbcDef123 --yes' },
|
|
187
|
+
{ description: 'Legacy positional form (still supported)',
|
|
188
|
+
command: 'plazbot whatsapp assign 51912345678 age_AbcDef123' },
|
|
189
|
+
]);
|
|
190
|
+
addNotes(assignCommand, [
|
|
191
|
+
'A number is answered by one agent: if another agent has it, it is removed from that agent (asks for confirmation).',
|
|
192
|
+
'Only the channels of the affected agents change; the rest of their configuration is kept.',
|
|
193
|
+
'List the current assignments with plazbot whatsapp channels.',
|
|
92
194
|
]);
|