@velaro/cli 0.7.0 → 1.4.8

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 (98) hide show
  1. package/README.md +161 -138
  2. package/bin/velaro.js +177 -62
  3. package/lib/api.js +91 -52
  4. package/lib/api.test.js +46 -0
  5. package/lib/banner.js +76 -0
  6. package/lib/commands/activity.js +133 -0
  7. package/lib/commands/acuity.js +66 -0
  8. package/lib/commands/agent.js +204 -50
  9. package/lib/commands/ai-config.js +193 -0
  10. package/lib/commands/ai-models.js +159 -0
  11. package/lib/commands/appointments.js +198 -0
  12. package/lib/commands/article.js +668 -388
  13. package/lib/commands/automation-draft.js +134 -0
  14. package/lib/commands/avatar.js +75 -0
  15. package/lib/commands/bigcommerce.js +50 -0
  16. package/lib/commands/billing-contacts.js +62 -0
  17. package/lib/commands/billing-email-preference.js +64 -0
  18. package/lib/commands/billing-subscription.js +265 -0
  19. package/lib/commands/billing.js +138 -0
  20. package/lib/commands/bot.js +141 -137
  21. package/lib/commands/bundle.js +168 -0
  22. package/lib/commands/calendly.js +62 -0
  23. package/lib/commands/callback.js +125 -0
  24. package/lib/commands/callrail.js +88 -0
  25. package/lib/commands/campaigns.js +44 -0
  26. package/lib/commands/case.js +102 -0
  27. package/lib/commands/check.js +163 -163
  28. package/lib/commands/compliance.js +229 -0
  29. package/lib/commands/conversation-efficiency.js +178 -0
  30. package/lib/commands/copilotstudio.js +114 -0
  31. package/lib/commands/coupon-grant.js +192 -0
  32. package/lib/commands/db.js +101 -0
  33. package/lib/commands/deployment.js +107 -107
  34. package/lib/commands/diagnostics.js +298 -0
  35. package/lib/commands/email-campaign.js +47 -0
  36. package/lib/commands/email-inbox.js +88 -0
  37. package/lib/commands/entitlement.js +176 -0
  38. package/lib/commands/env.js +45 -45
  39. package/lib/commands/feature-discovery.js +40 -0
  40. package/lib/commands/focus.js +278 -0
  41. package/lib/commands/index.js +38 -5
  42. package/lib/commands/ingest.js +31 -31
  43. package/lib/commands/inline-widget-config.js +126 -0
  44. package/lib/commands/integration.js +93 -0
  45. package/lib/commands/kb.js +450 -309
  46. package/lib/commands/login.js +86 -86
  47. package/lib/commands/logs.js +680 -0
  48. package/lib/commands/magento.js +210 -0
  49. package/lib/commands/mcp-key.js +188 -159
  50. package/lib/commands/migrate.js +134 -0
  51. package/lib/commands/migration-status.js +66 -0
  52. package/lib/commands/monday.js +137 -0
  53. package/lib/commands/netsuite.js +87 -0
  54. package/lib/commands/notifications.js +63 -0
  55. package/lib/commands/notion.js +70 -0
  56. package/lib/commands/ops.js +267 -173
  57. package/lib/commands/payment-recovery.js +170 -0
  58. package/lib/commands/pickup.js +172 -0
  59. package/lib/commands/pricing.js +132 -0
  60. package/lib/commands/product.js +55 -0
  61. package/lib/commands/recruiting.js +374 -0
  62. package/lib/commands/report.js +462 -0
  63. package/lib/commands/routing.js +304 -0
  64. package/lib/commands/rule.js +85 -85
  65. package/lib/commands/sharepoint.js +167 -0
  66. package/lib/commands/site-provision.js +68 -0
  67. package/lib/commands/site.js +62 -62
  68. package/lib/commands/sitesync.js +158 -0
  69. package/lib/commands/slack.js +64 -0
  70. package/lib/commands/squarespace.js +108 -0
  71. package/lib/commands/status.js +24 -24
  72. package/lib/commands/subscription.js +43 -0
  73. package/lib/commands/support.js +128 -0
  74. package/lib/commands/survey.js +216 -0
  75. package/lib/commands/team.js +144 -144
  76. package/lib/commands/teams-phone.js +131 -0
  77. package/lib/commands/teams.js +106 -0
  78. package/lib/commands/telephony.js +99 -0
  79. package/lib/commands/update.js +47 -47
  80. package/lib/commands/webflow.js +128 -0
  81. package/lib/commands/whoami.js +25 -22
  82. package/lib/commands/widget-container.js +152 -0
  83. package/lib/commands/woocommerce.js +240 -0
  84. package/lib/commands/workflow.js +233 -98
  85. package/lib/config.js +85 -83
  86. package/lib/kb-screenshot.js +320 -0
  87. package/lib/migrations/amscro.json +72 -0
  88. package/lib/migrations/azenta.json +68 -0
  89. package/lib/migrations/bluefire.json +49 -0
  90. package/lib/migrations/donaldson.json +75 -0
  91. package/lib/oauth.js +149 -135
  92. package/lib/run.js +21 -16
  93. package/lib/sharepoint-auth.js +138 -0
  94. package/lib/subscription.js +41 -39
  95. package/lib/track.js +35 -35
  96. package/lib/update-check.js +64 -64
  97. package/package.json +34 -19
  98. package/scripts/postinstall.js +12 -0
