@velaro/cli 1.4.72 → 1.4.74

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
@@ -40,11 +40,13 @@ import { appointmentsCommand } from '../lib/commands/appointments.js';
40
40
  import { bookingCommand } from '../lib/commands/booking.js';
41
41
  import { reportCommand } from '../lib/commands/report.js';
42
42
  import { scheduleAdherenceCommand } from '../lib/commands/schedule-adherence.js';
43
+ import { reportSubscriptionCommand } from '../lib/commands/report-subscription.js';
43
44
  import { experienceImpactCommand } from '../lib/commands/experience-impact.js';
44
45
  import { reportBackfillCommand } from '../lib/commands/report-backfill.js';
45
46
  import { staffCommand } from '../lib/commands/staff.js';
46
47
  import { reportExplainCommand } from '../lib/commands/report-explain.js';
47
48
  import { conversationEfficiencyCommand } from '../lib/commands/conversation-efficiency.js';
49
+ import { executiveInsightsCommand } from '../lib/commands/executive-insights.js';
48
50
  import { dbCommand } from '../lib/commands/db.js';
49
51
  import { focusCommand } from '../lib/commands/focus.js';
50
52
  import { triageCommand } from '../lib/commands/triage.js';
@@ -146,8 +148,10 @@ await yargs(hideBin(process.argv))
146
148
  .command(bookingCommand)
147
149
  .command(reportCommand)
148
150
  .command(conversationEfficiencyCommand)
151
+ .command(executiveInsightsCommand)
149
152
  .command(scheduleAdherenceCommand)
150
153
  .command(experienceImpactCommand)
154
+ .command(reportSubscriptionCommand)
151
155
  .command(reportBackfillCommand)
152
156
  .command({ command: 'staff', desc: 'Velaro staff: cross-site tools (backfill)', builder: staffCommand, handler: () => {} })
153
157
  .command(reportExplainCommand)
