plazbot-cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +179 -88
  10. package/dist/commands/agent/files.js +155 -73
  11. package/dist/commands/agent/get.js +24 -16
  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 +101 -53
  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 +77 -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 +2 -2
@@ -1,26 +1,43 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  import { Agent } from 'plazbot';
3
3
  import { getStoredCredentials } from '../../utils/credentials.js';
4
- import { logger } from '../../utils/logger.js';
5
4
  import { addExamples } from '../../utils/help.js';
6
5
  import { createSpinner } from '../../utils/ui.js';
7
6
  import chalk from 'chalk';
8
7
  import fs from 'fs';
9
8
  import path from 'path';
10
- import inquirer from 'inquirer';
11
- // Campos validos segun el schema de agentValidation.ts (additionalProperties: false)
9
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
10
+ import { addYesOption, confirmAction } from '../../utils/confirm.js';
11
+ function parseZone(value, flag) {
12
+ const zone = value.toUpperCase();
13
+ if (zone !== 'LA' && zone !== 'EU') {
14
+ throw new CliError(`${flag} must be LA or EU (got "${value}").`, 'invalid_zone');
15
+ }
16
+ return zone;
17
+ }
18
+ // Top-level fields of the agent schema the API validates on create/update
19
+ // (Plazbot-Back-v2/Schemas/agent.config.schema.json, additionalProperties: false).
20
+ // /api/agent/update replaces the whole document, so every field the agent can have must be
21
+ // listed here or an update built from this config would wipe it. `version` is left out
22
+ // (the API manages it).
12
23
  const VALID_AGENT_FIELDS = new Set([
13
24
  'name', 'description', 'prompt', 'zone', 'timezone', 'buffer',
14
25
  'color', 'question', 'enable', 'tags', 'showInChat',
15
26
  'instructions', 'person', 'fallbacks', 'rules',
16
27
  'examples', 'channels', 'services', 'actions', 'variables',
17
28
  'useToolCalling', 'customAIConfig', 'aiProviders',
18
- 'responseMode', 'batchWaitSeconds',
29
+ 'responseMode', 'batchWaitSeconds', 'enableWebSearch', 'enableContingency',
30
+ 'enableAudioInput', 'imagePrompt', 'voiceConfig', 'source',
19
31
  'enableWidget', 'enableWhatsappWidget', 'urlWhatsappWidget',
20
32
  'formWidget', 'darkWidget', 'nameWidget', 'initialShowWidget',
21
- 'fUseAutomationFlowWidget', 'iconWidget',
33
+ 'fUseAutomationFlowWidget', 'iconWidget', 'colorWidget', 'positionWidget',
34
+ 'typeWidget', 'formTypeWidget', 'formFieldsWidget', 'initialViewWidget',
35
+ 'multianswerWidget', 'enablePoweredBy', 'advisorNameWidget', 'advisorNameModeWidget',
22
36
  'enableAvatarWidget', 'avatarGender', 'avatarVoiceId', 'avatarLanguage',
37
+ 'avatarApiKey', 'avatarFaceId', 'avatarQuality', 'avatarBackground',
23
38
  ]);
39
+ /** Fields that hold third-party credentials: export/cross-copy only include them with --ai. */
40
+ export const SECRET_AGENT_FIELDS = ['aiProviders', 'avatarApiKey'];
24
41
  // Campos que el schema espera como string (no pueden ser null)
