@velaro/cli 1.4.101 → 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.
Files changed (47) hide show
  1. package/README.md +19 -0
  2. package/bin/velaro.js +12 -0
  3. package/lib/api.js +2 -1
  4. package/lib/apple-pay.js +32 -0
  5. package/lib/apple-pay.test.js +32 -0
  6. package/lib/avatar-switch.js +42 -0
  7. package/lib/avatar-switch.test.js +49 -0
  8. package/lib/build-info.js +28 -0
  9. package/lib/build-info.test.js +24 -0
  10. package/lib/commands/apple-pay.js +73 -0
  11. package/lib/commands/avatar.js +60 -2
  12. package/lib/commands/billing-automation.js +172 -0
  13. package/lib/commands/copilotstudio.js +14 -1
  14. package/lib/commands/enterprise-reports.js +13 -1
  15. package/lib/commands/entitlement.js +29 -1
  16. package/lib/commands/focus.js +1 -1
  17. package/lib/commands/index.js +38 -2
  18. package/lib/commands/logs-insights.js +342 -0
  19. package/lib/commands/logs.js +313 -3
  20. package/lib/commands/magento.js +16 -0
  21. package/lib/commands/mcp-key.js +23 -1
  22. package/lib/commands/netsuite.js +67 -2
  23. package/lib/commands/notifications.js +34 -1
  24. package/lib/commands/ops.js +23 -54
  25. package/lib/commands/report-backfill.js +23 -5
  26. package/lib/commands/report.js +77 -14
  27. package/lib/commands/salesforce.js +101 -0
  28. package/lib/commands/shopify.js +84 -0
  29. package/lib/commands/signature.js +79 -0
  30. package/lib/commands/site-builder.js +30 -0
  31. package/lib/commands/support.js +22 -0
  32. package/lib/commands/teams.js +11 -1
  33. package/lib/commands/workflow.js +33 -0
  34. package/lib/commands/zoom-phone.js +15 -0
  35. package/lib/enterprise-reports.test.js +20 -1
  36. package/lib/entitlement-report-packages.test.js +47 -0
  37. package/lib/logs-insights.test.js +146 -0
  38. package/lib/mcp-key.test.js +11 -0
  39. package/lib/messaging-routes.test.js +10 -1
  40. package/lib/oauth.js +14 -2
  41. package/lib/report-range.js +21 -0
  42. package/lib/report-range.test.js +38 -0
  43. package/lib/shopify-write-policy.test.js +34 -0
  44. package/lib/signature-policy.js +70 -0
  45. package/lib/signature-policy.test.js +48 -0
  46. package/lib/year-in-review-range.js +44 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -138,6 +138,25 @@ velaro kb article screenshot 1234 \
138
138
 
139
139
  > Titles are auto-normalized to Title Case with no dashes, even if you pass a slug-style name — use `--no-title-normalize` to skip that.
140
140
 
141
+ ### `velaro logs insights <subcommand>` (Velaro staff only)
142
+
143
+ Grouped views over Velaro Logger data (same data as the Admin UI "Insights" tab). All subcommands accept `--last 6h`, `--platform v10|v20|admin`, `--environment Production|Staging`, `--site <id>`, `--env staging|prod` and `--json`.
144
+
145
+ ```bash
146
+ velaro logs insights groups --last 6h --platform v20 # error groups, most frequent first
147
+ velaro logs insights new # groups not seen in the previous 7 days
148
+ velaro logs insights regression --at 2h # before vs after a deploy
149
+ velaro logs insights trace <correlationId> # cross-platform timeline
150
+ velaro logs insights trace --site 1032 --last 30m
151
+ velaro logs insights anomalies --hours 48
152
+ velaro logs insights sites # per-site scorecard
153
+ velaro logs insights mutes
154
+ velaro logs insights mute <fingerprint> --reason "known outage" --expires 7d
155
+ velaro logs insights unmute <fingerprint>
156
+ velaro logs insights sensitive # emails/tokens/cards in logs
157
+ velaro logs insights brief <fingerprint> # AI markdown brief
158
+ ```
159
+
141
160
  ### `velaro status`
142
161
  Check that the Velaro API is reachable.
143
162
 
package/bin/velaro.js CHANGED
@@ -35,6 +35,7 @@ import { updateCommand } from '../lib/commands/update.js';
35
35
  import { agentCommand } from '../lib/commands/agent.js';
36
36
  import { linkedIdentityCommand } from '../lib/commands/linked-identity.js';
37
37
  import { notificationsCommand } from '../lib/commands/notifications.js';
38
+ import { signatureCommand } from '../lib/commands/signature.js';
38
39
  import { articleCommand } from '../lib/commands/article.js';
