plazbot-cli 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +38 -2
  2. package/dist/commands/auth/browser-login.js +121 -0
  3. package/dist/commands/auth/index.js +5 -1
  4. package/dist/commands/auth/login.js +1 -3
  5. package/dist/commands/auth/logout.js +4 -3
  6. package/dist/commands/auth/status.js +8 -5
  7. package/dist/commands/auth/workspace.js +73 -0
  8. package/dist/commands/whatsapp/connect-flow.js +137 -0
  9. package/dist/commands/whatsapp/connect.js +26 -160
  10. package/dist/commands/whatsapp/index.js +4 -0
  11. package/dist/commands/whatsapp/setup.js +242 -0
  12. package/dist/schemas/agent.config.schema.json +24 -0
  13. package/dist/studio/components/App.js +1 -1
  14. package/dist/studio/runOneShot.js +2 -2
  15. package/dist/studio/runRepl.js +1 -1
  16. package/dist/utils/agent-errors.js +2 -2
  17. package/dist/utils/banner.js +10 -9
  18. package/dist/utils/credentials.js +31 -3
  19. package/dist/utils/open-browser.js +17 -0
  20. package/package.json +14 -6
  21. package/.claude/settings.local.json +0 -19
  22. package/CLAUDE.md +0 -121
  23. package/agent.config.json +0 -21
  24. package/dist/studio/components/AgentPanel.js +0 -35
  25. package/dist/studio/render/json.js +0 -16
  26. package/dist/studio/whatsapp/api.js +0 -63
  27. package/dist/studio/whatsapp/polling.js +0 -77
  28. package/dist/studio/whatsapp/types.js +0 -31
  29. package/src/cli.ts +0 -57
  30. package/src/commands/agent/ai-config.ts +0 -164
  31. package/src/commands/agent/chat.ts +0 -225
  32. package/src/commands/agent/copy.ts +0 -46
  33. package/src/commands/agent/create.ts +0 -103
  34. package/src/commands/agent/delete.ts +0 -71
  35. package/src/commands/agent/enable-widget.ts +0 -67
  36. package/src/commands/agent/export.ts +0 -430
  37. package/src/commands/agent/files.ts +0 -171
  38. package/src/commands/agent/get.ts +0 -233
  39. package/src/commands/agent/index.ts +0 -55
  40. package/src/commands/agent/list.ts +0 -58
  41. package/src/commands/agent/monitor.ts +0 -375
  42. package/src/commands/agent/on-message.ts +0 -82
  43. package/src/commands/agent/set.ts +0 -149
  44. package/src/commands/agent/templates.ts +0 -892
  45. package/src/commands/agent/tools.ts +0 -168
  46. package/src/commands/agent/update.ts +0 -61
  47. package/src/commands/agent/validate.ts +0 -135
  48. package/src/commands/agent/wizard.ts +0 -756
  49. package/src/commands/auth/index.ts +0 -10
  50. package/src/commands/auth/login.ts +0 -72
  51. package/src/commands/auth/logout.ts +0 -28
  52. package/src/commands/auth/status.ts +0 -28
  53. package/src/commands/portal/add-agent.ts +0 -66
  54. package/src/commands/portal/add-link.ts +0 -47
  55. package/src/commands/portal/clear-links.ts +0 -33
  56. package/src/commands/portal/create.ts +0 -58
  57. package/src/commands/portal/delete.ts +0 -70
  58. package/src/commands/portal/get.ts +0 -77
  59. package/src/commands/portal/index.ts +0 -32
  60. package/src/commands/portal/list.ts +0 -79
  61. package/src/commands/portal/update.ts +0 -93
  62. package/src/commands/whatsapp/broadcast.ts +0 -110
  63. package/src/commands/whatsapp/channels.ts +0 -109
  64. package/src/commands/whatsapp/chat.ts +0 -85
  65. package/src/commands/whatsapp/connect.ts +0 -220
  66. package/src/commands/whatsapp/delete-webhook.ts +0 -43
  67. package/src/commands/whatsapp/index.ts +0 -37
  68. package/src/commands/whatsapp/register-webhook.ts +0 -47
  69. package/src/commands/whatsapp/send-template.ts +0 -94
  70. package/src/commands/whatsapp/send.ts +0 -55
  71. package/src/commands/whatsapp/widget.ts +0 -75
  72. package/src/commands/workers/deploy.ts +0 -156
  73. package/src/commands/workers/index.ts +0 -30
  74. package/src/commands/workers/list.ts +0 -105
  75. package/src/commands/workers/logs.ts +0 -99
  76. package/src/commands/workers/remove.ts +0 -103
  77. package/src/commands/workers/secret.ts +0 -191
  78. package/src/commands/workers/test.ts +0 -145
  79. package/src/schemas/agent.config.schema.json +0 -569
  80. package/src/studio/api/sseClient.ts +0 -91
  81. package/src/studio/api/studioApi.ts +0 -27
  82. package/src/studio/api/types.ts +0 -96
  83. package/src/studio/components/App.tsx +0 -274
  84. package/src/studio/components/ChatLog.tsx +0 -121
  85. package/src/studio/components/ConnectCard.tsx +0 -171
  86. package/src/studio/components/Footer.tsx +0 -38
  87. package/src/studio/components/Header.tsx +0 -39
  88. package/src/studio/components/Input.tsx +0 -176
  89. package/src/studio/components/Message.tsx +0 -87
  90. package/src/studio/components/Suggestions.tsx +0 -26
  91. package/src/studio/components/ToolCall.tsx +0 -58
  92. package/src/studio/connect/api.ts +0 -98
  93. package/src/studio/connect/polling.ts +0 -94
  94. package/src/studio/connect/types.ts +0 -141
  95. package/src/studio/index.ts +0 -58
  96. package/src/studio/render/markdown.ts +0 -93
  97. package/src/studio/render/steps.ts +0 -57
  98. package/src/studio/runOneShot.ts +0 -114
  99. package/src/studio/runRepl.tsx +0 -82
  100. package/src/studio/slash/handlers.ts +0 -226
  101. package/src/studio/slash/parser.ts +0 -41
  102. package/src/studio/slash/registry.ts +0 -54
  103. package/src/studio/state/store.ts +0 -279
  104. package/src/types/agent.ts +0 -63
  105. package/src/types/auth.ts +0 -9
  106. package/src/types/common.ts +0 -10
  107. package/src/types/message.ts +0 -34
  108. package/src/types/portal.ts +0 -56
  109. package/src/types/workers.ts +0 -86
  110. package/src/utils/agent-errors.ts +0 -67
  111. package/src/utils/api.ts +0 -35
  112. package/src/utils/banner.ts +0 -99
  113. package/src/utils/credentials.ts +0 -38
  114. package/src/utils/help.ts +0 -51
  115. package/src/utils/logger.ts +0 -40
  116. package/src/utils/ui.ts +0 -109
  117. 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
