@velaro/cli 1.4.102 → 1.4.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/bin/velaro.js +14 -0
- package/lib/api.js +2 -1
- package/lib/apple-pay-presets.js +66 -0
- package/lib/apple-pay-presets.test.js +61 -0
- package/lib/apple-pay.js +32 -0
- package/lib/apple-pay.test.js +32 -0
- package/lib/avatar-switch.js +42 -0
- package/lib/avatar-switch.test.js +49 -0
- package/lib/build-info.js +28 -0
- package/lib/build-info.test.js +24 -0
- package/lib/commands/apple-pay-preset.js +65 -0
- package/lib/commands/apple-pay.js +73 -0
- package/lib/commands/avatar.js +60 -2
- package/lib/commands/billing-automation.js +184 -0
- package/lib/commands/copilotstudio.js +14 -1
- package/lib/commands/enterprise-reports.js +1 -0
- package/lib/commands/entitlement.js +79 -1
- package/lib/commands/env.js +1 -1
- package/lib/commands/focus.js +1 -1
- package/lib/commands/index.js +38 -2
- package/lib/commands/logs-insights.js +342 -0
- package/lib/commands/logs.js +310 -11
- package/lib/commands/magento.js +16 -0
- package/lib/commands/mcp-key.js +7 -1
- package/lib/commands/netsuite.js +67 -2
- package/lib/commands/notifications.js +34 -1
- package/lib/commands/ops.js +23 -54
- package/lib/commands/report-backfill.js +23 -5
- package/lib/commands/report.js +77 -14
- package/lib/commands/salesforce.js +101 -0
- package/lib/commands/shopify.js +84 -0
- package/lib/commands/signature.js +79 -0
- package/lib/commands/site-builder.js +30 -0
- package/lib/commands/support.js +22 -0
- package/lib/commands/teams.js +11 -1
- package/lib/commands/workflow.js +157 -2
- package/lib/commands/zoom-phone.js +15 -0
- package/lib/enterprise-reports.test.js +1 -1
- package/lib/entitlement-report-packages.test.js +47 -0
- package/lib/logs-insights.test.js +146 -0
- package/lib/mcp-key.test.js +11 -0
- package/lib/messaging-routes.test.js +10 -1
- package/lib/oauth.js +14 -2
- package/lib/report-range.js +21 -0
- package/lib/report-range.test.js +38 -0
- package/lib/shopify-write-policy.test.js +34 -0
- package/lib/signature-policy.js +70 -0
- package/lib/signature-policy.test.js +48 -0
- package/package.json +1 -1
package/lib/commands/avatar.js
CHANGED
|
@@ -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)'}
|
|
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
|
-
.
|
|
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
|
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// velaro billing-automation — review-hold and dry-run switches for invoice emails and charges
|
|
2
|
+
// (BillingAutomationController, /api/BillingAutomation). Staff only.
|
|
3
|
+
import { request } from '../api.js';
|
|
4
|
+
|
|
5
|
+
const MODES = ['auto', 'review', 'dry_run'];
|
|
6
|
+
const GATES = ['send', 'charge', 'both'];
|
|
7
|
+
|
|
8
|
+
function money(cents) {
|
|
9
|
+
if (cents == null) return '';
|
|
10
|
+
return `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fail(message) {
|
|
14
|
+
console.error(message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function listRules() {
|
|
19
|
+
const rules = await request('GET', '/api/BillingAutomation/rules');
|
|
20
|
+
if (!rules.length) {
|
|
21
|
+
console.log('No rules. With no rules, every invoice runs automatically.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const r of rules) {
|
|
25
|
+
const scope = [
|
|
26
|
+
r.accountType ? `account=${r.accountType}` : null,
|
|
27
|
+
r.paymentMethod ? `method=${r.paymentMethod}` : null,
|
|
28
|
+
r.minAmountCents != null ? `from=${money(r.minAmountCents)}` : null,
|
|
29
|
+
r.maxAmountCents != null ? `to=${money(r.maxAmountCents)}` : null,
|
|
30
|
+
].filter(Boolean).join(' ') || 'global default';
|
|
31
|
+
console.log(`#${r.id} ${r.enabled ? 'on ' : 'off'} ${r.appliesTo.padEnd(6)} ${r.mode.padEnd(8)} ${scope} ${r.name}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function setGlobal(appliesTo, mode) {
|
|
36
|
+
if (!GATES.includes(appliesTo)) fail(`appliesTo must be one of: ${GATES.join(', ')}`);
|
|
37
|
+
if (!MODES.includes(mode)) fail(`mode must be one of: ${MODES.join(', ')}`);
|
|
38
|
+
await request('PUT', `/api/BillingAutomation/global/${appliesTo}`, { mode });
|
|
39
|
+
console.log(`Global ${appliesTo} set to ${mode}.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function addRule(args) {
|
|
43
|
+
if (!args.name) fail('Usage: velaro billing-automation rule-add --name "Review big ACH" --applies-to charge --mode review [--account-type enterprise] [--min-cents 100000] [--max-cents 500000] [--payment-method ach]');
|
|
44
|
+
const body = {
|
|
45
|
+
name: args.name,
|
|
46
|
+
appliesTo: args.appliesTo ?? 'both',
|
|
47
|
+
mode: args.mode,
|
|
48
|
+
accountType: args.accountType ?? null,
|
|
49
|
+
minAmountCents: args.minCents ?? null,
|
|
50
|
+
maxAmountCents: args.maxCents ?? null,
|
|
51
|
+
paymentMethod: args.paymentMethod ?? null,
|
|
52
|
+
enabled: true,
|
|
53
|
+
};
|
|
54
|
+
const r = await request('POST', '/api/BillingAutomation/rules', body);
|
|
55
|
+
console.log(`Rule #${r.id} added: ${r.name} (${r.appliesTo} -> ${r.mode}).`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function setRuleEnabled(id, enabled) {
|
|
59
|
+
const rules = await request('GET', '/api/BillingAutomation/rules');
|
|
60
|
+
const rule = rules.find((r) => r.id === Number(id));
|
|
61
|
+
if (!rule) fail(`No rule #${id}.`);
|
|
62
|
+
await request('PUT', `/api/BillingAutomation/rules/${rule.id}`, {
|
|
63
|
+
name: rule.name,
|
|
64
|
+
appliesTo: rule.appliesTo,
|
|
65
|
+
mode: rule.mode,
|
|
66
|
+
accountType: rule.accountType,
|
|
67
|
+
minAmountCents: rule.minAmountCents,
|
|
68
|
+
maxAmountCents: rule.maxAmountCents,
|
|
69
|
+
paymentMethod: rule.paymentMethod,
|
|
70
|
+
enabled,
|
|
71
|
+
});
|
|
72
|
+
console.log(`Rule #${rule.id} ${enabled ? 'turned on' : 'turned off'}.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function deleteRule(id) {
|
|
76
|
+
await request('DELETE', `/api/BillingAutomation/rules/${id}`);
|
|
77
|
+
console.log(`Rule #${id} deleted.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printDecisions(rows, emptyText) {
|
|
81
|
+
if (!rows.length) {
|
|
82
|
+
console.log(emptyText);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const d of rows) {
|
|
86
|
+
const label = d.outcome === 'dry_run' ? 'WOULD' : 'HELD ';
|
|
87
|
+
const state = d.resolution ? ` [${d.resolution}]` : '';
|
|
88
|
+
console.log(`#${d.id} ${label} ${d.action.padEnd(6)} invoice ${d.invoiceId} ${money(d.amountCents)} ${d.paymentMethod ?? '-'} ${d.createdAt}${state}`);
|
|
89
|
+
console.log(` ${d.reason}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function queue() {
|
|
94
|
+
printDecisions(await request('GET', '/api/BillingAutomation/decisions?status=open&take=200'), 'Nothing is waiting for review.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function dryRuns() {
|
|
98
|
+
printDecisions(await request('GET', '/api/BillingAutomation/decisions?status=dry_run&take=100'), 'No dry-run entries.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function approve(id) {
|
|
102
|
+
const res = await request('POST', `/api/BillingAutomation/decisions/${id}/approve`, {});
|
|
103
|
+
console.log(`Approved #${id}. Released: ${res.released}.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function dismiss(id) {
|
|
107
|
+
await request('POST', `/api/BillingAutomation/decisions/${id}/dismiss`, {});
|
|
108
|
+
console.log(`Dismissed #${id}. It stays on hold until approved.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function setAccountType(subscriptionId, value) {
|
|
112
|
+
const res = await request('PUT', `/api/BillingAutomation/subscription/${subscriptionId}/account-type`, { accountType: value ?? null });
|
|
113
|
+
console.log(`Subscription ${res.id} account type: ${res.accountType ?? '(unset)'}.`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function notifySettings() {
|
|
117
|
+
const all = await request('GET', '/api/BillingNotifications/settings');
|
|
118
|
+
for (const [key, value] of Object.entries(all)) {
|
|
119
|
+
console.log(`${key.padEnd(20)} ${value ?? '(not set: code default)'}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function notifySet(key, value) {
|
|
124
|
+
await request('PUT', `/api/BillingNotifications/settings/${key}`, { value });
|
|
125
|
+
console.log(`${key} set to ${value}.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Only the channels you pass change. The others keep their current override (the API replaces all three).
|
|
129
|
+
async function setReceipts(subscriptionId, args) {
|
|
130
|
+
const sub = await request('GET', `/api/BillingSubscription/${subscriptionId}`);
|
|
131
|
+
const res = await request('PUT', `/api/BillingNotifications/subscriptions/${subscriptionId}/receipts`, {
|
|
132
|
+
card: args.card ?? sub.receiptCardOverride ?? null,
|
|
133
|
+
ach: args.ach ?? sub.receiptAchOverride ?? null,
|
|
134
|
+
other: args.other ?? sub.receiptOtherOverride ?? null,
|
|
135
|
+
});
|
|
136
|
+
console.log(`Subscription ${res.id} receipts: card=${res.card ?? 'inherit'} ach=${res.ach ?? 'inherit'} other=${res.other ?? 'inherit'}.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function digestSendNow() {
|
|
140
|
+
const res = await request('POST', '/api/BillingNotifications/digest/send-now', {});
|
|
141
|
+
console.log(`Digest sent to ${res.recipients.join(', ')}: ${res.subject}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function receiptsRunNow() {
|
|
145
|
+
const res = await request('POST', '/api/BillingNotifications/receipts/run-now', {});
|
|
146
|
+
console.log(`Receipts run: ${res.summary}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const billingAutomationCommand = {
|
|
150
|
+
command: 'billing-automation <subcommand>',
|
|
151
|
+
describe: 'Review-hold and dry-run switches for invoice emails and charges, the review queue, and account types',
|
|
152
|
+
builder: (yargs) =>
|
|
153
|
+
yargs
|
|
154
|
+
.command('rules', 'List every automation rule and global switch', {}, () => listRules())
|
|
155
|
+
.command('global <appliesTo> <mode>', 'Set the global default: appliesTo = send | charge | both; mode = auto | review | dry_run', {}, (args) => setGlobal(args.appliesTo, args.mode))
|
|
156
|
+
.command('rule-add', 'Add a scoped rule (--name required; filters are optional)', {
|
|
157
|
+
name: { type: 'string' },
|
|
158
|
+
appliesTo: { type: 'string', choices: GATES, default: 'both' },
|
|
159
|
+
mode: { type: 'string', choices: MODES, demandOption: true },
|
|
160
|
+
accountType: { type: 'string' },
|
|
161
|
+
minCents: { type: 'number' },
|
|
162
|
+
maxCents: { type: 'number' },
|
|
163
|
+
paymentMethod: { type: 'string', choices: ['cash', 'check', 'ach', 'wire', 'card'] },
|
|
164
|
+
}, (args) => addRule(args))
|
|
165
|
+
.command('rule-on <id>', 'Turn a rule on', {}, (args) => setRuleEnabled(args.id, true))
|
|
166
|
+
.command('rule-off <id>', 'Turn a rule off', {}, (args) => setRuleEnabled(args.id, false))
|
|
167
|
+
.command('rule-delete <id>', 'Delete a rule (queue entries keep their rule name)', {}, (args) => deleteRule(args.id))
|
|
168
|
+
.command('queue', 'Show held sends and charges waiting for review', {}, () => queue())
|
|
169
|
+
.command('dry-runs', 'Show what the dry-run switches would have sent or charged', {}, () => dryRuns())
|
|
170
|
+
.command('approve <id>', 'Approve a held entry and release its send or charge now', {}, (args) => approve(args.id))
|
|
171
|
+
.command('dismiss <id>', 'Dismiss a held entry (it stays on hold, alerts stop)', {}, (args) => dismiss(args.id))
|
|
172
|
+
.command('account-type <subscriptionId> [value]', 'Set a subscription account type that rules match on (omit value to clear)', {}, (args) => setAccountType(args.subscriptionId, args.value))
|
|
173
|
+
.command('notify-settings', 'Show the global notification settings (receipts, invoice BCC, digest recipients)', {}, () => notifySettings())
|
|
174
|
+
.command('digest-send-now', 'Send the billing digest now to digest.recipients (same email as the 7am run)', {}, () => digestSendNow())
|
|
175
|
+
.command('receipts-run-now', 'Decide pending payment receipts now instead of at :05 and print the counts', {}, () => receiptsRunNow())
|
|
176
|
+
.command('notify-set <key> <value>', 'Set a global notification setting: receipts.card|receipts.ach|receipts.other = never|send; invoices.bcc = emails (blank = none); digest.recipients = emails', {}, (args) => notifySet(args.key, args.value))
|
|
177
|
+
.command('receipts <subscriptionId>', 'Set receipt overrides on one subscription (--card/--ach/--other = never|send). Channels you omit keep their current override.', {
|
|
178
|
+
card: { type: 'string', choices: ['never', 'send'] },
|
|
179
|
+
ach: { type: 'string', choices: ['never', 'send'] },
|
|
180
|
+
other: { type: 'string', choices: ['never', 'send'] },
|
|
181
|
+
}, (args) => setReceipts(args.subscriptionId, args))
|
|
182
|
+
.demandCommand(1, 'Specify a subcommand: rules | global | rule-add | rule-on | rule-off | rule-delete | queue | dry-runs | approve | dismiss | account-type'),
|
|
183
|
+
handler: () => {},
|
|
184
|
+
};
|
|
@@ -110,5 +110,18 @@ export const copilotStudioCommand = {
|
|
|
110
110
|
}
|
|
111
111
|
}),
|
|
112
112
|
})
|
|
113
|
-
.
|
|
113
|
+
.command({
|
|
114
|
+
command: 'ask <id> <question>',
|
|
115
|
+
describe: 'Live diagnostic: ask the agent a real question and classify the reply (answered | vendor_limit | no_answer). Exit 1 unless answered.',
|
|
116
|
+
builder: y => y.positional('id', { type: 'number' }).positional('question', { type: 'string' }),
|
|
117
|
+
handler: runCommand(async argv => {
|
|
118
|
+
const r = await request('POST', '/CopilotStudio/message', { configId: argv.id, message: argv.question });
|
|
119
|
+
console.log(`
|
|
120
|
+
kind: ${r?.kind}
|
|
121
|
+
reply: ${r?.reply}
|
|
122
|
+
`);
|
|
123
|
+
if (r?.kind !== 'answered') process.exitCode = 1;
|
|
124
|
+
}),
|
|
125
|
+
})
|
|
126
|
+
.demandCommand(1, 'Specify a subcommand: list | add | update | remove | test | ask'),
|
|
114
127
|
};
|
|
@@ -19,6 +19,7 @@ const REPORTS = {
|
|
|
19
19
|
'bot-containment': 'Claimed vs true bot containment and handoff regret',
|
|
20
20
|
'effort-index': 'Inferred 0-100 customer effort by topic, program and team (not a survey CES)',
|
|
21
21
|
'promise-ledger': 'Agent commitments made in chat and whether a follow-up appeared; overdue list',
|
|
22
|
+
'new-agent-ramp': 'Agents whose first chat is in --from/--to: week-by-week ramp (chats, first response, QA, rating) against tenured agents',
|
|
22
23
|
'promise-dismiss': 'Dismiss one promise by --id (managers only)',
|
|
23
24
|
'weekly-brief-send-test': 'Email this week\'s brief to the signed-in user',
|
|
24
25
|
'year-in-review': 'Build the 12-month Year in review document: --from/--to (default last 12 whole Pacific months) starts a build and prints its id; --id <id> checks status and the download link',
|
|
@@ -39,6 +39,31 @@ ${pkg.displayName} (${pkg.packageKey}) - plan ${pkg.plan}, default OFF${pkg.site
|
|
|
39
39
|
console.log('');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Formats the GET Entitlements/report-packages payload (five category packages nested under Enterprise Reports). Exported for the unit test.
|
|
43
|
+
export function formatReportPackages(pkg) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const site = pkg.siteId ? `, site ${pkg.siteId}` : '';
|
|
46
|
+
lines.push(`${pkg.top.name} (${pkg.top.slug}) - plan ${pkg.plan}, default OFF${site}${pkg.siteId ? (pkg.top.enabled ? ' [ALL ON]' : '') : ''}`);
|
|
47
|
+
lines.push(` ${pkg.top.tagline}`);
|
|
48
|
+
for (const c of pkg.categories) {
|
|
49
|
+
const state = pkg.siteId ? ` [${c.enabled ? 'ON ' : 'off'}] ${c.enabledCount}/${c.reportCount} reports` : ` ${c.reportCount} reports`;
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push(`${c.name} (${c.slug})${state}`);
|
|
52
|
+
lines.push(` ${c.tagline}`);
|
|
53
|
+
for (const r of c.reports) {
|
|
54
|
+
const mark = pkg.siteId ? (r.enabled ? '[ON ] ' : '[off] ') : '';
|
|
55
|
+
lines.push(` ${mark}${r.name} (${r.id}) - package ${r.packageSlug}, key ${r.packageKey}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function reportPackages(args) {
|
|
62
|
+
const q = args.site ? `?siteId=${encodeURIComponent(args.site)}` : '';
|
|
63
|
+
const pkg = await request('GET', `Entitlements/report-packages${q}`);
|
|
64
|
+
console.log(`\n${formatReportPackages(pkg)}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
async function listFeatures() {
|
|
43
68
|
const features = await request('GET', 'Entitlements/features');
|
|
44
69
|
|
|
@@ -141,6 +166,45 @@ async function reconcile(args) {
|
|
|
141
166
|
}
|
|
142
167
|
}
|
|
143
168
|
|
|
169
|
+
async function trial(args) {
|
|
170
|
+
const sub = args.action;
|
|
171
|
+
const need = (v, n) => { if (v === undefined || v === null || v === '') { console.error(`--${n} is required for trial ${sub}`); process.exit(1); } };
|
|
172
|
+
if (sub === 'get' || sub === 'recommend') {
|
|
173
|
+
need(args.site, 'site');
|
|
174
|
+
const r = await request('GET', `Entitlements/${args.site}/trial${sub === 'recommend' ? '/recommendation' : ''}`);
|
|
175
|
+
console.log(JSON.stringify(r, null, 2));
|
|
176
|
+
} else if (sub === 'not-paying') {
|
|
177
|
+
console.log(JSON.stringify(await request('GET', 'Entitlements/admin/trial/not-paying'), null, 2));
|
|
178
|
+
} else if (sub === 'grant') {
|
|
179
|
+
need(args.site, 'site'); need(args.feature, 'feature'); need(args.reason, 'reason');
|
|
180
|
+
const r = await request('POST', `Entitlements/${args.site}/trial/grant`, { featureKey: args.feature, reason: args.reason });
|
|
181
|
+
console.log(`Site ${args.site}: ${r.featureKey} granted for the trial (${r.state}).`);
|
|
182
|
+
} else if (sub === 'hold' || sub === 'reactivate') {
|
|
183
|
+
need(args.site, 'site');
|
|
184
|
+
const body = { featureKey: args.feature, dryRun: !!args['dry-run'] };
|
|
185
|
+
console.log(JSON.stringify(await request('POST', `Entitlements/${args.site}/trial/${sub}`, body)));
|
|
186
|
+
} else if (sub === 'set-limit') {
|
|
187
|
+
need(args.site, 'site'); need(args.key, 'key'); need(args.value, 'value'); need(args.reason, 'reason');
|
|
188
|
+
const r = await request('POST', `Entitlements/${args.site}/trial/limit`, { key: args.key, value: Number(args.value), reason: args.reason });
|
|
189
|
+
console.log(`Site ${args.site}: ${r.key} ${r.old} -> ${r.value}.`);
|
|
190
|
+
} else if (sub === 'set-text') {
|
|
191
|
+
need(args.feature, 'feature');
|
|
192
|
+
console.log(JSON.stringify(await request('POST', 'Entitlements/admin/trial/feature-text', { featureKey: args.feature, text: args.text })));
|
|
193
|
+
} else if (sub === 'approve-limit' || sub === 'deny-limit') {
|
|
194
|
+
need(args.request, 'request');
|
|
195
|
+
if (sub === 'approve-limit') {
|
|
196
|
+
need(args.key, 'key'); need(args.value, 'value');
|
|
197
|
+
console.log(JSON.stringify(await request('POST', `Entitlements/admin/trial/limit-request/${args.request}/approve`, { key: args.key, value: Number(args.value), message: args.message })));
|
|
198
|
+
} else {
|
|
199
|
+
need(args.reason, 'reason');
|
|
200
|
+
console.log(JSON.stringify(await request('POST', `Entitlements/admin/trial/limit-request/${args.request}/deny`, { reason: args.reason })));
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
console.error('Unknown trial action. Use: get | recommend | not-paying | grant | hold | reactivate | set-limit | approve-limit | deny-limit | set-text');
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
144
208
|
async function setLock(args) {
|
|
145
209
|
if (!args.site) {
|
|
146
210
|
console.error('Usage: velaro entitlement set-lock --site 1032 --at 2026-01-01T00:00:00Z\n velaro entitlement set-lock --site 1032 --unlock');
|
|
@@ -193,6 +257,9 @@ export function entitlementCommand(yargs) {
|
|
|
193
257
|
.command('enterprise-reports', 'Show the Enterprise Reports package items (with --site, which are ON for that site)', y => y
|
|
194
258
|
.option('site', { type: 'number', desc: 'Site ID' })
|
|
195
259
|
, a => enterpriseReportsPackage(a))
|
|
260
|
+
.command('report-packages', 'Show the five report category packages and every report in them (with --site, which are ON for that site)', y => y
|
|
261
|
+
.option('site', { type: 'number', desc: 'Site ID' })
|
|
262
|
+
, a => reportPackages(a))
|
|
196
263
|
.command('set-override', 'Enable or disable a feature for a specific site (overrides plan)', y => y
|
|
197
264
|
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
198
265
|
.option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
|
|
@@ -222,10 +289,21 @@ export function entitlementCommand(yargs) {
|
|
|
222
289
|
.option('at', { type: 'string', desc: 'ISO 8601 timestamp to lock entitlement resolution to' })
|
|
223
290
|
.option('unlock', { type: 'boolean', desc: 'Clear an existing lock (resolves against current plan version again)' })
|
|
224
291
|
, a => setLock(a))
|
|
292
|
+
.command('trial <action>', 'Trial governance (superadmin): get | recommend | not-paying | grant | hold | reactivate | set-limit | approve-limit | deny-limit | set-text', y => y
|
|
293
|
+
.option('site', { type: 'number', desc: 'Site ID' })
|
|
294
|
+
.option('feature', { type: 'string', desc: 'Feature key, e.g. EnableIvr' })
|
|
295
|
+
.option('key', { type: 'string', desc: 'Cap name, e.g. MaxKbArticles' })
|
|
296
|
+
.option('value', { type: 'string', desc: 'Whole number cap' })
|
|
297
|
+
.option('reason', { type: 'string', desc: 'Reason (audited)' })
|
|
298
|
+
.option('request', { type: 'number', desc: 'Limit-increase request id' })
|
|
299
|
+
.option('message', { type: 'string', desc: 'approve-limit: note added to the customer reply' })
|
|
300
|
+
.option('text', { type: 'string', desc: 'set-text: customer-safe, number-free limit text' })
|
|
301
|
+
.option('dry-run', { type: 'boolean', desc: 'hold: report only, write nothing' })
|
|
302
|
+
, a => trial(a))
|
|
225
303
|
.command('list-cutover', 'List all feature keys and whether they are live (messaging reads admin) or not (Subscription.cs)', () => {}, () => listCutover())
|
|
226
304
|
.command('set-cutover', 'Flip a feature key live (or roll it back). Live = messaging reads admin value within 45s. --rollback reverts to Subscription.cs.', y => y
|
|
227
305
|
.option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
|
|
228
306
|
.option('rollback', { type: 'boolean', desc: 'Pass to roll back this flag to Subscription.cs (default: go live)' })
|
|
229
307
|
, a => setCutover(a))
|
|
230
|
-
.demandCommand(1, 'Specify a subcommand: get | list-features | enterprise-reports | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | list-cutover | set-cutover');
|
|
308
|
+
.demandCommand(1, 'Specify a subcommand: get | list-features | enterprise-reports | report-packages | set-override | remove-override | seed | seed-plans | backfill | backfill-sap-tiers | backfill-legacy-write-tiers | reconcile | comp | set-lock | trial | list-cutover | set-cutover');
|
|
231
309
|
}
|
package/lib/commands/env.js
CHANGED
|
@@ -32,7 +32,7 @@ export const envCommand = {
|
|
|
32
32
|
? `logged in as ${creds.userName ?? 'unknown'} (site ${creds.siteId})`
|
|
33
33
|
: 'not logged in';
|
|
34
34
|
const expiry = creds?.velaroExpires
|
|
35
|
-
? new Date(creds.velaroExpires) > new Date() ? '' : ' ⚠ token expired'
|
|
35
|
+
? new Date(creds.velaroExpires) > new Date() ? '' : creds.entraRefreshToken ? ' (session token idle; auto-refreshes on next command)' : ' ⚠ token expired, run velaro login'
|
|
36
36
|
: '';
|
|
37
37
|
console.log(`${marker}${name.padEnd(10)} ${status}${expiry}`);
|
|
38
38
|
console.log(` ${urls.adminApiBase}`);
|
package/lib/commands/focus.js
CHANGED
|
@@ -225,7 +225,7 @@ const setTriageRuleCommand = {
|
|
|
225
225
|
builder: (y) => y
|
|
226
226
|
.positional('label', { type: 'string', describe: `Label: ${VALID_LABELS.join(', ')}` })
|
|
227
227
|
.positional('enabled', { type: 'string', describe: 'true or false', choices: ['true', 'false'] })
|
|
228
|
-
.option('triage-mode', { type: 'string', choices: ['off', 'economy'], describe: 'off (
|
|
228
|
+
.option('triage-mode', { type: 'string', choices: ['off', 'economy'], describe: 'off (this site always uses the AI call) or economy (skip the AI call for obvious messages). Unset = platform default (economy)' }),
|
|
229
229
|
handler: async (argv) => {
|
|
230
230
|
const { label, enabled } = argv;
|
|
231
231
|
const mode = argv['triage-mode'];
|
package/lib/commands/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, post, getCredentials, messagingGet, messagingPost } from '../api.js';
|
|
1
|
+
import { get, post, getCredentials, messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
2
|
|
|
3
3
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -266,6 +266,41 @@ Index ready on site ${argv.site}: ${idx.name} (id: ${idx.id}, purpose: ${idx.pur
|
|
|
266
266
|
},
|
|
267
267
|
};
|
|
268
268
|
|
|
269
|
+
// -- profile (Velaro staff only) --------------------------------------------------
|
|
270
|
+
// PUT KnowledgeSetup/Staff/{siteId}/index-profile on velaro-messaging. Apply-if-present: only the
|
|
271
|
+
// flags you pass are changed. The index is addressed by name (the endpoint looks up siteId+name).
|
|
272
|
+
|
|
273
|
+
const profileCommand = {
|
|
274
|
+
command: 'profile',
|
|
275
|
+
describe: 'Set the ingestion profile of one index on any site (Velaro staff only; only passed flags change)',
|
|
276
|
+
builder: (y) =>
|
|
277
|
+
y
|
|
278
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Target site ID' })
|
|
279
|
+
.option('index', { type: 'string', demandOption: true, describe: 'Index name, e.g. 100140-troubleshooting' })
|
|
280
|
+
.option('strategy', { type: 'string', choices: ['procedure', 'section'], describe: 'Chunk strategy' })
|
|
281
|
+
.option('chunk-size', { type: 'number', describe: 'Chunk size' })
|
|
282
|
+
.option('chunk-overlap', { type: 'number', describe: 'Chunk overlap' })
|
|
283
|
+
.option('purpose', { type: 'string', choices: ['general', 'troubleshooting', 'catalog'], describe: 'Index purpose' })
|
|
284
|
+
.option('search-size', { type: 'number', describe: 'Results per search' })
|
|
285
|
+
.option('exclude-from-all-sources', { type: 'boolean', describe: 'Keep this index out of the ["*"] all-sources search' })
|
|
286
|
+
.option('json', { type: 'boolean', describe: 'Print raw JSON' }),
|
|
287
|
+
handler: async (argv) => {
|
|
288
|
+
const body = { indexName: argv.index };
|
|
289
|
+
if (argv.strategy !== undefined) body.chunkStrategy = argv.strategy;
|
|
290
|
+
if (argv['chunk-size'] !== undefined) body.chunkSize = argv['chunk-size'];
|
|
291
|
+
if (argv['chunk-overlap'] !== undefined) body.chunkOverlap = argv['chunk-overlap'];
|
|
292
|
+
if (argv.purpose !== undefined) body.purpose = argv.purpose;
|
|
293
|
+
if (argv['search-size'] !== undefined) body.searchSize = argv['search-size'];
|
|
294
|
+
if (argv['exclude-from-all-sources'] !== undefined) body.excludeFromAllSources = argv['exclude-from-all-sources'];
|
|
295
|
+
const idx = await messagingPut(`/KnowledgeSetup/Staff/${encodeURIComponent(argv.site)}/index-profile`, body);
|
|
296
|
+
if (argv.json) { console.log(JSON.stringify(idx, null, 2)); return; }
|
|
297
|
+
console.log(`
|
|
298
|
+
Profile updated on site ${argv.site}: ${idx.name} (purpose: ${idx.purpose}, strategy: ${idx.chunkStrategy}, size: ${idx.chunkSize}, overlap: ${idx.chunkOverlap}, searchSize: ${idx.searchSize}, excludeFromAllSources: ${!!idx.excludeFromAllSources})
|
|
299
|
+
Re-ingest to apply the new chunking to existing content.
|
|
300
|
+
`);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
269
304
|
export const indexCommand = {
|
|
270
305
|
command: 'index <subcommand>',
|
|
271
306
|
describe: 'Manage search indexes — view quota, physical index health, and reingest content',
|
|
@@ -277,6 +312,7 @@ export const indexCommand = {
|
|
|
277
312
|
.command(reingestCommand)
|
|
278
313
|
.command(testCommand)
|
|
279
314
|
.command(createCommand)
|
|
280
|
-
.
|
|
315
|
+
.command(profileCommand)
|
|
316
|
+
.demandCommand(1, 'Specify a subcommand: list, site, provision, reingest, test, create, profile'),
|
|
281
317
|
handler: () => {},
|
|
282
318
|
};
|