@velaro/cli 1.4.105 → 1.4.107

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
@@ -12,6 +12,7 @@ import { ingestCommand } from '../lib/commands/ingest.js';
12
12
  import { indexMoveCommand } from '../lib/commands/index-move.js';
13
13
  import { safetyCommand } from '../lib/commands/safety.js';
14
14
  import { knowledgeAdvisorCommand } from '../lib/commands/knowledge-advisor.js';
15
+ import { knowledgeCommand } from '../lib/commands/knowledge.js';
15
16
  import { productCommand } from '../lib/commands/product.js';
16
17
  import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
17
18
  import { opsCommand } from '../lib/commands/ops.js';
@@ -63,6 +64,7 @@ import { keywordIntelligenceCommand } from '../lib/commands/keyword-intelligence
63
64
  import { aiFieldExtractionCommand } from '../lib/commands/ai-field-extraction.js';
64
65
  import { sentimentCommand, qualityCommand, chatIntelligenceThemesCommand } from '../lib/commands/quality-reports.js';
65
66
  import { cannedLearningCommand } from '../lib/commands/canned-learning.js';
67
+ import { csatOverrideUnapproveCommand } from '../lib/commands/csat-override.js';
66
68
  import { chatIntelligenceTopicsCommand } from '../lib/commands/chat-intelligence-topics.js';
67
69
  import { focusCommand } from '../lib/commands/focus.js';
68
70
  import { triageCommand } from '../lib/commands/triage.js';
@@ -108,6 +110,7 @@ import { supportCommand } from '../lib/commands/support.js';
108
110
  import { routingCommand } from '../lib/commands/routing.js';
109
111
  import { diagnosticsCommand } from '../lib/commands/diagnostics.js';
110
112
  import { aiSpendCommand } from '../lib/commands/ai-spend.js';
113
+ import { skillUsageCommand } from '../lib/commands/skill-usage.js';
111
114
  import { aiFederationCommand } from '../lib/commands/ai-federation.js';
112
115
  import { statusPageCommand } from '../lib/commands/status-page.js';
113
116
  import { statusNotifyCommand } from '../lib/commands/status-notify.js';
@@ -116,6 +119,7 @@ import { emailInboxCommand } from '../lib/commands/email-inbox.js';
116
119
  import { callbackCommand } from '../lib/commands/callback.js';
117
120
  import { caseCommand } from '../lib/commands/case.js';
118
121
  import { pickupCommand } from '../lib/commands/pickup.js';
122
+ import { visitorMonitorCommand } from '../lib/commands/visitor-monitor.js';
119
123
  import { visitorLinkCommand } from '../lib/commands/visitor-link.js';
120
124
  import { applePayCommand } from '../lib/commands/apple-pay.js';
121
125
  import { siteBuilderCommand } from '../lib/commands/site-builder.js';
@@ -126,12 +130,18 @@ import { bundleCommand } from '../lib/commands/bundle.js';
126
130
  import { chatWindowCommand } from '../lib/commands/chat-window.js';
127
131
  import { featureDiscoveryCommand } from '../lib/commands/feature-discovery.js';
128
132
  import { tourPolicyCommand } from '../lib/commands/tour-policy.js';
133
+ import { livefluenceCommand, missedCommand, serviceCommand, pageCommand, uploadedCommand, msCommand, outlookCommand, oauthCommand, provisionCommand, sseCommand, skillCommand, v10Command, whatsappCommand, mobileCommand, signalrCommand, supportGroupCommand, pricingGroupCommand, scraperGroupCommand, tourPolicyGroupCommand } from '../lib/commands/parity-staff-tools.js';
129
134
  import { scraperCommand } from '../lib/commands/scraper.js';
130
135
  import { usageCommand } from '../lib/commands/usage.js';
131
136
  import { campaignsCommand } from '../lib/commands/campaigns.js';
132
137
  import { senderDomainHealthCommand } from '../lib/commands/sender-domain-health.js';
133
138
  import { contactsCommand } from '../lib/commands/contacts.js';
134
139
  import { contactListCommand } from '../lib/commands/contact-list.js';
140
+ import { recycleBinCommand, aiUsageCommand, siteInfoCommand, billingReceiptsCommand } from '../lib/commands/endpoint-parity.js';
141
+ import { adminCommand, appleCommand, forceExpireSiteCommand, backfillCommand, kpiCommand, auditTrailCommand, codeCommand, crmCommand, hangfireCommand } from '../lib/commands/diag-groups.js';
142
+ import { aiFollowupsCommand, businessActionCommand, callCommand, conversationCommand, conversionCommand, emailCommand, ivrCommand } from '../lib/commands/ops-groups.js';
143
+ import { entitlementGroup } from '../lib/commands/entitlement.js';
144
+ import { featureDiscoveryGroup } from '../lib/commands/feature-discovery.js';
135
145
  import { startUpdateCheck } from '../lib/update-check.js';
