@velaro/cli 1.2.0 → 1.4.9
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
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
import { get, post } from '../api.js';
|
|
2
|
-
import { runCommand } from '../run.js';
|
|
3
|
-
|
|
4
|
-
export const deploymentCommand = {
|
|
5
|
-
command: 'deployment <subcommand>',
|
|
6
|
-
describe: 'Manage chat widget deployments',
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
yargs
|
|
9
|
-
.command(deploymentListCommand)
|
|
10
|
-
.command(deploymentRenameCommand)
|
|
11
|
-
.command(deploymentAssignCommand)
|
|
12
|
-
.demandCommand(1, 'Specify a subcommand: list, rename, assign-team'),
|
|
13
|
-
handler: () => {},
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// ── list ───────────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const deploymentListCommand = {
|
|
19
|
-
command: 'list',
|
|
20
|
-
describe: 'List chat widget deployments',
|
|
21
|
-
handler: runCommand(async () => {
|
|
22
|
-
const [deployments, teams] = await Promise.all([
|
|
23
|
-
get('/Deployment'),
|
|
24
|
-
get('/Teams/List'),
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
if (!deployments?.length) {
|
|
28
|
-
console.log('No deployments found.');
|
|
29
|
-
console.log('\nA deployment is a chat widget embed — one per page or team you want to serve.');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
34
|
-
|
|
35
|
-
const rows = deployments.map(d => ({
|
|
36
|
-
id: `[${d.id}]`,
|
|
37
|
-
key: d.deploymentId ?? '—',
|
|
38
|
-
team: teamMap[d.teamId] ?? `team ${d.teamId}`,
|
|
39
|
-
name: d.displayName ?? '(unnamed)',
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
const idW = Math.max(...rows.map(r => r.id.length));
|
|
43
|
-
const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
|
|
44
|
-
const teamW = Math.max('team'.length, ...rows.map(r => r.team.length));
|
|
45
|
-
|
|
46
|
-
console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'team'.padEnd(teamW)} name`);
|
|
47
|
-
console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
|
|
48
|
-
for (const r of rows) {
|
|
49
|
-
console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
|
|
50
|
-
}
|
|
51
|
-
console.log(`\n${rows.length} deployment(s)`);
|
|
52
|
-
console.log('\nThe embed-key goes in your widget JavaScript snippet.');
|
|
53
|
-
}),
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// ── rename ─────────────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
const deploymentRenameCommand = {
|
|
59
|
-
command: 'rename <id> <name>',
|
|
60
|
-
describe: 'Rename a deployment',
|
|
61
|
-
builder: (y) =>
|
|
62
|
-
y
|
|
63
|
-
.positional('id', { type: 'number', describe: 'Deployment numeric ID' })
|
|
64
|
-
.positional('name', { type: 'string', describe: 'New display name' }),
|
|
65
|
-
handler: runCommand(async (argv) => {
|
|
66
|
-
const dep = await get(`/Deployment/${argv.id}`);
|
|
67
|
-
if (!dep) {
|
|
68
|
-
console.error(`Deployment ${argv.id} not found.`);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
|
|
73
|
-
console.log(`Deployment ${argv.id} renamed to "${argv.name}".`);
|
|
74
|
-
}),
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// ── assign-team ────────────────────────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
const deploymentAssignCommand = {
|
|
80
|
-
command: 'assign-team <id> <team-id>',
|
|
81
|
-
describe: 'Assign a team to a deployment',
|
|
82
|
-
builder: (y) =>
|
|
83
|
-
y
|
|
84
|
-
.positional('id', { type: 'number', describe: 'Deployment numeric ID' })
|
|
85
|
-
.positional('team-id', { type: 'number', describe: 'Team ID to assign' }),
|
|
86
|
-
handler: runCommand(async (argv) => {
|
|
87
|
-
const [dep, teams] = await Promise.all([
|
|
88
|
-
get(`/Deployment/${argv.id}`),
|
|
89
|
-
get('/Teams/List'),
|
|
90
|
-
]);
|
|
91
|
-
|
|
92
|
-
if (!dep) {
|
|
93
|
-
console.error(`Deployment ${argv.id} not found.`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const team = (teams ?? []).find(t => t.id === argv['team-id']);
|
|
98
|
-
if (!team) {
|
|
99
|
-
console.error(`Team ${argv['team-id']} not found.`);
|
|
100
|
-
console.error('Run: velaro team list');
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
|
|
105
|
-
console.log(`Deployment ${argv.id} ("${dep.displayName}") assigned to team "${team.name}".`);
|
|
106
|
-
}),
|
|
107
|
-
};
|
|
1
|
+
import { get, post } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
export const deploymentCommand = {
|
|
5
|
+
command: 'deployment <subcommand>',
|
|
6
|
+
describe: 'Manage chat widget deployments',
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs
|
|
9
|
+
.command(deploymentListCommand)
|
|
10
|
+
.command(deploymentRenameCommand)
|
|
11
|
+
.command(deploymentAssignCommand)
|
|
12
|
+
.demandCommand(1, 'Specify a subcommand: list, rename, assign-team'),
|
|
13
|
+
handler: () => {},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ── list ───────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const deploymentListCommand = {
|
|
19
|
+
command: 'list',
|
|
20
|
+
describe: 'List chat widget deployments',
|
|
21
|
+
handler: runCommand(async () => {
|
|
22
|
+
const [deployments, teams] = await Promise.all([
|
|
23
|
+
get('/Deployment'),
|
|
24
|
+
get('/Teams/List'),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (!deployments?.length) {
|
|
28
|
+
console.log('No deployments found.');
|
|
29
|
+
console.log('\nA deployment is a chat widget embed — one per page or team you want to serve.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
34
|
+
|
|
35
|
+
const rows = deployments.map(d => ({
|
|
36
|
+
id: `[${d.id}]`,
|
|
37
|
+
key: d.deploymentId ?? '—',
|
|
38
|
+
team: teamMap[d.teamId] ?? `team ${d.teamId}`,
|
|
39
|
+
name: d.displayName ?? '(unnamed)',
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const idW = Math.max(...rows.map(r => r.id.length));
|
|
43
|
+
const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
|
|
44
|
+
const teamW = Math.max('team'.length, ...rows.map(r => r.team.length));
|
|
45
|
+
|
|
46
|
+
console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'team'.padEnd(teamW)} name`);
|
|
47
|
+
console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
|
|
48
|
+
for (const r of rows) {
|
|
49
|
+
console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(`\n${rows.length} deployment(s)`);
|
|
52
|
+
console.log('\nThe embed-key goes in your widget JavaScript snippet.');
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ── rename ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
const deploymentRenameCommand = {
|
|
59
|
+
command: 'rename <id> <name>',
|
|
60
|
+
describe: 'Rename a deployment',
|
|
61
|
+
builder: (y) =>
|
|
62
|
+
y
|
|
63
|
+
.positional('id', { type: 'number', describe: 'Deployment numeric ID' })
|
|
64
|
+
.positional('name', { type: 'string', describe: 'New display name' }),
|
|
65
|
+
handler: runCommand(async (argv) => {
|
|
66
|
+
const dep = await get(`/Deployment/${argv.id}`);
|
|
67
|
+
if (!dep) {
|
|
68
|
+
console.error(`Deployment ${argv.id} not found.`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
|
|
73
|
+
console.log(`Deployment ${argv.id} renamed to "${argv.name}".`);
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ── assign-team ────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const deploymentAssignCommand = {
|
|
80
|
+
command: 'assign-team <id> <team-id>',
|
|
81
|
+
describe: 'Assign a team to a deployment',
|
|
82
|
+
builder: (y) =>
|
|
83
|
+
y
|
|
84
|
+
.positional('id', { type: 'number', describe: 'Deployment numeric ID' })
|
|
85
|
+
.positional('team-id', { type: 'number', describe: 'Team ID to assign' }),
|
|
86
|
+
handler: runCommand(async (argv) => {
|
|
87
|
+
const [dep, teams] = await Promise.all([
|
|
88
|
+
get(`/Deployment/${argv.id}`),
|
|
89
|
+
get('/Teams/List'),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
if (!dep) {
|
|
93
|
+
console.error(`Deployment ${argv.id} not found.`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const team = (teams ?? []).find(t => t.id === argv['team-id']);
|
|
98
|
+
if (!team) {
|
|
99
|
+
console.error(`Team ${argv['team-id']} not found.`);
|
|
100
|
+
console.error('Run: velaro team list');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
|
|
105
|
+
console.log(`Deployment ${argv.id} ("${dep.displayName}") assigned to team "${team.name}".`);
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { get as adminGet, getCredentials } from '../api.js';
|
|
2
|
+
import { readConfig, ENVS } from '../config.js';
|
|
3
|
+
|
|
4
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const RESET = '\x1b[0m';
|
|
7
|
+
const BOLD = '\x1b[1m';
|
|
8
|
+
const DIM = '\x1b[2m';
|
|
9
|
+
const CYN = '\x1b[36m';
|
|
10
|
+
const YEL = '\x1b[33m';
|
|
11
|
+
const GRN = '\x1b[32m';
|
|
12
|
+
const RED = '\x1b[31m';
|
|
13
|
+
|
|
14
|
+
function fmtDate(ts) {
|
|
15
|
+
if (!ts) return '—';
|
|
16
|
+
return new Date(ts).toLocaleString('en-US', {
|
|
17
|
+
timeZone: 'America/Los_Angeles',
|
|
18
|
+
dateStyle: 'short',
|
|
19
|
+
timeStyle: 'short',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function trunc(str, len = 100) {
|
|
24
|
+
if (!str) return '';
|
|
25
|
+
return str.length > len ? str.slice(0, len - 1) + '…' : str;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requireSite(argv) {
|
|
29
|
+
if (!argv.site) {
|
|
30
|
+
console.error(`\n${RED}Error:${RESET} Use --site <siteId> to specify which customer site to query.\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return argv.site;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getMsgBase(argv) {
|
|
37
|
+
const cfg = readConfig();
|
|
38
|
+
const env = argv.env || cfg.env || 'staging';
|
|
39
|
+
return cfg.envs?.[env]?.messagingApiBase ?? ENVS[env]?.messagingApiBase
|
|
40
|
+
?? 'https://velaro-messaging-api-staging.azurewebsites.net';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function requireAdmin(creds) {
|
|
44
|
+
if (creds.siteId !== 1032) {
|
|
45
|
+
console.error('velaro diagnostics crm-email-campaign-audit is only available to Velaro staff (site 1032).');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── velaro diagnostics skill-logs ─────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const skillLogsCommand = {
|
|
53
|
+
command: 'skill-logs',
|
|
54
|
+
describe: 'Show recent AI skill invocation logs for a site',
|
|
55
|
+
builder: (y) => y
|
|
56
|
+
.option('site', {
|
|
57
|
+
alias: 's',
|
|
58
|
+
type: 'number',
|
|
59
|
+
describe: 'Site ID to query (required)',
|
|
60
|
+
})
|
|
61
|
+
.option('errors-only', {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
default: false,
|
|
64
|
+
describe: 'Show only failed skill calls',
|
|
65
|
+
})
|
|
66
|
+
.option('take', {
|
|
67
|
+
alias: 'n',
|
|
68
|
+
type: 'number',
|
|
69
|
+
default: 25,
|
|
70
|
+
describe: 'Number of entries to return (default 25)',
|
|
71
|
+
}),
|
|
72
|
+
handler: async (argv) => {
|
|
73
|
+
const siteId = requireSite(argv);
|
|
74
|
+
const qs = new URLSearchParams({
|
|
75
|
+
siteId: String(siteId),
|
|
76
|
+
errorsOnly: String(argv['errors-only']),
|
|
77
|
+
take: String(argv.take),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let items;
|
|
81
|
+
try {
|
|
82
|
+
items = await adminGet(`SuperAdmin/Diagnostics/skill-logs?${qs}`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`\n${RED}Error:${RESET} ${err.message}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
items = Array.isArray(items?.logs) ? items.logs : [];
|
|
89
|
+
|
|
90
|
+
console.log(`\n${BOLD}── Skill Logs — Site ${siteId} ──────────────────────────────────────────${RESET}`);
|
|
91
|
+
|
|
92
|
+
if (!items.length) {
|
|
93
|
+
console.log(` ${DIM}No skill log entries found.${RESET}\n`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const entry of items) {
|
|
98
|
+
const ts = fmtDate(entry.calledAt);
|
|
99
|
+
const tool = (entry.toolName ?? '—').padEnd(32);
|
|
100
|
+
const success = entry.success ? `${GRN}✓${RESET}` : `${RED}✗${RESET}`;
|
|
101
|
+
const duration = entry.durationMs != null ? `${entry.durationMs}ms`.padStart(7) : ' —';
|
|
102
|
+
const category = entry.category ? `${DIM}[${entry.category}]${RESET}` : '';
|
|
103
|
+
|
|
104
|
+
console.log(` ${CYN}${ts}${RESET} ${success} ${BOLD}${tool}${RESET} ${duration} ${category}`);
|
|
105
|
+
|
|
106
|
+
if (!entry.success && entry.resultPreview) {
|
|
107
|
+
console.log(` ${RED}${trunc(entry.resultPreview, 120)}${RESET}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.log();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ── velaro diagnostics integration-logs ──────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const integrationLogsCommand = {
|
|
117
|
+
command: 'integration-logs',
|
|
118
|
+
describe: 'Show recent integration activity log entries for a site',
|
|
119
|
+
builder: (y) => y
|
|
120
|
+
.option('site', {
|
|
121
|
+
alias: 's',
|
|
122
|
+
type: 'number',
|
|
123
|
+
describe: 'Site ID to query (required)',
|
|
124
|
+
})
|
|
125
|
+
.option('severity', {
|
|
126
|
+
type: 'string',
|
|
127
|
+
describe: 'Filter by severity: error | warning | info',
|
|
128
|
+
choices: ['Error', 'Warning', 'Info'],
|
|
129
|
+
})
|
|
130
|
+
.option('integration', {
|
|
131
|
+
alias: 'i',
|
|
132
|
+
type: 'string',
|
|
133
|
+
describe: 'Integration slug to filter (e.g. HubSpot, Salesforce)',
|
|
134
|
+
})
|
|
135
|
+
.option('take', {
|
|
136
|
+
alias: 'n',
|
|
137
|
+
type: 'number',
|
|
138
|
+
default: 25,
|
|
139
|
+
describe: 'Number of entries to return (default 25)',
|
|
140
|
+
}),
|
|
141
|
+
handler: async (argv) => {
|
|
142
|
+
const siteId = requireSite(argv);
|
|
143
|
+
const params = { siteId: String(siteId), take: String(argv.take) };
|
|
144
|
+
if (argv.severity) params.severity = argv.severity;
|
|
145
|
+
if (argv.integration) params.integration = argv.integration;
|
|
146
|
+
const qs = new URLSearchParams(params);
|
|
147
|
+
|
|
148
|
+
let items;
|
|
149
|
+
try {
|
|
150
|
+
items = await adminGet(`SuperAdmin/Diagnostics/integration-logs?${qs}`);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`\n${RED}Error:${RESET} ${err.message}\n`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
items = Array.isArray(items?.logs) ? items.logs : [];
|
|
157
|
+
|
|
158
|
+
console.log(`\n${BOLD}── Integration Logs — Site ${siteId} ────────────────────────────────────${RESET}`);
|
|
159
|
+
|
|
160
|
+
if (!items.length) {
|
|
161
|
+
console.log(` ${DIM}No integration log entries found.${RESET}\n`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const entry of items) {
|
|
166
|
+
const ts = fmtDate(entry.createdAt);
|
|
167
|
+
const integration = (entry.integration ?? '—').padEnd(18);
|
|
168
|
+
const sevColor = entry.severity === 'Error' ? RED
|
|
169
|
+
: entry.severity === 'Warning' ? YEL
|
|
170
|
+
: DIM;
|
|
171
|
+
const sev = `${sevColor}${(entry.severity ?? 'Info').padEnd(7)}${RESET}`;
|
|
172
|
+
const msg = trunc(entry.message ?? '', 100);
|
|
173
|
+
|
|
174
|
+
console.log(` ${CYN}${ts}${RESET} ${sev} ${BOLD}${integration}${RESET} ${msg}`);
|
|
175
|
+
}
|
|
176
|
+
console.log();
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ── velaro diagnostics routing-decisions ─────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
const routingDecisionsCommand = {
|
|
183
|
+
command: 'routing-decisions',
|
|
184
|
+
describe: 'Show recent smart routing decision events for a site',
|
|
185
|
+
builder: (y) => y
|
|
186
|
+
.option('site', {
|
|
187
|
+
alias: 's',
|
|
188
|
+
type: 'number',
|
|
189
|
+
describe: 'Site ID to query (required)',
|
|
190
|
+
})
|
|
191
|
+
.option('take', {
|
|
192
|
+
alias: 'n',
|
|
193
|
+
type: 'number',
|
|
194
|
+
default: 25,
|
|
195
|
+
describe: 'Number of entries to return (default 25)',
|
|
196
|
+
}),
|
|
197
|
+
handler: async (argv) => {
|
|
198
|
+
const siteId = requireSite(argv);
|
|
199
|
+
const qs = new URLSearchParams({ siteId: String(siteId), take: String(argv.take) });
|
|
200
|
+
|
|
201
|
+
let items;
|
|
202
|
+
try {
|
|
203
|
+
items = await adminGet(`SuperAdmin/Diagnostics/routing-decisions?${qs}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error(`\n${RED}Error:${RESET} ${err.message}\n`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
items = Array.isArray(items?.decisions) ? items.decisions : [];
|
|
210
|
+
|
|
211
|
+
console.log(`\n${BOLD}── Routing Decisions — Site ${siteId} ───────────────────────────────────${RESET}`);
|
|
212
|
+
|
|
213
|
+
if (!items.length) {
|
|
214
|
+
console.log(` ${DIM}No routing decision events found.${RESET}\n`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const entry of items) {
|
|
219
|
+
const ts = fmtDate(entry.occurredAt);
|
|
220
|
+
const trigger = (entry.trigger ?? '—').padEnd(20);
|
|
221
|
+
const rule = entry.ruleName ? `${DIM}${entry.ruleName}${RESET} → ` : '';
|
|
222
|
+
const outcome = (entry.outcomeType ?? '—').padEnd(18);
|
|
223
|
+
const label = entry.outcomeLabel ? `${GRN}${entry.outcomeLabel}${RESET}` : '';
|
|
224
|
+
const agentCount = entry.agentsAvailable ?? entry.agentsOnline;
|
|
225
|
+
const agents = agentCount != null ? ` ${DIM}agents avail: ${agentCount}${RESET}` : '';
|
|
226
|
+
|
|
227
|
+
console.log(` ${CYN}${ts}${RESET} ${BOLD}${trigger}${RESET} ${rule}${outcome}${label}${agents}`);
|
|
228
|
+
}
|
|
229
|
+
console.log();
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// ── velaro diagnostics crm-email-campaign-audit ──────────────────────────────
|
|
234
|
+
// Retirement-decision diagnostic for the dead/duplicate CrmEmailCampaign system
|
|
235
|
+
// (velaro-messaging's server/Velaro.Messaging.Data/Data/Crm/CrmEmailCampaign*.cs) --
|
|
236
|
+
// the question of "does it have real data or is it safe to delete" was blocked all
|
|
237
|
+
// session on not wanting to run raw SQL against staging/prod. This calls the aggregate,
|
|
238
|
+
// non-PII SuperAdmin/crm-email-campaign-audit endpoint instead. Mirrors the ai-models.js
|
|
239
|
+
// --env/requireAdmin pattern exactly.
|
|
240
|
+
|
|
241
|
+
const crmEmailCampaignAuditCommand = {
|
|
242
|
+
command: 'crm-email-campaign-audit',
|
|
243
|
+
describe: 'Aggregate, non-PII row counts for the dead CrmEmailCampaign system (Velaro staff only) -- answers the delete-vs-migrate retirement question without raw SQL',
|
|
244
|
+
builder: (y) => y
|
|
245
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
246
|
+
handler: async (argv) => {
|
|
247
|
+
const creds = await getCredentials();
|
|
248
|
+
requireAdmin(creds);
|
|
249
|
+
const msgBase = await getMsgBase(argv);
|
|
250
|
+
|
|
251
|
+
const res = await fetch(`${msgBase}/SuperAdmin/crm-email-campaign-audit`, {
|
|
252
|
+
headers: {
|
|
253
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
254
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
255
|
+
},
|
|
256
|
+
signal: AbortSignal.timeout(20000),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
const env = argv.env || readConfig().env || 'staging';
|
|
266
|
+
|
|
267
|
+
console.log(`\n${BOLD}── CrmEmailCampaign Retirement Audit (${env}) ──────────────────────${RESET}`);
|
|
268
|
+
console.log(` Total campaigns: ${data.totalCampaigns}`);
|
|
269
|
+
console.log(` Total enrollments: ${data.totalEnrollments}`);
|
|
270
|
+
console.log(` Distinct sites w/ campaigns: ${data.distinctSitesWithCampaigns}`);
|
|
271
|
+
console.log(` Oldest campaign created: ${data.oldestCampaignCreatedAt ?? '—'}`);
|
|
272
|
+
console.log(` Newest campaign created: ${data.newestCampaignCreatedAt ?? '—'}`);
|
|
273
|
+
console.log(` Oldest enrollment date: ${data.oldestEnrollmentDate ?? '—'}`);
|
|
274
|
+
console.log(` Newest enrollment date: ${data.newestEnrollmentDate ?? '—'}`);
|
|
275
|
+
console.log(` Generated at (UTC): ${data.generatedAtUtc}`);
|
|
276
|
+
|
|
277
|
+
if (data.totalCampaigns === 0 && data.totalEnrollments === 0) {
|
|
278
|
+
console.log(`\n ${GRN}Both tables are empty -- safe to delete without a data migration.${RESET}\n`);
|
|
279
|
+
} else {
|
|
280
|
+
console.log(`\n ${YEL}Real rows exist -- retirement needs a data migration into admin's EmailCampaign engine, not a plain delete.${RESET}\n`);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ── top-level diagnostics command ─────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
export const diagnosticsCommand = {
|
|
288
|
+
command: 'diagnostics <subcommand>',
|
|
289
|
+
describe: 'Query skill logs, integration logs, routing decisions, and staff-only audits (superadmin only)',
|
|
290
|
+
builder: (y) => y
|
|
291
|
+
.command(skillLogsCommand)
|
|
292
|
+
.command(integrationLogsCommand)
|
|
293
|
+
.command(routingDecisionsCommand)
|
|
294
|
+
.command(crmEmailCampaignAuditCommand)
|
|
295
|
+
.demandCommand(1, 'Specify a subcommand: skill-logs, integration-logs, routing-decisions, crm-email-campaign-audit')
|
|
296
|
+
.strict(),
|
|
297
|
+
handler: () => {},
|
|
298
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro email-campaign - Drip/trigger email campaign (EmailCampaign) commands
|
|
3
|
+
*
|
|
4
|
+
* velaro email-campaign step-stats <id> Per-step open/click rates for a campaign
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { request } from '../api.js';
|
|
8
|
+
|
|
9
|
+
const GRN = '\x1b[32m';
|
|
10
|
+
const DIM = '\x1b[2m';
|
|
11
|
+
const BOLD = '\x1b[1m';
|
|
12
|
+
const RST = '\x1b[0m';
|
|
13
|
+
|
|
14
|
+
// -- step-stats ----------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
async function stepStatsHandler(argv) {
|
|
17
|
+
const id = argv.id;
|
|
18
|
+
const data = await request('GET', `/EmailCampaigns/${id}/StepStats`);
|
|
19
|
+
const steps = data?.steps ?? [];
|
|
20
|
+
if (!steps.length) {
|
|
21
|
+
console.log(`\nNo step data for campaign ${id} yet (no sends recorded).\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`\n${BOLD}Step stats for campaign ${id}${RST}\n`);
|
|
25
|
+
for (const s of steps) {
|
|
26
|
+
console.log(` Step ${s.stepOrder ?? '?'} ${DIM}(step id ${s.stepId})${RST}`);
|
|
27
|
+
console.log(` sent ${s.sentCount}, opened ${s.openedCount} (${GRN}${s.openRate}%${RST}), clicked ${s.clickedCount} (${GRN}${s.clickRate}%${RST})`);
|
|
28
|
+
}
|
|
29
|
+
console.log('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// -- command export ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export const emailCampaignCommand = {
|
|
35
|
+
command: 'email-campaign <subcommand>',
|
|
36
|
+
describe: 'Drip/trigger email campaign (EmailCampaign) commands',
|
|
37
|
+
builder: y => y
|
|
38
|
+
.command({
|
|
39
|
+
command: 'step-stats <id>',
|
|
40
|
+
describe: 'Per-step open/click rates for a drip/trigger campaign',
|
|
41
|
+
builder: yy => yy
|
|
42
|
+
.positional('id', { describe: 'EmailCampaign ID', type: 'number' }),
|
|
43
|
+
handler: stepStatsHandler,
|
|
44
|
+
})
|
|
45
|
+
.demandCommand(1, 'Specify a subcommand. Try: velaro email-campaign --help'),
|
|
46
|
+
handler: () => {},
|
|
47
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { messagingGet, messagingPatch, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro email-inbox ────────────────────────────────────────────────────────
|
|
5
|
+
// Staff commands to manage Gmail/Outlook inbox mailbox connections for any site.
|
|
6
|
+
// Requires a Velaro internal-staff JWT (site 1032) or vel_live_* service key bound
|
|
7
|
+
// to site 1032 — same gate as every other SupportTools/sites/{siteId}/... endpoint.
|
|
8
|
+
//
|
|
9
|
+
// Backed by velaro-messaging SupportToolsController's email-inbox endpoints:
|
|
10
|
+
// GET SupportTools/sites/{siteId}/email-inbox/{provider}/list
|
|
11
|
+
// GET SupportTools/sites/{siteId}/email-inbox/gmail/config-status
|
|
12
|
+
// PATCH SupportTools/sites/{siteId}/email-inbox/{provider}/{id}/team
|
|
13
|
+
// DELETE SupportTools/sites/{siteId}/email-inbox/{provider}/{id}
|
|
14
|
+
|
|
15
|
+
function providerOption(y) {
|
|
16
|
+
return y.option('provider', {
|
|
17
|
+
type: 'string', choices: ['gmail', 'outlook'], demandOption: true,
|
|
18
|
+
describe: 'Which inbox integration',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printMailboxes(provider, siteId, mailboxes) {
|
|
23
|
+
if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
|
|
24
|
+
console.log(`\nSite ${siteId}: no ${provider} mailboxes connected.\n`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(`\nSite ${siteId} — ${provider} mailboxes:\n`);
|
|
28
|
+
for (const m of mailboxes) {
|
|
29
|
+
const team = m.teamName ? m.teamName : (m.TeamId || m.teamId ? `team #${m.TeamId ?? m.teamId}` : '(no team — site default)');
|
|
30
|
+
const status = (m.IsEnabled ?? m.isEnabled) ? 'enabled' : 'disabled';
|
|
31
|
+
console.log(` #${m.Id ?? m.id} ${m.MailboxEmail ?? m.mailboxEmail} [${status}] ${team}`);
|
|
32
|
+
}
|
|
33
|
+
console.log('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const emailInboxCommand = {
|
|
37
|
+
command: 'email-inbox <subcommand>',
|
|
38
|
+
describe: 'Manage Gmail/Outlook inbox mailbox connections for any site (Velaro staff only)',
|
|
39
|
+
builder: yargs => yargs
|
|
40
|
+
.command({
|
|
41
|
+
command: 'list',
|
|
42
|
+
describe: 'List mailboxes connected for a site',
|
|
43
|
+
builder: y => providerOption(y)
|
|
44
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
|
|
45
|
+
handler: runCommand(async argv => {
|
|
46
|
+
const mailboxes = await messagingGet(`/SupportTools/sites/${argv.site}/email-inbox/${argv.provider}/list`);
|
|
47
|
+
printMailboxes(argv.provider, argv.site, mailboxes);
|
|
48
|
+
}),
|
|
49
|
+
})
|
|
50
|
+
.command({
|
|
51
|
+
command: 'status',
|
|
52
|
+
describe: 'Show global Gmail OAuth/DWD/Pub-Sub config status (gmail only — Outlook has no global status endpoint, use "list")',
|
|
53
|
+
builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID (accepted for a uniform call shape; global config is not per-site)' }),
|
|
54
|
+
handler: runCommand(async argv => {
|
|
55
|
+
const status = await messagingGet(`/SupportTools/sites/${argv.site}/email-inbox/gmail/config-status`);
|
|
56
|
+
console.log(`\nGmail global config status:`);
|
|
57
|
+
console.log(` OAuth configured: ${status.oauthConfigured ? 'yes' : 'no'}`);
|
|
58
|
+
console.log(` DWD configured: ${status.dwdConfigured ? 'yes' : 'no'}`);
|
|
59
|
+
console.log(` Pub/Sub configured: ${status.pubSubConfigured ? 'yes' : 'no'}\n`);
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
.command({
|
|
63
|
+
command: 'set-team',
|
|
64
|
+
describe: 'Reassign (or clear) the team a mailbox routes inbound mail to',
|
|
65
|
+
builder: y => providerOption(y)
|
|
66
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
|
|
67
|
+
.option('id', { type: 'number', demandOption: true, describe: 'Mailbox integration ID (from "list")' })
|
|
68
|
+
.option('team-id', { type: 'number', demandOption: true, describe: 'Team ID to assign, or 0 to clear (site default/catch-all)' }),
|
|
69
|
+
handler: runCommand(async argv => {
|
|
70
|
+
await messagingPatch(`/SupportTools/sites/${argv.site}/email-inbox/${argv.provider}/${argv.id}/team`, { teamId: argv['team-id'] });
|
|
71
|
+
console.log(argv['team-id'] === 0
|
|
72
|
+
? `\n✓ Mailbox #${argv.id} (${argv.provider}) team assignment cleared — now routes to site default.\n`
|
|
73
|
+
: `\n✓ Mailbox #${argv.id} (${argv.provider}) reassigned to team ${argv['team-id']}.\n`);
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
.command({
|
|
77
|
+
command: 'disconnect',
|
|
78
|
+
describe: 'Disconnect and remove a mailbox connection',
|
|
79
|
+
builder: y => providerOption(y)
|
|
80
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
|
|
81
|
+
.option('id', { type: 'number', demandOption: true, describe: 'Mailbox integration ID (from "list")' }),
|
|
82
|
+
handler: runCommand(async argv => {
|
|
83
|
+
await messagingDel(`/SupportTools/sites/${argv.site}/email-inbox/${argv.provider}/${argv.id}`);
|
|
84
|
+
console.log(`\n✓ Mailbox #${argv.id} (${argv.provider}) disconnected from site ${argv.site}.\n`);
|
|
85
|
+
}),
|
|
86
|
+
})
|
|
87
|
+
.demandCommand(1, 'Specify a subcommand: list | status | set-team | disconnect'),
|
|
88
|
+
};
|