@velaro/cli 0.1.1 → 0.2.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/bin/velaro.js CHANGED
@@ -4,25 +4,53 @@ import yargs from 'yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
5
  import { loginCommand, logoutCommand } from '../lib/commands/login.js';
6
6
  import { whoamiCommand } from '../lib/commands/whoami.js';
7
+ import { teamCommand } from '../lib/commands/team.js';
7
8
  import { botCommand } from '../lib/commands/bot.js';
9
+ import { kbCommand } from '../lib/commands/kb.js';
10
+ import { workflowCommand } from '../lib/commands/workflow.js';
11
+ import { ruleCommand } from '../lib/commands/rule.js';
12
+ import { agentCommand } from '../lib/commands/agent.js';
8
13
  import { ingestCommand } from '../lib/commands/ingest.js';
9
14
  import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
10
15
  import { statusCommand } from '../lib/commands/status.js';
16
+ import { siteCommand } from '../lib/commands/site.js';
17
+ import { checkCommand } from '../lib/commands/check.js';
18
+ import { updateCommand } from '../lib/commands/update.js';
19
+ import { startUpdateCheck } from '../lib/update-check.js';
11
20
 
12
- yargs(hideBin(process.argv))
21
+ // Start background update check — doesn't block the command
22
+ const printUpdateNotice = startUpdateCheck();
23
+
24
+ await yargs(hideBin(process.argv))
13
25
  .scriptName('velaro')
14
26
  .usage('$0 <command> [options]')
27
+ // Auth
15
28
  .command(loginCommand)
16
29
  .command(logoutCommand)
17
30
  .command(whoamiCommand)
31
+ // Info
32
+ .command(siteCommand)
33
+ .command(checkCommand)
34
+ .command(statusCommand)
35
+ // Config
36
+ .command(teamCommand)
18
37
  .command(botCommand)
38
+ .command(kbCommand)
39
+ .command(workflowCommand)
40
+ .command(ruleCommand)
41
+ .command(agentCommand)
42
+ // Integrations / keys
19
43
  .command(ingestCommand)
20
44
  .command(mcpKeyCommand)
21
- .command(statusCommand)
45
+ // CLI maintenance
46
+ .command(updateCommand)
22
47
  .demandCommand(1, 'Specify a command. Run velaro --help for a list.')
23
48
  .strict()
24
49
  .help()
25
50
  .alias('h', 'help')
26
51
  .alias('v', 'version')
27
52
  .wrap(Math.min(100, process.stdout.columns || 100))
28
- .argv;
53
+ .parseAsync();
54
+
55
+ // Print update notice after command output (non-intrusive)
56
+ await printUpdateNotice();
@@ -0,0 +1,50 @@
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const VALID_STATUSES = ['Available', 'Away', 'Offline'];
5
+
6
+ export const agentCommand = {
7
+ command: 'agent <subcommand>',
8
+ describe: 'View and manage agents',
9
+ builder: (yargs) => yargs
10
+ .command({
11
+ command: 'list',
12
+ describe: 'List agents and their current availability',
13
+ handler: runCommand(async () => {
14
+ const [agents, teams] = await Promise.all([
15
+ get('/Users/List'),
16
+ get('/Teams/List'),
17
+ ]);
18
+
19
+ const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
20
+
21
+ if (!agents.length) { console.log('No agents found.'); return; }
22
+
23
+ const rows = agents.map(a => ({
24
+ id: `[${a.id}]`,
25
+ status: (a.status ?? 'Offline').padEnd(9),
26
+ name: a.displayName || `${a.firstName} ${a.lastName}`.trim() || a.email,
27
+ teams: (a.teamIds ?? []).map(id => teamMap[id] ?? `team ${id}`).join(', ') || '—',
28
+ }));
29
+
30
+ const idW = Math.max(...rows.map(r => r.id.length));
31
+
32
+ for (const r of rows) {
33
+ console.log(`${r.id.padStart(idW)} ${r.status} ${r.name.padEnd(30)} ${r.teams}`);
34
+ }
35
+ console.log(`\n${rows.length} agent(s)`);
36
+ }),
37
+ })
38
+ .command({
39
+ command: 'set-status',
40
+ describe: 'Set an agent\'s availability status (admin only)',
41
+ builder: (y) => y
42
+ .option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
43
+ .option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
44
+ handler: runCommand(async (argv) => {
45
+ await post('/UserStatus/admin', { userId: argv.id, status: argv.status });
46
+ console.log(`Agent ${argv.id} set to ${argv.status}.`);
47
+ }),
48
+ })
49
+ .demandCommand(1, 'Specify a subcommand: list | set-status'),
50
+ };
@@ -1,5 +1,7 @@
1
- import { get, post } from '../api.js';
2
- import { runCommand } from '../run.js';
1
+ import { readFileSync } from 'fs';
2
+ import { get, post } from '../api.js';
3
+ import { requireFeature } from '../subscription.js';
4
+ import { runCommand } from '../run.js';
3
5
 