@@ -0,0 +1,91 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { messagingGet, messagingPost } from '../api.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ // -- velaro executive-insights ---------------------------------------------------
6
+ // CLI for the Friction Radar, the Executive Brief and the CSV data export. Endpoints live in
7
+ // velaro-messaging at /api/executive-insights/*; the site always comes from the login, never from
8
+ // CLI args. Gated on the Chat Intelligence entitlement (403 when not entitled).
9
+ // Scheduling reuses report subscriptions: `velaro executive-insights subscribe`.
10
+
11
+ function rangeArgs(argv) {
12
+ const days = Math.min(Math.max(Number(argv.days) || 7, 1), 92);
13
+ const to = argv.to ? new Date(argv.to) : new Date();
14
+ const from = argv.from ? new Date(argv.from) : new Date(to.getTime() - days * 86400000);
15
+ return new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
16
+ }
17
+
18
+ const rangeOptions = (y) => y
19
+ .option('days', { type: 'number', default: 7, describe: 'Look-back window in days (max 92)' })
20
+ .option('from', { type: 'string', describe: 'ISO start (overrides --days)' })
21
+ .option('to', { type: 'string', describe: 'ISO end (default now)' });
22
+
23
+ const frictionCommand = {
24
+ command: 'friction-radar',
25
+ describe: 'Topics ranked by estimated agent cost with week-over-week growth and example chats',
26
+ builder: (y) => rangeOptions(y).option('limit', { type: 'number', default: 20 }),
27
+ handler: runCommand(async (argv) => {
28
+ const qs = rangeArgs(argv);
29
+ qs.set('limit', String(argv.limit ?? 20));
30
+ const r = await messagingGet(`/executive-insights/friction-radar?${qs}`);
31
+ console.log(`Friction radar: ${r.conversationsScanned} conversations scanned${r.truncated ? ' (truncated)' : ''}`);
32
+ for (const t of r.topics ?? []) {
33
+ const growth = t.isNew ? 'new' : t.growthPct == null ? 'n/a' : `${t.growthPct}%`;
34
+ console.log(` #${t.costRank} ${t.topic}: ${t.volume} chats (volume rank ${t.volumeRank}, ${growth}), ${t.escalations} escalated, ${t.abandoned} abandoned, ~${Math.round(t.estimatedCostMinutes)} agent min, examples ${t.exampleConversationIds.join(', ') || '-'}`);
35
+ }
36
+ }),
37
+ };
38
+
39
+ const briefCommand = {
40
+ command: 'brief',
41
+ describe: 'Executive brief: volume, sentiment, QA, Experience Impact, rising keywords, top friction',
42
+ builder: rangeOptions,
43
+ handler: runCommand(async (argv) => {
44
+ const r = await messagingGet(`/executive-insights/brief?${rangeArgs(argv)}`);
45
+ console.log(JSON.stringify(r.brief, null, 2));
46
+ }),
47
+ };
48
+
49
+ const exportCommand = {
50
+ command: 'export',
51
+ describe: 'CSV of conversations with sentiment, QA score, tag, escalations, handle time and keywords',
52
+ builder: (y) => rangeOptions(y).option('out', { type: 'string', describe: 'Write the CSV to this file (default: stdout)' }),
53
+ handler: runCommand(async (argv) => {
54
+ const r = await messagingGet(`/executive-insights/export?${rangeArgs(argv)}`);
55
+ if (argv.out) {
56
+ writeFileSync(argv.out, r.csv, 'utf8');
57
+ console.log(`Wrote ${r.rows} rows to ${argv.out}${r.truncated ? ' (truncated at the row cap)' : ''}`);
58
+ } else {
59
+ process.stdout.write(r.csv);
60
+ }
61
+ }),
62
+ };
63
+
64
+ const subscribeCommand = {
65
+ command: 'subscribe <type>',
66
+ describe: 'Email yourself the executive-brief (Monday 7 AM site time by default) or data-export on a schedule',
67
+ builder: (y) => y
68
+ .positional('type', { choices: ['executive-brief', 'data-export'] })
69
+ .option('frequency', { type: 'string', default: 'weekly', choices: ['daily', 'biweekly', 'weekly', 'monthly'] })
70
+ .option('hour', { type: 'number', default: 7, describe: 'Site-local delivery hour 0-23' }),
71
+ handler: runCommand(async (argv) => {
72
+ await messagingPost('/report-subscriptions', {
73
+ reportType: argv.type,
74
+ frequency: argv.frequency,
75
+ deliveryHour: argv.hour,
76
+ });
77
+ console.log(`Subscribed to ${argv.type} (${argv.frequency}, ${argv.hour}:00 site time).`);
78
+ }),
79
+ };
80
+
81
+ export const executiveInsightsCommand = {
82
+ command: 'executive-insights',
83
+ describe: 'Friction radar, executive brief and data export (Chat Intelligence)',
84
+ builder: (y) => y
85
+ .command(frictionCommand)
86
+ .command(briefCommand)
87
+ .command(exportCommand)
88
+ .command(subscribeCommand)
89
+ .demandCommand(1, 'Specify a subcommand. Run "velaro executive-insights --help" for options.'),
90
+ handler: () => {},
91
+ };
@@ -0,0 +1,56 @@
1
+ import { messagingRequest } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+ import { buildSubscriptionUpdateBody, formatSubscriptionLine } from '../report-subscription.js';
4
+
5
+ // -- velaro report-subscription list | update | delete ------------------------------
6
+ // Backend: velaro-messaging GET/PUT/DELETE /report-subscriptions (the caller's own subscriptions).
7
+
8
+ const listCmd = {
9
+ command: 'list',
10
+ describe: 'List your report email subscriptions (incl. delivery hour, format, extra recipients)',
11
+ builder: (y) => y.option('json', { type: 'boolean', default: false }),
12
+ handler: runCommand(async (argv) => {
13
+ const rows = await messagingRequest('GET', '/report-subscriptions');
14
+ if (argv.json) { console.log(JSON.stringify(rows, null, 2)); return; }
15
+ if (!Array.isArray(rows) || !rows.length) { console.log('No report subscriptions.'); return; }
16
+ rows.forEach((s) => console.log(formatSubscriptionLine(s)));
17
+ }),
18
+ };
19
+
20
+ const updateCmd = {
21
+ command: 'update <id>',
22
+ describe: 'Update one subscription by id (only passed fields change)',
23
+ builder: (y) => y
24
+ .positional('id', { type: 'number' })
25
+ .option('frequency', { type: 'string', describe: 'daily | weekly | bi-monthly | monthly' })
26
+ .option('delivery-hour', { type: 'number', describe: 'Site-local hour 0-23 to deliver at or after' })
27
+ .option('clear-delivery-hour', { type: 'boolean', default: false })
28
+ .option('format', { type: 'string', choices: ['pdf', 'html'] })
29
+ .option('extra-recipients', { type: 'string', describe: 'Comma-separated emails of other users on this account ("" clears)' })
30
+ .option('active', { type: 'boolean', describe: 'true to resume, false to pause' }),
31
+ handler: runCommand(async (argv) => {
32
+ const body = buildSubscriptionUpdateBody({
33
+ frequency: argv.frequency, deliveryHour: argv.deliveryHour, clearDeliveryHour: argv.clearDeliveryHour,
34
+ format: argv.format, extraRecipients: argv.extraRecipients, active: argv.active,
35
+ });
36
+ await messagingRequest('PUT', `/report-subscriptions/${argv.id}`, body);
37
+ console.log(`Subscription #${argv.id} updated.`);
38
+ }),
39
+ };
40
+
41
+ const deleteCmd = {
42
+ command: 'delete <id>',
43
+ describe: 'Delete one subscription by id',
44
+ builder: (y) => y.positional('id', { type: 'number' }),
45
+ handler: runCommand(async (argv) => {
46
+ await messagingRequest('DELETE', `/report-subscriptions/${argv.id}`);
47
+ console.log(`Subscription #${argv.id} deleted.`);
48
+ }),
49
+ };
50
+
51
+ export const reportSubscriptionCommand = {
52
+ command: 'report-subscription',
53
+ describe: 'Manage your scheduled report email subscriptions',
54
+ builder: (y) => y.command(listCmd).command(updateCmd).command(deleteCmd).demandCommand(1),
55
+ handler: () => {},
56
+ };
@@ -0,0 +1,29 @@
1
+ // Pure helpers for `velaro report-subscription` (backend: velaro-messaging /report-subscriptions).
2
+ // Site + user scope come from the caller's auth, never an argument.
3
+
4
+ /** Partial-update body: only fields the caller actually passed are included. */
5
+ export function buildSubscriptionUpdateBody(opts = {}) {
6
+ const body = {};
7
+ if (opts.frequency != null && opts.frequency !== '') body.frequency = String(opts.frequency);
8
+ if (opts.deliveryHour != null) {
9
+ const h = Number(opts.deliveryHour);
10
+ if (!Number.isInteger(h) || h < 0 || h > 23) throw new Error('deliveryHour must be an integer 0-23');
11
+ body.deliveryHour = h;
12
+ }
13
+ if (opts.clearDeliveryHour === true) body.clearDeliveryHour = true;
14
+ if (opts.format != null && opts.format !== '') {
15
+ const f = String(opts.format).toLowerCase();
16
+ if (f !== 'pdf' && f !== 'html') throw new Error('format must be pdf or html');
17
+ body.format = f;
18
+ }
19
+ if (opts.extraRecipients != null) body.extraRecipients = String(opts.extraRecipients);
20
+ if (typeof opts.active === 'boolean') body.isActive = opts.active;
21
+ if (Object.keys(body).length === 0) throw new Error('nothing to update: pass at least one field');
22
+ return body;
23
+ }
24
+
25
+ export function formatSubscriptionLine(s) {
26
+ const hour = s.deliveryHour == null ? 'any hour' : `${String(s.deliveryHour).padStart(2, '0')}:00`;
27
+ const extra = s.extraRecipients ? ` | also to ${s.extraRecipients}` : '';
28
+ return `#${s.id} ${s.reportType}: ${s.isActive ? 'active' : 'paused'}, ${s.frequency}, ${hour}, ${s.format ?? 'pdf'}${extra}`;
29
+ }
@@ -0,0 +1,24 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildSubscriptionUpdateBody, formatSubscriptionLine } from './report-subscription.js';
4
+
5
+ test('update body includes only provided fields', () => {
6
+ assert.deepEqual(buildSubscriptionUpdateBody({ deliveryHour: 7, format: 'HTML', extraRecipients: 'a@x.com,b@x.com' }),
7
+ { deliveryHour: 7, format: 'html', extraRecipients: 'a@x.com,b@x.com' });
8
+ });
9
+ test('empty extraRecipients string is sent so it clears the list', () => {
10
+ assert.deepEqual(buildSubscriptionUpdateBody({ extraRecipients: '' }), { extraRecipients: '' });
11
+ });
12
+ test('active=false pauses, hour 0 is valid', () => {
13
+ assert.deepEqual(buildSubscriptionUpdateBody({ active: false, deliveryHour: 0 }), { isActive: false, deliveryHour: 0 });
14
+ });
15
+ test('rejects bad hour, bad format, empty update', () => {
16
+ assert.throws(() => buildSubscriptionUpdateBody({ deliveryHour: 24 }));
17
+ assert.throws(() => buildSubscriptionUpdateBody({ format: 'docx' }));
18
+ assert.throws(() => buildSubscriptionUpdateBody({}));
19
+ });
20
+ test('formats a line with hour, format and extras', () => {
21
+ assert.equal(formatSubscriptionLine({ id: 5, reportType: 'experience-impact', isActive: true, frequency: 'weekly', deliveryHour: 7, format: 'pdf', extraRecipients: 'a@x.com' }),
22
+ '#5 experience-impact: active, weekly, 07:00, pdf | also to a@x.com');
23
+ assert.equal(formatSubscriptionLine({ id: 6, reportType: 'x', isActive: false, frequency: 'daily' }), '#6 x: paused, daily, any hour, pdf');
24
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "1.4.72",
3
+ "version": "1.4.74",
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": {