@velaro/cli 1.2.0 → 1.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +161 -138
  2. package/bin/velaro.js +177 -62
  3. package/lib/api.js +91 -52
  4. package/lib/api.test.js +46 -0
  5. package/lib/banner.js +76 -0
  6. package/lib/commands/activity.js +133 -0
  7. package/lib/commands/acuity.js +66 -0
  8. package/lib/commands/agent.js +204 -50
  9. package/lib/commands/ai-config.js +193 -0
  10. package/lib/commands/ai-models.js +159 -0
  11. package/lib/commands/appointments.js +198 -0
  12. package/lib/commands/article.js +668 -388
  13. package/lib/commands/automation-draft.js +134 -0
  14. package/lib/commands/avatar.js +75 -0
  15. package/lib/commands/bigcommerce.js +50 -0
  16. package/lib/commands/billing-contacts.js +62 -0
  17. package/lib/commands/billing-email-preference.js +64 -0
  18. package/lib/commands/billing-subscription.js +265 -0
  19. package/lib/commands/billing.js +138 -0
  20. package/lib/commands/bot.js +141 -137
  21. package/lib/commands/bundle.js +168 -0
  22. package/lib/commands/calendly.js +62 -0
  23. package/lib/commands/callback.js +125 -0
  24. package/lib/commands/callrail.js +88 -0
  25. package/lib/commands/campaigns.js +44 -0
  26. package/lib/commands/case.js +102 -0
  27. package/lib/commands/check.js +163 -163
  28. package/lib/commands/compliance.js +229 -0
  29. package/lib/commands/conversation-efficiency.js +178 -0
  30. package/lib/commands/copilotstudio.js +114 -0
  31. package/lib/commands/coupon-grant.js +192 -0
  32. package/lib/commands/db.js +101 -0
  33. package/lib/commands/deployment.js +107 -107
  34. package/lib/commands/diagnostics.js +298 -0
  35. package/lib/commands/email-campaign.js +47 -0
  36. package/lib/commands/email-inbox.js +88 -0
  37. package/lib/commands/entitlement.js +176 -0
  38. package/lib/commands/env.js +45 -45
  39. package/lib/commands/feature-discovery.js +40 -0
  40. package/lib/commands/focus.js +278 -0
  41. package/lib/commands/index.js +38 -5
  42. package/lib/commands/ingest.js +31 -31
  43. package/lib/commands/inline-widget-config.js +126 -0
  44. package/lib/commands/integration.js +93 -0
  45. package/lib/commands/kb.js +450 -309
  46. package/lib/commands/login.js +86 -86
  47. package/lib/commands/logs.js +680 -0
  48. package/lib/commands/magento.js +210 -0
  49. package/lib/commands/mcp-key.js +188 -159
  50. package/lib/commands/migrate.js +134 -0
  51. package/lib/commands/migration-status.js +66 -0
  52. package/lib/commands/monday.js +137 -0
  53. package/lib/commands/netsuite.js +87 -0
  54. package/lib/commands/notifications.js +63 -0
  55. package/lib/commands/notion.js +70 -0
  56. package/lib/commands/ops.js +267 -173
  57. package/lib/commands/payment-recovery.js +170 -0
  58. package/lib/commands/pickup.js +172 -0
  59. package/lib/commands/pricing.js +132 -0
  60. package/lib/commands/product.js +55 -0
  61. package/lib/commands/recruiting.js +374 -0
  62. package/lib/commands/report.js +462 -0
  63. package/lib/commands/routing.js +304 -0
  64. package/lib/commands/rule.js +85 -85
  65. package/lib/commands/sharepoint.js +167 -0
  66. package/lib/commands/site-provision.js +68 -0
  67. package/lib/commands/site.js +62 -62
  68. package/lib/commands/sitesync.js +158 -0
  69. package/lib/commands/slack.js +64 -0
  70. package/lib/commands/squarespace.js +108 -0
  71. package/lib/commands/status.js +24 -24
  72. package/lib/commands/subscription.js +43 -0
  73. package/lib/commands/support.js +128 -0
  74. package/lib/commands/survey.js +216 -0
  75. package/lib/commands/team.js +144 -144
  76. package/lib/commands/teams-phone.js +131 -0
  77. package/lib/commands/teams.js +106 -0
  78. package/lib/commands/telephony.js +99 -0
  79. package/lib/commands/update.js +47 -47
  80. package/lib/commands/webflow.js +128 -0
  81. package/lib/commands/whoami.js +25 -22
  82. package/lib/commands/widget-container.js +152 -0
  83. package/lib/commands/woocommerce.js +240 -0
  84. package/lib/commands/workflow.js +233 -98
  85. package/lib/config.js +85 -83
  86. package/lib/kb-screenshot.js +320 -0
  87. package/lib/migrations/amscro.json +72 -0
  88. package/lib/migrations/azenta.json +68 -0
  89. package/lib/migrations/bluefire.json +49 -0
  90. package/lib/migrations/donaldson.json +75 -0
  91. package/lib/oauth.js +149 -135
  92. package/lib/run.js +21 -16
  93. package/lib/sharepoint-auth.js +138 -0
  94. package/lib/subscription.js +41 -39
  95. package/lib/track.js +35 -35
  96. package/lib/update-check.js +64 -64
  97. package/package.json +34 -19
  98. package/scripts/postinstall.js +12 -0