4
6
  export const botCommand = {
5
7
  command: 'bot <subcommand>',
@@ -7,17 +9,26 @@ export const botCommand = {
7
9
  builder: (yargs) =>
8
10
  yargs
9
11
  .command(botListCommand)
12
+ .command(botGetCommand)
10
13
  .command(botSetupCommand)
11
- .demandCommand(1, 'Specify a subcommand: list, setup'),
14
+ .command(botPromptCommand)
15
+ .demandCommand(1, 'Specify a subcommand: list, get, setup, prompt'),
12
16
  handler: () => {},
13
17
  };
14
18
 
19
+ // ── list ───────────────────────────────────────────────────────────────────────
20
+
15
21
  const botListCommand = {
16
22
  command: 'list',
17
23
  describe: 'List AI bots on your site',
18
24
  handler: runCommand(async () => {
25
+ await requireFeature('enableAI', 'AI Bots');
26
+
19
27
  const bots = await get('/AIConfiguration/list');
20
- if (!bots?.length) { console.log('No bots found.'); return; }
28
+ if (!bots?.length) {
29
+ console.log('No bots found. Create one with: velaro bot setup --name "My Bot"');
30
+ return;
31
+ }
21
32
  console.log(`Found ${bots.length} bot(s):\n`);
22
33
  for (const b of bots) {
23
34
  console.log(` [${b.id}] ${b.name} model=${b.aiModel ?? '(default)'} status=${b.status ?? 'ready'}`);
@@ -25,30 +36,102 @@ const botListCommand = {
25
36
  }),
26
37
  };
27
38
 
39
+ // ── get ────────────────────────────────────────────────────────────────────────
40
+
41
+ const botGetCommand = {
42
+ command: 'get <id>',
43
+ describe: 'Show full configuration for a bot',
44
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Bot ID' }),
45
+ handler: runCommand(async (argv) => {
46
+ await requireFeature('enableAI', 'AI Bots');
47
+
48
+ const bot = await get(`/AIConfiguration/${argv.id}`);
49
+ if (!bot) { console.error(`Bot ${argv.id} not found.`); process.exit(1); }
50
+
51
+ console.log(`\n[${bot.id}] ${bot.name}`);
52
+ console.log(` Model: ${bot.aiModel ?? '(default)'}`);
53
+ console.log(` Tone: ${bot.tone ?? '—'}`);
54
+ console.log(` Reply length: ${bot.replyLength ?? '—'}`);
55
+ console.log(` Status: ${bot.status ?? 'ready'}`);
56
+ if (bot.prompt) {
57
+ console.log(`\nSystem prompt:\n`);
58
+ console.log(bot.prompt);
59
+ } else {
60
+ console.log(`\nSystem prompt: (none)`);
61
+ }
62
+ }),
63
+ };
64
+
65
+ // ── setup ──────────────────────────────────────────────────────────────────────
66
+
28
67
  const botSetupCommand = {
29
68
  command: 'setup',
30
69
  describe: 'Create or update an AI bot (upsert by name)',
31
70
  builder: (y) =>
32
71
  y
33
- .option('name', { describe: 'Bot name', type: 'string', demandOption: true })
34
- .option('prompt', { describe: 'System prompt text', type: 'string' })
35
- .option('model', { describe: 'AI model override', type: 'string', default: 'velaro-gpt-4o-mini' })
36
- .option('tone', { describe: 'Response tone', choices: ['Professional','Friendly','Formal','Casual'], default: 'Professional' })
37
- .option('reply-length', { describe: 'Response length', choices: ['Short','Medium','Long'], default: 'Medium' })
38
- .option('urls', { describe: 'Comma-separated data source URLs', type: 'string' }),
72
+ .option('name', { describe: 'Bot name', type: 'string', demandOption: true })
73
+ .option('prompt', { describe: 'System prompt text (inline)', type: 'string' })
74
+ .option('prompt-file', { describe: 'Path to a file containing the system prompt', type: 'string' })
75
+ .option('model', { describe: 'AI model', choices: ['velaro-gpt-4o-mini','velaro-gpt-4o'], type: 'string', default: 'velaro-gpt-4o-mini' })
76
+ .option('tone', { describe: 'Response tone', choices: ['Professional','Friendly','Formal','Casual'], default: 'Professional' })
77
+ .option('reply-length', { describe: 'Response length', choices: ['Short','Medium','Long'], default: 'Medium' })
78
+ .option('urls', { describe: 'Comma-separated data source URLs', type: 'string' })
79
+ .conflicts('prompt', 'prompt-file'),
39
80
 
40
81
  handler: runCommand(async (argv) => {
82
+ await requireFeature('enableAI', 'AI Bots');
83
+
84
+ let promptText = argv.prompt;
85
+ if (argv['prompt-file']) {
86
+ promptText = readFileSync(argv['prompt-file'], 'utf8').trim();
87
+ console.log(`Read ${promptText.length} chars from ${argv['prompt-file']}`);
88
+ }
89
+
41
90
  const payload = {
42
91
  name: argv.name,
43
92
  aiModel: argv.model,
44
93
  tone: argv.tone,
45
94
  replyLength: argv['reply-length'],
46
95
  };
47
-
48
- if (argv.prompt) payload.prompt = argv.prompt;
49
- if (argv.urls) payload.dataUrls = argv.urls.split(',').map((u) => u.trim()).filter(Boolean);
96
+ if (promptText) payload.prompt = promptText;
97
+ if (argv.urls) payload.dataUrls = argv.urls.split(',').map((u) => u.trim()).filter(Boolean);
50
98
 
51
99
  const result = await post('/AIConfiguration', payload);
52
100
  console.log(`Saved: [${result.id}] ${result.name}`);
53
101
  }),
54
102
  };
103
+
104
+ // ── prompt ─────────────────────────────────────────────────────────────────────
105
+
106
+ const botPromptCommand = {
107
+ command: 'prompt <id>',
108
+ describe: 'Get or update a bot\'s system prompt',
109
+ builder: (y) =>
110
+ y
111
+ .positional('id', { type: 'number', describe: 'Bot ID' })
112
+ .option('set', { describe: 'New prompt text (inline)', type: 'string' })
113
+ .option('file', { describe: 'Path to a .txt/.md prompt file', type: 'string' })
114
+ .conflicts('set', 'file'),
115
+
116
+ handler: runCommand(async (argv) => {
117
+ await requireFeature('enableAI', 'AI Bots');
118
+
119
+ const bot = await get(`/AIConfiguration/${argv.id}`);
120
+ if (!bot) { console.error(`Bot ${argv.id} not found.`); process.exit(1); }
121
+
122
+ if (!argv.set && !argv.file) {
123
+ // Read mode — print current prompt
124
+ console.log(bot.prompt ?? '(no system prompt set)');
125
+ return;
126
+ }
127
+
128
+ let promptText = argv.set;
129
+ if (argv.file) {
130
+ promptText = readFileSync(argv.file, 'utf8').trim();
131
+ console.log(`Read ${promptText.length} chars from ${argv.file}`);
132
+ }
133
+
134
+ await post('/AIConfiguration', { ...bot, prompt: promptText });
135
+ console.log(`Bot [${argv.id}] "${bot.name}" prompt updated (${promptText.length} chars).`);
136
+ }),
137
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * velaro check — full site health and configuration summary.
3
+ *
4
+ * Shows every feature area with:
5
+ * ✓ enabled and configured
6
+ * ⚠ enabled but nothing configured yet
7
+ * ✗ not on your plan
8
+ */
9
+
10
+ import { get } from '../api.js';
11
+ import { getSubscription } from '../subscription.js';
12
+ import { runCommand } from '../run.js';
13
+
14
+ const TICK = '✓';
15
+ const WARN = '⚠';
16
+ const CROSS = '✗';
17
+
18
+ export const checkCommand = {
19
+ command: 'check',
20
+ describe: 'Show full site configuration and health summary',
21
+ handler: runCommand(async () => {
22
+ const [sub, apiStatus] = await Promise.all([
23
+ getSubscription().catch(() => null),
24
+ get('/Status').catch(() => null),
25
+ ]);
26
+
27
+ if (!sub) {
28
+ console.error('Could not retrieve subscription. Check your login: velaro whoami');
29
+ process.exit(1);
30
+ }
31
+
32
+ const ok = (msg) => console.log(` ${TICK} ${msg}`);
33
+ const warn = (msg) => console.log(` ${WARN} ${msg}`);
34
+ const off = (msg) => console.log(` ${CROSS} ${msg}`);
35
+
36
+ console.log('\n── API Health ────────────────────────────────────────────────');
37
+ if (apiStatus) {
38
+ ok(`API reachable`);
39
+ } else {
40
+ warn('API did not respond to /Status');
41
+ }
42
+
43
+ // ── Deployments ─────────────────────────────────────────────────────────
44
+ console.log('\n── Deployments ───────────────────────────────────────────────');
45
+ try {
46
+ const [deps, teams] = await Promise.all([get('/Deployment'), get('/Teams/List')]);
47
+ const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
48
+ if (deps?.length) {
49
+ for (const d of deps) {
50
+ ok(`[${d.id}] "${d.displayName}" → team: ${teamMap[d.teamId] ?? `#${d.teamId}`} (embed key: ${d.deploymentId})`);
51
+ }
52
+ } else {
53
+ warn('No deployments — create one in the admin UI to get your embed snippet.');
54
+ }
55
+ } catch { warn('Could not load deployments'); }
56
+
57
+ // ── AI Bots ──────────────────────────────────────────────────────────────
58
+ console.log('\n── AI Bots ───────────────────────────────────────────────────');
59
+ if (sub.enableAI) {
60
+ try {
61
+ const bots = await get('/AIConfiguration/list');
62
+ if (bots?.length) {
63
+ for (const b of bots) ok(`[${b.id}] "${b.name}" model=${b.aiModel ?? 'default'}`);
64
+ } else {
65
+ warn('AI enabled but no bots configured. Run: velaro bot setup --name "My Bot"');
66
+ }
67
+ } catch { warn('Could not load bot list'); }
68
+ } else {
69
+ off('AI Bots — not on your plan (upgrade at velaro.com/pricing)');
70
+ }
71
+
72
+ // ── Knowledge Base ───────────────────────────────────────────────────────
73
+ console.log('\n── Knowledge Base ────────────────────────────────────────────');
74
+ if (sub.enableKnowledgeBase) {
75
+ try {
76
+ const [qna, overrides] = await Promise.all([
77
+ get('/api/BotQnA').catch(() => []),
78
+ get('/Ticket/knowledge-overrides').catch(() => []),
79
+ ]);
80
+ ok(`${qna?.length ?? 0} Q&A pair(s) · ${overrides?.length ?? 0} override(s)`);
81
+ } catch { warn('Could not load knowledge base'); }
82
+ } else {
83
+ off('Knowledge Base — not on your plan');
84
+ }
85
+
86
+ // ── Workflows ────────────────────────────────────────────────────────────
87
+ console.log('\n── Workflows ─────────────────────────────────────────────────');
88
+ if (sub.enableAutomation) {
89
+ try {
90
+ const workflows = (await get('/Workflows/List') ?? []).filter(w => !w.isTemplate);
91
+ const enabled = workflows.filter(w => w.enabled).length;
92
+ const disabled = workflows.length - enabled;
93
+ if (workflows.length) {
94
+ ok(`${workflows.length} workflow(s) · ${enabled} enabled · ${disabled} disabled`);
95
+ for (const w of workflows) {
96
+ const sym = w.enabled ? TICK : WARN;
97
+ console.log(` ${sym} [${w.id}] "${w.name}" (${w.enabled ? 'on' : 'off'})`);
98
+ }
99
+ } else {
100
+ warn('Automation enabled but no workflows configured.');
101
+ }
102
+ } catch { warn('Could not load workflows'); }
103
+ } else {
104
+ off('Workflows — not on your plan');
105
+ }
106
+
107
+ // ── Routing Rules ────────────────────────────────────────────────────────
108
+ console.log('\n── Routing Rules ─────────────────────────────────────────────');
109
+ if (sub.enableWorkflowRules) {
110
+ try {
111
+ const rules = await get('/Rules/List');
112
+ if (rules?.length) {
113
+ ok(`${rules.length} routing rule(s) configured`);
114
+ } else {
115
+ warn('Routing rules enabled but none configured.');
116
+ }
117
+ } catch { warn('Could not load rules'); }
118
+ } else {
119
+ off('Routing Rules — not on your plan');
120
+ }
121
+
122
+ // ── Channels ─────────────────────────────────────────────────────────────
123
+ console.log('\n── Channels ──────────────────────────────────────────────────');
124
+ const channels = [
125
+ ['enableWeb', 'Web Chat'],
126
+ ['enableSms', 'SMS'],
127
+ ['enableEmail', 'Email'],
128
+ ['enableWhatsapp', 'WhatsApp'],
129
+ ['enableIvr', 'IVR / Voice'],
130
+ ['enableFacebook', 'Facebook Messenger'],
131
+ ['enableInstagram', 'Instagram'],
132
+ ['enableRcs', 'RCS'],
133
+ ];
134
+ for (const [flag, label] of channels) {
135
+ if (sub[flag]) ok(label); else off(`${label} — not on your plan`);
136
+ }
137
+
138
+ // ── IVR detail ───────────────────────────────────────────────────────────
139
+ if (sub.enableIvr) {
140
+ try {
141
+ const ivr = await get('/api/ivr/config');
142
+ if (ivr?.phoneNumber) {
143
+ console.log(`\n── IVR Config ────────────────────────────────────────────────`);
144
+ ok(`Phone: ${ivr.phoneNumber}`);
145
+ if (ivr.bridgeNumber) ok(`Bridge: ${ivr.bridgeNumber}`);
146
+ else warn('No bridge (transfer) number set — agents cannot receive transferred calls.');
147
+ }
148
+ } catch { /* IVR config is optional */ }
149
+ }
150
+
151
+ // ── Agents ───────────────────────────────────────────────────────────────
152
+ console.log('\n── Agents ────────────────────────────────────────────────────');
153
+ try {
154
+ const agents = await get('/Users/List');
155
+ const online = (agents ?? []).filter(a => a.status === 'Available').length;
156
+ const away = (agents ?? []).filter(a => a.status === 'Away').length;
157
+ const offline = (agents ?? []).length - online - away;
158
+ ok(`${agents?.length ?? 0} agent(s) · ${online} online · ${away} away · ${offline} offline`);
159
+ } catch { warn('Could not load agents'); }
160
+
161
+ console.log('\n──────────────────────────────────────────────────────────────\n');
162
+ }),
163
+ };
@@ -0,0 +1,107 @@
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const deploymentCommand = {
5
+ command: 'deployment <subcommand>',
6
+ describe: 'Manage chat widget deployments',
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command(deploymentListCommand)
10
+ .command(deploymentRenameCommand)
11
+ .command(deploymentAssignCommand)
12
+ .demandCommand(1, 'Specify a subcommand: list, rename, assign-team'),
13
+ handler: () => {},
14
+ };
15
+
16
+ // ── list ───────────────────────────────────────────────────────────────────────
17
+
18
+ const deploymentListCommand = {
19
+ command: 'list',
20
+ describe: 'List chat widget deployments',
21
+ handler: runCommand(async () => {
22
+ const [deployments, teams] = await Promise.all([
23
+ get('/Deployment'),
24
+ get('/Teams/List'),
25
+ ]);
26
+
27
+ if (!deployments?.length) {
28
+ console.log('No deployments found.');
29
+ console.log('\nA deployment is a chat widget embed — one per page or team you want to serve.');
30
+ return;
31
+ }
32
+
33
+ const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
34
+
35
+ const rows = deployments.map(d => ({
36
+ id: `[${d.id}]`,
37
+ key: d.deploymentId ?? '—',
38
+ team: teamMap[d.teamId] ?? `team ${d.teamId}`,
39
+ name: d.displayName ?? '(unnamed)',
40
+ }));
41
+
42
+ const idW = Math.max(...rows.map(r => r.id.length));
43
+ const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
44
+ const teamW = Math.max('team'.length, ...rows.map(r => r.team.length));
45
+
46
+ console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'team'.padEnd(teamW)} name`);
47
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
48
+ for (const r of rows) {
49
+ console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
50
+ }
51
+ console.log(`\n${rows.length} deployment(s)`);
52
+ console.log('\nThe embed-key goes in your widget JavaScript snippet.');
53
+ }),
54
+ };
55
+
56
+ // ── rename ─────────────────────────────────────────────────────────────────────
57
+
58
+ const deploymentRenameCommand = {
59
+ command: 'rename <id> <name>',
60
+ describe: 'Rename a deployment',
61
+ builder: (y) =>
62
+ y
63
+ .positional('id', { type: 'number', describe: 'Deployment numeric ID' })
64
+ .positional('name', { type: 'string', describe: 'New display name' }),
65
+ handler: runCommand(async (argv) => {
66
+ const dep = await get(`/Deployment/${argv.id}`);
67
+ if (!dep) {
68
+ console.error(`Deployment ${argv.id} not found.`);
69
+ process.exit(1);
70
+ }
71
+
72
+ await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
73
+ console.log(`Deployment ${argv.id} renamed to "${argv.name}".`);
74
+ }),
75
+ };
76
+
77
+ // ── assign-team ────────────────────────────────────────────────────────────────
78
+
79
+ const deploymentAssignCommand = {
80
+ command: 'assign-team <id> <team-id>',
81
+ describe: 'Assign a team to a deployment',
82
+ builder: (y) =>
83
+ y
84
+ .positional('id', { type: 'number', describe: 'Deployment numeric ID' })
85
+ .positional('team-id', { type: 'number', describe: 'Team ID to assign' }),
86
+ handler: runCommand(async (argv) => {
87
+ const [dep, teams] = await Promise.all([
88
+ get(`/Deployment/${argv.id}`),
89
+ get('/Teams/List'),
90
+ ]);
91
+
92
+ if (!dep) {
93
+ console.error(`Deployment ${argv.id} not found.`);
94
+ process.exit(1);
95
+ }
96
+
97
+ const team = (teams ?? []).find(t => t.id === argv['team-id']);
98
+ if (!team) {
99
+ console.error(`Team ${argv['team-id']} not found.`);
100
+ console.error('Run: velaro team list');
101
+ process.exit(1);
102
+ }
103
+
104
+ await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
105
+ console.log(`Deployment ${argv.id} ("${dep.displayName}") assigned to team "${team.name}".`);
106
+ }),
107
+ };
@@ -0,0 +1,132 @@
1
+ import { get, post, del } from '../api.js';
2
+ import { requireFeature } from '../subscription.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const kbCommand = {
6
+ command: 'kb <subcommand>',
7
+ describe: 'Manage knowledge base — Q&A pairs and bot overrides',
8
+ builder: (yargs) =>
9
+ yargs
10
+ .command(qnaCommand)
11
+ .command(overrideCommand)
12
+ .demandCommand(1, 'Specify a subcommand: qna, override'),
13
+ handler: () => {},
14
+ };
15
+
16
+ // ────────────────────────────────────────────────────────────────────────────
17
+ // Q&A
18
+ // ────────────────────────────────────────────────────────────────────────────
19
+
20
+ const qnaCommand = {
21
+ command: 'qna <subcommand>',
22
+ describe: 'Manage Q&A knowledge pairs',
23
+ builder: (yargs) =>
24
+ yargs
25
+ .command({
26
+ command: 'list',
27
+ describe: 'List Q&A pairs',
28
+ builder: (y) => y.option('bot-id', { type: 'number', describe: 'Filter by bot ID' }),
29
+ handler: runCommand(async (argv) => {
30
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
31
+ const params = argv['bot-id'] ? `?aiConfigurationId=${argv['bot-id']}` : '';
32
+ const items = await get(`/api/BotQnA${params}`);
33
+ if (!items?.length) { console.log('No Q&A pairs found.'); return; }
34
+
35
+ console.log(`\nFound ${items.length} Q&A pair(s):\n`);
36
+ for (const item of items) {
37
+ console.log(` [${item.id}] Q: ${truncate(item.question, 70)}`);
38
+ console.log(` A: ${truncate(item.answer, 70)}\n`);
39
+ }
40
+ }),
41
+ })
42
+ .command({
43
+ command: 'add',
44
+ describe: 'Add a Q&A pair',
45
+ builder: (y) =>
46
+ y
47
+ .option('bot-id', { type: 'number', demandOption: true, describe: 'Bot ID' })
48
+ .option('question', { type: 'string', demandOption: true, describe: 'Question text' })
49
+ .option('answer', { type: 'string', demandOption: true, describe: 'Answer text' }),
50
+ handler: runCommand(async (argv) => {
51
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
52
+ const result = await post('/api/BotQnA', {
53
+ aiConfigurationId: argv['bot-id'],
54
+ question: argv.question,
55
+ answer: argv.answer,
56
+ });
57
+ console.log(`Q&A pair added: [${result.id}]`);
58
+ }),
59
+ })
60
+ .command({
61
+ command: 'delete <id>',
62
+ describe: 'Delete a Q&A pair',
63
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Q&A pair ID' }),
64
+ handler: runCommand(async (argv) => {
65
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
66
+ await del(`/api/BotQnA/${argv.id}`);
67
+ console.log(`Q&A pair ${argv.id} deleted.`);
68
+ }),
69
+ })
70
+ .demandCommand(1, 'Specify a subcommand: list, add, delete'),
71
+ handler: () => {},
72
+ };
73
+
74
+ // ────────────────────────────────────────────────────────────────────────────
75
+ // Overrides — authoritative facts that take priority over KB search
76
+ // ────────────────────────────────────────────────────────────────────────────
77
+
78
+ const overrideCommand = {
79
+ command: 'override <subcommand>',
80
+ describe: 'Manage bot knowledge overrides (authoritative facts)',
81
+ builder: (yargs) =>
82
+ yargs
83
+ .command({
84
+ command: 'list',
85
+ describe: 'List knowledge overrides',
86
+ handler: runCommand(async () => {
87
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
88
+ const items = await get('/Ticket/knowledge-overrides');
89
+ if (!items?.length) { console.log('No overrides found.'); return; }
90
+
91
+ console.log(`\nFound ${items.length} override(s):\n`);
92
+ for (const item of items) {
93
+ const status = item.isActive ? 'active ' : 'inactive';
94
+ const expiry = item.expiresAt ? ` expires ${new Date(item.expiresAt).toLocaleDateString()}` : '';
95
+ console.log(` [${item.id}] ${status} ${truncate(item.title ?? item.content, 60)}${expiry}`);
96
+ }
97
+ }),
98
+ })
99
+ .command({
100
+ command: 'add',
101
+ describe: 'Add a knowledge override',
102
+ builder: (y) =>
103
+ y
104
+ .option('title', { type: 'string', demandOption: true, describe: 'Override title' })
105
+ .option('content', { type: 'string', demandOption: true, describe: 'Authoritative content' })
106
+ .option('expires', { type: 'string', describe: 'Expiry date (ISO 8601, e.g. 2026-12-31)' }),
107
+ handler: runCommand(async (argv) => {
108
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
109
+ const payload = { title: argv.title, content: argv.content, isActive: true };
110
+ if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
111
+ const result = await post('/Ticket/knowledge-overrides', payload);
112
+ console.log(`Override added: [${result.id}] "${argv.title}"`);
113
+ }),
114
+ })
115
+ .command({
116
+ command: 'delete <id>',
117
+ describe: 'Delete a knowledge override',
118
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Override ID' }),
119
+ handler: runCommand(async (argv) => {
120
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
121
+ await del(`/Ticket/knowledge-overrides/${argv.id}`);
122
+ console.log(`Override ${argv.id} deleted.`);
123
+ }),
124
+ })
125
+ .demandCommand(1, 'Specify a subcommand: list, add, delete'),
126
+ handler: () => {},
127
+ };
128
+
129
+ function truncate(str, max) {
130
+ if (!str) return '—';
131
+ return str.length <= max ? str : str.slice(0, max - 1) + '…';
132
+ }
@@ -1,19 +1,24 @@
1
1
  import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
- import { readConfig, writeConfig, DEFAULT_API_BASE } from '../config.js';
2
+ import { readConfig, writeConfig, DEFAULT_API_BASE, STAGING_API_BASE } from '../config.js';
3
3
  import { runCommand } from '../run.js';
4
4
 
5
5
  export const loginCommand = {
6
6
  command: 'login',
7
7
  describe: 'Authenticate with Velaro using your browser',
8
8
  builder: (y) =>
9
- y.option('api', {
10
- describe: 'Velaro Messaging API base URL',
11
- default: DEFAULT_API_BASE,
12
- type: 'string',
13
- }),
9
+ y
10
+ .option('staging', {
11
+ describe: 'Connect to the Velaro staging environment',
12
+ type: 'boolean',
13
+ default: false,
14
+ })
15
+ .option('api', {
16
+ describe: 'Override the Velaro API base URL (advanced)',
17
+ type: 'string',
18
+ }),
14
19
 
15
20
  handler: runCommand(async (argv) => {
16
- const apiBase = argv.api;
21
+ const apiBase = argv.api ?? (argv.staging ? STAGING_API_BASE : DEFAULT_API_BASE);
17
22
 
18
23
  console.log('Starting Velaro login...\n');
19
24
 
@@ -36,7 +41,8 @@ export const loginCommand = {
36
41
  userName: velaroResult.profile?.Name,
37
42
  });
38
43
 
39
- console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}`);
44
+ const envLabel = argv.staging || argv.api ? ` (${apiBase})` : '';
45
+ console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}${envLabel}`);
40
46
  console.log(`Site ID: ${velaroResult.profile?.SiteId}`);
41
47
  console.log('Credentials saved to ~/.velaro/config.json');
42
48
  }),
@@ -0,0 +1,85 @@
1
+ import { get, post, del } from '../api.js';
2
+ import { requireFeature } from '../subscription.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const ruleCommand = {
6
+ command: 'rule <subcommand>',
7
+ describe: 'Manage routing rules',
8
+ builder: (yargs) =>
9
+ yargs
10
+ .command(ruleListCommand)
11
+ .command(ruleCreateCommand)
12
+ .command(ruleDeleteCommand)
13
+ .demandCommand(1, 'Specify a subcommand: list, create, delete'),
14
+ handler: () => {},
15
+ };
16
+
17
+ // ── list ───────────────────────────────────────────────────────────────────────
18
+
19
+ const ruleListCommand = {
20
+ command: 'list',
21
+ describe: 'List all routing rules',
22
+ handler: runCommand(async () => {
23
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
24
+
25
+ const rules = await get('/Rules/List');
26
+ if (!rules?.length) { console.log('No routing rules found.'); return; }
27
+
28
+ const sorted = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
29
+ const idW = Math.max(...sorted.map(r => String(r.id).length + 2));
30
+ const priW = Math.max('pri'.length, ...sorted.map(r => String(r.priority ?? 0).length));
31
+ const nameW = Math.max('name'.length, ...sorted.map(r => (r.name ?? r.trigger ?? '—').length));
32
+
33
+ console.log(`\n${'id'.padStart(idW)} ${'pri'.padEnd(priW)} ${'name'.padEnd(nameW)} trigger`);
34
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(priW)} ${'-'.repeat(nameW)} -------`);
35
+ for (const r of sorted) {
36
+ const id = `[${r.id}]`.padStart(idW);
37
+ const pri = String(r.priority ?? 0).padEnd(priW);
38
+ const name = (r.name ?? r.trigger ?? '—').padEnd(nameW);
39
+ console.log(`${id} ${pri} ${name} ${r.trigger ?? '—'}`);
40
+ }
41
+ console.log(`\n${sorted.length} rule(s)`);
42
+ }),
43
+ };
44
+
45
+ // ── create ─────────────────────────────────────────────────────────────────────
46
+
47
+ const ruleCreateCommand = {
48
+ command: 'create',
49
+ describe: 'Create a routing rule',
50
+ builder: (y) =>
51
+ y
52
+ .option('name', { type: 'string', demandOption: true, describe: 'Rule name' })
53
+ .option('trigger', { type: 'string', demandOption: true, describe: 'Trigger type (e.g. pageUrl, country)' })
54
+ .option('value', { type: 'string', demandOption: true, describe: 'Trigger value to match' })
55
+ .option('team-id', { type: 'number', demandOption: true, describe: 'Team ID to route to' })
56
+ .option('priority', { type: 'number', default: 0, describe: 'Rule priority (lower = higher priority)' }),
57
+
58
+ handler: runCommand(async (argv) => {
59
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
60
+
61
+ const result = await post('/Rules', {
62
+ name: argv.name,
63
+ trigger: argv.trigger,
64
+ value: argv.value,
65
+ teamId: argv['team-id'],
66
+ priority: argv.priority,
67
+ isEnabled: true,
68
+ });
69
+
70
+ console.log(`Rule created: [${result.id}] ${result.name}`);
71
+ }),
72
+ };
73
+
74
+ // ── delete ─────────────────────────────────────────────────────────────────────
75
+
76
+ const ruleDeleteCommand = {
77
+ command: 'delete <id>',
78
+ describe: 'Delete a routing rule',
79
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Rule ID' }),
80
+ handler: runCommand(async (argv) => {
81
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
82
+ await del(`/Rules/${argv.id}`);
83
+ console.log(`Rule ${argv.id} deleted.`);
84
+ }),
85
+ };
@@ -0,0 +1,62 @@
1
+ import { get } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const FEATURE_MAP = {
5
+ enableWeb: 'Web Chat',
6
+ enableSms: 'SMS',
7
+ enableEmail: 'Email',
8
+ enableWhatsapp: 'WhatsApp',
9
+ enableIvr: 'IVR / Voice',
10
+ enableFacebook: 'Facebook Messenger',
11
+ enableInstagram: 'Instagram',
12
+ enableAI: 'AI Bots',
13
+ enableKnowledgeBase: 'Knowledge Base',
14
+ enableAutomation: 'Workflows',
15
+ enableWorkflowRules: 'Routing Rules',
16
+ enableChatTranslations: 'Chat Translation',
17
+ enableVideoChat: 'Video Chat',
18
+ enableTicketing: 'Ticketing',
19
+ enableAgentDashboard: 'Agent Dashboard',
20
+ enableMcpApi: 'MCP API Gateway',
21
+ enableOutboundCampaigns: 'Email Campaigns',
22
+ enableRcs: 'RCS Messaging',
23
+ enablePageWidgets: 'Page Widgets',
24
+ enableVisitorTracking: 'Visitor Tracking',
25
+ };
26
+
27
+ const STATUS_LABEL = { Available: 'online', Away: 'away', Offline: 'offline' };
28
+
29
+ export const siteCommand = {
30
+ command: 'site',
31
+ describe: 'Show site overview: active features and agent availability',
32
+ handler: runCommand(async () => {
33
+ const [sub, agents] = await Promise.all([
34
+ get('/Subscription'),
35
+ get('/Users/List'),
36
+ ]);
37
+
38
+ // Active features — friendly names only, no internal flag names
39
+ const active = Object.entries(FEATURE_MAP)
40
+ .filter(([key]) => sub[key] === true)
41
+ .map(([, label]) => label);
42
+
43
+ console.log('\nActive features:');
44
+ if (active.length) {
45
+ console.log(' ' + active.join(' · '));
46
+ } else {
47
+ console.log(' (none)');
48
+ }
49
+
50
+ if (sub.maxCreatedUsers) {
51
+ console.log(`\nAgent seats: ${sub.maxCreatedUsers} licensed`);
52
+ }
53
+
54
+ // Agent availability summary
55
+ const counts = { online: 0, away: 0, offline: 0 };
56
+ for (const a of agents) {
57
+ const key = STATUS_LABEL[a.status] ?? 'offline';
58
+ counts[key]++;
59
+ }
60
+ console.log(`Agent status: ${counts.online} online · ${counts.away} away · ${counts.offline} offline\n`);
61
+ }),
62
+ };
@@ -0,0 +1,144 @@
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const teamCommand = {
5
+ command: 'team <subcommand>',
6
+ describe: 'View teams and manage widget placements (deployments)',
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command(teamListCommand)
10
+ .command(widgetListCommand)
11
+ .command(widgetRenameCommand)
12
+ .command(widgetAssignCommand)
13
+ .demandCommand(1, 'Specify a subcommand: list, widget'),
14
+ handler: () => {},
15
+ };
16
+
17
+ // ── team list ──────────────────────────────────────────────────────────────────
18
+ // Shows teams and their widget placements inline — the mental model is:
19
+ // team = who handles conversations
20
+ // widget = which pages route to that team
21
+
22
+ const teamListCommand = {
23
+ command: 'list',
24
+ describe: 'List teams and their widget placements',
25
+ handler: runCommand(async () => {
26
+ const [teams, deployments] = await Promise.all([
27
+ get('/Teams/List'),
28
+ get('/Deployment'),
29
+ ]);
30
+
31
+ if (!teams?.length) {
32
+ console.log('No teams found.');
33
+ return;
34
+ }
35
+
36
+ // Group deployments by team
37
+ const byTeam = {};
38
+ for (const d of (deployments ?? [])) {
39
+ if (!byTeam[d.teamId]) byTeam[d.teamId] = [];
40
+ byTeam[d.teamId].push(d);
41
+ }
42
+
43
+ for (const t of teams) {
44
+ const widgets = byTeam[t.id] ?? [];
45
+ const routing = t.routingAction ? ` routing=${t.routingAction}` : '';
46
+ console.log(`\n[${t.id}] ${t.name}${routing}`);
47
+
48
+ if (widgets.length) {
49
+ for (const w of widgets) {
50
+ console.log(` widget [${w.id}] "${w.displayName ?? 'unnamed'}" key: ${w.deploymentId}`);
51
+ }
52
+ } else {
53
+ console.log(` (no widgets assigned — conversations can reach this team via routing rules)`);
54
+ }
55
+ }
56
+
57
+ console.log(`\n${teams.length} team(s) · ${(deployments ?? []).length} widget(s)`);
58
+ console.log('\nPaste a widget key into your page\'s embed snippet to route that page to its team.');
59
+ }),
60
+ };
61
+
62
+ // ── widget subcommands ─────────────────────────────────────────────────────────
63
+ // "Widget" is the user-facing term. Internally these are deployments.
64
+ // A widget is a snippet you paste on a page. The page routes to the widget's team.
65
+
66
+ const widgetListCommand = {
67
+ command: 'widget list',
68
+ describe: 'List all widgets (embed snippets) and which team each routes to',
69
+ handler: runCommand(async () => {
70
+ const [deployments, teams] = await Promise.all([
71
+ get('/Deployment'),
72
+ get('/Teams/List'),
73
+ ]);
74
+
75
+ if (!deployments?.length) {
76
+ console.log('No widgets found.');
77
+ console.log('\nCreate a widget in the Velaro admin under Deployments,');
78
+ console.log('then paste its embed snippet on the pages you want chat on.');
79
+ return;
80
+ }
81
+
82
+ const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
83
+ const rows = deployments.map(d => ({
84
+ id: `[${d.id}]`,
85
+ key: d.deploymentId ?? '—',
86
+ team: teamMap[d.teamId] ?? `team ${d.teamId}`,
87
+ name: d.displayName ?? '(unnamed)',
88
+ }));
89
+
90
+ const idW = Math.max(...rows.map(r => r.id.length));
91
+ const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
92
+ const teamW = Math.max('routes-to'.length, ...rows.map(r => r.team.length));
93
+
94
+ console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'routes-to'.padEnd(teamW)} name`);
95
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
96
+ for (const r of rows) {
97
+ console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
98
+ }
99
+ console.log(`\n${rows.length} widget(s)`);
100
+ }),
101
+ };
102
+
103
+ const widgetRenameCommand = {
104
+ command: 'widget rename <id> <name>',
105
+ describe: 'Rename a widget',
106
+ builder: (y) =>
107
+ y
108
+ .positional('id', { type: 'number', describe: 'Widget ID' })
109
+ .positional('name', { type: 'string', describe: 'New name' }),
110
+ handler: runCommand(async (argv) => {
111
+ const dep = await get(`/Deployment/${argv.id}`);
112
+ if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
113
+ await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
114
+ console.log(`Widget ${argv.id} renamed to "${argv.name}".`);
115
+ }),
116
+ };
117
+
118
+ const widgetAssignCommand = {
119
+ command: 'widget assign <id> <team-id>',
120
+ describe: 'Point a widget at a different team (embed code stays the same)',
121
+ builder: (y) =>
122
+ y
123
+ .positional('id', { type: 'number', describe: 'Widget ID' })
124
+ .positional('team-id', { type: 'number', describe: 'Team ID to route to' }),
125
+ handler: runCommand(async (argv) => {
126
+ const [dep, teams] = await Promise.all([
127
+ get(`/Deployment/${argv.id}`),
128
+ get('/Teams/List'),
129
+ ]);
130
+
131
+ if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
132
+
133
+ const team = (teams ?? []).find(t => t.id === argv['team-id']);
134
+ if (!team) {
135
+ console.error(`Team ${argv['team-id']} not found.`);
136
+ console.error('Run: velaro team list');
137
+ process.exit(1);
138
+ }
139
+
140
+ await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
141
+ console.log(`Widget "${dep.displayName ?? dep.id}" now routes to team "${team.name}".`);
142
+ console.log('Your embed snippet is unchanged — no website edits needed.');
143
+ }),
144
+ };
@@ -0,0 +1,47 @@
1
+ import { execSync } from 'child_process';
2
+ import { createRequire } from 'module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { version: currentVersion } = require('../../package.json');
6
+
7
+ const NPM_REGISTRY = 'https://registry.npmjs.org/@velaro/cli/latest';
8
+
9
+ export const updateCommand = {
10
+ command: 'update',
11
+ describe: 'Update the Velaro CLI to the latest version',
12
+ handler: async () => {
13
+ process.stdout.write('Checking for updates... ');
14
+
15
+ let latest;
16
+ try {
17
+ const res = await fetch(NPM_REGISTRY, { signal: AbortSignal.timeout(8000) });
18
+ const data = await res.json();
19
+ latest = data?.version;
20
+ } catch {
21
+ console.error('Could not reach npm registry. Check your connection and try again.');
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!latest) {
26
+ console.error('Could not determine the latest version.');
27
+ process.exit(1);
28
+ }
29
+
30
+ if (latest === currentVersion) {
31
+ console.log(`already up to date (${currentVersion}).`);
32
+ return;
33
+ }
34
+
35
+ console.log(`${currentVersion} → ${latest}`);
36
+ console.log('Running: npm install -g @velaro/cli@latest\n');
37
+
38
+ try {
39
+ execSync('npm install -g @velaro/cli@latest', { stdio: 'inherit' });
40
+ console.log('\nUpdated successfully. Run velaro --version to confirm.');
41
+ } catch {
42
+ console.error('\nUpdate failed. Try running manually:');
43
+ console.error(' npm install -g @velaro/cli@latest');
44
+ process.exit(1);
45
+ }
46
+ },
47
+ };
@@ -0,0 +1,98 @@
1
+ import { get, post } from '../api.js';
2
+ import { requireFeature } from '../subscription.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const workflowCommand = {
6
+ command: 'workflow <subcommand>',
7
+ describe: 'Manage automation workflows',
8
+ builder: (yargs) =>
9
+ yargs
10
+ .command(workflowListCommand)
11
+ .command(workflowGetCommand)
12
+ .command(workflowEnableCommand)
13
+ .command(workflowDisableCommand)
14
+ .demandCommand(1, 'Specify a subcommand: list, get, enable, disable'),
15
+ handler: () => {},
16
+ };
17
+
18
+ // ── list ───────────────────────────────────────────────────────────────────────
19
+
20
+ const workflowListCommand = {
21
+ command: 'list',
22
+ describe: 'List all workflows',
23
+ handler: runCommand(async () => {
24
+ await requireFeature('enableAutomation', 'Workflows');
25
+
26
+ const workflows = await get('/Workflows/List');
27
+ const rows = (workflows ?? []).filter(w => !w.isTemplate);
28
+
29
+ if (!rows.length) { console.log('No workflows found.'); return; }
30
+
31
+ const idW = Math.max(...rows.map(w => String(w.id).length + 2));
32
+ const triggerW = Math.max('trigger'.length, ...rows.map(w => (w.triggerType ?? '—').length));
33
+
34
+ console.log(`\n${'id'.padStart(idW)} status ${'trigger'.padEnd(triggerW)} name`);
35
+ console.log(`${'-'.repeat(idW)} -------- ${'-'.repeat(triggerW)} ----`);
36
+ for (const w of rows) {
37
+ const id = `[${w.id}]`.padStart(idW);
38
+ const status = w.enabled ? 'enabled ' : 'disabled';
39
+ const trigger = (w.triggerType ?? '—').padEnd(triggerW);
40
+ console.log(`${id} ${status} ${trigger} ${w.name}`);
41
+ }
42
+ console.log(`\n${rows.length} workflow(s)`);
43
+ }),
44
+ };
45
+
46
+ // ── get ────────────────────────────────────────────────────────────────────────
47
+
48
+ const workflowGetCommand = {
49
+ command: 'get <id>',
50
+ describe: 'Show workflow details',
51
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
52
+ handler: runCommand(async (argv) => {
53
+ await requireFeature('enableAutomation', 'Workflows');
54
+
55
+ const w = await get(`/Workflows/${argv.id}`);
56
+ if (!w) { console.error(`Workflow ${argv.id} not found.`); process.exit(1); }
57
+
58
+ console.log(`\n[${w.id}] ${w.name}`);
59
+ console.log(` Status: ${w.enabled ? 'enabled' : 'disabled'}`);
60
+ console.log(` Trigger: ${w.triggerType ?? '—'}`);
61
+ if (w.nodes?.length) {
62
+ console.log(` Nodes: ${w.nodes.length}`);
63
+ }
64
+ }),
65
+ };
66
+
67
+ // ── enable / disable ──────────────────────────────────────────────────────────
68
+
69
+ const workflowEnableCommand = {
70
+ command: 'enable <id>',
71
+ describe: 'Enable a workflow',
72
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
73
+ handler: runCommand(async (argv) => {
74
+ await requireFeature('enableAutomation', 'Workflows');
75
+ await post(`/Workflows/toggle/${argv.id}`, null);
76
+ console.log(`Workflow ${argv.id} enabled.`);
77
+ }),
78
+ };
79
+
80
+ const workflowDisableCommand = {
81
+ command: 'disable <id>',
82
+ describe: 'Disable a workflow',
83
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
84
+ handler: runCommand(async (argv) => {
85
+ await requireFeature('enableAutomation', 'Workflows');
86
+
87
+ const w = await get(`/Workflows/${argv.id}`);
88
+ if (!w) { console.error(`Workflow ${argv.id} not found.`); process.exit(1); }
89
+
90
+ if (!w.enabled) {
91
+ console.log(`Workflow ${argv.id} is already disabled.`);
92
+ return;
93
+ }
94
+
95
+ await post(`/Workflows/toggle/${argv.id}`, null);
96
+ console.log(`Workflow ${argv.id} disabled.`);
97
+ }),
98
+ };
package/lib/config.js CHANGED
@@ -1,25 +1,26 @@
1
- import { homedir } from 'os';
2
- import { join } from 'path';
3
- import { readFileSync, writeFileSync, mkdirSync } from 'fs';
4
-
5
- const CONFIG_DIR = join(homedir(), '.velaro');
6
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
-
8
- export const DEFAULT_API_BASE = process.env.VELARO_API_BASE || 'https://velaro-messaging-api-prod.azurewebsites.net';
9
-
10
- export function readConfig() {
11
- try {
12
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
13
- } catch {
14
- return {};
15
- }
16
- }
17
-
18
- export function writeConfig(data) {
19
- mkdirSync(CONFIG_DIR, { recursive: true }); // no-op if already exists
20
- writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
21
- }
22
-
23
- export function clearConfig() {
24
- writeConfig({});
25
- }
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.velaro');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ export const DEFAULT_API_BASE = process.env.VELARO_API_BASE || 'https://messaging.velaro.com';
9
+ export const STAGING_API_BASE = process.env.VELARO_STAGING_API || 'https://velaro-messaging-api-staging.azurewebsites.net';
10
+
11
+ export function readConfig() {
12
+ try {
13
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+
19
+ export function writeConfig(data) {
20
+ mkdirSync(CONFIG_DIR, { recursive: true }); // no-op if already exists
21
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
22
+ }
23
+
24
+ export function clearConfig() {
25
+ writeConfig({});
26
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Subscription helpers for CLI commands.
3
+ * Provides a clean gate that prints a plan upgrade message instead of
4
+ * throwing a confusing 403/400 from the API.
5
+ */
6
+
7
+ import { get } from './api.js';
8
+
9
+ let _cachedSub = null;
10
+
11
+ export async function getSubscription() {
12
+ if (!_cachedSub) {
13
+ _cachedSub = await get('/Subscription');
14
+ }
15
+ return _cachedSub;
16
+ }
17
+
18
+ /**
19
+ * Throws an Error with a friendly upgrade message if the feature flag is off.
20
+ * Usage: await requireFeature('enableAI', 'AI Bots');
21
+ */
22
+ export async function requireFeature(flag, featureName) {
23
+ const sub = await getSubscription();
24
+ if (!sub[flag]) {
25
+ throw new Error(
26
+ `${featureName} is not enabled on your plan.\n` +
27
+ ` Upgrade at https://velaro.com/pricing or contact sales@velaro.com.`
28
+ );
29
+ }
30
+ return sub;
31
+ }
32
+
33
+ /**
34
+ * Returns true/false without throwing — for check/status commands.
35
+ */
36
+ export async function hasFeature(flag) {
37
+ const sub = await getSubscription();
38
+ return sub[flag] === true;
39
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Non-blocking update checker.
3
+ * Fetches the latest version from npm once per day (cached in ~/.velaro/config.json).
4
+ * Prints a one-line notice AFTER the command completes if a newer version is available.
5
+ */
6
+
7
+ import { readConfig, writeConfig } from './config.js';
8
+ import { createRequire } from 'module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const { version: currentVersion } = require('../package.json');
12
+
13
+ const NPM_REGISTRY = 'https://registry.npmjs.org/@velaro/cli/latest';
14
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // once per day
15
+
16
+ /**
17
+ * Starts the update check in the background.
18
+ * Returns a function you await AFTER your command finishes to print any notice.
19
+ */
20
+ export function startUpdateCheck() {
21
+ const cfg = readConfig();
22
+ const lastCheck = cfg._lastUpdateCheck ?? 0;
23
+ const now = Date.now();
24
+
25
+ if (now - lastCheck < CHECK_INTERVAL_MS) {
26
+ // Use cached result — no network call
27
+ return async () => printNoticeIfStale(cfg._latestVersion);
28
+ }
29
+
30
+ // Fire off the network request without awaiting it here
31
+ const fetchPromise = fetch(NPM_REGISTRY, { signal: AbortSignal.timeout(4000) })
32
+ .then((r) => r.json())
33
+ .then((data) => {
34
+ const latest = data?.version ?? null;
35
+ writeConfig({ ...readConfig(), _lastUpdateCheck: now, _latestVersion: latest });
36
+ return latest;
37
+ })
38
+ .catch(() => null); // never block the CLI on a network failure
39
+
40
+ return async () => {
41
+ const latest = await fetchPromise;
42
+ printNoticeIfStale(latest);
43
+ };
44
+ }
45
+
46
+ function printNoticeIfStale(latest) {
47
+ if (!latest || latest === currentVersion) return;
48
+ if (!isNewer(latest, currentVersion)) return;
49
+
50
+ process.stderr.write(
51
+ `\n Update available: ${currentVersion} → ${latest}\n` +
52
+ ` Run: npm install -g @velaro/cli@latest\n\n`
53
+ );
54
+ }
55
+
56
+ function isNewer(a, b) {
57
+ const pa = a.split('.').map(Number);
58
+ const pb = b.split('.').map(Number);
59
+ for (let i = 0; i < 3; i++) {
60
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
61
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
62
+ }
63
+ return false;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Velaro Workspace v20 — command-line interface for managing bots, knowledge base ingestion, and MCP API keys.",
5
5
  "type": "module",
6
6
  "bin": {