plazbot-cli 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +67 -111
  2. package/dist/cli.js +26 -11
  3. package/dist/commands/agent/ai-config.js +264 -63
  4. package/dist/commands/agent/chat.js +16 -11
  5. package/dist/commands/agent/copy.js +49 -13
  6. package/dist/commands/agent/create.js +62 -17
  7. package/dist/commands/agent/delete.js +50 -39
  8. package/dist/commands/agent/enable-widget.js +47 -27
  9. package/dist/commands/agent/export.js +180 -91
  10. package/dist/commands/agent/files.js +155 -73
  11. package/dist/commands/agent/get.js +25 -19
  12. package/dist/commands/agent/index.js +12 -10
  13. package/dist/commands/agent/list.js +20 -11
  14. package/dist/commands/agent/monitor.js +103 -57
  15. package/dist/commands/agent/on-message.js +55 -32
  16. package/dist/commands/agent/set.js +230 -82
  17. package/dist/commands/agent/templates.js +259 -156
  18. package/dist/commands/agent/tools.js +356 -123
  19. package/dist/commands/agent/update.js +69 -28
  20. package/dist/commands/agent/validate.js +72 -40
  21. package/dist/commands/agent/wizard.js +240 -329
  22. package/dist/commands/auth/browser-login.js +37 -31
  23. package/dist/commands/auth/login.js +49 -45
  24. package/dist/commands/auth/logout.js +20 -11
  25. package/dist/commands/auth/status.js +36 -12
  26. package/dist/commands/auth/workspace.js +61 -27
  27. package/dist/commands/portal/add-agent.js +106 -45
  28. package/dist/commands/portal/add-link.js +25 -28
  29. package/dist/commands/portal/clear-links.js +39 -19
  30. package/dist/commands/portal/create.js +66 -37
  31. package/dist/commands/portal/delete.js +40 -45
  32. package/dist/commands/portal/get.js +21 -54
  33. package/dist/commands/portal/index.js +12 -12
  34. package/dist/commands/portal/list.js +34 -52
  35. package/dist/commands/portal/shared.js +92 -0
  36. package/dist/commands/portal/update.js +100 -70
  37. package/dist/commands/whatsapp/broadcast.js +169 -77
  38. package/dist/commands/whatsapp/channels.js +167 -65
  39. package/dist/commands/whatsapp/chat.js +45 -46
  40. package/dist/commands/whatsapp/connect-flow.js +16 -15
  41. package/dist/commands/whatsapp/connect.js +36 -21
  42. package/dist/commands/whatsapp/delete-webhook.js +42 -25
  43. package/dist/commands/whatsapp/index.js +15 -16
  44. package/dist/commands/whatsapp/register-webhook.js +43 -26
  45. package/dist/commands/whatsapp/send-template.js +62 -61
  46. package/dist/commands/whatsapp/send.js +46 -33
  47. package/dist/commands/whatsapp/setup.js +62 -52
  48. package/dist/commands/whatsapp/shared.js +75 -0
  49. package/dist/commands/whatsapp/widget.js +82 -45
  50. package/dist/commands/workers/deploy.js +80 -58
  51. package/dist/commands/workers/index.js +8 -8
  52. package/dist/commands/workers/list.js +42 -20
  53. package/dist/commands/workers/logs.js +139 -36
  54. package/dist/commands/workers/remove.js +45 -47
  55. package/dist/commands/workers/secret.js +159 -83
  56. package/dist/commands/workers/test.js +108 -60
  57. package/dist/schemas/agent.config.schema.json +159 -8
  58. package/dist/studio/api/sseClient.js +1 -1
  59. package/dist/studio/api/studioApi.js +8 -0
  60. package/dist/studio/components/App.js +12 -0
  61. package/dist/studio/components/Footer.js +1 -1
  62. package/dist/studio/index.js +74 -14
  63. package/dist/studio/runOneShot.js +62 -23
  64. package/dist/studio/runRepl.js +5 -5
  65. package/dist/studio/slash/handlers.js +11 -8
  66. package/dist/studio/state/store.js +1 -1
  67. package/dist/utils/agent-errors.js +14 -11
  68. package/dist/utils/agent-patch.js +60 -0
  69. package/dist/utils/banner.js +9 -5
  70. package/dist/utils/confirm.js +31 -0
  71. package/dist/utils/credentials.js +13 -3
  72. package/dist/utils/logger.js +13 -10
  73. package/dist/utils/output.js +78 -0
  74. package/dist/utils/phone.js +22 -0
  75. package/dist/utils/ui.js +14 -11
  76. package/package.json +3 -3
@@ -3,12 +3,56 @@ import chalk from 'chalk';
3
3
  import axios from 'axios';
4
4
  import ora from 'ora';
5
5
  import { theme, section, kvPair } from '../../utils/ui.js';
6
- const MODELS = {
7
- openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4', 'gpt-3.5-turbo', 'o1-preview', 'o1-mini'],
8
- claude: ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
9
- gemini: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
10
- };
6
+ import { requireInteractive } from '../../utils/confirm.js';
7
+ import { normalizePhone } from '../../utils/phone.js';
8
+ import { CliError } from '../../utils/output.js';
9
+ import { AI_MODELS, AI_PROVIDERS, DEFAULT_AI_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS } from './ai-config.js';
10
+ import { promptService, promptAction } from './tools.js';
11
11
  const COLORS = ['blue', 'green', 'orange', 'gray', 'white'];