136
146
  import { printBanner, printBannerIfFirstRun } from '../lib/banner.js';
137
147
  import { createRequire } from 'module';
@@ -159,6 +169,7 @@ await yargs(hideBin(process.argv))
159
169
  .command(indexMoveCommand)
160
170
  .command(safetyCommand)
161
171
  .command(knowledgeAdvisorCommand)
172
+ .command(knowledgeCommand)
162
173
  .command(productCommand)
163
174
  .command(mcpKeyCommand)
164
175
  .command(opsCommand)
@@ -213,6 +224,7 @@ await yargs(hideBin(process.argv))
213
224
  .command(chatIntelligenceThemesCommand)
214
225
  .command(cannedLearningCommand)
215
226
  .command(chatIntelligenceTopicsCommand)
227
+ .command(csatOverrideUnapproveCommand)
216
228
  .command(focusCommand)
217
229
  .command(triageCommand)
218
230
  .command(automationDraftCommand)
@@ -250,13 +262,14 @@ await yargs(hideBin(process.argv))
250
262
  .command(signCommand)
251
263
  .command(widgetContainerCommand)
252
264
  .command(inlineWidgetConfigCommand)
253
- .command({ command: 'pricing', desc: 'View and manage the pricing catalog', builder: pricingCommand, handler: () => {} })
254
- .command({ command: 'entitlement', desc: 'Query and manage site feature entitlements (superadmin only)', builder: entitlementCommand, handler: () => {} })
265
+ .command(pricingGroupCommand)
266
+ .command(entitlementGroup)
255
267
  .command({ command: 'subscription', desc: 'Apply/diff SubscriptionPackage packages on a site (superadmin only)', builder: subscriptionCommand, handler: () => {} })
256
- .command({ command: 'support', desc: 'Staff SupportTools actions on a site (superadmin only)', builder: supportCommand, handler: () => {} })
268
+ .command(supportGroupCommand)
257
269
  .command(routingCommand)
258
270
  .command(diagnosticsCommand)
259
271
  .command(aiSpendCommand)
272
+ .command(skillUsageCommand)
260
273
  .command(aiFederationCommand)
261
274
  .command(statusPageCommand)
262
275
  .command(statusNotifyCommand)
@@ -265,6 +278,7 @@ await yargs(hideBin(process.argv))
265
278
  .command(callbackCommand)
266
279
  .command(caseCommand)
267
280
  .command(pickupCommand)
281
+ .command(visitorMonitorCommand)
268
282
  .command(visitorLinkCommand)
269
283
  .command(applePayCommand)
270
284
  .command(siteBuilderCommand)
@@ -273,14 +287,49 @@ await yargs(hideBin(process.argv))
273
287
  .command(surveyCommand)
274
288
  .command(bundleCommand)
275
289
  .command(chatWindowCommand)
276
- .command({ command: 'feature-discovery', desc: 'View and act on feature-discovery suggestions for this site', builder: featureDiscoveryCommand, handler: () => {} })
290
+ .command(featureDiscoveryGroup)
291
+ .command(scraperGroupCommand)
292
+ .command(tourPolicyGroupCommand)
293
+ .command(livefluenceCommand)
294
+ .command(missedCommand)
295
+ .command(serviceCommand)
296
+ .command(pageCommand)
297
+ .command(uploadedCommand)
298
+ .command(msCommand)
299
+ .command(outlookCommand)
300
+ .command(oauthCommand)
301
+ .command(provisionCommand)
302
+ .command(sseCommand)
303
+ .command(skillCommand)
304
+ .command(v10Command)
305
+ .command(whatsappCommand)
306
+ .command(mobileCommand)
307
+ .command(signalrCommand)
277
308
  .command(usageCommand)
278
- .command({ command: 'scraper', desc: 'Velaro staff: scraper / upload / chunk usage for any site', builder: scraperCommand, handler: () => {} })
279
- .command({ command: 'tour-policy', desc: 'Velaro staff: guided-tour auto-open policy (versioned, rollback-able)', builder: tourPolicyCommand, handler: () => {} })
280
309
  .command(campaignsCommand)
281
310
  .command(senderDomainHealthCommand)
282
311
  .command(contactsCommand)
283
312
  .command(contactListCommand)
313
+ .command(recycleBinCommand)
314
+ .command(aiUsageCommand)
315
+ .command(siteInfoCommand)
316
+ .command(billingReceiptsCommand)
317
+ .command(adminCommand)
318
+ .command(appleCommand)
319
+ .command(forceExpireSiteCommand)
320
+ .command(backfillCommand)
321
+ .command(kpiCommand)
322
+ .command(auditTrailCommand)
323
+ .command(codeCommand)
324
+ .command(crmCommand)
325
+ .command(hangfireCommand)
326
+ .command(aiFollowupsCommand)
327
+ .command(businessActionCommand)
328
+ .command(callCommand)
329
+ .command(conversationCommand)
330
+ .command(conversionCommand)
331
+ .command(emailCommand)
332
+ .command(ivrCommand)
284
333
  .demandCommand(1, 'Specify a command. Run "velaro --help" for options.')
