plazbot-cli 0.4.0 → 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 +183 -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
@@ -3,14 +3,247 @@ import { Agent } from 'plazbot';
3
3
  import inquirer from 'inquirer';
4
4
  import { getStoredCredentials } from '../../utils/credentials.js';
5
5
  import { logger } from '../../utils/logger.js';
6
- import { addExamples } from '../../utils/help.js';
6
+ import { addExamples, addNotes } from '../../utils/help.js';
7
+ import { describeAgentLoadError } from '../../utils/agent-errors.js';
7
8
  import { createSpinner, createTable, theme, section, kvPair, statusBadge } from '../../utils/ui.js';
9
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
10
+ import { confirmAction, requireInteractive } from '../../utils/confirm.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Shared tool definitions (wizard.ts reuses these prompts).
13
+ // Action values match the front (SectionUIEdition.tsx) and the engine.
14
+ // ---------------------------------------------------------------------------
15
+ // Same list as the agent schema the API validates (Plazbot-Back-v2/Schemas/agent.config.schema.json)
16
+ export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
17
+ export const ACTION_TYPES = [
18
+ { group: 'Contact', value: 'action.asign', label: 'Assign agent', valueHint: 'agent user ID or email' },
19
+ { group: 'Contact', value: 'action.unassign', label: 'Unassign agent' },
20
+ { group: 'Contact', value: 'action.stage', label: 'Set stage', valueHint: 'stage ID' },
21
+ { group: 'Contact', value: 'action.stage.remove', label: 'Remove stage' },
22
+ { group: 'Contact', value: 'action.segmentation', label: 'Set segmentation', valueHint: 'segmentation ID' },
23
+ { group: 'Contact', value: 'action.segmentation.remove', label: 'Remove segmentation' },
24
+ { group: 'Contact', value: 'action.tag', label: 'Add tag', valueHint: 'tag ID' },
25
+ { group: 'Contact', value: 'action.tag.remove', label: 'Remove tag', valueHint: 'tag ID' },
26
+ { group: 'Contact', value: 'action.solved', label: 'Mark chat as resolved' },
27
+ { group: 'Contact', value: 'action.agentShutDown', label: 'Hand off to human agent (turns the AI agent off)' },
28
+ { group: 'Contact', value: 'action.secuence', label: 'Assign sequence', valueHint: 'sequence ID' },
29
+ { group: 'Contact', value: 'action.worker', label: 'Run worker', valueHint: 'worker ID' },
30
+ { group: 'Events', value: 'action.event.add', label: 'Schedule event' },
31
+ { group: 'Events', value: 'action.event.update', label: 'Reschedule event' },
32
+ { group: 'Events', value: 'action.event.delete', label: 'Delete event' },
33
+ { group: 'Events', value: 'action.event.list', label: 'List events' },
34
+ { group: 'Events', value: 'action.event.confirm', label: 'Confirm event' },
35
+ { group: 'Events', value: 'action.event.cancel', label: 'Cancel event' },
36
+ { group: 'Files', value: 'action.send.image', label: 'Send image', valueHint: 'image URL' },
37
+ { group: 'Files', value: 'action.send.file', label: 'Send file', valueHint: 'file URL' },
38
+ ];
39
+ export function actionLabel(type) {
40
+ return ACTION_TYPES.find(a => a.value === type)?.label || type;
41
+ }
42
+ const FIELD_TYPES = ['string', 'number', 'integer', 'boolean', 'date', 'datetime', 'email', 'phone'];
43
+ const required = (label) => (v) => (v && v.trim().length > 0) || `${label} is required`;
44
+ function validateUrl(v) {
45
+ try {
46
+ const u = new URL(v.trim());
47
+ return (u.protocol === 'http:' || u.protocol === 'https:') || 'Use an http(s) URL';
48
+ }
49
+ catch {
50
+ return 'Enter a valid URL (e.g. https://api.example.com/orders/{{orderId}})';
51
+ }
52
+ }
53
+ async function promptRequiredFields() {
54
+ const fields = [];
55
+ const { addFields } = await inquirer.prompt([{
56
+ type: 'confirm', name: 'addFields',
57
+ message: 'Add required fields (data the agent must collect from the user first)?', default: false,
58
+ }]);
59
+ let more = addFields;
60
+ while (more) {
61
+ const field = await inquirer.prompt([
62
+ { type: 'input', name: 'name', message: 'Field name (e.g. orderId):', validate: required('Field name') },
63
+ { type: 'list', name: 'type', message: 'Type:', choices: FIELD_TYPES },
64
+ { type: 'input', name: 'description', message: 'Description:', default: '' },
65
+ { type: 'input', name: 'promptHint', message: 'Hint for the AI (how to ask for it):', default: '' },
66
+ ]);
67
+ fields.push({ ...field, name: field.name.trim() });
68
+ ({ more } = await inquirer.prompt([{ type: 'confirm', name: 'more', message: 'Add another field?', default: false }]));
69
+ }
70
+ return fields;
71
+ }
72
+ async function promptHeaders() {
73
+ const headers = {};
74
+ const { addHeaders } = await inquirer.prompt([{
75
+ type: 'confirm', name: 'addHeaders', message: 'Add HTTP headers (e.g. Authorization)?', default: false,
76
+ }]);
77
+ if (!addHeaders)
78
+ return headers;
79
+ while (true) {
80
+ const { name } = await inquirer.prompt([{ type: 'input', name: 'name', message: 'Header name (leave empty to finish):' }]);
81
+ if (!name.trim())
82
+ break;
83
+ const { value } = await inquirer.prompt([{ type: 'input', name: 'value', message: `Value for ${name.trim()}:`, validate: required('Value') }]);
84
+ headers[name.trim()] = value;
85
+ }
86
+ return headers;
87
+ }
88
+ async function promptBodyTemplate() {
89
+ const { body } = await inquirer.prompt([{
90
+ type: 'input',
91
+ name: 'body',
92
+ message: 'JSON body template (use {{fieldName}} for required fields; leave empty for none):',
93
+ default: '',
94
+ validate: (v) => {
95
+ if (!v.trim())
96
+ return true;
97
+ try {
98
+ const parsed = JSON.parse(v);
99
+ return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) || 'The body must be a JSON object, e.g. {"orderId":"{{orderId}}"}';
100
+ }
101
+ catch (e) {
102
+ return `Invalid JSON: ${e instanceof Error ? e.message : e}`;
103
+ }
104
+ },
105
+ }]);
106
+ return body.trim() ? JSON.parse(body) : {};
107
+ }
108
+ /** Interactive prompts for a new service (external API call). */
109
+ export async function promptService() {
110
+ const svc = await inquirer.prompt([
111
+ { type: 'input', name: 'intent', message: 'Service name/intent (e.g. check_order_status):', validate: required('Name') },
112
+ { type: 'input', name: 'reference', message: 'When should the agent use it? (description/keywords):', default: '' },
113
+ { type: 'list', name: 'method', message: 'HTTP method:', choices: [...HTTP_METHODS] },
114
+ { type: 'input', name: 'endpoint', message: 'Endpoint URL:', validate: validateUrl },
115
+ ]);
116
+ const requiredFields = await promptRequiredFields();
117
+ const headers = await promptHeaders();
118
+ const bodyTemplate = (svc.method === 'GET' || svc.method === 'DELETE') ? {} : await promptBodyTemplate();
119
+ const { responseMessage } = await inquirer.prompt([
120
+ { type: 'input', name: 'responseMessage', message: 'Response message (optional):', default: '' },
121
+ ]);
122
+ return {
123
+ intent: svc.intent.trim(),
124
+ reference: svc.reference,
125
+ enabled: true,
126
+ method: svc.method,
127
+ endpoint: svc.endpoint.trim(),
128
+ requiredFields,
129
+ headers,
130
+ bodyTemplate,
131
+ responseMapping: {},
132
+ responseMessage,
133
+ responseConditions: [],
134
+ };
135
+ }
136
+ /** Interactive prompts for a new action (tag, stage, hand-off, events...). */
137
+ export async function promptAction() {
138
+ const choices = [];
139
+ let lastGroup = '';
140
+ for (const a of ACTION_TYPES) {
141
+ if (a.group !== lastGroup) {
142
+ choices.push(new inquirer.Separator(theme.muted(`-- ${a.group} --`)));
143
+ lastGroup = a.group;
144
+ }
145
+ choices.push({ name: a.label, value: a.value });
146
+ }
147
+ const act = await inquirer.prompt([
148
+ { type: 'input', name: 'intent', message: 'Action name/intent (e.g. talk_to_human):', validate: required('Name') },
149
+ { type: 'input', name: 'reference', message: 'When should the agent use it? (description/keywords):', default: '' },
150
+ { type: 'list', name: 'actionType', message: 'Action type:', choices, pageSize: 14 },
151
+ ]);
152
+ const hint = ACTION_TYPES.find(a => a.value === act.actionType)?.valueHint;
153
+ const { actionValue } = hint
154
+ ? await inquirer.prompt([{ type: 'input', name: 'actionValue', message: `Value (${hint}):`, default: '' }])
155
+ : { actionValue: '' };
156
+ const requiredFields = await promptRequiredFields();
157
+ const { responseMessage } = await inquirer.prompt([
158
+ { type: 'input', name: 'responseMessage', message: 'Response message (optional):', default: '' },
159
+ ]);
160
+ return {
161
+ intent: act.intent.trim(),
162
+ reference: act.reference,
163
+ enabled: true,
164
+ requiredFields,
165
+ responseMessage,
166
+ action: [{ type: act.actionType, value: actionValue }],
167
+ };
168
+ }
169
+ // ---------------------------------------------------------------------------
170
+ const SECRET_HEADER = /auth|token|key|secret|password|cookie/i;
171
+ function maskHeaders(headers) {
172
+ const out = {};
173
+ for (const [k, v] of Object.entries(headers || {})) {
174
+ out[k] = SECRET_HEADER.test(k) ? (String(v).length > 10 ? `${String(v).slice(0, 4)}…${String(v).slice(-4)}` : '••••') : String(v);
175
+ }
176
+ return out;
177
+ }
178
+ function toolsAsJson(agentId, agentData) {
179
+ return {
180
+ id: agentId,
181
+ name: agentData.name,
182
+ useToolCalling: !!agentData.useToolCalling,
183
+ services: (agentData.services || []).map((s) => ({
184
+ intent: s.intent,
185
+ reference: s.reference || '',
186
+ method: s.method || 'POST',
187
+ endpoint: s.endpoint || '',
188
+ enabled: s.enabled !== false,
189
+ requiredFields: (s.requiredFields || []).map((f) => ({ name: f.name, type: f.type })),
190
+ headers: maskHeaders(s.headers),
191
+ })),
192
+ actions: (agentData.actions || []).map((a) => ({
193
+ intent: a.intent,
194
+ reference: a.reference || '',
195
+ enabled: a.enabled !== false,
196
+ actions: (a.action || []).map((ac) => ({ type: ac.type, label: actionLabel(ac.type), value: ac.value ?? '' })),
197
+ })),
198
+ };
199
+ }
200
+ function printTools(agentId, agentData) {
201
+ console.log(section('Tool Calling - ' + (agentData.name || agentId)));
202
+ console.log(kvPair('Status', agentData.useToolCalling ? theme.success('Enabled') : theme.error('Disabled')));
203
+ const services = agentData.services || [];
204
+ console.log(section(`Services - API calls (${services.length})`));
205
+ if (services.length > 0) {
206
+ const rows = services.map((s) => [
207
+ s.intent,
208
+ s.method || 'POST',
209
+ s.endpoint ? (s.endpoint.length > 45 ? s.endpoint.substring(0, 44) + '…' : s.endpoint) : '-',
210
+ statusBadge(s.enabled !== false),
211
+ ]);
212
+ console.log(createTable(['Intent', 'Method', 'Endpoint', 'Status'], rows));
213
+ }
214
+ else {
215
+ console.log(theme.muted(' No services configured'));
216
+ }
217
+ const actions = agentData.actions || [];
218
+ console.log(section(`Actions (${actions.length})`));
219
+ if (actions.length > 0) {
220
+ const rows = actions.map((a) => [
221
+ a.intent,
222
+ (a.action || []).map((ac) => actionLabel(ac.type)).join(', '),
223
+ statusBadge(a.enabled !== false),
224
+ ]);
225
+ console.log(createTable(['Intent', 'Type', 'Status'], rows));
226
+ }
227
+ else {
228
+ console.log(theme.muted(' No actions configured'));
229
+ }
230
+ if (!agentData.useToolCalling && (services.length > 0 || actions.length > 0)) {
231
+ logger.warning('Tool Calling is disabled, so the agent will not use these tools.');
232
+ }
233
+ console.log();
234
+ }
8
235
  export const toolsCommand = new Command('tools')
