@velaro/cli 1.4.106 → 1.4.108
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/bin/velaro.js +53 -6
- package/lib/commands/agent.js +11 -1
- package/lib/commands/avatar.js +3 -0
- package/lib/commands/billing-agents.js +51 -0
- package/lib/commands/billing-automation.js +1 -1
- package/lib/commands/diag-groups.js +108 -0
- package/lib/commands/endpoint-parity.js +58 -0
- package/lib/commands/entitlement.js +27 -1
- package/lib/commands/feature-discovery.js +3 -0
- package/lib/commands/magento.js +52 -0
- package/lib/commands/ops-groups.js +188 -0
- package/lib/commands/order-form.js +6 -1
- package/lib/commands/package-builder.js +35 -0
- package/lib/commands/parity-staff-tools.js +192 -0
- package/lib/commands/price-increase.js +93 -0
- package/lib/commands/safety.js +78 -2
- package/lib/commands/teams-phone.js +63 -0
- package/lib/log-search.js +43 -0
- package/lib/log-search.test.js +1 -1
- package/lib/parity-util.js +27 -0
- package/lib/sign-versions.test.js +1 -1
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -97,6 +97,7 @@ import { billingContactsCommand } from '../lib/commands/billing-contacts.js';
|
|
|
97
97
|
import { billingSubscriptionCommand } from '../lib/commands/billing-subscription.js';
|
|
98
98
|
import { billingAutomationCommand } from '../lib/commands/billing-automation.js';
|
|
99
99
|
import { orderFormCommand } from '../lib/commands/order-form.js';
|
|
100
|
+
import { priceIncreaseCommand } from '../lib/commands/price-increase.js';
|
|
100
101
|
import { billingEmailPreferenceCommand } from '../lib/commands/billing-email-preference.js';
|
|
101
102
|
import { billingCommand } from '../lib/commands/billing.js';
|
|
102
103
|
import { couponGrantCommand } from '../lib/commands/coupon-grant.js';
|
|
@@ -104,6 +105,8 @@ import { signCommand } from '../lib/commands/sign.js';
|
|
|
104
105
|
import { widgetContainerCommand } from '../lib/commands/widget-container.js';
|
|
105
106
|
import { inlineWidgetConfigCommand } from '../lib/commands/inline-widget-config.js';
|
|
106
107
|
import { pricingCommand } from '../lib/commands/pricing.js';
|
|
108
|
+
import { billingAgentsCommand } from '../lib/commands/billing-agents.js';
|
|
109
|
+
import { packageBuilderCommand } from '../lib/commands/package-builder.js';
|
|
107
110
|
import { entitlementCommand } from '../lib/commands/entitlement.js';
|
|
108
111
|
import { subscriptionCommand } from '../lib/commands/subscription.js';
|
|
109
112
|
import { supportCommand } from '../lib/commands/support.js';
|
|
@@ -130,12 +133,18 @@ import { bundleCommand } from '../lib/commands/bundle.js';
|
|
|
130
133
|
import { chatWindowCommand } from '../lib/commands/chat-window.js';
|
|
131
134
|
import { featureDiscoveryCommand } from '../lib/commands/feature-discovery.js';
|
|
132
135
|
import { tourPolicyCommand } from '../lib/commands/tour-policy.js';
|
|
136
|
+
import { livefluenceCommand, missedCommand, serviceCommand, pageCommand, uploadedCommand, msCommand, outlookCommand, oauthCommand, provisionCommand, sseCommand, skillCommand, v10Command, whatsappCommand, mobileCommand, signalrCommand, supportGroupCommand, pricingGroupCommand, scraperGroupCommand, tourPolicyGroupCommand } from '../lib/commands/parity-staff-tools.js';
|
|
133
137
|
import { scraperCommand } from '../lib/commands/scraper.js';
|
|
134
138
|
import { usageCommand } from '../lib/commands/usage.js';
|
|
135
139
|
import { campaignsCommand } from '../lib/commands/campaigns.js';
|
|
136
140
|
import { senderDomainHealthCommand } from '../lib/commands/sender-domain-health.js';
|
|
137
141
|
import { contactsCommand } from '../lib/commands/contacts.js';
|
|
138
142
|
import { contactListCommand } from '../lib/commands/contact-list.js';
|
|
143
|
+
import { recycleBinCommand, aiUsageCommand, siteInfoCommand, billingReceiptsCommand } from '../lib/commands/endpoint-parity.js';
|
|
144
|
+
import { adminCommand, appleCommand, forceExpireSiteCommand, backfillCommand, kpiCommand, auditTrailCommand, codeCommand, crmCommand, hangfireCommand } from '../lib/commands/diag-groups.js';
|
|
145
|
+
import { aiFollowupsCommand, businessActionCommand, callCommand, conversationCommand, conversionCommand, emailCommand, ivrCommand } from '../lib/commands/ops-groups.js';
|
|
146
|
+
import { entitlementGroup } from '../lib/commands/entitlement.js';
|
|
147
|
+
import { featureDiscoveryGroup } from '../lib/commands/feature-discovery.js';
|
|
139
148
|
import { startUpdateCheck } from '../lib/update-check.js';
|
|
140
149
|
import { printBanner, printBannerIfFirstRun } from '../lib/banner.js';
|
|
141
150
|
import { createRequire } from 'module';
|
|
@@ -250,16 +259,19 @@ await yargs(hideBin(process.argv))
|
|
|
250
259
|
.command(billingSubscriptionCommand)
|
|
251
260
|
.command(billingAutomationCommand)
|
|
252
261
|
.command(orderFormCommand)
|
|
262
|
+
.command(priceIncreaseCommand)
|
|
253
263
|
.command(billingEmailPreferenceCommand)
|
|
254
264
|
.command(billingCommand)
|
|
255
265
|
.command(couponGrantCommand)
|
|
256
266
|
.command(signCommand)
|
|
257
267
|
.command(widgetContainerCommand)
|
|
258
268
|
.command(inlineWidgetConfigCommand)
|
|
259
|
-
.command(
|
|
260
|
-
.command(
|
|
269
|
+
.command(pricingGroupCommand)
|
|
270
|
+
.command(entitlementGroup)
|
|
271
|
+
.command({ command: 'billing-agents', desc: 'Run and review the billing agents (staff)', builder: billingAgentsCommand, handler: () => {} })
|
|
272
|
+
.command({ command: 'package-builder', desc: 'Package graph, drafts, simulation, audit (staff)', builder: packageBuilderCommand, handler: () => {} })
|
|
261
273
|
.command({ command: 'subscription', desc: 'Apply/diff SubscriptionPackage packages on a site (superadmin only)', builder: subscriptionCommand, handler: () => {} })
|
|
262
|
-
.command(
|
|
274
|
+
.command(supportGroupCommand)
|
|
263
275
|
.command(routingCommand)
|
|
264
276
|
.command(diagnosticsCommand)
|
|
265
277
|
.command(aiSpendCommand)
|
|
@@ -281,14 +293,49 @@ await yargs(hideBin(process.argv))
|
|
|
281
293
|
.command(surveyCommand)
|
|
282
294
|
.command(bundleCommand)
|
|
283
295
|
.command(chatWindowCommand)
|
|
284
|
-
.command(
|
|
296
|
+
.command(featureDiscoveryGroup)
|
|
297
|
+
.command(scraperGroupCommand)
|
|
298
|
+
.command(tourPolicyGroupCommand)
|
|
299
|
+
.command(livefluenceCommand)
|
|
300
|
+
.command(missedCommand)
|
|
301
|
+
.command(serviceCommand)
|
|
302
|
+
.command(pageCommand)
|
|
303
|
+
.command(uploadedCommand)
|
|
304
|
+
.command(msCommand)
|
|
305
|
+
.command(outlookCommand)
|
|
306
|
+
.command(oauthCommand)
|
|
307
|
+
.command(provisionCommand)
|
|
308
|
+
.command(sseCommand)
|
|
309
|
+
.command(skillCommand)
|
|
310
|
+
.command(v10Command)
|
|
311
|
+
.command(whatsappCommand)
|
|
312
|
+
.command(mobileCommand)
|
|
313
|
+
.command(signalrCommand)
|
|
285
314
|
.command(usageCommand)
|
|
286
|
-
.command({ command: 'scraper', desc: 'Velaro staff: scraper / upload / chunk usage for any site', builder: scraperCommand, handler: () => {} })
|
|
287
|
-
.command({ command: 'tour-policy', desc: 'Velaro staff: guided-tour auto-open policy (versioned, rollback-able)', builder: tourPolicyCommand, handler: () => {} })
|
|
288
315
|
.command(campaignsCommand)
|
|
289
316
|
.command(senderDomainHealthCommand)
|
|
290
317
|
.command(contactsCommand)
|
|
291
318
|
.command(contactListCommand)
|
|
319
|
+
.command(recycleBinCommand)
|
|
320
|
+
.command(aiUsageCommand)
|
|
321
|
+
.command(siteInfoCommand)
|
|
322
|
+
.command(billingReceiptsCommand)
|
|
323
|
+
.command(adminCommand)
|
|
324
|
+
.command(appleCommand)
|
|
325
|
+
.command(forceExpireSiteCommand)
|
|
326
|
+
.command(backfillCommand)
|
|
327
|
+
.command(kpiCommand)
|
|
328
|
+
.command(auditTrailCommand)
|
|
329
|
+
.command(codeCommand)
|
|
330
|
+
.command(crmCommand)
|
|
331
|
+
.command(hangfireCommand)
|
|
332
|
+
.command(aiFollowupsCommand)
|
|
333
|
+
.command(businessActionCommand)
|
|
334
|
+
.command(callCommand)
|
|
335
|
+
.command(conversationCommand)
|
|
336
|
+
.command(conversionCommand)
|
|
337
|
+
.command(emailCommand)
|
|
338
|
+
.command(ivrCommand)
|
|
292
339
|
.demandCommand(1, 'Specify a command. Run "velaro --help" for options.')
|
|
293
340
|
.strict()
|
|
294
341
|
.help()
|
package/lib/commands/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { get, post, messagingGet, messagingPost, messagingPut, getCredentials } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
|
-
const VALID_STATUSES = ['Available', 'Away', 'Offline'];
|
|
4
|
+
const VALID_STATUSES = ['Available', 'Busy', 'Away', 'Offline'];
|
|
5
5
|
|
|
6
6
|
const MISSED_REASON_LABELS = {
|
|
7
7
|
NoAgentsOnline: 'No agents online',
|
|
@@ -120,6 +120,16 @@ export const agentCommand = {
|
|
|
120
120
|
console.log(`Agent ${argv.id} set to ${argv.status}.`);
|
|
121
121
|
}),
|
|
122
122
|
})
|
|
123
|
+
.command({
|
|
124
|
+
command: 'force-sign-out',
|
|
125
|
+
describe: 'Force an agent to sign in again: revokes all their sessions now and sets them Offline (administrator only, same site, audited)',
|
|
126
|
+
builder: (y) => y
|
|
127
|
+
.option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' }),
|
|
128
|
+
handler: runCommand(async (argv) => {
|
|
129
|
+
await messagingPost(`/Users/${argv.id}/ForceSignOut`, {});
|
|
130
|
+
console.log(`Agent ${argv.id} was signed out of every session and set to Offline.`);
|
|
131
|
+
}),
|
|
132
|
+
})
|
|
123
133
|
.command({
|
|
124
134
|
command: 'missed-chats',
|
|
125
135
|
describe: 'Show recent missed chats with routing snapshots (why each chat was missed)',
|
package/lib/commands/avatar.js
CHANGED
|
@@ -35,6 +35,7 @@ export const avatarCommand = {
|
|
|
35
35
|
console.log(` Enabled: ${data.enabled ? 'yes' : 'no'}`);
|
|
36
36
|
console.log(` "Call me": ${data.showCallMeOption ? 'yes' : 'no'}`);
|
|
37
37
|
console.log(` Callback window: ${data.callbackConfirmWindowMinutes} min`);
|
|
38
|
+
console.log(` Show transcript: ${data.showTranscript ? 'yes' : 'no'}`);
|
|
38
39
|
console.log(` Display name: ${data.displayName || '(AI)'}`);
|
|
39
40
|
console.log(` Badge shown: ${data.showBadge ? 'yes' : 'no'}`);
|
|
40
41
|
console.log(` Size: ${data.sizePx}px`);
|
|
@@ -78,6 +79,7 @@ export const avatarCommand = {
|
|
|
78
79
|
.option('avatar-id',{ type: 'string', describe: 'Provider-specific avatar/face ID. Omit for provider default.' })
|
|
79
80
|
.option('voice', { type: 'string', describe: 'ElevenLabs voice ID override' })
|
|
80
81
|
.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.' })
|
|
82
|
+
.option('show-transcript', { type: 'boolean', describe: 'Show an on-screen avatar transcript (default off). Omit on a re-run to leave it untouched.' })
|
|
81
83
|
.option('call-me', { type: 'boolean', describe: 'Show the "Call me instead" option. Omit on a re-run to leave it untouched.' })
|
|
82
84
|
.option('display-name', { type: 'string', describe: 'Visitor-facing avatar name on the badge (e.g. Moshky). Empty string resets to "AI".' })
|
|
83
85
|
.option('show-badge', { type: 'boolean', describe: 'Show the name badge over the avatar video.' })
|
|
@@ -97,6 +99,7 @@ export const avatarCommand = {
|
|
|
97
99
|
if (argv.voice !== undefined) body.voiceId = argv.voice;
|
|
98
100
|
if (argv['keep-on-agent-join'] !== undefined) body.enabledOnAgentTransfer = argv['keep-on-agent-join'];
|
|
99
101
|
if (argv['call-me'] !== undefined) body.showCallMeOption = argv['call-me'];
|
|
102
|
+
if (argv['show-transcript'] !== undefined) body.showTranscript = argv['show-transcript'];
|
|
100
103
|
if (argv['display-name'] !== undefined) body.displayName = argv['display-name'];
|
|
101
104
|
if (argv['show-badge'] !== undefined) body.showBadge = argv['show-badge'];
|
|
102
105
|
if (argv.size !== undefined) body.sizePx = argv.size;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// velaro billing-agents: run and review the billing agents (staff only). Contract: docs/architecture/billing-agents-and-package-builder.md
|
|
2
|
+
import { request } from '../api.js';
|
|
3
|
+
|
|
4
|
+
const show = (o) => console.log(JSON.stringify(o, null, 2));
|
|
5
|
+
|
|
6
|
+
async function list() {
|
|
7
|
+
const agents = await request('GET', '/BillingAgents');
|
|
8
|
+
for (const a of agents) console.log(`${a.key.padEnd(12)} ${a.name.padEnd(18)} open findings: ${a.openFindings} last run: ${a.lastRun ? `${a.lastRun.mode}/${a.lastRun.status} ${a.lastRun.startedAt}` : 'never'}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function run(args) {
|
|
12
|
+
const mode = args.live ? 'live' : 'test';
|
|
13
|
+
const body = { mode };
|
|
14
|
+
if (args.site) body.siteId = args.site;
|
|
15
|
+
if (args.days) body.params = { days: args.days };
|
|
16
|
+
const r = await request('POST', `/BillingAgents/${args.agent}/run`, body);
|
|
17
|
+
console.log(`Run ${r.run.id} [${r.run.mode}] ${r.run.status}: ${r.findings.length} findings${mode === 'test' ? ' (dry run, no side effects)' : ''}`);
|
|
18
|
+
for (const f of r.findings) console.log(` #${f.id} ${f.severity.padEnd(5)} ${f.ruleKey.padEnd(26)} ${f.title}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function runs(args) {
|
|
22
|
+
const q = new URLSearchParams();
|
|
23
|
+
if (args.agent) q.set('agentKey', args.agent);
|
|
24
|
+
if (args.mode) q.set('mode', args.mode);
|
|
25
|
+
show(await request('GET', `/BillingAgents/runs?${q}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function findings(args) {
|
|
29
|
+
const q = new URLSearchParams();
|
|
30
|
+
for (const k of ['status', 'severity']) if (args[k]) q.set(k, args[k]);
|
|
31
|
+
if (args.agent) q.set('agentKey', args.agent);
|
|
32
|
+
show(await request('GET', `/BillingAgents/findings?${q}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const step = (verb) => async (args) => show(await request('POST', `/BillingAgents/findings/${args.id}/${verb}`, {}));
|
|
36
|
+
|
|
37
|
+
export function billingAgentsCommand(yargs) {
|
|
38
|
+
return yargs
|
|
39
|
+
.command('list', 'List agents with last run and open findings', () => {}, () => list())
|
|
40
|
+
.command('run <agent>', 'Run an agent (default TEST mode = dry run)', y => y
|
|
41
|
+
.positional('agent', { type: 'string', desc: 'close|collections|analyst|pricing|orders|utilization|syncfailure' })
|
|
42
|
+
.option('live', { type: 'boolean', desc: 'Live mode (findings + approval queue; still no side effects until apply)' })
|
|
43
|
+
.option('site', { type: 'number' }).option('days', { type: 'number' }), a => run(a))
|
|
44
|
+
.command('runs', 'List runs', y => y.option('agent', { type: 'string' }).option('mode', { type: 'string' }), a => runs(a))
|
|
45
|
+
.command('findings', 'List findings', y => y.option('status', { type: 'string' }).option('severity', { type: 'string' }).option('agent', { type: 'string' }), a => findings(a))
|
|
46
|
+
.command('approve <id>', 'Approve a finding (two-person rule for financial actions)', y => y.positional('id', { type: 'number' }), step('approve'))
|
|
47
|
+
.command('reject <id>', 'Reject a finding', y => y.positional('id', { type: 'number' }), step('reject'))
|
|
48
|
+
.command('apply <id>', 'Apply an approved finding (draft-safe actions only)', y => y.positional('id', { type: 'number' }), step('apply'))
|
|
49
|
+
.command('monitor', 'Agent health: last runs, 7d failure rate, stale agents', () => {}, async () => show(await request('GET', '/BillingAgents/monitor')))
|
|
50
|
+
.demandCommand(1, 'Specify a subcommand: list | run | runs | findings | approve | reject | apply | monitor');
|
|
51
|
+
}
|
|
@@ -35,7 +35,7 @@ async function listRules() {
|
|
|
35
35
|
async function setGlobal(appliesTo, mode) {
|
|
36
36
|
if (!GATES.includes(appliesTo)) fail(`appliesTo must be one of: ${GATES.join(', ')}`);
|
|
37
37
|
if (!MODES.includes(mode)) fail(`mode must be one of: ${MODES.join(', ')}`);
|
|
38
|
-
await request('PUT', `/api/BillingAutomation/global/${appliesTo}`, { mode });
|
|
38
|
+
await request('PUT', `/api/BillingAutomation/global/${appliesTo}`, { mode }); // controller action: SaveGlobal
|
|
39
39
|
console.log(`Global ${appliesTo} set to ${mode}.`);
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// MCP parity command groups: admin, apple, force-expire-site, backfill, kpi, audit-trail, code, crm, hangfire.
|
|
2
|
+
// Each subcommand calls the same REST route as the MCP tool of the same name (mcp/server.js, mcp/staff-server.js).
|
|
3
|
+
// Staff-only routes are enforced by the backend.
|
|
4
|
+
import { get, post, del, messagingGet, messagingPost } from '../api.js';
|
|
5
|
+
import { show, qs, needYes, group } from '../parity-util.js';
|
|
6
|
+
|
|
7
|
+
export const adminCommand = { command: 'admin', ...group('Velaro staff: admin API diagnostics', [
|
|
8
|
+
['startup-diagnostics', 'Recent AdminStartupDiagnostics rows (migrations, startup exceptions)',
|
|
9
|
+
(y) => y.option('context-label', { type: 'string' }).option('count', { type: 'number' }),
|
|
10
|
+
async (a) => show(await get(`/SuperAdmin/StartupDiagnostics${qs({ contextLabel: a['context-label'], count: a.count })}`))],
|
|
11
|
+
]) };
|
|
12
|
+
|
|
13
|
+
export const appleCommand = { command: 'apple', ...group('Velaro staff: Apple Messages for Business diagnostics', [
|
|
14
|
+
['health <siteId>', 'Apple Messages health for a site',
|
|
15
|
+
(y) => y.positional('siteId', { type: 'number' }),
|
|
16
|
+
async (a) => show(await messagingGet(`/SupportTools/sites/${a.siteId}/apple/health`))],
|
|
17
|
+
['test-control <siteId>', 'Send an Apple test-control action to a conversation',
|
|
18
|
+
(y) => y.positional('siteId', { type: 'number' })
|
|
19
|
+
.option('control', { type: 'string', demandOption: true, describe: 'Control name' })
|
|
20
|
+
.option('conversation-id', { type: 'number' }),
|
|
21
|
+
async (a) => show(await messagingPost(`/SupportTools/sites/${a.siteId}/apple/test-control`, { control: a.control, conversationId: a['conversation-id'] }))],
|
|
22
|
+
]) };
|
|
23
|
+
|
|
24
|
+
export const forceExpireSiteCommand = {
|
|
25
|
+
command: 'force-expire-site <siteId>',
|
|
26
|
+
describe: 'Velaro staff: force-expire a site subscription (destructive, needs --yes)',
|
|
27
|
+
builder: (y) => y.positional('siteId', { type: 'number' }).option('yes', { type: 'boolean', describe: 'Confirm' }),
|
|
28
|
+
handler: async (a) => {
|
|
29
|
+
needYes(a, `force-expire site ${a.siteId}`);
|
|
30
|
+
show(await messagingPost(`/SuperAdmin/subscription/${a.siteId}/force-expire`));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const backfillCommand = { command: 'backfill', ...group('Velaro staff: per-site data backfills (preview unless --apply --yes)', [
|
|
35
|
+
['types', 'List backfill types', null, async () => show(await messagingGet('/SuperAdmin/backfill-types'))],
|
|
36
|
+
['run <siteId>', 'Preview or apply a backfill for a site',
|
|
37
|
+
(y) => y.positional('siteId', { type: 'number' })
|
|
38
|
+
.option('type', { type: 'string', demandOption: true })
|
|
39
|
+
.option('from', { type: 'string', demandOption: true, describe: 'yyyy-MM-dd' })
|
|
40
|
+
.option('to', { type: 'string', demandOption: true, describe: 'yyyy-MM-dd' })
|
|
41
|
+
.option('apply', { type: 'boolean', describe: 'Write the backfill (default is a preview)' })
|
|
42
|
+
.option('yes', { type: 'boolean', describe: 'Confirm --apply' }),
|
|
43
|
+
async (a) => {
|
|
44
|
+
if (a.apply) needYes(a, 'apply a backfill');
|
|
45
|
+
show(await messagingPost(`/SuperAdmin/sites/${encodeURIComponent(a.siteId)}/backfill`, { type: a.type, from: a.from, to: a.to, apply: a.apply === true }));
|
|
46
|
+
}],
|
|
47
|
+
]) };
|
|
48
|
+
|
|
49
|
+
export const kpiCommand = { command: 'kpi', ...group('Live operations KPIs', [
|
|
50
|
+
['snapshot', 'Real-time ops snapshot: agents, queues, waits, channels, teams', null, async () => show(await get('/LiveDashboard/snapshot'))],
|
|
51
|
+
]) };
|
|
52
|
+
|
|
53
|
+
export const auditTrailCommand = { command: 'audit-trail', ...group('Audit trail of configuration changes', [
|
|
54
|
+
['list', 'List audit entries',
|
|
55
|
+
(y) => y.option('entity-type', { type: 'string' }).option('entity-id', { type: 'string' }).option('take', { type: 'number', default: 200 }),
|
|
56
|
+
async (a) => show(await get(`/AuditTrail${qs({ entityType: a['entity-type'], entityId: a['entity-id'], take: Math.min(500, a.take ?? 200) })}`))],
|
|
57
|
+
]) };
|
|
58
|
+
|
|
59
|
+
export const codeCommand = { command: 'code', ...group('Push files to a Velaro repo branch (needs GITHUB_TOKEN)', [
|
|
60
|
+
['push-files', 'Create one commit with files on dev/main/master (destructive, needs --yes)',
|
|
61
|
+
(y) => y.option('message', { type: 'string', demandOption: true })
|
|
62
|
+
.option('file', { type: 'array', demandOption: true, describe: 'repoPath=localPath, repeatable' })
|
|
63
|
+
.option('repo', { type: 'string', default: 'velaro-admin' })
|
|
64
|
+
.option('branch', { type: 'string', default: 'dev' })
|
|
65
|
+
.option('yes', { type: 'boolean' }),
|
|
66
|
+
async (a) => {
|
|
67
|
+
needYes(a, 'push a commit');
|
|
68
|
+
const token = process.env.GITHUB_TOKEN;
|
|
69
|
+
if (!token) throw new Error('GITHUB_TOKEN env var is not set.');
|
|
70
|
+
const { readFileSync } = await import('node:fs');
|
|
71
|
+
const files = a.file.map((f) => { const [p, l] = String(f).split('='); return { path: p, content: readFileSync(l || p, 'utf8') }; });
|
|
72
|
+
const REPOS = ['velaro-admin', 'velaro-messaging', 'velaro-shared', 'velaro-shared-frontend'];
|
|
73
|
+
if (!REPOS.includes(a.repo)) throw new Error(`Repo not allowed. Allowed: ${REPOS.join(', ')}`);
|
|
74
|
+
if (!['dev', 'master', 'main'].includes(a.branch)) throw new Error('Branch must be dev, master or main.');
|
|
75
|
+
const norm = (p) => p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\//, '');
|
|
76
|
+
if (files.some((f) => norm(f.path) === '.github' || norm(f.path).startsWith('.github/'))) throw new Error('Cannot push to .github/ paths.');
|
|
77
|
+
const base = `https://api.github.com/repos/velaro/${a.repo}`;
|
|
78
|
+
const headers = { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'Content-Type': 'application/json' };
|
|
79
|
+
const gh = async (path, opts = {}) => {
|
|
80
|
+
const r = await fetch(base + path, { headers, ...opts });
|
|
81
|
+
if (!r.ok) throw new Error(`GitHub API ${path} -> ${r.status}: ${await r.text()}`);
|
|
82
|
+
return r.json();
|
|
83
|
+
};
|
|
84
|
+
const head = (await gh(`/git/refs/heads/${a.branch}`)).object.sha;
|
|
85
|
+
const baseTree = (await gh(`/git/commits/${head}`)).tree.sha;
|
|
86
|
+
const tree = await Promise.all(files.map(async (f) => {
|
|
87
|
+
const b = await gh('/git/blobs', { method: 'POST', body: JSON.stringify({ content: f.content, encoding: 'utf-8' }) });
|
|
88
|
+
return { path: f.path, mode: '100644', type: 'blob', sha: b.sha };
|
|
89
|
+
}));
|
|
90
|
+
const nt = await gh('/git/trees', { method: 'POST', body: JSON.stringify({ base_tree: baseTree, tree }) });
|
|
91
|
+
const c = await gh('/git/commits', { method: 'POST', body: JSON.stringify({ message: a.message, tree: nt.sha, parents: [head] }) });
|
|
92
|
+
await gh(`/git/refs/heads/${a.branch}`, { method: 'PATCH', body: JSON.stringify({ sha: c.sha }) });
|
|
93
|
+
console.log(`Pushed to velaro/${a.repo}@${a.branch} commit ${c.sha.slice(0, 8)}\n${files.map((f) => ' ' + f.path).join('\n')}`);
|
|
94
|
+
}],
|
|
95
|
+
]) };
|
|
96
|
+
|
|
97
|
+
export const crmCommand = { command: 'crm', ...group('Velaro staff: CRM email-campaign engine audit', [
|
|
98
|
+
['email-campaign-audit', 'Row counts for the legacy CRM email-campaign tables (safe-to-retire check)', null,
|
|
99
|
+
async () => show(await messagingGet('/SuperAdmin/crm-email-campaign-audit'))],
|
|
100
|
+
]) };
|
|
101
|
+
|
|
102
|
+
export const hangfireCommand = { command: 'hangfire', ...group('Velaro staff: failed Hangfire jobs', [
|
|
103
|
+
['list-failed', 'List failed jobs', null, async () => show(await get('/DatabaseTool/FailedJobs'))],
|
|
104
|
+
['retry <id>', 'Requeue one failed job', (y) => y.positional('id', { type: 'string' }),
|
|
105
|
+
async (a) => show(await post(`/DatabaseTool/FailedJobs/${a.id}/retry`))],
|
|
106
|
+
['delete-failed <id>', 'Delete one failed job (needs --yes)', (y) => y.positional('id', { type: 'string' }).option('yes', { type: 'boolean' }),
|
|
107
|
+
async (a) => { needYes(a, `delete failed job ${a.id}`); show(await del(`/DatabaseTool/FailedJobs/${a.id}`)); }],
|
|
108
|
+
]) };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// CLI halves of the MCP parity tools in mcp/parity/endpoints.customer.js and endpoints.staff.js.
|
|
2
|
+
// Each handler names the controller action it calls.
|
|
3
|
+
import { request } from '../api.js';
|
|
4
|
+
|
|
5
|
+
const show = (r) => console.log(JSON.stringify(r, null, 2));
|
|
6
|
+
const int = (y, k, d) => y.option(k, { type: 'number', demandOption: true, describe: d });
|
|
7
|
+
|
|
8
|
+
export const recycleBinCommand = {
|
|
9
|
+
command: 'recycle-bin',
|
|
10
|
+
describe: 'List and restore soft-deleted contact lists, members, segments and KB articles/topics (one id at a time)',
|
|
11
|
+
builder: (y) => y
|
|
12
|
+
.command({ command: 'contact-lists', describe: 'List deleted contact lists', handler: async () => show(await request('GET', '/ContactLists/deleted')) }) // controller action: GetDeletedLists
|
|
13
|
+
.command({ command: 'restore-contact-list', describe: 'Restore one contact list', builder: (z) => int(z, 'id', 'List id'), handler: async (a) => show(await request('POST', `/ContactLists/${a.id}/restore`)) }) // controller action: RestoreList
|
|
14
|
+
.command({ command: 'members', describe: 'List deleted members of a list', builder: (z) => int(z, 'list', 'List id'), handler: async (a) => show(await request('GET', `/ContactLists/${a.list}/Members/deleted`)) }) // controller action: GetDeletedMembers
|
|
15
|
+
.command({ command: 'restore-member', describe: 'Restore one list member', builder: (z) => int(int(z, 'list', 'List id'), 'member', 'Member id'), handler: async (a) => show(await request('POST', `/ContactLists/${a.list}/Members/${a.member}/restore`)) }) // controller action: RestoreMember
|
|
16
|
+
.command({ command: 'segments', describe: 'List deleted segments', handler: async () => show(await request('GET', '/ContactSegments/deleted')) }) // controller action: ListDeleted (ContactSegments)
|
|
17
|
+
.command({ command: 'kb-articles', describe: 'List deleted KB articles', handler: async () => show(await request('GET', '/api/kb/articles/deleted')) }) // controller action: ListDeleted (KBArticles)
|
|
18
|
+
.command({ command: 'restore-kb-article', describe: 'Restore one KB article', builder: (z) => int(z, 'id', 'Article id'), handler: async (a) => show(await request('POST', `/api/kb/articles/${a.id}/restore`)) }) // controller action: RestoreDeleted (KBArticles)
|
|
19
|
+
.command({ command: 'kb-topics', describe: 'List deleted KB topics', handler: async () => show(await request('GET', '/api/kb/topics/deleted')) }) // controller action: ListDeleted (KBTopics)
|
|
20
|
+
.command({ command: 'restore-kb-topic', describe: 'Restore one KB topic', builder: (z) => int(z, 'id', 'Topic id'), handler: async (a) => show(await request('POST', `/api/kb/topics/${a.id}/restore`)) }) // controller action: RestoreDeleted (KBTopics)
|
|
21
|
+
.demandCommand(1),
|
|
22
|
+
handler: () => {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const aiUsageCommand = {
|
|
26
|
+
command: 'ai-usage',
|
|
27
|
+
describe: 'Your site AI usage summary and budget alert',
|
|
28
|
+
builder: (y) => y
|
|
29
|
+
.command({ command: 'summary', describe: 'AI usage summary', handler: async () => show(await request('GET', '/AiUsageReport/summary')) }) // controller action: GetSummary
|
|
30
|
+
.command({
|
|
31
|
+
command: 'budget-alert-set', describe: 'Update the budget alert',
|
|
32
|
+
builder: (z) => z.option('enabled', { type: 'boolean' }).option('threshold-percent', { type: 'number' }).option('recipients', { type: 'string', describe: 'Comma-separated emails' }),
|
|
33
|
+
handler: async (a) => show(await request('PUT', '/AiUsageReport/budget-alert', { enabled: a.enabled, thresholdPercent: a.thresholdPercent, recipientEmails: a.recipients })), // controller action: UpdateBudgetAlert
|
|
34
|
+
})
|
|
35
|
+
.command({ command: 'budget-alert-delete', describe: 'Delete the budget alert', handler: async () => show(await request('DELETE', '/AiUsageReport/budget-alert')) }) // controller action: DeleteBudgetAlert
|
|
36
|
+
.demandCommand(1),
|
|
37
|
+
handler: () => {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const siteInfoCommand = {
|
|
41
|
+
command: 'site-info',
|
|
42
|
+
describe: 'Read-only site settings, entitlements and platform uptime for your site',
|
|
43
|
+
builder: (y) => y
|
|
44
|
+
.command({ command: 'settings', describe: 'Site settings', handler: async () => show(await request('GET', '/api/SiteSettings')) }) // controller action: GetSiteSettings
|
|
45
|
+
.command({ command: 'entitlements', describe: 'Your resolved entitlements', handler: async () => show(await request('GET', '/Entitlements/my')) }) // controller action: GetMyEntitlements
|
|
46
|
+
.command({ command: 'uptime', describe: 'Platform uptime', handler: async () => show(await request('GET', '/PlatformUptime')) }) // controller action: GetAsync (PlatformUptime)
|
|
47
|
+
.command({ command: 'last-copilot-session', describe: 'Your last Copilot session summary', handler: async () => show(await request('GET', '/Copilot/LastSession')) }) // controller action: LastSession
|
|
48
|
+
.command({ command: 'sender-domain-health', describe: 'Sender domain health', handler: async () => show(await request('GET', '/SenderDomains/Health')) }) // controller action: GetHealth
|
|
49
|
+
.command({ command: 'netsuite-interaction-summary', describe: 'NetSuite AI interaction summary for a conversation', builder: (z) => int(z, 'conversation', 'Conversation id'), handler: async (a) => show(await request('POST', `/NetSuite/interaction-summary?conversationId=${a.conversation}`)) }) // controller action: GetInteractionSummary
|
|
50
|
+
.demandCommand(1),
|
|
51
|
+
handler: () => {},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const billingReceiptsCommand = {
|
|
55
|
+
command: 'billing-receipts-run',
|
|
56
|
+
describe: 'Staff only: run the payment receipt job now',
|
|
57
|
+
handler: async () => show(await request('POST', '/api/BillingNotifications/receipts/run-now')), // controller action: RunReceiptsNow
|
|
58
|
+
};
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
// velaro entitlement — query and manage site feature entitlements (superadmin only)
|
|
2
2
|
import { request } from '../api.js';
|
|
3
3
|
|
|
4
|
+
async function syncStripe(a) {
|
|
5
|
+
const r = await request('PUT', `Entitlements/admin/sync-stripe/${a.site}`, {
|
|
6
|
+
planKey: a.plan,
|
|
7
|
+
status: a.status,
|
|
8
|
+
cancelAtPeriodEnd: a['cancel-at-period-end'] ?? false,
|
|
9
|
+
currentPeriodEnd: a['current-period-end'] ?? null,
|
|
10
|
+
trialEndAt: a['trial-end-at'] ?? null,
|
|
11
|
+
stripeCustomerId: a['stripe-customer-id'],
|
|
12
|
+
stripeSubscriptionId: a['stripe-subscription-id'],
|
|
13
|
+
});
|
|
14
|
+
console.log(`Site ${a.site} synced: status=${r.status} plan=${r.planKey || 'unchanged'}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
async function getSite(args) {
|
|
5
18
|
if (!args.site) { console.error('Usage: velaro entitlement get --site 1032'); process.exit(1); }
|
|
6
19
|
const result = await request('GET', `Entitlements/${args.site}`);
|
|
@@ -289,6 +302,16 @@ export function entitlementCommand(yargs) {
|
|
|
289
302
|
.option('at', { type: 'string', desc: 'ISO 8601 timestamp to lock entitlement resolution to' })
|
|
290
303
|
.option('unlock', { type: 'boolean', desc: 'Clear an existing lock (resolves against current plan version again)' })
|
|
291
304
|
, a => setLock(a))
|
|
305
|
+
.command('sync-stripe', 'Sync the Stripe subscription state into its entitlement record (superadmin only)', y => y
|
|
306
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
307
|
+
.option('status', { type: 'string', demandOption: true, desc: 'Stripe subscription status, e.g. active, trialing, canceled' })
|
|
308
|
+
.option('plan', { type: 'string', desc: 'Plan key, e.g. plan.professional' })
|
|
309
|
+
.option('cancel-at-period-end', { type: 'boolean', desc: 'Subscription cancels at period end' })
|
|
310
|
+
.option('current-period-end', { type: 'string', desc: 'ISO 8601 current period end' })
|
|
311
|
+
.option('trial-end-at', { type: 'string', desc: 'ISO 8601 trial end' })
|
|
312
|
+
.option('stripe-customer-id', { type: 'string', desc: 'Stripe customer id' })
|
|
313
|
+
.option('stripe-subscription-id', { type: 'string', desc: 'Stripe subscription id' })
|
|
314
|
+
, a => syncStripe(a))
|
|
292
315
|
.command('trial <action>', 'Trial governance (superadmin): get | recommend | not-paying | grant | hold | reactivate | set-limit | approve-limit | deny-limit | set-text', y => y
|
|
293
316
|
.option('site', { type: 'number', desc: 'Site ID' })
|
|
294
317
|
.option('feature', { type: 'string', desc: 'Feature key, e.g. EnableIvr' })
|
|
@@ -305,5 +328,8 @@ export function entitlementCommand(yargs) {
|
|
|
305
328
|
.option('key', { type: 'string', demandOption: true, desc: 'Feature key, e.g. EnableIvr' })
|
|
306
329
|
.option('rollback', { type: 'boolean', desc: 'Pass to roll back this flag to Subscription.cs (default: go live)' })
|
|
307
330
|
, a => setCutover(a))
|
|
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');
|
|
331
|
+
.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 | sync-stripe | trial | list-cutover | set-cutover');
|
|
309
332
|
}
|
|
333
|
+
|
|
334
|
+
// Registered group object (the CLI/MCP parity validator reads `command:` from a named const).
|
|
335
|
+
export const entitlementGroup = { command: 'entitlement', desc: 'Query and manage site feature entitlements (superadmin only)', builder: entitlementCommand, handler: () => {} };
|
|
@@ -38,3 +38,6 @@ export function featureDiscoveryCommand(yargs) {
|
|
|
38
38
|
, a => requestFeature(a))
|
|
39
39
|
.demandCommand(1, 'Specify a subcommand: list | request');
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
// Registered group object (the CLI/MCP parity validator reads `command:` from a named const).
|
|
43
|
+
export const featureDiscoveryGroup = { command: 'feature-discovery', desc: 'View and act on feature-discovery suggestions for this site', builder: featureDiscoveryCommand, handler: () => {} };
|
package/lib/commands/magento.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* velaro magento products Search the product catalog
|
|
9
9
|
* velaro magento logs View integration activity logs
|
|
10
10
|
* velaro magento diagnose Live connection diagnostic (token, /rest/V1, scopes)
|
|
11
|
+
* velaro magento sidebar-fields get|set Agent sidebar fields (site from your login)
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { request, messagingRequest } from '../api.js';
|
|
@@ -189,6 +190,48 @@ async function diagnoseHandler(argv) {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// ── sidebar-fields ────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
async function sidebarFieldsHandler(argv) {
|
|
196
|
+
const action = argv.action;
|
|
197
|
+
try {
|
|
198
|
+
if (action === 'get') {
|
|
199
|
+
const r = await messagingRequest('GET', '/Magento/data/sidebar-fields');
|
|
200
|
+
console.log(JSON.stringify(r, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (action !== 'set') {
|
|
204
|
+
console.error(`${RED}✗ action must be get or set${RST}`);
|
|
205
|
+
process.exitCode = 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
let fields;
|
|
209
|
+
if (argv.clear) {
|
|
210
|
+
fields = [];
|
|
211
|
+
} else if (argv.fields !== undefined) {
|
|
212
|
+
try { fields = JSON.parse(argv.fields); } catch (e) {
|
|
213
|
+
console.error(`${RED}✗ --fields must be valid JSON: ${e.message}${RST}`);
|
|
214
|
+
process.exitCode = 1;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(fields)) {
|
|
218
|
+
console.error(`${RED}✗ --fields must be a JSON array of {fieldName, entity, label}${RST}`);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Partial body: omitting --fields leaves the saved list untouched.
|
|
224
|
+
const body = fields === undefined ? {} : { fields };
|
|
225
|
+
const r = await messagingRequest('PUT', '/Magento/data/sidebar-fields', body);
|
|
226
|
+
console.log(JSON.stringify(r, null, 2));
|
|
227
|
+
} catch (e) {
|
|
228
|
+
console.error(`
|
|
229
|
+
${RED}✗ sidebar-fields ${action} failed: ${e.message}${RST}
|
|
230
|
+
`);
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
192
235
|
// ── command export ────────────────────────────────────────────────────────────
|
|
193
236
|
|
|
194
237
|
export const magentoCommand = {
|
|
@@ -250,6 +293,15 @@ export const magentoCommand = {
|
|
|
250
293
|
.option('site-id', { describe: 'Site ID to diagnose', type: 'number' }),
|
|
251
294
|
handler: diagnoseHandler,
|
|
252
295
|
})
|
|
296
|
+
.command({
|
|
297
|
+
command: 'sidebar-fields <action>',
|
|
298
|
+
describe: 'Get or set the Magento agent sidebar fields (action: get|set)',
|
|
299
|
+
builder: yy => yy
|
|
300
|
+
.positional('action', { choices: ['get', 'set'], type: 'string' })
|
|
301
|
+
.option('fields', { describe: 'JSON array of {fieldName, entity, label} (set only)', type: 'string' })
|
|
302
|
+
.option('clear', { describe: 'Clear the saved list (set only)', type: 'boolean', default: false }),
|
|
303
|
+
handler: sidebarFieldsHandler,
|
|
304
|
+
})
|
|
253
305
|
.demandCommand(1, 'Specify a subcommand. Try: velaro magento --help'),
|
|
254
306
|
handler: () => {},
|
|
255
307
|
};
|