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
@@ -1,8 +1,11 @@
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
4
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
5
+ import { addExamples, addNotes } from '../../utils/help.js';
6
+ import { jsonOption, fail, CliError } from '../../utils/output.js';
7
+ import { describeAgentLoadError } from '../../utils/agent-errors.js';
8
+ import { createApiClient } from '../../utils/api.js';
6
9
  import chalk from 'chalk';
7
10
  import readline from 'readline';
8
11
  import { HubConnectionBuilder, HttpTransportType, HubConnectionState } from '@microsoft/signalr';
@@ -48,8 +51,8 @@ function shortSession(sessionId) {
48
51
  return sessionId.substring(0, 6);
49
52
  }
50
53
  function renderLogLine(log, opts, lastSessionId) {
51
- // Modo JSON crudo
52
- if (opts.jsonMode) {
54
+ // Modo crudo: una linea JSON por evento
55
+ if (opts.raw) {
53
56
  return JSON.stringify(log);
54
57
  }
55
58
  const config = EVENT_COLORS[log.event_type] || DEFAULT_EVENT_COLOR;
@@ -102,38 +105,46 @@ const COMMANDS_HELP = `
102
105
  ${chalk.hex('#4CAF50')('/filter clear')} Remove all filters
103
106
  ${chalk.hex('#4CAF50')('/filters')} Show active filters
104
107
  ${chalk.hex('#4CAF50')('/clear')} Clear screen
105
- ${chalk.hex('#4CAF50')('/json')} Toggle expanded JSON mode
108
+ ${chalk.hex('#4CAF50')('/raw')} Toggle raw mode (one JSON object per event)
106
109
  ${chalk.hex('#4CAF50')('/help')} Show these commands
107
110
 
108
111
  ${chalk.bold('Event types:')}
109
112
  ${Object.entries(EVENT_COLORS).map(([key, val]) => ` ${chalk.hex(val.color)('●')} ${key}`).join('\n')}
110
113
  `;
111
114
  export const monitorCommand = new Command('monitor')
112
- .description('Real-time agent monitor (logs via SignalR)')
115
+ .description('Stream the activity of an agent in real time')
113
116
  .argument('<agentId>', 'Agent ID')
114
- .option('-f, --filter <types>', 'Filter by comma-separated types (e.g. msg_in,msg_out)')
115
- .option('--no-session', 'Hide session ID')
116
- .option('--json', 'Raw JSON mode (for piping)', false)
117
- .option('-w, --workspace <id>', 'Workspace ID (overrides local config)')
118
- .option('-z, --zone <zone>', 'Zone LA or EU (overrides local config)')
117
+ .option('-f, --filter <types>', `Only show these comma-separated event types (${Object.keys(EVENT_COLORS).join(', ')})`)
118
+ .option('--no-session', 'Hide the short session ID on each line')
119
+ .addOption(jsonOption('Print one JSON object per event (JSON Lines) for piping'))
120
+ .addOption(new Option('-w, --workspace <id>', 'Workspace ID (overrides local config)').hideHelp())
121
+ .addOption(new Option('-z, --zone <zone>', 'Zone LA or EU (overrides local config)').hideHelp())
119
122
  .option('--dev', 'Use development environment', false)
120
123
  .action(async (agentId, options) => {
121
124
  try {
122
125
  const credentials = await getStoredCredentials();
123
126
  const effectiveWorkspace = options.workspace || credentials.workspace;
124
- const effectiveZone = (options.zone?.toUpperCase() === 'EU' ? 'EU' : options.zone?.toUpperCase() === 'LA' ? 'LA' : credentials.zone);
127
+ const zoneOverride = options.zone?.toUpperCase();
128
+ if (zoneOverride && zoneOverride !== 'LA' && zoneOverride !== 'EU') {
129
+ throw new CliError(`Invalid zone "${options.zone}".`, 'invalid_zone', 'Use -z LA or -z EU.');
130
+ }
131
+ const effectiveZone = (zoneOverride || credentials.zone);
125
132
  const baseUrl = getBaseUrl(effectiveZone, options.dev);
133
+ // En modo --json todo lo que no es un evento va a stderr, para que stdout sea JSONL puro.
134
+ const status = (text) => (options.json ? process.stderr : process.stdout).write(text);
135
+ const statusLine = (text) => status(text + '\n');
126
136
  if (options.workspace || options.zone) {
127
- console.log(chalk.hex('#FFA726')(`\n Support mode: workspace=${effectiveWorkspace} zone=${effectiveZone}`));
137
+ status(chalk.hex('#FFA726')(`\n Support mode: workspace=${effectiveWorkspace} zone=${effectiveZone}\n`));
128
138
  }
129
139
  // Filtros iniciales
130
140
  const activeFilters = new Set(options.filter ? options.filter.split(',').map(f => f.trim()) : []);
131
- let jsonMode = options.json;
141
+ let rawMode = options.json;
132
142
  let lastSessionId = '';
133
- // Cargar info del agente
143
+ // Cargar info del agente: si falla (ID, workspace o zona incorrectos) no se conecta,
144
+ // porque el monitor quedaria escuchando un agente que no existe sin mostrar nada.
134
145
  let agentName = 'Agent';
135
146
  try {
136
- process.stdout.write(chalk.gray(' Connecting to agent...'));
147
+ status(chalk.gray(' Connecting to agent...'));
137
148
  const agent = new Agent({
138
149
  workspaceId: effectiveWorkspace,
139
150
  apiKey: credentials.apiKey,
@@ -141,16 +152,27 @@ export const monitorCommand = new Command('monitor')
141
152
  ...(options.dev && { customUrl: 'http://localhost:5090' }),
142
153
  });
143
154
  const _res = await agent.getAgentById({ id: agentId });
144
- const info = _res.agent || _res.data || _res;
145
- if (info?.name)
155
+ const info = _res?.agent || _res?.data || _res;
156
+ if (!info || (!info.id && !info.name)) {
157
+ throw Object.assign(new Error('Agent not found'), { response: { status: 404 } });
158
+ }
159
+ if (info.name)
146
160
  agentName = info.name;
147
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
161
+ status('\r' + ' '.repeat(40) + '\r');
148
162
  }
149
- catch {
150
- process.stdout.write('\r' + ' '.repeat(40) + '\r');
163
+ catch (loadErr) {
164
+ status('\r' + ' '.repeat(40) + '\r');
165
+ if (options.json)
166
+ fail(loadErr);
167
+ logger.error(describeAgentLoadError(loadErr, agentId, credentials, {
168
+ dev: options.dev,
169
+ workspaceOverride: options.workspace,
170
+ zoneOverride: options.zone,
171
+ }));
172
+ process.exit(1);
151
173
  }
152
174
  // Dibujar header
153
- if (!jsonMode) {
175
+ if (!options.json) {
154
176
  console.clear();
155
177
  printHeader(agentName);
156
178
  }
@@ -163,6 +185,38 @@ export const monitorCommand = new Command('monitor')
163
185
  })
164
186
  .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
165
187
  .build();
188
+ // El hub no lleva headers de auth (WebSocket): la suscripcion se autoriza con un ticket
189
+ // firmado que emite GET /api/agent/monitor-token (valida que el agente sea del workspace).
190
+ // Si la API aun no tiene ese endpoint se usa el JoinAgentMonitor legado.
191
+ let monitorToken = null;
192
+ const api = createApiClient({
193
+ apiKey: credentials.apiKey,
194
+ workspace: effectiveWorkspace,
195
+ zone: effectiveZone,
196
+ dev: options.dev,
197
+ });
198
+ const joinMonitor = async () => {
199
+ if (!monitorToken) {
200
+ try {
201
+ const { data } = await api.get('/api/agent/monitor-token', {
202
+ params: { agentId, workspaceId: effectiveWorkspace },
203
+ });
204
+ monitorToken = data?.token ?? null;
205
+ }
206
+ catch (err) {
207
+ const code = err?.response?.status;
208
+ if (code && code !== 404) {
209
+ throw new Error(err?.response?.data?.message || `Could not authorize the monitor (HTTP ${code})`);
210
+ }
211
+ }
212
+ }
213
+ if (monitorToken) {
214
+ await connection.invoke('JoinAgentMonitorSecure', agentId, monitorToken);
215
+ }
216
+ else {
217
+ await connection.invoke('JoinAgentMonitor', agentId);
218
+ }
219
+ };
166
220
  // Evento de log
167
221
  connection.on('agent_log', (log) => {
168
222
  // Aplicar filtros
@@ -170,40 +224,35 @@ export const monitorCommand = new Command('monitor')
170
224
  return;
171
225
  const line = renderLogLine(log, {
172
226
  showSession: options.session,
173
- jsonMode,
227
+ raw: rawMode,
174
228
  }, lastSessionId);
175
229
  console.log(line);
176
230
  lastSessionId = log.session_id || lastSessionId;
177
231
  });
178
232
  // Eventos de conexion
233
+ // En --json los avisos de conexion van a stderr (stdout solo lleva eventos)
179
234
  connection.onreconnecting(() => {
180
- if (!jsonMode) {
181
- console.log(chalk.hex('#FFA726')(' ⟳ Reconnecting...'));
182
- }
235
+ statusLine(chalk.hex('#FFA726')(' ⟳ Reconnecting...'));
183
236
  });
184
237
  connection.onreconnected(() => {
185
- if (!jsonMode) {
186
- console.log(chalk.hex('#4ade80')(' ● Reconnected'));
187
- }
188
- connection.invoke('JoinAgentMonitor', agentId).catch(() => { });
238
+ statusLine(chalk.hex('#4ade80')(' ● Reconnected'));
239
+ joinMonitor().catch((err) => {
240
+ statusLine(chalk.hex('#ef4444')(` Could not rejoin the monitor: ${err instanceof Error ? err.message : err}`));
241
+ });
189
242
  });
190
- connection.onclose(() => {
191
- if (!jsonMode) {
192
- console.log(chalk.hex('#ef4444')(' ● Disconnected'));
193
- }
243
+ connection.onclose((err) => {
244
+ statusLine(chalk.hex('#ef4444')(` ● Disconnected${err?.message ? `: ${err.message}` : ''}`));
194
245
  });
195
246
  // Iniciar conexion
196
247
  try {
197
248
  await connection.start();
198
- await connection.invoke('JoinAgentMonitor', agentId);
199
- if (!jsonMode) {
200
- console.log(chalk.hex('#4ade80')(' ● Connected to real-time monitor'));
249
+ await joinMonitor();
250
+ statusLine(chalk.hex('#4ade80')(' ● Connected to the real-time monitor'));
251
+ if (!options.json)
201
252
  console.log();
202
- }
203
253
  }
204
254
  catch (err) {
205
- logger.error(`Could not connect to monitor: ${err instanceof Error ? err.message : err}`);
206
- process.exit(1);
255
+ fail(new CliError(`Could not connect to the monitor: ${err instanceof Error ? err.message : err}`, 'monitor_connect_failed', 'Check your connection and try again.'));
207
256
  }
208
257
  // readline para comandos interactivos
209
258
  const rl = readline.createInterface({
@@ -223,9 +272,10 @@ export const monitorCommand = new Command('monitor')
223
272
  printHeader(agentName);
224
273
  return;
225
274
  }
226
- if (cmd === '/json') {
227
- jsonMode = !jsonMode;
228
- console.log(chalk.gray(` JSON mode: ${jsonMode ? 'enabled' : 'disabled'}`));
275
+ // /json se mantiene como alias de /raw
276
+ if (cmd === '/raw' || cmd === '/json') {
277
+ rawMode = !rawMode;
278
+ statusLine(chalk.gray(` Raw mode: ${rawMode ? 'on' : 'off'}`));
229
279
  return;
230
280
  }
231
281
  if (cmd === '/filters') {
@@ -273,9 +323,7 @@ export const monitorCommand = new Command('monitor')
273
323
  });
274
324
  // Cierre limpio
275
325
  const cleanup = async () => {
276
- if (!jsonMode) {
277
- console.log(chalk.gray('\n Closing monitor...'));
278
- }
326
+ statusLine(chalk.gray('\n Closing monitor...'));
279
327
  try {
280
328
  if (connection.state === HubConnectionState.Connected) {
281
329
  await connection.invoke('LeaveAgentMonitor', agentId);
@@ -286,27 +334,25 @@ export const monitorCommand = new Command('monitor')
286
334
  // Ignorar errores al cerrar
287
335
  }
288
336
  rl.close();
289
- if (!jsonMode) {
290
- console.log(chalk.gray(' Monitor closed.\n'));
291
- }
337
+ statusLine(chalk.gray(' Monitor closed.'));
292
338
  process.exit(0);
293
339
  };
294
340
  process.on('SIGINT', cleanup);
295
341
  process.on('SIGTERM', cleanup);
296
342
  }
297
343
  catch (error) {
298
- const message = error instanceof Error ? error.message : 'Unknown error';
299
- logger.error(message);
300
- process.exit(1);
344
+ fail(error);
301
345
  }
302
346
  });
303
347
  addExamples(monitorCommand, [
304
348
  { description: 'Stream every event of an agent in real time',
305
- command: 'plazbot agent monitor agt_AbcDef123' },
349
+ command: 'plazbot agent monitor age_AbcDef123' },
306
350
  { description: 'Only show inbound/outbound messages and errors',
307
- command: 'plazbot agent monitor agt_AbcDef123 -f msg_in,msg_out,error' },
351
+ command: 'plazbot agent monitor age_AbcDef123 -f msg_in,msg_out,error' },
308
352
  { description: 'Emit raw JSON (one event per line) for piping to jq',
309
- command: 'plazbot agent monitor agt_AbcDef123 --json' },
310
- { description: 'Tail an agent from a different workspace (support mode)',
311
- command: 'plazbot agent monitor agt_AbcDef123 -w wok_Other123 -z LA' },
353
+ command: 'plazbot agent monitor age_AbcDef123 --json' },
354
+ ]);
355
+ addNotes(monitorCommand, [
356
+ 'While it runs, type /help for commands: /filter <type>, /filters, /raw, /clear. Press Ctrl+C to exit.',
357
+ 'With --json, stdout only carries events; status messages go to stderr.',
312
358
  ]);
@@ -2,18 +2,24 @@ import { Command } from 'commander';
2
2
  import { Agent } from 'plazbot';
3
3
  import { getStoredCredentials } from '../../utils/credentials.js';
4
4
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
5
+ import { addExamples, addNotes } from '../../utils/help.js';
6
+ import { createSpinner } from '../../utils/ui.js';
7
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
6
8
  import crypto from 'crypto';
7
9
  export const messageCommand = new Command('message')
8
- .description('Send a message to an agent and get its response')
10
+ .description('Send a single message to an agent and print its answer')
9
11
  .argument('<agentId>', 'Agent ID')
10
- .requiredOption('-q, --question <text>', 'Message or question for the agent')
11
- .option('-s, --session-id <id>', 'Session ID (optional)')
12
- .option('-m, --multiple-answers', 'Allow multiple answers', false)
12
+ .requiredOption('-q, --question <text>', 'Message or question for the agent (required)')
13
+ .option('-s, --session-id <id>', 'Session ID to continue a conversation (default: a new random ID)')
14
+ .option('-m, --multiple-answers', 'Allow the agent to send several answers per turn', false)
15
+ .addOption(jsonOption('Print the answer as JSON ({"answer","sessionId","sources"})'))
13
16
  .option('--dev', 'Use development environment', false)
14
17
  .action(async (agentId, options) => {
15
18
  try {
16
- // Obtener credenciales guardadas
19
+ const question = (options.question || '').trim();
20
+ if (!question) {
21
+ throw new CliError('The question cannot be empty.', 'missing_question', 'Pass it with -q, --question "<text>".');
22
+ }
17
23
  const credentials = await getStoredCredentials();
18
24
  const agent = new Agent({
19
25
  workspaceId: credentials.workspace,
@@ -23,46 +29,63 @@ export const messageCommand = new Command('message')
23
29
  });
24
30
  // Generar un sessionId si no se proporcionó uno
25
31
  const sessionId = options.sessionId || crypto.randomUUID();
26
- logger.title('Sending message to agent');
27
- logger.label('Agent ID', agentId);
28
- logger.label('Question', options.question);
29
- logger.label('Session ID', sessionId);
30
- if (options.multipleAnswers) {
31
- logger.info('Mode: Multiple answers');
32
+ const spinner = createSpinner('Waiting for the agent...');
33
+ spinner.start();
34
+ let response;
35
+ try {
36
+ response = await agent.onMessage({
37
+ agentId,
38
+ question,
39
+ sessionId,
40
+ multipleAnswers: options.multipleAnswers
41
+ });
32
42
  }
33
- const response = await agent.onMessage({
34
- agentId: agentId,
35
- question: options.question,
36
- sessionId,
37
- multipleAnswers: options.multipleAnswers
38
- });
39
- logger.title('Agent Response');
40
- console.log(response.answer);
41
- if (response.sources && response.sources.length > 0) {
43
+ catch (err) {
44
+ spinner.fail('The agent did not answer');
45
+ throw err;
46
+ }
47
+ spinner.stop();
48
+ const sources = (response.sources || []).map((s) => ({
49
+ title: s.title || null,
50
+ url: s.url || null,
51
+ }));
52
+ if (isJsonMode()) {
53
+ printJson({
54
+ answer: response.answer ?? '',
55
+ sessionId,
56
+ ...(sources.length > 0 && { sources }),
57
+ });
58
+ return;
59
+ }
60
+ logger.title('Answer');
61
+ console.log(response.answer ?? '');
62
+ if (sources.length > 0) {
42
63
  logger.title('Sources');
43
- response.sources.forEach((source) => {
64
+ sources.forEach((source) => {
44
65
  logger.label('Title', source.title || 'Untitled');
45
66
  if (source.url)
46
67
  logger.label('URL', source.url);
47
68
  });
48
69
  }
49
- logger.title('Session');
50
- logger.label('ID', sessionId);
70
+ console.log();
71
+ logger.dim(`Session: ${sessionId}`);
72
+ logger.dim(`Continue it with: plazbot agent message ${agentId} -s ${sessionId} -q "..."`);
51
73
  if (options.dev) {
52
- logger.warning('\nEnvironment: development');
74
+ logger.warning('Environment: development');
53
75
  }
54
76
  }
55
77
  catch (error) {
56
- const message = error instanceof Error ? error.message : 'Unknown error while sending the message';
57
- logger.error(message);
58
- process.exit(1);
78
+ fail(error);
59
79
  }
60
80
  });
61
81
  addExamples(messageCommand, [
62
82
  { description: 'Send a one-shot question to an agent',
63
- command: 'plazbot agent message agt_AbcDef123 -q "What plans do you offer?"' },
83
+ command: 'plazbot agent message age_AbcDef123 -q "What plans do you offer?"' },
64
84
  { description: 'Continue an existing session',
65
- command: 'plazbot agent message agt_AbcDef123 -q "And the price?" -s sess_AbcDef123' },
66
- { description: 'Allow multiple answers per turn',
67
- command: 'plazbot agent message agt_AbcDef123 -q "Tell me everything" -m' },
85
+ command: 'plazbot agent message age_AbcDef123 -q "And the price?" -s sess_AbcDef123' },
86
+ { description: 'Get only the answer text (scripts)',
87
+ command: "plazbot agent message age_AbcDef123 -q \"Hi\" --json | jq -r '.answer'" },
88
+ ]);
89
+ addNotes(messageCommand, [
90
+ 'Each call without -s starts a new conversation. Reuse the printed session ID to keep context.',
68
91
  ]);