9
- .description('Manage agent Tool Calling tools')
10
- .argument('<agentId>', 'Agent ID')
236
+ .description('List, add and remove the services and actions the agent can call (Tool Calling)')
237
+ .argument('<agentId>', 'Agent ID (e.g. age_AbcDef123)')
238
+ .option('--list', 'Print the current services and actions and exit (no prompts)')
239
+ .addOption(jsonOption('Output machine-readable JSON (implies --list)'))
11
240
  .option('--dev', 'Use development environment', false)
12
241
  .action(async (agentId, options) => {
13
242
  try {
243
+ const listOnly = !!options.list || isJsonMode();
244
+ if (!listOnly) {
245
+ requireInteractive('The tools menu', `List tools without prompts: plazbot agent tools ${agentId} --list --json`);
246
+ }
14
247
  const credentials = await getStoredCredentials();
15
248
  const agent = new Agent({
16
249
  workspaceId: credentials.workspace,
@@ -20,136 +253,136 @@ export const toolsCommand = new Command('tools')
20
253
  });
21
254
  const spinner = createSpinner('Loading agent...');
22
255
  spinner.start();
23
- const _res = await agent.getAgentById({ id: agentId });
24
- const agentData = _res.agent || _res.data || _res;
25
- spinner.stop();
26
- console.log(section('Tool Calling - ' + (agentData.name || agentId)));
27
- console.log(kvPair('Status', agentData.useToolCalling ? theme.success('Enabled') : theme.error('Disabled')));
28
- // Servicios
29
- const services = agentData.services || [];
30
- if (services.length > 0) {
31
- console.log(section('Services (API calls)'));
32
- const rows = services.map((s) => [
33
- s.intent,
34
- s.method || 'POST',
35
- s.endpoint ? s.endpoint.substring(0, 40) + '...' : 'N/A',
36
- statusBadge(s.enabled !== false),
37
- ]);
38
- console.log(createTable(['Intent', 'Method', 'Endpoint', 'Status'], rows));
39
- }
40
- else {
41
- console.log(theme.muted('\n No services configured'));
256
+ let agentData;
257
+ try {
258
+ const _res = await agent.getAgentById({ id: agentId });
259
+ agentData = _res.agent || _res.data || _res;
260
+ spinner.stop();
42
261
  }
43
- // Acciones
44
- const actions = agentData.actions || [];
45
- if (actions.length > 0) {
46
- console.log(section('Actions'));
47
- const rows = actions.map((a) => [
48
- a.intent,
49
- (a.action || []).map((ac) => ac.type).join(', '),
50
- statusBadge(a.enabled !== false),
51
- ]);
52
- console.log(createTable(['Intent', 'Type', 'Status'], rows));
262
+ catch (loadErr) {
263
+ spinner.stop();
264
+ throw new CliError(describeAgentLoadError(loadErr, agentId, credentials, { dev: options.dev }), 'agent_not_found');
53
265
  }
54
- else {
55
- console.log(theme.muted('\n No actions configured'));
56
- }
57
- // Menu de opciones
58
- console.log();
59
- const { action } = await inquirer.prompt([{
60
- type: 'list',
61
- name: 'action',
62
- message: 'What would you like to do?',
63
- choices: [
64
- { name: 'Add service (API call)', value: 'add-service' },
65
- { name: 'Add action', value: 'add-action' },
66
- { name: agentData.useToolCalling ? 'Disable Tool Calling' : 'Enable Tool Calling', value: 'toggle' },
67
- { name: 'Exit', value: 'exit' },
68
- ],
69
- }]);
70
- if (action === 'exit')
71
- return;
72
- if (action === 'toggle') {
73
- const toggleSpinner = createSpinner('Updating...');
74
- toggleSpinner.start();
75
- const { id: _i1, _id: _o1, ...updatedConfig } = { ...agentData, useToolCalling: !agentData.useToolCalling };
76
- await agent.updateAgent(agentId, updatedConfig);
77
- toggleSpinner.succeed(`Tool Calling ${!agentData.useToolCalling ? 'enabled' : 'disabled'}`);
266
+ if (listOnly) {
267
+ if (isJsonMode())
268
+ printJson(toolsAsJson(agentId, agentData));
269
+ else
270
+ printTools(agentId, agentData);
78
271
  return;
79
272
  }
80
- if (action === 'add-service') {
81
- const svc = await inquirer.prompt([
82
- { type: 'input', name: 'intent', message: 'Service name/intent:', validate: (v) => v.length > 0 || 'Required' },
83
- { type: 'input', name: 'reference', message: 'Keywords:', default: '' },
84
- { type: 'list', name: 'method', message: 'HTTP method:', choices: ['GET', 'POST'] },
85
- { type: 'input', name: 'endpoint', message: 'Endpoint URL:', validate: (v) => v.length > 0 || 'Required' },
86
- { type: 'input', name: 'responseMessage', message: 'Response message:', default: '' },
87
- ]);
88
- const newService = {
89
- intent: svc.intent,
90
- reference: svc.reference,
91
- enabled: true,
92
- method: svc.method,
93
- endpoint: svc.endpoint,
94
- requiredFields: [],
95
- headers: {},
96
- bodyTemplate: {},
97
- responseMapping: {},
98
- responseMessage: svc.responseMessage,
99
- responseConditions: [],
100
- };
101
- const updatedServices = [...services, newService];
102
- const { id: _i2, _id: _o2, ...updatedConfig } = { ...agentData, services: updatedServices, useToolCalling: true };
103
- const updateSpinner = createSpinner('Saving service...');
104
- updateSpinner.start();
105
- await agent.updateAgent(agentId, updatedConfig);
106
- updateSpinner.succeed(`Service "${svc.intent}" added`);
107
- }
108
- if (action === 'add-action') {
109
- const act = await inquirer.prompt([
110
- { type: 'input', name: 'intent', message: 'Action name/intent:', validate: (v) => v.length > 0 || 'Required' },
111
- { type: 'input', name: 'reference', message: 'Keywords:', default: '' },
112
- {
113
- type: 'list', name: 'actionType', message: 'Action type:',
273
+ const save = async (patch, text, done) => {
274
+ const s = createSpinner(text);
275
+ s.start();
276
+ const { id: _i, _id: _o, ...payload } = { ...agentData, ...patch };
277
+ await agent.updateAgent(agentId, payload);
278
+ agentData = { ...agentData, ...patch };
279
+ s.succeed(done);
280
+ };
281
+ // Adding a tool while Tool Calling is off: ask, never enable silently.
282
+ const resolveToolCalling = async () => {
283
+ if (agentData.useToolCalling)
284
+ return true;
285
+ const { enable } = await inquirer.prompt([{
286
+ type: 'confirm', name: 'enable', default: true,
287
+ message: 'Tool Calling is disabled for this agent. Enable it so the agent can use this tool?',
288
+ }]);
289
+ return enable;
290
+ };
291
+ printTools(agentId, agentData);
292
+ while (true) {
293
+ const services = agentData.services || [];
294
+ const actions = agentData.actions || [];
295
+ const { action } = await inquirer.prompt([{
296
+ type: 'list',
297
+ name: 'action',
298
+ message: 'What would you like to do?',
114
299
  choices: [
115
- { name: 'Schedule event', value: 'action.event.add' },
116
- { name: 'Update event (reschedule)', value: 'action.event.update' },
117
- { name: 'List events', value: 'action.event.list' },
118
- { name: 'Delete event (cancel)', value: 'action.event.delete' },
119
- { name: 'Add tag', value: 'action.tag' },
120
- { name: 'Change stage', value: 'action.stage' },
121
- { name: 'Hand off to human agent', value: 'action.agentShutDown' },
122
- { name: 'Mark as resolved', value: 'action.solved' },
123
- { name: 'Assign agent', value: 'action.asign' },
124
- { name: 'Segmentation', value: 'action.segmentation' },
300
+ { name: 'List services and actions', value: 'list' },
301
+ { name: 'Add service (API call)', value: 'add-service' },
302
+ { name: 'Add action', value: 'add-action' },
303
+ { name: 'Remove service', value: 'remove-service', disabled: services.length === 0 ? 'none configured' : false },
304
+ { name: 'Remove action', value: 'remove-action', disabled: actions.length === 0 ? 'none configured' : false },
305
+ { name: agentData.useToolCalling ? 'Disable Tool Calling' : 'Enable Tool Calling', value: 'toggle' },
306
+ { name: 'Exit', value: 'exit' },
125
307
  ],
126
- },
127
- { type: 'input', name: 'actionValue', message: 'Value (optional):', default: '' },
128
- { type: 'input', name: 'responseMessage', message: 'Response message:', default: '' },
129
- ]);
130
- const newAction = {
131
- intent: act.intent,
132
- reference: act.reference,
133
- enabled: true,
134
- requiredFields: [],
135
- responseMessage: act.responseMessage,
136
- action: [{ type: act.actionType, value: act.actionValue }],
137
- };
138
- const updatedActions = [...actions, newAction];
139
- const { id: _i3, _id: _o3, ...updatedConfig } = { ...agentData, actions: updatedActions, useToolCalling: true };
140
- const updateSpinner = createSpinner('Saving action...');
141
- updateSpinner.start();
142
- await agent.updateAgent(agentId, updatedConfig);
143
- updateSpinner.succeed(`Action "${act.intent}" added`);
308
+ }]);
309
+ if (action === 'exit')
310
+ return;
311
+ if (action === 'list') {
312
+ printTools(agentId, agentData);
313
+ continue;
314
+ }
315
+ if (action === 'toggle') {
316
+ const next = !agentData.useToolCalling;
317
+ await save({ useToolCalling: next }, 'Updating Tool Calling...', `Tool Calling ${next ? 'enabled' : 'disabled'}`);
318
+ continue;
319
+ }
320
+ if (action === 'add-service' || action === 'add-action') {
321
+ const isService = action === 'add-service';
322
+ const item = isService ? await promptService() : await promptAction();
323
+ const list = isService ? services : actions;
324
+ if (list.some((x) => x.intent === item.intent)) {
325
+ logger.warning(`A ${isService ? 'service' : 'action'} named "${item.intent}" already exists. Nothing was saved.`);
326
+ continue;
327
+ }
328
+ const enableTc = await resolveToolCalling();
329
+ const patch = isService
330
+ ? { services: [...services, item] }
331
+ : { actions: [...actions, item] };
332
+ const turningOn = enableTc && !agentData.useToolCalling;
333
+ if (turningOn)
334
+ patch.useToolCalling = true;
335
+ await save(patch, `Saving ${isService ? 'service' : 'action'}...`, `${isService ? 'Service' : 'Action'} "${item.intent}" added`);
336
+ if (turningOn)
337
+ logger.success('Tool Calling was enabled for this agent.');
338
+ if (!agentData.useToolCalling)
339
+ logger.warning('Tool Calling is still disabled: the agent will not use this tool until you enable it.');
340
+ continue;
341
+ }
342
+ if (action === 'remove-service' || action === 'remove-action') {
343
+ const isService = action === 'remove-service';
344
+ const list = isService ? services : actions;
345
+ const { selected } = await inquirer.prompt([{
346
+ type: 'checkbox',
347
+ name: 'selected',
348
+ message: `Select the ${isService ? 'services' : 'actions'} to remove:`,
349
+ choices: list.map((x, i) => ({
350
+ name: isService
351
+ ? `${x.intent} ${theme.muted(`${x.method || 'POST'} ${x.endpoint || ''}`)}`
352
+ : `${x.intent} ${theme.muted((x.action || []).map((ac) => actionLabel(ac.type)).join(', '))}`,
353
+ value: i,
354
+ })),
355
+ }]);
356
+ if (!selected.length) {
357
+ logger.dim('Nothing selected.');
358
+ continue;
359
+ }
360
+ const names = selected.map((i) => list[i].intent).join(', ');
361
+ const ok = await confirmAction(`Remove ${selected.length} ${isService ? 'service(s)' : 'action(s)'}: ${names}?`, {});
362
+ if (!ok) {
363
+ logger.dim('Aborted.');
364
+ continue;
365
+ }
366
+ const remaining = list.filter((_, i) => !selected.includes(i));
367
+ await save(isService ? { services: remaining } : { actions: remaining }, 'Removing...', `Removed ${selected.length} ${isService ? 'service(s)' : 'action(s)'}`);
368
+ continue;
369
+ }
144
370
  }
145
371
  }
146
372
  catch (error) {
147
- const message = error instanceof Error ? error.message : 'Unknown error';
148
- logger.error(message);
149
- process.exit(1);
373
+ fail(error);
150
374
  }
151
375
  });
152
376
  addExamples(toolsCommand, [
153
- { description: 'View services/actions and add new ones interactively',
154
- command: 'plazbot agent tools agt_AbcDef123' },
377
+ { description: 'Open the interactive menu to list, add or remove tools',
378
+ command: 'plazbot agent tools age_AbcDef123' },
379
+ { description: 'Print the configured services and actions',
380
+ command: 'plazbot agent tools age_AbcDef123 --list' },
381
+ { description: 'Export services and actions as JSON (secret headers masked)',
382
+ command: 'plazbot agent tools age_AbcDef123 --list --json' },
383
+ ]);
384
+ addNotes(toolsCommand, [
385
+ 'Services support GET, POST, PUT, PATCH and DELETE, with optional headers and a JSON body template (POST, PUT, PATCH).',
386
+ 'Adding a tool while Tool Calling is disabled asks before enabling it.',
387
+ 'For bulk edits, export the agent, edit services/actions in the JSON and run plazbot agent update.',
155
388
  ]);
@@ -1,53 +1,94 @@
1
1
  import { Command } from 'commander';
2
- import { Agent } from 'plazbot';
3
2
  import { getStoredCredentials } from '../../utils/credentials.js';
4
3
  import { logger } from '../../utils/logger.js';
5
- import { addExamples } from '../../utils/help.js';
4
+ import { addExamples, addNotes } from '../../utils/help.js';
5
+ import { createApiClient } from '../../utils/api.js';
6
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
7
+ import { patchAgent } from '../../utils/agent-patch.js';
6
8
  import fs from 'fs/promises';
7
9
  export const updateCommand = new Command('update')
8
- .description('Update an existing agent in Plazbot')
10
+ .description('Update an existing agent: only the fields present in the JSON file are changed')
9
11
  .argument('<agentId>', 'Agent ID to update')
10
- .argument('<configPath>', 'Path to the agent JSON configuration file')
12
+ .argument('<configPath>', 'Path to a JSON file with the fields to change')
13
+ .option('--replace', 'Replace the whole agent with the file (fields not in the file are removed)', false)
14
+ .addOption(jsonOption('Print the result as JSON ({"id","updated","fields"})'))
11
15
  .option('--dev', 'Use development environment', false)
12
16
  .action(async (agentId, configPath, options) => {
13
17
  try {
14
- // Obtener credenciales guardadas
15
18
  const credentials = await getStoredCredentials();
16
19
  // Leer archivo de configuración del agente
17
- let agentConfig;
20
+ let fileContent;
18
21
  try {
19
- const fileContent = await fs.readFile(configPath, 'utf-8');
20
- agentConfig = JSON.parse(fileContent);
22
+ fileContent = await fs.readFile(configPath, 'utf-8');
21
23
  }
22
24
  catch (error) {
23
25
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
24
- throw new Error(`Error reading configuration file: ${errorMessage}`);
25
- }
26
- // Crear instancia del agente con las credenciales guardadas
27
- const agent = new Agent({
28
- workspaceId: credentials.workspace,
29
- apiKey: credentials.apiKey,
30
- zone: credentials.zone,
31
- ...(options.dev && { customUrl: "http://localhost:5090" })
32
- });
33
- logger.dim('Updating agent...');
34
- // Actualizar el agente
35
- const result = await agent.updateAgent(agentId, agentConfig);
26
+ throw new CliError(`Could not read the config file: ${errorMessage}`, 'file_not_readable');
27
+ }
28
+ let fileConfig;
29
+ try {
30
+ fileConfig = JSON.parse(fileContent);
31
+ }
32
+ catch (error) {
33
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
34
+ throw new CliError(`The config file is not valid JSON: ${errorMessage}`, 'invalid_json');
35
+ }
36
+ if (!fileConfig || typeof fileConfig !== 'object' || Array.isArray(fileConfig)) {
37
+ throw new CliError('The config file must contain a JSON object.', 'invalid_config');
38
+ }
39
+ const fields = Object.keys(fileConfig);
40
+ if (fields.length === 0) {
41
+ throw new CliError('The config file has no fields to update.', 'empty_config', 'Add the fields to change, e.g. {"question": "Hi!"}.');
42
+ }
43
+ const target = { apiKey: credentials.apiKey, workspace: credentials.workspace, zone: credentials.zone, dev: options.dev };
44
+ if (options.replace) {
45
+ // Reemplazo completo: el archivo se envia tal cual a /api/agent/update
46
+ logger.dim('Replacing agent...');
47
+ const api = createApiClient(target);
48
+ const { data, status } = await api.post('/api/agent/update', fileConfig, {
49
+ headers: { 'x-agent-id': agentId },
50
+ validateStatus: () => true,
51
+ });
52
+ if (!data?.success) {
53
+ // Errores de schema que devuelve la API (uno por campo)
54
+ const errors = (Array.isArray(data?.errors) ? data.errors : [])
55
+ .map((err) => (typeof err === 'string' ? err : JSON.stringify(err)));
56
+ const base = data?.message || `Agent update failed${status ? ` (HTTP ${status})` : ''}.`;
57
+ const message = errors.length
58
+ ? `${base.replace(/\.?$/, ':')}\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`
59
+ : base;
60
+ throw new CliError(message, 'update_failed', `Check the file with: plazbot agent validate ${configPath}`);
61
+ }
62
+ }
63
+ else {
64
+ // Solo los campos del archivo (POST /api/agent/patch)
65
+ logger.dim(`Updating fields: ${fields.join(', ')}`);
66
+ await patchAgent(target, agentId, fileConfig);
67
+ }
68
+ if (isJsonMode()) {
69
+ printJson({ id: agentId, updated: true, fields, replaced: Boolean(options.replace) });
70
+ return;
71
+ }
36
72
  logger.success('Agent updated successfully');
37
- logger.info(`Message: ${result.message}`);
38
73
  if (options.dev) {
39
74
  logger.warning('Environment: development');
40
75
  }
41
76
  }
42
77
  catch (error) {
43
- const message = error instanceof Error ? error.message : 'Unknown error while updating the agent';
44
- logger.error(message);
45
- process.exit(1);
78
+ fail(error);
46
79
  }
47
80
  });
48
81
  addExamples(updateCommand, [
49
- { description: 'Push a new configuration to the agent',
50
- command: 'plazbot agent update agt_AbcDef123 ./agent.json' },
51
- { description: 'Tip: export → edit → update workflow',
52
- command: 'plazbot agent export agt_AbcDef123 -o agent.json && plazbot agent update agt_AbcDef123 ./agent.json' },
82
+ { description: 'Change only the fields in the file (e.g. {"question": "Hi!"})',
83
+ command: 'plazbot agent update age_AbcDef123 ./changes.json' },
84
+ { description: 'Export, edit and update workflow',
85
+ command: 'plazbot agent export age_AbcDef123 -o agent.json && plazbot agent update age_AbcDef123 ./agent.json' },
86
+ { description: 'Replace the whole agent with the file',
87
+ command: 'plazbot agent update age_AbcDef123 ./agent.json --replace' },
88
+ { description: 'Update and get a JSON result (scripts)',
89
+ command: 'plazbot agent update age_AbcDef123 ./changes.json --json' },
90
+ ]);
91
+ addNotes(updateCommand, [
92
+ 'Without --replace, only the fields in the file change; each top-level field replaces the stored one (e.g. "services" replaces the whole list).',
93
+ 'With --replace, fields missing from the file are removed from the agent.',
53
94
  ]);