@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
package/lib/commands/check.js
CHANGED
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* velaro check — full site health and configuration summary.
|
|
3
|
-
*
|
|
4
|
-
* Shows every feature area with:
|
|
5
|
-
* ✓ enabled and configured
|
|
6
|
-
* ⚠ enabled but nothing configured yet
|
|
7
|
-
* ✗ not on your plan
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { get }
|
|
11
|
-
import { getSubscription } from '../subscription.js';
|
|
12
|
-
import { runCommand } from '../run.js';
|
|
13
|
-
|
|
14
|
-
const TICK = '✓';
|
|
15
|
-
const WARN = '⚠';
|
|
16
|
-
const CROSS = '✗';
|
|
17
|
-
|
|
18
|
-
export const checkCommand = {
|
|
19
|
-
command: 'check',
|
|
20
|
-
describe: 'Show full site configuration and health summary',
|
|
21
|
-
handler: runCommand(async () => {
|
|
22
|
-
const [sub, apiStatus] = await Promise.all([
|
|
23
|
-
getSubscription().catch(() => null),
|
|
24
|
-
get('/Status').catch(() => null),
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
if (!sub) {
|
|
28
|
-
console.error('Could not retrieve subscription. Check your login: velaro whoami');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const ok = (msg) => console.log(` ${TICK} ${msg}`);
|
|
33
|
-
const warn = (msg) => console.log(` ${WARN} ${msg}`);
|
|
34
|
-
const off = (msg) => console.log(` ${CROSS} ${msg}`);
|
|
35
|
-
|
|
36
|
-
console.log('\n── API Health ────────────────────────────────────────────────');
|
|
37
|
-
if (apiStatus) {
|
|
38
|
-
ok(`API reachable`);
|
|
39
|
-
} else {
|
|
40
|
-
warn('API did not respond to /Status');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── Deployments ─────────────────────────────────────────────────────────
|
|
44
|
-
console.log('\n── Deployments ───────────────────────────────────────────────');
|
|
45
|
-
try {
|
|
46
|
-
const [deps, teams] = await Promise.all([get('/Deployment'), get('/Teams/List')]);
|
|
47
|
-
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
48
|
-
if (deps?.length) {
|
|
49
|
-
for (const d of deps) {
|
|
50
|
-
ok(`[${d.id}] "${d.displayName}" → team: ${teamMap[d.teamId] ?? `#${d.teamId}`} (embed key: ${d.deploymentId})`);
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
warn('No deployments — create one in the admin UI to get your embed snippet.');
|
|
54
|
-
}
|
|
55
|
-
} catch { warn('Could not load deployments'); }
|
|
56
|
-
|
|
57
|
-
// ── AI Bots ──────────────────────────────────────────────────────────────
|
|
58
|
-
console.log('\n── AI Bots ───────────────────────────────────────────────────');
|
|
59
|
-
if (sub.enableAI) {
|
|
60
|
-
try {
|
|
61
|
-
const bots = await
|
|
62
|
-
if (bots?.length) {
|
|
63
|
-
for (const b of bots) ok(`[${b.id}] "${b.name}" model=${b.aiModel ?? 'default'}`);
|
|
64
|
-
} else {
|
|
65
|
-
warn('AI enabled but no bots configured. Run: velaro bot setup --name "My Bot"');
|
|
66
|
-
}
|
|
67
|
-
} catch { warn('Could not load bot list'); }
|
|
68
|
-
} else {
|
|
69
|
-
off('AI Bots — not on your plan (upgrade at velaro.com/pricing)');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Knowledge Base ───────────────────────────────────────────────────────
|
|
73
|
-
console.log('\n── Knowledge Base ────────────────────────────────────────────');
|
|
74
|
-
if (sub.enableKnowledgeBase) {
|
|
75
|
-
try {
|
|
76
|
-
const [qna, overrides] = await Promise.all([
|
|
77
|
-
|
|
78
|
-
get('/Ticket/knowledge-overrides').catch(() => []),
|
|
79
|
-
]);
|
|
80
|
-
ok(`${qna?.length ?? 0} Q&A pair(s) · ${overrides?.length ?? 0} override(s)`);
|
|
81
|
-
} catch { warn('Could not load knowledge base'); }
|
|
82
|
-
} else {
|
|
83
|
-
off('Knowledge Base — not on your plan');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── Workflows ────────────────────────────────────────────────────────────
|
|
87
|
-
console.log('\n── Workflows ─────────────────────────────────────────────────');
|
|
88
|
-
if (sub.enableAutomation) {
|
|
89
|
-
try {
|
|
90
|
-
const workflows = (await get('/Workflows/List') ?? []).filter(w => !w.isTemplate);
|
|
91
|
-
const enabled = workflows.filter(w => w.enabled).length;
|
|
92
|
-
const disabled = workflows.length - enabled;
|
|
93
|
-
if (workflows.length) {
|
|
94
|
-
ok(`${workflows.length} workflow(s) · ${enabled} enabled · ${disabled} disabled`);
|
|
95
|
-
for (const w of workflows) {
|
|
96
|
-
const sym = w.enabled ? TICK : WARN;
|
|
97
|
-
console.log(` ${sym} [${w.id}] "${w.name}" (${w.enabled ? 'on' : 'off'})`);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
warn('Automation enabled but no workflows configured.');
|
|
101
|
-
}
|
|
102
|
-
} catch { warn('Could not load workflows'); }
|
|
103
|
-
} else {
|
|
104
|
-
off('Workflows — not on your plan');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Routing Rules ────────────────────────────────────────────────────────
|
|
108
|
-
console.log('\n── Routing Rules ─────────────────────────────────────────────');
|
|
109
|
-
if (sub.enableWorkflowRules) {
|
|
110
|
-
try {
|
|
111
|
-
const rules = await get('/Rules/List');
|
|
112
|
-
if (rules?.length) {
|
|
113
|
-
ok(`${rules.length} routing rule(s) configured`);
|
|
114
|
-
} else {
|
|
115
|
-
warn('Routing rules enabled but none configured.');
|
|
116
|
-
}
|
|
117
|
-
} catch { warn('Could not load rules'); }
|
|
118
|
-
} else {
|
|
119
|
-
off('Routing Rules — not on your plan');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Channels ─────────────────────────────────────────────────────────────
|
|
123
|
-
console.log('\n── Channels ──────────────────────────────────────────────────');
|
|
124
|
-
const channels = [
|
|
125
|
-
['enableWeb', 'Web Chat'],
|
|
126
|
-
['enableSms', 'SMS'],
|
|
127
|
-
['enableEmail', 'Email'],
|
|
128
|
-
['enableWhatsapp', 'WhatsApp'],
|
|
129
|
-
['enableIvr', 'IVR / Voice'],
|
|
130
|
-
['enableFacebook', 'Facebook Messenger'],
|
|
131
|
-
['enableInstagram', 'Instagram'],
|
|
132
|
-
['enableRcs', 'RCS'],
|
|
133
|
-
];
|
|
134
|
-
for (const [flag, label] of channels) {
|
|
135
|
-
if (sub[flag]) ok(label); else off(`${label} — not on your plan`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── IVR detail ───────────────────────────────────────────────────────────
|
|
139
|
-
if (sub.enableIvr) {
|
|
140
|
-
try {
|
|
141
|
-
const ivr = await get('/api/ivr/config');
|
|
142
|
-
if (ivr?.phoneNumber) {
|
|
143
|
-
console.log(`\n── IVR Config ────────────────────────────────────────────────`);
|
|
144
|
-
ok(`Phone: ${ivr.phoneNumber}`);
|
|
145
|
-
if (ivr.bridgeNumber) ok(`Bridge: ${ivr.bridgeNumber}`);
|
|
146
|
-
else warn('No bridge (transfer) number set — agents cannot receive transferred calls.');
|
|
147
|
-
}
|
|
148
|
-
} catch { /* IVR config is optional */ }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── Agents ───────────────────────────────────────────────────────────────
|
|
152
|
-
console.log('\n── Agents ────────────────────────────────────────────────────');
|
|
153
|
-
try {
|
|
154
|
-
const agents = await get('/Users/List');
|
|
155
|
-
const online = (agents ?? []).filter(a => a.status === 'Available').length;
|
|
156
|
-
const away = (agents ?? []).filter(a => a.status === 'Away').length;
|
|
157
|
-
const offline = (agents ?? []).length - online - away;
|
|
158
|
-
ok(`${agents?.length ?? 0} agent(s) · ${online} online · ${away} away · ${offline} offline`);
|
|
159
|
-
} catch { warn('Could not load agents'); }
|
|
160
|
-
|
|
161
|
-
console.log('\n──────────────────────────────────────────────────────────────\n');
|
|
162
|
-
}),
|
|
163
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* velaro check — full site health and configuration summary.
|
|
3
|
+
*
|
|
4
|
+
* Shows every feature area with:
|
|
5
|
+
* ✓ enabled and configured
|
|
6
|
+
* ⚠ enabled but nothing configured yet
|
|
7
|
+
* ✗ not on your plan
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { get, messagingGet } from '../api.js';
|
|
11
|
+
import { getSubscription } from '../subscription.js';
|
|
12
|
+
import { runCommand } from '../run.js';
|
|
13
|
+
|
|
14
|
+
const TICK = '✓';
|
|
15
|
+
const WARN = '⚠';
|
|
16
|
+
const CROSS = '✗';
|
|
17
|
+
|
|
18
|
+
export const checkCommand = {
|
|
19
|
+
command: 'check',
|
|
20
|
+
describe: 'Show full site configuration and health summary',
|
|
21
|
+
handler: runCommand(async () => {
|
|
22
|
+
const [sub, apiStatus] = await Promise.all([
|
|
23
|
+
getSubscription().catch(() => null),
|
|
24
|
+
get('/Status').catch(() => null),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (!sub) {
|
|
28
|
+
console.error('Could not retrieve subscription. Check your login: velaro whoami');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ok = (msg) => console.log(` ${TICK} ${msg}`);
|
|
33
|
+
const warn = (msg) => console.log(` ${WARN} ${msg}`);
|
|
34
|
+
const off = (msg) => console.log(` ${CROSS} ${msg}`);
|
|
35
|
+
|
|
36
|
+
console.log('\n── API Health ────────────────────────────────────────────────');
|
|
37
|
+
if (apiStatus) {
|
|
38
|
+
ok(`API reachable`);
|
|
39
|
+
} else {
|
|
40
|
+
warn('API did not respond to /Status');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Deployments ─────────────────────────────────────────────────────────
|
|
44
|
+
console.log('\n── Deployments ───────────────────────────────────────────────');
|
|
45
|
+
try {
|
|
46
|
+
const [deps, teams] = await Promise.all([get('/Deployment'), get('/Teams/List')]);
|
|
47
|
+
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
48
|
+
if (deps?.length) {
|
|
49
|
+
for (const d of deps) {
|
|
50
|
+
ok(`[${d.id}] "${d.displayName}" → team: ${teamMap[d.teamId] ?? `#${d.teamId}`} (embed key: ${d.deploymentId})`);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
warn('No deployments — create one in the admin UI to get your embed snippet.');
|
|
54
|
+
}
|
|
55
|
+
} catch { warn('Could not load deployments'); }
|
|
56
|
+
|
|
57
|
+
// ── AI Bots ──────────────────────────────────────────────────────────────
|
|
58
|
+
console.log('\n── AI Bots ───────────────────────────────────────────────────');
|
|
59
|
+
if (sub.enableAI) {
|
|
60
|
+
try {
|
|
61
|
+
const bots = await messagingGet('/AIConfiguration/list');
|
|
62
|
+
if (bots?.length) {
|
|
63
|
+
for (const b of bots) ok(`[${b.id}] "${b.name}" model=${b.aiModel ?? 'default'}`);
|
|
64
|
+
} else {
|
|
65
|
+
warn('AI enabled but no bots configured. Run: velaro bot setup --name "My Bot"');
|
|
66
|
+
}
|
|
67
|
+
} catch { warn('Could not load bot list'); }
|
|
68
|
+
} else {
|
|
69
|
+
off('AI Bots — not on your plan (upgrade at velaro.com/pricing)');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Knowledge Base ───────────────────────────────────────────────────────
|
|
73
|
+
console.log('\n── Knowledge Base ────────────────────────────────────────────');
|
|
74
|
+
if (sub.enableKnowledgeBase) {
|
|
75
|
+
try {
|
|
76
|
+
const [qna, overrides] = await Promise.all([
|
|
77
|
+
messagingGet('/BotQnA').catch(() => []),
|
|
78
|
+
get('/Ticket/knowledge-overrides').catch(() => []),
|
|
79
|
+
]);
|
|
80
|
+
ok(`${qna?.length ?? 0} Q&A pair(s) · ${overrides?.length ?? 0} override(s)`);
|
|
81
|
+
} catch { warn('Could not load knowledge base'); }
|
|
82
|
+
} else {
|
|
83
|
+
off('Knowledge Base — not on your plan');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Workflows ────────────────────────────────────────────────────────────
|
|
87
|
+
console.log('\n── Workflows ─────────────────────────────────────────────────');
|
|
88
|
+
if (sub.enableAutomation) {
|
|
89
|
+
try {
|
|
90
|
+
const workflows = (await get('/Workflows/List') ?? []).filter(w => !w.isTemplate);
|
|
91
|
+
const enabled = workflows.filter(w => w.enabled).length;
|
|
92
|
+
const disabled = workflows.length - enabled;
|
|
93
|
+
if (workflows.length) {
|
|
94
|
+
ok(`${workflows.length} workflow(s) · ${enabled} enabled · ${disabled} disabled`);
|
|
95
|
+
for (const w of workflows) {
|
|
96
|
+
const sym = w.enabled ? TICK : WARN;
|
|
97
|
+
console.log(` ${sym} [${w.id}] "${w.name}" (${w.enabled ? 'on' : 'off'})`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
warn('Automation enabled but no workflows configured.');
|
|
101
|
+
}
|
|
102
|
+
} catch { warn('Could not load workflows'); }
|
|
103
|
+
} else {
|
|
104
|
+
off('Workflows — not on your plan');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Routing Rules ────────────────────────────────────────────────────────
|
|
108
|
+
console.log('\n── Routing Rules ─────────────────────────────────────────────');
|
|
109
|
+
if (sub.enableWorkflowRules) {
|
|
110
|
+
try {
|
|
111
|
+
const rules = await get('/Rules/List');
|
|
112
|
+
if (rules?.length) {
|
|
113
|
+
ok(`${rules.length} routing rule(s) configured`);
|
|
114
|
+
} else {
|
|
115
|
+
warn('Routing rules enabled but none configured.');
|
|
116
|
+
}
|
|
117
|
+
} catch { warn('Could not load rules'); }
|
|
118
|
+
} else {
|
|
119
|
+
off('Routing Rules — not on your plan');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Channels ─────────────────────────────────────────────────────────────
|
|
123
|
+
console.log('\n── Channels ──────────────────────────────────────────────────');
|
|
124
|
+
const channels = [
|
|
125
|
+
['enableWeb', 'Web Chat'],
|
|
126
|
+
['enableSms', 'SMS'],
|
|
127
|
+
['enableEmail', 'Email'],
|
|
128
|
+
['enableWhatsapp', 'WhatsApp'],
|
|
129
|
+
['enableIvr', 'IVR / Voice'],
|
|
130
|
+
['enableFacebook', 'Facebook Messenger'],
|
|
131
|
+
['enableInstagram', 'Instagram'],
|
|
132
|
+
['enableRcs', 'RCS'],
|
|
133
|
+
];
|
|
134
|
+
for (const [flag, label] of channels) {
|
|
135
|
+
if (sub[flag]) ok(label); else off(`${label} — not on your plan`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── IVR detail ───────────────────────────────────────────────────────────
|
|
139
|
+
if (sub.enableIvr) {
|
|
140
|
+
try {
|
|
141
|
+
const ivr = await get('/api/ivr/config');
|
|
142
|
+
if (ivr?.phoneNumber) {
|
|
143
|
+
console.log(`\n── IVR Config ────────────────────────────────────────────────`);
|
|
144
|
+
ok(`Phone: ${ivr.phoneNumber}`);
|
|
145
|
+
if (ivr.bridgeNumber) ok(`Bridge: ${ivr.bridgeNumber}`);
|
|
146
|
+
else warn('No bridge (transfer) number set — agents cannot receive transferred calls.');
|
|
147
|
+
}
|
|
148
|
+
} catch { /* IVR config is optional */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Agents ───────────────────────────────────────────────────────────────
|
|
152
|
+
console.log('\n── Agents ────────────────────────────────────────────────────');
|
|
153
|
+
try {
|
|
154
|
+
const agents = await get('/Users/List');
|
|
155
|
+
const online = (agents ?? []).filter(a => a.status === 'Available').length;
|
|
156
|
+
const away = (agents ?? []).filter(a => a.status === 'Away').length;
|
|
157
|
+
const offline = (agents ?? []).length - online - away;
|
|
158
|
+
ok(`${agents?.length ?? 0} agent(s) · ${online} online · ${away} away · ${offline} offline`);
|
|
159
|
+
} catch { warn('Could not load agents'); }
|
|
160
|
+
|
|
161
|
+
console.log('\n──────────────────────────────────────────────────────────────\n');
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { get, post } from '../api.js';
|
|
3
|
+
import { runCommand } from '../run.js';
|
|
4
|
+
|
|
5
|
+
export const complianceCommand = {
|
|
6
|
+
command: 'compliance [subcommand]',
|
|
7
|
+
describe: 'Compliance document signing — run with no args to see status and sign',
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.command(complianceStatusCommand)
|
|
11
|
+
.command(complianceSignCommand)
|
|
12
|
+
.command(complianceSignaturesCommand)
|
|
13
|
+
.command(complianceAttestationCommand)
|
|
14
|
+
.command(complianceRedactionTestCommand)
|
|
15
|
+
.command(complianceRedactionStatusCommand)
|
|
16
|
+
.command(complianceRedactionEventsCommand),
|
|
17
|
+
// Default: no subcommand = run status (same as `velaro compliance status`)
|
|
18
|
+
handler: runCommand(async () => { await runStatus(); }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ── Shared status + sign flow ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function runStatus({ autoSign = false } = {}) {
|
|
24
|
+
const docs = await get('/Compliance/documents');
|
|
25
|
+
if (!docs?.length) { console.log('No compliance documents found.'); return; }
|
|
26
|
+
|
|
27
|
+
const unsigned = docs.filter(d => !d.signed || d.documentChanged);
|
|
28
|
+
const signed = docs.filter(d => d.signed && !d.documentChanged);
|
|
29
|
+
|
|
30
|
+
console.log('\n━━━ Velaro Compliance Status ━━━\n');
|
|
31
|
+
|
|
32
|
+
for (const d of signed) {
|
|
33
|
+
const signedAt = new Date(d.signedAt).toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles', month: 'short', day: 'numeric', year: 'numeric' });
|
|
34
|
+
console.log(`✅ ${d.title}`);
|
|
35
|
+
console.log(` Signed ${signedAt} by ${d.signerName} (v${d.version})\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!unsigned.length) {
|
|
39
|
+
console.log('All compliance documents are signed. Nothing to do.\n');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`⚠️ ${unsigned.length} document${unsigned.length > 1 ? 's' : ''} need${unsigned.length === 1 ? 's' : ''} your signature:\n`);
|
|
44
|
+
for (const d of unsigned) {
|
|
45
|
+
const tag = d.documentChanged ? ' (document updated — re-sign)' : ' (unsigned)';
|
|
46
|
+
console.log(` • ${d.title}${tag}`);
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
|
|
50
|
+
// Interactive: ask to sign each one now
|
|
51
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
52
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
53
|
+
|
|
54
|
+
// rl.close() must run even if ask()/an unexpected throw escapes below -- an open stdin
|
|
55
|
+
// handle keeps the event loop alive, so a stray exception used to hang the process forever
|
|
56
|
+
// instead of exiting (this got worse once run.js stopped calling process.exit(1) on error).
|
|
57
|
+
try {
|
|
58
|
+
for (const d of unsigned) {
|
|
59
|
+
const answer = await ask(`Sign "${d.title}" now as Alex Bloom <alex@velaro.com>? [Y/n] `);
|
|
60
|
+
if (!answer || answer.toLowerCase() === 'y') {
|
|
61
|
+
process.stdout.write(' Signing... ');
|
|
62
|
+
try {
|
|
63
|
+
const result = await post('/Compliance/sign', {
|
|
64
|
+
documentSlug: d.slug,
|
|
65
|
+
signerName: 'Alex Bloom',
|
|
66
|
+
signerEmail: 'alex@velaro.com',
|
|
67
|
+
});
|
|
68
|
+
console.log(`done ✅`);
|
|
69
|
+
console.log(` ${result.legalAttestation.split('\n').slice(0, 5).join('\n ')}\n`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.log(`failed ❌ ${err?.message ?? err}`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log(` Skipped.\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} finally {
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\nDone. Run `velaro compliance status` to confirm all signatures.\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// velaro compliance status — show status and offer to sign unsigned docs
|
|
85
|
+
const complianceStatusCommand = {
|
|
86
|
+
command: 'status',
|
|
87
|
+
describe: 'Show compliance document signature status (and offer to sign unsigned docs)',
|
|
88
|
+
handler: runCommand(async () => { await runStatus(); }),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// velaro compliance sign <slug> — sign a specific doc directly
|
|
92
|
+
const complianceSignCommand = {
|
|
93
|
+
command: 'sign <slug>',
|
|
94
|
+
describe: 'Sign a specific compliance document (pci-saq-a | infosec-policy | irp)',
|
|
95
|
+
builder: (y) => y
|
|
96
|
+
.positional('slug', { describe: 'Document slug', type: 'string' })
|
|
97
|
+
.option('name', { type: 'string', describe: 'Signer name (default: Alex Bloom)' })
|
|
98
|
+
.option('email', { type: 'string', describe: 'Signer email (default: alex@velaro.com)' })
|
|
99
|
+
.option('notes', { type: 'string', describe: 'Optional note to attach to the signature' }),
|
|
100
|
+
handler: runCommand(async ({ slug, name, email, notes }) => {
|
|
101
|
+
console.log(`\nSigning: ${slug}...`);
|
|
102
|
+
const result = await post('/Compliance/sign', {
|
|
103
|
+
documentSlug: slug,
|
|
104
|
+
signerName: name ?? 'Alex Bloom',
|
|
105
|
+
signerEmail: email ?? 'alex@velaro.com',
|
|
106
|
+
notes,
|
|
107
|
+
});
|
|
108
|
+
console.log(`\n${result.message}`);
|
|
109
|
+
console.log('\n' + result.legalAttestation);
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// velaro compliance signatures — view audit log
|
|
114
|
+
const complianceSignaturesCommand = {
|
|
115
|
+
command: 'signatures',
|
|
116
|
+
describe: 'View the full compliance signature audit log',
|
|
117
|
+
builder: (y) => y
|
|
118
|
+
.option('slug', { type: 'string', describe: 'Filter by document slug' }),
|
|
119
|
+
handler: runCommand(async ({ slug }) => {
|
|
120
|
+
const sigs = await get(slug ? `/Compliance/signatures?slug=${slug}` : '/Compliance/signatures');
|
|
121
|
+
if (!sigs?.length) { console.log('No signatures on record.'); return; }
|
|
122
|
+
console.log(`\nCompliance Signatures (${sigs.length})\n${'─'.repeat(50)}`);
|
|
123
|
+
for (const s of sigs) {
|
|
124
|
+
const at = new Date(s.signedAt).toLocaleString('en-US', { timeZone: 'America/Los_Angeles' });
|
|
125
|
+
console.log(`\n[${s.id}] ${s.documentTitle} v${s.version}`);
|
|
126
|
+
console.log(` ${at} PT · ${s.signerName} <${s.signerEmail}>`);
|
|
127
|
+
console.log(` sha256: ${s.documentHash?.slice(0, 32)}...`);
|
|
128
|
+
if (s.notes) console.log(` note: ${s.notes}`);
|
|
129
|
+
}
|
|
130
|
+
console.log();
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// velaro compliance redaction-test <text> — Compliance & Redaction Phase 4 (2026-08-06):
|
|
135
|
+
// "why was this redacted?" sample tester, distinct subsystem (message-scrubber PII redaction)
|
|
136
|
+
// from the PCI document signing commands above — same `compliance` prefix family per the
|
|
137
|
+
// CLI+MCP parity rule's `feature_verb` naming convention, different resource underneath.
|
|
138
|
+
const complianceRedactionTestCommand = {
|
|
139
|
+
command: 'redaction-test <text>',
|
|
140
|
+
describe: 'Test sample text against Velaro\'s message-redaction patterns and see why each pattern fired',
|
|
141
|
+
builder: (y) => y
|
|
142
|
+
.positional('text', { describe: 'Sample text to test (wrap in quotes)', type: 'string' }),
|
|
143
|
+
handler: runCommand(async ({ text }) => {
|
|
144
|
+
const result = await post('/Scrubber/Test', { text });
|
|
145
|
+
console.log(`\nRedacted:\n ${result.redactedText}\n`);
|
|
146
|
+
if (!result.matches?.length) {
|
|
147
|
+
console.log('No patterns matched.\n');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log(`${result.matches.length} pattern(s) matched:\n`);
|
|
151
|
+
for (const m of result.matches) {
|
|
152
|
+
console.log(` • ${m.patternName} (${m.isBuiltIn ? 'built-in' : 'custom'})`);
|
|
153
|
+
console.log(` ${m.reason}\n`);
|
|
154
|
+
}
|
|
155
|
+
if (result.llmPassEnabled) {
|
|
156
|
+
console.log(`Note: ${result.llmNote}\n`);
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// velaro compliance redaction-status — Compliance & Redaction Phase 6 (2026-08-06): current
|
|
162
|
+
// channel/group override settings for the site. Same distinct-subsystem note as
|
|
163
|
+
// complianceRedactionTestCommand above — this is message-scrubber PII redaction, not PCI
|
|
164
|
+
// document signing.
|
|
165
|
+
const complianceRedactionStatusCommand = {
|
|
166
|
+
command: 'redaction-status',
|
|
167
|
+
describe: 'Show current channel/group PII-redaction override settings for this site',
|
|
168
|
+
handler: runCommand(async () => {
|
|
169
|
+
const settings = await get('/ComplianceRedactionSettings');
|
|
170
|
+
if (!settings?.length) {
|
|
171
|
+
console.log('\nNo channel/group redaction overrides configured — every channel is using the site-wide default.\n');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(`\nCompliance Redaction Settings (${settings.length} override row(s))\n${'─'.repeat(50)}`);
|
|
175
|
+
for (const s of settings) {
|
|
176
|
+
const scope = s.groupId != null ? `Group #${s.groupId}` : 'Site default';
|
|
177
|
+
const channel = s.conversationSource || 'All channels';
|
|
178
|
+
console.log(` [${s.id}] ${scope} · ${channel} → ${s.mode}`);
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// velaro compliance redaction-events — Compliance & Redaction Phase 6 (2026-08-06): recent
|
|
185
|
+
// audit log entries. Never prints the actual redacted value, only the pattern/hint metadata.
|
|
186
|
+
const complianceRedactionEventsCommand = {
|
|
187
|
+
command: 'redaction-events',
|
|
188
|
+
describe: 'View the PII-redaction audit log (which pattern fired, on which conversation)',
|
|
189
|
+
builder: (y) => y
|
|
190
|
+
.option('page', { type: 'number', describe: 'Page number (default 1)' })
|
|
191
|
+
.option('page-size', { type: 'number', describe: 'Rows per page, max 200 (default 50)' })
|
|
192
|
+
.option('pattern-name', { type: 'string', describe: 'Filter by pattern name (partial match)' })
|
|
193
|
+
.option('conversation-id',{ type: 'string', describe: 'Filter by conversation ID' })
|
|
194
|
+
.option('direction', { type: 'string', describe: 'Filter by direction (visitor | agent)' }),
|
|
195
|
+
handler: runCommand(async (argv) => {
|
|
196
|
+
const params = new URLSearchParams();
|
|
197
|
+
if (argv.page) params.set('page', argv.page);
|
|
198
|
+
if (argv['page-size']) params.set('pageSize', argv['page-size']);
|
|
199
|
+
if (argv['pattern-name']) params.set('patternName', argv['pattern-name']);
|
|
200
|
+
if (argv['conversation-id']) params.set('conversationId', argv['conversation-id']);
|
|
201
|
+
if (argv.direction) params.set('direction', argv.direction);
|
|
202
|
+
const qs = params.toString();
|
|
203
|
+
const result = await get(`/Scrubber/events${qs ? '?' + qs : ''}`);
|
|
204
|
+
if (!result?.events?.length) {
|
|
205
|
+
console.log(`\nNo redaction audit events found (page ${result?.page ?? 1}, total ${result?.total ?? 0}).\n`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
console.log(`\nRedaction Audit Log — ${result.total} total, page ${result.page} (${result.events.length} rows)\n${'─'.repeat(50)}`);
|
|
209
|
+
for (const e of result.events) {
|
|
210
|
+
console.log(`\n[${e.id}] ${e.createdAt}`);
|
|
211
|
+
console.log(` Pattern: ${e.patternName} (${e.isBuiltIn ? 'built-in' : 'custom'}) Direction: ${e.direction}`);
|
|
212
|
+
console.log(` Conversation: ${e.conversationId}`);
|
|
213
|
+
if (e.messageHint) console.log(` Hint: ${e.messageHint}`);
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// velaro compliance attestation <slug> — print legal attestation
|
|
220
|
+
const complianceAttestationCommand = {
|
|
221
|
+
command: 'attestation <slug>',
|
|
222
|
+
describe: 'Print the legal signature attestation for a document',
|
|
223
|
+
builder: (y) => y
|
|
224
|
+
.positional('slug', { describe: 'Document slug', type: 'string' }),
|
|
225
|
+
handler: runCommand(async ({ slug }) => {
|
|
226
|
+
const result = await get(`/Compliance/attestation/${slug}`);
|
|
227
|
+
console.log('\n' + result.attestationText);
|
|
228
|
+
}),
|
|
229
|
+
};
|