@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,462 @@
1
+ import { getCredentials, request, messagingRequest } from '../api.js';
2
+
3
+ // ── helpers ───────────────────────────────────────────────────────────────────
4
+
5
+ function fmtSeconds(sec) {
6
+ if (sec == null || sec === 0) return '—';
7
+ const m = Math.floor(sec / 60);
8
+ const s = Math.round(sec % 60);
9
+ if (m === 0) return `${s}s`;
10
+ return `${m}m ${s}s`;
11
+ }
12
+
13
+ function fmtPct(val) {
14
+ if (val == null) return '—';
15
+ return `${(val * 100).toFixed(1)}%`;
16
+ }
17
+
18
+ function fmtNum(val) {
19
+ if (val == null) return '—';
20
+ return String(val);
21
+ }
22
+
23
+ function fmtRow(label, value) {
24
+ console.log(` ${label.padEnd(32)}${value}`);
25
+ }
26
+
27
+ function section(title) {
28
+ console.log(`\n\x1b[1m── ${title} ${'─'.repeat(Math.max(0, 55 - title.length))}\x1b[0m`);
29
+ }
30
+
31
+ function dateParams(argv) {
32
+ const params = [];
33
+ if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
34
+ if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
35
+ return params.length ? `?${params.join('&')}` : '';
36
+ }
37
+
38
+ async function fetchReport(path) {
39
+ const res = await getCredentials();
40
+ const raw = await fetch(`${res.adminApiBase}${path}`, {
41
+ headers: {
42
+ Authorization: `Bearer ${res.velaroToken}`,
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ signal: AbortSignal.timeout(30000),
46
+ });
47
+ if (raw.status === 402) {
48
+ console.error('This feature requires an Advanced Analytics subscription.');
49
+ process.exit(1);
50
+ }
51
+ if (!raw.ok) {
52
+ let msg = `GET ${path} → ${raw.status}`;
53
+ try { const t = await raw.text(); if (t) msg += `: ${t}`; } catch { /* captured above */ }
54
+ throw new Error(msg);
55
+ }
56
+ const text = await raw.text();
57
+ return text ? JSON.parse(text) : null;
58
+ }
59
+
60
+ // ── callcenter subcommands ────────────────────────────────────────────────────
61
+
62
+ const callcenterTeamsCommand = {
63
+ command: 'teams',
64
+ describe: 'Team breakdown for the call center report',
65
+ builder: (y) => y
66
+ .option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
67
+ .option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
68
+ .option('sla', { type: 'number', describe: 'SLA threshold in seconds (overrides server default)' }),
69
+ handler: async (argv) => {
70
+ const params = [];
71
+ if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
72
+ if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
73
+ if (argv.sla) params.push(`slaSeconds=${argv.sla}`);
74
+ const qs = params.length ? `?${params.join('&')}` : '';
75
+ const data = await fetchReport(`/CallCenterAnalytics/TeamBreakdown${qs}`);
76
+ const teams = Array.isArray(data) ? data : (data?.teams ?? []);
77
+
78
+ section('Call Center — Team Breakdown');
79
+ if (!teams.length) { console.log(' No data for the selected range.'); return; }
80
+
81
+ const COL = [28, 10, 10, 10, 10, 10, 12];
82
+ const header = ['Team', 'Volume', 'AHT', 'Wait', 'Abandon', 'SLA%', 'CSAT'];
83
+ console.log('\n ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
84
+ console.log(' ' + '─'.repeat(COL.reduce((a, b) => a + b, 0)));
85
+
86
+ for (const t of teams) {
87
+ const row = [
88
+ (t.teamName ?? t.name ?? '—').slice(0, 26),
89
+ fmtNum(t.totalConversations ?? t.volume),
90
+ fmtSeconds(t.avgHandleTimeSeconds ?? t.aht),
91
+ fmtSeconds(t.avgWaitTimeSeconds ?? t.waitTime),
92
+ fmtPct(t.abandonRate),
93
+ fmtPct(t.slaComplianceRate ?? t.slaRate),
94
+ t.csat != null ? fmtPct(t.csat) : '—',
95
+ ];
96
+ console.log(' ' + row.map((v, i) => String(v).padEnd(COL[i])).join(''));
97
+ }
98
+ console.log('');
99
+ },
100
+ };
101
+
102
+ const callcenterChannelsCommand = {
103
+ command: 'channels',
104
+ describe: 'Channel breakdown for the call center report',
105
+ builder: (y) => y
106
+ .option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
107
+ .option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' }),
108
+ handler: async (argv) => {
109
+ const data = await fetchReport(`/CallCenterAnalytics/ChannelBreakdown${dateParams(argv)}`);
110
+ const channels = Array.isArray(data) ? data : (data?.channels ?? []);
111
+
112
+ section('Call Center — Channel Breakdown');
113
+ if (!channels.length) { console.log(' No data for the selected range.'); return; }
114
+
115
+ const COL = [22, 10, 10, 10, 10, 12];
116
+ const header = ['Channel', 'Volume', 'AHT', 'Wait', 'Abandon', 'Bot%'];
117
+ console.log('\n ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
118
+ console.log(' ' + '─'.repeat(COL.reduce((a, b) => a + b, 0)));
119
+
120
+ for (const c of channels) {
121
+ const row = [
122
+ (c.channel ?? c.channelName ?? '—').slice(0, 20),
123
+ fmtNum(c.totalConversations ?? c.volume),
124
+ fmtSeconds(c.avgHandleTimeSeconds ?? c.aht),
125
+ fmtSeconds(c.avgWaitTimeSeconds ?? c.waitTime),
126
+ fmtPct(c.abandonRate),
127
+ fmtPct(c.botContainmentRate ?? c.botRate),
128
+ ];
129
+ console.log(' ' + row.map((v, i) => String(v).padEnd(COL[i])).join(''));
130
+ }
131
+ console.log('');
132
+ },
133
+ };
134
+
135
+ const callcenterCommand = {
136
+ command: 'callcenter',
137
+ describe: 'Call center analytics report (volume, speed, abandon, bot metrics)',
138
+ builder: (y) => y
139
+ .command(callcenterTeamsCommand)
140
+ .command(callcenterChannelsCommand)
141
+ .option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
142
+ .option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' }),
143
+ handler: async (argv) => {
144
+ const data = await fetchReport(`/CallCenterAnalytics/Report${dateParams(argv)}`);
145
+
146
+ section('Call Center Report');
147
+ if (!data) { console.log(' No data returned.'); return; }
148
+
149
+ if (argv.start || argv.end) {
150
+ const range = [argv.start, argv.end].filter(Boolean).join(' → ');
151
+ console.log(` Period: ${range}`);
152
+ }
153
+
154
+ // Volume
155
+ section('Volume');
156
+ fmtRow('Total conversations', fmtNum(data.totalConversations));
157
+ fmtRow('Bot-handled (no agent)', fmtNum(data.botOnlyConversations));
158
+ fmtRow('Agent-handled', fmtNum(data.agentConversations));
159
+ fmtRow('Missed / abandoned', fmtNum(data.missedConversations ?? data.abandonedConversations));
160
+
161
+ // Speed
162
+ section('Speed');
163
+ fmtRow('Avg handle time (AHT)', fmtSeconds(data.avgHandleTimeSeconds));
164
+ fmtRow('Avg first response time', fmtSeconds(data.avgFirstResponseTimeSeconds));
165
+ fmtRow('Avg queue wait time', fmtSeconds(data.avgQueueWaitTimeSeconds ?? data.avgWaitTimeSeconds));
166
+ fmtRow('Avg resolution time', fmtSeconds(data.avgResolutionTimeSeconds));
167
+
168
+ // Abandon
169
+ section('Abandon');
170
+ fmtRow('Abandon rate', fmtPct(data.abandonRate));
171
+ fmtRow('Avg wait before abandon', fmtSeconds(data.avgWaitBeforeAbandonSeconds));
172
+
173
+ // Bot metrics
174
+ section('Bot Metrics');
175
+ fmtRow('Bot containment rate', fmtPct(data.botContainmentRate));
176
+ fmtRow('Bot handoff rate', fmtPct(data.botHandoffRate));
177
+ fmtRow('Bot conversations', fmtNum(data.botConversations ?? data.botTotalConversations));
178
+
179
+ // Alerts / thresholds
180
+ if (data.alerts?.length) {
181
+ section('Threshold Alerts');
182
+ for (const a of data.alerts) {
183
+ console.log(` \x1b[33m⚠\x1b[0m ${a.metric ?? a.name}: ${a.message ?? a.detail}`);
184
+ }
185
+ }
186
+
187
+ console.log('');
188
+ },
189
+ };
190
+
191
+ // ── servicelevel command ──────────────────────────────────────────────────────
192
+
193
+ const servicelevelCommand = {
194
+ command: 'servicelevel',
195
+ describe: 'Service level compliance report over time',
196
+ builder: (y) => y
197
+ .option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
198
+ .option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
199
+ .option('granularity', { type: 'string', choices: ['hour', 'day', 'week', 'month'], default: 'day', describe: 'Time bucket size' }),
200
+ handler: async (argv) => {
201
+ const params = [];
202
+ if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
203
+ if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
204
+ if (argv.granularity) params.push(`granularity=${argv.granularity}`);
205
+ const qs = params.length ? `?${params.join('&')}` : '';
206
+ const data = await fetchReport(`/ServiceLevel${qs}`);
207
+
208
+ section('Service Level Report');
209
+
210
+ // Summary row if present
211
+ if (data?.summary) {
212
+ const s = data.summary;
213
+ fmtRow('Overall SLA compliance', fmtPct(s.complianceRate ?? s.slaRate));
214
+ fmtRow('SLA threshold', s.thresholdSeconds != null ? fmtSeconds(s.thresholdSeconds) : '—');
215
+ fmtRow('Total conversations', fmtNum(s.totalConversations));
216
+ fmtRow('Within SLA', fmtNum(s.withinSla));
217
+ fmtRow('Breached SLA', fmtNum(s.breachedSla));
218
+ }
219
+
220
+ const buckets = Array.isArray(data) ? data : (data?.buckets ?? data?.data ?? []);
221
+ if (buckets.length) {
222
+ console.log('');
223
+ const COL = [22, 12, 10, 10, 10];
224
+ const header = ['Period', 'SLA %', 'Total', 'Within', 'Breached'];
225
+ console.log(' ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
226
+ console.log(' ' + '─'.repeat(COL.reduce((a, b) => a + b, 0)));
227
+ for (const b of buckets) {
228
+ const label = b.period ?? b.date ?? b.bucket ?? '—';
229
+ const row = [
230
+ String(label).slice(0, 20),
231
+ fmtPct(b.complianceRate ?? b.slaRate),
232
+ fmtNum(b.totalConversations ?? b.total),
233
+ fmtNum(b.withinSla),
234
+ fmtNum(b.breachedSla),
235
+ ];
236
+ console.log(' ' + row.map((v, i) => String(v).padEnd(COL[i])).join(''));
237
+ }
238
+ }
239
+ console.log('');
240
+ },
241
+ };
242
+
243
+ // ── conversations command ─────────────────────────────────────────────────────
244
+
245
+ const conversationsCommand = {
246
+ command: 'conversations',
247
+ describe: 'Conversation volume report over time',
248
+ builder: (y) => y
249
+ .option('start', { type: 'string', describe: 'Start date (YYYY-MM-DD)' })
250
+ .option('end', { type: 'string', describe: 'End date (YYYY-MM-DD)' })
251
+ .option('granularity', { type: 'string', choices: ['hour', 'day', 'week', 'month'], default: 'day', describe: 'Time bucket size' })
252
+ .option('channel', { type: 'string', describe: 'Filter by channel (e.g. Web, TwilioSms, WhatsApp)' }),
253
+ handler: async (argv) => {
254
+ const params = [];
255
+ if (argv.start) params.push(`start=${encodeURIComponent(argv.start)}`);
256
+ if (argv.end) params.push(`end=${encodeURIComponent(argv.end)}`);
257
+ if (argv.granularity) params.push(`granularity=${argv.granularity}`);
258
+ if (argv.channel) params.push(`channel=${encodeURIComponent(argv.channel)}`);
259
+ const qs = params.length ? `?${params.join('&')}` : '';
260
+ const data = await fetchReport(`/Conversations${qs}`);
261
+
262
+ section('Conversations Report');
263
+
264
+ if (data?.summary) {
265
+ const s = data.summary;
266
+ fmtRow('Total conversations', fmtNum(s.total ?? s.totalConversations));
267
+ fmtRow('Resolved', fmtNum(s.resolved));
268
+ fmtRow('Missed', fmtNum(s.missed));
269
+ fmtRow('Bot contained', fmtNum(s.botContained));
270
+ fmtRow('Avg handle time', fmtSeconds(s.avgHandleTimeSeconds));
271
+ fmtRow('CSAT', s.csat != null ? fmtPct(s.csat) : '—');
272
+ console.log('');
273
+ }
274
+
275
+ const buckets = Array.isArray(data) ? data : (data?.buckets ?? data?.data ?? []);
276
+ if (buckets.length) {
277
+ const COL = [22, 10, 10, 10, 10];
278
+ const header = ['Period', 'Total', 'Resolved', 'Missed', 'Bot'];
279
+ console.log(' ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
280
+ console.log(' ' + '─'.repeat(COL.reduce((a, b) => a + b, 0)));
281
+ for (const b of buckets) {
282
+ const label = b.period ?? b.date ?? b.bucket ?? '—';
283
+ const row = [
284
+ String(label).slice(0, 20),
285
+ fmtNum(b.total ?? b.totalConversations),
286
+ fmtNum(b.resolved),
287
+ fmtNum(b.missed),
288
+ fmtNum(b.botContained ?? b.bot),
289
+ ];
290
+ console.log(' ' + row.map((v, i) => String(v).padEnd(COL[i])).join(''));
291
+ }
292
+ }
293
+ console.log('');
294
+ },
295
+ };
296
+
297
+ // ── callactivity command ───────────────────────────────────────────────────────
298
+ // Unified call history across every origin: agent-dialed (CTI), inbound, IVR
299
+ // outbound single calls, and auto-dialer campaigns. Merges velaro-messaging's
300
+ // Voice/CallRecords/history with this repo's own campaign-call data (not yet
301
+ // mirrored into CallRecord — see CallActivityController.cs) so a manager can
302
+ // answer "how many calls did this agent really make?" from one command.
303
+ const callactivityCommand = {
304
+ command: 'callactivity',
305
+ describe: 'Unified call history report (agent-dialed, inbound, IVR outbound, auto-dialer campaigns)',
306
+ builder: (y) => y
307
+ .option('agentId', { type: 'number', describe: 'Filter to a specific agent' })
308
+ .option('direction', { type: 'string', choices: ['Inbound', 'Outbound'], describe: 'Filter by call direction' })
309
+ .option('origin', { type: 'string', choices: ['CtiManual', 'InboundExternal', 'AutoDialerCampaign', 'IvrOutboundSingle'], describe: 'Filter by call origin' })
310
+ .option('providerSlug', { type: 'string', describe: 'Filter by provider (e.g. Twilio, SkySwitch)' })
311
+ .option('dateRangeDays', { type: 'number', default: 30, describe: 'Lookback window in days (default 30)' })
312
+ .option('disposition', { type: 'string', describe: 'Filter by disposition code' })
313
+ .option('page', { type: 'number', default: 1 })
314
+ .option('pageSize', { type: 'number', default: 50 }),
315
+ handler: async (argv) => {
316
+ const params = new URLSearchParams();
317
+ params.set('page', String(argv.page));
318
+ params.set('pageSize', String(argv.pageSize));
319
+ params.set('dateRangeDays', String(argv.dateRangeDays));
320
+ if (argv.agentId) params.set('agentId', String(argv.agentId));
321
+ if (argv.direction) params.set('direction', argv.direction);
322
+ if (argv.origin) params.set('origin', argv.origin);
323
+ if (argv.providerSlug) params.set('providerSlug', argv.providerSlug);
324
+ if (argv.disposition) params.set('disposition', argv.disposition);
325
+ const qs = params.toString();
326
+
327
+ let messagingItems = [];
328
+ let messagingErr = null;
329
+ try {
330
+ const r = await messagingRequest('GET', `/Voice/CallRecords/history?${qs}`);
331
+ messagingItems = Array.isArray(r?.items) ? r.items : [];
332
+ } catch (e) {
333
+ messagingErr = e;
334
+ }
335
+
336
+ let campaignItems = [];
337
+ let campaignErr = null;
338
+ if (!argv.origin || argv.origin === 'AutoDialerCampaign') {
339
+ try {
340
+ const r = await request('GET', `/CallActivity/CampaignCalls?${qs}`);
341
+ campaignItems = Array.isArray(r?.items) ? r.items : [];
342
+ } catch (e) {
343
+ campaignErr = e;
344
+ }
345
+ }
346
+
347
+ if (messagingErr && campaignErr) {
348
+ console.error(`Failed to load call activity from either source: ${messagingErr.message}`);
349
+ process.exit(1);
350
+ }
351
+ if (messagingErr) console.error(`\x1b[33m⚠\x1b[0m Live call history unavailable (${messagingErr.message}) — showing auto-dialer campaign calls only.`);
352
+ if (campaignErr) console.error(`\x1b[33m⚠\x1b[0m Campaign call history unavailable (${campaignErr.message}).`);
353
+
354
+ const items = [...messagingItems, ...campaignItems]
355
+ .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
356
+
357
+ section(`Call Activity (${items.length} calls, last ${argv.dateRangeDays}d)`);
358
+ if (!items.length) { console.log(' No calls match the current filters.'); return; }
359
+
360
+ const ORIGIN_LABELS = {
361
+ CtiManual: 'Agent-Dialed',
362
+ InboundExternal: 'Inbound',
363
+ AutoDialerCampaign: 'Auto-Dialer Campaign',
364
+ IvrOutboundSingle: 'IVR Outbound',
365
+ };
366
+ for (const c of items) {
367
+ const originLabel = ORIGIN_LABELS[c.origin] ?? c.origin ?? 'Unknown';
368
+ console.log(` [${originLabel}] ${c.startedAt ?? '—'} | ${c.agentName ?? 'Auto-Dialer'} | ${c.providerSlug ?? '—'} | ${c.fromNumber ?? '—'} → ${c.toNumber ?? '—'} | ${fmtSeconds(c.durationSeconds)} | ${c.disposition ?? '—'}${c.associationStatus ? ` | ${c.associationStatus}` : ''}`);
369
+ }
370
+ console.log('');
371
+ },
372
+ };
373
+
374
+ // -- worked-hours command --------------------------------------------------
375
+ // Time Tracking: worked hours breakdown, entitlement-gated (EnableTimeTracking),
376
+ // hard-capped server-side at MAX_WORKED_HOURS_RANGE_DAYS days per query -- validated
377
+ // client-side too so we fail fast instead of round-tripping a guaranteed 400.
378
+ const MAX_WORKED_HOURS_RANGE_DAYS = 90;
379
+
380
+ const workedHoursCommand = {
381
+ command: 'worked-hours',
382
+ describe: `Worked hours breakdown report: site KPIs + per-agent/per-day detail (Time Tracking, max ${MAX_WORKED_HOURS_RANGE_DAYS}-day range per query)`,
383
+ builder: (y) => y
384
+ .option('start', { type: 'string', demandOption: true, describe: 'Start date (ISO 8601 or YYYY-MM-DD)' })
385
+ .option('end', { type: 'string', demandOption: true, describe: `End date (ISO 8601 or YYYY-MM-DD) -- must be at most ${MAX_WORKED_HOURS_RANGE_DAYS} days after --start` }),
386
+ handler: async (argv) => {
387
+ const start = new Date(argv.start);
388
+ const end = new Date(argv.end);
389
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
390
+ console.error('--start and --end must be valid dates (ISO 8601 or YYYY-MM-DD).');
391
+ process.exit(1);
392
+ }
393
+ const rangeDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
394
+ if (rangeDays > MAX_WORKED_HOURS_RANGE_DAYS) {
395
+ console.error(`Date range too large (${rangeDays.toFixed(1)} days). Worked Hours can be queried for at most ${MAX_WORKED_HOURS_RANGE_DAYS} days at a time.`);
396
+ process.exit(1);
397
+ }
398
+
399
+ let data;
400
+ try {
401
+ data = await messagingRequest('POST', '/Reports/worked-hours-breakdown', { Start: start.toISOString(), End: end.toISOString() });
402
+ } catch (err) {
403
+ if (/403/.test(err.message)) {
404
+ console.error('Time Tracking is not enabled for this account.');
405
+ process.exit(1);
406
+ }
407
+ if (/400/.test(err.message)) {
408
+ console.error(`Server rejected the request: ${err.message}`);
409
+ process.exit(1);
410
+ }
411
+ throw err;
412
+ }
413
+
414
+ section('Worked Hours Breakdown');
415
+ if (argv.start || argv.end) {
416
+ console.log(` Period: ${argv.start} -> ${argv.end}`);
417
+ }
418
+
419
+ const k = data?.kpis ?? {};
420
+ fmtRow('Total worked hours', fmtNum(k.totalWorkedHours));
421
+ fmtRow('Agent count', fmtNum(k.agentCount));
422
+ fmtRow('Unworked absent days', fmtNum(k.daysUnworkedAbsent));
423
+ fmtRow('Excused days', fmtNum(k.excusedDays));
424
+
425
+ const agents = Array.isArray(data?.agentMetrics) ? data.agentMetrics : [];
426
+ if (!agents.length) {
427
+ console.log(`\n ${data?.message ?? 'No agent data for the selected range (no WorkSchedules configured for this site).'}\n`);
428
+ return;
429
+ }
430
+
431
+ console.log('');
432
+ const COL = [28, 14, 18, 14];
433
+ const header = ['Agent', 'Worked Hrs', 'Unworked Absent', 'Excused'];
434
+ console.log(' ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
435
+ console.log(' ' + '-'.repeat(COL.reduce((a, b) => a + b, 0)));
436
+ for (const a of agents) {
437
+ const row = [
438
+ (a.agentName ?? `Agent ${a.agentId}`).slice(0, 26),
439
+ fmtNum(a.workedHours),
440
+ fmtNum(a.unworkedAbsentDays),
441
+ fmtNum(a.excusedDays),
442
+ ];
443
+ console.log(' ' + row.map((v, i) => String(v).padEnd(COL[i])).join(''));
444
+ }
445
+ console.log('');
446
+ },
447
+ };
448
+
449
+ // ── top-level report command ──────────────────────────────────────────────────
450
+
451
+ export const reportCommand = {
452
+ command: 'report',
453
+ describe: 'Analytics reports: call center, service level, conversations, call activity',
454
+ builder: (y) => y
455
+ .command(callcenterCommand)
456
+ .command(servicelevelCommand)
457
+ .command(conversationsCommand)
458
+ .command(callactivityCommand)
459
+ .command(workedHoursCommand)
460
+ .demandCommand(1, 'Specify a report type. Run "velaro report --help" for options.'),
461
+ handler: () => {},
462
+ };