@velaro/cli 1.2.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.
- package/README.md +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- 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
|
+
};
|