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.
- package/README.md +67 -111
- package/dist/cli.js +26 -11
- package/dist/commands/agent/ai-config.js +264 -63
- package/dist/commands/agent/chat.js +16 -11
- package/dist/commands/agent/copy.js +49 -13
- package/dist/commands/agent/create.js +62 -17
- package/dist/commands/agent/delete.js +50 -39
- package/dist/commands/agent/enable-widget.js +47 -27
- package/dist/commands/agent/export.js +179 -88
- package/dist/commands/agent/files.js +155 -73
- package/dist/commands/agent/get.js +24 -16
- package/dist/commands/agent/index.js +12 -10
- package/dist/commands/agent/list.js +20 -11
- package/dist/commands/agent/monitor.js +101 -53
- package/dist/commands/agent/on-message.js +55 -32
- package/dist/commands/agent/set.js +230 -82
- package/dist/commands/agent/templates.js +259 -156
- package/dist/commands/agent/tools.js +356 -123
- package/dist/commands/agent/update.js +69 -28
- package/dist/commands/agent/validate.js +72 -40
- package/dist/commands/agent/wizard.js +240 -329
- package/dist/commands/auth/browser-login.js +37 -31
- package/dist/commands/auth/login.js +49 -45
- package/dist/commands/auth/logout.js +20 -11
- package/dist/commands/auth/status.js +36 -12
- package/dist/commands/auth/workspace.js +61 -27
- package/dist/commands/portal/add-agent.js +106 -45
- package/dist/commands/portal/add-link.js +25 -28
- package/dist/commands/portal/clear-links.js +39 -19
- package/dist/commands/portal/create.js +66 -37
- package/dist/commands/portal/delete.js +40 -45
- package/dist/commands/portal/get.js +21 -54
- package/dist/commands/portal/index.js +12 -12
- package/dist/commands/portal/list.js +34 -52
- package/dist/commands/portal/shared.js +92 -0
- package/dist/commands/portal/update.js +100 -70
- package/dist/commands/whatsapp/broadcast.js +169 -77
- package/dist/commands/whatsapp/channels.js +167 -65
- package/dist/commands/whatsapp/chat.js +45 -46
- package/dist/commands/whatsapp/connect-flow.js +16 -15
- package/dist/commands/whatsapp/connect.js +36 -21
- package/dist/commands/whatsapp/delete-webhook.js +42 -25
- package/dist/commands/whatsapp/index.js +15 -16
- package/dist/commands/whatsapp/register-webhook.js +43 -26
- package/dist/commands/whatsapp/send-template.js +62 -61
- package/dist/commands/whatsapp/send.js +46 -33
- package/dist/commands/whatsapp/setup.js +62 -52
- package/dist/commands/whatsapp/shared.js +75 -0
- package/dist/commands/whatsapp/widget.js +82 -45
- package/dist/commands/workers/deploy.js +80 -58
- package/dist/commands/workers/index.js +8 -8
- package/dist/commands/workers/list.js +42 -20
- package/dist/commands/workers/logs.js +139 -36
- package/dist/commands/workers/remove.js +45 -47
- package/dist/commands/workers/secret.js +159 -83
- package/dist/commands/workers/test.js +108 -60
- package/dist/schemas/agent.config.schema.json +159 -8
- package/dist/studio/api/sseClient.js +1 -1
- package/dist/studio/api/studioApi.js +8 -0
- package/dist/studio/components/App.js +12 -0
- package/dist/studio/components/Footer.js +1 -1
- package/dist/studio/index.js +77 -14
- package/dist/studio/runOneShot.js +62 -23
- package/dist/studio/runRepl.js +5 -5
- package/dist/studio/slash/handlers.js +11 -8
- package/dist/studio/state/store.js +1 -1
- package/dist/utils/agent-errors.js +14 -11
- package/dist/utils/agent-patch.js +60 -0
- package/dist/utils/banner.js +9 -5
- package/dist/utils/confirm.js +31 -0
- package/dist/utils/credentials.js +13 -3
- package/dist/utils/logger.js +13 -10
- package/dist/utils/output.js +78 -0
- package/dist/utils/phone.js +22 -0
- package/dist/utils/ui.js +14 -11
- 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('
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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: '
|
|
116
|
-
{ name: '
|
|
117
|
-
{ name: '
|
|
118
|
-
{ name: '
|
|
119
|
-
{ name: '
|
|
120
|
-
{ name: '
|
|
121
|
-
{ name: '
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
logger.error(message);
|
|
149
|
-
process.exit(1);
|
|
373
|
+
fail(error);
|
|
150
374
|
}
|
|
151
375
|
});
|
|
152
376
|
addExamples(toolsCommand, [
|
|
153
|
-
{ description: '
|
|
154
|
-
command: 'plazbot agent tools
|
|
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
|
|
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
|
|
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
|
|
20
|
+
let fileContent;
|
|
18
21
|
try {
|
|
19
|
-
|
|
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
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
44
|
-
logger.error(message);
|
|
45
|
-
process.exit(1);
|
|
78
|
+
fail(error);
|
|
46
79
|
}
|
|
47
80
|
});
|
|
48
81
|
addExamples(updateCommand, [
|
|
49
|
-
{ description: '
|
|
50
|
-
command: 'plazbot agent update
|
|
51
|
-
{ description: '
|
|
52
|
-
command: 'plazbot agent export
|
|
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
|
]);
|