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
@@ -54,15 +54,18 @@ slashRegistry.register({
54
54
  for (const cmd of items) {
55
55
  const usage = cmd.usage ?? `/${cmd.name}`;
56
56
  const aliasPart = cmd.aliases?.length
57
- ? ` _(alias: ${cmd.aliases.map((a) => '/' + a).join(', ')})_`
57
+ ? ` (${cmd.aliases.length > 1 ? 'aliases' : 'alias'}: ${cmd.aliases.map((a) => '`/' + a + '`').join(', ')})`
58
58
  : '';
59
59
  lines.push(`- \`${usage}\`${aliasPart} — ${cmd.description}`);
60
60
  }
61
61
  lines.push('');
62
62
  }
63
63
  lines.push('**Keyboard shortcuts**');
64
- lines.push('- `Esc` — cancel the current stream');
65
- lines.push('- `Ctrl+C` — exit Studio');
64
+ lines.push('- `Enter` — send the message');
65
+ lines.push('- `↑` / `↓` — browse your previous messages');
66
+ lines.push('- `Tab` or `→` — accept the gray suggestion from your history');
67
+ lines.push('- `Esc` — cancel the current response (or a pending channel connection)');
68
+ lines.push('- `Ctrl+C` — exit Studio immediately');
66
69
  lines.push('');
67
70
  lines.push('**Tip:** you can also speak naturally — e.g. _"list my agents"_, _"create a sales agent for WhatsApp"_ or _"connect this agent to WhatsApp Business"_.');
68
71
  useStudioStore.getState().pushSyntheticAssistant(lines.join('\n'));
@@ -72,7 +75,7 @@ slashRegistry.register({
72
75
  slashRegistry.register({
73
76
  name: 'clear',
74
77
  aliases: ['cls'],
75
- description: 'Clear the conversation (keeps the loaded agent and token usage)',
78
+ description: 'Clear the message history and the input history (keeps the loaded agent and token usage)',
76
79
  category: 'session',
77
80
  handler() {
78
81
  useStudioStore.getState().clearMessages();
@@ -171,14 +174,14 @@ slashRegistry.register({
171
174
  slashRegistry.register({
172
175
  name: 'export',
173
176
  aliases: ['download'],
174
- description: 'Export the current agent configuration to a JSON file on disk',
177
+ description: 'Write the loaded agent configuration as JSON to [path] (default: ./<agent-name>.json)',
175
178
  category: 'agent',
176
179
  usage: '/export [path]',
177
180
  async handler(parsed) {
178
181
  const store = useStudioStore.getState();
179
182
  const { agentConfig } = store;
180
183
  if (!agentConfig) {
181
- store.pushSyntheticAssistant('No hay agente cargado. Crea o carga uno antes con `/load <id>`.', { error: true });
184
+ store.pushSyntheticAssistant('No agent configuration loaded. Create one first or use `/load <id>`.', { error: true });
182
185
  return { kind: 'noop' };
183
186
  }
184
187
  const cfg = agentConfig;
@@ -188,11 +191,11 @@ slashRegistry.register({
188
191
  const targetPath = resolve(process.cwd(), expandTilde(targetArg));
189
192
  try {
190
193
  await writeFile(targetPath, JSON.stringify(agentConfig, null, 2), 'utf-8');
191
- store.pushSyntheticAssistant(`Agente exportado a \`${targetPath}\`.\n\nValídalo con: \`plazbot agent validate ${targetPath}\``);
194
+ store.pushSyntheticAssistant(`Agent configuration exported to \`${targetPath}\`.\n\nValidate it with: \`plazbot agent validate ${targetPath}\``);
192
195
  }
193
196
  catch (err) {
194
197
  const message = err instanceof Error ? err.message : String(err);
195
- store.pushSyntheticAssistant(`No pude escribir el archivo: ${message}`, { error: true });
198
+ store.pushSyntheticAssistant(`Could not write the file: ${message}`, { error: true });
196
199
  }
197
200
  return { kind: 'noop' };
198
201
  },
@@ -64,7 +64,7 @@ export const useStudioStore = create((set, get) => ({
64
64
  },
65
65
  failAllRunningSteps(message) {
66
66
  set((s) => ({
67
- steps: s.steps.map((x) => x.status === 'running' ? { ...x, status: 'error', errorMessage: message ?? 'Cancelado' } : x),
67
+ steps: s.steps.map((x) => x.status === 'running' ? { ...x, status: 'error', errorMessage: message ?? 'Cancelled' } : x),
68
68
  }));
69
69
  },
70
70
  setAgentConfig(cfg) {
@@ -9,35 +9,38 @@ export function describeAgentLoadError(error, agentId, credentials, context = {}
9
9
  const status = err?.response?.status;
10
10
  const workspace = context.workspaceOverride || credentials.workspace;
11
11
  const zone = context.zoneOverride || credentials.zone;
12
- const envHint = context.dev ? ' (env: --dev / localhost:5090)' : '';
12
+ const envHint = context.dev ? ' (development environment, localhost:5090)' : '';
13
13
  if (status === 404) {
14
14
  return [
15
15
  `Agent "${agentId}" was not found in workspace ${workspace} (zone ${zone})${envHint}.`,
16
- chalk.gray(' Verify the ID and active workspace:'),
17
- chalk.gray(' plazbot status'),
16
+ chalk.gray(' Check the ID and the active workspace:'),
17
+ chalk.gray(' plazbot whoami'),
18
18
  chalk.gray(' plazbot agent list'),
19
- chalk.gray(' If the agent lives in a different zone, re-init with the right one:'),
20
- chalk.gray(' plazbot login'),
19
+ chalk.gray(' If the agent is in another workspace, switch to it:'),
20
+ chalk.gray(' plazbot workspace use <workspaceId>'),
21
+ chalk.gray(' If it is in another zone, sign in to that zone:'),
22
+ chalk.gray(' plazbot login --zone EU'),
21
23
  ].join('\n');
22
24
  }
23
25
  if (status === 401) {
24
26
  return [
25
- 'Authentication token rejected (401).',
26
- chalk.gray(' Re-run: plazbot login'),
27
+ 'Your session is invalid or expired (401).',
28
+ chalk.gray(' Sign in again: plazbot login'),
27
29
  ].join('\n');
28
30
  }
29
31
  if (status === 403) {
30
32
  return [
31
- `No permissions to access agent "${agentId}" in workspace ${workspace}.`,
32
- chalk.gray(' Confirm your user has access to that workspace.'),
33
+ `You do not have permission to access agent "${agentId}" in workspace ${workspace}.`,
34
+ chalk.gray(' Check the active workspace: plazbot whoami'),
35
+ chalk.gray(' Switch with: plazbot workspace use <workspaceId>'),
33
36
  ].join('\n');
34
37
  }
35
38
  if (status === 429) {
36
- return 'Rate limit reached. Try again in a few seconds.';
39
+ return 'Too many requests. Try again in a few seconds.';
37
40
  }
38
41
  if (typeof status === 'number' && status >= 500) {
39
42
  return [
40
- `Backend error (HTTP ${status})${envHint}.`,
43
+ `Plazbot API error (HTTP ${status})${envHint}.`,
41
44
  chalk.gray(` ${err.response?.data?.message || err.message || ''}`).trimEnd(),
42
45
  ].join('\n');
43
46
  }
@@ -0,0 +1,60 @@
1
+ // Partial agent updates. POST /api/agent/patch changes only the fields sent (each top-level
2
+ // field replaces the stored one). With an API that doesn't have /patch yet (404 on the route),
3
+ // it falls back to fetching the agent, applying the fields and sending the full document to
4
+ // /api/agent/update, which replaces the whole agent.
5
+ import { Agent } from 'plazbot';
6
+ import { createApiClient } from './api.js';
7
+ import { CliError } from './output.js';
8
+ import { cleanAgentForExport } from '../commands/agent/export.js';
9
+ function toCliError(data, status, agentId) {
10
+ if (status === 401)
11
+ return new CliError(data?.message || 'Your session is invalid or expired.', 'unauthorized', "Run 'plazbot login' to sign in again.");
12
+ if (status === 403)
13
+ return new CliError(data?.message || 'You do not have permission to update this agent.', 'forbidden', "Check the active workspace with 'plazbot whoami'.");
14
+ if (status === 404)
15
+ return new CliError(`Agent ${agentId} was not found in the active workspace.`, 'not_found', "Check the ID with 'plazbot agent list'.");
16
+ const errors = (Array.isArray(data?.errors) ? data.errors : [])
17
+ .map((err) => (typeof err === 'string' ? err : JSON.stringify(err)));
18
+ const base = data?.message || `Agent update failed (HTTP ${status}).`;
19
+ const message = errors.length
20
+ ? `${base.replace(/\.?$/, ':')}\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`
21
+ : base;
22
+ return new CliError(message, 'update_failed', "Check the fields with 'plazbot agent validate <file>'.");
23
+ }
24
+ /**
25
+ * Applies `patch` to the agent. `current` (the stored agent) is only needed for the fallback;
26
+ * pass it when you already have it to avoid an extra request.
27
+ */
28
+ export async function patchAgent(target, agentId, patch, current) {
29
+ const api = createApiClient(target);
30
+ const headers = { 'x-agent-id': agentId };
31
+ const res = await api.post('/api/agent/patch', patch, { headers, validateStatus: () => true });
32
+ const routeMissing = res.status === 404 && typeof res.data?.success !== 'boolean';
33
+ if (!routeMissing) {
34
+ if (!res.data?.success)
35
+ throw toCliError(res.data, res.status, agentId);
36
+ return;
37
+ }
38
+ // API without /patch: full-document update built from the stored agent
39
+ let stored = current;
40
+ if (!stored) {
41
+ const agent = new Agent({
42
+ workspaceId: target.workspace,
43
+ apiKey: target.apiKey,
44
+ zone: target.zone,
45
+ ...(target.dev && { customUrl: 'http://localhost:5090' }),
46
+ });
47
+ const response = await agent.getAgentById({ id: agentId });
48
+ stored = response?.agent || response?.data || response;
49
+ if (!stored || !stored.name)
50
+ throw toCliError(null, 404, agentId);
51
+ }
52
+ const payload = { ...cleanAgentForExport(stored), ...patch };
53
+ for (const [key, value] of Object.entries(patch)) {
54
+ if (value === null)
55
+ delete payload[key];
56
+ }
57
+ const full = await api.post('/api/agent/update', payload, { headers, validateStatus: () => true });
58
+ if (!full.data?.success)
59
+ throw toCliError(full.data, full.status, agentId);
60
+ }
@@ -40,29 +40,33 @@ export async function showBanner() {
40
40
  const rightColumn = [];
41
41
  if (credentials) {
42
42
  rightColumn.push(chalk.bold.hex('#4CAF50')('Connected workspace'));
43
- rightColumn.push(chalk.gray(` ${credentials.workspace || 'N/A'}`));
43
+ rightColumn.push(chalk.gray(` ${credentials.workspaceName || credentials.workspace || 'N/A'}`));
44
+ if (credentials.workspaceName && credentials.workspace)
45
+ rightColumn.push(chalk.gray(` ${credentials.workspace}`));
44
46
  if (credentials.email)
45
47
  rightColumn.push(chalk.gray(` ${credentials.email}`));
46
48
  rightColumn.push(chalk.gray(` Zone: ${credentials.zone || 'N/A'}`));
47
49
  rightColumn.push('');
48
50
  rightColumn.push(chalk.hex('#4CAF50')('─'.repeat(35)));
49
51
  rightColumn.push('');
50
- rightColumn.push(chalk.bold.hex('#4CAF50')('Comandos rápidos'));
52
+ rightColumn.push(chalk.bold.hex('#4CAF50')('Quick commands'));
51
53
  rightColumn.push(chalk.white(' plazbot whatsapp setup'));
52
54
  rightColumn.push(chalk.white(' plazbot whatsapp send-message'));
53
55
  rightColumn.push(chalk.white(' plazbot agent list'));
54
56
  rightColumn.push(chalk.white(' plazbot studio'));
57
+ rightColumn.push(chalk.white(' plazbot --help'));
55
58
  }
56
59
  else {
57
- rightColumn.push(chalk.bold.hex('#FFA726')('Sin sesión'));
60
+ rightColumn.push(chalk.bold.hex('#FFA726')('Not signed in'));
58
61
  rightColumn.push('');
59
- rightColumn.push(chalk.bold.hex('#4CAF50')('Empieza aquí'));
62
+ rightColumn.push(chalk.bold.hex('#4CAF50')('Get started'));
60
63
  rightColumn.push(chalk.white(' plazbot login'));
61
64
  rightColumn.push(chalk.white(' plazbot whatsapp setup'));
65
+ rightColumn.push(chalk.white(' plazbot --help'));
62
66
  rightColumn.push('');
63
67
  rightColumn.push(chalk.hex('#4CAF50')('─'.repeat(35)));
64
68
  rightColumn.push('');
65
- rightColumn.push(chalk.bold.hex('#4CAF50')('Documentación'));
69
+ rightColumn.push(chalk.bold.hex('#4CAF50')('Documentation'));
66
70
  rightColumn.push(chalk.gray(' https://developers.plazbot.com'));
67
71
  }
68
72
  // Imprimir título
@@ -0,0 +1,31 @@
1
+ // Confirmation prompts with a Stripe-style escape hatch for scripts: -y, --yes (alias --confirm).
2
+ import { Option } from 'commander';
3
+ import inquirer from 'inquirer';
4
+ import { CliError, isJsonMode } from './output.js';
5
+ /** Adds `-y, --yes` (plus hidden `--confirm` and legacy `-f, --force`) to a command. */
6
+ export function addYesOption(cmd) {
7
+ return cmd
8
+ .addOption(new Option('-y, --yes', 'Skip the confirmation prompt (for scripts and CI)').default(false))
9
+ .addOption(new Option('--confirm', 'Alias of --yes').hideHelp())
10
+ .addOption(new Option('-f, --force', 'Alias of --yes').hideHelp());
11
+ }
12
+ /**
13
+ * Asks for confirmation unless --yes was passed. Without an interactive terminal
14
+ * (CI, pipes, --json) it fails instead of waiting for input forever.
15
+ * Returns false when the user answers no.
16
+ */
17
+ export async function confirmAction(message, options) {
18
+ if (options.yes || options.confirm || options.force)
19
+ return true;
20
+ if (!process.stdin.isTTY || isJsonMode()) {
21
+ throw new CliError('This action needs confirmation and there is no interactive terminal.', 'confirmation_required', 'Re-run the command with --yes to confirm.');
22
+ }
23
+ const { ok } = await inquirer.prompt([{ type: 'confirm', name: 'ok', message, default: false }]);
24
+ return ok;
25
+ }
26
+ /** Fails when an interactive prompt would be needed but there is no terminal. */
27
+ export function requireInteractive(what, hint) {
28
+ if (!process.stdin.isTTY || isJsonMode()) {
29
+ throw new CliError(`${what} needs an interactive terminal.`, 'interactive_required', hint);
30
+ }
31
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { CliError } from './output.js';
4
5
  const configDir = path.join(os.homedir(), '.plazbot');
5
6
  const configPath = path.join(configDir, 'config.json');
6
7
  /**
@@ -24,12 +25,13 @@ export async function getStoredCredentials() {
24
25
  const configRaw = await fs.readFile(configPath, 'utf-8');
25
26
  const config = JSON.parse(configRaw);
26
27
  if (!config.apiKey || !config.workspace || !config.zone) {
27
- throw new Error('Credenciales incompletas');
28
+ throw new Error('Incomplete credentials');
28
29
  }
29
- return config;
30
+ // Older versions of `plazbot init` stored the zone as typed (e.g. "eu").
31
+ return { ...config, zone: parseZone(config.zone) ?? 'LA' };
30
32
  }
31
33
  catch (error) {
32
- throw new Error("No hay una sesión activa. Ejecuta 'plazbot login' para iniciar sesión.");
34
+ throw new CliError('You are not signed in.', 'not_signed_in', "Run 'plazbot login', or set PLAZBOT_API_KEY and PLAZBOT_WORKSPACE_ID.");
33
35
  }
34
36
  }
35
37
  export async function saveCredentials(credentials) {
@@ -41,6 +43,14 @@ export async function saveCredentials(credentials) {
41
43
  export async function removeCredentials() {
42
44
  await fs.rm(configPath, { force: true });
43
45
  }
46
+ /**
47
+ * Normalizes a zone flag (case-insensitive) to 'LA' | 'EU'. Returns null when invalid,
48
+ * so each command can report it in its own way.
49
+ */
50
+ export function parseZone(value) {
51
+ const zone = (value ?? '').trim().toUpperCase();
52
+ return zone === 'LA' || zone === 'EU' ? zone : null;
53
+ }
44
54
  export function apiBaseUrl(zone, dev = false) {
45
55
  if (dev)
46
56
  return 'http://localhost:5090';
@@ -1,39 +1,42 @@
1
1
  import chalk from 'chalk';
2
+ import { isJsonMode } from './output.js';
3
+ // In --json mode human output goes to stderr so stdout stays pure JSON.
4
+ const out = (message) => (isJsonMode() ? console.error : console.log)(message);
2
5
  export const logger = {
3
6
  info: (message) => {
4
- console.log(chalk.white(message));
7
+ out(chalk.white(message));
5
8
  },
6
9
  success: (message) => {
7
- console.log(chalk.hex('#66BB6A')(`\n ${message}`));
10
+ out(chalk.hex('#66BB6A')(`\n ${message}`));
8
11
  },
9
12
  warning: (message) => {
10
- console.log(chalk.hex('#FFA726')(`\n ${message}`));
13
+ out(chalk.hex('#FFA726')(`\n ${message}`));
11
14
  },
12
15
  error: (error) => {
13
16
  const message = error instanceof Error ? error.message : error;
14
17
  console.error(chalk.hex('#EF5350')(`\n Error: ${message}`));
15
18
  },
16
19
  divider: (length = 50) => {
17
- console.log(chalk.gray('─'.repeat(length)));
20
+ out(chalk.gray('─'.repeat(length)));
18
21
  },
19
22
  doubleDivider: (length = 50) => {
20
- console.log(chalk.gray('═'.repeat(length)));
23
+ out(chalk.gray('═'.repeat(length)));
21
24
  },
22
25
  label: (key, value) => {
23
- console.log(` ${chalk.gray(key + ':')} ${chalk.white(value)}`);
26
+ out(` ${chalk.gray(key + ':')} ${chalk.white(value)}`);
24
27
  },
25
28
  title: (text) => {
26
- console.log(chalk.bold.hex('#4CAF50')(`\n ${text}`));
27
- console.log(chalk.gray(' ' + '─'.repeat(40)));
29
+ out(chalk.bold.hex('#4CAF50')(`\n ${text}`));
30
+ out(chalk.gray(' ' + '─'.repeat(40)));
28
31
  },
29
32
  dim: (message) => {
30
- console.log(chalk.gray(` ${message}`));
33
+ out(chalk.gray(` ${message}`));
31
34
  },
32
35
  json: (data) => {
33
36
  const formatted = JSON.stringify(data, null, 2)
34
37
  .split('\n')
35
38
  .map(line => ' ' + line)
36
39
  .join('\n');
37
- console.log(chalk.gray(formatted));
40
+ out(chalk.gray(formatted));
38
41
  }
39
42
  };
@@ -0,0 +1,78 @@
1
+ // Shared output conventions for every command:
2
+ // - `--json`: stdout carries only JSON; human output (logger, spinners) goes to stderr.
3
+ // - Errors: a readable message (or {"error": {...}} on stderr in JSON mode) and exit code 1.
4
+ import chalk from 'chalk';
5
+ import { Option } from 'commander';
6
+ let jsonMode = false;
7
+ export function setJsonMode(value) {
8
+ jsonMode = value;
9
+ }
10
+ export function isJsonMode() {
11
+ return jsonMode;
12
+ }
13
+ /** Standard `--json` option. */
14
+ export function jsonOption(description = 'Output machine-readable JSON') {
15
+ return new Option('--json', description).default(false);
16
+ }
17
+ /** Prints a JSON document to stdout. */
18
+ export function printJson(data) {
19
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
20
+ }
21
+ /** An expected failure with a message meant for the user (no stack trace). */
22
+ export class CliError extends Error {
23
+ code;
24
+ hint;
25
+ constructor(message, code = 'error', hint) {
26
+ super(message);
27
+ this.code = code;
28
+ this.hint = hint;
29
+ this.name = 'CliError';
30
+ }
31
+ }
32
+ /** Turns any thrown value (CliError, SDK error, axios error) into a message and a status. */
33
+ export function describeError(error) {
34
+ if (error instanceof CliError) {
35
+ return { message: error.message, code: error.code, hint: error.hint };
36
+ }
37
+ const err = error;
38
+ const status = err?.response?.status;
39
+ const data = err?.response?.data;
40
+ const apiMessage = typeof data === 'string' ? data : data?.message || data?.error;
41
+ if (status === 401) {
42
+ return { message: apiMessage || 'Your session is invalid or expired.', code: 'unauthorized', status, hint: "Run 'plazbot login' to sign in again." };
43
+ }
44
+ if (status === 403) {
45
+ return { message: apiMessage || 'You do not have permission for this action.', code: 'forbidden', status, hint: "Check the active workspace with 'plazbot whoami'." };
46
+ }
47
+ if (status === 404) {
48
+ return { message: apiMessage || 'Resource not found.', code: 'not_found', status, hint: "Check the ID and the active workspace ('plazbot whoami')." };
49
+ }
50
+ if (status === 429) {
51
+ return { message: apiMessage || 'Too many requests.', code: 'rate_limited', status, hint: 'Wait a few seconds and try again.' };
52
+ }
53
+ if (status && status >= 500) {
54
+ return { message: apiMessage || `Plazbot API error (HTTP ${status}).`, code: 'api_error', status, hint: 'Try again in a few minutes.' };
55
+ }
56
+ if (status) {
57
+ return { message: apiMessage || `Request failed (HTTP ${status}).`, code: 'request_failed', status };
58
+ }
59
+ if (err?.code === 'ENOTFOUND' || err?.code === 'ECONNREFUSED' || err?.code === 'ETIMEDOUT' || err?.code === 'ECONNRESET') {
60
+ return { message: `Could not reach the Plazbot API (${err.code}).`, code: 'network_error', hint: 'Check your internet connection.' };
61
+ }
62
+ const message = error instanceof Error ? error.message : String(error ?? 'Unknown error');
63
+ return { message: message || 'Unknown error', code: 'error' };
64
+ }
65
+ /** Prints the error in the active format and exits with code 1. */
66
+ export function fail(error) {
67
+ const { message, code, status, hint } = describeError(error);
68
+ if (jsonMode) {
69
+ process.stderr.write(JSON.stringify({ error: { message, code, ...(status && { status }), ...(hint && { hint }) } }) + '\n');
70
+ }
71
+ else {
72
+ console.error(chalk.hex('#EF5350')(`\n Error: ${message}`));
73
+ if (hint)
74
+ console.error(chalk.gray(` ${hint}`));
75
+ console.error();
76
+ }
77
+ process.exit(1);
78
+ }
@@ -0,0 +1,22 @@
1
+ // Phone numbers: every command takes the number as -p, --phone and accepts any common
2
+ // international format (+51 912 345 678, 0051-912345678, 51912345678).
3
+ import { Option } from 'commander';
4
+ import { CliError } from './output.js';
5
+ export function phoneOption(description = 'WhatsApp number in international format, e.g. +51912345678') {
6
+ return new Option('-p, --phone <number>', description);
7
+ }
8
+ /** Returns the number as digits with country code (E.164 without '+'), or throws. */
9
+ export function normalizePhone(input, flag = '--phone') {
10
+ if (!input || !String(input).trim()) {
11
+ throw new CliError(`Missing phone number.`, 'missing_phone', `Pass it with ${flag}, e.g. ${flag} +51912345678`);
12
+ }
13
+ let digits = String(input).trim().replace(/[\s().-]/g, '');
14
+ if (digits.startsWith('+'))
15
+ digits = digits.slice(1);
16
+ else if (digits.startsWith('00'))
17
+ digits = digits.slice(2);
18
+ if (!/^\d{8,15}$/.test(digits)) {
19
+ throw new CliError(`Invalid phone number "${input}".`, 'invalid_phone', 'Use the international format with country code, e.g. +51912345678 or 51912345678.');
20
+ }
21
+ return digits;
22
+ }
package/dist/utils/ui.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import Table from 'cli-table3';
4
- // Colores del tema Plazbot
4
+ import { isJsonMode } from './output.js';
5
+ // Plazbot theme colors
5
6
  export const theme = {
6
7
  primary: chalk.hex('#4CAF50'),
7
8
  secondary: chalk.hex('#2196F3'),
@@ -19,9 +20,11 @@ export function createSpinner(text) {
19
20
  text: theme.muted(text),
20
21
  spinner: 'dots',
21
22
  color: 'green',
23
+ // ora writes to stderr; in --json mode it stays silent
24
+ isSilent: isJsonMode(),
22
25
  });
23
26
  }
24
- // Tabla formateada
27
+ // Formatted table
25
28
  export function createTable(headers, rows) {
26
29
  const table = new Table({
27
30
  head: headers.map(h => theme.bold.hex('#4CAF50')(h)),
@@ -39,7 +42,7 @@ export function createTable(headers, rows) {
39
42
  rows.forEach(row => table.push(row));
40
43
  return table.toString();
41
44
  }
42
- // Box simple
45
+ // Simple box
43
46
  export function box(content, title) {
44
47
  const lines = content.split('\n');
45
48
  const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), title ? stripAnsi(title).length + 4 : 0);
@@ -58,14 +61,14 @@ export function box(content, title) {
58
61
  result += theme.muted(' └' + '─'.repeat(width) + '┘');
59
62
  return result;
60
63
  }
61
- // Strip ANSI codes para calcular longitud real
64
+ // Strip ANSI codes to measure the visible length
62
65
  function stripAnsi(str) {
63
66
  return str.replace(/\x1b\[[0-9;]*m/g, '');
64
67
  }
65
- // Formato de fecha legible
68
+ // Readable date
66
69
  export function formatDate(date) {
67
70
  const d = new Date(date);
68
- return d.toLocaleDateString('es-ES', {
71
+ return d.toLocaleDateString('en-US', {
69
72
  year: 'numeric',
70
73
  month: 'short',
71
74
  day: 'numeric',
@@ -76,18 +79,18 @@ export function formatDate(date) {
76
79
  // Status badge
77
80
  export function statusBadge(enabled) {
78
81
  return enabled
79
- ? theme.success('● activo')
80
- : theme.error('○ inactivo');
82
+ ? theme.success('● active')
83
+ : theme.error('○ inactive');
81
84
  }
82
- // Key-value pair formateado
85
+ // Formatted key-value pair
83
86
  export function kvPair(key, value) {
84
87
  return ` ${theme.muted(key + ':')} ${value}`;
85
88
  }
86
- // Sección con título
89
+ // Section with title
87
90
  export function section(title) {
88
91
  return '\n' + theme.bold.hex('#4CAF50')(` ${title}`) + '\n' + theme.muted(' ' + '─'.repeat(40));
89
92
  }
90
- // Progress bar simple
93
+ // Simple progress bar
91
94
  export function progressBar(current, total, width = 30) {
92
95
  const percent = Math.round((current / total) * 100);
93
96
  const filled = Math.round((current / total) * width);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plazbot-cli",
3
- "version": "0.4.0",
4
- "description": "CLI oficial de Plazbot: WhatsApp for Developers, agentes IA y workers desde tu terminal",
3
+ "version": "0.5.0",
4
+ "description": "Official Plazbot CLI: WhatsApp for Developers, AI agents and workers from your terminal",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
7
7
  "bin": {