- function getBaseUrl(zone, dev) {
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('Connect a WhatsApp number to the workspace (without creating an agent)')
43
- .option('--dev', 'Use development environment', false)
44
- .option('--business', 'Connect as WhatsApp Business', false)
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
- const baseUrl = getBaseUrl(credentials.zone, options.dev);
49
- const headers = {
50
- 'Authorization': `Bearer ${credentials.apiKey}`,
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} number(s) already connected:`));
65
- existing.forEach((ig) => {
66
- const label = getIntegrationLabel(ig);
67
- const status = ig.isActive ? chalk.hex('#66BB6A')('active') : chalk.hex('#EF5350')('inactive');
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
- // Obtener o generar link de onboarding (mismo flujo que Plazbot Front)
73
- const ONBOARDING_SHORT_DOMAIN = 'https://co.plzb.link';
74
- const spinner = ora({ text: chalk.gray('Fetching connection link...'), spinner: 'dots', color: 'green' }).start();
75
- let linkData = null;
76
- // 1. Intentar obtener link existente (GET)
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
- const detectedName = getIntegrationLabel(detected);
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
- pollSpinner.warn(chalk.hex('#FFA726')('Timeout: no connection detected within 5 minutes.'));
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
- const message = error instanceof Error ? error.message : 'Unknown error';
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: 'Generate a QR connection link for WhatsApp',
184
- command: 'plazbot whatsapp connect' },
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
+ ]);
@@ -99,6 +99,30 @@
99
99
  "enableWhatsappWidget": { "type": "boolean" },
100
100
  "urlWhatsappWidget": { "type": "string" },
101
101
  "formWidget": { "type": "boolean" },
102
+ "formTypeWidget": {
103
+ "type": ["string", "null"],
104
+ "enum": ["chat", "fields", null],
105
+ "default": "chat",
106
+ "description": "Estilo del formulario de registro: chat (conversacional) o fields (campos en una sola pantalla). Solo aplica si formWidget es true."
107
+ },
108
+ "formFieldsWidget": {
109
+ "type": ["array", "null"],
110
+ "maxItems": 5,
111
+ "description": "Campos adicionales del formulario por campos (formTypeWidget = fields). Cada valor se guarda en una variable del contacto.",
112
+ "items": {
113
+ "type": "object",
114
+ "properties": {
115
+ "id": { "type": "string" },
116
+ "label": { "type": "string", "maxLength": 60 },
117
+ "type": { "type": "string", "enum": ["text", "select", "date"], "default": "text" },
118
+ "options": { "type": ["array", "null"], "items": { "type": "string", "maxLength": 60 }, "maxItems": 20 },
119
+ "variable": { "type": "string" },
120
+ "required": { "type": "boolean" }
121
+ },
122
+ "required": ["label", "type", "variable"],
123
+ "additionalProperties": false
124
+ }
125
+ },
102
126
  "darkWidget": { "type": "boolean" },
103
127
  "colorWidget": {
104
128
  "type": ["string", "null"],
@@ -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 init` with a valid JWT.';
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 init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
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 init`.';
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)
@@ -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 init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
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 init -z LA # or EU'),
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 init -k <jwt> -w <workspace> -z LA|EU'),
26
+ chalk.gray(' Re-run: plazbot login'),
27
27
  ].join('\n');
28
28
  }
29
29
  if (status === 403) {
@@ -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')('Quick commands'));
51
- rightColumn.push(chalk.white(' plazbot agent list'));
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')('Not connected'));
57
+ rightColumn.push(chalk.bold.hex('#FFA726')('Sin sesión'));
58
58
  rightColumn.push('');
59
- rightColumn.push(chalk.bold.hex('#4CAF50')('To get started'));
60
- rightColumn.push(chalk.white(' plazbot init -e <email> -k <key> -w <workspace> -z <zone>'));
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')('Documentation'));
65
- rightColumn.push(chalk.gray(' https://docs.plazbot.com'));
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 se encontró una sesión activa. Ejecuta 'plazbot init' primero.");
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
- await fs.writeFile(configPath, JSON.stringify(credentials, null, 2));
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
+ }