@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 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({ command: 'pricing', desc: 'View and manage the pricing catalog', builder: pricingCommand, handler: () => {} })
260
- .command({ command: 'entitlement', desc: 'Query and manage site feature entitlements (superadmin only)', builder: entitlementCommand, handler: () => {} })
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({ command: 'support', desc: 'Staff SupportTools actions on a site (superadmin only)', builder: supportCommand, handler: () => {} })
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({ command: 'feature-discovery', desc: 'View and act on feature-discovery suggestions for this site', builder: featureDiscoveryCommand, handler: () => {} })
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()
@@ -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)',
@@ -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: () => {} };
@@ -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
  };