@velaro/cli 1.4.87 → 1.4.89

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
@@ -44,6 +44,7 @@ import { reportCommand } from '../lib/commands/report.js';
44
44
  import { scheduleAdherenceCommand } from '../lib/commands/schedule-adherence.js';
45
45
  import { reportSubscriptionCommand } from '../lib/commands/report-subscription.js';
46
46
  import { experienceImpactCommand } from '../lib/commands/experience-impact.js';
47
+ import { enterpriseReportsCommand } from '../lib/commands/enterprise-reports.js';
47
48
  import { reportBackfillCommand } from '../lib/commands/report-backfill.js';
48
49
  import { staffCommand } from '../lib/commands/staff.js';
49
50
  import { reportExplainCommand } from '../lib/commands/report-explain.js';
@@ -54,6 +55,7 @@ import { skillRoutingCommand } from '../lib/commands/skill-routing.js';
54
55
  import { keywordIntelligenceCommand } from '../lib/commands/keyword-intelligence.js';
55
56
  import { aiFieldExtractionCommand } from '../lib/commands/ai-field-extraction.js';
56
57
  import { sentimentCommand, qualityCommand, chatIntelligenceThemesCommand } from '../lib/commands/quality-reports.js';
58
+ import { cannedLearningCommand } from '../lib/commands/canned-learning.js';
57
59
  import { focusCommand } from '../lib/commands/focus.js';
58
60
  import { triageCommand } from '../lib/commands/triage.js';
59
61
  import { automationDraftCommand } from '../lib/commands/automation-draft.js';
@@ -68,6 +70,7 @@ import { outboundActivityCommand } from '../lib/commands/outbound-activity.js';
68
70
  import { klaviyoMigrationCommand } from '../lib/commands/klaviyo-migration.js';
69
71
  import { telephonyCommand } from '../lib/commands/telephony.js';
70
72
  import { teamsPhoneCommand } from '../lib/commands/teams-phone.js';
73
+ import { zoomPhoneCommand } from '../lib/commands/zoom-phone.js';
71
74
  import { teamsCommand } from '../lib/commands/teams.js';
72
75
  import { slackCommand } from '../lib/commands/slack.js';
73
76
  import { webflowCommand } from '../lib/commands/webflow.js';
@@ -95,6 +98,7 @@ import { diagnosticsCommand } from '../lib/commands/diagnostics.js';
95
98
  import { aiSpendCommand } from '../lib/commands/ai-spend.js';
96
99
  import { statusPageCommand } from '../lib/commands/status-page.js';
97
100
  import { statusNotifyCommand } from '../lib/commands/status-notify.js';
101
+ import { statusSlaCommand } from '../lib/commands/status-sla.js';
98
102
  import { emailInboxCommand } from '../lib/commands/email-inbox.js';
99
103
  import { callbackCommand } from '../lib/commands/callback.js';
100
104
  import { caseCommand } from '../lib/commands/case.js';
@@ -170,6 +174,7 @@ await yargs(hideBin(process.argv))
170
174
  .command(executiveInsightsCommand)
171
175
  .command(scheduleAdherenceCommand)
172
176
  .command(experienceImpactCommand)
177
+ .command(enterpriseReportsCommand)
173
178
  .command(reportSubscriptionCommand)
174
179
  .command(reportBackfillCommand)
175
180
  .command({ command: 'staff', desc: 'Velaro staff: cross-site tools (backfill)', builder: staffCommand, handler: () => {} })
@@ -181,6 +186,7 @@ await yargs(hideBin(process.argv))
181
186
  .command(sentimentCommand)
182
187
  .command(qualityCommand)
183
188
  .command(chatIntelligenceThemesCommand)
189
+ .command(cannedLearningCommand)
184
190
  .command(focusCommand)
185
191
  .command(triageCommand)
186
192
  .command(automationDraftCommand)
@@ -195,6 +201,7 @@ await yargs(hideBin(process.argv))
195
201
  .command(klaviyoMigrationCommand)
196
202
  .command(telephonyCommand)
197
203
  .command(teamsPhoneCommand)
