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,16 +1,21 @@
1
- import { Command } from 'commander';
1
+ import { Command, Option } from 'commander';
2
2
  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
7
  import { describeAgentLoadError } from '../../utils/agent-errors.js';
8
8
  import { createSpinner, theme, section, kvPair } from '../../utils/ui.js';
9
- // Catalogo de modelos sincronizado con el front (AIModelSection.tsx).
10
- // Si se actualiza el dropdown del front, actualizar aqui tambien.
11
- const MODELS = {
9
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
10
+ import { requireInteractive } from '../../utils/confirm.js';
11
+ export const AI_PROVIDERS = [
12
+ { value: 'openai', label: 'OpenAI' },
13
+ { value: 'claude', label: 'Anthropic (Claude)' },
14
+ { value: 'gemini', label: 'Google (Gemini)' },
15
+ ];
16
+ export const AI_MODELS = {
12
17
  openai: [
13
- // GPT-5.4 (flagship actual)
18
+ // GPT-5.4 (current flagship)
14
19
  'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano',
15
20
  // GPT-5.2
16
21
  'gpt-5.2',
@@ -22,7 +27,7 @@ const MODELS = {
22
27
  'gpt-4.1', 'gpt-4.1-mini', 'gpt-4.1-nano',
23
28
  // GPT-4o
24
29
  'gpt-4o', 'gpt-4o-mini',
25
- // Razonamiento (o-series)
30
+ // Reasoning (o-series)
26
31
  'o4-mini', 'o3', 'o3-mini', 'o1-2024-12-17',
27
32
  // Legacy
28
33
  'gpt-4-turbo', 'gpt-3.5-turbo',
@@ -38,7 +43,7 @@ const MODELS = {
38
43
  'claude-opus-4-1-20250805', 'claude-opus-4-20250514', 'claude-sonnet-4-20250514',
39
44
  ],
40
45
  gemini: [
41
- // Gemini 3.1 (flagship actual)
46
+ // Gemini 3.1 (current flagship)
42
47
  'gemini-3.1-pro-preview', 'gemini-3.1-flash-lite-preview',
43
48
  // Gemini 2.5
44
49
  'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite',
@@ -46,17 +51,135 @@ const MODELS = {
46
51
  'gemini-2.0-flash',
47
52
  ],
48
53
  };
49
- const DEFAULT_MODEL = {
54
+ export const DEFAULT_AI_MODEL = {
50
55
  openai: 'gpt-4o',
51
56
  claude: 'claude-sonnet-4-6',
52
57
  gemini: 'gemini-2.5-pro',
53
58
  };
59
+ export const DEFAULT_TEMPERATURE = 0.7;
60
+ export const DEFAULT_MAX_TOKENS = 4096;
61
+ export const MAX_TOKENS_LIMIT = 128000;
62
+ const PROVIDER_ALIASES = {
63
+ openai: 'openai',
64
+ gpt: 'openai',
65
+ claude: 'claude',
66
+ anthropic: 'claude',
67
+ gemini: 'gemini',
68
+ google: 'gemini',
69
+ };
70
+ /** Maps user input (openai, anthropic, claude, google, gemini) to the API provider value. */
71
+ export function normalizeProvider(value) {
72
+ const provider = PROVIDER_ALIASES[String(value || '').trim().toLowerCase()];
73
+ if (!provider) {
74
+ throw new CliError(`Unknown AI provider "${value}".`, 'invalid_provider', 'Use one of: openai, anthropic (claude), google (gemini).');
75
+ }
76
+ return provider;
77
+ }
78
+ /** Validates a temperature (0-2). */
79
+ export function parseTemperature(value) {
80
+ const raw = String(value ?? '').trim();
81
+ const n = Number(raw);
82
+ if (raw === '' || !Number.isFinite(n) || n < 0 || n > 2) {
83
+ throw new CliError(`Invalid temperature "${value}".`, 'invalid_temperature', 'Use a number between 0 and 2 (e.g. 0.7).');
84
+ }
85
+ return n;
86
+ }
87
+ /** Validates max tokens (integer, 1-128000). */
88
+ export function parseMaxTokens(value) {
89
+ const raw = String(value ?? '').trim();
90
+ const n = Number(raw);
91
+ if (raw === '' || !Number.isInteger(n) || n < 1 || n > MAX_TOKENS_LIMIT) {
92
+ throw new CliError(`Invalid max tokens "${value}".`, 'invalid_max_tokens', `Use a whole number between 1 and ${MAX_TOKENS_LIMIT} (e.g. 4096).`);
93
+ }
94
+ return n;
95
+ }
96
+ /** Masks a secret so it is never printed in full: "sk-p…a1B2". */
97
+ export function maskToken(token) {
98
+ if (!token)
99
+ return '';
100
+ const t = String(token);
101
+ if (t.length <= 10)
102
+ return '••••';
103
+ return `${t.slice(0, 4)}…${t.slice(-4)}`;
104
+ }
105
+ /** Inquirer validator built from a parser that throws CliError. */
106
+ export function validateWith(parser) {
107
+ return (v) => {
108
+ try {
109
+ parser(v);
110
+ return true;
111
+ }
112
+ catch (e) {
113
+ return e instanceof CliError ? `${e.message} ${e.hint || ''}`.trim() : 'Invalid value';
114
+ }
115
+ };
116
+ }
117
+ function warnIfUnknownModel(provider, model) {
118
+ if (!AI_MODELS[provider].includes(model)) {
119
+ logger.warning(`Model "${model}" is not in the known ${provider} list. It will be saved as is; make sure the provider accepts it.`);
120
+ }
121
+ }
122
+ function publicProviders(providers) {
123
+ return (providers || []).map((p) => ({
124
+ provider: p.provider,
125
+ model: p.model,
126
+ apiToken: maskToken(p.apiToken),
127
+ temperature: p.temperature,
128
+ maxTokens: p.maxTokens,
129
+ isDefault: !!p.isDefault,
130
+ }));
131
+ }
132
+ function printCurrentConfig(agentData, agentId) {
133
+ console.log(section('AI configuration - ' + (agentData.name || agentId)));
134
+ const providers = agentData.aiProviders || [];
135
+ if (providers.length > 0 && agentData.customAIConfig) {
136
+ providers.forEach((p) => {
137
+ console.log(kvPair(' Provider', p.provider));
138
+ console.log(kvPair(' Model', p.model));
139
+ console.log(kvPair(' API token', p.apiToken ? maskToken(p.apiToken) : theme.warning('not set')));
140
+ console.log(kvPair(' Temperature', String(p.temperature ?? DEFAULT_TEMPERATURE)));
141
+ console.log(kvPair(' Max tokens', String(p.maxTokens ?? '-')));
142
+ console.log(kvPair(' Default', p.isDefault ? 'Yes' : 'No'));
143
+ console.log();
144
+ });
145
+ }
146
+ else {
147
+ console.log(theme.muted('\n Using the platform default AI (Plazbot)\n'));
148
+ }
149
+ }
150
+ function stripIds(data) {
151
+ const { id: _id1, _id: _id2, ...rest } = data;
152
+ return rest;
153
+ }
54
154
  export const aiConfigCommand = new Command('ai-config')
55
- .description('Configure the agent AI provider')
56
- .argument('<agentId>', 'Agent ID')
155
+ .description("Show or change the agent's AI provider, model and API token")
156
+ .argument('<agentId>', 'Agent ID (e.g. age_AbcDef123)')
157
+ .option('--show', 'Print the current AI configuration and exit (tokens are masked)')
158
+ .addOption(new Option('--provider <provider>', 'AI provider: openai, anthropic (claude) or google (gemini)'))
159
+ .option('--model <id>', 'Model ID (default depends on the provider, e.g. gpt-4o, claude-sonnet-4-6)')
160
+ .option('--api-token <token>', "Provider API key (kept from the current config when the provider doesn't change)")
161
+ .option('--temperature <0-2>', `Sampling temperature between 0 and 2 (default: ${DEFAULT_TEMPERATURE})`)
162
+ .option('--max-tokens <n>', `Maximum tokens per response, 1-${MAX_TOKENS_LIMIT} (default: ${DEFAULT_MAX_TOKENS})`)
163
+ .option('--default', 'Drop the custom provider and use the platform default AI')
164
+ .addOption(jsonOption())
57
165
  .option('--dev', 'Use development environment', false)
58
166
  .action(async (agentId, options) => {
59
167
  try {
168
+ const hasSetFlags = !!(options.provider || options.model || options.apiToken || options.temperature !== undefined || options.maxTokens !== undefined);
169
+ if (options.default && hasSetFlags) {
170
+ throw new CliError('--default cannot be combined with --provider, --model, --api-token, --temperature or --max-tokens.', 'conflicting_flags');
171
+ }
172
+ if (options.show && (hasSetFlags || options.default)) {
173
+ throw new CliError('--show cannot be combined with flags that change the configuration.', 'conflicting_flags');
174
+ }
175
+ // Validate flag values before any API call
176
+ const flagProvider = options.provider ? normalizeProvider(options.provider) : undefined;
177
+ const flagTemperature = options.temperature !== undefined ? parseTemperature(options.temperature) : undefined;
178
+ const flagMaxTokens = options.maxTokens !== undefined ? parseMaxTokens(options.maxTokens) : undefined;
179
+ const interactive = !options.show && !options.default && !hasSetFlags;
180
+ if (interactive) {
181
+ requireInteractive('The AI configuration menu', `Pass flags instead, e.g. plazbot agent ai-config ${agentId} --provider openai --model gpt-4o --api-token sk-..., or use --show / --default.`);
182
+ }
60
183
  const credentials = await getStoredCredentials();
61
184
  const agent = new Agent({
62
185
  workspaceId: credentials.workspace,
@@ -74,79 +197,157 @@ export const aiConfigCommand = new Command('ai-config')
74
197
  }
75
198
  catch (loadErr) {
76
199
  spinner.stop();
77
- logger.error(describeAgentLoadError(loadErr, agentId, credentials, { dev: options.dev }));
78
- process.exit(1);
200
+ throw new CliError(describeAgentLoadError(loadErr, agentId, credentials, { dev: options.dev }), 'agent_not_found');
79
201
  }
80
- console.log(section('AI Configuration - ' + (agentData.name || agentId)));
81
202
  const currentProviders = agentData.aiProviders || [];
82
- if (currentProviders.length > 0 && agentData.customAIConfig) {
83
- console.log(theme.bold('\n Current configuration:'));
84
- currentProviders.forEach((p) => {
85
- console.log(kvPair(' Provider', p.provider));
86
- console.log(kvPair(' Model', p.model));
87
- console.log(kvPair(' Temperature', String(p.temperature)));
88
- console.log(kvPair(' Max Tokens', String(p.maxTokens)));
89
- console.log(kvPair(' Default', p.isDefault ? 'Yes' : 'No'));
90
- });
203
+ const current = agentData.customAIConfig
204
+ ? (currentProviders.find((p) => p.isDefault) || currentProviders[0])
205
+ : undefined;
206
+ const saveDefault = async () => {
207
+ const s = createSpinner('Switching to the platform default AI...');
208
+ s.start();
209
+ await agent.updateAgent(agentId, stripIds({ ...agentData, customAIConfig: false, aiProviders: [] }));
210
+ s.succeed('Using the platform default AI (Plazbot)');
211
+ if (isJsonMode())
212
+ printJson({ id: agentId, provider: 'default', model: null, updated: true });
213
+ };
214
+ const saveProvider = async (p) => {
215
+ const s = createSpinner('Saving AI configuration...');
216
+ s.start();
217
+ await agent.updateAgent(agentId, stripIds({
218
+ ...agentData,
219
+ customAIConfig: true,
220
+ aiProviders: [{ ...p, isDefault: true }],
221
+ }));
222
+ s.succeed(`AI provider set: ${p.provider} / ${p.model} (token ${maskToken(p.apiToken)})`);
223
+ if (isJsonMode())
224
+ printJson({ id: agentId, provider: p.provider, model: p.model, updated: true });
225
+ };
226
+ // --show
227
+ if (options.show) {
228
+ if (isJsonMode()) {
229
+ printJson({
230
+ id: agentId,
231
+ name: agentData.name,
232
+ customAIConfig: !!agentData.customAIConfig,
233
+ providers: agentData.customAIConfig ? publicProviders(currentProviders) : [],
234
+ });
235
+ }
236
+ else {
237
+ printCurrentConfig(agentData, agentId);
238
+ }
239
+ return;
91
240
  }
92
- else {
93
- console.log(theme.muted('\n Using default AI configuration (Plazbot)'));
241
+ // --default
242
+ if (options.default) {
243
+ await saveDefault();
244
+ return;
245
+ }
246
+ // Non-interactive set via flags
247
+ if (hasSetFlags) {
248
+ const provider = flagProvider || current?.provider;
249
+ if (!provider) {
250
+ throw new CliError('This agent uses the platform default AI, so --provider is required.', 'missing_provider', 'Pass --provider openai|anthropic|google and --api-token <token>.');
251
+ }
252
+ const sameProvider = !!current && current.provider === provider;
253
+ const apiToken = options.apiToken || (sameProvider ? current.apiToken : '');
254
+ if (!apiToken) {
255
+ throw new CliError(`An API token is required for ${provider}.`, 'missing_api_token', 'Pass it with --api-token <token>.');
256
+ }
257
+ const model = options.model || (sameProvider && current.model) || DEFAULT_AI_MODEL[provider];
258
+ warnIfUnknownModel(provider, model);
259
+ await saveProvider({
260
+ provider,
261
+ model,
262
+ apiToken,
263
+ temperature: flagTemperature ?? (sameProvider ? current.temperature : undefined) ?? DEFAULT_TEMPERATURE,
264
+ maxTokens: flagMaxTokens ?? (sameProvider ? current.maxTokens : undefined) ?? DEFAULT_MAX_TOKENS,
265
+ });
266
+ return;
94
267
  }
268
+ // Interactive
269
+ printCurrentConfig(agentData, agentId);
95
270
  const { action } = await inquirer.prompt([{
96
271
  type: 'list',
97
272
  name: 'action',
98
273
  message: 'What would you like to do?',
99
274
  choices: [
100
- { name: 'Configure new provider', value: 'set' },
101
- { name: 'Use default configuration', value: 'reset' },
275
+ { name: 'Set a custom AI provider', value: 'set' },
276
+ { name: 'Use the platform default AI', value: 'reset' },
102
277
  { name: 'Exit', value: 'exit' },
103
278
  ],
104
279
  }]);
105
280
  if (action === 'exit')
106
281
  return;
107
282
  if (action === 'reset') {
108
- const { id, _id, ...resetConfig } = { ...agentData, customAIConfig: false, aiProviders: [] };
109
- const resetSpinner = createSpinner('Resetting configuration...');
110
- resetSpinner.start();
111
- await agent.updateAgent(agentId, resetConfig);
112
- resetSpinner.succeed('Using default configuration');
283
+ await saveDefault();
113
284
  return;
114
285
  }
115
- // Configurar nuevo proveedor
116
- const ai = await inquirer.prompt([
117
- { type: 'list', name: 'provider', message: 'Provider:', choices: ['openai', 'claude', 'gemini'] },
118
- ]);
119
- const models = MODELS[ai.provider] || MODELS.openai;
120
- const config = await inquirer.prompt([
121
- { type: 'list', name: 'model', message: 'Model:', choices: models, default: DEFAULT_MODEL[ai.provider] },
122
- { type: 'password', name: 'apiToken', message: 'API Token:', mask: '*', validate: (v) => v.length > 0 || 'Required' },
123
- { type: 'number', name: 'temperature', message: 'Temperature (0-2):', default: 0.7 },
124
- { type: 'number', name: 'maxTokens', message: 'Max tokens (1024-16384):', default: 4096 },
286
+ const { provider } = await inquirer.prompt([{
287
+ type: 'list',
288
+ name: 'provider',
289
+ message: 'Provider:',
290
+ choices: AI_PROVIDERS.map(p => ({ name: p.label, value: p.value })),
291
+ default: current?.provider || 'openai',
292
+ }]);
293
+ const providerId = provider;
294
+ const sameProvider = !!current && current.provider === providerId;
295
+ const keepToken = sameProvider && !!current.apiToken;
296
+ const answers = await inquirer.prompt([
297
+ {
298
+ type: 'list',
299
+ name: 'model',
300
+ message: 'Model:',
301
+ choices: AI_MODELS[providerId],
302
+ default: (sameProvider && AI_MODELS[providerId].includes(current.model)) ? current.model : DEFAULT_AI_MODEL[providerId],
303
+ pageSize: 12,
304
+ },
305
+ {
306
+ type: 'password',
307
+ name: 'apiToken',
308
+ message: keepToken ? `API token (leave empty to keep ${maskToken(current.apiToken)}):` : 'API token:',
309
+ mask: '*',
310
+ validate: (v) => (v.trim().length > 0 || keepToken) ? true : 'The API token is required',
311
+ },
312
+ {
313
+ type: 'input',
314
+ name: 'temperature',
315
+ message: 'Temperature (0-2):',
316
+ default: String((sameProvider ? current.temperature : undefined) ?? DEFAULT_TEMPERATURE),
317
+ validate: validateWith(parseTemperature),
318
+ },
319
+ {
320
+ type: 'input',
321
+ name: 'maxTokens',
322
+ message: `Max tokens per response (1-${MAX_TOKENS_LIMIT}):`,
323
+ default: String((sameProvider ? current.maxTokens : undefined) || DEFAULT_MAX_TOKENS),
324
+ validate: validateWith(parseMaxTokens),
325
+ },
125
326
  ]);
126
- const { id: _uid, _id: _oid, ...updatedConfig } = {
127
- ...agentData,
128
- customAIConfig: true,
129
- aiProviders: [{
130
- provider: ai.provider,
131
- model: config.model,
132
- apiToken: config.apiToken,
133
- temperature: config.temperature,
134
- maxTokens: config.maxTokens,
135
- isDefault: true,
136
- }],
137
- };
138
- const updateSpinner = createSpinner('Saving configuration...');
139
- updateSpinner.start();
140
- await agent.updateAgent(agentId, updatedConfig);
141
- updateSpinner.succeed(`Provider configured: ${ai.provider} / ${config.model}`);
327
+ await saveProvider({
328
+ provider: providerId,
329
+ model: answers.model,
330
+ apiToken: answers.apiToken.trim() || current.apiToken,
331
+ temperature: parseTemperature(answers.temperature),
332
+ maxTokens: parseMaxTokens(answers.maxTokens),
333
+ });
142
334
  }
143
335
  catch (error) {
144
- const message = error instanceof Error ? error.message : 'Unknown error';
145
- logger.error(message);
146
- process.exit(1);
336
+ fail(error);
147
337
  }
148
338
  });
149
339
  addExamples(aiConfigCommand, [
150
- { description: 'Pick provider/model/API key for the agent interactively',
151
- command: 'plazbot agent ai-config agt_AbcDef123' },
340
+ { description: 'Pick provider, model and API token interactively',
341
+ command: 'plazbot agent ai-config age_AbcDef123' },
342
+ { description: 'Show the current AI configuration as JSON (tokens masked)',
343
+ command: 'plazbot agent ai-config age_AbcDef123 --show --json' },
344
+ { description: 'Switch to Claude non-interactively',
345
+ command: 'plazbot agent ai-config age_AbcDef123 --provider anthropic --model claude-sonnet-4-6 --api-token sk-ant-xxx --temperature 0.5' },
346
+ { description: 'Go back to the platform default AI',
347
+ command: 'plazbot agent ai-config age_AbcDef123 --default' },
348
+ ]);
349
+ addNotes(aiConfigCommand, [
350
+ 'Changing only --model, --temperature or --max-tokens keeps the current provider and API token.',
351
+ `Known models: openai (${AI_MODELS.openai.length}), claude (${AI_MODELS.claude.length}), gemini (${AI_MODELS.gemini.length}). Unknown model IDs are accepted with a warning.`,
352
+ 'API tokens are never printed in full.',
152
353
  ]);
@@ -2,9 +2,11 @@ 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
6
  import { describeAgentLoadError } from '../../utils/agent-errors.js';
7
7
  import { section } from '../../utils/ui.js';
8
+ import { fail, describeError } from '../../utils/output.js';
9
+ import { requireInteractive } from '../../utils/confirm.js';
8
10
  import crypto from 'crypto';
9
11
  import readline from 'readline';
10
12
  import chalk from 'chalk';
@@ -19,11 +21,12 @@ const COMMANDS_HELP = `
19
21
  export const chatCommand = new Command('chat')
20
22
  .description('Start an interactive chat session with an agent')
21
23
  .argument('<agentId>', 'Agent ID')
22
- .option('-s, --session-id <id>', 'Session ID (optional)')
23
- .option('-m, --multiple-answers', 'Allow multiple answers', false)
24
+ .option('-s, --session-id <id>', 'Reuse a session ID to continue a conversation (default: a new random ID)')
25
+ .option('-m, --multiple-answers', 'Allow the agent to send several answers per turn', false)
24
26
  .option('--dev', 'Use development environment', false)
25
27
  .action(async (agentId, options) => {
26
28
  try {
29
+ requireInteractive('The interactive chat', `For scripts, send one message at a time: plazbot agent message ${agentId} -q "Hello" --json`);
27
30
  const credentials = await getStoredCredentials();
28
31
  const agent = new Agent({
29
32
  workspaceId: credentials.workspace,
@@ -143,8 +146,9 @@ export const chatCommand = new Command('chat')
143
146
  askQuestion();
144
147
  }
145
148
  catch (error) {
146
- const message = error instanceof Error ? error.message : 'Unknown error';
147
- console.log(chalk.hex('#EF5350')(`\n ✖ Error: ${message}\n`));
149
+ // Error de un turno: se informa y la sesion sigue abierta
150
+ const { message, hint } = describeError(error);
151
+ console.log(chalk.hex('#EF5350')(`\n Error: ${message}`) + (hint ? chalk.gray(`\n ${hint}`) : '') + '\n');
148
152
  askQuestion();
149
153
  }
150
154
  });
@@ -152,18 +156,19 @@ export const chatCommand = new Command('chat')
152
156
  askQuestion();
153
157
  }
154
158
  catch (error) {
155
- const message = error instanceof Error ? error.message : 'Unknown error';
156
- logger.error(message);
157
- process.exit(1);
159
+ fail(error);
158
160
  }
159
161
  });
160
162
  addExamples(chatCommand, [
161
163
  { description: 'Open an interactive chat with the agent',
162
- command: 'plazbot agent chat agt_AbcDef123' },
164
+ command: 'plazbot agent chat age_AbcDef123' },
163
165
  { description: 'Reuse a previous session ID to continue the conversation',
164
- command: 'plazbot agent chat agt_AbcDef123 -s sess_AbcDef123' },
166
+ command: 'plazbot agent chat age_AbcDef123 -s sess_AbcDef123' },
165
167
  { description: 'Allow the agent to send multiple answers per turn',
166
- command: 'plazbot agent chat agt_AbcDef123 -m' },
168
+ command: 'plazbot agent chat age_AbcDef123 -m' },
169
+ ]);
170
+ addNotes(chatCommand, [
171
+ 'Needs an interactive terminal. For scripts use: plazbot agent message <agentId> -q "..." --json',
167
172
  ]);
168
173
  function formatResponse(text) {
169
174
  if (!text)
@@ -2,11 +2,13 @@ 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
6
  import { createSpinner } from '../../utils/ui.js';
7
+ import { jsonOption, isJsonMode, printJson, fail, CliError } from '../../utils/output.js';
7
8
  export const copyCommand = new Command('copy')
8
- .description('Clone an existing agent')
9
+ .description('Clone an agent inside the active workspace')
9
10
  .argument('<agentId>', 'Agent ID to clone')
11
+ .addOption(jsonOption('Print the result as JSON ({"sourceId","id","name"})'))
10
12
  .option('--dev', 'Use development environment', false)
11
13
  .action(async (agentId, options) => {
12
14
  try {
@@ -19,21 +21,55 @@ export const copyCommand = new Command('copy')
19
21
  });
20
22
  const spinner = createSpinner('Cloning agent...');
21
23
  spinner.start();
22
- const result = await agent.copyAgent({ id: agentId });
23
- spinner.succeed('Agent cloned successfully');
24
- logger.title('Cloned agent');
25
- if (result?.agentId || result?.id) {
26
- logger.label('New ID', result.agentId || result.id);
24
+ let result;
25
+ try {
26
+ result = await agent.copyAgent({ id: agentId });
27
+ }
28
+ catch (err) {
29
+ spinner.fail('Could not clone the agent');
30
+ throw err;
31
+ }
32
+ if (result?.success === false) {
33
+ spinner.fail('Could not clone the agent');
34
+ throw new CliError(result.message || 'The API rejected the copy.', 'copy_failed');
35
+ }
36
+ const payload = result?.agent || result?.data || result || {};
37
+ const newId = result?.agentId || payload?.agentId || payload?.id;
38
+ let newName = payload?.name;
39
+ // La respuesta puede no traer el nombre: se consulta el agente nuevo (solo informativo)
40
+ if (newId && !newName) {
41
+ try {
42
+ const res = await agent.getAgentById({ id: newId });
43
+ newName = (res?.agent || res?.data || res)?.name;
44
+ }
45
+ catch {
46
+ // Cosmetico: el clon ya se creo
47
+ }
27
48
  }
28
- logger.dim('\nThe cloned agent has the widget disabled by default.');
49
+ spinner.succeed(newName ? `Agent cloned as "${newName}"` : 'Agent cloned');
50
+ if (isJsonMode()) {
51
+ printJson({ sourceId: agentId, id: newId ?? null, name: newName ?? null });
52
+ return;
53
+ }
54
+ logger.title('Cloned agent');
55
+ logger.label('Source ID', agentId);
56
+ logger.label('New ID', newId || '—');
57
+ logger.label('Name', newName || '—');
58
+ console.log();
59
+ logger.dim('The cloned agent has the widget disabled.');
60
+ if (newId)
61
+ logger.dim(`Test it with: plazbot agent chat ${newId}`);
29
62
  }
30
63
  catch (error) {
31
- const message = error instanceof Error ? error.message : 'Unknown error';
32
- logger.error(message);
33
- process.exit(1);
64
+ fail(error);
34
65
  }
35
66
  });
36
67
  addExamples(copyCommand, [
37
- { description: 'Clone an agent (widget stays disabled on the copy)',
38
- command: 'plazbot agent copy agt_AbcDef123' },
68
+ { description: 'Clone an agent (the widget stays disabled on the copy)',
69
+ command: 'plazbot agent copy age_AbcDef123' },
70
+ { description: 'Clone and capture the new ID (scripts)',
71
+ command: "plazbot agent copy age_AbcDef123 --json | jq -r '.id'" },
72
+ ]);
73
+ addNotes(copyCommand, [
74
+ 'To copy an agent to another workspace use: plazbot agent cross-copy',
39
75
  ]);