@@ -0,0 +1,138 @@
1
+ // velaro billing — plan status, contracts, invoices, QB sync, subscription flags
2
+ import { request } from '../api.js';
3
+
4
+ function fmtCents(cents) {
5
+ return `$${(cents / 100).toFixed(2)}`;
6
+ }
7
+
8
+ function fmtDate(iso) {
9
+ if (!iso) return '—';
10
+ return iso.slice(0, 10);
11
+ }
12
+
13
+ // ── plan-status ───────────────────────────────────────────────────────────────
14
+
15
+ async function planStatus() {
16
+ const s = await request('GET', '/BillingPayment/plan-status');
17
+
18
+ console.log(`\nPlan Status — Site ${s.siteId}\n${'─'.repeat(50)}`);
19
+
20
+ if (!s.subscription) {
21
+ console.log(' No subscription record found.');
22
+ } else {
23
+ const sub = s.subscription;
24
+ console.log(` Seats: ${sub.maxLicensedSeats}`);
25
+ console.log(` Conv/mo: ${sub.maxConversationsPerMonth || '∞'}`);
26
+ console.log(` Self-serve: ${sub.enableSelfServeSeats ? 'YES' : 'no'}`);
27
+ console.log(`\n Active flags (${sub.enabledFlags.length}):`);
28
+ if (sub.enabledFlags.length === 0) {
29
+ console.log(' none');
30
+ } else {
31
+ for (const f of sub.enabledFlags) console.log(` ✓ ${f}`);
32
+ }
33
+ }
34
+
35
+ if (s.recentSessions?.length) {
36
+ console.log(`\n Recent Stripe Sessions:`);
37
+ for (const p of s.recentSessions) {
38
+ console.log(` [${p.id}] ${fmtCents(p.amountCents)} ${p.status} ${fmtDate(p.createdAt)}`);
39
+ }
40
+ }
41
+
42
+ if (s.recentInvoices?.length) {
43
+ console.log(`\n Recent Invoices:`);
44
+ for (const i of s.recentInvoices) {
45
+ const qb = i.qboSyncStatus || 'not pushed';
46
+ const err = i.qboLastSyncError ? ` ERR: ${i.qboLastSyncError.slice(0, 60)}` : '';
47
+ console.log(` ${i.invoiceNumber || i.id} ${fmtCents(i.totalCents)} ${i.status} QB:${qb}${err}`);
48
+ }
49
+ }
50
+
51
+ if (s.deferredSchedules?.length) {
52
+ console.log(`\n Deferred Revenue Schedules:`);
53
+ for (const d of s.deferredSchedules) {
54
+ const pct = d.totalCents > 0 ? Math.round((d.recognizedCents / d.totalCents) * 100) : 0;
55
+ console.log(` ${d.startMonth}→${d.endMonth} ${fmtCents(d.totalCents)} ${pct}% recognized ${d.status}`);
56
+ }
57
+ }
58
+ console.log('');
59
+ }
60
+
61
+ // ── contracts ─────────────────────────────────────────────────────────────────
62
+
63
+ async function listContracts() {
64
+ const contracts = await request('GET', '/api/Contract');
65
+ if (!Array.isArray(contracts) || !contracts.length) {
66
+ console.log('No contracts found.');
67
+ return;
68
+ }
69
+ console.log(`\nContracts (${contracts.length})\n${'─'.repeat(60)}`);
70
+ for (const c of contracts) {
71
+ const monthly = c.totalMonthlyValue ? ` $${Number(c.totalMonthlyValue).toFixed(2)}/mo` : '';
72
+ const signed = c.signedAt ? ` signed ${fmtDate(c.signedAt)}` : '';
73
+ console.log(` [${c.id}] ${c.status.padEnd(8)} "${c.title}" ${c.customerEmail || ''}${monthly}${signed}`);
74
+ }
75
+ console.log('');
76
+ }
77
+
78
+ async function showContract(id) {
79
+ const c = await request('GET', `/api/Contract/${id}`);
80
+ console.log(`\nContract ${c.id}: "${c.title}"\n${'─'.repeat(50)}`);
81
+ console.log(` Status: ${c.status}`);
82
+ console.log(` Customer: ${c.customerName || '—'} <${c.customerEmail || '—'}>`);
83
+ if (c.customerCompany) console.log(` Company: ${c.customerCompany}`);
84
+ console.log(` Monthly: $${Number(c.totalMonthlyValue || 0).toFixed(2)}/mo Setup: $${Number(c.setupFee || 0).toFixed(2)}`);
85
+ console.log(` Service: ${fmtDate(c.serviceStartDate)} → ${fmtDate(c.serviceEndDate)}`);
86
+ console.log(` Activation: ${c.subscriptionActivationMode} activated: ${fmtDate(c.subscriptionActivatedAt)}`);
87
+ console.log(` QB Customer: ${c.qboCustomerId || 'not synced'}`);
88
+ console.log(` QB Invoice: ${c.qboInvoiceId || '—'}`);
89
+ if (c.signedAt) console.log(` Signed: ${fmtDate(c.signedAt)} by ${c.signedByEmail}`);
90
+ if (c.cancellationRequestedAt) console.log(` ⚠️ Cancelled: ${fmtDate(c.cancellationRequestedAt)} by ${c.cancellationRequestedBy}`);
91
+ if (c.amendments?.length) console.log(` Amendments: ${c.amendments.length}`);
92
+ console.log('');
93
+ }
94
+
95
+ async function sendContract(id) {
96
+ await request('POST', `/api/Contract/${id}/send`);
97
+ console.log(`✅ Contract ${id} sent — signing link dispatched to customer.`);
98
+ }
99
+
100
+ // ── invoices ──────────────────────────────────────────────────────────────────
101
+
102
+ async function listInvoices() {
103
+ const invoices = await request('GET', '/api/BillingInvoiceRecord');
104
+ if (!Array.isArray(invoices) || !invoices.length) {
105
+ console.log('No invoices found.');
106
+ return;
107
+ }
108
+ console.log(`\nInvoices (${invoices.length})\n${'─'.repeat(70)}`);
109
+ for (const i of invoices) {
110
+ const qb = `QB:${(i.qboSyncStatus || 'not pushed').padEnd(10)}`;
111
+ const attempts = i.qboSyncAttempts > 0 ? ` (${i.qboSyncAttempts}x)` : '';
112
+ console.log(` [${i.id}] ${(i.invoiceNumber || '').padEnd(16)} ${fmtCents(i.totalCents).padEnd(10)} ${i.status.padEnd(8)} ${qb}${attempts} ${fmtDate(i.createdAt)}`);
113
+ if (i.qboLastSyncError) console.log(` QB error: ${i.qboLastSyncError.slice(0, 80)}`);
114
+ }
115
+ console.log('');
116
+ }
117
+
118
+ async function dispatchInvoice(id) {
119
+ await request('POST', `/api/BillingInvoiceRecord/${id}/dispatch`);
120
+ console.log(`✅ Invoice ${id} dispatched — email sent and status set to open.`);
121
+ }
122
+
123
+ // ── command export ────────────────────────────────────────────────────────────
124
+
125
+ export const billingCommand = {
126
+ command: 'billing <subcommand>',
127
+ describe: 'Billing operations: plan status, contracts, invoices, QB sync',
128
+ builder: (yargs) =>
129
+ yargs
130
+ .command('plan-status', 'Show current plan flags, seats, QB sync state, and deferred revenue', {}, () => planStatus())
131
+ .command('contracts', 'List all contracts', {}, () => listContracts())
132
+ .command('contract <id>', 'Show a specific contract', {}, (args) => showContract(args.id))
133
+ .command('contract-send <id>', 'Send a contract to the customer for signing', {}, (args) => sendContract(args.id))
134
+ .command('invoices', 'List invoice records with QB sync status', {}, () => listInvoices())
135
+ .command('invoice-dispatch <id>', 'Email an invoice to the customer (sets status to open)', {}, (args) => dispatchInvoice(args.id))
136
+ .demandCommand(1, 'Specify a subcommand: plan-status | contracts | contract <id> | contract-send <id> | invoices | invoice-dispatch <id>'),
137
+ handler: () => {},
138
+ };
@@ -1,137 +1,141 @@
1
- import { readFileSync } from 'fs';
2
- import { get, post } from '../api.js';
3
- import { requireFeature } from '../subscription.js';
4
- import { runCommand } from '../run.js';
5
-
6
- export const botCommand = {
7
- command: 'bot <subcommand>',
8
- describe: 'Manage AI bots',
9
- builder: (yargs) =>
10
- yargs
11
- .command(botListCommand)
12
- .command(botGetCommand)
13
- .command(botSetupCommand)
14
- .command(botPromptCommand)
15
- .demandCommand(1, 'Specify a subcommand: list, get, setup, prompt'),
16
- handler: () => {},
17
- };
18
-
19
- // ── list ───────────────────────────────────────────────────────────────────────
20
-
21
- const botListCommand = {
22
- command: 'list',
23
- describe: 'List AI bots on your site',
24
- handler: runCommand(async () => {
25
- await requireFeature('enableAI', 'AI Bots');
26
-
27
- const bots = await get('/AIConfiguration/list');
28
- if (!bots?.length) {
29
- console.log('No bots found. Create one with: velaro bot setup --name "My Bot"');
30
- return;
31
- }
32
- console.log(`Found ${bots.length} bot(s):\n`);
33
- for (const b of bots) {
34
- console.log(` [${b.id}] ${b.name} model=${b.aiModel ?? '(default)'} status=${b.status ?? 'ready'}`);
35
- }
36
- }),
37
- };
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
-
67
- const botSetupCommand = {
68
- command: 'setup',
69
- describe: 'Create or update an AI bot (upsert by name)',
70
- builder: (y) =>
71
- y
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'),
80
-
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
-
90
- const payload = {
91
- name: argv.name,
92
- aiModel: argv.model,
93
- tone: argv.tone,
94
- replyLength: argv['reply-length'],
95
- };
96
- if (promptText) payload.prompt = promptText;
97
- if (argv.urls) payload.dataUrls = argv.urls.split(',').map((u) => u.trim()).filter(Boolean);
98
-
99
- const result = await post('/AIConfiguration', payload);
100
- console.log(`Saved: [${result.id}] ${result.name}`);
101
- }),
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
- };
1
+ import { readFileSync } from 'fs';
2
+ import { messagingGet as get, messagingPost as post } from '../api.js';
3
+ import { requireFeature } from '../subscription.js';
4
+ import { runCommand } from '../run.js';
5
+
6
+ export const botCommand = {
7
+ command: 'bot <subcommand>',
8
+ describe: 'Manage AI bots',
9
+ builder: (yargs) =>
10
+ yargs
11
+ .command(botListCommand)
12
+ .command(botGetCommand)
13
+ .command(botSetupCommand)
14
+ .command(botPromptCommand)
15
+ .demandCommand(1, 'Specify a subcommand: list, get, setup, prompt'),
16
+ handler: () => {},
17
+ };
18
+
19
+ // ── list ───────────────────────────────────────────────────────────────────────
20
+
21
+ const botListCommand = {
22
+ command: 'list',
23
+ describe: 'List AI bots on your site',
24
+ handler: runCommand(async () => {
25
+ await requireFeature('enableAI', 'AI Bots');
26
+
27
+ const bots = await get('/AIConfiguration/list');
28
+ if (!bots?.length) {
29
+ console.log('No bots found. Create one with: velaro bot setup --name "My Bot"');
30
+ return;
31
+ }
32
+ console.log(`Found ${bots.length} bot(s):\n`);
33
+ for (const b of bots) {
34
+ console.log(` [${b.id}] ${b.name} model=${b.aiModel ?? '(default)'} status=${b.status ?? 'ready'}`);
35
+ }
36
+ }),
37
+ };
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
+ // No GET /AIConfiguration/{id} route exists -- list + filter client-side, same pattern
49
+ // ai-config.js already uses.
50
+ const all = (await get('/AIConfiguration/list')) ?? [];
51
+ const bot = all.find((x) => x.id === argv.id);
52
+ if (!bot) { console.error(`Bot ${argv.id} not found.`); process.exitCode = 1; return; }
53
+
54
+ console.log(`\n[${bot.id}] ${bot.name}`);
55
+ console.log(` Model: ${bot.aiModel ?? '(default)'}`);
56
+ console.log(` Tone: ${bot.tone ?? '—'}`);
57
+ console.log(` Reply length: ${bot.replyLength ?? '—'}`);
58
+ console.log(` Status: ${bot.status ?? 'ready'}`);
59
+ if (bot.prompt) {
60
+ console.log(`\nSystem prompt:\n`);
61
+ console.log(bot.prompt);
62
+ } else {
63
+ console.log(`\nSystem prompt: (none)`);
64
+ }
65
+ }),
66
+ };
67
+
68
+ // ── setup ──────────────────────────────────────────────────────────────────────
69
+
70
+ const botSetupCommand = {
71
+ command: 'setup',
72
+ describe: 'Create or update an AI bot (upsert by name)',
73
+ builder: (y) =>
74
+ y
75
+ .option('name', { describe: 'Bot name', type: 'string', demandOption: true })
76
+ .option('prompt', { describe: 'System prompt text (inline)', type: 'string' })
77
+ .option('prompt-file', { describe: 'Path to a file containing the system prompt', type: 'string' })
78
+ .option('model', { describe: 'AI model', choices: ['velaro-gpt-4o-mini','velaro-gpt-4o'], type: 'string', default: 'velaro-gpt-4o-mini' })
79
+ .option('tone', { describe: 'Response tone', choices: ['Professional','Friendly','Formal','Casual'], default: 'Professional' })
80
+ .option('reply-length', { describe: 'Response length', choices: ['Short','Medium','Long'], default: 'Medium' })
81
+ .option('urls', { describe: 'Comma-separated data source URLs', type: 'string' })
82
+ .conflicts('prompt', 'prompt-file'),
83
+
84
+ handler: runCommand(async (argv) => {
85
+ await requireFeature('enableAI', 'AI Bots');
86
+
87
+ let promptText = argv.prompt;
88
+ if (argv['prompt-file']) {
89
+ promptText = readFileSync(argv['prompt-file'], 'utf8').trim();
90
+ console.log(`Read ${promptText.length} chars from ${argv['prompt-file']}`);
91
+ }
92
+
93
+ const payload = {
94
+ name: argv.name,
95
+ aiModel: argv.model,
96
+ tone: argv.tone,
97
+ replyLength: argv['reply-length'],
98
+ };
99
+ if (promptText) payload.prompt = promptText;
100
+ if (argv.urls) payload.dataUrls = argv.urls.split(',').map((u) => u.trim()).filter(Boolean);
101
+
102
+ const result = await post('/AIConfiguration', payload);
103
+ console.log(`Saved: [${result.id}] ${result.name}`);
104
+ }),
105
+ };
106
+
107
+ // ── prompt ─────────────────────────────────────────────────────────────────────
108
+
109
+ const botPromptCommand = {
110
+ command: 'prompt <id>',
111
+ describe: 'Get or update a bot\'s system prompt',
112
+ builder: (y) =>
113
+ y
114
+ .positional('id', { type: 'number', describe: 'Bot ID' })
115
+ .option('set', { describe: 'New prompt text (inline)', type: 'string' })
116
+ .option('file', { describe: 'Path to a .txt/.md prompt file', type: 'string' })
117
+ .conflicts('set', 'file'),
118
+
119
+ handler: runCommand(async (argv) => {
120
+ await requireFeature('enableAI', 'AI Bots');
121
+
122
+ const all = (await get('/AIConfiguration/list')) ?? [];
123
+ const bot = all.find((x) => x.id === argv.id);
124
+ if (!bot) { console.error(`Bot ${argv.id} not found.`); process.exitCode = 1; return; }
125
+
126
+ if (!argv.set && !argv.file) {
127
+ // Read mode — print current prompt
128
+ console.log(bot.prompt ?? '(no system prompt set)');
129
+ return;
130
+ }
131
+
132
+ let promptText = argv.set;
133
+ if (argv.file) {
134
+ promptText = readFileSync(argv.file, 'utf8').trim();
135
+ console.log(`Read ${promptText.length} chars from ${argv.file}`);
136
+ }
137
+
138
+ await post('/AIConfiguration', { ...bot, prompt: promptText });
139
+ console.log(`Bot [${argv.id}] "${bot.name}" prompt updated (${promptText.length} chars).`);
140
+ }),
141
+ };
@@ -0,0 +1,168 @@
1
+ // velaro bundle — manage Chat Experience Bundles (Bundle/* on velaro-messaging).
2
+ // Site isolation is enforced server-side by BundleController's [Authorize]-scoped
3
+ // SiteId (ClientControllerBase) — never passed by the client.
4
+ import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
5
+ import { runCommand } from '../run.js';
6
+
7
+ const get = messagingGet;
8
+ const post = messagingPost;
9
+ const put = messagingPut;
10
+ const del = messagingDel;
11
+
12
+ export const bundleCommand = {
13
+ command: 'bundle <subcommand>',
14
+ describe: 'Manage Chat Experience Bundles — list, get, create, update, delete',
15
+ builder: (yargs) =>
16
+ yargs
17
+ .command(bundleListCommand)
18
+ .command(bundleGetCommand)
19
+ .command(bundleGetDefaultCommand)
20
+ .command(bundleCreateCommand)
21
+ .command(bundleUpdateCommand)
22
+ .command(bundleDeleteCommand)
23
+ .demandCommand(1, 'Specify a subcommand: list, get, get-default, create, update, delete'),
24
+ handler: () => {},
25
+ };
26
+
27
+ function printBundle(b) {
28
+ console.log(` id: ${b.id}`);
29
+ console.log(` name: ${b.name}`);
30
+ console.log(` isDefault: ${b.isDefault}`);
31
+ console.log(` chatWindowId: ${b.chatWindowId ?? '(none)'}`);
32
+ console.log(` preChatSurveyId: ${b.preChatSurveyId ?? '(none)'}`);
33
+ console.log(` postChatSurveyId: ${b.postChatSurveyId ?? '(none)'}`);
34
+ console.log(` offlineSurveyId: ${b.offlineSurveyId ?? '(none)'}`);
35
+ console.log(` defaultTeamId: ${b.defaultTeamId ?? '(none)'}`);
36
+ console.log(` personaId: ${b.personaId ?? '(none)'}`);
37
+ console.log(` aiConfigurationId: ${b.aiConfigurationId ?? '(none)'}`);
38
+ console.log(` showOnClosedLauncher: ${b.showOnClosedLauncher}`);
39
+ console.log(` createdUtc: ${b.createdUtc}`);
40
+ console.log(` modifiedUtc: ${b.modifiedUtc}`);
41
+ }
42
+
43
+ // ── list ─────────────────────────────────────────────────────────────────────
44
+
45
+ const bundleListCommand = {
46
+ command: 'list',
47
+ describe: 'List all Chat Experience Bundles on the current site',
48
+ handler: runCommand(async () => {
49
+ const bundles = (await get('/Bundle/list')) ?? [];
50
+ if (!bundles.length) { console.log('No experience bundles found.'); return; }
51
+
52
+ const idW = Math.max(2, ...bundles.map((b) => String(b.id).length));
53
+ console.log(`\n${'id'.padStart(idW)} name`);
54
+ console.log(`${'-'.repeat(idW)} ----`);
55
+ for (const b of bundles) {
56
+ const defaultNote = b.isDefault ? ' [default]' : '';
57
+ const closedNote = b.showOnClosedLauncher ? ' [shows on closed launcher]' : '';
58
+ console.log(`${String(b.id).padStart(idW)} ${b.name ?? '(unnamed)'}${defaultNote}${closedNote}`);
59
+ }
60
+ console.log(`\n${bundles.length} bundle(s)`);
61
+ }),
62
+ };
63
+
64
+ // ── get ──────────────────────────────────────────────────────────────────────
65
+
66
+ const bundleGetCommand = {
67
+ command: 'get <id>',
68
+ describe: 'Get one Chat Experience Bundle by ID',
69
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Bundle ID' }),
70
+ handler: runCommand(async (argv) => {
71
+ const bundle = await get(`/Bundle/get/${argv.id}`);
72
+ printBundle(bundle);
73
+ }),
74
+ };
75
+
76
+ // ── get-default ──────────────────────────────────────────────────────────────
77
+
78
+ const bundleGetDefaultCommand = {
79
+ command: 'get-default',
80
+ describe: "Get the current site's default Chat Experience Bundle",
81
+ handler: runCommand(async () => {
82
+ const bundle = await get('/Bundle/default');
83
+ printBundle(bundle);
84
+ }),
85
+ };
86
+
87
+ // ── create ───────────────────────────────────────────────────────────────────
88
+
89
+ function buildInputFromArgv(argv) {
90
+ const input = { name: argv.name };
91
+ if (argv.isDefault !== undefined) input.isDefault = argv.isDefault;
92
+ if (argv.chatWindowId !== undefined) input.chatWindowId = argv.chatWindowId;
93
+ if (argv.preChatSurveyId !== undefined) input.preChatSurveyId = argv.preChatSurveyId;
94
+ if (argv.postChatSurveyId !== undefined) input.postChatSurveyId = argv.postChatSurveyId;
95
+ if (argv.offlineSurveyId !== undefined) input.offlineSurveyId = argv.offlineSurveyId;
96
+ if (argv.defaultTeamId !== undefined) input.defaultTeamId = argv.defaultTeamId;
97
+ if (argv.personaId !== undefined) input.personaId = argv.personaId;
98
+ if (argv.aiConfigurationId !== undefined) input.aiConfigurationId = argv.aiConfigurationId;
99
+ // showOnClosedLauncher is nullable/apply-if-present server-side (ExperienceBundleInput
100
+ // doc comment) so it must only be sent when the caller actually passed the flag —
101
+ // never default it to false, which would silently revert an opted-in site.
102
+ if (argv.showOnClosedLauncher !== undefined) input.showOnClosedLauncher = argv.showOnClosedLauncher;
103
+ return input;
104
+ }
105
+
106
+ const bundleInputOptions = (y) =>
107
+ y
108
+ .option('name', { type: 'string', describe: 'Bundle name' })
109
+ .option('is-default', { type: 'boolean', describe: 'Make this the site default bundle' })
110
+ .option('chat-window-id', { type: 'number', describe: 'ChatWindow ID' })
111
+ .option('pre-chat-survey-id', { type: 'number', describe: 'Pre-chat Survey ID' })
112
+ .option('post-chat-survey-id', { type: 'number', describe: 'Post-chat Survey ID' })
113
+ .option('offline-survey-id', { type: 'number', describe: 'Offline Survey ID' })
114
+ .option('default-team-id', { type: 'number', describe: 'Default Team ID' })
115
+ .option('persona-id', { type: 'number', describe: 'Persona ID' })
116
+ .option('ai-configuration-id', { type: 'number', describe: 'AI Configuration ID' })
117
+ .option('show-on-closed-launcher', { type: 'boolean', describe: 'Show the postchat/offline survey on the closed launcher (AM-4013 Part 6)' });
118
+
119
+ const bundleCreateCommand = {
120
+ command: 'create',
121
+ describe: 'Create a new Chat Experience Bundle',
122
+ builder: (y) => bundleInputOptions(y).demandOption('name', 'Bundle name is required'),
123
+ handler: runCommand(async (argv) => {
124
+ const bundle = await post('/Bundle/create', buildInputFromArgv(argv));
125
+ console.log(`Created bundle ${bundle.id}.`);
126
+ printBundle(bundle);
127
+ }),
128
+ };
129
+
130
+ // ── update ───────────────────────────────────────────────────────────────────
131
+
132
+ const UPDATABLE_FIELDS = [
133
+ 'isDefault', 'chatWindowId', 'preChatSurveyId', 'postChatSurveyId',
134
+ 'offlineSurveyId', 'defaultTeamId', 'personaId', 'aiConfigurationId',
135
+ ];
136
+
137
+ const bundleUpdateCommand = {
138
+ command: 'update <id>',
139
+ describe: 'Update an existing Chat Experience Bundle, including toggling --show-on-closed-launcher (AM-4013 Part 6). Only the flags you pass are changed — every other field is read from the bundle\'s current state first and re-sent unchanged, because ExperienceBundleService.UpdateBundleAsync applies those fields unconditionally and would otherwise null them out.',
140
+ builder: (y) => bundleInputOptions(y.positional('id', { type: 'number', describe: 'Bundle ID' })),
141
+ handler: runCommand(async (argv) => {
142
+ const current = await get(`/Bundle/get/${argv.id}`);
143
+
144
+ const input = { name: argv.name !== undefined ? argv.name : current.name };
145
+ for (const field of UPDATABLE_FIELDS) {
146
+ input[field] = argv[field] !== undefined ? argv[field] : current[field];
147
+ }
148
+ // showOnClosedLauncher is the one nullable/apply-if-present field server-side
149
+ // (ExperienceBundleInput doc comment) — only send it when explicitly passed.
150
+ if (argv.showOnClosedLauncher !== undefined) input.showOnClosedLauncher = argv.showOnClosedLauncher;
151
+
152
+ const bundle = await put(`/Bundle/update/${argv.id}`, input);
153
+ console.log(`Updated bundle ${bundle.id}.`);
154
+ printBundle(bundle);
155
+ }),
156
+ };
157
+
158
+ // ── delete ───────────────────────────────────────────────────────────────────
159
+
160
+ const bundleDeleteCommand = {
161
+ command: 'delete <id>',
162
+ describe: 'Delete a Chat Experience Bundle (the default bundle cannot be deleted)',
163
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Bundle ID' }),
164
+ handler: runCommand(async (argv) => {
165
+ await del(`/Bundle/delete/${argv.id}`);
166
+ console.log(`Deleted bundle ${argv.id}.`);
167
+ }),
168
+ };