204
+ .command(zoomPhoneCommand)
198
205
  .command(teamsCommand)
199
206
  .command(slackCommand)
200
207
  .command(webflowCommand)
@@ -222,6 +229,7 @@ await yargs(hideBin(process.argv))
222
229
  .command(aiSpendCommand)
223
230
  .command(statusPageCommand)
224
231
  .command(statusNotifyCommand)
232
+ .command(statusSlaCommand)
225
233
  .command(emailInboxCommand)
226
234
  .command(callbackCommand)
227
235
  .command(caseCommand)
package/lib/api.js CHANGED
@@ -90,3 +90,18 @@ export const messagingPost = (path, body) => messagingRequest('POST', path, bod
90
90
  export const messagingPut = (path, body) => messagingRequest('PUT', path, body);
91
91
  export const messagingPatch = (path, body) => messagingRequest('PATCH', path, body);
92
92
  export const messagingDel = (path) => messagingRequest('DELETE', path);
93
+
94
+ // Same as messagingRequest but returns the raw response text (for CSV downloads).
95
+ export async function messagingRequestText(method, path, envOverride) {
96
+ const creds = await getCredentials(envOverride);
97
+ const res = await fetch(joinUrl(creds.messagingApiBase, path), {
98
+ method,
99
+ headers: {
100
+ Authorization: `Bearer ${creds.velaroToken}`,
101
+ ...(creds.siteId ? { 'vmsg-siteId': String(creds.siteId) } : {}),
102
+ },
103
+ });
104
+ const text = await res.text();
105
+ if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}${text ? `: ${text}` : ''}`);
106
+ return text;
107
+ }
@@ -1,4 +1,4 @@
1
- import { get, getCredentials } from '../api.js';
1
+ import { get, getCredentials, messagingGet } from '../api.js';
2
2
 
3
3
  // ── helpers ──────────────────────────────────────────────────────────────────
4
4
 
@@ -86,7 +86,20 @@ async function activityHandler(argv) {
86
86
  params.set('pageSize', String(Math.min(100, argv.pageSize ?? 25)));
87
87
 
88
88
  const qs = params.toString();
89
- const data = await get(`${path}${qs ? '?' + qs : ''}`);
89
+ let data;
90
+ if (argv.site) {
91
+ // Staff: read ANOTHER site's Activity Log (customer/test site) without logging in as it.
92
+ const sp = new URLSearchParams();
93
+ if (isSkills) sp.set('slug', 'AiTool');
94
+ else if (argv.slug) sp.set('slug', argv.slug);
95
+ if (argv.severity && argv.severity !== 'All') sp.set('severity', argv.severity);
96
+ if (argv.search) sp.set('search', argv.search);
97
+ sp.set('take', String(Math.min(200, argv.pageSize ?? 25)));
98
+ const r = await messagingGet(`/SupportTools/sites/${argv.site}/integration-logs?${sp.toString()}`);
99
+ data = { items: r?.items ?? [], total: r?.count ?? 0 };
100
+ } else {
101
+ data = await get(`${path}${qs ? '?' + qs : ''}`);
102
+ }
90
103
 
91
104
  if (!data?.items?.length) {
92
105
  console.log('\nNo activity log entries.');
@@ -124,6 +137,7 @@ export const activityCommand = {
124
137
  type: 'string',
125
138
  })
126
139
  .option('search', { describe: 'Message search', type: 'string' })
140
+ .option('site', { describe: "Staff only: read this site's Activity Log instead of your own", type: 'number' })
127
141
  .option('page', { describe: 'Page number', default: 1, type: 'number' })
128
142
  .option('pageSize', { describe: 'Results per page', default: 25, type: 'number' })
129
143
  .option('entityType', { describe: '[audit-trail] Filter to one entity type, e.g. Workflow', type: 'string' })
@@ -30,6 +30,9 @@ export const avatarCommand = {
30
30
  console.log(` Enabled: ${data.enabled ? 'yes' : 'no'}`);
31
31
  console.log(` "Call me": ${data.showCallMeOption ? 'yes' : 'no'}`);
32
32
  console.log(` Callback window: ${data.callbackConfirmWindowMinutes} min`);
33
+ console.log(` Display name: ${data.displayName || '(AI)'}`);
34
+ console.log(` Badge shown: ${data.showBadge ? 'yes' : 'no'}`);
35
+ console.log(` Size: ${data.sizePx}px`);
33
36
  console.log(` Max minutes/mo: ${data.maxAvatarMinutesPerMonth ?? '(plan default)'}`);
34
37
  console.log(` Max TTS chars/mo: ${data.maxAvatarTtsChars ?? '(plan default)'}\n`);
35
38
  }),
@@ -43,7 +46,10 @@ export const avatarCommand = {
43
46
  .option('bot', { type: 'string', describe: 'Reserved for a future per-bot override (not yet implemented on this endpoint)' })
44
47
  .option('avatar-id',{ type: 'string', describe: 'Provider-specific avatar/face ID. Omit for provider default.' })
45
48
  .option('voice', { type: 'string', describe: 'ElevenLabs voice ID override' })
46
- .option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' }),
49
+ .option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' })
50
+ .option('display-name', { type: 'string', describe: 'Visitor-facing avatar name on the badge (e.g. Moshky). Empty string resets to "AI".' })
51
+ .option('show-badge', { type: 'boolean', describe: 'Show the name badge over the avatar video.' })
52
+ .option('size', { type: 'number', describe: 'Avatar panel max width in px (160-640). Default 360.' }),
47
53
  handler: runCommand(async argv => {
48
54
  if (argv.bot) {
49
55
  console.log(`\nNote: --bot is not yet supported by this endpoint — this sets the SITE-level avatar config only. Use the admin UI's per-bot override toggle for bot-specific control.\n`);
@@ -56,6 +62,9 @@ export const avatarCommand = {
56
62
  if (argv['avatar-id'] !== undefined) body.avatarId = argv['avatar-id'];
57
63
  if (argv.voice !== undefined) body.voiceId = argv.voice;
58
64
  if (argv['call-me'] !== undefined) body.showCallMeOption = argv['call-me'];
65
+ if (argv['display-name'] !== undefined) body.displayName = argv['display-name'];
66
+ if (argv['show-badge'] !== undefined) body.showBadge = argv['show-badge'];
67
+ if (argv.size !== undefined) body.sizePx = argv.size;
59
68
  const data = await request('POST', `/Avatar/superadmin/config?siteId=${argv.site}`, body);
60
69
  if (!data.success) throw new Error(data.error || 'Configure failed');
61
70
  console.log(`\n✓ AI Video Avatar enabled for site ${argv.site}`);
@@ -0,0 +1,31 @@
1
+ import { messagingGet, messagingPost } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const print = (data) => console.log(JSON.stringify(data, null, 2));
5
+
6
+ // velaro canned-learning <list|generate|approve|reject|rollback|edits|record-edit> (REST: messaging /CannedLearning/*)
7
+ export const cannedLearningCommand = {
8
+ command: 'canned-learning <action> [id]',
9
+ describe: 'AI canned message learning: list/generate suggestions, approve/reject/rollback (human review)',
10
+ builder: (y) => y
11
+ .positional('action', { choices: ['list', 'generate', 'approve', 'reject', 'rollback', 'edits', 'record-edit'] })
12
+ .positional('id', { type: 'number' })
13
+ .option('state', { type: 'string', default: 'pending_review', describe: 'pending_review|approved|rejected|rolled_back|all' })
14
+ .option('text', { type: 'string', describe: 'approve: edited replacement text (defaults to the AI proposal)' })
15
+ .option('notes', { type: 'string', describe: 'rollback: reason' })
16
+ .option('final-text', { type: 'string', describe: 'record-edit: the text the agent actually sent (redacted server-side)' }),
17
+ handler: runCommand(async (a) => {
18
+ const base = '/CannedLearning';
19
+ if (a.action === 'list') return print(await messagingGet(`${base}/suggestions?state=${encodeURIComponent(a.state)}`));
20
+ if (a.action === 'generate') return print(await messagingPost(`${base}/generate`, {}));
21
+ if (a.action === 'edits') return print(await messagingGet(`${base}/edit-events`));
22
+ if (a.action === 'record-edit') {
23
+ if (a.id == null || !a.finalText) throw new Error('record-edit requires a canned response id and --final-text');
24
+ return print(await messagingPost(`${base}/edit-events`, { cannedResponseId: a.id, finalText: a.finalText }));
25
+ }
26
+ if (a.id == null) throw new Error(`${a.action} requires a suggestion id`);
27
+ if (a.action === 'approve') return print(await messagingPost(`${base}/suggestions/${a.id}/approve`, { editedText: a.text ?? null }));
28
+ if (a.action === 'reject') return print(await messagingPost(`${base}/suggestions/${a.id}/reject`, {}));
29
+ return print(await messagingPost(`${base}/suggestions/${a.id}/rollback`, { notes: a.notes ?? null }));
30
+ }),
31
+ };
@@ -1,4 +1,4 @@
1
- import { messagingGet, messagingPatch, messagingDel } from '../api.js';
1
+ import { messagingGet, messagingPost, messagingPatch, messagingDel } from '../api.js';
2
2
  import { runCommand } from '../run.js';
3
3
 
4
4
  // ── velaro email-inbox ────────────────────────────────────────────────────────
@@ -10,6 +10,7 @@ import { runCommand } from '../run.js';
10
10
  // GET SupportTools/sites/{siteId}/email-inbox/{provider}/list
11
11
  // GET SupportTools/sites/{siteId}/email-inbox/gmail/config-status
12
12
  // PATCH SupportTools/sites/{siteId}/email-inbox/{provider}/{id}/team
13
+ // POST SupportTools/sites/{siteId}/email-inbox/outlook/{id}/resubscribe
13
14
  // DELETE SupportTools/sites/{siteId}/email-inbox/{provider}/{id}
14
15
 
15
16
  function providerOption(y) {
@@ -56,7 +57,15 @@ export const emailInboxCommand = {
56
57
  console.log(`\nGmail global config status:`);
57
58
  console.log(` OAuth configured: ${status.oauthConfigured ? 'yes' : 'no'}`);
58
59
  console.log(` DWD configured: ${status.dwdConfigured ? 'yes' : 'no'}`);
59
- console.log(` Pub/Sub configured: ${status.pubSubConfigured ? 'yes' : 'no'}\n`);
60
+ console.log(` Pub/Sub configured: ${status.pubSubConfigured ? 'yes' : 'no'}`);
61
+ if (status.clientIdPrefix) console.log(` Client ID prefix: ${status.clientIdPrefix}...`);
62
+ if (status.credentialPair) console.log(` Credential pair: ${status.credentialPair}${status.credentialPair === 'PairValid' ? '' : ' <-- ' + (status.credentialPairDetail ?? '')}`);
63
+ if (status.redirectUri) console.log(` Redirect URI: ${status.redirectUri}`);
64
+ if (status.pubSubTopic) console.log(` Pub/Sub topic: ${status.pubSubTopic}`);
65
+ for (const d of status.duplicateKeys ?? []) {
66
+ console.log(` DUPLICATE SETTING: ${d.colonKey} and ${d.underscoreKey}${d.valuesDiffer ? ' with DIFFERENT values - delete the stale one' : ' (same value)'}`);
67
+ }
68
+ console.log('');
60
69
  }),
61
70
  })