12
+ // Labels are English; values stay as the platform stores them (the front uses the same values).
13
+ const TONES = [
14
+ { name: 'Professional', value: 'profesional' },
15
+ { name: 'Friendly', value: 'amigable' },
16
+ { name: 'Formal', value: 'formal' },
17
+ { name: 'Casual', value: 'casual' },
18
+ { name: 'Technical', value: 'tecnico' },
19
+ { name: 'Empathetic', value: 'empatico' },
20
+ ];
21
+ const STYLES = [
22
+ { name: 'Concise', value: 'conciso' },
23
+ { name: 'Detailed', value: 'detallado' },
24
+ { name: 'Conversational', value: 'conversacional' },
25
+ { name: 'Direct', value: 'directo' },
26
+ ];
27
+ const LANGUAGES = [
28
+ { name: 'Spanish', value: 'Espanol' },
29
+ { name: 'English', value: 'English' },
30
+ { name: 'Portuguese', value: 'Portugues' },
31
+ { name: 'French', value: 'Frances' },
32
+ ];
33
+ // Default fallback messages are shown to end users, so they follow the agent language.
34
+ const FALLBACK_DEFAULTS = {
35
+ Espanol: {
36
+ noAnswer: 'Lo siento, no tengo información sobre eso. ¿Puedo ayudarte con otra consulta?',
37
+ serviceError: 'Disculpa, estamos experimentando dificultades técnicas. Intenta de nuevo en unos momentos.',
38
+ doNotUnderstand: 'No estoy seguro de entender tu consulta. ¿Podrías reformularla?',
39
+ },
40
+ English: {
41
+ noAnswer: "Sorry, I don't have information about that. Can I help you with something else?",
42
+ serviceError: "Sorry, we're having technical difficulties. Please try again in a few moments.",
43
+ doNotUnderstand: "I'm not sure I understand your question. Could you rephrase it?",
44
+ },
45
+ Portugues: {
46
+ noAnswer: 'Desculpe, não tenho informações sobre isso. Posso ajudar com outra dúvida?',
47
+ serviceError: 'Desculpe, estamos com dificuldades técnicas. Tente novamente em alguns instantes.',
48
+ doNotUnderstand: 'Não tenho certeza se entendi sua pergunta. Poderia reformulá-la?',
49
+ },
50
+ Frances: {
51
+ noAnswer: "Désolé, je n'ai pas d'informations à ce sujet. Puis-je vous aider avec autre chose ?",
52
+ serviceError: 'Désolé, nous rencontrons des difficultés techniques. Réessayez dans quelques instants.',
53
+ doNotUnderstand: "Je ne suis pas sûr de comprendre votre question. Pourriez-vous la reformuler ?",
54
+ },
55
+ };
12
56
  function getBaseUrl(zone, dev) {
13
57
  if (dev)
14
58
  return 'http://localhost:5090';
@@ -40,14 +84,9 @@ function getIntegrationKey(ig) {
40
84
  return ig.aliasName || ig.srcName || ig.id;
41
85
  }
42
86
  async function fetchWorkspaceIntegrations(baseUrl, workspaceId, headers) {
43
- try {
44
- const wkRes = await axios.get(`${baseUrl}/api/workspace/${workspaceId}`, { headers });
45
- const wsData = Array.isArray(wkRes.data) ? wkRes.data[0] : wkRes.data;
46
- return wsData?.integrations || [];
47
- }
48
- catch {
49
- return [];
50
- }
87
+ const wkRes = await axios.get(`${baseUrl}/api/workspace/${workspaceId}`, { headers });
88
+ const wsData = Array.isArray(wkRes.data) ? wkRes.data[0] : wkRes.data;
89
+ return wsData?.integrations || [];
51
90
  }
52
91
  async function activateIntegration(baseUrl, workspaceId, integrationId, headers) {
53
92
  try {
@@ -58,7 +97,61 @@ async function activateIntegration(baseUrl, workspaceId, integrationId, headers)
58
97
  return false;
59
98
  }
60
99
  }
100
+ async function activateWithSpinner(baseUrl, workspaceId, integrationId, headers) {
101
+ const actSpinner = ora({ text: chalk.gray('Activating integration...'), spinner: 'dots', color: 'green' }).start();
102
+ const activated = await activateIntegration(baseUrl, workspaceId, integrationId, headers);
103
+ if (activated) {
104
+ actSpinner.succeed(chalk.hex('#4ade80')('Integration activated.'));
105
+ }
106
+ else {
107
+ actSpinner.warn(chalk.hex('#FFA726')('Could not activate the integration. Activate it from the platform (Settings > Integrations).'));
108
+ }
109
+ }
110
+ /**
111
+ * Resolves after `ms`, or earlier when the user presses Enter. Restores the terminal
112
+ * mode afterwards and keeps Ctrl+C working while stdin is in raw mode.
113
+ */
114
+ function waitOrEnter(ms) {
115
+ let cleanup = () => { };
116
+ const promise = new Promise(resolve => {
117
+ const stdin = process.stdin;
118
+ const isTTY = !!stdin.isTTY;
119
+ const wasRaw = isTTY ? stdin.isRaw : false;
120
+ const timer = setTimeout(() => finish('timeout'), ms);
121
+ const onData = (data) => {
122
+ const s = data.toString();
123
+ if (s.includes('\u0003')) {
124
+ cleanup();
125
+ process.exit(130);
126
+ }
127
+ if (s.includes('\n') || s.includes('\r'))
128
+ finish('enter');
129
+ };
130
+ function finish(reason) {
131
+ cleanup();
132
+ resolve(reason);
133
+ }
134
+ cleanup = () => {
135
+ clearTimeout(timer);
136
+ stdin.removeListener('data', onData);
137
+ if (isTTY) {
138
+ try {
139
+ stdin.setRawMode(!!wasRaw);
140
+ }
141
+ catch { /* terminal already closed */ }
142
+ }
143
+ stdin.pause();
144
+ cleanup = () => { };
145
+ };
146
+ if (isTTY)
147
+ stdin.setRawMode(true);
148
+ stdin.resume();
149
+ stdin.on('data', onData);
150
+ });
151
+ return { promise, cancel: () => cleanup() };
152
+ }
61
153
  export async function connectChannelFlow(ctx) {
154
+ requireInteractive('Connecting a channel', 'Connect channels later from the platform (Settings > Integrations).');
62
155
  const channels = [];
63
156
  const baseUrl = getBaseUrl(ctx.zone, ctx.dev);
64
157
  const headers = {
@@ -76,20 +169,25 @@ export async function connectChannelFlow(ctx) {
76
169
  }]);
77
170
  const channelLabel = CHANNEL_CHOICES.find(c => c.value === channelType)?.name || channelType;
78
171
  const targetType = channelType === 'whatsapp_business' ? 'whatsapp' : channelType;
79
- // Cargar integraciones existentes del tipo seleccionado
172
+ // Load the workspace integrations of the selected type
80
173
  const loadSpinner = ora({ text: chalk.gray('Loading integrations...'), spinner: 'dots', color: 'green' }).start();
81
- const allIntegrations = await fetchWorkspaceIntegrations(baseUrl, ctx.workspaceId, headers);
174
+ let allIntegrations = [];
175
+ try {
176
+ allIntegrations = await fetchWorkspaceIntegrations(baseUrl, ctx.workspaceId, headers);
177
+ loadSpinner.stop();
178
+ }
179
+ catch (err) {
180
+ loadSpinner.warn(chalk.hex('#FFA726')(`Could not load existing integrations (${err instanceof Error ? err.message : err}). You can still generate a connection link.`));
181
+ }
82
182
  const existing = allIntegrations.filter((ig) => ig.type === targetType);
83
- loadSpinner.stop();
84
183
  let selectedIntegration = null;
85
184
  if (existing.length > 0) {
86
- // Mostrar integraciones existentes + opcion de agregar nueva
87
185
  const choices = existing.map((ig) => ({
88
- name: `${getIntegrationLabel(ig)}`,
186
+ name: `${getIntegrationLabel(ig)}${ig.isActive ? '' : chalk.gray(' (inactive)')}`,
89
187
  value: ig.id,
90
188
  }));
91
189
  choices.push(new inquirer.Separator());
92
- choices.push({ name: chalk.hex('#4ade80')('+ Add a new one (generate link)'), value: '__new__' });
190
+ choices.push({ name: chalk.hex('#4ade80')('+ Connect a new one (generate link)'), value: '__new__' });
93
191
  const { selected } = await inquirer.prompt([{
94
192
  type: 'list',
95
193
  name: 'selected',
@@ -97,19 +195,10 @@ export async function connectChannelFlow(ctx) {
97
195
  choices,
98
196
  }]);
99
197
  if (selected !== '__new__') {
100
- // Selecciono una existente
101
198
  selectedIntegration = existing.find((ig) => ig.id === selected);
102
199
  const label = getIntegrationLabel(selectedIntegration);
103
- // Activar la integracion si no esta activa
104
200
  if (!selectedIntegration.isActive) {
105
- const actSpinner = ora({ text: chalk.gray('Activating integration...'), spinner: 'dots', color: 'green' }).start();
106
- const activated = await activateIntegration(baseUrl, ctx.workspaceId, selectedIntegration.id, headers);
107
- if (activated) {
108
- actSpinner.succeed(chalk.hex('#4ade80')('Integration activated successfully.'));
109
- }
110
- else {
111
- actSpinner.warn(chalk.hex('#FFA726')('Could not activate the integration. You can activate it manually from the platform.'));
112
- }
201
+ await activateWithSpinner(baseUrl, ctx.workspaceId, selectedIntegration.id, headers);
113
202
  }
114
203
  channels.push({
115
204
  channel: CHANNEL_AGENT_MAP[channelType],
@@ -119,130 +208,87 @@ export async function connectChannelFlow(ctx) {
119
208
  console.log(chalk.hex('#4ade80')(` Channel linked: ${label}`));
120
209
  }
121
210
  }
122
- // Si no hay existentes o eligio "Agregar nuevo" → generar link de onboarding
211
+ // No existing integrations, or "connect a new one": generate an onboarding link
123
212
  if (!selectedIntegration) {
124
213
  const spinner = ora({ text: chalk.gray('Generating connection link...'), spinner: 'dots', color: 'green' }).start();
125
214
  try {
126
215
  const linkRes = await axios.post(`${baseUrl}/api/workspace/${ctx.workspaceId}/onboarding-link`, { type: channelType }, { headers });
127
216
  if (!linkRes.data?.success) {
128
- spinner.fail(chalk.hex('#EF5350')(`Error: ${linkRes.data?.message || 'Could not generate the link'}`));
217
+ spinner.fail(chalk.hex('#EF5350')(`Could not generate the link: ${linkRes.data?.message || 'unknown error'}.`));
129
218
  }
130
219
  else {
131
220
  const shortUrl = linkRes.data.data?.shortUrl || '';
132
221
  spinner.stop();
133
- // Mostrar link
134
222
  console.log();
135
- console.log(chalk.hex('#4CAF50')(' ─'.repeat(32)));
136
- console.log(chalk.bold(` Send this link to connect ${channelLabel}:`));
223
+ console.log(chalk.hex('#4CAF50')(' ' + '─'.repeat(60)));
224
+ console.log(chalk.bold(` Open this link (or send it to the account owner) to connect ${channelLabel}:`));
137
225
  console.log();
138
226
  console.log(' ' + chalk.hex('#22d3ee')(shortUrl));
139
227
  console.log();
140
- console.log(chalk.hex('#4CAF50')(' ─'.repeat(32)));
228
+ console.log(chalk.hex('#4CAF50')(' ' + '─'.repeat(60)));
141
229
  console.log();
142
- // Guardar IDs actuales para detectar nuevos
230
+ // Remember current IDs to detect the new integration
143
231
  const prevIds = new Set(allIntegrations.map((i) => i.id));
144
- // Polling para detectar nueva integracion (Enter para saltar)
145
- const pollSpinner = ora({ text: chalk.gray('Waiting for connection... (Enter to skip)'), spinner: 'dots', color: 'cyan' }).start();
146
232
  const POLL_INTERVAL = 5000;
147
233
  const MAX_POLLS = 60;
148
- let detected = null;
234
+ const pollSpinner = ora({ text: chalk.gray('Waiting for the connection... (press Enter to skip)'), spinner: 'dots', color: 'cyan' }).start();
235
+ const skip = waitOrEnter(POLL_INTERVAL * MAX_POLLS + 1000);
149
236
  let skipped = false;
150
- // Escuchar Enter para saltar
151
- const skipPromise = new Promise(resolve => {
152
- const onData = (data) => {
153
- if (data.toString().includes('\n') || data.toString().includes('\r')) {
154
- skipped = true;
155
- process.stdin.removeListener('data', onData);
156
- if (wasRaw)
157
- process.stdin.setRawMode(false);
158
- resolve();
159
- }
160
- };
161
- const wasRaw = process.stdin.isRaw;
162
- if (process.stdin.isTTY)
163
- process.stdin.setRawMode(true);
164
- process.stdin.resume();
165
- process.stdin.on('data', onData);
166
- setTimeout(() => {
167
- process.stdin.removeListener('data', onData);
168
- if (process.stdin.isTTY && wasRaw !== undefined) {
169
- try {
170
- process.stdin.setRawMode(wasRaw);
171
- }
172
- catch { }
173
- }
174
- resolve();
175
- }, POLL_INTERVAL * MAX_POLLS + 1000);
176
- });
237
+ skip.promise.then(reason => { if (reason === 'enter')
238
+ skipped = true; });
239
+ let detected = null;
240
+ let pollErrors = 0;
177
241
  for (let i = 0; i < MAX_POLLS; i++) {
178
- await Promise.race([
179
- new Promise(r => setTimeout(r, POLL_INTERVAL)),
180
- skipPromise,
181
- ]);
242
+ await Promise.race([new Promise(r => setTimeout(r, POLL_INTERVAL)), skip.promise]);
182
243
  if (skipped)
183
244
  break;
184
245
  try {
185
246
  const freshIntegrations = await fetchWorkspaceIntegrations(baseUrl, ctx.workspaceId, headers);
186
247
  const newOnes = freshIntegrations.filter((ig) => !prevIds.has(ig.id));
187
248
  if (newOnes.length > 0) {
188
- const match = newOnes.find((ig) => ig.type === targetType);
189
- detected = match || newOnes[0];
249
+ detected = newOnes.find((ig) => ig.type === targetType) || newOnes[0];
190
250
  break;
191
251
  }
192
252
  }
193
- catch { }
253
+ catch {
254
+ pollErrors++;
255
+ }
194
256
  const elapsed = Math.floor(((i + 1) * POLL_INTERVAL) / 1000);
195
- pollSpinner.text = chalk.gray(`Waiting for connection... ${elapsed}s (Enter to skip)`);
257
+ pollSpinner.text = chalk.gray(`Waiting for the connection... ${elapsed}s (press Enter to skip)${pollErrors ? ` · ${pollErrors} failed checks` : ''}`);
196
258
  }
259
+ skip.cancel();
197
260
  if (detected) {
198
- const detectedName = getIntegrationLabel(detected);
199
- pollSpinner.succeed(chalk.hex('#4ade80')(`Connection detected: ${detectedName}`));
200
- // Activar la integracion detectada
201
- const actSpinner = ora({ text: chalk.gray('Activating integration...'), spinner: 'dots', color: 'green' }).start();
202
- const activated = await activateIntegration(baseUrl, ctx.workspaceId, detected.id, headers);
203
- if (activated) {
204
- actSpinner.succeed(chalk.hex('#4ade80')('Integration activated successfully.'));
205
- }
206
- else {
207
- actSpinner.warn(chalk.hex('#FFA726')('Could not activate the integration. You can activate it manually from the platform.'));
208
- }
261
+ pollSpinner.succeed(chalk.hex('#4ade80')(`Connection detected: ${getIntegrationLabel(detected)}`));
262
+ await activateWithSpinner(baseUrl, ctx.workspaceId, detected.id, headers);
209
263
  channels.push({
210
264
  channel: CHANNEL_AGENT_MAP[channelType],
211
265
  key: getIntegrationKey(detected),
212
266
  multianswer: false,
213
267
  });
214
- console.log(chalk.hex('#4ade80')(' Channel linked successfully.'));
268
+ console.log(chalk.hex('#4ade80')(' Channel linked.'));
215
269
  }
216
270
  else {
217
- pollSpinner.warn(chalk.hex('#FFA726')(skipped ? 'Skipped.' : 'Timeout: no connection detected.'));
271
+ pollSpinner.warn(chalk.hex('#FFA726')(skipped ? 'Skipped.' : 'Timed out: no new connection detected.'));
218
272
  const { action } = await inquirer.prompt([{
219
273
  type: 'list', name: 'action',
220
274
  message: 'What would you like to do?',
221
275
  choices: [
222
- { name: 'Enter ID manually', value: 'manual' },
223
- { name: 'Skip', value: 'skip' },
276
+ { name: 'Enter the integration ID manually', value: 'manual' },
277
+ { name: 'Skip this channel', value: 'skip' },
224
278
  ],
225
279
  }]);
226
280
  if (action === 'manual') {
227
281
  const { key } = await inquirer.prompt([
228
- { type: 'input', name: 'key', message: `${channelLabel} channel ID/number:`, validate: (v) => v.length > 0 || 'Required' },
282
+ { type: 'input', name: 'key', message: `${channelLabel} integration ID:`, validate: (v) => v.trim().length > 0 || 'Required' },
229
283
  ]);
230
- // Activar la integracion ingresada manualmente
231
- const actSpinner = ora({ text: chalk.gray('Activating integration...'), spinner: 'dots', color: 'green' }).start();
232
- const activated = await activateIntegration(baseUrl, ctx.workspaceId, key, headers);
233
- if (activated) {
234
- actSpinner.succeed(chalk.hex('#4ade80')('Integration activated successfully.'));
235
- }
236
- else {
237
- actSpinner.warn(chalk.hex('#FFA726')('Could not activate the integration. You can activate it manually from the platform.'));
238
- }
239
- channels.push({ channel: CHANNEL_AGENT_MAP[channelType], key, multianswer: false });
284
+ await activateWithSpinner(baseUrl, ctx.workspaceId, key.trim(), headers);
285
+ channels.push({ channel: CHANNEL_AGENT_MAP[channelType], key: key.trim(), multianswer: false });
240
286
  }
241
287
  }
242
288
  }
243
289
  }
244
290
  catch (err) {
245
- spinner.fail(chalk.hex('#EF5350')(`Error: ${err instanceof Error ? err.message : err}`));
291
+ spinner.fail(chalk.hex('#EF5350')(`Could not generate the link: ${err instanceof Error ? err.message : err}.`));
246
292
  }
247
293
  }
248
294
  const { more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Connect another channel?', default: false }]);
@@ -250,22 +296,32 @@ export async function connectChannelFlow(ctx) {
250
296
  }
251
297
  return channels;
252
298
  }
299
+ function validatePhone(v) {
300
+ try {
301
+ normalizePhone(v);
302
+ return true;
303
+ }
304
+ catch (e) {
305
+ return e instanceof CliError ? e.message : 'Invalid phone number';
306
+ }
307
+ }
253
308
  export async function runAgentWizard(zone, workspaceId, apiKey, dev) {
254
- console.log(section('Create new AI agent'));
255
- console.log(theme.muted(' Answer the following questions to configure your agent.\n'));
256
- // Paso 1: Informacion basica
309
+ requireInteractive('The agent wizard', 'Pass a JSON file instead: plazbot agent create ./agent.json');
310
+ console.log(section('Create a new AI agent'));
311
+ console.log(theme.muted(' Answer a few questions to configure your agent. Press Enter to accept defaults.\n'));
312
+ // Step 1: basic information
257
313
  console.log(theme.bold('\n Step 1/8: Basic information'));
258
314
  const basic = await inquirer.prompt([
259
315
  {
260
316
  type: 'input',
261
317
  name: 'name',
262
318
  message: 'Agent name:',
263
- validate: (v) => v.length > 0 || 'Name is required',
319
+ validate: (v) => v.trim().length > 0 || 'The name is required',
264
320
  },
265
321
  {
266
322
  type: 'input',
267
323
  name: 'description',
268
- message: 'Short description:',
324
+ message: 'Short description (optional):',
269
325
  default: '',
270
326
  },
271
327
  {
@@ -275,11 +331,15 @@ export async function runAgentWizard(zone, workspaceId, apiKey, dev) {
275
331
  default: 'You are a friendly and professional virtual assistant.',
276
332
  },
277
333
  {
278
- type: 'number',
334
+ type: 'input',
279
335
  name: 'buffer',
280
- message: 'Conversation buffer (3-20 messages):',
281
- default: 5,
282
- validate: (v) => (v >= 3 && v <= 20) || 'Must be between 3 and 20',
336
+ message: 'Conversation memory: how many previous messages the agent reads (3-20):',
337
+ default: '5',
338
+ validate: (v) => {
339
+ const n = Number(String(v).trim());
340
+ return (Number.isInteger(n) && n >= 3 && n <= 20) || 'Enter a whole number of messages between 3 and 20';
341
+ },
342
+ filter: (v) => String(v).trim(),
283
343
  },
284
344
  {
285
345
  type: 'list',
@@ -289,283 +349,132 @@ export async function runAgentWizard(zone, workspaceId, apiKey, dev) {
289
349
  default: 'blue',
290
350
  },
291
351
  ]);
292
- // Paso 2: Personalidad
352
+ // Step 2: personality
293
353
  console.log(theme.bold('\n Step 2/8: Personality and instructions'));
294
354
  const personality = await inquirer.prompt([
295
- {
296
- type: 'list',
297
- name: 'tone',
298
- message: 'Communication tone:',
299
- choices: ['profesional', 'amigable', 'formal', 'casual', 'tecnico', 'empatico'],
300
- default: 'profesional',
301
- },
302
- {
303
- type: 'list',
304
- name: 'style',
305
- message: 'Response style:',
306
- choices: ['conciso', 'detallado', 'conversacional', 'directo'],
307
- default: 'conciso',
308
- },
309
- {
310
- type: 'input',
311
- name: 'personality',
312
- message: 'Personality in one sentence:',
313
- default: 'Helpful and knowledgeable',
314
- },
315
- {
316
- type: 'input',
317
- name: 'objective',
318
- message: 'Main objective of the agent:',
319
- default: 'Help users with their queries',
320
- },
321
- {
322
- type: 'list',
323
- name: 'language',
324
- message: 'Primary language:',
325
- choices: ['Espanol', 'English', 'Portugues', 'Frances'],
326
- default: 'Espanol',
327
- },
328
- {
329
- type: 'confirm',
330
- name: 'useEmojis',
331
- message: 'Use emojis in responses?',
332
- default: true,
333
- },
355
+ { type: 'list', name: 'tone', message: 'Tone:', choices: TONES, default: 'profesional' },
356
+ { type: 'list', name: 'style', message: 'Response style:', choices: STYLES, default: 'conciso' },
357
+ { type: 'input', name: 'personality', message: 'Personality in one sentence:', default: 'Helpful and knowledgeable' },
358
+ { type: 'input', name: 'objective', message: 'Main objective of the agent:', default: 'Help users with their questions' },
359
+ { type: 'list', name: 'language', message: 'Language the agent replies in:', choices: LANGUAGES, default: 'Espanol' },
360
+ { type: 'confirm', name: 'useEmojis', message: 'Use emojis in replies?', default: true },
334
361
  ]);
335
- // Paso 3: Persona
362
+ // Step 3: identity
336
363
  console.log(theme.bold('\n Step 3/8: Agent identity'));
337
364
  const person = await inquirer.prompt([
338
- {
339
- type: 'input',
340
- name: 'name',
341
- message: 'Character name (how it introduces itself):',
342
- default: basic.name,
343
- },
344
- {
345
- type: 'input',
346
- name: 'role',
347
- message: 'Role (e.g. Sales assistant, Tech support):',
348
- default: 'Virtual assistant',
349
- },
350
- {
351
- type: 'confirm',
352
- name: 'firstPerson',
353
- message: 'Speak in first person?',
354
- default: true,
355
- },
365
+ { type: 'input', name: 'name', message: 'Character name (how it introduces itself):', default: basic.name.trim() },
366
+ { type: 'input', name: 'role', message: 'Role (e.g. Sales assistant, Tech support):', default: 'Virtual assistant' },
367
+ { type: 'confirm', name: 'firstPerson', message: 'Speak in first person?', default: true },
356
368
  ]);
357
- // Paso 4: Fallbacks
369
+ // Step 4: fallbacks (end-user facing, defaults follow the agent language)
358
370
  console.log(theme.bold('\n Step 4/8: Fallback messages'));
371
+ console.log(theme.muted(' Sent to your customers, in the agent language.\n'));
372
+ const fb = FALLBACK_DEFAULTS[personality.language] || FALLBACK_DEFAULTS.Espanol;
359
373
  const fallbacks = await inquirer.prompt([
360
- {
361
- type: 'input',
362
- name: 'noAnswer',
363
- message: 'When it has no answer:',
364
- default: 'Lo siento, no tengo informacion sobre eso. Puedo ayudarte con otra consulta?',
365
- },
366
- {
367
- type: 'input',
368
- name: 'serviceError',
369
- message: 'When there is a service error:',
370
- default: 'Disculpa, estamos experimentando dificultades tecnicas. Intenta de nuevo en unos momentos.',
371
- },
372
- {
373
- type: 'input',
374
- name: 'misunderstanding',
375
- message: 'When it does not understand the question:',
376
- default: 'No estoy seguro de entender tu consulta. Podrias reformularla?',
377
- },
374
+ { type: 'input', name: 'noAnswer', message: 'When it has no answer:', default: fb.noAnswer },
375
+ { type: 'input', name: 'serviceError', message: 'When an external service fails:', default: fb.serviceError },
376
+ { type: 'input', name: 'misunderstanding', message: 'When it does not understand the question:', default: fb.doNotUnderstand },
378
377
  ]);
379
- // Paso 5: Tool Calling
378
+ // Step 5: Tool Calling
380
379
  console.log(theme.bold('\n Step 5/8: Tool Calling'));
381
- console.log(theme.muted(' Tool Calling lets the agent execute automatic actions\n'));
380
+ console.log(theme.muted(' Tool Calling lets the agent call your APIs and run actions (tag, stage, hand off...).\n'));
382
381
  const { useToolCalling } = await inquirer.prompt([
383
- {
384
- type: 'confirm',
385
- name: 'useToolCalling',
386
- message: 'Enable Tool Calling?',
387
- default: true,
388
- },
382
+ { type: 'confirm', name: 'useToolCalling', message: 'Enable Tool Calling?', default: true },
389
383
  ]);
390
- // Servicios (API calls)
391
384
  const services = [];
385
+ const actions = [];
392
386
  if (useToolCalling) {
393
387
  const { addServices } = await inquirer.prompt([{
394
- type: 'confirm',
395
- name: 'addServices',
396
- message: 'Add external services (API calls)?',
397
- default: false,
388
+ type: 'confirm', name: 'addServices', message: 'Add services (external API calls)?', default: false,
398
389
  }]);
399
- if (addServices) {
400
- let addMore = true;
401
- while (addMore) {
402
- console.log(theme.muted('\n New service:'));
403
- const svc = await inquirer.prompt([
404
- { type: 'input', name: 'intent', message: 'Service name/intent:', validate: (v) => v.length > 0 || 'Required' },
405
- { type: 'input', name: 'reference', message: 'Reference keywords:', default: '' },
406
- { type: 'list', name: 'method', message: 'HTTP method:', choices: ['GET', 'POST'] },
407
- { type: 'input', name: 'endpoint', message: 'Endpoint URL:', validate: (v) => v.length > 0 || 'Required' },
408
- { type: 'input', name: 'responseMessage', message: 'Response message:', default: '' },
409
- ]);
410
- // Required fields
411
- const fields = [];
412
- const { addFields } = await inquirer.prompt([{
413
- type: 'confirm', name: 'addFields',
414
- message: 'Add required fields?', default: false,
415
- }]);
416
- if (addFields) {
417
- let moreFields = true;
418
- while (moreFields) {
419
- const field = await inquirer.prompt([
420
- { type: 'input', name: 'name', message: 'Field name:' },
421
- { type: 'list', name: 'type', message: 'Type:', choices: ['string', 'number', 'boolean', 'date'] },
422
- { type: 'input', name: 'description', message: 'Description:' },
423
- { type: 'input', name: 'promptHint', message: 'Hint for the LLM:' },
424
- ]);
425
- fields.push(field);
426
- const { more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another field?', default: false }]);
427
- moreFields = more;
428
- }
429
- }
430
- services.push({
431
- intent: svc.intent,
432
- reference: svc.reference,
433
- enabled: true,
434
- method: svc.method,
435
- endpoint: svc.endpoint,
436
- requiredFields: fields,
437
- headers: {},
438
- bodyTemplate: {},
439
- responseMapping: {},
440
- responseMessage: svc.responseMessage,
441
- responseConditions: [],
442
- });
443
- const { more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another service?', default: false }]);
444
- addMore = more;
445
- }
390
+ let more = addServices;
391
+ while (more) {
392
+ console.log(theme.muted('\n New service:'));
393
+ services.push(await promptService());
394
+ ({ more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another service?', default: false }]));
446
395
  }
447
- }
448
- // Acciones
449
- const actions = [];
450
- if (useToolCalling) {
451
396
  const { addActions } = await inquirer.prompt([{
452
- type: 'confirm',
453
- name: 'addActions',
454
- message: 'Add actions (tag, stage, assign agent)?',
455
- default: false,
397
+ type: 'confirm', name: 'addActions', message: 'Add actions (tag, stage, assign, hand off to a human...)?', default: false,
456
398
  }]);
457
- if (addActions) {
458
- let addMore = true;
459
- while (addMore) {
460
- console.log(theme.muted('\n New action:'));
461
- const act = await inquirer.prompt([
462
- { type: 'input', name: 'intent', message: 'Action name/intent:', validate: (v) => v.length > 0 || 'Required' },
463
- { type: 'input', name: 'reference', message: 'Reference keywords:', default: '' },
464
- {
465
- type: 'list', name: 'actionType', message: 'Action type:',
466
- choices: [
467
- { name: 'Schedule event', value: 'action.event.add' },
468
- { name: 'Update event (reschedule)', value: 'action.event.update' },
469
- { name: 'List events', value: 'action.event.list' },
470
- { name: 'Delete event (cancel)', value: 'action.event.delete' },
471
- { name: 'Add tag', value: 'action.tag' },
472
- { name: 'Change stage', value: 'action.stage' },
473
- { name: 'Hand off to human agent', value: 'action.agentShutDown' },
474
- { name: 'Mark as resolved', value: 'action.solved' },
475
- { name: 'Assign agent', value: 'action.asign' },
476
- { name: 'Segmentation', value: 'action.segmentation' },
477
- ],
478
- },
479
- { type: 'input', name: 'actionValue', message: 'Action value:', default: '' },
480
- { type: 'input', name: 'responseMessage', message: 'Response message:', default: '' },
481
- ]);
482
- actions.push({
483
- intent: act.intent,
484
- reference: act.reference,
485
- enabled: true,
486
- requiredFields: [],
487
- responseMessage: act.responseMessage,
488
- action: [{ type: act.actionType, value: act.actionValue }],
489
- });
490
- const { more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another action?', default: false }]);
491
- addMore = more;
492
- }
399
+ more = addActions;
400
+ while (more) {
401
+ console.log(theme.muted('\n New action:'));
402
+ actions.push(await promptAction());
403
+ ({ more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another action?', default: false }]));
493
404
  }
494
405
  }
495
- // Paso 6: AI Provider
406
+ // Step 6: AI provider
496
407
  console.log(theme.bold('\n Step 6/8: AI provider'));
497
408
  const { configureAI } = await inquirer.prompt([{
498
409
  type: 'confirm',
499
410
  name: 'configureAI',
500
- message: 'Configure a custom AI provider?',
411
+ message: 'Use your own AI provider and API key? (No = platform default AI)',
501
412
  default: false,
502
413
  }]);
503
414
  const aiProviders = [];
504
415
  if (configureAI) {
505
- const ai = await inquirer.prompt([
506
- { type: 'list', name: 'provider', message: 'Provider:', choices: ['openai', 'claude', 'gemini'] },
416
+ const { provider } = await inquirer.prompt([
417
+ { type: 'list', name: 'provider', message: 'Provider:', choices: AI_PROVIDERS.map(p => ({ name: p.label, value: p.value })) },
507
418
  ]);
508
- const models = MODELS[ai.provider] || MODELS.openai;
419
+ const providerId = provider;
509
420
  const aiConfig = await inquirer.prompt([
510
- { type: 'list', name: 'model', message: 'Model:', choices: models },
511
- { type: 'password', name: 'apiToken', message: 'API Token:', mask: '*', validate: (v) => v.length > 0 || 'Required' },
421
+ { type: 'list', name: 'model', message: 'Model:', choices: AI_MODELS[providerId], default: DEFAULT_AI_MODEL[providerId], pageSize: 12 },
422
+ { type: 'password', name: 'apiToken', message: 'API token:', mask: '*', validate: (v) => v.trim().length > 0 || 'The API token is required' },
512
423
  ]);
513
424
  aiProviders.push({
514
- provider: ai.provider,
425
+ provider: providerId,
515
426
  model: aiConfig.model,
516
- apiToken: aiConfig.apiToken,
517
- temperature: 0.7,
518
- maxTokens: 4096,
427
+ apiToken: aiConfig.apiToken.trim(),
428
+ temperature: DEFAULT_TEMPERATURE,
429
+ maxTokens: DEFAULT_MAX_TOKENS,
519
430
  isDefault: true,
520
431
  });
432
+ console.log(theme.muted(` Temperature ${DEFAULT_TEMPERATURE}, max tokens ${DEFAULT_MAX_TOKENS}. Tune them later with: plazbot agent ai-config <agentId>`));
521
433
  }
522
- // Paso 7: Conectar canal
523
- console.log(theme.bold('\n Step 7/8: Connect channel'));
434
+ // Step 7: channels
435
+ console.log(theme.bold('\n Step 7/8: Connect a channel'));
524
436
  let channels = [];
525
437
  const { addChannel } = await inquirer.prompt([{
526
438
  type: 'confirm',
527
439
  name: 'addChannel',
528
- message: 'Connect a channel (WhatsApp, Instagram, Messenger)?',
440
+ message: 'Connect a channel now (WhatsApp, Instagram, Messenger, Telegram)?',
529
441
  default: false,
530
442
  }]);
531
443
  if (addChannel && workspaceId && apiKey) {
532
444
  channels = await connectChannelFlow({ zone, workspaceId, apiKey, dev: dev || false });
533
445
  }
534
446
  else if (addChannel) {
535
- // Fallback manual si no hay credenciales para API
447
+ // Manual fallback when there are no credentials to call the API
536
448
  const wa = await inquirer.prompt([
537
- { type: 'input', name: 'key', message: 'WhatsApp number (with country code):', validate: (v) => v.length > 0 || 'Required' },
538
- { type: 'confirm', name: 'multianswer', message: 'Allow multiple answers?', default: false },
449
+ { type: 'input', name: 'phone', message: 'WhatsApp number with country code (e.g. +51912345678):', validate: validatePhone },
450
+ { type: 'confirm', name: 'multianswer', message: 'Allow multiple replies per message?', default: false },
539
451
  ]);
540
- channels.push({ channel: 'whatsapp', key: wa.key, multianswer: wa.multianswer });
452
+ channels.push({ channel: 'whatsapp', key: normalizePhone(wa.phone), multianswer: wa.multianswer });
541
453
  }
542
- // Paso 8: Ejemplos
454
+ // Step 8: examples
543
455
  console.log(theme.bold('\n Step 8/8: Conversation examples'));
544
456
  const examples = [];
545
457
  const { addExamples } = await inquirer.prompt([{
546
458
  type: 'confirm',
547
459
  name: 'addExamples',
548
- message: 'Add conversation examples?',
460
+ message: 'Add example conversations to guide the agent?',
549
461
  default: false,
550
462
  }]);
551
- if (addExamples) {
552
- let addMore = true;
553
- while (addMore) {
554
- const ex = await inquirer.prompt([
555
- { type: 'input', name: 'user', message: 'User message:' },
556
- { type: 'input', name: 'agent', message: 'Agent response:' },
557
- ]);
558
- examples.push({ value: `User: ${ex.user}\nAgent: ${ex.agent}`, color: 'blue' });
559
- const { more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another example?', default: false }]);
560
- addMore = more;
561
- }
463
+ let moreExamples = addExamples;
464
+ while (moreExamples) {
465
+ const ex = await inquirer.prompt([
466
+ { type: 'input', name: 'user', message: 'Customer message:', validate: (v) => v.trim().length > 0 || 'Required' },
467
+ { type: 'input', name: 'agent', message: 'Agent reply:', validate: (v) => v.trim().length > 0 || 'Required' },
468
+ ]);
469
+ examples.push({ value: `User: ${ex.user}\nAgent: ${ex.agent}`, color: 'blue' });
470
+ ({ more: moreExamples } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another example?', default: false }]));
562
471
  }
563
- // Armar config final (alineado con agent.config.schema.json del backend)
472
+ // Final config (aligned with the backend agent.config.schema.json)
564
473
  const config = {
565
- name: basic.name,
474
+ name: basic.name.trim(),
566
475
  description: basic.description || '',
567
476
  prompt: basic.prompt,
568
- buffer: basic.buffer,
477
+ buffer: Number(basic.buffer),
569
478
  zone,
570
479
  color: basic.color,
571
480
  useToolCalling,
@@ -598,18 +507,20 @@ export async function runAgentWizard(zone, workspaceId, apiKey, dev) {
598
507
  // Preview
599
508
  console.log(section('Configuration preview'));
600
509
  console.log(kvPair('Name', config.name));
601
- console.log(kvPair('Prompt', config.prompt.substring(0, 80) + '...'));
510
+ console.log(kvPair('Prompt', config.prompt.length > 80 ? config.prompt.substring(0, 80) + '…' : config.prompt));
511
+ console.log(kvPair('Memory', `${config.buffer} messages`));
512
+ console.log(kvPair('Language', LANGUAGES.find(l => l.value === config.instructions.language)?.name || config.instructions.language));
602
513
  console.log(kvPair('Tool Calling', config.useToolCalling ? 'Enabled' : 'Disabled'));
603
514
  console.log(kvPair('Services', String(config.services.length)));
604
515
  console.log(kvPair('Actions', String(config.actions.length)));
605
- console.log(kvPair('AI Provider', config.customAIConfig ? config.aiProviders[0]?.provider + ' / ' + config.aiProviders[0]?.model : 'Default (Plazbot)'));
516
+ console.log(kvPair('AI provider', config.customAIConfig ? `${config.aiProviders[0]?.provider} / ${config.aiProviders[0]?.model}` : 'Platform default (Plazbot)'));
606
517
  if (config.channels.length > 0) {
607
518
  config.channels.forEach((ch, i) => {
608
519
  console.log(kvPair(`Channel ${i + 1} (${ch.channel})`, ch.key));
609
520
  });
610
521
  }
611
522
  else {
612
- console.log(kvPair('Channels', 'Not configured'));
523
+ console.log(kvPair('Channels', 'Not connected'));
613
524
  }
614
525
  console.log();
615
526
  return config;