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.
- 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 +183 -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
|
@@ -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
|
-
? `
|
|
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('- `
|
|
65
|
-
lines.push('-
|
|
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
|
|
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: '
|
|
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
|
|
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(`
|
|
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(`
|
|
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 ?? '
|
|
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 ? ' (
|
|
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('
|
|
17
|
-
chalk.gray(' plazbot
|
|
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
|
|
20
|
-
chalk.gray(' plazbot
|
|
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
|
-
'
|
|
26
|
-
chalk.gray('
|
|
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
|
-
`
|
|
32
|
-
chalk.gray('
|
|
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 '
|
|
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
|
-
`
|
|
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
|
+
}
|
package/dist/utils/banner.js
CHANGED
|
@@ -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')('
|
|
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')('
|
|
60
|
+
rightColumn.push(chalk.bold.hex('#FFA726')('Not signed in'));
|
|
58
61
|
rightColumn.push('');
|
|
59
|
-
rightColumn.push(chalk.bold.hex('#4CAF50')('
|
|
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')('
|
|
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('
|
|
28
|
+
throw new Error('Incomplete credentials');
|
|
28
29
|
}
|
|
29
|
-
|
|
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
|
|
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';
|
package/dist/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
7
|
+
out(chalk.white(message));
|
|
5
8
|
},
|
|
6
9
|
success: (message) => {
|
|
7
|
-
|
|
10
|
+
out(chalk.hex('#66BB6A')(`\n ${message}`));
|
|
8
11
|
},
|
|
9
12
|
warning: (message) => {
|
|
10
|
-
|
|
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
|
-
|
|
20
|
+
out(chalk.gray('─'.repeat(length)));
|
|
18
21
|
},
|
|
19
22
|
doubleDivider: (length = 50) => {
|
|
20
|
-
|
|
23
|
+
out(chalk.gray('═'.repeat(length)));
|
|
21
24
|
},
|
|
22
25
|
label: (key, value) => {
|
|
23
|
-
|
|
26
|
+
out(` ${chalk.gray(key + ':')} ${chalk.white(value)}`);
|
|
24
27
|
},
|
|
25
28
|
title: (text) => {
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
out(chalk.bold.hex('#4CAF50')(`\n ${text}`));
|
|
30
|
+
out(chalk.gray(' ' + '─'.repeat(40)));
|
|
28
31
|
},
|
|
29
32
|
dim: (message) => {
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
68
|
+
// Readable date
|
|
66
69
|
export function formatDate(date) {
|
|
67
70
|
const d = new Date(date);
|
|
68
|
-
return d.toLocaleDateString('
|
|
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('●
|
|
80
|
-
: theme.error('○
|
|
82
|
+
? theme.success('● active')
|
|
83
|
+
: theme.error('○ inactive');
|
|
81
84
|
}
|
|
82
|
-
//
|
|
85
|
+
// Formatted key-value pair
|
|
83
86
|
export function kvPair(key, value) {
|
|
84
87
|
return ` ${theme.muted(key + ':')} ${value}`;
|
|
85
88
|
}
|
|
86
|
-
//
|
|
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
|
-
//
|
|
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
|
-
"description": "
|
|
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": {
|