plazbot-cli 0.3.6 → 0.4.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 +38 -2
- package/dist/commands/agent/export.js +25 -2
- package/dist/commands/auth/browser-login.js +121 -0
- package/dist/commands/auth/index.js +5 -1
- package/dist/commands/auth/login.js +1 -3
- package/dist/commands/auth/logout.js +4 -3
- package/dist/commands/auth/status.js +8 -5
- package/dist/commands/auth/workspace.js +73 -0
- package/dist/commands/whatsapp/connect-flow.js +137 -0
- package/dist/commands/whatsapp/connect.js +26 -160
- package/dist/commands/whatsapp/index.js +4 -0
- package/dist/commands/whatsapp/setup.js +242 -0
- package/dist/studio/components/App.js +1 -1
- package/dist/studio/runOneShot.js +2 -2
- package/dist/studio/runRepl.js +1 -1
- package/dist/utils/agent-errors.js +2 -2
- package/dist/utils/banner.js +10 -9
- package/dist/utils/credentials.js +31 -3
- package/dist/utils/open-browser.js +17 -0
- package/package.json +14 -6
- package/.claude/settings.local.json +0 -15
- package/CLAUDE.md +0 -121
- package/agent.config.json +0 -21
- package/dist/studio/components/AgentPanel.js +0 -35
- package/dist/studio/render/json.js +0 -16
- package/dist/studio/whatsapp/api.js +0 -63
- package/dist/studio/whatsapp/polling.js +0 -77
- package/dist/studio/whatsapp/types.js +0 -31
- package/src/cli.ts +0 -57
- package/src/commands/agent/ai-config.ts +0 -164
- package/src/commands/agent/chat.ts +0 -225
- package/src/commands/agent/copy.ts +0 -46
- package/src/commands/agent/create.ts +0 -103
- package/src/commands/agent/delete.ts +0 -71
- package/src/commands/agent/enable-widget.ts +0 -67
- package/src/commands/agent/export.ts +0 -408
- package/src/commands/agent/files.ts +0 -171
- package/src/commands/agent/get.ts +0 -233
- package/src/commands/agent/index.ts +0 -55
- package/src/commands/agent/list.ts +0 -58
- package/src/commands/agent/monitor.ts +0 -375
- package/src/commands/agent/on-message.ts +0 -82
- package/src/commands/agent/set.ts +0 -149
- package/src/commands/agent/templates.ts +0 -892
- package/src/commands/agent/tools.ts +0 -168
- package/src/commands/agent/update.ts +0 -61
- package/src/commands/agent/validate.ts +0 -135
- package/src/commands/agent/wizard.ts +0 -756
- package/src/commands/auth/index.ts +0 -10
- package/src/commands/auth/login.ts +0 -72
- package/src/commands/auth/logout.ts +0 -28
- package/src/commands/auth/status.ts +0 -28
- package/src/commands/portal/add-agent.ts +0 -66
- package/src/commands/portal/add-link.ts +0 -47
- package/src/commands/portal/clear-links.ts +0 -33
- package/src/commands/portal/create.ts +0 -58
- package/src/commands/portal/delete.ts +0 -70
- package/src/commands/portal/get.ts +0 -77
- package/src/commands/portal/index.ts +0 -32
- package/src/commands/portal/list.ts +0 -79
- package/src/commands/portal/update.ts +0 -93
- package/src/commands/whatsapp/broadcast.ts +0 -110
- package/src/commands/whatsapp/channels.ts +0 -109
- package/src/commands/whatsapp/chat.ts +0 -85
- package/src/commands/whatsapp/connect.ts +0 -220
- package/src/commands/whatsapp/delete-webhook.ts +0 -43
- package/src/commands/whatsapp/index.ts +0 -37
- package/src/commands/whatsapp/register-webhook.ts +0 -47
- package/src/commands/whatsapp/send-template.ts +0 -94
- package/src/commands/whatsapp/send.ts +0 -55
- package/src/commands/whatsapp/widget.ts +0 -75
- package/src/commands/workers/deploy.ts +0 -156
- package/src/commands/workers/index.ts +0 -30
- package/src/commands/workers/list.ts +0 -105
- package/src/commands/workers/logs.ts +0 -99
- package/src/commands/workers/remove.ts +0 -103
- package/src/commands/workers/secret.ts +0 -191
- package/src/commands/workers/test.ts +0 -145
- package/src/schemas/agent.config.schema.json +0 -569
- package/src/studio/api/sseClient.ts +0 -91
- package/src/studio/api/studioApi.ts +0 -27
- package/src/studio/api/types.ts +0 -96
- package/src/studio/components/App.tsx +0 -274
- package/src/studio/components/ChatLog.tsx +0 -121
- package/src/studio/components/ConnectCard.tsx +0 -171
- package/src/studio/components/Footer.tsx +0 -38
- package/src/studio/components/Header.tsx +0 -39
- package/src/studio/components/Input.tsx +0 -176
- package/src/studio/components/Message.tsx +0 -87
- package/src/studio/components/Suggestions.tsx +0 -26
- package/src/studio/components/ToolCall.tsx +0 -58
- package/src/studio/connect/api.ts +0 -98
- package/src/studio/connect/polling.ts +0 -94
- package/src/studio/connect/types.ts +0 -141
- package/src/studio/index.ts +0 -58
- package/src/studio/render/markdown.ts +0 -93
- package/src/studio/render/steps.ts +0 -57
- package/src/studio/runOneShot.ts +0 -114
- package/src/studio/runRepl.tsx +0 -82
- package/src/studio/slash/handlers.ts +0 -226
- package/src/studio/slash/parser.ts +0 -41
- package/src/studio/slash/registry.ts +0 -54
- package/src/studio/state/store.ts +0 -279
- package/src/types/agent.ts +0 -63
- package/src/types/auth.ts +0 -9
- package/src/types/common.ts +0 -10
- package/src/types/message.ts +0 -34
- package/src/types/portal.ts +0 -56
- package/src/types/workers.ts +0 -86
- package/src/utils/agent-errors.ts +0 -67
- package/src/utils/api.ts +0 -35
- package/src/utils/banner.ts +0 -99
- package/src/utils/credentials.ts +0 -38
- package/src/utils/help.ts +0 -51
- package/src/utils/logger.ts +0 -40
- package/src/utils/ui.ts +0 -109
- package/tsconfig.json +0 -18
|
@@ -1,187 +1,53 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import axios from 'axios';
|
|
3
|
-
import ora from 'ora';
|
|
4
2
|
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
5
4
|
import { getStoredCredentials } from '../../utils/credentials.js';
|
|
6
5
|
import { logger } from '../../utils/logger.js';
|
|
7
6
|
import { addExamples } from '../../utils/help.js';
|
|
8
7
|
import { theme, section } from '../../utils/ui.js';
|
|
9
|
-
|
|
10
|
-
if (dev)
|
|
11
|
-
return 'http://localhost:5090';
|
|
12
|
-
return zone === 'EU' ? 'https://apieu.plazbot.com' : 'https://api.plazbot.com';
|
|
13
|
-
}
|
|
14
|
-
function getIntegrationLabel(ig) {
|
|
15
|
-
if (ig.type === 'whatsapp') {
|
|
16
|
-
return ig.aliasName
|
|
17
|
-
? `${ig.aliasName} (${ig.cellphoneNumberFormat || ig.cellphoneNumber || ''})`
|
|
18
|
-
: ig.cellphoneNumberFormat || ig.cellphoneNumber || ig.id;
|
|
19
|
-
}
|
|
20
|
-
return ig.aliasName || ig.srcName || ig.id;
|
|
21
|
-
}
|
|
22
|
-
async function fetchWorkspaceIntegrations(baseUrl, workspaceId, headers) {
|
|
23
|
-
try {
|
|
24
|
-
const wkRes = await axios.get(`${baseUrl}/api/workspace/${workspaceId}`, { headers });
|
|
25
|
-
const wsData = Array.isArray(wkRes.data) ? wkRes.data[0] : wkRes.data;
|
|
26
|
-
return wsData?.integrations || [];
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function activateIntegration(baseUrl, workspaceId, integrationId, headers) {
|
|
33
|
-
try {
|
|
34
|
-
const res = await axios.post(`${baseUrl}/api/workspace/${workspaceId}/integrations/${integrationId}/activate`, { isActive: true }, { headers });
|
|
35
|
-
return res.data?.success === true;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
8
|
+
import { connectNewNumber, fetchIntegrations, integrationLabel, whatsappNumbers } from './connect-flow.js';
|
|
41
9
|
export const whatsappConnectCommand = new Command('connect')
|
|
42
|
-
.description('
|
|
43
|
-
.option('--
|
|
44
|
-
.option('--
|
|
10
|
+
.description('Conecta un número de WhatsApp al workspace')
|
|
11
|
+
.option('--business', 'Conectar tu app WhatsApp Business actual (coexistencia)', false)
|
|
12
|
+
.option('--no-browser', 'No abrir el navegador automáticamente')
|
|
13
|
+
.option('--dev', 'Usar el entorno de desarrollo local', false)
|
|
45
14
|
.action(async (options) => {
|
|
46
15
|
try {
|
|
47
16
|
const credentials = await getStoredCredentials();
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
'x-workspace-id': credentials.workspace,
|
|
52
|
-
'Content-Type': 'application/json',
|
|
53
|
-
};
|
|
54
|
-
const channelType = options.business ? 'whatsapp_business' : 'whatsapp';
|
|
55
|
-
const channelLabel = options.business ? 'WhatsApp Business' : 'WhatsApp';
|
|
56
|
-
console.log(section(`Connect ${channelLabel}`));
|
|
57
|
-
console.log();
|
|
58
|
-
// Cargar integraciones existentes
|
|
59
|
-
const loadSpinner = ora({ text: chalk.gray('Loading existing integrations...'), spinner: 'dots', color: 'green' }).start();
|
|
60
|
-
const allIntegrations = await fetchWorkspaceIntegrations(baseUrl, credentials.workspace, headers);
|
|
61
|
-
const existing = allIntegrations.filter((ig) => ig.type === 'whatsapp');
|
|
17
|
+
console.log(section(options.business ? 'Conectar WhatsApp Business' : 'Conectar WhatsApp'));
|
|
18
|
+
const loadSpinner = ora({ text: chalk.gray('Cargando números conectados...'), spinner: 'dots', color: 'green' }).start();
|
|
19
|
+
const existing = whatsappNumbers(await fetchIntegrations(credentials, options.dev));
|
|
62
20
|
loadSpinner.stop();
|
|
63
21
|
if (existing.length > 0) {
|
|
64
|
-
console.log(theme.muted(` ${existing.length}
|
|
65
|
-
existing.forEach(
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
console.log(` ${theme.muted('•')} ${label} ${status}`);
|
|
22
|
+
console.log(theme.muted(` ${existing.length} número(s) ya conectado(s):`));
|
|
23
|
+
existing.forEach(ig => {
|
|
24
|
+
const status = ig.isActive ? chalk.hex('#66BB6A')('activo') : chalk.hex('#EF5350')('inactivo');
|
|
25
|
+
console.log(` ${theme.muted('•')} ${integrationLabel(ig)} ${status}`);
|
|
69
26
|
});
|
|
70
|
-
console.log();
|
|
71
27
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const getRes = await axios.get(`${baseUrl}/api/workspace/${credentials.workspace}/onboarding-link`, { headers, params: { type: channelType } });
|
|
79
|
-
linkData = getRes.data?.data ?? null;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
linkData = null;
|
|
83
|
-
}
|
|
84
|
-
// 2. Si existe pero esta expirado, regenerar
|
|
85
|
-
if (linkData?.expiresAt && new Date(linkData.expiresAt) < new Date()) {
|
|
86
|
-
try {
|
|
87
|
-
const regenRes = await axios.post(`${baseUrl}/api/workspace/${credentials.workspace}/onboarding-link/regenerate`, { type: channelType }, { headers });
|
|
88
|
-
linkData = regenRes.data?.data ?? null;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
linkData = null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// 3. Si no existe, generar uno nuevo (POST)
|
|
95
|
-
if (!linkData?.token) {
|
|
96
|
-
try {
|
|
97
|
-
const createRes = await axios.post(`${baseUrl}/api/workspace/${credentials.workspace}/onboarding-link`, { type: channelType }, { headers });
|
|
98
|
-
linkData = createRes.data?.data ?? null;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
linkData = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!linkData?.token) {
|
|
105
|
-
spinner.fail(chalk.hex('#EF5350')('Could not obtain the connection link'));
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
const shortUrl = `${ONBOARDING_SHORT_DOMAIN}/${linkData.token}`;
|
|
109
|
-
const expiresAt = linkData.expiresAt ? new Date(linkData.expiresAt).toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' }) : '';
|
|
110
|
-
spinner.stop();
|
|
111
|
-
// Mostrar link
|
|
112
|
-
const W = 54;
|
|
113
|
-
const pad = (text) => text + ' '.repeat(Math.max(0, W - text.length));
|
|
114
|
-
const border = chalk.hex('#25D366');
|
|
115
|
-
console.log(border(' ┌' + '─'.repeat(W) + '┐'));
|
|
116
|
-
console.log(border(' │') + chalk.bold(pad(' Open this link in your browser:')) + border('│'));
|
|
117
|
-
console.log(border(' │') + ' '.repeat(W) + border('│'));
|
|
118
|
-
console.log(border(' │') + chalk.hex('#22d3ee')(pad(` ${shortUrl}`)) + border('│'));
|
|
119
|
-
console.log(border(' │') + ' '.repeat(W) + border('│'));
|
|
120
|
-
console.log(border(' │') + chalk.gray(pad(' Scan the QR code from your WhatsApp')) + border('│'));
|
|
121
|
-
if (expiresAt) {
|
|
122
|
-
console.log(border(' │') + chalk.gray(pad(` Expires: ${expiresAt}`)) + border('│'));
|
|
123
|
-
}
|
|
124
|
-
console.log(border(' └' + '─'.repeat(W) + '┘'));
|
|
28
|
+
const detected = await connectNewNumber(credentials, {
|
|
29
|
+
business: options.business,
|
|
30
|
+
dev: options.dev,
|
|
31
|
+
openInBrowser: options.browser,
|
|
32
|
+
});
|
|
125
33
|
console.log();
|
|
126
|
-
// Guardar IDs actuales para detectar nuevos
|
|
127
|
-
const prevIds = new Set(allIntegrations.map((i) => i.id));
|
|
128
|
-
// Polling para detectar nueva integracion
|
|
129
|
-
const pollSpinner = ora({ text: chalk.gray('Waiting for connection... (Ctrl+C to cancel)'), spinner: 'dots', color: 'cyan' }).start();
|
|
130
|
-
const POLL_INTERVAL = 5000;
|
|
131
|
-
const MAX_POLLS = 60; // 5 minutos
|
|
132
|
-
let detected = null;
|
|
133
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
134
|
-
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
135
|
-
try {
|
|
136
|
-
const freshIntegrations = await fetchWorkspaceIntegrations(baseUrl, credentials.workspace, headers);
|
|
137
|
-
const newOnes = freshIntegrations.filter((ig) => !prevIds.has(ig.id));
|
|
138
|
-
if (newOnes.length > 0) {
|
|
139
|
-
const match = newOnes.find((ig) => ig.type === 'whatsapp');
|
|
140
|
-
detected = match || newOnes[0];
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch { }
|
|
145
|
-
const elapsed = Math.floor(((i + 1) * POLL_INTERVAL) / 1000);
|
|
146
|
-
pollSpinner.text = chalk.gray(`Waiting for connection... ${elapsed}s (Ctrl+C to cancel)`);
|
|
147
|
-
}
|
|
148
34
|
if (detected) {
|
|
149
|
-
|
|
150
|
-
pollSpinner.succeed(chalk.hex('#4ade80')(`Connection detected: ${detectedName}`));
|
|
151
|
-
// Activar la integracion
|
|
152
|
-
const actSpinner = ora({ text: chalk.gray('Activating integration...'), spinner: 'dots', color: 'green' }).start();
|
|
153
|
-
const activated = await activateIntegration(baseUrl, credentials.workspace, detected.id, headers);
|
|
154
|
-
if (activated) {
|
|
155
|
-
actSpinner.succeed(chalk.hex('#4ade80')('Integration activated successfully'));
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
actSpinner.warn(chalk.hex('#FFA726')('Could not activate automatically. Activate it from the platform.'));
|
|
159
|
-
}
|
|
160
|
-
console.log();
|
|
161
|
-
console.log(theme.success(' WhatsApp connected successfully'));
|
|
162
|
-
console.log(theme.muted(` Number: ${detectedName}`));
|
|
163
|
-
console.log();
|
|
164
|
-
console.log(theme.muted(' You can now:'));
|
|
165
|
-
console.log(theme.muted(' plazbot whatsapp send-message Send a message'));
|
|
166
|
-
console.log(theme.muted(' plazbot whatsapp chat <phone> Interactive chat'));
|
|
167
|
-
console.log(theme.muted(' plazbot whatsapp assign <phone> <agentId> Assign to an agent'));
|
|
35
|
+
console.log(theme.success(` WhatsApp conectado: ${integrationLabel(detected)}`));
|
|
168
36
|
console.log();
|
|
37
|
+
console.log(theme.muted(' Siguiente paso:'));
|
|
38
|
+
console.log(` ${chalk.cyan('plazbot whatsapp setup')} ${theme.muted('Prueba enviar y recibir tu primer mensaje')}`);
|
|
169
39
|
}
|
|
170
40
|
else {
|
|
171
|
-
|
|
172
|
-
console.log(theme.muted(' You can try again with: plazbot whatsapp connect'));
|
|
173
|
-
console.log();
|
|
41
|
+
console.log(theme.muted(' Puedes intentarlo otra vez con: plazbot whatsapp connect'));
|
|
174
42
|
}
|
|
43
|
+
console.log();
|
|
175
44
|
}
|
|
176
45
|
catch (error) {
|
|
177
|
-
|
|
178
|
-
logger.error(message);
|
|
46
|
+
logger.error(error instanceof Error ? error.message : 'Error desconocido');
|
|
179
47
|
process.exit(1);
|
|
180
48
|
}
|
|
181
49
|
});
|
|
182
50
|
addExamples(whatsappConnectCommand, [
|
|
183
|
-
{ description: '
|
|
184
|
-
|
|
185
|
-
{ description: 'Connect a WhatsApp Business number',
|
|
186
|
-
command: 'plazbot whatsapp connect --business' },
|
|
51
|
+
{ description: 'Conectar un número con WhatsApp API', command: 'plazbot whatsapp connect' },
|
|
52
|
+
{ description: 'Conectar tu app WhatsApp Business actual', command: 'plazbot whatsapp connect --business' },
|
|
187
53
|
]);
|
|
@@ -7,10 +7,12 @@ import { deleteWebhookCommand } from './delete-webhook.js';
|
|
|
7
7
|
import { broadcastCommand } from './broadcast.js';
|
|
8
8
|
import { whatsappChatCommand } from './chat.js';
|
|
9
9
|
import { whatsappConnectCommand } from './connect.js';
|
|
10
|
+
import { whatsappSetupCommand } from './setup.js';
|
|
10
11
|
import { widgetCommand } from './widget.js';
|
|
11
12
|
import { channelsCommand, assignCommand } from './channels.js';
|
|
12
13
|
export const whatsappCommands = new Command('whatsapp')
|
|
13
14
|
.description('WhatsApp commands: messages, templates, broadcast and more')
|
|
15
|
+
.addCommand(whatsappSetupCommand)
|
|
14
16
|
.addCommand(whatsappConnectCommand)
|
|
15
17
|
.addCommand(sendMessageCommand)
|
|
16
18
|
.addCommand(sendTemplateCommand)
|
|
@@ -22,6 +24,8 @@ export const whatsappCommands = new Command('whatsapp')
|
|
|
22
24
|
.addCommand(channelsCommand)
|
|
23
25
|
.addCommand(assignCommand);
|
|
24
26
|
addExamples(whatsappCommands, [
|
|
27
|
+
{ description: 'Asistente: conectar WhatsApp y enviar tu primer mensaje',
|
|
28
|
+
command: 'plazbot whatsapp setup' },
|
|
25
29
|
{ description: 'Connect a WhatsApp number to the workspace',
|
|
26
30
|
command: 'plazbot whatsapp connect' },
|
|
27
31
|
{ description: 'Send a quick message',
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { apiBaseUrl, getStoredCredentials } from '../../utils/credentials.js';
|
|
6
|
+
import { createSpinner, theme } from '../../utils/ui.js';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
import { addExamples, addNotes } from '../../utils/help.js';
|
|
9
|
+
import { runBrowserLogin } from '../auth/browser-login.js';
|
|
10
|
+
import { activateIntegration, apiHeaders, connectNewNumber, fetchIntegrations, integrationLabel, whatsappNumbers, } from './connect-flow.js';
|
|
11
|
+
/**
|
|
12
|
+
* plazbot whatsapp setup — wizard de primer uso:
|
|
13
|
+
* 1. Sesión → si no hay, login por navegador.
|
|
14
|
+
* 2. Número → usa uno conectado o conecta uno nuevo (link de Meta).
|
|
15
|
+
* 3. Prueba → el developer escribe "hola" desde su celular (crea el contacto y abre la
|
|
16
|
+
* ventana de 24 h de WhatsApp) y el CLI responde por la API.
|
|
17
|
+
* 4. Webhook → opcional, registra la URL que recibirá los mensajes entrantes.
|
|
18
|
+
* 5. Código → snippet con sus datos para su app.
|
|
19
|
+
*
|
|
20
|
+
* El paso 3 es necesario: POST /api/message solo envía a contactos que ya existen y
|
|
21
|
+
* WhatsApp solo permite texto libre dentro de las 24 h desde el último mensaje del cliente.
|
|
22
|
+
*/
|
|
23
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
24
|
+
const step = (n, title) => console.log(`\n${chalk.hex('#25D366').bold(`[${n}/4]`)} ${chalk.bold(title)}`);
|
|
25
|
+
/** Variantes del número tal como WhatsApp lo reporta (México 52/521, Argentina 54/549). */
|
|
26
|
+
function phoneVariants(digits) {
|
|
27
|
+
const v = new Set([digits]);
|
|
28
|
+
if (digits.startsWith('521') && digits.length === 13)
|
|
29
|
+
v.add('52' + digits.slice(3));
|
|
30
|
+
else if (digits.startsWith('52') && digits.length === 12)
|
|
31
|
+
v.add('521' + digits.slice(2));
|
|
32
|
+
if (digits.startsWith('549') && digits.length === 13)
|
|
33
|
+
v.add('54' + digits.slice(3));
|
|
34
|
+
else if (digits.startsWith('54') && digits.length === 12)
|
|
35
|
+
v.add('549' + digits.slice(2));
|
|
36
|
+
return [...v];
|
|
37
|
+
}
|
|
38
|
+
async function findRecentContact(creds, dev, phones, since, numberId) {
|
|
39
|
+
for (const phone of phones) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await axios.get(`${apiBaseUrl(creds.zone, dev)}/api/contact/searchByCellphone`, {
|
|
42
|
+
headers: apiHeaders(creds),
|
|
43
|
+
params: { workspaceId: creds.workspace, cellphone: phone },
|
|
44
|
+
});
|
|
45
|
+
const contacts = res.data?.data?.data ?? [];
|
|
46
|
+
const hit = contacts.find(c => c.lastMessageDate && new Date(c.lastMessageDate).getTime() >= since &&
|
|
47
|
+
(!numberId || !c.platformSenderId || c.platformSenderId === numberId));
|
|
48
|
+
if (hit)
|
|
49
|
+
return hit;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// error puntual: se reintenta en la siguiente vuelta
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
async function ensureSession(opts) {
|
|
58
|
+
try {
|
|
59
|
+
const creds = await getStoredCredentials();
|
|
60
|
+
console.log(` ${chalk.green('✓')} ${creds.workspaceName || creds.workspace} ${theme.muted(`(${creds.zone})`)}${creds.email ? theme.muted(` · ${creds.email}`) : ''}`);
|
|
61
|
+
return creds;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log(theme.muted(' No tienes una sesión activa. Vamos a iniciarla desde el navegador.'));
|
|
65
|
+
return runBrowserLogin({ dev: opts.dev, browser: opts.browser });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function chooseNumber(creds, opts) {
|
|
69
|
+
const spinner = createSpinner('Buscando números conectados...').start();
|
|
70
|
+
const numbers = whatsappNumbers(await fetchIntegrations(creds, opts.dev)).filter(n => !n.isUnofficialWsp);
|
|
71
|
+
spinner.stop();
|
|
72
|
+
let choice = 'new';
|
|
73
|
+
if (numbers.length > 0) {
|
|
74
|
+
const answer = await inquirer.prompt([{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'choice',
|
|
77
|
+
message: '¿Qué número quieres usar?',
|
|
78
|
+
choices: [
|
|
79
|
+
...numbers.map(n => ({ name: `${integrationLabel(n)}${n.isActive ? '' : chalk.hex('#EF5350')(' (inactivo)')}`, value: n.id })),
|
|
80
|
+
new inquirer.Separator(),
|
|
81
|
+
{ name: 'Conectar un número nuevo', value: 'new' },
|
|
82
|
+
],
|
|
83
|
+
}]);
|
|
84
|
+
choice = answer.choice;
|
|
85
|
+
}
|
|
86
|
+
if (choice !== 'new') {
|
|
87
|
+
const selected = numbers.find(n => n.id === choice);
|
|
88
|
+
if (!selected.isActive) {
|
|
89
|
+
const act = createSpinner('Activando el número...').start();
|
|
90
|
+
if (await activateIntegration(creds, selected.id, opts.dev))
|
|
91
|
+
act.succeed('Número activo');
|
|
92
|
+
else
|
|
93
|
+
act.warn('No se pudo activar. Actívalo en el panel → Integraciones.');
|
|
94
|
+
}
|
|
95
|
+
console.log(` ${chalk.green('✓')} ${integrationLabel(selected)}`);
|
|
96
|
+
return selected;
|
|
97
|
+
}
|
|
98
|
+
const { kind } = await inquirer.prompt([{
|
|
99
|
+
type: 'list',
|
|
100
|
+
name: 'kind',
|
|
101
|
+
message: '¿Cómo quieres conectar WhatsApp?',
|
|
102
|
+
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
|
+
],
|
|
106
|
+
}]);
|
|
107
|
+
return connectNewNumber(creds, { business: kind === 'business', dev: opts.dev, openInBrowser: opts.browser });
|
|
108
|
+
}
|
|
109
|
+
async function firstMessageTest(creds, number, dev) {
|
|
110
|
+
const { phone } = await inquirer.prompt([{
|
|
111
|
+
type: 'input',
|
|
112
|
+
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
|
+
}]);
|
|
116
|
+
const digits = String(phone).replace(/\D/g, '');
|
|
117
|
+
const target = number.cellphoneNumber || '';
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(` Desde ese celular envía ${chalk.bold('"hola"')} por WhatsApp a ${chalk.bold(`+${target}`)}`);
|
|
120
|
+
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.'));
|
|
123
|
+
console.log();
|
|
124
|
+
const since = Date.now() - 60_000;
|
|
125
|
+
const spinner = createSpinner('Esperando tu mensaje... (Ctrl+C para cancelar)').start();
|
|
126
|
+
const deadline = Date.now() + 5 * 60_000;
|
|
127
|
+
let contact = null;
|
|
128
|
+
while (Date.now() < deadline && !contact) {
|
|
129
|
+
await sleep(3000);
|
|
130
|
+
contact = await findRecentContact(creds, dev, phoneVariants(digits), since, number.codeCellphoneNumberId);
|
|
131
|
+
}
|
|
132
|
+
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.'));
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
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 }]);
|
|
139
|
+
if (!reply)
|
|
140
|
+
return contact.internalWhatsappNumber;
|
|
141
|
+
const sendSpinner = createSpinner('Enviando respuesta...').start();
|
|
142
|
+
try {
|
|
143
|
+
const res = await axios.post(`${apiBaseUrl(creds.zone, dev)}/api/message`, {
|
|
144
|
+
workspaceId: creds.workspace,
|
|
145
|
+
recipientPhone: contact.internalWhatsappNumber,
|
|
146
|
+
content: '¡Funciona! 🎉 Este mensaje lo envió tu código a través de la API de Plazbot.',
|
|
147
|
+
}, { headers: apiHeaders(creds) });
|
|
148
|
+
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.');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const msg = axios.isAxiosError(error) ? (error.response?.data?.message || error.message) : error.message;
|
|
154
|
+
sendSpinner.fail(`No se pudo enviar: ${msg}`);
|
|
155
|
+
}
|
|
156
|
+
return contact.internalWhatsappNumber;
|
|
157
|
+
}
|
|
158
|
+
async function webhookStep(creds, number, dev) {
|
|
159
|
+
const { url } = await inquirer.prompt([{
|
|
160
|
+
type: 'input',
|
|
161
|
+
name: 'url',
|
|
162
|
+
message: 'URL de tu webhook para recibir los mensajes entrantes (Enter para omitir):',
|
|
163
|
+
default: number.webhookUrl || '',
|
|
164
|
+
validate: (v) => !v || /^https:\/\/\S+$/.test(v) || 'Debe ser una URL https://',
|
|
165
|
+
}]);
|
|
166
|
+
if (!url) {
|
|
167
|
+
console.log(theme.muted(' Omitido. Puedes registrarlo después con: plazbot whatsapp register-webhook'));
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const spinner = createSpinner('Registrando webhook...').start();
|
|
171
|
+
try {
|
|
172
|
+
const res = await axios.post(`${apiBaseUrl(creds.zone, dev)}/api/workspace/${creds.workspace}/whatsapp/numbers/${number.cellphoneNumber}/webhook`, { webhook: url }, { headers: apiHeaders(creds) });
|
|
173
|
+
if (res.data?.success === false)
|
|
174
|
+
throw new Error(res.data?.message);
|
|
175
|
+
spinner.succeed(`Webhook registrado: ${url}`);
|
|
176
|
+
return url;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
spinner.fail(`No se pudo registrar el webhook${error instanceof Error && error.message ? `: ${error.message}` : ''}`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function printSnippet(creds, to) {
|
|
184
|
+
const code = [
|
|
185
|
+
`import { Plazbot } from "plazbot"`,
|
|
186
|
+
``,
|
|
187
|
+
`const bot = new Plazbot({`,
|
|
188
|
+
` apiKey: process.env.PLAZBOT_KEY!,`,
|
|
189
|
+
` workspaceId: "${creds.workspace}",`,
|
|
190
|
+
` zone: "${creds.zone}",`,
|
|
191
|
+
`})`,
|
|
192
|
+
``,
|
|
193
|
+
`await bot.message.onWhatsappMessage({`,
|
|
194
|
+
` to: "${to || '51912345678'}",`,
|
|
195
|
+
` message: "Hola desde mi app",`,
|
|
196
|
+
`})`,
|
|
197
|
+
];
|
|
198
|
+
console.log(`\n${chalk.bold('Listo. Usa esto en tu app')} ${theme.muted('(npm i plazbot)')}\n`);
|
|
199
|
+
code.forEach(l => console.log(' ' + chalk.hex('#9CDCFE')(l)));
|
|
200
|
+
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.'));
|
|
202
|
+
console.log(theme.muted(' Docs: https://developers.plazbot.com/sdk'));
|
|
203
|
+
console.log();
|
|
204
|
+
}
|
|
205
|
+
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)
|
|
209
|
+
.action(async (options) => {
|
|
210
|
+
try {
|
|
211
|
+
console.log(`\n${chalk.bold('WhatsApp · configuración inicial')}`);
|
|
212
|
+
step(1, 'Tu cuenta');
|
|
213
|
+
const creds = await ensureSession(options);
|
|
214
|
+
step(2, 'Tu número de WhatsApp');
|
|
215
|
+
const number = await chooseNumber(creds, options);
|
|
216
|
+
if (!number) {
|
|
217
|
+
console.log(theme.muted('\n Ejecuta plazbot whatsapp setup de nuevo cuando termines el registro en Meta.\n'));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
step(3, 'Prueba de ida y vuelta');
|
|
221
|
+
const to = await firstMessageTest(creds, number, options.dev);
|
|
222
|
+
step(4, 'Webhook (opcional)');
|
|
223
|
+
await webhookStep(creds, number, options.dev);
|
|
224
|
+
printSnippet(creds, to);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
// Ctrl+C durante un prompt de inquirer
|
|
228
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
229
|
+
console.log(theme.muted('\n Cancelado.\n'));
|
|
230
|
+
process.exit(130);
|
|
231
|
+
}
|
|
232
|
+
logger.error(error instanceof Error ? error.message : 'Error desconocido');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
addExamples(whatsappSetupCommand, [
|
|
237
|
+
{ description: 'Configurar WhatsApp paso a paso', command: 'plazbot whatsapp setup' },
|
|
238
|
+
]);
|
|
239
|
+
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).',
|
|
242
|
+
]);
|
|
@@ -200,7 +200,7 @@ export function App({ version, stream, initialAgentId, dev, supportMode }) {
|
|
|
200
200
|
function formatError(err, dev) {
|
|
201
201
|
if (err instanceof StudioHttpError) {
|
|
202
202
|
if (err.status === 401)
|
|
203
|
-
return '✖ Token expired or invalid. Run `plazbot
|
|
203
|
+
return '✖ Token expired or invalid. Run `plazbot login`.';
|
|
204
204
|
if (err.status === 403)
|
|
205
205
|
return '✖ No permission for this workspace.';
|
|
206
206
|
if (err.status === 429)
|
|
@@ -10,7 +10,7 @@ export async function runOneShot(opts) {
|
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
12
|
logger.error('No active session. Run:');
|
|
13
|
-
console.log(' plazbot
|
|
13
|
+
console.log(' plazbot login');
|
|
14
14
|
process.exit(1);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
@@ -78,7 +78,7 @@ export async function runOneShot(opts) {
|
|
|
78
78
|
function formatError(err, dev) {
|
|
79
79
|
if (err instanceof StudioHttpError) {
|
|
80
80
|
if (err.status === 401)
|
|
81
|
-
return '✖ Token expired or invalid. Run `plazbot
|
|
81
|
+
return '✖ Token expired or invalid. Run `plazbot login`.';
|
|
82
82
|
if (err.status === 403)
|
|
83
83
|
return '✖ No permission for this workspace.';
|
|
84
84
|
if (err.status === 429)
|
package/dist/studio/runRepl.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runRepl(opts) {
|
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
20
|
logger.error('No active session. Run:');
|
|
21
|
-
console.log(' plazbot
|
|
21
|
+
console.log(' plazbot login');
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
const effectiveWorkspace = opts.workspaceOverride ?? creds.workspace;
|
|
@@ -17,13 +17,13 @@ export function describeAgentLoadError(error, agentId, credentials, context = {}
|
|
|
17
17
|
chalk.gray(' plazbot status'),
|
|
18
18
|
chalk.gray(' plazbot agent list'),
|
|
19
19
|
chalk.gray(' If the agent lives in a different zone, re-init with the right one:'),
|
|
20
|
-
chalk.gray(' plazbot
|
|
20
|
+
chalk.gray(' plazbot login'),
|
|
21
21
|
].join('\n');
|
|
22
22
|
}
|
|
23
23
|
if (status === 401) {
|
|
24
24
|
return [
|
|
25
25
|
'Authentication token rejected (401).',
|
|
26
|
-
chalk.gray(' Re-run: plazbot
|
|
26
|
+
chalk.gray(' Re-run: plazbot login'),
|
|
27
27
|
].join('\n');
|
|
28
28
|
}
|
|
29
29
|
if (status === 403) {
|
package/dist/utils/banner.js
CHANGED
|
@@ -47,22 +47,23 @@ export async function showBanner() {
|
|
|
47
47
|
rightColumn.push('');
|
|
48
48
|
rightColumn.push(chalk.hex('#4CAF50')('─'.repeat(35)));
|
|
49
49
|
rightColumn.push('');
|
|
50
|
-
rightColumn.push(chalk.bold.hex('#4CAF50')('
|
|
51
|
-
rightColumn.push(chalk.white(' plazbot
|
|
52
|
-
rightColumn.push(chalk.white(' plazbot agent create'));
|
|
53
|
-
rightColumn.push(chalk.white(' plazbot agent chat -a <id>'));
|
|
50
|
+
rightColumn.push(chalk.bold.hex('#4CAF50')('Comandos rápidos'));
|
|
51
|
+
rightColumn.push(chalk.white(' plazbot whatsapp setup'));
|
|
54
52
|
rightColumn.push(chalk.white(' plazbot whatsapp send-message'));
|
|
53
|
+
rightColumn.push(chalk.white(' plazbot agent list'));
|
|
54
|
+
rightColumn.push(chalk.white(' plazbot studio'));
|
|
55
55
|
}
|
|
56
56
|
else {
|
|
57
|
-
rightColumn.push(chalk.bold.hex('#FFA726')('
|
|
57
|
+
rightColumn.push(chalk.bold.hex('#FFA726')('Sin sesión'));
|
|
58
58
|
rightColumn.push('');
|
|
59
|
-
rightColumn.push(chalk.bold.hex('#4CAF50')('
|
|
60
|
-
rightColumn.push(chalk.white(' plazbot
|
|
59
|
+
rightColumn.push(chalk.bold.hex('#4CAF50')('Empieza aquí'));
|
|
60
|
+
rightColumn.push(chalk.white(' plazbot login'));
|
|
61
|
+
rightColumn.push(chalk.white(' plazbot whatsapp setup'));
|
|
61
62
|
rightColumn.push('');
|
|
62
63
|
rightColumn.push(chalk.hex('#4CAF50')('─'.repeat(35)));
|
|
63
64
|
rightColumn.push('');
|
|
64
|
-
rightColumn.push(chalk.bold.hex('#4CAF50')('
|
|
65
|
-
rightColumn.push(chalk.gray(' https://
|
|
65
|
+
rightColumn.push(chalk.bold.hex('#4CAF50')('Documentación'));
|
|
66
|
+
rightColumn.push(chalk.gray(' https://developers.plazbot.com'));
|
|
66
67
|
}
|
|
67
68
|
// Imprimir título
|
|
68
69
|
console.log(title);
|
|
@@ -3,7 +3,23 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
const configDir = path.join(os.homedir(), '.plazbot');
|
|
5
5
|
const configPath = path.join(configDir, 'config.json');
|
|
6
|
+
/**
|
|
7
|
+
* Credenciales desde variables de entorno (CI / servidores):
|
|
8
|
+
* PLAZBOT_API_KEY + PLAZBOT_WORKSPACE_ID (+ PLAZBOT_ZONE, por defecto LA).
|
|
9
|
+
* Tienen prioridad sobre ~/.plazbot/config.json.
|
|
10
|
+
*/
|
|
11
|
+
function getEnvCredentials() {
|
|
12
|
+
const apiKey = process.env.PLAZBOT_API_KEY;
|
|
13
|
+
const workspace = process.env.PLAZBOT_WORKSPACE_ID;
|
|
14
|
+
if (!apiKey || !workspace)
|
|
15
|
+
return null;
|
|
16
|
+
const zone = (process.env.PLAZBOT_ZONE || 'LA').toUpperCase() === 'EU' ? 'EU' : 'LA';
|
|
17
|
+
return { apiKey, workspace, zone, source: 'env' };
|
|
18
|
+
}
|
|
6
19
|
export async function getStoredCredentials() {
|
|
20
|
+
const fromEnv = getEnvCredentials();
|
|
21
|
+
if (fromEnv)
|
|
22
|
+
return fromEnv;
|
|
7
23
|
try {
|
|
8
24
|
const configRaw = await fs.readFile(configPath, 'utf-8');
|
|
9
25
|
const config = JSON.parse(configRaw);
|
|
@@ -13,13 +29,25 @@ export async function getStoredCredentials() {
|
|
|
13
29
|
return config;
|
|
14
30
|
}
|
|
15
31
|
catch (error) {
|
|
16
|
-
throw new Error("No
|
|
32
|
+
throw new Error("No hay una sesión activa. Ejecuta 'plazbot login' para iniciar sesión.");
|
|
17
33
|
}
|
|
18
34
|
}
|
|
19
35
|
export async function saveCredentials(credentials) {
|
|
20
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
21
|
-
|
|
36
|
+
await fs.mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
37
|
+
// 0600: el archivo contiene el token de acceso
|
|
38
|
+
await fs.writeFile(configPath, JSON.stringify(credentials, null, 2), { mode: 0o600 });
|
|
39
|
+
await fs.chmod(configPath, 0o600).catch(() => { });
|
|
22
40
|
}
|
|
23
41
|
export async function removeCredentials() {
|
|
24
42
|
await fs.rm(configPath, { force: true });
|
|
25
43
|
}
|
|
44
|
+
export function apiBaseUrl(zone, dev = false) {
|
|
45
|
+
if (dev)
|
|
46
|
+
return 'http://localhost:5090';
|
|
47
|
+
return zone === 'EU' ? 'https://apieu.plazbot.com' : 'https://api.plazbot.com';
|
|
48
|
+
}
|
|
49
|
+
export function appBaseUrl(zone, dev = false) {
|
|
50
|
+
if (dev)
|
|
51
|
+
return 'http://localhost:5173';
|
|
52
|
+
return zone === 'EU' ? 'https://appeu.plazbot.com' : 'https://appla.plazbot.com';
|
|
53
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/** Abre una URL en el navegador del sistema. Devuelve false si no se pudo (p.ej. servidor sin GUI). */
|
|
3
|
+
export function openBrowser(url) {
|
|
4
|
+
try {
|
|
5
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]] :
|
|
6
|
+
// En cmd.exe "&" separa comandos: se escapa con ^
|
|
7
|
+
process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url.replace(/&/g, '^&')]] :
|
|
8
|
+
['xdg-open', [url]];
|
|
9
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
10
|
+
child.on('error', () => { });
|
|
11
|
+
child.unref();
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|