62
71
  .command({
@@ -73,6 +82,19 @@ export const emailInboxCommand = {
73
82
  : `\n✓ Mailbox #${argv.id} (${argv.provider}) reassigned to team ${argv['team-id']}.\n`);
74
83
  }),
75
84
  })
85
+ .command({
86
+ command: 'resubscribe',
87
+ describe: 'Outlook only: (re)create the Microsoft Graph inbound-mail subscription for a mailbox (fixes connected-but-receives-nothing)',
88
+ builder: y => y
89
+ .option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
90
+ .option('id', { type: 'number', demandOption: true, describe: 'Mailbox integration ID (from "list")' }),
91
+ handler: runCommand(async argv => {
92
+ const r = await messagingPost(`/SupportTools/sites/${argv.site}/email-inbox/outlook/${argv.id}/resubscribe`, {});
93
+ console.log(r.created
94
+ ? `\n✓ Mailbox #${argv.id} subscribed (id ${r.graphSubscriptionId}, expires ${r.graphSubscriptionExpiresAt}).\n`
95
+ : `\n✗ Subscription NOT created. ${r.note ?? ''}\n Run: velaro activity OutlookInbox --site ${argv.site} --severity Error\n`);
96
+ }),
97
+ })
76
98
  .command({
77
99
  command: 'disconnect',
78
100
  describe: 'Disconnect and remove a mailbox connection',
@@ -84,5 +106,5 @@ export const emailInboxCommand = {
84
106
  console.log(`\n✓ Mailbox #${argv.id} (${argv.provider}) disconnected from site ${argv.site}.\n`);
85
107
  }),
86
108
  })