25
42
  const STRING_FIELDS = new Set([
26
43
  'name', 'description', 'prompt', 'zone', 'timezone', 'color',
@@ -31,11 +48,27 @@ const STRING_FIELDS = new Set([
31
48
  const ARRAY_FIELDS = new Set([
32
49
  'tags', 'examples', 'channels', 'services', 'actions', 'aiProviders', 'variables',
33
50
  ]);
51
+ // Removes null/undefined recursively. Nested objects of the schema accept extra keys, so
52
+ // everything else the agent has is kept (an update built from this config must not lose data).
53
+ function dropNulls(value) {
54
+ if (Array.isArray(value))
55
+ return value.filter((v) => v !== null && v !== undefined).map(dropNulls);
56
+ if (value && typeof value === 'object') {
57
+ const out = {};
58
+ for (const [k, v] of Object.entries(value)) {
59
+ if (v !== null && v !== undefined)
60
+ out[k] = dropNulls(v);
61
+ }
62
+ return out;
63
+ }
64
+ return value;
65
+ }
34
66
  function cleanRequiredFields(fields) {
35
67
  if (!fields || !Array.isArray(fields))
36
68
  return [];
37
69
  return fields.map((f) => {
38
- const clean = {};
70
+ const clean = dropNulls(f);
71
+ delete clean.properties;
39
72
  if (f.name)
40
73
  clean.name = f.name;
41
74
  if (f.description)
@@ -81,7 +114,7 @@ function cleanServices(services) {
81
114
  clean.responseMessage = svc.responseMessage;
82
115
  if (svc.responseConditions && Array.isArray(svc.responseConditions)) {
83
116
  clean.responseConditions = svc.responseConditions.map((rc) => {
84
- const c = { condition: rc.condition || '' };
117
+ const c = { ...dropNulls(rc), condition: rc.condition || '' };
85
118
  if (typeof rc.message === 'string')
86
119
  c.message = rc.message;
87
120
  if (typeof rc.nextService === 'string')
@@ -103,6 +136,7 @@ function cleanActions(actions) {
103
136
  reference: act.reference || '',
104
137
  enabled: act.enabled ?? true,
105
138
  action: Array.isArray(act.action) ? act.action.map((a) => ({
139
+ ...dropNulls(a),
106
140
  type: a.type || '',
107
141
  value: a.value ?? '',
108
142
  })) : [],
@@ -114,6 +148,8 @@ function cleanActions(actions) {
114
148
  clean.responseMessage = act.responseMessage;
115
149
  if (typeof act.responseJson === 'boolean')
116
150
  clean.responseJson = act.responseJson;
151
+ if (typeof act.responseExact === 'boolean')
152
+ clean.responseExact = act.responseExact;
117
153
  return clean;
118
154
  });
119
155
  }
@@ -137,7 +173,7 @@ function cleanVariables(variables) {
137
173
  return clean;
138
174
  });
139
175
  }
140
- function cleanAgentForExport(agentData) {
176
+ export function cleanAgentForExport(agentData) {
141
177
  const cleaned = {};
142
178
  for (const key of Object.keys(agentData)) {
143
179
  if (!VALID_AGENT_FIELDS.has(key))
@@ -207,17 +243,22 @@ export const exportCommand = new Command('export')
207
243
  .argument('<agentId>', 'Agent ID to export')
208
244
  .option('-w, --workspace <id>', 'Source workspace ID (overrides local config)')
209
245
  .option('-z, --zone <zone>', 'Zone LA or EU (overrides local config)')
210
- .option('-o, --output <path>', 'Output file path (.json)')
211
- .option('--no-channels', 'Exclude channels from export')
212
- .option('--no-ai', 'Exclude custom AI configuration')
246
+ .option('-o, --output <path>', 'Write the JSON to this file instead of stdout')
247
+ .option('--channels', 'Include channels (workspace-specific)', false)
248
+ .option('--ai', 'Include the custom AI configuration and the avatar key (contains third-party API keys)', false)
249
+ // Compatibilidad: antes canales e IA se exportaban por defecto y se excluian con --no-*
250
+ .addOption(new Option('--no-channels').hideHelp())
251
+ .addOption(new Option('--no-ai').hideHelp())
252
+ .addOption(jsonOption('With -o, print {"output": "<path>"}; without -o the agent JSON is printed anyway'))
213
253
  .option('--dev', 'Use development environment', false)
214
254
  .action(async (agentId, options) => {
215
255
  try {
216
256
  const credentials = await getStoredCredentials();
217
257
  const effectiveWorkspace = options.workspace || credentials.workspace;
218
- const effectiveZone = (options.zone?.toUpperCase() === 'EU' ? 'EU' : options.zone?.toUpperCase() === 'LA' ? 'LA' : credentials.zone);
258
+ const effectiveZone = options.zone ? parseZone(options.zone, '--zone') : credentials.zone;
219
259
  if (options.workspace || options.zone) {
220
- console.log(chalk.hex('#FFA726')(`\n Support mode: workspace=${effectiveWorkspace} zone=${effectiveZone}`));
260
+ // stderr: stdout queda solo para el JSON
261
+ console.error(chalk.hex('#FFA726')(`\n Support mode: workspace=${effectiveWorkspace} zone=${effectiveZone}`));
221
262
  }
222
263
  const agent = new Agent({
223
264
  workspaceId: effectiveWorkspace,
@@ -227,51 +268,72 @@ export const exportCommand = new Command('export')
227
268
  });
228
269
  const spinner = createSpinner('Fetching agent...');
229
270
  spinner.start();
230
- const response = await agent.getAgentById({ id: agentId });
231
- const agentData = response.agent || response.data || response;
232
- spinner.succeed('Agent fetched');
271
+ let agentData;
272
+ try {
273
+ const response = await agent.getAgentById({ id: agentId });
274
+ agentData = response?.agent || response?.data || response;
275
+ }
276
+ catch (err) {
277
+ spinner.fail('Could not fetch the agent');
278
+ throw err;
279
+ }
280
+ if (!agentData || !agentData.name) {
281
+ spinner.fail('Agent not found');
282
+ throw new CliError(`Agent ${agentId} was not found in workspace ${effectiveWorkspace}.`, 'not_found', "Check the ID with 'plazbot agent list'.");
283
+ }
284
+ spinner.succeed(`Agent fetched: ${agentData.name}`);
233
285
  // Limpiar para exportacion
234
286
  const cleaned = cleanAgentForExport(agentData);
235
- // Aplicar opciones de exclusion
287
+ // Canales e IA solo con --channels / --ai: la config de IA lleva los apiToken de los
288
+ // proveedores. Se omiten las claves (no se ponen en false) para que un
289
+ // export -> editar -> update no borre los canales ni la IA del agente.
236
290
  if (!options.channels) {
237
291
  delete cleaned.channels;
238
292
  }
239
293
  if (!options.ai) {
240
- cleaned.customAIConfig = false;
241
- delete cleaned.aiProviders;
294
+ delete cleaned.customAIConfig;
295
+ SECRET_AGENT_FIELDS.forEach((field) => delete cleaned[field]);
242
296
  }
243
297
  const jsonOutput = JSON.stringify(cleaned, null, 2);
244
298
  // Guardar en archivo o mostrar en consola
245
299
  if (options.output) {
246
300
  const outputPath = path.resolve(options.output);
247
- fs.writeFileSync(outputPath, jsonOutput, 'utf-8');
301
+ fs.writeFileSync(outputPath, jsonOutput + '\n', 'utf-8');
302
+ if (isJsonMode()) {
303
+ printJson({ output: outputPath, id: agentId, name: cleaned.name ?? null });
304
+ return;
305
+ }
248
306
  console.log(chalk.hex('#4ade80')(`\n Exported to: ${outputPath}`));
249
307
  }
250
308
  else {
251
- console.log();
309
+ // stdout solo lleva el JSON, para poder redirigirlo (export > agent.json)
252
310
  console.log(jsonOutput);
311
+ if (isJsonMode())
312
+ return;
253
313
  }
254
- console.log();
255
- console.log(chalk.gray(' This JSON is compatible with:'));
256
- console.log(chalk.gray(' - plazbot agent create <file.json>'));
257
- console.log(chalk.gray(' - Importing JSON in the platform (AgentTraining)'));
258
- console.log();
314
+ console.error();
315
+ console.error(chalk.gray(' This JSON is compatible with:'));
316
+ console.error(chalk.gray(' - plazbot agent create <file.json>'));
317
+ console.error(chalk.gray(' - plazbot agent update <agentId> <file.json>'));
318
+ console.error(chalk.gray(' - Importing JSON in the platform (AgentTraining)'));
319
+ if (!options.channels || !options.ai) {
320
+ console.error(chalk.gray(` Not included: ${[!options.channels && 'channels (--channels)', !options.ai && 'AI configuration (--ai)'].filter(Boolean).join(', ')}`));
321
+ }
322
+ console.error();
259
323
  }
260
324
  catch (error) {
261
- const message = error instanceof Error ? error.message : 'Unknown error';
262
- logger.error(message);
263
- process.exit(1);
325
+ fail(error);
264
326
  }
265
327
  });
266
328
  addExamples(exportCommand, [
267
329
  { description: 'Print the cleaned JSON to stdout',
268
- command: 'plazbot agent export agt_AbcDef123' },
330
+ command: 'plazbot agent export age_AbcDef123' },
269
331
  { description: 'Write the cleaned JSON to a file',
270
- command: 'plazbot agent export agt_AbcDef123 -o ./agent.json' },
271
- { description: 'Export without channels nor AI credentials (portable JSON)',
272
- command: 'plazbot agent export agt_AbcDef123 -o ./agent.json --no-channels --no-ai' },
332
+ command: 'plazbot agent export age_AbcDef123 -o ./agent.json' },
333
+ { description: 'Include channels and the AI configuration (contains provider API tokens)',
334
+ command: 'plazbot agent export age_AbcDef123 -o ./agent.json --channels --ai' },
273
335
  { description: 'Export from a different workspace (support mode)',
274
- command: 'plazbot agent export agt_AbcDef123 -w wok_Other123 -z LA -o ./agent.json' },
336
+ command: 'plazbot agent export age_AbcDef123 -w wok_Other123 -z LA -o ./agent.json' },
275
337
  ]);
276
338
  export const crossCopyCommand = new Command('cross-copy')
277
339
  .description('Copy an agent from one workspace to another')
@@ -280,23 +342,29 @@ export const crossCopyCommand = new Command('cross-copy')
280
342
  .requiredOption('--to-workspace <id>', 'Target workspace ID')
281
343
  .option('--from-zone <zone>', 'Source workspace zone (LA or EU)', 'LA')
282
344
  .option('--to-zone <zone>', 'Target workspace zone (LA or EU)', 'LA')
283
- .option('--no-channels', 'Do not copy channels (recommended)')
284
- .option('--no-ai', 'Do not copy custom AI configuration')
285
- .option('--dev', 'Use development environment', false)
286
- .action(async (agentId, options) => {
345
+ .option('--channels', 'Also copy channels (usually wrong: channels belong to the source workspace)', false)
346
+ .option('--ai', 'Also copy the custom AI configuration and the avatar key (contains third-party API keys)', false)
347
+ .addOption(new Option('--no-channels').hideHelp())
348
+ .addOption(new Option('--no-ai').hideHelp())
349
+ .addOption(jsonOption('Print the result as JSON ({"id","sourceId","fromWorkspace","toWorkspace"})'))
350
+ .option('--dev', 'Use development environment', false);
351
+ addYesOption(crossCopyCommand);
352
+ crossCopyCommand.action(async (agentId, options) => {
287
353
  try {
288
354
  const credentials = await getStoredCredentials();
289
- const fromZone = (options.fromZone.toUpperCase() === 'EU' ? 'EU' : 'LA');
290
- const toZone = (options.toZone.toUpperCase() === 'EU' ? 'EU' : 'LA');
291
- console.log();
292
- console.log(chalk.hex('#4CAF50')(' ┌' + '─'.repeat(58) + '┐'));
293
- console.log(chalk.hex('#4CAF50')(' │') + chalk.bold(' Copy agent between workspaces').padEnd(68) + chalk.hex('#4CAF50')('│'));
294
- console.log(chalk.hex('#4CAF50')(' └' + '─'.repeat(58) + '┘'));
295
- console.log();
296
- console.log(chalk.gray(` Source: ${options.fromWorkspace} (${fromZone})`));
297
- console.log(chalk.gray(` Target: ${options.toWorkspace} (${toZone})`));
298
- console.log(chalk.gray(` Agent: ${agentId}`));
299
- console.log();
355
+ // En --json todo lo informativo va a stderr
356
+ const out = (message = '') => (isJsonMode() ? console.error : console.log)(message);
357
+ const fromZone = parseZone(options.fromZone, '--from-zone');
358
+ const toZone = parseZone(options.toZone, '--to-zone');
359
+ out();
360
+ out(chalk.hex('#4CAF50')(' ┌' + '─'.repeat(58) + '┐'));
361
+ out(chalk.hex('#4CAF50')(' │') + chalk.bold(' Copy agent between workspaces').padEnd(68) + chalk.hex('#4CAF50')('│'));
362
+ out(chalk.hex('#4CAF50')(' └' + '─'.repeat(58) + '┘'));
363
+ out();
364
+ out(chalk.gray(` Source: ${options.fromWorkspace} (${fromZone})`));
365
+ out(chalk.gray(` Target: ${options.toWorkspace} (${toZone})`));
366
+ out(chalk.gray(` Agent: ${agentId}`));
367
+ out();
300
368
  // 1. Obtener agente del workspace origen
301
369
  const spinnerGet = createSpinner('Fetching agent from source workspace...');
302
370
  spinnerGet.start();
@@ -306,11 +374,18 @@ export const crossCopyCommand = new Command('cross-copy')
306
374
  zone: fromZone,
307
375
  ...(options.dev && { customUrl: 'http://localhost:5090' }),
308
376
  });
309
- const response = await sourceAgent.getAgentById({ id: agentId });
310
- const agentData = response.agent || response.data || response;
377
+ let agentData;
378
+ try {
379
+ const response = await sourceAgent.getAgentById({ id: agentId });
380
+ agentData = response?.agent || response?.data || response;
381
+ }
382
+ catch (err) {
383
+ spinnerGet.fail('Could not fetch the agent from the source workspace');
384
+ throw err;
385
+ }
311
386
  if (!agentData || !agentData.name) {
312
- spinnerGet.fail('Agent not found in source workspace');
313
- process.exit(1);
387
+ spinnerGet.fail('Agent not found in the source workspace');
388
+ throw new CliError(`Agent ${agentId} was not found in workspace ${options.fromWorkspace} (${fromZone}).`, 'not_found', 'Check --from-workspace and --from-zone.');
314
389
  }
315
390
  spinnerGet.succeed(`Agent fetched: ${agentData.name}`);
316
391
  // 2. Limpiar JSON
@@ -324,29 +399,24 @@ export const crossCopyCommand = new Command('cross-copy')
324
399
  // Limpiar AI config si se solicita
325
400
  if (!options.ai) {
326
401
  cleaned.customAIConfig = false;
327
- delete cleaned.aiProviders;
402
+ SECRET_AGENT_FIELDS.forEach((field) => delete cleaned[field]);
328
403
  }
329
404
  // Desactivar widget (seguridad)
330
405
  cleaned.enableWidget = false;
331
406
  // 3. Mostrar resumen
332
- console.log();
333
- console.log(chalk.bold(' Configuration to copy:'));
334
- console.log(chalk.gray(` Name: ${cleaned.name}`));
335
- console.log(chalk.gray(` Target zone: ${cleaned.zone}`));
336
- console.log(chalk.gray(` Channels: ${cleaned.channels?.length || 0}`));
337
- console.log(chalk.gray(` Services: ${cleaned.services?.length || 0}`));
338
- console.log(chalk.gray(` Actions: ${cleaned.actions?.length || 0}`));
339
- console.log(chalk.gray(` Custom AI: ${cleaned.customAIConfig ? 'Yes' : 'No'}`));
340
- console.log();
341
- const { confirm } = await inquirer.prompt([{
342
- type: 'confirm',
343
- name: 'confirm',
344
- message: 'Create this agent in the target workspace?',
345
- default: true,
346
- }]);
347
- if (!confirm) {
348
- console.log(chalk.gray(' Operation cancelled.'));
349
- return;
407
+ out();
408
+ out(chalk.bold(' Configuration to copy:'));
409
+ out(chalk.gray(` Name: ${cleaned.name}`));
410
+ out(chalk.gray(` Target zone: ${cleaned.zone}`));
411
+ out(chalk.gray(` Channels: ${cleaned.channels?.length || 0}`));
412
+ out(chalk.gray(` Services: ${cleaned.services?.length || 0}`));
413
+ out(chalk.gray(` Actions: ${cleaned.actions?.length || 0}`));
414
+ out(chalk.gray(` Custom AI: ${cleaned.customAIConfig ? 'Yes' : 'No'}`));
415
+ out();
416
+ const ok = await confirmAction(`Create "${cleaned.name}" in workspace ${options.toWorkspace} (${toZone})?`, options);
417
+ if (!ok) {
418
+ out(chalk.gray(' Aborted.'));
419
+ process.exit(1);
350
420
  }
351
421
  // 4. Crear en workspace destino
352
422
  const spinnerCreate = createSpinner('Creating agent in target workspace...');
@@ -357,32 +427,53 @@ export const crossCopyCommand = new Command('cross-copy')
357
427
  zone: toZone,
358
428
  ...(options.dev && { customUrl: 'http://localhost:5090' }),
359
429
  });
360
- const result = await targetAgent.addAgent(cleaned);
430
+ let result;
431
+ try {
432
+ result = await targetAgent.addAgent(cleaned);
433
+ }
434
+ catch (err) {
435
+ spinnerCreate.fail('Could not create the agent in the target workspace');
436
+ throw err;
437
+ }
361
438
  if (result?.success === false) {
362
- spinnerCreate.fail(`Error: ${result.message || 'Could not create the agent'}`);
363
- process.exit(1);
439
+ spinnerCreate.fail('Could not create the agent in the target workspace');
440
+ throw new CliError(result.message || 'The API rejected the agent configuration.', 'create_failed');
364
441
  }
365
442
  const newId = result?.agentId || result?.id || result?.data?.agentId;
366
443
  spinnerCreate.succeed('Agent created successfully');
367
- console.log();
444
+ if (isJsonMode()) {
445
+ printJson({
446
+ id: newId ?? null,
447
+ sourceId: agentId,
448
+ name: cleaned.name ?? null,
449
+ fromWorkspace: options.fromWorkspace,
450
+ toWorkspace: options.toWorkspace,
451
+ fromZone,
452
+ toZone,
453
+ });
454
+ return;
455
+ }
456
+ out();
368
457
  if (newId) {
369
- console.log(chalk.hex('#4ade80')(` New ID: ${newId}`));
458
+ out(chalk.hex('#4ade80')(` New ID: ${newId}`));
459
+ }
460
+ out(chalk.gray(' Widget disabled by default.'));
461
+ if (!options.channels) {
462
+ out(chalk.gray(' Channels were not copied: connect them in the target workspace.'));
370
463
  }
371
- console.log(chalk.gray(' Widget disabled by default.'));
372
- console.log(chalk.gray(' Channels must be configured manually in the target workspace.'));
373
- console.log();
464
+ out();
374
465
  }
375
466
  catch (error) {
376
- const message = error instanceof Error ? error.message : 'Unknown error';
377
- logger.error(message);
378
- process.exit(1);
467
+ fail(error);
379
468
  }
380
469
  });
381
470
  addExamples(crossCopyCommand, [
382
- { description: 'Copy an agent between two LA workspaces (skip channels)',
383
- command: 'plazbot agent cross-copy agt_AbcDef123 --from-workspace wok_Source123 --to-workspace wok_Target456' },
471
+ { description: 'Copy an agent between two LA workspaces (without channels nor AI config)',
472
+ command: 'plazbot agent cross-copy age_AbcDef123 --from-workspace wok_Source123 --to-workspace wok_Target456' },
384
473
  { description: 'Copy across zones (LA → EU)',
385
- command: 'plazbot agent cross-copy agt_AbcDef123 --from-workspace wok_Source123 --from-zone LA --to-workspace wok_Target456 --to-zone EU' },
474
+ command: 'plazbot agent cross-copy age_AbcDef123 --from-workspace wok_Source123 --from-zone LA --to-workspace wok_Target456 --to-zone EU' },
475
+ { description: 'Copy without the confirmation prompt (scripts and CI)',
476
+ command: 'plazbot agent cross-copy age_AbcDef123 --from-workspace wok_Source123 --to-workspace wok_Target456 --yes --json' },
386
477
  { description: 'Copy preserving channels and AI providers',
387
- command: 'plazbot agent cross-copy agt_AbcDef123 --from-workspace wok_Source123 --to-workspace wok_Target456 --channels --ai' },
478
+ command: 'plazbot agent cross-copy age_AbcDef123 --from-workspace wok_Source123 --to-workspace wok_Target456 --channels --ai' },
388
479
  ]);