285
334
  .strict()
286
335
  .help()
@@ -84,7 +84,7 @@ const aiConfigCreateCommand = {
84
84
  y
85
85
  .option('name', { type: 'string', describe: 'Configuration name' })
86
86
  .option('prompt', { type: 'string', describe: 'System prompt text' })
87
- .option('model', { type: 'string', describe: 'Model (e.g. gpt-4o-mini, claude-haiku-4-5)' })
87
+ .option('model', { type: 'string', describe: 'Model (e.g. gpt-4o-mini, claude-haiku-5-5)' })
88
88
  .option('tone', { type: 'string', describe: 'Tone (e.g. friendly, professional)' })
89
89
  .option('language', { type: 'string', default: 'en', describe: 'Language code' })
90
90
  .option('reply-length',{ type: 'string', describe: 'Reply length (e.g. short, medium, long)' })
@@ -35,7 +35,7 @@ async function listRules() {
35
35
  async function setGlobal(appliesTo, mode) {
36
36
  if (!GATES.includes(appliesTo)) fail(`appliesTo must be one of: ${GATES.join(', ')}`);
37
37
  if (!MODES.includes(mode)) fail(`mode must be one of: ${MODES.join(', ')}`);
38
- await request('PUT', `/api/BillingAutomation/global/${appliesTo}`, { mode });
38
+ await request('PUT', `/api/BillingAutomation/global/${appliesTo}`, { mode }); // controller action: SaveGlobal
39
39
  console.log(`Global ${appliesTo} set to ${mode}.`);
40
40
  }
41
41
 
@@ -0,0 +1,14 @@
1
+ import { messagingPost } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ // velaro csat-override-unapprove <id>
5
+ // REST: messaging POST /api/intelligence/csat-override-candidates/{id}/unapprove (site comes from the CLI session)
6
+ export const csatOverrideUnapproveCommand = {
7
+ command: 'csat-override-unapprove <id>',
8
+ describe: 'Return an approved Chat Intelligence improvement candidate to Pending (deactivates the override, removes any vectorized copy)',
9
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Approved override candidate id' }),
10
+ handler: runCommand(async (a) => {
11
+ if (!Number.isInteger(a.id) || a.id <= 0) throw new Error('id must be a positive integer');
12
+ console.log(JSON.stringify(await messagingPost(`/api/intelligence/csat-override-candidates/${a.id}/unapprove`, {}), null, 2));
13
+ }),
14
+ };
@@ -0,0 +1,108 @@
1
+ // MCP parity command groups: admin, apple, force-expire-site, backfill, kpi, audit-trail, code, crm, hangfire.
2
+ // Each subcommand calls the same REST route as the MCP tool of the same name (mcp/server.js, mcp/staff-server.js).
3
+ // Staff-only routes are enforced by the backend.
4
+ import { get, post, del, messagingGet, messagingPost } from '../api.js';
5
+ import { show, qs, needYes, group } from '../parity-util.js';
6
+
7
+ export const adminCommand = { command: 'admin', ...group('Velaro staff: admin API diagnostics', [
8
+ ['startup-diagnostics', 'Recent AdminStartupDiagnostics rows (migrations, startup exceptions)',
9
+ (y) => y.option('context-label', { type: 'string' }).option('count', { type: 'number' }),
10
+ async (a) => show(await get(`/SuperAdmin/StartupDiagnostics${qs({ contextLabel: a['context-label'], count: a.count })}`))],
11
+ ]) };
12
+
13
+ export const appleCommand = { command: 'apple', ...group('Velaro staff: Apple Messages for Business diagnostics', [
14
+ ['health <siteId>', 'Apple Messages health for a site',
15
+ (y) => y.positional('siteId', { type: 'number' }),
16
+ async (a) => show(await messagingGet(`/SupportTools/sites/${a.siteId}/apple/health`))],
17
+ ['test-control <siteId>', 'Send an Apple test-control action to a conversation',
18
+ (y) => y.positional('siteId', { type: 'number' })
19
+ .option('control', { type: 'string', demandOption: true, describe: 'Control name' })
20
+ .option('conversation-id', { type: 'number' }),
21
+ async (a) => show(await messagingPost(`/SupportTools/sites/${a.siteId}/apple/test-control`, { control: a.control, conversationId: a['conversation-id'] }))],
22
+ ]) };
23
+
24
+ export const forceExpireSiteCommand = {
25
+ command: 'force-expire-site <siteId>',
26
+ describe: 'Velaro staff: force-expire a site subscription (destructive, needs --yes)',
27
+ builder: (y) => y.positional('siteId', { type: 'number' }).option('yes', { type: 'boolean', describe: 'Confirm' }),
28
+ handler: async (a) => {
29
+ needYes(a, `force-expire site ${a.siteId}`);
30
+ show(await messagingPost(`/SuperAdmin/subscription/${a.siteId}/force-expire`));
31
+ },
32
+ };
33
+
34
+ export const backfillCommand = { command: 'backfill', ...group('Velaro staff: per-site data backfills (preview unless --apply --yes)', [
35
+ ['types', 'List backfill types', null, async () => show(await messagingGet('/SuperAdmin/backfill-types'))],
36
+ ['run <siteId>', 'Preview or apply a backfill for a site',
37
+ (y) => y.positional('siteId', { type: 'number' })
38
+ .option('type', { type: 'string', demandOption: true })
39
+ .option('from', { type: 'string', demandOption: true, describe: 'yyyy-MM-dd' })
40
+ .option('to', { type: 'string', demandOption: true, describe: 'yyyy-MM-dd' })
41
+ .option('apply', { type: 'boolean', describe: 'Write the backfill (default is a preview)' })
42
+ .option('yes', { type: 'boolean', describe: 'Confirm --apply' }),
43
+ async (a) => {
44
+ if (a.apply) needYes(a, 'apply a backfill');
45
+ show(await messagingPost(`/SuperAdmin/sites/${encodeURIComponent(a.siteId)}/backfill`, { type: a.type, from: a.from, to: a.to, apply: a.apply === true }));
46
+ }],
47
+ ]) };
48
+
49
+ export const kpiCommand = { command: 'kpi', ...group('Live operations KPIs', [
50
+ ['snapshot', 'Real-time ops snapshot: agents, queues, waits, channels, teams', null, async () => show(await get('/LiveDashboard/snapshot'))],
51
+ ]) };
52
+
53
+ export const auditTrailCommand = { command: 'audit-trail', ...group('Audit trail of configuration changes', [
54
+ ['list', 'List audit entries',
55
+ (y) => y.option('entity-type', { type: 'string' }).option('entity-id', { type: 'string' }).option('take', { type: 'number', default: 200 }),
56
+ async (a) => show(await get(`/AuditTrail${qs({ entityType: a['entity-type'], entityId: a['entity-id'], take: Math.min(500, a.take ?? 200) })}`))],
57
+ ]) };
58
+
59
+ export const codeCommand = { command: 'code', ...group('Push files to a Velaro repo branch (needs GITHUB_TOKEN)', [
60
+ ['push-files', 'Create one commit with files on dev/main/master (destructive, needs --yes)',
61
+ (y) => y.option('message', { type: 'string', demandOption: true })
62
+ .option('file', { type: 'array', demandOption: true, describe: 'repoPath=localPath, repeatable' })
63
+ .option('repo', { type: 'string', default: 'velaro-admin' })
64
+ .option('branch', { type: 'string', default: 'dev' })
65
+ .option('yes', { type: 'boolean' }),
66
+ async (a) => {
67
+ needYes(a, 'push a commit');
68
+ const token = process.env.GITHUB_TOKEN;
69
+ if (!token) throw new Error('GITHUB_TOKEN env var is not set.');
70
+ const { readFileSync } = await import('node:fs');
71
+ const files = a.file.map((f) => { const [p, l] = String(f).split('='); return { path: p, content: readFileSync(l || p, 'utf8') }; });
72
+ const REPOS = ['velaro-admin', 'velaro-messaging', 'velaro-shared', 'velaro-shared-frontend'];
73
+ if (!REPOS.includes(a.repo)) throw new Error(`Repo not allowed. Allowed: ${REPOS.join(', ')}`);
74
+ if (!['dev', 'master', 'main'].includes(a.branch)) throw new Error('Branch must be dev, master or main.');
75
+ const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\//, '');
76
+ if (files.some((f) => norm(f.path) === '.github' || norm(f.path).startsWith('.github/'))) throw new Error('Cannot push to .github/ paths.');
77
+ const base = `https://api.github.com/repos/velaro/${a.repo}`;
78
+ const headers = { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'Content-Type': 'application/json' };
79
+ const gh = async (path, opts = {}) => {
80
+ const r = await fetch(base + path, { headers, ...opts });
81
+ if (!r.ok) throw new Error(`GitHub API ${path} -> ${r.status}: ${await r.text()}`);
82
+ return r.json();
83
+ };
84
+ const head = (await gh(`/git/refs/heads/${a.branch}`)).object.sha;
85
+ const baseTree = (await gh(`/git/commits/${head}`)).tree.sha;
86
+ const tree = await Promise.all(files.map(async (f) => {
87
+ const b = await gh('/git/blobs', { method: 'POST', body: JSON.stringify({ content: f.content, encoding: 'utf-8' }) });
88
+ return { path: f.path, mode: '100644', type: 'blob', sha: b.sha };
89
+ }));
90
+ const nt = await gh('/git/trees', { method: 'POST', body: JSON.stringify({ base_tree: baseTree, tree }) });
91
+ const c = await gh('/git/commits', { method: 'POST', body: JSON.stringify({ message: a.message, tree: nt.sha, parents: [head] }) });
92
+ await gh(`/git/refs/heads/${a.branch}`, { method: 'PATCH', body: JSON.stringify({ sha: c.sha }) });
93
+ console.log(`Pushed to velaro/${a.repo}@${a.branch} commit ${c.sha.slice(0, 8)}\n${files.map((f) => ' ' + f.path).join('\n')}`);
94
+ }],
95
+ ]) };
96
+
97
+ export const crmCommand = { command: 'crm', ...group('Velaro staff: CRM email-campaign engine audit', [
98
+ ['email-campaign-audit', 'Row counts for the legacy CRM email-campaign tables (safe-to-retire check)', null,
99
+ async () => show(await messagingGet('/SuperAdmin/crm-email-campaign-audit'))],
100
+ ]) };
101
+
102
+ export const hangfireCommand = { command: 'hangfire', ...group('Velaro staff: failed Hangfire jobs', [
103
+ ['list-failed', 'List failed jobs', null, async () => show(await get('/DatabaseTool/FailedJobs'))],
104
+ ['retry <id>', 'Requeue one failed job', (y) => y.positional('id', { type: 'string' }),
105
+ async (a) => show(await post(`/DatabaseTool/FailedJobs/${a.id}/retry`))],
106
+ ['delete-failed <id>', 'Delete one failed job (needs --yes)', (y) => y.positional('id', { type: 'string' }).option('yes', { type: 'boolean' }),
107
+ async (a) => { needYes(a, `delete failed job ${a.id}`); show(await del(`/DatabaseTool/FailedJobs/${a.id}`)); }],
108
+ ]) };
@@ -0,0 +1,58 @@
1
+ // CLI halves of the MCP parity tools in mcp/parity/endpoints.customer.js and endpoints.staff.js.
2
+ // Each handler names the controller action it calls.
3
+ import { request } from '../api.js';
4
+
5
+ const show = (r) => console.log(JSON.stringify(r, null, 2));
6
+ const int = (y, k, d) => y.option(k, { type: 'number', demandOption: true, describe: d });
7
+
8
+ export const recycleBinCommand = {
9
+ command: 'recycle-bin',
10
+ describe: 'List and restore soft-deleted contact lists, members, segments and KB articles/topics (one id at a time)',
11
+ builder: (y) => y
12
+ .command({ command: 'contact-lists', describe: 'List deleted contact lists', handler: async () => show(await request('GET', '/ContactLists/deleted')) }) // controller action: GetDeletedLists
13
+ .command({ command: 'restore-contact-list', describe: 'Restore one contact list', builder: (z) => int(z, 'id', 'List id'), handler: async (a) => show(await request('POST', `/ContactLists/${a.id}/restore`)) }) // controller action: RestoreList
14
+ .command({ command: 'members', describe: 'List deleted members of a list', builder: (z) => int(z, 'list', 'List id'), handler: async (a) => show(await request('GET', `/ContactLists/${a.list}/Members/deleted`)) }) // controller action: GetDeletedMembers
15
+ .command({ command: 'restore-member', describe: 'Restore one list member', builder: (z) => int(int(z, 'list', 'List id'), 'member', 'Member id'), handler: async (a) => show(await request('POST', `/ContactLists/${a.list}/Members/${a.member}/restore`)) }) // controller action: RestoreMember
16
+ .command({ command: 'segments', describe: 'List deleted segments', handler: async () => show(await request('GET', '/ContactSegments/deleted')) }) // controller action: ListDeleted (ContactSegments)
17
+ .command({ command: 'kb-articles', describe: 'List deleted KB articles', handler: async () => show(await request('GET', '/api/kb/articles/deleted')) }) // controller action: ListDeleted (KBArticles)
18
+ .command({ command: 'restore-kb-article', describe: 'Restore one KB article', builder: (z) => int(z, 'id', 'Article id'), handler: async (a) => show(await request('POST', `/api/kb/articles/${a.id}/restore`)) }) // controller action: RestoreDeleted (KBArticles)
19
+ .command({ command: 'kb-topics', describe: 'List deleted KB topics', handler: async () => show(await request('GET', '/api/kb/topics/deleted')) }) // controller action: ListDeleted (KBTopics)
20
+ .command({ command: 'restore-kb-topic', describe: 'Restore one KB topic', builder: (z) => int(z, 'id', 'Topic id'), handler: async (a) => show(await request('POST', `/api/kb/topics/${a.id}/restore`)) }) // controller action: RestoreDeleted (KBTopics)
21
+ .demandCommand(1),
22
+ handler: () => {},
23
+ };
24
+
25
+ export const aiUsageCommand = {
26
+ command: 'ai-usage',
27
+ describe: 'Your site AI usage summary and budget alert',
28
+ builder: (y) => y
29
+ .command({ command: 'summary', describe: 'AI usage summary', handler: async () => show(await request('GET', '/AiUsageReport/summary')) }) // controller action: GetSummary
30
+ .command({
31
+ command: 'budget-alert-set', describe: 'Update the budget alert',
32
+ builder: (z) => z.option('enabled', { type: 'boolean' }).option('threshold-percent', { type: 'number' }).option('recipients', { type: 'string', describe: 'Comma-separated emails' }),
33
+ handler: async (a) => show(await request('PUT', '/AiUsageReport/budget-alert', { enabled: a.enabled, thresholdPercent: a.thresholdPercent, recipientEmails: a.recipients })), // controller action: UpdateBudgetAlert
34
+ })
35
+ .command({ command: 'budget-alert-delete', describe: 'Delete the budget alert', handler: async () => show(await request('DELETE', '/AiUsageReport/budget-alert')) }) // controller action: DeleteBudgetAlert
36
+ .demandCommand(1),
37
+ handler: () => {},
38
+ };
39
+
40
+ export const siteInfoCommand = {
41
+ command: 'site-info',
42
+ describe: 'Read-only site settings, entitlements and platform uptime for your site',
43
+ builder: (y) => y
44
+ .command({ command: 'settings', describe: 'Site settings', handler: async () => show(await request('GET', '/api/SiteSettings')) }) // controller action: GetSiteSettings
45
+ .command({ command: 'entitlements', describe: 'Your resolved entitlements', handler: async () => show(await request('GET', '/Entitlements/my')) }) // controller action: GetMyEntitlements
46
+ .command({ command: 'uptime', describe: 'Platform uptime', handler: async () => show(await request('GET', '/PlatformUptime')) }) // controller action: GetAsync (PlatformUptime)
47
+ .command({ command: 'last-copilot-session', describe: 'Your last Copilot session summary', handler: async () => show(await request('GET', '/Copilot/LastSession')) }) // controller action: LastSession
48
+ .command({ command: 'sender-domain-health', describe: 'Sender domain health', handler: async () => show(await request('GET', '/SenderDomains/Health')) }) // controller action: GetHealth
49
+ .command({ command: 'netsuite-interaction-summary', describe: 'NetSuite AI interaction summary for a conversation', builder: (z) => int(z, 'conversation', 'Conversation id'), handler: async (a) => show(await request('POST', `/NetSuite/interaction-summary?conversationId=${a.conversation}`)) }) // controller action: GetInteractionSummary
50
+ .demandCommand(1),
51
+ handler: () => {},
52
+ };
53
+
54
+ export const billingReceiptsCommand = {
55
+ command: 'billing-receipts-run',
56
+ describe: 'Staff only: run the payment receipt job now',
57
+ handler: async () => show(await request('POST', '/api/BillingNotifications/receipts/run-now')), // controller action: RunReceiptsNow
58
+ };
@@ -1,6 +1,19 @@
1
1
  // velaro entitlement — query and manage site feature entitlements (superadmin only)
2
2
  import { request } from '../api.js';
3
3
 
4
+ async function syncStripe(a) {
5
+ const r = await request('PUT', `Entitlements/admin/sync-stripe/${a.site}`, {
6
+ planKey: a.plan,
7
+ status: a.status,
8
+ cancelAtPeriodEnd: a['cancel-at-period-end'] ?? false,
9
+ currentPeriodEnd: a['current-period-end'] ?? null,
10
+ trialEndAt: a['trial-end-at'] ?? null,
11
+ stripeCustomerId: a['stripe-customer-id'],
12
+ stripeSubscriptionId: a['stripe-subscription-id'],
13
+ });
14
+ console.log(`Site ${a.site} synced: status=${r.status} plan=${r.planKey || 'unchanged'}`);
15
+ }
16
+
4
17
  async function getSite(args) {
5
18
  if (!args.site) { console.error('Usage: velaro entitlement get --site 1032'); process.exit(1); }
6
19
  const result = await request('GET', `Entitlements/${args.site}`);
@@ -289,6 +302,16 @@ export function entitlementCommand(yargs) {
289
302
  .option('at', { type: 'string', desc: 'ISO 8601 timestamp to lock entitlement resolution to' })
290
303
  .option('unlock', { type: 'boolean', desc: 'Clear an existing lock (resolves against current plan version again)' })
291
304
  , a => setLock(a))
305
+ .command('sync-stripe', 'Sync the Stripe subscription state into its entitlement record (superadmin only)', y => y
306
+ .option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
307
+ .option('status', { type: 'string', demandOption: true, desc: 'Stripe subscription status, e.g. active, trialing, canceled' })
308
+ .option('plan', { type: 'string', desc: 'Plan key, e.g. plan.professional' })
309
+ .option('cancel-at-period-end', { type: 'boolean', desc: 'Subscription cancels at period end' })
310
+ .option('current-period-end', { type: 'string', desc: 'ISO 8601 current period end' })
311
+ .option('trial-end-at', { type: 'string', desc: 'ISO 8601 trial end' })
312
+ .option('stripe-customer-id', { type: 'string', desc: 'Stripe customer id' })
313
+ .option('stripe-subscription-id', { type: 'string', desc: 'Stripe subscription id' })
314
+ , a => syncStripe(a))
292
315
  .command('trial <action>', 'Trial governance (superadmin): get | recommend | not-paying | grant | hold | reactivate | set-limit | approve-limit | deny-limit | set-text', y => y
293
316
  .option('site', { type: 'number', desc: 'Site ID' })
294
317
  .option('feature', { type: 'string', desc: 'Feature key, e.g. EnableIvr' })
@@ -305,5 +328,8 @@ export function entitlementCommand(yargs) {
305
328
  .option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
306
329
  .option('rollback', { type: 'boolean', desc: 'Pass to roll back this flag to Subscription.cs (default: go live)' })
307
330
  , a => setCutover(a))
308
- .demandCommand(1, 'Specify a subcommand: get | list-features | enterprise-reports | report-packages | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | trial | list-cutover | set-cutover');
331
+ .demandCommand(1, 'Specify a subcommand: get | list-features | enterprise-reports | report-packages | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | sync-stripe | trial | list-cutover | set-cutover');
309
332
  }
333
+
334
+ // Registered group object (the CLI/MCP parity validator reads `command:` from a named const).
335
+ export const entitlementGroup = { command: 'entitlement', desc: 'Query and manage site feature entitlements (superadmin only)', builder: entitlementCommand, handler: () => {} };
@@ -38,3 +38,6 @@ export function featureDiscoveryCommand(yargs) {
38
38
  , a => requestFeature(a))
39
39
  .demandCommand(1, 'Specify a subcommand: list | request');
40
40
  }
41
+
42
+ // Registered group object (the CLI/MCP parity validator reads `command:` from a named const).
43
+ export const featureDiscoveryGroup = { command: 'feature-discovery', desc: 'View and act on feature-discovery suggestions for this site', builder: featureDiscoveryCommand, handler: () => {} };
@@ -0,0 +1,114 @@
1
+ // velaro knowledge clinical-text get|set -- emergency and safety wording per channel and language.
2
+ // Backend: velaro-messaging KnowledgeSetupController. Customer routes take the site from auth
3
+ // (GET KnowledgeSetup/state, PUT KnowledgeSetup/clinical-text); --site switches to the staff twins
4
+ // (GET/PUT KnowledgeSetup/Staff/{siteId}/...), Velaro super admin only.
5
+ import fs from 'node:fs';
6
+ import { messagingGet, messagingPut } from '../api.js';
7
+ import { runCommand } from '../run.js';
8
+
9
+ const CHANNELS = ['chat', 'voice', 'sms', 'whatsapp', 'email'];
10
+
11
+ export function basePath(site) {
12
+ return site ? `/KnowledgeSetup/Staff/${encodeURIComponent(site)}` : '/KnowledgeSetup';
13
+ }
14
+
15
+ /** Key is default | channel | lang | lang.channel (es.voice). Value is the wording. */
16
+ export function parseEntries(list) {
17
+ const out = {};
18
+ for (const raw of [].concat(list ?? [])) {
19
+ const i = String(raw).indexOf('=');
20
+ if (i < 1) throw new Error(`Expected key=text (for example es.voice="..."), got: ${raw}`);
21
+ out[String(raw).slice(0, i).trim().toLowerCase()] = String(raw).slice(i + 1);
22
+ }
23
+ return out;
24
+ }
25
+
26
+ export function buildBody(argv) {
27
+ if (argv.reset) return { resetToDefault: true };
28
+ let body = {};
29
+ if (argv.file) body = JSON.parse(fs.readFileSync(argv.file, 'utf8'));
30
+ const em = parseEntries(argv.emergency);
31
+ const rf = parseEntries(argv.referral);
32
+ if (Object.keys(em).length) body.emergency = { ...(body.emergency ?? {}), ...em };
33
+ if (Object.keys(rf).length) body.referral = { ...(body.referral ?? {}), ...rf };
34
+ if (argv['emergency-number'] !== undefined) body.emergencyNumber = argv['emergency-number'];
35
+ if (argv['service-mode'] !== undefined || argv['service-mode-text'] !== undefined) {
36
+ body.emergencyServiceMode = {};
37
+ if (argv['service-mode'] !== undefined) body.emergencyServiceMode.enabled = argv['service-mode'];
38
+ if (argv['service-mode-text'] !== undefined) body.emergencyServiceMode.text = argv['service-mode-text'];
39
+ if (argv.confirm) body.emergencyServiceMode.confirm = true;
40
+ }
41
+ return body;
42
+ }
43
+
44
+ export function printState(ct) {
45
+ console.log(`\nEmergency number: ${ct.overrides?.emergencyNumber || `(default) ${ct.defaultEmergencyNumber}`}`);
46
+ console.log(`Limits: ${ct.limits?.minLength}-${ct.limits?.maxLength} characters. Emergency text must keep ${ct.placeholder}.`);
47
+ console.log(`Supported languages: ${(ct.supportedLanguages ?? []).join(', ') || '-'}`);
48
+ const m = ct.emergencyServiceMode;
49
+ console.log(`Emergency-service mode: ${m?.enabled ? 'ON' : 'off'}${m?.text ? ` | text: ${m.text}` : ''} (staff only)`);
50
+ console.log('Custom wording (key = text):');
51
+ for (const kind of ['emergency', 'referral']) {
52
+ const e = Object.entries(ct.overrides?.[kind] ?? {});
53
+ if (!e.length) console.log(` ${kind}: none, built-in wording`);
54
+ for (const [k, v] of e) console.log(` ${kind}.${k} = ${v}`);
55
+ }
56
+ console.log('Effective English wording per channel:');
57
+ for (const c of CHANNELS) {
58
+ const x = ct.effective?.[c];
59
+ if (x) console.log(` ${c}: emergency "${x.emergency}" | referral "${x.referral}"`);
60
+ }
61
+ }
62
+
63
+ const clinicalGet = {
64
+ command: 'get',
65
+ describe: 'Show emergency/safety wording, supported languages and emergency-service mode',
66
+ builder: (y) => y
67
+ .option('site', { type: 'number', describe: 'Target site ID (Velaro staff only; omit for your own site)' })
68
+ .option('json', { type: 'boolean', default: false }),
69
+ handler: runCommand(async (argv) => {
70
+ const state = await messagingGet(`${basePath(argv.site)}/state`);
71
+ const ct = state?.clinicalText;
72
+ if (!ct) throw new Error('The setup state did not include clinicalText (backend too old?).');
73
+ if (argv.json) { console.log(JSON.stringify(ct, null, 2)); return; }
74
+ printState(ct);
75
+ }),
76
+ };
77
+
78
+ const clinicalSet = {
79
+ command: 'set',
80
+ describe: 'Set emergency/safety wording. Entries replace the stored set: pass every override you want to keep (use get --json).',
81
+ builder: (y) => y
82
+ .option('site', { type: 'number', describe: 'Target site ID (Velaro staff only)' })
83
+ .option('emergency', { type: 'array', string: true, describe: 'key=text, key is default|chat|voice|sms|whatsapp|email|<lang>|<lang>.<channel>; must contain {emergencyNumber}' })
84
+ .option('referral', { type: 'array', string: true, describe: 'key=text, same keys as --emergency' })
85
+ .option('emergency-number', { type: 'string', describe: 'Replaces {emergencyNumber}' })
86
+ .option('file', { type: 'string', describe: 'JSON body { emergency, referral, emergencyNumber }' })
87
+ .option('reset', { type: 'boolean', default: false, describe: 'Clear all custom wording' })
88
+ .option('service-mode', { type: 'boolean', describe: 'Staff only (--site): turn emergency-service mode on/off' })
89
+ .option('service-mode-text', { type: 'string', describe: 'Staff only: required emergency text for service mode (20-600 chars)' })
90
+ .option('confirm', { type: 'boolean', default: false, describe: 'Required to turn service mode on' })
91
+ .option('json', { type: 'boolean', default: false }),
92
+ handler: runCommand(async (argv) => {
93
+ const body = buildBody(argv);
94
+ if (body.emergencyServiceMode && !argv.site) throw new Error('Emergency-service mode is staff only: pass --site <id>.');
95
+ const r = await messagingPut(`${basePath(argv.site)}/clinical-text`, body);
96
+ if (argv.json) { console.log(JSON.stringify(r, null, 2)); return; }
97
+ console.log(r.changed ? 'Saved.' : 'No change.');
98
+ if (r.clinicalText) printState(r.clinicalText);
99
+ }),
100
+ };
101
+
102
+ export const knowledgeCommand = {
103
+ command: 'knowledge <subcommand>',
104
+ describe: 'Knowledge setup safety settings',
105
+ builder: (y) => y
106
+ .command({
107
+ command: 'clinical-text <action>',
108
+ describe: 'Emergency and safety messages per channel and language',
109
+ builder: (yy) => yy.command(clinicalGet).command(clinicalSet).demandCommand(1, 'Specify get or set'),
110
+ handler: () => {},
111
+ })
112
+ .demandCommand(1, 'Specify a subcommand: clinical-text'),
113
+ handler: () => {},
114
+ };