87
- .demandCommand(1, 'Specify a subcommand: list | status | set-team | disconnect'),
109
+ .demandCommand(1, 'Specify a subcommand: list | status | set-team | resubscribe | disconnect'),
88
110
  };
@@ -0,0 +1,44 @@
1
+ import { messagingGet, messagingPost } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ // -- velaro enterprise-reports ----------------------------------------------------
5
+ // Backend: velaro-messaging /api/enterprise-reports/* (Enterprise Reports package: enterprise-internal, default OFF;
6
+ // every endpoint returns 403 when the site does not hold that package item). Site scope comes from auth, never an argument.
7
+
8
+ const REPORTS = {
9
+ 'program-breakdown': 'Compare websites, teams, platforms, devices or entry pages',
10
+ 'agent-scorecard': 'Per agent: outcomes, speed, AI score, reviewed score, sentiment impact',
11
+ 'qa-coverage': 'Share of resolved chats AI-scored and supervisor-reviewed',
12
+ 'coaching-queue': 'Low-scoring chats nobody has reviewed, per agent',
13
+ 'sentiment-breakdown': 'Sentiment by website, team, agent or topic',
14
+ 'topic-radar': 'Topics by volume, growth, transfer rate and handle time',
15
+ 'weekly-brief': 'Preview the Monday brief sections',
16
+ 'weekly-brief-send-test': 'Email this week\'s brief to the signed-in user',
17
+ };
18
+
19
+ export const enterpriseReportsCommand = {
20
+ command: 'enterprise-reports <report>',
21
+ describe: 'Enterprise Reports package (enterprise-internal): ' + Object.keys(REPORTS).join(' | '),
22
+ builder: (y) => y
23
+ .positional('report', { type: 'string', choices: Object.keys(REPORTS), describe: 'Which report' })
24
+ .option('from', { type: 'string', describe: 'yyyy-MM-dd (default: 30 days ago)' })
25
+ .option('to', { type: 'string', describe: 'yyyy-MM-dd, inclusive (default: today)' })
26
+ .option('group-by', { type: 'string', describe: 'program-breakdown: website|team|platform|device|entrypage; sentiment-breakdown: website|team|agent|theme' })
27
+ .option('deployment-id', { type: 'number', describe: 'Narrow to one website' })
28
+ .option('team-id', { type: 'number', describe: 'Narrow to one team' })
29
+ .option('threshold', { type: 'number', describe: 'coaching-queue: queue chats scored below this (1-100)' })
30
+ .option('per-agent', { type: 'number', describe: 'coaching-queue: chats listed per agent' })
31
+ .option('limit', { type: 'number', describe: 'topic-radar: max topics' })
32
+ .option('days', { type: 'number', describe: 'weekly-brief: days to cover' }),
33
+ handler: runCommand(async (argv) => {
34
+ if (argv.report === 'weekly-brief-send-test') {
35
+ console.log(JSON.stringify(await messagingPost('/api/enterprise-reports/weekly-brief/send-test', {}), null, 2));
36
+ return;
37
+ }
38
+ const qs = new URLSearchParams();
39
+ const map = { from: 'from', to: 'to', groupBy: 'groupBy', deploymentId: 'deploymentId', teamId: 'teamId', threshold: 'threshold', perAgent: 'perAgent', limit: 'limit', days: 'days' };
40
+ for (const [arg, key] of Object.entries(map)) if (argv[arg] != null && argv[arg] !== '') qs.set(key, String(argv[arg]));
41
+ const q = qs.toString();
42
+ console.log(JSON.stringify(await messagingGet(`/api/enterprise-reports/${argv.report}${q ? `?${q}` : ''}`), null, 2));
43
+ }),
44
+ };
@@ -24,6 +24,21 @@ async function getSite(args) {
24
24
  }
