@velaro/cli 1.4.99 → 1.4.101

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
@@ -15,6 +15,7 @@ import { knowledgeAdvisorCommand } from '../lib/commands/knowledge-advisor.js';
15
15
  import { productCommand } from '../lib/commands/product.js';
16
16
  import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
17
17
  import { opsCommand } from '../lib/commands/ops.js';
18
+ import { cobrowseCommand } from '../lib/commands/cobrowse.js';
18
19
  import { deployStateCommand } from '../lib/commands/deploy-state.js';
19
20
  import { checkCommand } from '../lib/commands/check.js';
20
21
  import { logsCommand } from '../lib/commands/logs.js';
@@ -151,6 +152,7 @@ await yargs(hideBin(process.argv))
151
152
  .command(productCommand)
152
153
  .command(mcpKeyCommand)
153
154
  .command(opsCommand)
155
+ .command(cobrowseCommand)
154
156
  .command(deployStateCommand)
155
157
  .command(checkCommand)
156
158
  .command(logsCommand)
@@ -0,0 +1,90 @@
1
+ // Pure helpers for `velaro cobrowse ...` (kept free of I/O so they are unit-testable).
2
+ //
3
+ // The platform API (messaging: GET/PUT superadmin/sites/{siteId}/cobrowse...) has REPLACE semantics: every setting
4
+ // that is not sent goes back to the platform default. For a CLI that is a footgun, so `set` first reads the current
5
+ // raw settings and only overwrites what the operator named on the command line.
6
+
7
+ export const TRI_FIELDS = [
8
+ ['cues', 'cuesEnabled'],
9
+ ['formFill', 'formFillEnabled'],
10
+ ['strictMask', 'strictMaskMode'],
11
+ ['disableMobile', 'disableOnMobile'],
12
+ ];
13
+
14
+ /** "on" / "off" / "default" (also true/false) -> true / false / null. Throws on anything else. */
15
+ export function parseTri(value, flag) {
16
+ if (value === undefined) return undefined;
17
+ const v = String(value).trim().toLowerCase();
18
+ if (['on', 'true', 'yes', '1'].includes(v)) return true;
19
+ if (['off', 'false', 'no', '0'].includes(v)) return false;
20
+ if (['default', 'inherit', 'null', ''].includes(v)) return null;
21
+ throw new Error(`--${flag} must be on, off or default (got "${value}")`);
22
+ }
23
+
24
+ function parseIntOrDefault(value, flag) {
25
+ if (value === undefined) return undefined;
26
+ const v = String(value).trim().toLowerCase();
27
+ if (v === 'default' || v === '') return null;
28
+ const n = Number(v);
29
+ if (!Number.isInteger(n)) throw new Error(`--${flag} must be a whole number of minutes or "default" (got "${value}")`);
30
+ return n;
31
+ }
32
+
33
+ function parseTextOrDefault(value) {
34
+ if (value === undefined) return undefined;
35
+ return String(value).trim().toLowerCase() === 'default' ? null : String(value);
36
+ }
37
+
38
+ /**
39
+ * Merge operator flags over the site's current raw settings. Returns the full request body to PUT.
40
+ * Protected pages: "default" -> platform list, "none" -> no protected pages, otherwise a newline/comma list.
41
+ */
42
+ export function buildCobrowseUpdate(current, args) {
43
+ const body = { ...(current ?? {}) };
44
+ delete body.updatedAt;
45
+
46
+ for (const [flag, field] of TRI_FIELDS) {
47
+ const v = parseTri(args[flag], flag);
48
+ if (v !== undefined) body[field] = v;
49
+ }
50
+
51
+ if (args.protectedPages !== undefined) {
52
+ const raw = String(args.protectedPages).trim();
53
+ const lower = raw.toLowerCase();
54
+ body.blockedUrlPatterns = lower === 'default' ? null : lower === 'none' ? '' : raw.split(/[\n,]/).map(s => s.trim()).filter(Boolean).join('\n');
55
+ }
56
+
57
+ const max = parseIntOrDefault(args.maxMinutes, 'max-minutes');
58
+ if (max !== undefined) body.maxSessionMinutes = max;
59
+ const idle = parseIntOrDefault(args.idleMinutes, 'idle-minutes');
60
+ if (idle !== undefined) body.idleStopMinutes = idle;
61
+
62
+ const title = parseTextOrDefault(args.consentTitle);
63
+ if (title !== undefined) body.consentTitle = title;
64
+ const message = parseTextOrDefault(args.consentMessage);
65
+ if (message !== undefined) body.consentMessage = message;
66
+ const allow = parseTextOrDefault(args.consentAllow);
67
+ if (allow !== undefined) body.consentAllowLabel = allow;
68
+
69
+ const teams = parseTextOrDefault(args.teams);
70
+ if (teams !== undefined) body.allowedTeamIds = teams === 'none' ? null : teams;
71
+ const roles = parseTextOrDefault(args.roles);
72
+ if (roles !== undefined) body.allowedRoles = roles === 'none' ? null : roles;
73
+
74
+ return body;
75
+ }
76
+
77
+ export function describeEffective(e) {
78
+ if (!e) return [];
79
+ return [
80
+ ['Agent cues', e.cuesEnabled ? 'on' : 'off'],
81
+ ['Guided form fill', e.formFillEnabled ? 'on' : 'off'],
82
+ ['Strict mask mode', e.strictMask ? 'on' : 'off'],
83
+ ['Off on mobile', e.disableOnMobile ? 'yes' : 'no'],
84
+ ['Protected pages', (e.blockedUrlPatterns ?? []).length ? e.blockedUrlPatterns.join(' ') : 'none'],
85
+ ['Max session / idle stop', `${e.maxSessionMinutes} min / ${e.idleStopMinutes} min`],
86
+ ['Who can start', (e.allowedTeamIds ?? []).length || (e.allowedRoles ?? []).length
87
+ ? `teams [${(e.allowedTeamIds ?? []).join(', ')}] roles [${(e.allowedRoles ?? []).join(', ')}]`
88
+ : 'everyone with a seat'],
89
+ ];
90
+ }
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { buildCobrowseUpdate, parseTri, describeEffective } from './cobrowse-settings.js';
4
+
5
+ test('parseTri maps on/off/default and rejects anything else', () => {
6
+ assert.equal(parseTri('on', 'cues'), true);
7
+ assert.equal(parseTri('OFF', 'cues'), false);
8
+ assert.equal(parseTri('default', 'cues'), null);
9
+ assert.equal(parseTri(undefined, 'cues'), undefined);
10
+ assert.throws(() => parseTri('maybe', 'cues'), /--cues must be on, off or default/);
11
+ });
12
+
13
+ test('set only changes the flags named; everything else is preserved (the API itself replaces)', () => {
14
+ const current = { cuesEnabled: true, strictMaskMode: true, maxSessionMinutes: 30, blockedUrlPatterns: '/a\n/b', updatedAt: '2026-10-15T00:00:00Z' };
15
+ const body = buildCobrowseUpdate(current, { formFill: 'on', maxMinutes: '60' });
16
+ assert.deepEqual(body, { cuesEnabled: true, strictMaskMode: true, maxSessionMinutes: 60, blockedUrlPatterns: '/a\n/b', formFillEnabled: true });
17
+ });
18
+
19
+ test('default clears a setting back to the platform default', () => {
20
+ const body = buildCobrowseUpdate({ cuesEnabled: false, maxSessionMinutes: 30 }, { cues: 'default', maxMinutes: 'default' });
21
+ assert.equal(body.cuesEnabled, null);
22
+ assert.equal(body.maxSessionMinutes, null);
23
+ });
24
+
25
+ test('protected pages: default / none / explicit list', () => {
26
+ assert.equal(buildCobrowseUpdate({}, { protectedPages: 'default' }).blockedUrlPatterns, null);
27
+ assert.equal(buildCobrowseUpdate({}, { protectedPages: 'none' }).blockedUrlPatterns, '');
28
+ assert.equal(buildCobrowseUpdate({}, { protectedPages: '/checkout, /portal/pay ' }).blockedUrlPatterns, '/checkout\n/portal/pay');
29
+ });
30
+
31
+ test('teams and roles accept "none"', () => {
32
+ const body = buildCobrowseUpdate({ allowedTeamIds: '1,2', allowedRoles: 'Manager' }, { teams: 'none', roles: 'Agent' });
33
+ assert.equal(body.allowedTeamIds, null);
34
+ assert.equal(body.allowedRoles, 'Agent');
35
+ });
36
+
37
+ test('non-integer minutes are rejected before anything is sent', () => {
38
+ assert.throws(() => buildCobrowseUpdate({}, { maxMinutes: 'soon' }), /whole number/);
39
+ });
40
+
41
+ test('describeEffective renders a readable summary', () => {
42
+ const rows = describeEffective({ cuesEnabled: true, formFillEnabled: false, strictMask: false, disableOnMobile: false, blockedUrlPatterns: ['/checkout'], maxSessionMinutes: 120, idleStopMinutes: 15, allowedTeamIds: [], allowedRoles: [] });
43
+ assert.ok(rows.some(([k, v]) => k === 'Protected pages' && v === '/checkout'));
44
+ assert.ok(rows.some(([k, v]) => k === 'Who can start' && v === 'everyone with a seat'));
45
+ });
@@ -0,0 +1,99 @@
1
+ import { messagingGet, messagingPut } from '../api.js';
2
+ import { buildCobrowseUpdate, describeEffective } from '../cobrowse-settings.js';
3
+
4
+ // velaro cobrowse show|set|reset|sessions <siteId>
5
+ //
6
+ // Staff view and control of a site's Co-Browse configuration (what the plan entitles it to, what the site switched
7
+ // on, the effective values after platform defaults and hard caps, and recent session metadata). Goes through the
8
+ // messaging SuperAdmin endpoints, which share ONE validated service with the customer admin UI and the MCP tools,
9
+ // so the CLI cannot store anything the UI cannot, and every change lands in the site's Activity Log.
10
+
11
+ const RESET = '\x1b[0m';
12
+ const BOLD = '\x1b[1m';
13
+ const DIM = '\x1b[2m';
14
+ const GRN = '\x1b[32m';
15
+ const RED = '\x1b[31m';
16
+
17
+ function fmtDate(iso) {
18
+ if (!iso) return '-';
19
+ return new Date(iso).toLocaleString('en-US', { timeZone: 'America/Los_Angeles', dateStyle: 'short', timeStyle: 'short' });
20
+ }
21
+
22
+ const siteArg = (y) => y.positional('siteId', { type: 'number', describe: 'Site id' });
23
+
24
+ function printSite(data) {
25
+ const ent = data.entitlements ?? {};
26
+ const chip = (on) => (on ? `${GRN}yes${RESET}` : `${RED}no${RESET}`);
27
+ console.log(`\n${BOLD}Co-Browse - site ${data.siteId}${RESET}`);
28
+ console.log(` Entitled: Co-Browse ${chip(ent.coBrowse)} Plus cues ${chip(ent.cues)} Plus form fill ${chip(ent.formFill)} Plus redaction ${chip(ent.enhancedPiiRedaction)}`);
29
+ console.log(`\n${BOLD}Effective settings${RESET} ${DIM}(null in the raw settings = platform default)${RESET}`);
30
+ for (const [k, v] of describeEffective(data.effective)) console.log(` ${k.padEnd(26)} ${v}`);
31
+ }
32
+
33
+ const showCommand = {
34
+ command: 'show <siteId>',
35
+ describe: 'Show entitlement, settings and recent sessions for a site',
36
+ builder: siteArg,
37
+ handler: async (argv) => {
38
+ const data = await messagingGet(`/superadmin/sites/${argv.siteId}/cobrowse`);
39
+ if (argv.json) return console.log(JSON.stringify(data, null, 2));
40
+ printSite(data);
41
+ console.log(`\n${BOLD}Recent sessions${RESET} ${DIM}(metadata only, Pacific time)${RESET}`);
42
+ const rows = data.recentSessions ?? [];
43
+ if (rows.length === 0) console.log(' none');
44
+ for (const s of rows) {
45
+ console.log(` ${fmtDate(s.startedAt).padEnd(18)} conv #${String(s.conversationId).padEnd(8)} agent ${String(s.initiatedByUserId ?? '-').padEnd(7)} ${String(s.durationSeconds ?? '-').padStart(5)}s ${s.outcome ?? '-'}${s.endReason ? ` (${s.endReason})` : ''}`);
46
+ }
47
+ },
48
+ };
49
+
50
+ const setCommand = {
51
+ command: 'set <siteId>',
52
+ describe: 'Change Co-Browse settings for a site (only the flags you pass change)',
53
+ builder: (y) => siteArg(y)
54
+ .option('cues', { type: 'string', describe: 'Agent pointer/highlight/annotation (Plus): on | off | default' })
55
+ .option('form-fill', { type: 'string', describe: 'Guided form fill (Plus): on | off | default', alias: 'formFill' })
56
+ .option('strict-mask', { type: 'string', describe: 'Strict mask mode: on | off | default', alias: 'strictMask' })
57
+ .option('disable-mobile', { type: 'string', describe: 'Turn off on mobile: on | off | default', alias: 'disableMobile' })
58
+ .option('protected-pages', { type: 'string', describe: 'URL patterns (newline/comma separated), "default" or "none"', alias: 'protectedPages' })
59
+ .option('max-minutes', { type: 'string', describe: 'Max session minutes (5-240) or default', alias: 'maxMinutes' })
60
+ .option('idle-minutes', { type: 'string', describe: 'Idle auto-stop minutes (2-60) or default', alias: 'idleMinutes' })
61
+ .option('consent-title', { type: 'string', alias: 'consentTitle' })
62
+ .option('consent-message', { type: 'string', alias: 'consentMessage' })
63
+ .option('consent-allow', { type: 'string', alias: 'consentAllow' })
64
+ .option('teams', { type: 'string', describe: 'Comma separated team ids allowed to start, or "none"' })
65
+ .option('roles', { type: 'string', describe: 'Comma separated roles allowed to start (Administrator, Manager, Agent), or "none"' }),
66
+ handler: async (argv) => {
67
+ const current = await messagingGet(`/superadmin/sites/${argv.siteId}/cobrowse`);
68
+ const body = buildCobrowseUpdate(current.settings, argv);
69
+ const saved = await messagingPut(`/superadmin/sites/${argv.siteId}/cobrowse-settings`, body);
70
+ if (argv.json) return console.log(JSON.stringify(saved, null, 2));
71
+ console.log(`${GRN}Saved.${RESET} Effective settings now:`);
72
+ for (const [k, v] of describeEffective(saved.effective)) console.log(` ${k.padEnd(26)} ${v}`);
73
+ },
74
+ };
75
+
76
+ const resetCommand = {
77
+ command: 'reset <siteId>',
78
+ describe: 'Reset every Co-Browse setting for a site to the platform defaults',
79
+ builder: siteArg,
80
+ handler: async (argv) => {
81
+ const saved = await messagingPut(`/superadmin/sites/${argv.siteId}/cobrowse-settings`, {});
82
+ if (argv.json) return console.log(JSON.stringify(saved, null, 2));
83
+ console.log(`${GRN}Reset to platform defaults.${RESET}`);
84
+ for (const [k, v] of describeEffective(saved.effective)) console.log(` ${k.padEnd(26)} ${v}`);
85
+ },
86
+ };
87
+
88
+ export const cobrowseCommand = {
89
+ command: 'cobrowse <subcommand>',
90
+ describe: 'View and control a site\'s Co-Browse settings and session history',
91
+ builder: (y) => y
92
+ .option('json', { type: 'boolean', describe: 'Print raw JSON' })
93
+ .command(showCommand)
94
+ .command(setCommand)
95
+ .command(resetCommand)
96
+ .demandCommand(1, 'Specify a subcommand: show, set, reset')
97
+ .strict(),
98
+ handler: () => {},
99
+ };
@@ -12,7 +12,8 @@ export const migrateCommand = {
12
12
  .command(migrateTeamsCommand)
13
13
  .command(migrateWorkflowsCommand)
14
14
  .command(migrateRulesCommand)
15
- .demandCommand(1, 'Specify a subcommand: export, livefluence, url-rules, teams, workflows, rules'),
15
+ .command(migrateAdviseCommand)
16
+ .demandCommand(1, 'Specify a subcommand: export, livefluence, url-rules, teams, workflows, rules, advise'),
16
17
  handler: () => {},
17
18
  };