39
40
  import { sharepointCommand } from '../lib/commands/sharepoint.js';
40
41
  import { calendlyCommand } from '../lib/commands/calendly.js';
@@ -67,6 +68,7 @@ import { triageCommand } from '../lib/commands/triage.js';
67
68
  import { automationDraftCommand } from '../lib/commands/automation-draft.js';
68
69
  import { magentoCommand } from '../lib/commands/magento.js';
69
70
  import { sapCommand } from '../lib/commands/sap.js';
71
+ import { salesforceCommand } from '../lib/commands/salesforce.js';
70
72
  import { actionsCommand } from '../lib/commands/actions.js';
71
73
  import { mondayCommand } from '../lib/commands/monday.js';
72
74
  import { notionCommand } from '../lib/commands/notion.js';
@@ -80,6 +82,7 @@ import { zoomPhoneCommand } from '../lib/commands/zoom-phone.js';
80
82
  import { teamsCommand } from '../lib/commands/teams.js';
81
83
  import { slackCommand } from '../lib/commands/slack.js';
82
84
  import { webflowCommand } from '../lib/commands/webflow.js';
85
+ import { shopifyCommand } from '../lib/commands/shopify.js';
83
86
  import { netsuiteCommand } from '../lib/commands/netsuite.js';
84
87
  import { woocommerceCommand } from '../lib/commands/woocommerce.js';
85
88
  import { squarespaceCommand } from '../lib/commands/squarespace.js';
@@ -89,6 +92,7 @@ import { migrationStatusCommand } from '../lib/commands/migration-status.js';
89
92
  import { complianceCommand } from '../lib/commands/compliance.js';
90
93
  import { billingContactsCommand } from '../lib/commands/billing-contacts.js';
91
94
  import { billingSubscriptionCommand } from '../lib/commands/billing-subscription.js';
95
+ import { billingAutomationCommand } from '../lib/commands/billing-automation.js';
92
96
  import { orderFormCommand } from '../lib/commands/order-form.js';
93
97
  import { billingEmailPreferenceCommand } from '../lib/commands/billing-email-preference.js';
94
98
  import { billingCommand } from '../lib/commands/billing.js';
@@ -110,6 +114,8 @@ import { emailInboxCommand } from '../lib/commands/email-inbox.js';
110
114
  import { callbackCommand } from '../lib/commands/callback.js';
111
115
  import { caseCommand } from '../lib/commands/case.js';
112
116
  import { pickupCommand } from '../lib/commands/pickup.js';
117
+ import { applePayCommand } from '../lib/commands/apple-pay.js';
118
+ import { siteBuilderCommand } from '../lib/commands/site-builder.js';
113
119
  import { paymentRecoveryCommand } from '../lib/commands/payment-recovery.js';
114
120
  import { recruitingCommand } from '../lib/commands/recruiting.js';
115
121
  import { surveyCommand } from '../lib/commands/survey.js';
@@ -172,6 +178,7 @@ await yargs(hideBin(process.argv))
172
178
  .command(agentCommand)
173
179
  .command(linkedIdentityCommand)
174
180
  .command(notificationsCommand)
181
+ .command(signatureCommand)
175
182
  .command(articleCommand)
176
183
  .command(sharepointCommand)
177
184
  .command(calendlyCommand)
@@ -206,6 +213,7 @@ await yargs(hideBin(process.argv))
206
213
  .command(automationDraftCommand)
207
214
  .command(magentoCommand)
208
215
  .command(sapCommand)
216
+ .command(salesforceCommand)
209
217
  .command(actionsCommand)
210
218
  .command(mondayCommand)
211
219
  .command(notionCommand)
@@ -219,6 +227,7 @@ await yargs(hideBin(process.argv))
219
227
  .command(teamsCommand)
220
228
  .command(slackCommand)
221
229
  .command(webflowCommand)
230
+ .command(shopifyCommand)
222
231
  .command(netsuiteCommand)
223
232
  .command(woocommerceCommand)
224
233
  .command(squarespaceCommand)
@@ -228,6 +237,7 @@ await yargs(hideBin(process.argv))
228
237
  .command(complianceCommand)
229
238
  .command(billingContactsCommand)
230
239
  .command(billingSubscriptionCommand)
240
+ .command(billingAutomationCommand)
231
241
  .command(orderFormCommand)
232
242
  .command(billingEmailPreferenceCommand)
233
243
  .command(billingCommand)
@@ -249,6 +259,8 @@ await yargs(hideBin(process.argv))
249
259
  .command(callbackCommand)
250
260
  .command(caseCommand)
251
261
  .command(pickupCommand)