25
25
  }
26
26
 
27
+ async function enterpriseReportsPackage(args) {
28
+ const q = args.site ? `?siteId=${args.site}` : '';
29
+ const pkg = await request('GET', `Entitlements/packages/enterprise-reports${q}`);
30
+ console.log(`
31
+ ${pkg.displayName} (${pkg.packageKey}) - plan ${pkg.plan}, default OFF${pkg.siteId ? `, site ${pkg.siteId}` : ''}
32
+ `);
33
+ for (const it of pkg.items) {
34
+ const mark = pkg.siteId ? (it.enabled ? '[ON ] ' : '[off] ') : '';
35
+ console.log(`${mark}${it.ask}. ${it.title}`);
36
+ console.log(` key: ${it.featureKey}; builds on: ${it.buildsOn.join(', ')}`);
37
+ console.log(` ${it.summary}`);
38
+ }
39
+ console.log('');
40
+ }
41
+
27
42
  async function listFeatures() {
28
43
  const features = await request('GET', 'Entitlements/features');
29
44
 
@@ -175,6 +190,9 @@ export function entitlementCommand(yargs) {
175
190
  .option('all', { type: 'boolean', desc: 'Show disabled features too (default: enabled only)' })
176
191
  , a => getSite(a))
177
192
  .command('list-features', 'List all registered entitlement feature keys', () => {}, () => listFeatures())
193
+ .command('enterprise-reports', 'Show the Enterprise Reports package items (with --site, which are ON for that site)', y => y
194
+ .option('site', { type: 'number', desc: 'Site ID' })
195
+ , a => enterpriseReportsPackage(a))
178
196
  .command('set-override', 'Enable or disable a feature for a specific site (overrides plan)', y => y
179
197
  .option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
180
198
  .option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
@@ -209,5 +227,5 @@ export function entitlementCommand(yargs) {
209
227
  .option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
210
228
  .option('rollback', { type: 'boolean', desc: 'Pass to roll back this flag to Subscription.cs (default: go live)' })
211
229
  , a => setCutover(a))
212
- .demandCommand(1, 'Specify a subcommand: get | list-features | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | list-cutover | set-cutover');
230
+ .demandCommand(1, 'Specify a subcommand: get | list-features | enterprise-reports | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | list-cutover | set-cutover');
213
231
  }
@@ -0,0 +1,47 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { messagingRequest, messagingRequestText } from '../api.js';
3
+
4
+ // Staff SLA report for the public status page (velaro-messaging SuperAdmin/status-sla).
5
+ // Endpoint is live only after velaro-messaging PR 2223 is deployed.
6
+
7
+ export function buildStatusSlaQuery(argv) {
8
+ const p = new URLSearchParams();
9
+ if (argv.from) p.set('from', argv.from);
10
+ if (argv.to) p.set('to', argv.to);
11
+ p.set('platform', argv.platform || 'v20');
12
+ if (argv.includeMaintenance) p.set('includeMaintenance', 'true');
13
+ if (argv.includeDeployments) p.set('includeDeployments', 'true');
14
+ if (argv.includeExcludedChecks) p.set('includeExcludedChecks', 'true');
15
+ if (argv.degradedDown) p.set('degradedCountsAsDown', 'true');
16
+ if (argv.slowMs !== undefined) p.set('slowResponseMs', String(argv.slowMs));
17
+ p.set('format', argv.csv ? 'csv' : 'json');
18
+ return p.toString();
19
+ }
20
+
21
+ export const statusSlaCommand = {
22
+ command: 'status-sla',
23
+ describe: 'SLA / uptime report for the status page, per component (staff only)',
24
+ builder: (y) => y
25
+ .option('from', { type: 'string', demandOption: true, describe: 'Start date (YYYY-MM-DD)' })
26
+ .option('to', { type: 'string', demandOption: true, describe: 'End date (YYYY-MM-DD)' })
27
+ .option('platform', { type: 'string', default: 'v20', choices: ['v20', 'v11'] })
28
+ .option('include-maintenance', { type: 'boolean', default: false, describe: 'Count scheduled maintenance as downtime' })
29
+ .option('include-deployments', { type: 'boolean', default: false, describe: 'Count deployment windows as downtime' })
30
+ .option('include-excluded-checks', { type: 'boolean', default: false })
31
+ .option('degraded-down', { type: 'boolean', default: false, describe: 'Degraded counts as down' })
32
+ .option('slow-ms', { type: 'number', describe: 'Responses slower than this many ms count as slow' })
33
+ .option('csv', { type: 'boolean', default: false, describe: 'CSV output (stdout, or --out file)' })
34
+ .option('out', { type: 'string', describe: 'Write CSV to this file' })
35
+ .option('staging', { type: 'boolean', default: false, describe: 'Use the staging messaging API' }),
36
+ handler: async (argv) => {
37
+ const path = `SuperAdmin/status-sla?${buildStatusSlaQuery(argv)}`;
38
+ const env = argv.staging ? 'staging' : undefined;
39
+ if (argv.csv) {
40
+ const csv = await messagingRequestText('GET', path, env);
41
+ if (argv.out) { writeFileSync(argv.out, csv); console.error(`Wrote ${argv.out}`); }
42
+ else process.stdout.write(csv);
43
+ return;
44
+ }
45
+ console.log(JSON.stringify(await messagingRequest('GET', path, undefined, env), null, 2));
46
+ },
47
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * velaro zoom-phone - Zoom Phone (SiteVoiceConfig ProviderSlug="zoom") screen-pop config
3
+ *
4
+ * velaro zoom-phone status Show provider, saved-secret flag and Zoom account_id
5
+ * velaro zoom-phone set --account-id ... [--webhook-secret] Save the Zoom account_id (and optionally the webhook secret)
6
+ *
7
+ * Backend contract: VoiceController GET/POST voice/admin/config (velaro-messaging). Site is
8
+ * resolved server-side from the auth context. The Zoom account_id is what the app-level webhook
9
+ * (POST /ZoomPhoneWebhook) routes events by; without it every call event is dropped.
10
+ * Repeatable end-to-end proof: scripts/smoke-zoom-phone.sh.
11
+ */
12
+
13
+ import { messagingGet, messagingPost } from '../api.js';
14
+
15
+ const GRN = '\x1b[32m';
16
+ const YEL = '\x1b[33m';
17
+ const DIM = '\x1b[2m';
18
+ const BOLD = '\x1b[1m';
19
+ const RST = '\x1b[0m';
20
+
21
+ async function statusHandler() {
22
+ const c = await messagingGet('/voice/admin/config');
23
+ if (!c?.configured) {
24
+ console.log(`\n${DIM}No voice provider configured for this site.${RST}\n`);
25
+ return;
26
+ }
27
+ console.log(`\n${BOLD}Voice provider${RST}: ${c.providerSlug}`);
28
+ if (c.providerSlug !== 'zoom') {
29
+ console.log(` ${YEL}Zoom Phone is not the active provider; "set" will refuse unless you switch it in the admin UI.${RST}\n`);
30
+ return;
31
+ }
32
+ console.log(` Zoom account_id: ${c.zoomAccountId || `${YEL}(not set - call events are dropped)${RST}`}`);
33
+ console.log(` Webhook secret saved: ${c.hasWebhookSecret ? `${GRN}yes${RST}` : `${DIM}no${RST}`}\n`);
34
+ }
35
+
36
+ async function setHandler(argv) {
37
+ if (!argv.accountId && !argv.webhookSecret) throw new Error('Pass --account-id and/or --webhook-secret.');
38
+ const current = await messagingGet('/voice/admin/config');
39
+ if (current?.configured && current.providerSlug && current.providerSlug !== 'zoom') {
40
+ throw new Error(`Site already uses voice provider "${current.providerSlug}". Switch providers in the admin UI (explicit, destructive) before configuring Zoom Phone.`);
41
+ }
42
+ await messagingPost('/voice/admin/config', {
43
+ providerSlug: 'zoom',
44
+ zoomAccountId: argv.accountId ? String(argv.accountId).trim() : undefined,
45
+ webhookSecret: argv.webhookSecret || undefined,
46
+ });
47
+ console.log(`\n${GRN}✓${RST} Zoom Phone settings saved.`);
48
+ await statusHandler();
49
+ }
50
+
51
+ export const zoomPhoneCommand = {
52
+ command: 'zoom-phone <subcommand>',
53
+ describe: 'Zoom Phone screen-pop configuration (account_id + webhook secret)',
54
+ builder: y => y
55
+ .command({ command: 'status', describe: 'Show the Zoom Phone configuration for this site', builder: yy => yy, handler: statusHandler })
56
+ .command({
57
+ command: 'set',
58
+ describe: 'Save the Zoom account_id and/or webhook secret (apply-if-present)',
59
+ builder: yy => yy
60
+ .option('account-id', { describe: 'Zoom account_id from a webhook payload or Account Profile (not the account number)', type: 'string' })
61
+ .option('webhook-secret', { describe: 'Secret token from the Zoom Event Subscription', type: 'string' }),
62
+ handler: setHandler,
63
+ })
64
+ .demandCommand(1, 'Specify a zoom-phone subcommand.')
65
+ .strict(),
66
+ handler: () => {},
67
+ };
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
 
11
11
  const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'commands');
12
- const MESSAGING_ONLY = ['/Teams/List', '/Deployment', '/Users/List', '/Workflows/List', '/Rules', '/Ticket/knowledge-overrides', '/UserStatus/admin', '/api/ivr/config', '/WooCommerce/billing', '/WooCommerce/auth', '/diagnostics/test-magento'];
12
+ const MESSAGING_ONLY = ['/Teams/List', '/Deployment', '/Users/List', '/Workflows/List', '/Rules', '/Ticket/knowledge-overrides', '/UserStatus/admin', '/api/ivr/config', '/WooCommerce/billing', '/WooCommerce/auth', '/diagnostics/test-magento', '/voice/admin'];
13
13
 
14
14
  test('messaging-only routes are never called through the admin api helpers', () => {
15
15
  const offenders = [];
@@ -0,0 +1,23 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildStatusSlaQuery } from './commands/status-sla.js';
4
+
5
+ test('status-sla query maps flags to endpoint params', () => {
6
+ const q = new URLSearchParams(buildStatusSlaQuery({
7
+ from: '2026-09-01', to: '2026-09-30', platform: 'v11', includeMaintenance: true,
8
+ degradedDown: true, slowMs: 1500, csv: true,
9
+ }));
10
+ assert.equal(q.get('from'), '2026-09-01');
11
+ assert.equal(q.get('platform'), 'v11');
12
+ assert.equal(q.get('includeMaintenance'), 'true');
13
+ assert.equal(q.get('degradedCountsAsDown'), 'true');
14
+ assert.equal(q.get('slowResponseMs'), '1500');
15
+ assert.equal(q.get('format'), 'csv');
16
+ assert.equal(q.get('includeDeployments'), null);
17
+ });
18
+
19
+ test('status-sla query defaults to v20 json', () => {
20
+ const q = new URLSearchParams(buildStatusSlaQuery({ from: 'a', to: 'b' }));
21
+ assert.equal(q.get('platform'), 'v20');
22
+ assert.equal(q.get('format'), 'json');
23
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "1.4.87",
3
+ "version": "1.4.89",
4
4
  "description": "Velaro Workspace v20 — command-line interface for managing bots, knowledge base ingestion, MCP API keys, search indexes, and ops health.",
5
5
  "type": "module",
6
6
  "bin": {