@velaro/cli 1.4.102 → 1.4.103
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 +19 -0
- package/bin/velaro.js +12 -0
- package/lib/api.js +2 -1
- package/lib/apple-pay.js +32 -0
- package/lib/apple-pay.test.js +32 -0
- package/lib/avatar-switch.js +42 -0
- package/lib/avatar-switch.test.js +49 -0
- package/lib/build-info.js +28 -0
- package/lib/build-info.test.js +24 -0
- package/lib/commands/apple-pay.js +73 -0
- package/lib/commands/avatar.js +60 -2
- package/lib/commands/billing-automation.js +172 -0
- package/lib/commands/copilotstudio.js +14 -1
- package/lib/commands/enterprise-reports.js +1 -0
- package/lib/commands/entitlement.js +29 -1
- package/lib/commands/focus.js +1 -1
- package/lib/commands/index.js +38 -2
- package/lib/commands/logs-insights.js +342 -0
- package/lib/commands/logs.js +313 -3
- package/lib/commands/magento.js +16 -0
- package/lib/commands/mcp-key.js +23 -1
- package/lib/commands/netsuite.js +67 -2
- package/lib/commands/notifications.js +34 -1
- package/lib/commands/ops.js +23 -54
- package/lib/commands/report-backfill.js +23 -5
- package/lib/commands/report.js +77 -14
- package/lib/commands/salesforce.js +101 -0
- package/lib/commands/shopify.js +84 -0
- package/lib/commands/signature.js +79 -0
- package/lib/commands/site-builder.js +30 -0
- package/lib/commands/support.js +22 -0
- package/lib/commands/teams.js +11 -1
- package/lib/commands/workflow.js +33 -0
- package/lib/commands/zoom-phone.js +15 -0
- package/lib/enterprise-reports.test.js +1 -1
- package/lib/entitlement-report-packages.test.js +47 -0
- package/lib/logs-insights.test.js +146 -0
- package/lib/mcp-key.test.js +11 -0
- package/lib/messaging-routes.test.js +10 -1
- package/lib/oauth.js +14 -2
- package/lib/report-range.js +21 -0
- package/lib/report-range.test.js +38 -0
- package/lib/shopify-write-policy.test.js +34 -0
- package/lib/signature-policy.js +70 -0
- package/lib/signature-policy.test.js +48 -0
- package/package.json +1 -1
package/lib/commands/logs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCredentials } from '../api.js';
|
|
2
2
|
import { readConfig, ENVS } from '../config.js';
|
|
3
|
+
import { logsInsightsCommand } from './logs-insights.js';
|
|
3
4
|
import { completeLogSearch, describeCompleteness, describeRawMeta, fetchJsonRetry } from '../log-search.js';
|
|
4
5
|
|
|
5
6
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -64,6 +65,10 @@ async function searchLogsInner(argv) {
|
|
|
64
65
|
const creds = await getCredentials();
|
|
65
66
|
await requireAdmin(creds);
|
|
66
67
|
const msgBase = await getMsgBase(argv);
|
|
68
|
+
// Always say WHICH environment is searched. The CLI defaults to prod, and a staging problem searched
|
|
69
|
+
// there looks identical to "nothing was logged" (cost days on 2026-10-21).
|
|
70
|
+
const searchedEnv = /staging|stg/i.test(msgBase) ? 'staging' : 'prod';
|
|
71
|
+
console.log(`${DIM}environment: ${searchedEnv} (${msgBase})${searchedEnv === 'prod' ? ' - add --env staging for staging logs' : ''}${RESET}`);
|
|
67
72
|
|
|
68
73
|
const to = argv.to ? new Date(argv.to) : new Date();
|
|
69
74
|
const from = argv.from ? new Date(argv.from) : new Date(to.getTime() - parseDuration(argv.last ?? '2h'));
|
|
@@ -112,7 +117,10 @@ async function searchLogsInner(argv) {
|
|
|
112
117
|
wanted: argv.take ?? 50,
|
|
113
118
|
});
|
|
114
119
|
console.log(`${result.complete ? DIM : YEL}${describeCompleteness(result)}${RESET}`);
|
|
115
|
-
if (!result.entries.length) {
|
|
120
|
+
if (!result.entries.length) {
|
|
121
|
+
console.log(searchedEnv === 'prod' ? 'No results in PROD. If you meant staging, re-run with --env staging.' : 'No results.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
116
124
|
const shown = result.entries.slice(0, argv.take ?? 50);
|
|
117
125
|
console.log(`\n${DIM}showing ${shown.length} of ${result.entries.length} (newest first) from ${from.toISOString().slice(0, 16)} UTC${RESET}\n`);
|
|
118
126
|
for (const e of shown) printEntry(e);
|
|
@@ -343,20 +351,53 @@ function parsePositiveIntFlag(flagName, rawValue, { max } = {}) {
|
|
|
343
351
|
return { ok: true, value: n };
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
// A rule with none of the
|
|
354
|
+
// A rule with none of the five notification channels fires into the void —
|
|
347
355
|
// it's pure log noise that pages no one. Mirrors the same guard in
|
|
348
356
|
// client/src/pages/Messaging/Integrations/AlertRules.tsx (extended here to
|
|
357
|
+
// also cover webhook/teams, which that component's check omits).
|
|
358
|
+
// NOTE: a blank emails/sms does NOT necessarily mean "no one" any more --
|
|
359
|
+
// LogAlertEvaluationService.ResolveRecipients falls back to global-default
|
|
360
|
+
// AlertContacts when a rule leaves those blank. Callers should pass
|
|
361
|
+
// hasGlobalDefaultContacts (from fetchHasGlobalDefaultContacts) so a blank
|
|
362
|
+
// emails/sms isn't wrongly flagged as "fires into the void" when defaults
|
|
363
|
+
// actually cover it.
|
|
364
|
+
function hasAnyRecipient({ emails, sms, webhook, teams, mobileUserIds, hasGlobalDefaultContacts }) {
|
|
349
365
|
// also cover webhook, which that component's check omits).
|
|
350
366
|
function hasAnyRecipient({ emails, sms, webhook, mobileUserIds, teams }) {
|
|
351
367
|
return Boolean(
|
|
352
368
|
(emails && String(emails).trim()) ||
|
|
353
369
|
(sms && String(sms).trim()) ||
|
|
354
370
|
(webhook && String(webhook).trim()) ||
|
|
371
|
+
teams ||
|
|
372
|
+
(mobileUserIds && String(mobileUserIds).trim()) ||
|
|
373
|
+
// Blank emails AND blank sms both fall back to global defaults together (see
|
|
374
|
+
// ResolveRecipients) -- if there's at least one global-default contact with a phone or
|
|
375
|
+
// email, a rule with both blank is NOT actually silent, even though neither field is set.
|
|
376
|
+
((!emails || !String(emails).trim()) && (!sms || !String(sms).trim()) && hasGlobalDefaultContacts)
|
|
355
377
|
(mobileUserIds && String(mobileUserIds).trim()) ||
|
|
356
378
|
teams
|
|
357
379
|
);
|
|
358
380
|
}
|
|
359
381
|
|
|
382
|
+
// Fetches whether at least one global-default AlertContact exists with a phone or email --
|
|
383
|
+
// used only to soften the "fires into the void" guard above, never cached across CLI
|
|
384
|
+
// invocations, so a stale answer can't cause a false pass. Returns false (fail toward the
|
|
385
|
+
// stricter behavior) on any fetch error rather than silently skipping the guard.
|
|
386
|
+
async function fetchHasGlobalDefaultContacts(creds, msgBase) {
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/contacts`, {
|
|
389
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
390
|
+
signal: AbortSignal.timeout(15000),
|
|
391
|
+
});
|
|
392
|
+
if (!res.ok) return false;
|
|
393
|
+
const contacts = await res.json();
|
|
394
|
+
return Array.isArray(contacts) && contacts.some(c =>
|
|
395
|
+
c.isGlobalDefault && ((c.phoneNumber && c.phoneNumber.trim()) || (c.email && c.email.trim())));
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
360
401
|
function printAlertRule(r) {
|
|
361
402
|
const state = r.enabled === false ? `${DIM}paused${RESET}` : `${CYN}active${RESET}`;
|
|
362
403
|
const bits = [`>=${r.thresholdCount} in ${r.windowMinutes}m`];
|
|
@@ -365,9 +406,12 @@ function printAlertRule(r) {
|
|
|
365
406
|
if (r.integration) bits.push(`tag=${r.integration}`);
|
|
366
407
|
if (r.siteId) bits.push(`site=${r.siteId}`);
|
|
367
408
|
if (r.notifyPhoneNumbers) bits.push('SMS');
|
|
409
|
+
else if (!r.notifyEmails) bits.push('SMS+email (global defaults)');
|
|
368
410
|
if (r.notifyEmails) bits.push('email');
|
|
369
411
|
if (r.notifyWebhookUrl) bits.push('webhook');
|
|
412
|
+
if (r.notifyTeams) bits.push('Teams');
|
|
370
413
|
if (r.notifyMobileUserIds) bits.push('mobile push');
|
|
414
|
+
if (r.excludeContactNames) bits.push(`excl:${r.excludeContactNames}`);
|
|
371
415
|
if (r.notifyTeams) bits.push('Teams');
|
|
372
416
|
bits.push(`cooldown ${r.minRefireMinutes}m`);
|
|
373
417
|
console.log(`${DIM}[${r.id}]${RESET} ${r.name} ${state}`);
|
|
@@ -481,11 +525,19 @@ async function createAlertRule(argv) {
|
|
|
481
525
|
}
|
|
482
526
|
const [thresholdCheck, windowCheck, cooldownCheck] = checks;
|
|
483
527
|
|
|
528
|
+
// HIGH: a rule with zero notification channels fires into the void. A blank --emails/--sms
|
|
529
|
+
// may still be covered by global-default AlertContacts (see hasAnyRecipient docs), so check
|
|
530
|
+
// those before failing.
|
|
531
|
+
const hasDefaults = await fetchHasGlobalDefaultContacts(creds, msgBase);
|
|
532
|
+
if (!hasAnyRecipient({ emails: argv.emails, sms: argv.sms, webhook: argv.webhook, teams: argv.teams, mobileUserIds: argv.mobileUserIds, hasGlobalDefaultContacts: hasDefaults })
|
|
533
|
+
&& !argv.noRecipientsOk) {
|
|
534
|
+
console.error('No notification channel given (--emails, --sms, --webhook, --teams, or --mobile-user-ids), and no global-default contact exists to fall back to — this rule would fire but notify no one.');
|
|
484
535
|
// HIGH: a rule with zero notification channels fires into the void.
|
|
485
536
|
if (!hasAnyRecipient({ emails: argv.emails, sms: argv.sms, webhook: argv.webhook, mobileUserIds: argv.mobileUserIds, teams: argv.teams })
|
|
486
537
|
&& !argv.noRecipientsOk) {
|
|
487
538
|
console.error('No notification channel given (--emails, --sms, --webhook, --teams, or --mobile-user-ids) — this rule would fire but notify no one.');
|
|
488
539
|
console.error('If that\'s intentional (e.g. you only want it visible in Alert History), pass --no-recipients-ok to proceed anyway.');
|
|
540
|
+
console.error('To set up global defaults so a blank --emails/--sms is covered automatically: velaro logs contacts add --name alex --phone +1... --email alex@velaro.com --global');
|
|
489
541
|
process.exit(1);
|
|
490
542
|
}
|
|
491
543
|
|
|
@@ -500,7 +552,9 @@ async function createAlertRule(argv) {
|
|
|
500
552
|
notifyEmails: argv.emails,
|
|
501
553
|
notifyPhoneNumbers: argv.sms,
|
|
502
554
|
notifyWebhookUrl: argv.webhook,
|
|
555
|
+
notifyTeams: Boolean(argv.teams),
|
|
503
556
|
notifyMobileUserIds: argv.mobileUserIds,
|
|
557
|
+
excludeContactNames: argv.excludeContacts,
|
|
504
558
|
notifyTeams: Boolean(argv.teams),
|
|
505
559
|
enabled: true,
|
|
506
560
|
};
|
|
@@ -590,7 +644,9 @@ async function updateAlertRule(argv) {
|
|
|
590
644
|
notifyEmails: argv.emails ?? target.notifyEmails,
|
|
591
645
|
notifyPhoneNumbers: argv.sms ?? target.notifyPhoneNumbers,
|
|
592
646
|
notifyWebhookUrl: argv.webhook ?? target.notifyWebhookUrl,
|
|
647
|
+
notifyTeams: argv.teams !== undefined ? Boolean(argv.teams) : target.notifyTeams,
|
|
593
648
|
notifyMobileUserIds: argv.mobileUserIds ?? target.notifyMobileUserIds,
|
|
649
|
+
excludeContactNames: argv.excludeContacts ?? target.excludeContactNames,
|
|
594
650
|
notifyTeams: argv.teams !== undefined ? Boolean(argv.teams) : Boolean(target.notifyTeams),
|
|
595
651
|
enabled: argv.enabled !== undefined ? argv.enabled : target.enabled,
|
|
596
652
|
};
|
|
@@ -598,6 +654,10 @@ async function updateAlertRule(argv) {
|
|
|
598
654
|
// HIGH: same "fires into the void" guard as create -- an update that strips
|
|
599
655
|
// every notification channel (or a rule that already had none, being
|
|
600
656
|
// re-enabled without adding one) silently produces an alert nobody sees.
|
|
657
|
+
const hasDefaults = await fetchHasGlobalDefaultContacts(creds, msgBase);
|
|
658
|
+
if (!hasAnyRecipient({ emails: body.notifyEmails, sms: body.notifyPhoneNumbers, webhook: body.notifyWebhookUrl, teams: body.notifyTeams, mobileUserIds: body.notifyMobileUserIds, hasGlobalDefaultContacts: hasDefaults })
|
|
659
|
+
&& !argv.noRecipientsOk) {
|
|
660
|
+
console.error('This update would leave the rule with no notification channel (--emails, --sms, --webhook, --teams, or --mobile-user-ids), and no global-default contact exists to fall back to — it would fire but notify no one.');
|
|
601
661
|
if (!hasAnyRecipient({ emails: body.notifyEmails, sms: body.notifyPhoneNumbers, webhook: body.notifyWebhookUrl, mobileUserIds: body.notifyMobileUserIds, teams: body.notifyTeams })
|
|
602
662
|
&& !argv.noRecipientsOk) {
|
|
603
663
|
console.error('This update would leave the rule with no notification channel (--emails, --sms, --webhook, --teams, or --mobile-user-ids) — it would fire but notify no one.');
|
|
@@ -704,6 +764,198 @@ async function deleteAlertRule(argv) {
|
|
|
704
764
|
console.log(`\nDeleted rule ${argv.id}.\n`);
|
|
705
765
|
}
|
|
706
766
|
|
|
767
|
+
// ── Alert contacts ───────────────────────────────────────────────────────────
|
|
768
|
+
|
|
769
|
+
function printAlertContact(c) {
|
|
770
|
+
const scope = c.isGlobalDefault ? `${CYN}global default${RESET}` : `${DIM}not global${RESET}`;
|
|
771
|
+
const bits = [];
|
|
772
|
+
if (c.phoneNumber) bits.push(`SMS ${c.phoneNumber}`);
|
|
773
|
+
if (c.email) bits.push(`email ${c.email}`);
|
|
774
|
+
console.log(`${DIM}[${c.id}]${RESET} ${c.name}${c.displayName ? ` (${c.displayName})` : ''} ${scope}`);
|
|
775
|
+
console.log(` ${bits.join(' ') || '(no phone/email set)'}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function listAlertContacts(argv) {
|
|
779
|
+
const creds = await getCredentials();
|
|
780
|
+
await requireAdmin(creds);
|
|
781
|
+
const msgBase = await getMsgBase(argv);
|
|
782
|
+
|
|
783
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/contacts`, {
|
|
784
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
785
|
+
signal: AbortSignal.timeout(15000),
|
|
786
|
+
});
|
|
787
|
+
if (!res.ok) {
|
|
788
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
const contacts = await res.json();
|
|
792
|
+
if (!Array.isArray(contacts) || !contacts.length) {
|
|
793
|
+
console.log('No alert contacts yet. Add one with: velaro logs contacts add --name alex --phone +1... --email alex@velaro.com');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
console.log(`\n${DIM}${contacts.length} alert contact(s)${RESET}\n`);
|
|
797
|
+
for (const c of contacts) printAlertContact(c);
|
|
798
|
+
console.log('');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function addAlertContact(argv) {
|
|
802
|
+
const creds = await getCredentials();
|
|
803
|
+
await requireAdmin(creds);
|
|
804
|
+
const msgBase = await getMsgBase(argv);
|
|
805
|
+
|
|
806
|
+
if (!argv.name) {
|
|
807
|
+
console.error('--name is required. Example: velaro logs contacts add --name alex --phone +14436295872 --email alex@velaro.com');
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
if (!argv.phone && !argv.email) {
|
|
811
|
+
console.error('At least one of --phone or --email is required.');
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const body = {
|
|
816
|
+
name: argv.name,
|
|
817
|
+
displayName: argv.displayName,
|
|
818
|
+
phoneNumber: argv.phone,
|
|
819
|
+
email: argv.email,
|
|
820
|
+
isGlobalDefault: argv.global,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (!argv.yes) {
|
|
824
|
+
console.log(`\n${DIM}About to create this contact (dry run — nothing sent yet):${RESET}\n`);
|
|
825
|
+
printAlertContact({ ...body, id: '(new)' });
|
|
826
|
+
console.log(`\nRe-run with --yes to actually create it.\n`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/contacts`, {
|
|
831
|
+
method: 'POST',
|
|
832
|
+
headers: {
|
|
833
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
834
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
835
|
+
'Content-Type': 'application/json',
|
|
836
|
+
},
|
|
837
|
+
body: JSON.stringify(body),
|
|
838
|
+
signal: AbortSignal.timeout(15000),
|
|
839
|
+
});
|
|
840
|
+
if (!res.ok) {
|
|
841
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
const data = await res.json();
|
|
845
|
+
console.log(`\n${CYN}✓${RESET} Created contact [${data.id}] "${data.name}"\n`);
|
|
846
|
+
printAlertContact(data);
|
|
847
|
+
console.log('');
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function updateAlertContact(argv) {
|
|
851
|
+
const creds = await getCredentials();
|
|
852
|
+
await requireAdmin(creds);
|
|
853
|
+
const msgBase = await getMsgBase(argv);
|
|
854
|
+
|
|
855
|
+
if (!argv.id) {
|
|
856
|
+
console.error('Contact id is required. Example: velaro logs contacts update 1 --phone +1...');
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const listRes = await fetch(`${msgBase}/superadmin/log-alerts/contacts`, {
|
|
861
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
862
|
+
signal: AbortSignal.timeout(15000),
|
|
863
|
+
});
|
|
864
|
+
if (!listRes.ok) {
|
|
865
|
+
console.error(`HTTP ${listRes.status}: ${await listRes.text().catch(() => '')}`);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
const contacts = await listRes.json().catch(() => []);
|
|
869
|
+
const target = Array.isArray(contacts) ? contacts.find(c => String(c.id) === String(argv.id)) : null;
|
|
870
|
+
if (!target) {
|
|
871
|
+
console.error(`No contact with id ${argv.id} found. Run 'velaro logs contacts list' to see current ids.`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const body = {
|
|
876
|
+
...target,
|
|
877
|
+
displayName: argv.displayName ?? target.displayName,
|
|
878
|
+
phoneNumber: argv.phone ?? target.phoneNumber,
|
|
879
|
+
email: argv.email ?? target.email,
|
|
880
|
+
isGlobalDefault: argv.global !== undefined ? argv.global : target.isGlobalDefault,
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (!argv.yes) {
|
|
884
|
+
console.log(`\n${DIM}Current contact:${RESET}\n`);
|
|
885
|
+
printAlertContact(target);
|
|
886
|
+
console.log(`\n${DIM}About to update to (dry run — nothing sent yet):${RESET}\n`);
|
|
887
|
+
printAlertContact(body);
|
|
888
|
+
console.log(`\nRe-run with --yes to actually apply this update.\n`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/contacts/${argv.id}`, {
|
|
893
|
+
method: 'PUT',
|
|
894
|
+
headers: {
|
|
895
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
896
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
897
|
+
'Content-Type': 'application/json',
|
|
898
|
+
},
|
|
899
|
+
body: JSON.stringify(body),
|
|
900
|
+
signal: AbortSignal.timeout(15000),
|
|
901
|
+
});
|
|
902
|
+
if (!res.ok) {
|
|
903
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const data = await res.json();
|
|
907
|
+
console.log(`\n${CYN}✓${RESET} Updated contact [${data.id}] "${data.name}"\n`);
|
|
908
|
+
printAlertContact(data);
|
|
909
|
+
console.log('');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function removeAlertContact(argv) {
|
|
913
|
+
const creds = await getCredentials();
|
|
914
|
+
await requireAdmin(creds);
|
|
915
|
+
const msgBase = await getMsgBase(argv);
|
|
916
|
+
|
|
917
|
+
if (!argv.id) {
|
|
918
|
+
console.error('Contact id is required. Example: velaro logs contacts remove 1');
|
|
919
|
+
process.exit(1);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const listRes = await fetch(`${msgBase}/superadmin/log-alerts/contacts`, {
|
|
923
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
924
|
+
signal: AbortSignal.timeout(15000),
|
|
925
|
+
});
|
|
926
|
+
let target = null;
|
|
927
|
+
if (listRes.ok) {
|
|
928
|
+
const contacts = await listRes.json().catch(() => []);
|
|
929
|
+
target = Array.isArray(contacts) ? contacts.find(c => String(c.id) === String(argv.id)) : null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (target) {
|
|
933
|
+
console.log(`\n${DIM}About to delete this contact:${RESET}\n`);
|
|
934
|
+
printAlertContact(target);
|
|
935
|
+
if (target.isGlobalDefault) {
|
|
936
|
+
console.log(`${YEL}This is a global-default contact -- removing it stops it from covering any rule that leaves --emails/--sms blank.${RESET}`);
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
console.log(`\n${YEL}Could not fetch contact ${argv.id} to preview it (it may already be gone) — proceeding on id alone.${RESET}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (!argv.yes) {
|
|
943
|
+
console.log(`\nRe-run with --yes to actually delete it.\n`);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/contacts/${argv.id}`, {
|
|
948
|
+
method: 'DELETE',
|
|
949
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
950
|
+
signal: AbortSignal.timeout(15000),
|
|
951
|
+
});
|
|
952
|
+
if (!res.ok) {
|
|
953
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
console.log(`\nDeleted contact ${argv.id}.\n`);
|
|
957
|
+
}
|
|
958
|
+
|
|
707
959
|
// ── command export ────────────────────────────────────────────────────────────
|
|
708
960
|
|
|
709
961
|
export const logsCommand = {
|
|
@@ -797,6 +1049,11 @@ export const logsCommand = {
|
|
|
797
1049
|
.option('window-minutes', { describe: 'Rolling window in minutes', type: 'number' })
|
|
798
1050
|
.option('threshold', { describe: 'Fire when count >= this within the window', type: 'number' })
|
|
799
1051
|
.option('cooldown-minutes', { describe: 'Minimum minutes between re-fires', type: 'number' })
|
|
1052
|
+
.option('emails', { describe: 'Comma-separated notification email addresses. Leave blank to fall back to global-default contacts (see: velaro logs contacts list)', type: 'string' })
|
|
1053
|
+
.option('sms', { describe: 'Comma-separated E.164 phone numbers for SMS. Leave blank to fall back to global-default contacts', type: 'string' })
|
|
1054
|
+
.option('webhook', { describe: 'Custom webhook URL for a generic JSON POST on fire (Slack, PagerDuty, etc). NOT for Teams -- use --teams instead, which posts a properly formatted Adaptive Card through Velaro\'s existing Teams pipeline', type: 'string' })
|
|
1055
|
+
.option('teams', { describe: 'Post fired/resolved notifications to the velaro-alerts-to-teams Teams channel', type: 'boolean' })
|
|
1056
|
+
.option('exclude-contacts', { describe: 'Comma-separated global-default contact names to exclude from this rule (e.g. "andrea") -- only relevant when --emails/--sms are left blank', type: 'string' })
|
|
800
1057
|
.option('emails', { describe: 'Comma-separated notification email addresses', type: 'string' })
|
|
801
1058
|
.option('sms', { describe: 'Comma-separated E.164 phone numbers for SMS', type: 'string' })
|
|
802
1059
|
.option('webhook', { describe: 'Webhook URL (Slack/Teams) to POST on fire', type: 'string' })
|
|
@@ -819,6 +1076,11 @@ export const logsCommand = {
|
|
|
819
1076
|
.option('window-minutes', { describe: 'Rolling window in minutes', type: 'number' })
|
|
820
1077
|
.option('threshold', { describe: 'Fire when count >= this within the window', type: 'number' })
|
|
821
1078
|
.option('cooldown-minutes', { describe: 'Minimum minutes between re-fires', type: 'number' })
|
|
1079
|
+
.option('emails', { describe: 'Comma-separated notification email addresses. Leave blank to fall back to global-default contacts (see: velaro logs contacts list)', type: 'string' })
|
|
1080
|
+
.option('sms', { describe: 'Comma-separated E.164 phone numbers for SMS. Leave blank to fall back to global-default contacts', type: 'string' })
|
|
1081
|
+
.option('webhook', { describe: 'Custom webhook URL for a generic JSON POST on fire (Slack, PagerDuty, etc). NOT for Teams -- use --teams instead, which posts a properly formatted Adaptive Card through Velaro\'s existing Teams pipeline', type: 'string' })
|
|
1082
|
+
.option('teams', { describe: 'Post fired/resolved notifications to the velaro-alerts-to-teams Teams channel', type: 'boolean' })
|
|
1083
|
+
.option('exclude-contacts', { describe: 'Comma-separated global-default contact names to exclude from this rule (e.g. "andrea") -- only relevant when --emails/--sms are left blank', type: 'string' })
|
|
822
1084
|
.option('emails', { describe: 'Comma-separated notification email addresses', type: 'string' })
|
|
823
1085
|
.option('sms', { describe: 'Comma-separated E.164 phone numbers for SMS', type: 'string' })
|
|
824
1086
|
.option('webhook', { describe: 'Webhook URL (Slack/Teams) to POST on fire', type: 'string' })
|
|
@@ -849,5 +1111,53 @@ export const logsCommand = {
|
|
|
849
1111
|
})
|
|
850
1112
|
.demandCommand(1, 'Specify a subcommand: list, suggest, create, update, toggle, or delete'),
|
|
851
1113
|
})
|
|
852
|
-
.
|
|
1114
|
+
.command({
|
|
1115
|
+
command: 'contacts <subcommand>',
|
|
1116
|
+
describe: 'Manage global-default notification contacts for alert rules (Velaro staff only)',
|
|
1117
|
+
builder: y => y
|
|
1118
|
+
.command({
|
|
1119
|
+
command: 'list',
|
|
1120
|
+
describe: 'List all alert contacts',
|
|
1121
|
+
builder: yy => yy.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
1122
|
+
handler: listAlertContacts,
|
|
1123
|
+
})
|
|
1124
|
+
.command({
|
|
1125
|
+
command: 'add',
|
|
1126
|
+
describe: 'Add a new alert contact',
|
|
1127
|
+
builder: yy => yy
|
|
1128
|
+
.option('name', { describe: 'Short unique handle, e.g. "alex" -- used in --exclude-contacts', type: 'string', demandOption: true })
|
|
1129
|
+
.option('display-name', { describe: 'Human-readable label, e.g. "Alex Bloom"', type: 'string' })
|
|
1130
|
+
.option('phone', { describe: 'E.164 phone number for SMS, e.g. +14436295872', type: 'string' })
|
|
1131
|
+
.option('email', { describe: 'Email address', type: 'string' })
|
|
1132
|
+
.option('global', { describe: 'Include this contact in every rule that leaves --emails/--sms blank (the normal case)', type: 'boolean', default: true })
|
|
1133
|
+
.option('yes', { describe: 'Skip the dry-run preview and actually create the contact', type: 'boolean', default: false })
|
|
1134
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
1135
|
+
handler: addAlertContact,
|
|
1136
|
+
})
|
|
1137
|
+
.command({
|
|
1138
|
+
command: 'update <id>',
|
|
1139
|
+
describe: 'Change an existing contact\'s phone/email/global-default flag',
|
|
1140
|
+
builder: yy => yy
|
|
1141
|
+
.positional('id', { describe: 'Contact id (from: velaro logs contacts list)', type: 'number' })
|
|
1142
|
+
.option('display-name', { describe: 'Human-readable label', type: 'string' })
|
|
1143
|
+
.option('phone', { describe: 'E.164 phone number for SMS', type: 'string' })
|
|
1144
|
+
.option('email', { describe: 'Email address', type: 'string' })
|
|
1145
|
+
.option('global', { describe: 'Include/exclude this contact from global defaults', type: 'boolean' })
|
|
1146
|
+
.option('yes', { describe: 'Skip the dry-run preview and actually apply the update', type: 'boolean', default: false })
|
|
1147
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
1148
|
+
handler: updateAlertContact,
|
|
1149
|
+
})
|
|
1150
|
+
.command({
|
|
1151
|
+
command: 'remove <id>',
|
|
1152
|
+
describe: 'Delete an alert contact',
|
|
1153
|
+
builder: yy => yy
|
|
1154
|
+
.positional('id', { describe: 'Contact id', type: 'number' })
|
|
1155
|
+
.option('yes', { describe: 'Skip the confirmation preview and actually delete', type: 'boolean', default: false })
|
|
1156
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
1157
|
+
handler: removeAlertContact,
|
|
1158
|
+
})
|
|
1159
|
+
.demandCommand(1, 'Specify a subcommand: list, add, update, or remove'),
|
|
1160
|
+
})
|
|
1161
|
+
.command(logsInsightsCommand)
|
|
1162
|
+
.demandCommand(1, 'Specify a subcommand: search, tail, diagnose, digest-recipients, set-digest-recipients, alerts, contacts, or insights'),
|
|
853
1163
|
};
|
package/lib/commands/magento.js
CHANGED
|
@@ -40,6 +40,21 @@ async function listHandler() {
|
|
|
40
40
|
// ── test ──────────────────────────────────────────────────────────────────────
|
|
41
41
|
|
|
42
42
|
async function testHandler(argv) {
|
|
43
|
+
if (argv.id) {
|
|
44
|
+
try {
|
|
45
|
+
const r = await request('POST', `/Magento/Configs/${encodeURIComponent(argv.id)}/Test`);
|
|
46
|
+
if (r && r.valid === false) throw new Error(r.error);
|
|
47
|
+
console.log(`
|
|
48
|
+
${GRN}✓ Connected${RST} to "${r.storeName}" (${r.edition} ${r.version})
|
|
49
|
+
`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(`
|
|
52
|
+
${RED}✗ Connection failed: ${e.message}${RST}
|
|
53
|
+
`);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
43
58
|
if (!argv.url || !argv.token) {
|
|
44
59
|
console.error(`${RED}✗ --url and --token are required${RST}`);
|
|
45
60
|
process.exitCode = 1;
|
|
@@ -189,6 +204,7 @@ export const magentoCommand = {
|
|
|
189
204
|
command: 'test',
|
|
190
205
|
describe: 'Test a Magento store connection',
|
|
191
206
|
builder: yy => yy
|
|
207
|
+
.option('id', { describe: 'Test a connected store by config id (stored token)', type: 'number' })
|
|
192
208
|
.option('url', { describe: 'Magento store URL', type: 'string' })
|
|
193
209
|
.option('token', { describe: 'Admin API bearer token', type: 'string' }),
|
|
194
210
|
handler: testHandler,
|
package/lib/commands/mcp-key.js
CHANGED
|
@@ -31,7 +31,12 @@ const mcpKeyListCommand = {
|
|
|
31
31
|
command: 'list',
|
|
32
32
|
describe: 'List MCP API keys for your site',
|
|
33
33
|
handler: runCommand(async () => {
|
|
34
|
-
const
|
|
34
|
+
const response = await get('/McpApiKeys');
|
|
35
|
+
if (response?.entitled === false) {
|
|
36
|
+
console.log('MCP keys are included with the Professional plan and above. Contact support@velaro.com to upgrade.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const keys = normalizeMcpKeysResponse(response);
|
|
35
40
|
if (!keys.length) { console.log('No MCP keys found.'); return; }
|
|
36
41
|
console.log(`Found ${keys.length} key(s):\n`);
|
|
37
42
|
for (const k of keys) {
|
|
@@ -101,6 +106,21 @@ const mcpKeyInstallCommand = {
|
|
|
101
106
|
const result = await request('POST', '/McpApiKeys', payload, env);
|
|
102
107
|
const rawKey = result.rawKey;
|
|
103
108
|
|
|
109
|
+
// Claude Code reads user-scope MCP server definitions from the
|
|
110
|
+
// top-level "mcpServers" key in ~/.claude.json — NOT from
|
|
111
|
+
// ~/.claude/settings.json (that file is for permissions/hooks/env,
|
|
112
|
+
// and any mcpServers entry written there is silently ignored).
|
|
113
|
+
// prod → entry named "velaro"
|
|
114
|
+
// staging → entry named "velaro-staging"
|
|
115
|
+
const serverName = env === 'prod' ? 'velaro' : `velaro-${env}`;
|
|
116
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
117
|
+
let claudeJson = {};
|
|
118
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
119
|
+
try { claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8')); } catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
claudeJson.mcpServers = claudeJson.mcpServers || {};
|
|
123
|
+
claudeJson.mcpServers[serverName] = {
|
|
104
124
|
// Claude Code reads MCP server definitions from the top-level "mcpServers" key
|
|
105
125
|
// in ~/.claude.json (NOT ~/.claude/settings.json — that file has no effect here).
|
|
106
126
|
// This is the same shape `claude mcp add` itself writes.
|
|
@@ -132,6 +152,8 @@ const mcpKeyInstallCommand = {
|
|
|
132
152
|
},
|
|
133
153
|
};
|
|
134
154
|
|
|
155
|
+
fs.mkdirSync(path.dirname(claudeJsonPath), { recursive: true });
|
|
156
|
+
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
135
157
|
// Claude Code itself writes ~/.claude.json continuously (history, project state),
|
|
136
158
|
// so this must never be a plain in-place write: write to a temp file and rename,
|
|
137
159
|
// which is atomic on the same filesystem — a crash or concurrent write mid-save
|
package/lib/commands/netsuite.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (velaro-messaging). Site is resolved server-side from the auth context — never caller-supplied.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { messagingGet, messagingPost } from '../api.js';
|
|
12
|
+
import { messagingGet, messagingPost, messagingDel } from '../api.js';
|
|
13
13
|
|
|
14
14
|
const GRN = '\x1b[32m';
|
|
15
15
|
const DIM = '\x1b[2m';
|
|
@@ -84,6 +84,42 @@ async function m2mVerifyHandler(argv) {
|
|
|
84
84
|
printChecks(await messagingPost('/NetSuite/M2M/Verify', { id: Number(argv.configId) }));
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
async function suiteappHandler(argv) {
|
|
88
|
+
const includeKey = argv['include-key'] !== false;
|
|
89
|
+
const r = await messagingGet(`/api/v1/CrmEmbed/suiteapp-package?includeKey=${includeKey}&format=json`);
|
|
90
|
+
const d = r?.data ?? r;
|
|
91
|
+
if (!d?.contentBase64) throw new Error('SuiteApp package was not returned.');
|
|
92
|
+
const { writeFileSync } = await import('node:fs');
|
|
93
|
+
const out = argv.out || d.fileName || 'velaro-netsuite-screenpop-suiteapp.zip';
|
|
94
|
+
writeFileSync(out, Buffer.from(d.contentBase64, 'base64'));
|
|
95
|
+
console.log(`\n ${GRN}Saved${RST} ${out}`);
|
|
96
|
+
if (d.keyIncluded) console.log(' Your Velaro key is already inside: no NetSuite API Secret, nothing to paste. Keep the zip private.\n');
|
|
97
|
+
else console.log(` Key-less zip. In NetSuite create an API Secret: type ID ${d.secretFormId} (NetSuite adds the prefix, giving ${d.secretFullId}), paste your crm_embed_ key as Password.\n`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function configDeleteHandler(argv) {
|
|
101
|
+
await messagingDel(`/Netsuite/Integrations/${Number(argv.configId)}`);
|
|
102
|
+
console.log(`
|
|
103
|
+
${GRN}Deleted${RST} NetSuite config ${argv.configId}.
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function m2mClearLegacyHandler(argv) {
|
|
108
|
+
const r = await messagingPost('/NetSuite/M2M/ClearLegacy', { id: Number(argv.configId) });
|
|
109
|
+
console.log(`
|
|
110
|
+
${GRN}Legacy consumer secret/token cleared${RST} for config ${r?.id ?? argv.configId}.
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function embedStatusHandler() {
|
|
115
|
+
const r = await messagingGet('/api/v1/CrmEmbed/embed-status');
|
|
116
|
+
const d = r?.data ?? r;
|
|
117
|
+
console.log(`\n${BOLD}NetSuite call-pop (CRM embed) status${RST}`);
|
|
118
|
+
if (!d?.hasEmbedKey) { console.log(` ${DIM}No crm_embed API key set for this site.${RST}\n`); return; }
|
|
119
|
+
if (d.connected) console.log(` ${GRN}SuiteApp connected${RST}, last polled ${d.lastPolledAt}\n`);
|
|
120
|
+
else console.log(` ${DIM}Embed key exists; SuiteApp not connected (last polled: ${d.lastPolledAt ?? 'never'})${RST}\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
export const netsuiteCommand = {
|
|
88
124
|
command: 'netsuite <subcommand>',
|
|
89
125
|
describe: 'NetSuite integration setup (click-to-dial / CTI)',
|
|
@@ -111,6 +147,30 @@ export const netsuiteCommand = {
|
|
|
111
147
|
.strict(),
|
|
112
148
|
handler: () => {},
|
|
113
149
|
})
|
|
150
|
+
.command({
|
|
151
|
+
command: 'suiteapp',
|
|
152
|
+
describe: 'Download the screen-pop SuiteApp zip (key included by default: no API Secret to create)',
|
|
153
|
+
builder: a => a.option('include-key', { type: 'boolean', default: true }).option('out', { type: 'string' }),
|
|
154
|
+
handler: suiteappHandler,
|
|
155
|
+
})
|
|
156
|
+
.command({
|
|
157
|
+
command: 'config <action>',
|
|
158
|
+
describe: 'NetSuite connection (config) management',
|
|
159
|
+
builder: yy => yy
|
|
160
|
+
.command({ command: 'delete <configId>', describe: 'Delete a NetSuite connection from the site', handler: configDeleteHandler })
|
|
161
|
+
.demandCommand(1, 'Specify a config action: delete.')
|
|
162
|
+
.strict(),
|
|
163
|
+
handler: () => {},
|
|
164
|
+
})
|
|
165
|
+
.command({
|
|
166
|
+
command: 'embed <action>',
|
|
167
|
+
describe: 'NetSuite call-pop (CRM embed) SuiteApp',
|
|
168
|
+
builder: yy => yy
|
|
169
|
+
.command({ command: 'status', describe: 'Is the crm_embed key set, and last heartbeat', handler: embedStatusHandler })
|
|
170
|
+
.demandCommand(1, 'Specify an embed action: status.')
|
|
171
|
+
.strict(),
|
|
172
|
+
handler: () => {},
|
|
173
|
+
})
|
|
114
174
|
.command({
|
|
115
175
|
command: 'm2m <action>',
|
|
116
176
|
describe: 'NetSuite machine-to-machine (OAuth 2.0 Client Credentials) setup',
|
|
@@ -132,7 +192,12 @@ export const netsuiteCommand = {
|
|
|
132
192
|
describe: 'Run the per-check verification (token, REST, customer read)',
|
|
133
193
|
handler: m2mVerifyHandler,
|
|
134
194
|
})
|
|
135
|
-
.
|
|
195
|
+
.command({
|
|
196
|
+
command: 'clear-legacy <configId>',
|
|
197
|
+
describe: 'Disconnect the old consumer-key/token credentials (requires M2M saved)',
|
|
198
|
+
handler: m2mClearLegacyHandler,
|
|
199
|
+
})
|
|
200
|
+
.demandCommand(1, 'Specify an m2m action: init, save, verify, or clear-legacy.')
|
|
136
201
|
.strict(),
|
|
137
202
|
handler: () => {},
|
|
138
203
|
})
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { messagingGet, messagingPost } from '../api.js';
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
|
|
3
|
+
// Alert routing: Channel x Handling x Kind (+ ivrBotAlerts). Backend: GET/PUT /api/v1/alert-routing.
|
|
4
|
+
const ROUTING_GROUPS = {
|
|
5
|
+
channel: ['channels', ['web', 'sms', 'whatsapp', 'appleMessages', 'email', 'phone']],
|
|
6
|
+
handling: ['handling', ['mine', 'unassigned', 'bot', 'teammates']],
|
|
7
|
+
kind: ['kinds', ['conversations', 'tickets', 'calls', 'visitors']],
|
|
8
|
+
};
|
|
9
|
+
const csv = (v) => String(v).split(',').map((x) => x.trim()).filter(Boolean);
|
|
2
10
|
|
|
3
11
|
const FLAGS = [
|
|
4
12
|
['new-conversations', 'newConversations', 'NewConversations'],
|
|
@@ -24,6 +32,15 @@ const getCommand = {
|
|
|
24
32
|
}
|
|
25
33
|
console.log(` ${'sticky-duration'.padEnd(28)} ${s.stickyDuration}ms`);
|
|
26
34
|
console.log(` ${'sms-alert-phone'.padEnd(28)} ${s.smsAlertPhone ?? '(none)'}`);
|
|
35
|
+
try {
|
|
36
|
+
const r = (await messagingGet('/api/v1/alert-routing')).data;
|
|
37
|
+
console.log('\n\x1b[1mAlert routing\x1b[0m (alert fires only if channel, handling and kind all match)');
|
|
38
|
+
for (const [label, [group]] of Object.entries(ROUTING_GROUPS)) {
|
|
39
|
+
const on = Object.entries(r[group] ?? {}).filter(([, v]) => v).map(([k]) => k);
|
|
40
|
+
console.log(` ${label.padEnd(28)} ${on.join(', ') || '(none)'}`);
|
|
41
|
+
}
|
|
42
|
+
console.log(` ${'ivr-bot-alerts'.padEnd(28)} ${r.ivrBotAlerts ? '\x1b[32mon\x1b[0m' : '\x1b[90moff\x1b[0m'}`);
|
|
43
|
+
} catch { /* older server without alert routing */ }
|
|
27
44
|
console.log('');
|
|
28
45
|
},
|
|
29
46
|
};
|
|
@@ -35,6 +52,10 @@ const setCommand = {
|
|
|
35
52
|
for (const [flag] of FLAGS) {
|
|
36
53
|
y.option(flag, { type: 'boolean', describe: `Turn ${flag.replace(/-/g, ' ')} on/off` });
|
|
37
54
|
}
|
|
55
|
+
y.option('channel', { type: 'string', describe: 'Channels to alert on, comma list of: web,sms,whatsapp,appleMessages,email,phone (others turned off)' });
|
|
56
|
+
y.option('handling', { type: 'string', describe: 'Handling to alert on, comma list of: mine,unassigned,bot,teammates (others turned off)' });
|
|
57
|
+
y.option('kind', { type: 'string', describe: 'Kinds to alert on, comma list of: conversations,tickets,calls,visitors (others turned off)' });
|
|
58
|
+
y.option('ivr-bot-alerts', { type: 'boolean', describe: 'Alert for inbound IVR / phone bot chats' });
|
|
38
59
|
y.option('sms-alert-phone', { type: 'string', describe: 'Phone number for SMS alerts (E.164, e.g. +14155551234)' });
|
|
39
60
|
return y;
|
|
40
61
|
},
|
|
@@ -47,6 +68,18 @@ const setCommand = {
|
|
|
47
68
|
if (argv['sms-alert-phone'] !== undefined) current.smsAlertPhone = argv['sms-alert-phone'];
|
|
48
69
|
|
|
49
70
|
await messagingPost('/NotificationSettings', current);
|
|
71
|
+
|
|
72
|
+
const patch = {};
|
|
73
|
+
for (const [flag, [group, keys]] of Object.entries(ROUTING_GROUPS)) {
|
|
74
|
+
if (argv[flag] === undefined) continue;
|
|
75
|
+
const wanted = csv(argv[flag]);
|
|
76
|
+
const bad = wanted.filter((k) => !keys.includes(k));
|
|
77
|
+
if (bad.length) { console.error(`Unknown ${flag}: ${bad.join(', ')}. Valid: ${keys.join(', ')}`); process.exit(1); }
|
|
78
|
+
patch[group] = Object.fromEntries(keys.map((k) => [k, wanted.includes(k)]));
|
|
79
|
+
}
|
|
80
|
+
if (argv['ivr-bot-alerts'] !== undefined) patch.ivrBotAlerts = argv['ivr-bot-alerts'];
|
|
81
|
+
if (Object.keys(patch).length) await messagingPut('/api/v1/alert-routing', patch);
|
|
82
|
+
|
|
50
83
|
console.log('\x1b[32m✓ Notification settings updated.\x1b[0m Run `velaro notifications get` to review.');
|
|
51
84
|
},
|
|
52
85
|
};
|