262
+ .command(applePayCommand)
263
+ .command(siteBuilderCommand)
252
264
  .command(paymentRecoveryCommand)
253
265
  .command(recruitingCommand)
254
266
  .command(surveyCommand)
package/lib/api.js CHANGED
@@ -60,7 +60,7 @@ export const post = (path, body) => request('POST', path, body);
60
60
  export const put = (path, body) => request('PUT', path, body);
61
61
  export const del = (path) => request('DELETE', path);
62
62
 
63
- export async function messagingRequest(method, path, body, envOverride, extraHeaders = {}) {
63
+ export async function messagingRequest(method, path, body, envOverride, extraHeaders = {}, opts = {}) {
64
64
  const creds = await getCredentials(envOverride);
65
65
  const res = await fetch(joinUrl(creds.messagingApiBase, path), {
66
66
  method,
@@ -75,6 +75,7 @@ export async function messagingRequest(method, path, body, envOverride, extraHea
75
75
  ...extraHeaders,
76
76
  },
77
77
  body: body !== undefined ? JSON.stringify(body) : undefined,
78
+ ...(opts.timeoutMs ? { signal: AbortSignal.timeout(opts.timeoutMs) } : {}),
78
79
  });
79
80
  if (!res.ok) {
80
81
  let msg = `${method} ${path} → ${res.status}`;
@@ -0,0 +1,32 @@
1
+ // Pure helpers for `velaro apple-pay ...` (no I/O except the optional logo file read, so they are unit-testable).
2
+ // Backend: velaro-messaging ApplePayConfigsController (GET/POST ApplePayConfig, PUT ApplePayConfig/{id}).
3
+ // Only display + test-mode fields are exposed here. The merchant id / certificate decide the CHARGE and stay behind
4
+ // secure Velaro admin provisioning; this CLI deliberately cannot set them.
5
+
6
+ import fs from 'node:fs';
7
+
8
+ export const LOGO_MAX_BYTES = 500 * 1024;
9
+
10
+ /** Returns the base64 of a PNG/JPEG file under 500 KB, or throws with a plain-language reason. */
11
+ export function readLogoBase64(filePath) {
12
+ const bytes = fs.readFileSync(filePath);
13
+ if (bytes.length > LOGO_MAX_BYTES) throw new Error('--logo must be under 500 KB.');
14
+ const png = bytes.length > 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47;
15
+ const jpg = bytes.length > 8 && bytes[0] === 0xff && bytes[1] === 0xd8;
16
+ if (!png && !jpg) throw new Error('--logo must be a PNG or JPEG image.');
17
+ return bytes.toString('base64');
18
+ }
19
+
20
+ /**
21
+ * Builds the PUT/POST body from CLI args. Apply-if-present: only fields the operator named are sent.
22
+ * argv: { name, testMode, logo, clearLogo }. Returns {} when nothing was named.
23
+ */
24
+ export function buildApplePayUpdate(argv, readLogo = readLogoBase64) {
25
+ if (argv.logo && argv.clearLogo) throw new Error('Use either --logo or --clear-logo, not both.');
26
+ const body = {};
27
+ if (argv.name !== undefined) body.merchantDisplayName = String(argv.name);
28
+ if (argv.testMode !== undefined) body.isTestMode = !!argv.testMode;
29
+ if (argv.logo) body.bubbleLogoBase64 = readLogo(argv.logo);
30
+ else if (argv.clearLogo) body.bubbleLogoBase64 = '';
31
+ return body;
32
+ }
@@ -0,0 +1,32 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { buildApplePayUpdate, readLogoBase64, LOGO_MAX_BYTES } from './apple-pay.js';
7
+
8
+ function tmpFile(bytes) {
9
+ const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'applepay-')), 'logo.bin');
10
+ fs.writeFileSync(f, bytes);
11
+ return f;
12
+ }
13
+ const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
14
+
15
+ test('only named fields are sent, with the exact JSON names the controller binds', () => {
16
+ assert.deepEqual(buildApplePayUpdate({ name: 'Aurora Guitars' }), { merchantDisplayName: 'Aurora Guitars' });
17
+ assert.deepEqual(buildApplePayUpdate({ testMode: true }), { isTestMode: true });
18
+ assert.deepEqual(buildApplePayUpdate({ testMode: false }), { isTestMode: false });
19
+ assert.deepEqual(buildApplePayUpdate({}), {});
20
+ });
21
+
22
+ test('clear-logo sends an empty string; logo and clear-logo together are rejected', () => {
23
+ assert.deepEqual(buildApplePayUpdate({ clearLogo: true }), { bubbleLogoBase64: '' });
24
+ assert.throws(() => buildApplePayUpdate({ logo: 'x.png', clearLogo: true }), /either --logo or --clear-logo/);
25
+ });
26
+
27
+ test('logo is read as base64 and validated as PNG/JPEG under 500 KB', () => {
28
+ assert.equal(readLogoBase64(tmpFile(PNG)), PNG.toString('base64'));
29
+ assert.equal(buildApplePayUpdate({ logo: tmpFile(PNG) }).bubbleLogoBase64, PNG.toString('base64'));
30
+ assert.throws(() => readLogoBase64(tmpFile(Buffer.from('GIF89a-not-allowed'))), /PNG or JPEG/);
31
+ assert.throws(() => readLogoBase64(tmpFile(Buffer.concat([PNG, Buffer.alloc(LOGO_MAX_BYTES)]))), /under 500 KB/);
32
+ });
@@ -0,0 +1,42 @@
1
+ // Pure helpers for `velaro avatar workflow-switch | bulk-switch`.
2
+ // The on / off / rules switch per workflow or team lives in velaro-messaging (VelaroAvatarController):
3
+ // PUT /avatar/workflows/{id}/enabled body {"mode":"on"|"off"|"rules"}
4
+ // PUT /avatar/overrides/enabled body {"mode":..., "workflowIds":[], "groupIds":[]} (max 500 rows)
5
+ // "rules" clears the switch so the site rules decide. Kept dependency-free so it is unit-testable.
6
+
7
+ export const SWITCH_VALUES = ['on', 'off', 'rules'];
8
+ const MAX_BULK_ROWS = 500;
9
+
10
+ /** Returns the normalized mode or throws. Mirrors AvatarOverrideEnabledMode.TryParse on the server. */
11
+ export function parseSwitchValue(raw) {
12
+ const v = String(raw ?? '').trim().toLowerCase();
13
+ if (!SWITCH_VALUES.includes(v)) throw new Error(`The value must be one of: ${SWITCH_VALUES.join(', ')}`);
14
+ return v;
15
+ }
16
+
17
+ /** Parses "12,13, 14" into positive whole ids, de-duplicated. Throws on anything else. */
18
+ export function parseIdList(raw, label = '--ids') {
19
+ const parts = String(raw ?? '').split(',').map(s => s.trim()).filter(s => s !== '');
20
+ if (parts.length === 0) throw new Error(`${label} needs at least one id, e.g. ${label} 12,13`);
21
+ const ids = parts.map(p => {
22
+ const n = Number(p);
23
+ if (!/^\d+$/.test(p) || !Number.isInteger(n) || n <= 0) throw new Error(`${label} must be positive whole numbers; got "${p}"`);
24
+ return n;
25
+ });
26
+ return [...new Set(ids)];
27
+ }
28
+
29
+ /** Body for PUT /avatar/workflows/{id}/enabled. */
30
+ export function workflowSwitchBody(raw) {
31
+ return { mode: parseSwitchValue(raw) };
32
+ }
33
+
34
+ /** Body for PUT /avatar/overrides/enabled. Enforces the server's selection rules up front so a bad call fails before the network. */
35
+ export function bulkSwitchBody({ ids, groups, value }) {
36
+ const mode = parseSwitchValue(value);
37
+ const workflowIds = ids === undefined ? [] : parseIdList(ids, '--ids');
38
+ const groupIds = groups === undefined ? [] : parseIdList(groups, '--groups');
39
+ if (workflowIds.length + groupIds.length === 0) throw new Error('Pass --ids <workflowIds> and/or --groups <teamIds>');
40
+ if (workflowIds.length + groupIds.length > MAX_BULK_ROWS) throw new Error(`Too many rows in one call (max ${MAX_BULK_ROWS})`);
41
+ return { mode, workflowIds, groupIds };
42
+ }
@@ -0,0 +1,49 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import { parseSwitchValue, parseIdList, workflowSwitchBody, bulkSwitchBody } from './avatar-switch.js';
5
+
6
+ test('switch value accepts on, off, rules and normalizes case; anything else is rejected', () => {
7
+ assert.equal(parseSwitchValue('ON'), 'on');
8
+ assert.equal(parseSwitchValue(' rules '), 'rules');
9
+ assert.throws(() => parseSwitchValue('sometimes'), /one of: on, off, rules/);
10
+ assert.throws(() => parseSwitchValue(''), /one of/);
11
+ assert.throws(() => parseSwitchValue(undefined), /one of/);
12
+ });
13
+
14
+ test('id list parses comma lists, de-duplicates, and rejects non-positive or non-integer ids', () => {
15
+ assert.deepEqual(parseIdList('12, 13,12'), [12, 13]);
16
+ assert.throws(() => parseIdList(''), /at least one id/);
17
+ assert.throws(() => parseIdList('12,abc'), /positive whole numbers/);
18
+ assert.throws(() => parseIdList('0'), /positive whole numbers/);
19
+ assert.throws(() => parseIdList('1.5'), /positive whole numbers/);
20
+ assert.throws(() => parseIdList('-3'), /positive whole numbers/);
21
+ });
22
+
23
+ test('workflow switch sends exactly {mode}', () => {
24
+ assert.deepEqual(workflowSwitchBody('off'), { mode: 'off' });
25
+ assert.deepEqual(workflowSwitchBody('rules'), { mode: 'rules' });
26
+ assert.throws(() => workflowSwitchBody('maybe'), /one of/);
27
+ });
28
+
29
+ test('bulk switch builds the request shape and enforces selection rules', () => {
30
+ assert.deepEqual(bulkSwitchBody({ ids: '5,6', value: 'on' }), { mode: 'on', workflowIds: [5, 6], groupIds: [] });
31
+ assert.deepEqual(bulkSwitchBody({ groups: '9', value: 'rules' }), { mode: 'rules', workflowIds: [], groupIds: [9] });
32
+ assert.throws(() => bulkSwitchBody({ value: 'on' }), /Pass --ids/);
33
+ assert.throws(() => bulkSwitchBody({ ids: '5', value: 'bogus' }), /one of/);
34
+ const many = Array.from({ length: 501 }, (_, i) => i + 1).join(',');
35
+ assert.throws(() => bulkSwitchBody({ ids: many, value: 'off' }), /max 500/);
36
+ });
37
+
38
+ test('avatar command registers workflow-switch and bulk-switch and the MCP server exposes matching tools', () => {
39
+ const src = fs.readFileSync(new URL('./commands/avatar.js', import.meta.url), 'utf8');
40
+ assert.match(src, /command:\s+'workflow-switch <workflowId> <value>'/);
41
+ assert.match(src, /command:\s+'bulk-switch'/);
42
+ assert.match(src, /\/avatar\/workflows\/\$\{id\}\/enabled/);
43
+ assert.match(src, /\/avatar\/overrides\/enabled/);
44
+ const mcp = fs.readFileSync(new URL('../../mcp/server.js', import.meta.url), 'utf8');
45
+ assert.match(mcp, /name: 'avatar_set_workflow_switch'/);
46
+ assert.match(mcp, /name: 'avatar_bulk_set_switch'/);
47
+ assert.match(mcp, /`\/avatar\/workflows\/\$\{id\}\/enabled`/);
48
+ assert.match(mcp, /'\/avatar\/overrides\/enabled'/);
49
+ });
@@ -0,0 +1,28 @@
1
+ // Real running-build info from each service's /api/health `build` block.
2
+ // Admin reports {sha, shortSha, builtAt}; messaging reports {commitId, shortCommitId, builtAt}.
3
+ export function parseBuild(health) {
4
+ const b = health?.build;
5
+ if (!b) return null;
6
+ const full = b.commitId ?? b.sha ?? null;
7
+ const short = b.shortCommitId ?? b.shortSha ?? (full ? String(full).slice(0, 7) : null);
8
+ if (!short && !b.builtAt) return null;
9
+ return { short: short ?? '?', builtAt: b.builtAt ?? null };
10
+ }
11
+
12
+ export function ptLabel(dateStr) {
13
+ const d = new Date(dateStr);
14
+ const s = d.toLocaleString('en-US', {
15
+ timeZone: 'America/Los_Angeles', month: '2-digit', day: '2-digit',
16
+ hour: '2-digit', minute: '2-digit', hour12: false,
17
+ });
18
+ const tz = d.toLocaleString('en-US', { timeZone: 'America/Los_Angeles', timeZoneName: 'short' }).split(' ').pop();
19
+ return `${s.replace(',', '')} ${tz}`;
20
+ }
21
+
22
+ export function formatBuild(build, now = Date.now()) {
23
+ if (!build) return 'build unknown';
24
+ if (!build.builtAt) return `commit ${build.short} built unknown`;
25
+ const mins = Math.max(0, Math.round((now - new Date(build.builtAt).getTime()) / 60000));
26
+ const age = mins < 2 ? 'just now' : mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.round(mins / 60)}h ago` : `${Math.round(mins / 1440)}d ago`;
27
+ return `commit ${build.short} built ${ptLabel(build.builtAt)} (${age})`;
28
+ }
@@ -0,0 +1,24 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parseBuild, formatBuild } from './build-info.js';
4
+
5
+ test('parses messaging health build (commitId/shortCommitId)', () => {
6
+ const b = parseBuild({ build: { commitId: '5398fce8069e98de38edacb74db8c77155ca70a5', shortCommitId: '5398fce', builtAt: '2026-10-12T05:28:25Z' } });
7
+ assert.deepEqual(b, { short: '5398fce', builtAt: '2026-10-12T05:28:25Z' });
8
+ });
9
+
10
+ test('parses admin health build (sha/shortSha)', () => {
11
+ const b = parseBuild({ build: { sha: 'a908399d25ee3b0d35bcfca2a499ae6508d03c88', shortSha: 'a908399', builtAt: '2026-10-12T01:22:28Z' } });
12
+ assert.equal(b.short, 'a908399');
13
+ });
14
+
15
+ test('derives short from full sha; null without build block', () => {
16
+ assert.equal(parseBuild({ build: { sha: 'abcdef1234567', builtAt: 'x' } }).short, 'abcdef1');
17
+ assert.equal(parseBuild({}), null);
18
+ });
19
+
20
+ test('formats commit, PT time and age', () => {
21
+ const now = new Date('2026-10-12T05:50:25Z').getTime();
22
+ const s = formatBuild({ short: '5398fce', builtAt: '2026-10-12T05:28:25Z' }, now);
23
+ assert.match(s, /^commit 5398fce built 10\/11 22:28 PDT \(22m ago\)$/);
24
+ });
@@ -0,0 +1,73 @@
1
+ import { messagingGet, messagingPost, messagingPut } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+ import { buildApplePayUpdate } from '../apple-pay.js';
4
+
5
+ // ── velaro apple-pay ───────────────────────────────────────────────────────────
6
+ // CLI surface for the per-site Apple Pay display settings (business name, payment bubble logo, test mode).
7
+ // siteId is applied server-side from the vmsg-siteId header -- never a CLI arg.
8
+ //
9
+ // Backend contract: server/Velaro.Messaging/Controllers/Channels/ApplePayConfigsController.cs
10
+ // GET ApplePayConfig -> dto (id 0 when the site has no row yet)
11
+ // POST ApplePayConfig -> create (409 if one already exists)
12
+ // PUT ApplePayConfig/{id} -> apply-if-present update
13
+
14
+ const BOLD = '\x1b[1m';
15
+ const DIM = '\x1b[2m';
16
+ const GRN = '\x1b[32m';
17
+ const RST = '\x1b[0m';
18
+
19
+ async function configGetHandler() {
20
+ const c = await messagingGet('/ApplePayConfig');
21
+ console.log(`\n${BOLD}Apple Pay config${RST} (id ${c.id || 'none yet'})`);
22
+ console.log(` Enabled: ${c.isEnabled ? `${GRN}yes${RST}` : 'no'}`);
23
+ console.log(` Test mode (sandbox): ${c.isTestMode ? 'on -- no real money moves' : 'off'}`);
24
+ console.log(` Business name shown: ${c.merchantDisplayName ?? `${DIM}(default)${RST}`}`);
25
+ console.log(` Bubble logo: ${c.hasBubbleLogo ? 'custom' : `${DIM}(default image)${RST}`}`);
26
+ console.log(` Merchant: ${c.usingPlatformDefault ? `Velaro platform (${c.platformMerchantIdentifier || 'default'})` : c.merchantIdentifier}`);
27
+ console.log('');
28
+ }
29
+
30
+ async function configSetHandler(argv) {
31
+ const body = buildApplePayUpdate(argv);
32
+ if (Object.keys(body).length === 0) {
33
+ throw new Error('Nothing to set. Pass --name, --test-mode, --logo <file> or --clear-logo.');
34
+ }
35
+ const existing = await messagingGet('/ApplePayConfig');
36
+ const config = existing && existing.id > 0
37
+ ? await messagingPut(`/ApplePayConfig/${existing.id}`, body)
38
+ : await messagingPost('/ApplePayConfig', body);
39
+ console.log(`\n${GRN}✓${RST} Apple Pay config ${existing && existing.id > 0 ? 'updated' : 'created'} (id ${config.id}).\n`);
40
+ }
41
+
42
+ const configCommand = {
43
+ command: 'config <subcommand>',
44
+ describe: 'Get or set this site\'s Apple Pay display settings and test mode',
45
+ builder: (y) => y
46
+ .command({
47
+ command: 'get',
48
+ describe: 'Show this site\'s Apple Pay config',
49
+ handler: runCommand(configGetHandler),
50
+ })
51
+ .command({
52
+ command: 'set',
53
+ describe: 'Create or update display settings (apply-if-present -- omit a flag to leave it unchanged). Display only: never changes the charge.',
54
+ builder: (y2) => y2
55
+ .option('name', { describe: 'Business name shown on the Apple Pay sheet (pass "" to clear)', type: 'string' })
56
+ .option('test-mode', { describe: 'Sandbox mode: Velaro Stripe test account, no real money, real Wallet cards decline', type: 'boolean' })
57
+ .option('logo', { describe: 'Path to a PNG/JPEG (under 500 KB) for the payment bubble', type: 'string' })
58
+ .option('clear-logo', { describe: 'Remove the custom logo and use the default image', type: 'boolean' }),
59
+ handler: runCommand(configSetHandler),
60
+ })
61
+ .demandCommand(1, 'Specify a config subcommand: get | set'),
62
+ handler: () => {},
63
+ };
64
+
65
+ export const applePayCommand = {
66
+ command: 'apple-pay <subcommand>',
67
+ describe: 'Apple Pay (Apple Messages for Business) -- display settings and test mode',
68
+ builder: (y) => y
69
+ .command(configCommand)
70
+ .demandCommand(1, 'Specify a subcommand: config')
71
+ .strict(),
72
+ handler: () => {},
73
+ };
@@ -1,6 +1,7 @@
1
1
  import { request, messagingGet, messagingPut, messagingDel } from '../api.js';
2
2
  import { overrideBody, scopePath } from '../avatar-override.js';
3
3
  import { applyMotionArgs } from '../avatar-motion.js';
4
+ import { SWITCH_VALUES, workflowSwitchBody, bulkSwitchBody } from '../avatar-switch.js';
4
5
  import { runCommand } from '../run.js';
5
6
 
6
7
  // ── velaro avatar ─────────────────────────────────────────────────────────────
@@ -28,7 +29,9 @@ export const avatarCommand = {
28
29
  console.log(`\nSite ${argv.site}: AI Video Avatar ${data.enabled ? 'ENABLED ✓' : 'configured but DISABLED'}`);
29
30
  console.log(` Provider: ${data.providerSlug}`);
30
31
  console.log(` Avatar ID: ${data.avatarId || '(provider default)'}`);
32
+ console.log(` Face/provider: ${data.avatarIdByProvider && Object.keys(data.avatarIdByProvider).length ? JSON.stringify(data.avatarIdByProvider) : '(none saved)'} (read-only)`);
31
33
  console.log(` Voice ID: ${data.voiceId || '(site default)'}`);
34
+ console.log(` Keep on agent join: ${data.enabledOnAgentTransfer ? 'yes' : 'no'}`);
32
35
  console.log(` Enabled: ${data.enabled ? 'yes' : 'no'}`);
33
36
  console.log(` "Call me": ${data.showCallMeOption ? 'yes' : 'no'}`);
34
37
  console.log(` Callback window: ${data.callbackConfirmWindowMinutes} min`);
@@ -37,7 +40,32 @@ export const avatarCommand = {
37
40
  console.log(` Size: ${data.sizePx}px`);
38
41
  console.log(` Body motion: ${data.motionLevel || '(default: natural)'} @ ${data.motionIntensityPercent ?? 100}%`);
39
42
  console.log(` Max minutes/mo: ${data.maxAvatarMinutesPerMonth ?? '(plan default)'}`);
40
- console.log(` Max TTS chars/mo: ${data.maxAvatarTtsChars ?? '(plan default)'}\n`);
43
+ console.log(` Max TTS chars/mo: ${data.maxAvatarTtsChars ?? '(plan default)'}`);
44
+ try {
45
+ const ent = await request('GET', `/Entitlements/${argv.site}`);
46
+ console.log(` HeyGen entitled: ${ent?.features?.EnableAvatarHeyGen ? 'yes' : 'no'}\n`);
47
+ } catch (e) { console.log(` HeyGen entitled: unknown (${e.message})\n`); }
48
+ }),
49
+ })
50
+ .command({
51
+ command: 'heygen',
52
+ describe: 'Grant or revoke the HeyGen provider entitlement (EnableAvatarHeyGen) for a site',
53
+ builder: y => y
54
+ .option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
55
+ .option('enabled', { type: 'boolean', demandOption: true, describe: 'true = grant, false = revoke' }),
56
+ handler: runCommand(async argv => {
57
+ await request('POST', `/Entitlements/${argv.site}/override`, { featureKey: 'EnableAvatarHeyGen', value: argv.enabled, reason: 'velaro avatar heygen' });
58
+ const ent = await request('GET', `/Entitlements/${argv.site}`);
59
+ console.log(`\n✓ Site ${argv.site}: EnableAvatarHeyGen=${ent?.features?.EnableAvatarHeyGen ? 'true' : 'false'}\n`);
60
+ }),
61
+ })
62
+ .command({
63
+ command: 'session-readiness',
64
+ describe: 'Staff diagnostic: why an avatar session will or will not start for a site (plan, config, key, billing, concurrency). Read-only, no provider call.',
65
+ builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
66
+ handler: runCommand(async argv => {
67
+ const data = await messagingGet(`/avatar/diagnostics/session-readiness?siteId=${argv.site}`);
68
+ console.log(JSON.stringify(data, null, 2));
41
69
  }),
42
70
  })
43
71
  .command({
@@ -49,6 +77,7 @@ export const avatarCommand = {
49
77
  .option('bot', { type: 'string', describe: 'Reserved for a future per-bot override (not yet implemented on this endpoint)' })
50
78
  .option('avatar-id',{ type: 'string', describe: 'Provider-specific avatar/face ID. Omit for provider default.' })
51
79
  .option('voice', { type: 'string', describe: 'ElevenLabs voice ID override' })
80
+ .option('keep-on-agent-join', { type: 'boolean', describe: 'Keep the avatar running after a human agent joins. Omit on a re-run to leave it untouched.' })
52
81
  .option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' })
53
82
  .option('display-name', { type: 'string', describe: 'Visitor-facing avatar name on the badge (e.g. Moshky). Empty string resets to "AI".' })
54
83
  .option('show-badge', { type: 'boolean', describe: 'Show the name badge over the avatar video.' })
@@ -66,6 +95,7 @@ export const avatarCommand = {
66
95
  body.providerSlug = argv.provider ?? 'velaro';
67
96
  if (argv['avatar-id'] !== undefined) body.avatarId = argv['avatar-id'];
68
97
  if (argv.voice !== undefined) body.voiceId = argv.voice;
98
+ if (argv['keep-on-agent-join'] !== undefined) body.enabledOnAgentTransfer = argv['keep-on-agent-join'];
69
99
  if (argv['call-me'] !== undefined) body.showCallMeOption = argv['call-me'];
70
100
  if (argv['display-name'] !== undefined) body.displayName = argv['display-name'];
71
101
  if (argv['show-badge'] !== undefined) body.showBadge = argv['show-badge'];
@@ -163,5 +193,33 @@ export const avatarCommand = {
163
193
  `);