@@ -1,50 +1,204 @@
1
- import { get, post } from '../api.js';
2
- import { runCommand } from '../run.js';
3
-
4
- const VALID_STATUSES = ['Available', 'Away', 'Offline'];
5
-
6
- export const agentCommand = {
7
- command: 'agent <subcommand>',
8
- describe: 'View and manage agents',
9
- builder: (yargs) => yargs
10
- .command({
11
- command: 'list',
12
- describe: 'List agents and their current availability',
13
- handler: runCommand(async () => {
14
- const [agents, teams] = await Promise.all([
15
- get('/Users/List'),
16
- get('/Teams/List'),
17
- ]);
18
-
19
- const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
20
-
21
- if (!agents.length) { console.log('No agents found.'); return; }
22
-
23
- const rows = agents.map(a => ({
24
- id: `[${a.id}]`,
25
- status: (a.status ?? 'Offline').padEnd(9),
26
- name: a.displayName || `${a.firstName} ${a.lastName}`.trim() || a.email,
27
- teams: (a.teamIds ?? []).map(id => teamMap[id] ?? `team ${id}`).join(', ') || '—',
28
- }));
29
-
30
- const idW = Math.max(...rows.map(r => r.id.length));
31
-
32
- for (const r of rows) {
33
- console.log(`${r.id.padStart(idW)} ${r.status} ${r.name.padEnd(30)} ${r.teams}`);
34
- }
35
- console.log(`\n${rows.length} agent(s)`);
36
- }),
37
- })
38
- .command({
39
- command: 'set-status',
40
- describe: 'Set an agent\'s availability status (admin only)',
41
- builder: (y) => y
42
- .option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
43
- .option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
44
- handler: runCommand(async (argv) => {
45
- await post('/UserStatus/admin', { userId: argv.id, status: argv.status });
46
- console.log(`Agent ${argv.id} set to ${argv.status}.`);
47
- }),
48
- })
49
- .demandCommand(1, 'Specify a subcommand: list | set-status'),
50
- };
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const VALID_STATUSES = ['Available', 'Away', 'Offline'];
5
+
6
+ const MISSED_REASON_LABELS = {
7
+ NoAgentsOnline: 'No agents online',
8
+ AgentsNotAvailable: 'Agents not available',
9
+ AgentsBusy: 'Agents at capacity',
10
+ OutOfSchedule: 'Outside business hours',
11
+ TimedOut: 'Queue timeout',
12
+ Abandoned: 'Visitor abandoned',
13
+ Rejected: 'Agent rejected',
14
+ PreChatSurveyAbandoned: 'Pre-chat abandoned',
15
+ BotTransferAbandoned: 'Bot transfer abandoned',
16
+ };
17
+
18
+ function fmtSec(s) {
19
+ if (s == null) return '—';
20
+ if (s < 60) return `${s}s`;
21
+ if (s < 3600) return `${Math.round(s / 60)}m`;
22
+ const h = Math.floor(s / 3600), m = Math.round((s % 3600) / 60);
23
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
24
+ }
25
+
26
+ export const agentCommand = {
27
+ command: 'agent <subcommand>',
28
+ describe: 'View and manage agents',
29
+ builder: (yargs) => yargs
30
+ .command({
31
+ command: 'list',
32
+ describe: 'List agents and their current availability',
33
+ handler: runCommand(async () => {
34
+ const [agents, teams] = await Promise.all([
35
+ get('/Users/List'),
36
+ get('/Teams/List'),
37
+ ]);
38
+
39
+ const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
40
+
41
+ if (!agents.length) { console.log('No agents found.'); return; }
42
+
43
+ const rows = agents.map(a => ({
44
+ id: `[${a.id}]`,
45
+ status: (a.status ?? 'Offline').padEnd(9),
46
+ name: a.displayName || `${a.firstName} ${a.lastName}`.trim() || a.email,
47
+ teams: (a.teamIds ?? []).map(id => teamMap[id] ?? `team ${id}`).join(', ') || '—',
48
+ }));
49
+
50
+ const idW = Math.max(...rows.map(r => r.id.length));
51
+
52
+ for (const r of rows) {
53
+ console.log(`${r.id.padStart(idW)} ${r.status} ${r.name.padEnd(30)} ${r.teams}`);
54
+ }
55
+ console.log(`\n${rows.length} agent(s)`);
56
+ }),
57
+ })
58
+ .command({
59
+ command: 'set-status',
60
+ describe: 'Set an agent\'s availability status (admin only)',
61
+ builder: (y) => y
62
+ .option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
63
+ .option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
64
+ handler: runCommand(async (argv) => {
65
+ await post('/UserStatus/admin', { userId: argv.id, status: argv.status });
66
+ console.log(`Agent ${argv.id} set to ${argv.status}.`);
67
+ }),
68
+ })
69
+ .command({
70
+ command: 'missed-chats',
71
+ describe: 'Show recent missed chats with routing snapshots (why each chat was missed)',
72
+ builder: (y) => y
73
+ .option('start', { type: 'string', describe: 'Start date, e.g. 2026-06-01 (default: 7 days ago)' })
74
+ .option('end', { type: 'string', describe: 'End date, e.g. 2026-06-15 (default: now)' })
75
+ .option('reason', { type: 'string', describe: 'Filter: NoAgentsOnline | AgentsNotAvailable | AgentsBusy | OutOfSchedule | TimedOut | Abandoned | Rejected' })
76
+ .option('team-id', { type: 'number', describe: 'Filter by team ID' })
77
+ .option('site-id', { type: 'number', describe: 'SuperAdmin only — run for a specific customer site' }),
78
+ handler: runCommand(async (argv) => {
79
+ const qs = new URLSearchParams();
80
+ if (argv.start) qs.set('start', argv.start);
81
+ if (argv.end) qs.set('end', argv.end);
82
+ if (argv.reason) qs.set('reason', argv.reason);
83
+ if (argv['team-id']) qs.set('teamId', String(argv['team-id']));
84
+ if (argv['site-id']) qs.set('siteId', String(argv['site-id']));
85
+
86
+ const r = await get(`/CallCenterAnalytics/MissedChatDiagnostics?${qs}`);
87
+
88
+ if (!r?.rows?.length) {
89
+ console.log('No missed chats in this period.');
90
+ return;
91
+ }
92
+
93
+ console.log(`Missed chats: ${r.total} (${r.dateRange?.start?.slice(0,10)} – ${r.dateRange?.end?.slice(0,10)})\n`);
94
+
95
+ // Summary by reason
96
+ const byReason = {};
97
+ for (const c of r.rows) byReason[c.missedReason] = (byReason[c.missedReason] ?? 0) + 1;
98
+ for (const [reason, count] of Object.entries(byReason).sort((a, b) => b[1] - a[1])) {
99
+ console.log(` ${String(count).padStart(4)} ${MISSED_REASON_LABELS[reason] ?? reason}`);
100
+ }
101
+
102
+ console.log('\nRecent missed chats (most recent first):');
103
+ for (const c of r.rows.slice(0, 20)) {
104
+ const snap = c.routingSnapshot;
105
+ const ts = new Date(c.startTimestamp).toISOString().replace('T', ' ').slice(0, 16);
106
+ const reason = MISSED_REASON_LABELS[c.missedReason] ?? c.missedReason;
107
+ console.log(`\n [${c.conversationId}] ${ts} UTC — ${reason}`);
108
+ if (c.teamName) console.log(` Team: ${c.teamName}`);
109
+ if (c.queueWaitSeconds) console.log(` Queue wait: ${fmtSec(c.queueWaitSeconds)}`);
110
+ if (c.assignedAgentName) console.log(` Offered to: ${c.assignedAgentName}`);
111
+ if (snap) {
112
+ console.log(` At miss time: ${snap.agentsOnline ?? '?'} online, ${snap.agentsAvailable ?? '?'} available, ${snap.agentsBusy ?? '?'} busy`);
113
+ if (snap.missDetail) console.log(` Detail: ${snap.missDetail}`);
114
+ }
115
+ }
116
+ if (r.total > 20) console.log(`\n ... and ${r.total - 20} more (use --start/--end to narrow)`);
117
+ }),
118
+ })
119
+ .command({
120
+ command: 'availability',
121
+ describe: 'Show per-agent status history and connectivity events',
122
+ builder: (y) => y
123
+ .option('start', { type: 'string', describe: 'Start date, e.g. 2026-06-01 (default: 7 days ago)' })
124
+ .option('end', { type: 'string', describe: 'End date, e.g. 2026-06-15 (default: now). Max 31 days.' })
125
+ .option('agent-id', { type: 'number', describe: 'Filter to a specific agent (WorkspaceUser ID)' })
126
+ .option('site-id', { type: 'number', describe: 'SuperAdmin only — run for a specific customer site' }),
127
+ handler: runCommand(async (argv) => {
128
+ const qs = new URLSearchParams();
129
+ if (argv.start) qs.set('start', argv.start);
130
+ if (argv.end) qs.set('end', argv.end);
131
+ if (argv['agent-id']) qs.set('agentId', String(argv['agent-id']));
132
+ if (argv['site-id']) qs.set('siteId', String(argv['site-id']));
133
+
134
+ const r = await get(`/CallCenterAnalytics/AgentAvailability?${qs}`);
135
+
136
+ if (!r?.agents?.length) {
137
+ console.log('No agent activity data in this period.');
138
+ return;
139
+ }
140
+
141
+ console.log(`Agent availability (${r.dateRange?.start?.slice(0,10)} – ${r.dateRange?.end?.slice(0,10)})\n`);
142
+
143
+ for (const a of r.agents) {
144
+ const s = a.summary;
145
+ const parts = [];
146
+ if (s.availableSeconds > 0) parts.push(`Available ${fmtSec(s.availableSeconds)}`);
147
+ if (s.awaySeconds > 0) parts.push(`Away ${fmtSec(s.awaySeconds)}`);
148
+ if (s.offlineSeconds > 0) parts.push(`Offline ${fmtSec(s.offlineSeconds)}`);
149
+ if (s.onCallSeconds > 0) parts.push(`OnCall ${fmtSec(s.onCallSeconds)}`);
150
+
151
+ const alerts = [];
152
+ if (s.disconnectEvents > 0) alerts.push(`${s.disconnectEvents} disconnect(s)`);
153
+ if (s.tabHiddenEvents > 0) alerts.push(`${s.tabHiddenEvents} tab hidden`);
154
+
155
+ console.log(` ${a.agentName}${a.email ? ` <${a.email}>` : ''}`);
156
+ console.log(` ${parts.join(' | ') || 'no status data'}${alerts.length ? ' ⚠ ' + alerts.join(', ') : ''}`);
157
+
158
+ // Show last 5 status transitions
159
+ if (a.statusLog?.length) {
160
+ for (const e of a.statusLog.slice(-5)) {
161
+ const ts = new Date(e.startedAt).toISOString().slice(11, 16);
162
+ console.log(` → ${e.status.padEnd(9)} ${ts} UTC (${e.trigger}${e.durationSeconds != null ? ', ' + fmtSec(e.durationSeconds) : ''})`);
163
+ }
164
+ }
165
+
166
+ // Show recent disconnect events
167
+ const disconnects = (a.connectionEvents ?? []).filter(e => e.event?.toLowerCase().includes('disconnect'));
168
+ if (disconnects.length) {
169
+ console.log(` Disconnects: ${disconnects.map(e => new Date(e.occurredAt).toISOString().slice(11,16) + ' UTC').join(', ')}`);
170
+ }
171
+ console.log('');
172
+ }
173
+ }),
174
+ })
175
+ .command({
176
+ command: 'ai-summary',
177
+ describe: 'Generate a plain-English AI advisory for agent connectivity (max 7-day window, enterprise feature)',
178
+ builder: (y) => y
179
+ .option('start', { type: 'string', describe: 'Start date, e.g. 2026-06-10 (max 7 days before end)' })
180
+ .option('end', { type: 'string', describe: 'End date, e.g. 2026-06-17 (default: today)' })
181
+ .option('site-id', { type: 'number', describe: 'SuperAdmin only — run for a specific customer site' }),
182
+ handler: runCommand(async (argv) => {
183
+ const qs = new URLSearchParams();
184
+ if (argv.start) qs.set('start', argv.start);
185
+ if (argv.end) qs.set('end', argv.end);
186
+ if (argv['site-id']) qs.set('siteId', String(argv['site-id']));
187
+
188
+ console.log('Generating AI advisory (this may take a few seconds)…\n');
189
+ const r = await post(`/CallCenterAnalytics/AiSummary${qs.toString() ? '?' + qs : ''}`, {});
190
+
191
+ if (!r?.summary) {
192
+ console.log('No summary returned. Check that the date range contains data.');
193
+ return;
194
+ }
195
+
196
+ console.log(`Agent Connectivity AI Advisory`);
197
+ console.log(`Period: ${r.dateRange?.start?.slice(0,10)} – ${r.dateRange?.end?.slice(0,10)}`);
198
+ console.log(`Data: ${r.dataPoints?.missedCount ?? '?'} missed chats, ${r.dataPoints?.agentCount ?? '?'} agents analyzed`);
199
+ console.log('─'.repeat(60));
200
+ console.log(r.summary);
201
+ }),
202
+ })
203
+ .demandCommand(1, 'Specify a subcommand: list | set-status | missed-chats | availability | ai-summary'),
204
+ };
@@ -0,0 +1,193 @@
1
+ import fs from 'node:fs';
2
+ import { messagingGet, messagingPost, messagingPut } from '../api.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ // messagingGet() authenticates with the caller's own user JWT, scoped to the caller's site — fine
6
+ // for the subcommands above. staff-list below hits SupportTools/sites/{siteId}/ai-configs instead,
7
+ // which is gated by staff auth (Velaro staff JWT or the internal vel_live_* service key), never the
8
+ // caller's site JWT, so it can read ANY site's AI config without needing that site's own login.
9
+
10
+ const get = messagingGet;
11
+ const post = messagingPost;
12
+
13
+ export const aiConfigCommand = {
14
+ command: 'ai-config <subcommand>',
15
+ describe: 'Manage AI configurations (bot prompts, channel overrides)',
16
+ builder: (yargs) =>
17
+ yargs
18
+ .command(aiConfigListCommand)
19
+ .command(aiConfigGetCommand)
20
+ .command(aiConfigCreateCommand)
21
+ .command(aiConfigSetChannelOverrideCommand)
22
+ .command(aiConfigListChannelOverridesCommand)
23
+ .command(aiConfigStaffListCommand)
24
+ .demandCommand(1, 'Specify a subcommand: list, get, create, set-channel-override, list-channel-overrides, staff-list'),
25
+ handler: () => {},
26
+ };
27
+
28
+ // ── list ──────────────────────────────────────────────────────────────────────
29
+
30
+ const aiConfigListCommand = {
31
+ command: 'list',
32
+ describe: 'List all AI configurations on the current site',
33
+ handler: runCommand(async () => {
34
+ const configs = await get('/AIConfiguration/list');
35
+ const rows = configs ?? [];
36
+ if (!rows.length) { console.log('No AI configurations found.'); return; }
37
+
38
+ const idW = Math.max(...rows.map(c => String(c.id).length + 2));
39
+ const nameW = Math.max('name'.length, ...rows.map(c => (c.name ?? '').length));
40
+
41
+ console.log(`\n${'id'.padStart(idW)} ${'name'.padEnd(nameW)} status model`);
42
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(nameW)} ------- -----`);
43
+ for (const c of rows) {
44
+ const id = `[${c.id}]`.padStart(idW);
45
+ const name = (c.name ?? '').padEnd(nameW);
46
+ const stat = (c.status ?? '—').padEnd(7);
47
+ console.log(`${id} ${name} ${stat} ${c.aiModel ?? '—'}`);
48
+ }
49
+ console.log(`\n${rows.length} configuration(s)`);
50
+ }),
51
+ };
52
+
53
+ // ── get ───────────────────────────────────────────────────────────────────────
54
+
55
+ const aiConfigGetCommand = {
56
+ command: 'get <id>',
57
+ describe: 'Show one AI configuration',
58
+ builder: (y) => y.positional('id', { type: 'number', describe: 'AI configuration ID' }),
59
+ handler: runCommand(async (argv) => {
60
+ const all = (await get('/AIConfiguration/list')) ?? [];
61
+ const c = all.find(x => x.id === argv.id);
62
+ if (!c) { console.error(`AI configuration ${argv.id} not found.`); process.exit(1); }
63
+ console.log(`\n[${c.id}] ${c.name}`);
64
+ console.log(` Status: ${c.status ?? '—'}`);
65
+ console.log(` Model: ${c.aiModel ?? '—'}`);
66
+ console.log(` Language: ${c.language ?? '—'}`);
67
+ console.log(` Tone: ${c.tone ?? '—'}`);
68
+ console.log(` Index: ${c.indexName ?? '—'}`);
69
+ if (c.prompt) {
70
+ console.log(`\n Prompt:`);
71
+ console.log(c.prompt.split('\n').map(l => ' ' + l).join('\n'));
72
+ }
73
+ }),
74
+ };
75
+
76
+ // ── create ────────────────────────────────────────────────────────────────────
77
+
78
+ const aiConfigCreateCommand = {
79
+ command: 'create',
80
+ describe: 'Create an AI configuration. Provide --name + --prompt, or --file path/to/config.json',
81
+ builder: (y) =>
82
+ y
83
+ .option('name', { type: 'string', describe: 'Configuration name' })
84
+ .option('prompt', { type: 'string', describe: 'System prompt text' })
85
+ .option('model', { type: 'string', describe: 'Model (e.g. gpt-4o-mini, claude-haiku-4-5)' })
86
+ .option('tone', { type: 'string', describe: 'Tone (e.g. friendly, professional)' })
87
+ .option('language', { type: 'string', default: 'en', describe: 'Language code' })
88
+ .option('reply-length',{ type: 'string', describe: 'Reply length (e.g. short, medium, long)' })
89
+ .option('index', { type: 'string', describe: 'KB index name to use for retrieval' })
90
+ .option('temperature', { type: 'number', describe: 'Sampling temperature (0–2)' })
91
+ .option('file', { type: 'string', describe: 'JSON file with AIConfigurationViewModel body (overrides flags)' }),
92
+ handler: runCommand(async (argv) => {
93
+ let body;
94
+ if (argv.file) {
95
+ body = JSON.parse(fs.readFileSync(argv.file, 'utf8'));
96
+ if (body.id == null) body.id = 0;
97
+ } else {
98
+ if (!argv.name) { console.error('--name is required (or use --file).'); process.exit(1); }
99
+ if (!argv.prompt) { console.error('--prompt is required (or use --file).'); process.exit(1); }
100
+ body = {
101
+ id: 0,
102
+ name: argv.name,
103
+ prompt: argv.prompt,
104
+ aiModel: argv.model ?? null,
105
+ tone: argv.tone ?? null,
106
+ language: argv.language,
107
+ replyLength: argv['reply-length'] ?? null,
108
+ indexName: argv.index ?? null,
109
+ temperature: argv.temperature ?? null,
110
+ dataUrls: [],
111
+ };
112
+ }
113
+
114
+ const result = await post('/AIConfiguration', body);
115
+ const id = result?.id ?? result;
116
+ console.log(`AI configuration created: ${id}`);
117
+ console.log(`View: velaro ai-config get ${id}`);
118
+ }),
119
+ };
120
+
121
+ // ── channel overrides ─────────────────────────────────────────────────────────
122
+
123
+ const CHANNELS = ['Web', 'TwilioSms', 'TwilioIvr', 'WhatsApp', 'Facebook', 'Twitter', 'Email', 'Apple'];
124
+ const MODES = ['additive', 'replace', 'suppress'];
125
+
126
+ const aiConfigListChannelOverridesCommand = {
127
+ command: 'list-channel-overrides <id>',
128
+ describe: 'List channel prompt overrides on an AI configuration',
129
+ builder: (y) => y.positional('id', { type: 'number', describe: 'AI configuration ID' }),
130
+ handler: runCommand(async (argv) => {
131
+ const list = await get(`/AIConfiguration/${argv.id}/channel-overrides`);
132
+ if (!list?.length) { console.log('No channel overrides set.'); return; }
133
+ console.log(`\nchannel mode text`);
134
+ console.log(`------------- --------- ----`);
135
+ for (const o of list) {
136
+ const ch = (o.conversationSource ?? '').padEnd(13);
137
+ const mode = (o.mode ?? '').padEnd(9);
138
+ const txt = (o.overrideText ?? '').slice(0, 60);
139
+ console.log(`${ch} ${mode} ${txt}`);
140
+ }
141
+ console.log(`\n${list.length} override(s)`);
142
+ }),
143
+ };
144
+
145
+ // ── staff-list (SupportTools, service-authed, any site) ───────────────────────
146
+
147
+ const aiConfigStaffListCommand = {
148
+ command: 'staff-list <siteId>',
149
+ describe: 'Velaro staff only — read AI configurations for ANY site by ID (including IndexDocumentCount), without needing that site\'s own user JWT.',
150
+ builder: (y) => y.positional('siteId', { type: 'number', describe: 'Site ID to inspect' }),
151
+ handler: runCommand(async (argv) => {
152
+ const data = await get(`/SupportTools/sites/${argv.siteId}/ai-configs`);
153
+ const rows = data?.configs ?? [];
154
+ if (!rows.length) { console.log(`No AI configurations found for site ${argv.siteId}.`); return; }
155
+
156
+ const idW = Math.max(...rows.map(c => String(c.id).length + 2));
157
+ const nameW = Math.max('name'.length, ...rows.map(c => (c.name ?? '').length));
158
+
159
+ console.log(`\nSite ${argv.siteId} — ${rows.length} AI configuration(s)\n`);
160
+ console.log(`${'id'.padStart(idW)} ${'name'.padEnd(nameW)} status model index docs`);
161
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(nameW)} ------- ------------ ------------------- ----`);
162
+ for (const c of rows) {
163
+ const id = `[${c.id}]`.padStart(idW);
164
+ const name = (c.name ?? '').padEnd(nameW);
165
+ const stat = (c.status ?? '—').padEnd(7);
166
+ const model = (c.aiModel ?? '—').padEnd(12);
167
+ const index = (c.indexName ?? '—').padEnd(19);
168
+ console.log(`${id} ${name} ${stat} ${model} ${index} ${c.indexDocumentCount ?? 0}`);
169
+ }
170
+ }),
171
+ };
172
+
173
+ const aiConfigSetChannelOverrideCommand = {
174
+ command: 'set-channel-override <id>',
175
+ describe: 'Add or replace a channel override on an AI configuration',
176
+ builder: (y) =>
177
+ y
178
+ .positional('id', { type: 'number', describe: 'AI configuration ID' })
179
+ .option('channel', { type: 'string', choices: CHANNELS, demandOption: true, describe: 'Conversation source (PascalCase)' })
180
+ .option('mode', { type: 'string', choices: MODES, demandOption: true, describe: 'Override mode' })
181
+ .option('text', { type: 'string', describe: 'Override prompt text (omit for suppress mode)' }),
182
+ handler: runCommand(async (argv) => {
183
+ if (argv.mode !== 'suppress' && !argv.text) {
184
+ console.error('--text is required for additive/replace modes.');
185
+ process.exit(1);
186
+ }
187
+ const existing = (await get(`/AIConfiguration/${argv.id}/channel-overrides`)) ?? [];
188
+ const others = existing.filter(o => o.conversationSource !== argv.channel);
189
+ const next = [...others, { conversationSource: argv.channel, mode: argv.mode, overrideText: argv.text ?? '' }];
190
+ await messagingPut(`/AIConfiguration/${argv.id}/channel-overrides`, next);
191
+ console.log(`Channel override set: ai-config=${argv.id} channel=${argv.channel} mode=${argv.mode}`);
192
+ }),
193
+ };
@@ -0,0 +1,159 @@
1
+ import { getCredentials } from '../api.js';
2
+ import { readConfig, ENVS } from '../config.js';
3
+
4
+ // -- helpers -------------------------------------------------------------------
5
+
6
+ const RESET = '\x1b[0m';
7
+ const DIM = '\x1b[2m';
8
+ const BOLD = '\x1b[1m';
9
+ const GREEN = '\x1b[32m';
10
+ const YEL = '\x1b[33m';
11
+ const CYN = '\x1b[36m';
12
+
13
+ async function getMsgBase(argv) {
14
+ const cfg = readConfig();
15
+ const env = argv.env || cfg.env || 'staging';
16
+ return cfg.envs?.[env]?.messagingApiBase ?? ENVS[env]?.messagingApiBase
17
+ ?? 'https://velaro-messaging-api-staging.azurewebsites.net';
18
+ }
19
+
20
+ async function requireAdmin(creds) {
21
+ if (creds.siteId !== 1032) {
22
+ console.error('velaro ai-models is only available to Velaro staff (site 1032).');
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ async function callResolver(argv, path, opts = {}) {
28
+ const creds = await getCredentials();
29
+ await requireAdmin(creds);
30
+ const msgBase = await getMsgBase(argv);
31
+
32
+ const res = await fetch(`${msgBase}/superadmin/ai-models/${path}`, {
33
+ method: opts.method ?? 'GET',
34
+ headers: {
35
+ Authorization: `Bearer ${creds.velaroToken}`,
36
+ 'X-Internal-SiteId': String(creds.siteId),
37
+ ...(opts.body ? { 'Content-Type': 'application/json' } : {}),
38
+ },
39
+ body: opts.body ? JSON.stringify(opts.body) : undefined,
40
+ signal: AbortSignal.timeout(20000),
41
+ });
42
+
43
+ if (!res.ok) {
44
+ const body = await res.text().catch(() => '');
45
+ console.error(`HTTP ${res.status}: ${body}`);
46
+ process.exit(1);
47
+ }
48
+ return res.json();
49
+ }
50
+
51
+ // -- resolver-map (the full "what is everything actually using" dashboard) ----
52
+
53
+ async function showResolverMap(argv) {
54
+ const data = await callResolver(argv, 'resolver-map');
55
+ const roles = argv.role
56
+ ? data.roles.filter(r => r.role === argv.role)
57
+ : data.roles;
58
+
59
+ if (argv.role && !roles.length) {
60
+ console.error(`Unknown role '${argv.role}'. Known roles: ${data.roles.map(r => r.role).join(', ')}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log(`\n${BOLD}AI Model Resolver, ${argv.role ? `role: ${argv.role}` : 'all roles'}${RESET}\n`);
65
+ for (const r of roles) {
66
+ console.log(`${CYN}${r.role}${RESET} ${DIM}(code default: ${r.codeDefault})${RESET}`);
67
+ console.log(` global: ${r.globalDefault ?? `${DIM}(none, falls back to code default)${RESET}`}`);
68
+ const activeTiers = r.tierDefaults.filter(t => t.model);
69
+ if (activeTiers.length) {
70
+ for (const t of activeTiers) console.log(` tier: ${t.tier.padEnd(12)} -> ${t.model}`);
71
+ }
72
+ if (r.siteOverrideCount > 0) {
73
+ console.log(` ${YEL}site overrides (${r.siteOverrideCount}):${RESET}`);
74
+ for (const o of r.siteOverrides) console.log(` site ${o.siteId}: ${o.model} ${DIM}(${o.updatedBy}, ${o.updatedAt})${RESET}`);
75
+ }
76
+ console.log('');
77
+ }
78
+
79
+ if (data.unrecognizedTierRows?.length) {
80
+ console.log(`${YEL}WARNING: unrecognized tier rows (legacy format, should self-migrate on next deploy):${RESET}`);
81
+ for (const row of data.unrecognizedTierRows) console.log(` ${row.key} = ${row.value}`);
82
+ console.log('');
83
+ }
84
+ }
85
+
86
+ // -- effective (what a specific site actually resolves to, per role) ----------
87
+
88
+ async function showEffective(argv) {
89
+ if (!argv.site) {
90
+ console.error('--site is required, e.g. velaro ai-models effective --site 1032');
91
+ process.exit(1);
92
+ }
93
+ const data = await callResolver(argv, `site/${argv.site}/effective`);
94
+ console.log(`\n${BOLD}Effective models, site ${argv.site}${RESET}\n`);
95
+ for (const r of data) {
96
+ const marker = r.isOverridden ? `${GREEN}[override]${RESET}` : `${DIM}[default] ${RESET}`;
97
+ console.log(`${marker} ${r.role.padEnd(28)} ${r.model}${r.isOverridden ? `${DIM} (code default: ${r.codeDefault})${RESET}` : ''}`);
98
+ }
99
+ console.log(`\n${DIM}[override] = overridden from code default [default] = using code default${RESET}\n`);
100
+ }
101
+
102
+ // -- set / clear a role-scoped tier default ------------------------------------
103
+
104
+ async function setTierDefault(argv) {
105
+ const data = await callResolver(argv, `tier/${argv.tier}/${argv.role}`, {
106
+ method: 'POST',
107
+ body: { model: argv.model, updatedBy: 'velaro-cli' },
108
+ });
109
+ console.log(`\n${GREEN}OK${RESET} Tier '${data.tier}' role '${data.role}' set to ${data.model}\n`);
110
+ }
111
+
112
+ async function clearTierDefault(argv) {
113
+ await callResolver(argv, `tier/${argv.tier}/${argv.role}`, { method: 'DELETE' });
114
+ console.log(`\n${GREEN}OK${RESET} Cleared tier '${argv.tier}' role '${argv.role}'. Reverts to global default.\n`);
115
+ }
116
+
117
+ // -- command export -------------------------------------------------------------
118
+
119
+ export const aiModelsCommand = {
120
+ command: 'ai-models <subcommand>',
121
+ describe: 'AI model resolver diagnostics (Velaro staff only)',
122
+ builder: yargs => yargs
123
+ .command({
124
+ command: 'resolver [role]',
125
+ describe: 'Show the full resolver map: code default, global default, tier defaults, and every site override, per role',
126
+ builder: y => y
127
+ .positional('role', { describe: 'Show only this role (e.g. ivr, chatbot, skill_synthesis)', type: 'string' })
128
+ .option('env', { describe: 'staging or prod', type: 'string' }),
129
+ handler: showResolverMap,
130
+ })
131
+ .command({
132
+ command: 'effective',
133
+ describe: 'Show what a specific site actually resolves to for every role',
134
+ builder: y => y
135
+ .option('site', { describe: 'Site ID', type: 'number', demandOption: true })
136
+ .option('env', { describe: 'staging or prod', type: 'string' }),
137
+ handler: showEffective,
138
+ })
139
+ .command({
140
+ command: 'set-tier <tier> <role> <model>',
141
+ describe: 'Set a role-scoped tier default, e.g. velaro ai-models set-tier enterprise ivr claude-sonnet-4-6',
142
+ builder: y => y
143
+ .positional('tier', { describe: 'starter, professional, or enterprise', type: 'string' })
144
+ .positional('role', { describe: 'e.g. ivr, chatbot, skill_synthesis', type: 'string' })
145
+ .positional('model', { describe: 'Model alias/ID', type: 'string' })
146
+ .option('env', { describe: 'staging or prod', type: 'string' }),
147
+ handler: setTierDefault,
148
+ })
149
+ .command({
150
+ command: 'clear-tier <tier> <role>',
151
+ describe: 'Clear a role-scoped tier default, reverting to the global default',
152
+ builder: y => y
153
+ .positional('tier', { describe: 'starter, professional, or enterprise', type: 'string' })
154
+ .positional('role', { describe: 'e.g. ivr, chatbot, skill_synthesis', type: 'string' })
155
+ .option('env', { describe: 'staging or prod', type: 'string' }),
156
+ handler: clearTierDefault,
157
+ })
158
+ .demandCommand(1, 'Specify a subcommand: resolver, effective, set-tier, or clear-tier'),
159
+ };