18
19
 
@@ -321,3 +322,52 @@ const migrateRulesCommand = {
321
322
  .demandCommand(1, 'Specify: list, set-enabled'),
322
323
  handler: () => {},
323
324
  };
325
+
326
+ // ── conversion advisor: consolidated v20 design from a v10 site or an intent list ──
327
+
328
+ const migrateAdviseCommand = {
329
+ command: 'advise',
330
+ describe: 'Livefluence conversion advisor: recommend (dry run, default) or build (--apply) a consolidated v20 routing design. Staff only.',
331
+ builder: (y) =>
332
+ y
333
+ .option('v10-site-id', { type: 'number', alias: 'v', describe: 'V10 (Livefluence) site to read (omit when using --intents)' })
334
+ .option('v20-site-id', { type: 'number', alias: 's', describe: 'V20 destination site ID', demandOption: true })
335
+ .option('intents', { type: 'string', describe: 'JSON array (or @file.json) of intents: [{"intent":"support","paths":["/support"],"device":"any","target":"Service team + troubleshooting AI node"}]' })
336
+ .option('apply', { type: 'boolean', default: false, describe: 'Create missing teams and DISABLED workflows (default: dry run + diff)' })
337
+ .option('include-non-routing', { type: 'boolean', default: false, describe: 'Also read v10 rules with IsRouting=false (group-assignment rules)' }),
338
+ handler: runCommand(async (argv) => {
339
+ let intents = null;
340
+ if (argv.intents) {
341
+ const raw = argv.intents.startsWith('@')
342
+ ? (await import('node:fs')).readFileSync(argv.intents.slice(1), 'utf8')
343
+ : argv.intents;
344
+ intents = JSON.parse(raw);
345
+ }
346
+ if (!intents && !argv['v10-site-id']) throw new Error('Give -v <v10 site> or --intents.');
347
+
348
+ const result = await post('/DatabaseTool/ConversionAdvisor', {
349
+ v10SiteId: intents ? null : argv['v10-site-id'],
350
+ v20SiteId: argv['v20-site-id'],
351
+ apply: argv.apply,
352
+ includeNonRouting: argv['include-non-routing'],
353
+ intents,
354
+ });
355
+
356
+ console.log(`\n${'─'.repeat(60)}`);
357
+ console.log(` Conversion advisor (${result.source}) -> V20 site ${result.v20SiteId} ${result.dryRun ? 'DRY RUN' : 'APPLIED (all workflows disabled)'}`);
358
+ if (result.v10RuleCount) console.log(` ${result.v10RuleCount} v10 rule(s) -> ${result.consolidatedWorkflows} workflow(s)`);
359
+ console.log(`${'─'.repeat(60)}\n`);
360
+ for (const t of result.teams ?? [])
361
+ console.log(` team ${t.status.padEnd(8)} ${t.name}${t.v10Members ? ` (${t.v10Members} v10 members, assign v20 agents by hand)` : ''}`);
362
+ console.log('');
363
+ for (const w of result.workflows ?? []) {
364
+ console.log(` [${w.status}] #${w.priority} ${w.name} mode=${w.mode}${w.device !== 'any' ? ` device=${w.device}` : ''}`);
365
+ console.log(` team: ${w.teamName ?? '-'} AI node: ${w.aiNodeName ?? '-'} condition groups (OR): ${w.conditionGroups}`);
366
+ if (w.statusReason) console.log(` ${w.statusReason}`);
367
+ for (const x of w.warnings ?? []) console.log(` ! ${x}`);
368
+ }
369
+ if (result.existingNotInPlan?.length) console.log(`\n Already on the v20 site, not in this plan: ${result.existingNotInPlan.join(', ')}`);
370
+ for (const n of result.notes ?? []) console.log(` note: ${n}`);
371
+ console.log(`\n ${result.message}\n`);
372
+ }),
373
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "1.4.99",
3
+ "version": "1.4.101",
4
4
  "description": "Velaro Workspace v20 — command-line interface for managing bots, knowledge base ingestion, MCP API keys, search indexes, and ops health.",
5
5
  "type": "module",
6
6
  "bin": {