164
194
  }),
165
195
  })
166
- .demandCommand(1, 'Specify a subcommand: get | enable | disable | set | override | effective | reset'),
196
+ .command({
197
+ command: 'workflow-switch <workflowId> <value>',
198
+ describe: 'Switch the avatar on, off, or back to the site rules (rules) for ONE workflow of your site',
199
+ builder: y => y
200
+ .positional('workflowId', { type: 'number', describe: 'Workflow id' })
201
+ .positional('value', { type: 'string', choices: SWITCH_VALUES, describe: 'on | off | rules (rules = the site rules decide)' }),
202
+ handler: runCommand(async argv => {
203
+ const id = Number(argv.workflowId);
204
+ if (!Number.isInteger(id) || id <= 0) throw new Error('The workflow id must be a positive whole number');
205
+ const data = await messagingPut(`/avatar/workflows/${id}/enabled`, workflowSwitchBody(argv.value));
206
+ if (!data?.success) throw new Error(data?.error || 'Switch failed');
207
+ console.log(`\n✓ Workflow ${id} avatar switch: ${data.mode} (${data.changed ? 'changed' : 'already set'})\n`);
208
+ }),
209
+ })
210
+ .command({
211
+ command: 'bulk-switch',
212
+ describe: 'Set the avatar switch for many workflows and/or teams of your site in one call (max 500)',
213
+ builder: y => y
214
+ .option('ids', { type: 'string', describe: 'Comma-separated workflow ids, e.g. 12,13,14' })
215
+ .option('groups', { type: 'string', describe: 'Comma-separated team (group) ids' })
216
+ .option('value', { type: 'string', choices: SWITCH_VALUES, demandOption: true, describe: 'on | off | rules' }),
217
+ handler: runCommand(async argv => {
218
+ const body = bulkSwitchBody({ ids: argv.ids, groups: argv.groups, value: argv.value });
219
+ const data = await messagingPut('/avatar/overrides/enabled', body);
220
+ if (!data?.success) throw new Error(data?.error || 'Bulk switch failed');
221
+ console.log(`\n✓ Bulk avatar switch ${data.mode}: ${data.changed} of ${data.requested} rows changed\n`);
222
+ }),
223
+ })
224
+ .demandCommand(1, 'Specify a subcommand: get | enable | disable | set | override | effective | reset | workflow-switch | bulk-switch'),
167
225
  };