@velaro/cli 1.4.65 → 1.4.67

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.
@@ -12,15 +12,18 @@ export const experienceImpactCommand = {
12
12
  .option('from', { type: 'string', describe: 'yyyy-MM-dd (default: 7 days ago)' })
13
13
  .option('to', { type: 'string', describe: 'yyyy-MM-dd (default: today)' })
14
14
  .option('team-id', { type: 'number', describe: 'Narrow to one team' })
15
+ .option('include-demo', { type: 'boolean', default: false, describe: 'Show only the seeded sample (demo) data instead of real conversations' })
15
16
  .option('json', { type: 'boolean', default: false, describe: 'Raw JSON output' }),
16
17
  handler: runCommand(async (argv) => {
17
18
  const qs = new URLSearchParams();
18
19
  if (argv.from) qs.set('from', argv.from);
19
20
  if (argv.to) qs.set('to', argv.to);
20
21
  if (argv.teamId) qs.set('teamId', String(argv.teamId));
22
+ if (argv.includeDemo) qs.set('includeDemo', 'true');
21
23
  const q = qs.toString();
22
24
  const d = await messagingGet(`/Reports/experience-impact${q ? `?${q}` : ''}`);
23
25
  if (argv.json) { console.log(JSON.stringify(d, null, 2)); return; }
26
+ if (d.demoAvailable && !argv.includeDemo) console.log('(Sample data is available for this range: re-run with --include-demo)');
24
27
  const s = d.summary ?? {};
25
28
  console.log(`\nExperience Impact ${d.range?.from}..${d.range?.to}`);
26
29
  console.log(` Experience index: ${s.experienceIndex} / 100`);
@@ -1,7 +1,7 @@
1
1
  import { messagingGet, messagingPut, messagingPost, messagingDel, get as adminGet } from '../api.js';
2
2
  import {
3
3
  TERMINAL_ACTION_NAMES, buildPolicyUpdate, validatePolicyUpdate, describeChain,
4
- describeSimulation, describeTransfers,
4
+ describeSimulation, describeTransfers, validateAdvancedUpdate,
5
5
  } from '../routing-inactivity.js';
6
6
 
7
7
  // ── helpers ───────────────────────────────────────────────────────────────────
@@ -199,13 +199,24 @@ const policySetCommand = {
199
199
  .option('queue-escalation-minutes', { type: 'number', describe: 'Minutes in queue before escalation alert (1-240)' })
200
200
  .option('terminal-workflow', { type: 'number', describe: 'Workflow id to run when the terminal action is RunWorkflow' })
201
201
  .option('visitor-text', { type: 'string', describe: 'Visitor status text (max 500). Tokens: {{releasedFromAgent}} {{reason}} {{teamName}}' })
202
- .option('agent-text', { type: 'string', describe: 'Agent status text (max 500)' }),
202
+ .option('agent-text', { type: 'string', describe: 'Agent status text (max 500)' })
203
+ .option('no-typing-after', { type: 'number', describe: 'Advanced: seconds with no agent typing before the stepped-away steps run (10-3600)' })
204
+ .option('no-typing-message', { type: 'string', describe: 'Advanced: message to the visitor when the agent steps away (max 500)' })
205
+ .option('no-typing-alert-supervisor', { type: 'boolean', describe: 'Advanced: alert a supervisor when the agent stops typing' })
206
+ .option('no-typing-reroute', { type: 'boolean', describe: 'Advanced: move the chat to another agent when the agent stops typing' })
207
+ .option('escalation-team', { type: 'number', describe: 'Advanced: group alerted when the chat stays unattended' })
208
+ .option('escalation-after', { type: 'number', describe: 'Advanced: seconds unattended before the escalation group is alerted (30-14400)' })
209
+ .option('terminal-bot', { type: 'number', describe: 'Advanced: bot (AI configuration id) for terminal action ReturnToBot (9)' })
210
+ .option('apply-to', { type: 'string', choices: ['auto-route', 'queue'], describe: 'Advanced: apply to auto-routing only or the queue only (omit = unchanged)' }),
203
211
  handler: async (argv) => {
204
212
  // Load current policy first, then merge
205
213
  const current = await messagingGet('/AutoRoutingPolicy');
206
214
 
207
215
  const updated = buildPolicyUpdate(current, argv);
208
- const problems = validatePolicyUpdate(updated);
216
+ const problems = [
217
+ ...validatePolicyUpdate(updated),
218
+ ...validateAdvancedUpdate(updated.advanced, updated.terminalActionOverride, current),
219
+ ];
209
220
  if (problems.length) {
210
221
  for (const m of problems) console.error(` ${RED}x${RESET} ${m}`);
211
222
  process.exitCode = 1;
@@ -4,7 +4,7 @@
4
4
  // Persisted as ints by messaging (append-only enum); the API accepts and returns the NAME.
5
5
  export const TERMINAL_ACTION_NAMES = [
6
6
  'Survey', 'CreateTicket', 'SurveyAndCreateTicket', 'Queue', 'OfflineMessage', 'NoOp',
7
- 'EndChat', 'NotifyManager', 'RunWorkflow',
7
+ 'EndChat', 'NotifyManager', 'RunWorkflow', 'ReturnToBot',
8
8
  ];
9
9
 
10
10
  export function terminalLabel(v) {
@@ -18,7 +18,7 @@ export function terminalForSave(v) {
18
18
  if (v === null || v === undefined || v === '') return null;
19
19
  if (typeof v === 'number' || /^\d+$/.test(String(v))) {
20
20
  const name = TERMINAL_ACTION_NAMES[Number(v)];
21
- if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-8: ${TERMINAL_ACTION_NAMES.map((n, i) => `${i}=${n}`).join(' ')}`);
21
+ if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-9: ${TERMINAL_ACTION_NAMES.map((n, i) => `${i}=${n}`).join(' ')}`);
22
22
  return name;
23
23
  }
24
24
  const hit = TERMINAL_ACTION_NAMES.find(n => n.toLowerCase() === String(v).toLowerCase());
@@ -47,6 +47,72 @@ export const CHAIN_FLAGS = {
47
47
  * reassignmentMode is ALWAYS sent (an absent value is treated as Observe by the API and would silently
48
48
  * switch reassignment off). The chain block is only sent when the server reports one or a chain flag is set.
49
49
  */
50
+ // CLI flag -> AutoRoutingPolicy.advanced field (stepped away / not typing). null on PUT = unchanged.
51
+ export const ADVANCED_FLAGS = {
52
+ 'no-typing-after': 'noTypingAfterSeconds',
53
+ 'no-typing-message': 'noTypingVisitorMessage',
54
+ 'no-typing-alert-supervisor': 'noTypingAlertSupervisor',
55
+ 'no-typing-reroute': 'noTypingReroute',
56
+ 'escalation-team': 'escalationTeamId',
57
+ 'escalation-after': 'escalationAfterSeconds',
58
+ 'terminal-bot': 'terminalBotProfileId',
59
+ 'apply-to': 'applyToRoutingActions',
60
+ };
61
+
62
+ const APPLY_TO = { acd: 1, 'auto-route': 1, queue: 2 };
63
+
64
+ /** Returns the `advanced` PUT block for the flags given (others null = unchanged), or null when no advanced flag was passed. */
65
+ export function buildAdvancedUpdate(current, argv) {
66
+ const given = Object.keys(ADVANCED_FLAGS).filter(f => argv[f] !== undefined);
67
+ if (!given.length) return null;
68
+ if (!('advanced' in current)) throw new Error('This messaging build does not report the advanced stepped-away settings yet; they cannot be saved.');
69
+ const out = Object.fromEntries(Object.values(ADVANCED_FLAGS).map(k => [k, null]));
70
+ for (const f of given) {
71
+ let v = argv[f];
72
+ if (f === 'apply-to') {
73
+ v = typeof v === 'number' ? v : APPLY_TO[String(v).toLowerCase()];
74
+ if (v !== 1 && v !== 2) throw new Error('--apply-to must be auto-route or queue. Omit it to leave the setting unchanged.');
75
+ }
76
+ out[ADVANCED_FLAGS[f]] = v;
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /** Cross-field/range checks for the advanced block, mirroring the server. Returns a list of messages. */
82
+ export function validateAdvancedUpdate(adv, terminalAction, current) {
83
+ const errors = [];
84
+ if (adv) {
85
+ const i = Number.isInteger;
86
+ if (adv.noTypingAfterSeconds != null && !(i(adv.noTypingAfterSeconds) && adv.noTypingAfterSeconds >= 10 && adv.noTypingAfterSeconds <= 3600))
87
+ errors.push('--no-typing-after must be a whole number from 10 to 3600.');
88
+ if (adv.escalationAfterSeconds != null && !(i(adv.escalationAfterSeconds) && adv.escalationAfterSeconds >= 30 && adv.escalationAfterSeconds <= 14400))
89
+ errors.push('--escalation-after must be a whole number from 30 to 14400.');
90
+ if (typeof adv.noTypingVisitorMessage === 'string' && adv.noTypingVisitorMessage.length > 500)
91
+ errors.push('--no-typing-message must be 500 characters or fewer.');
92
+ if (adv.escalationAfterSeconds != null && adv.escalationTeamId == null && current?.advanced?.escalationTeamId == null)
93
+ errors.push('--escalation-after needs --escalation-team <id>.');
94
+ }
95
+ if (terminalAction === 'ReturnToBot' && adv?.terminalBotProfileId == null && current?.advanced?.terminalBotProfileId == null)
96
+ errors.push('Terminal action ReturnToBot needs --terminal-bot <id>.');
97
+ return errors;
98
+ }
99
+
100
+ export function describeAdvanced(adv) {
101
+ if (!adv) return [' (This messaging build does not report the advanced stepped-away settings.)'];
102
+ const from = f => (adv.inheritedFrom?.[f] ? ` [from ${adv.inheritedFrom[f]}]` : '');
103
+ const v = x => (x === null || x === undefined ? 'not set' : x);
104
+ return [
105
+ 'Advanced (stepped away / not typing):',
106
+ ` No typing after: ${adv.noTypingAfterSeconds != null ? `${adv.noTypingAfterSeconds}s` : 'off'}${from('noTypingAfterSeconds')}`,
107
+ ` Visitor message: ${v(adv.noTypingVisitorMessage)}${from('noTypingVisitorMessage')}`,
108
+ ` Alert supervisor: ${v(adv.noTypingAlertSupervisor)}${from('noTypingAlertSupervisor')}`,
109
+ ` Move to next agent: ${v(adv.noTypingReroute)}${from('noTypingReroute')}`,
110
+ ` Escalation group: ${v(adv.escalationTeamId)}${adv.escalationAfterSeconds != null ? ` after ${adv.escalationAfterSeconds}s` : ''}${from('escalationTeamId')}`,
111
+ ` Return-to-bot bot: ${v(adv.terminalBotProfileId)}${from('terminalBotProfileId')}`,
112
+ ` Applies to: ${adv.applyToRoutingActions === 1 ? 'auto-routing only' : adv.applyToRoutingActions === 2 ? 'queue only' : 'auto-routing and queue'}${from('applyToRoutingActions')}`,
113
+ ];
114
+ }
115
+
50
116
  export function buildPolicyUpdate(current, argv) {
51
117
  const merged = { ...current };
52
118
  const map = {
@@ -64,7 +130,7 @@ export function buildPolicyUpdate(current, argv) {
64
130
  argv['terminal-action'] !== undefined ? argv['terminal-action'] : current.terminalActionOverride
65
131
  );
66
132
  // Actions 6-8 exist only on a messaging build with the inactivity chain; an older one would store a different value.
67
- if (['EndChat', 'NotifyManager', 'RunWorkflow'].includes(merged.terminalActionOverride) && !('inactivityChain' in current))
133
+ if (['EndChat', 'NotifyManager', 'RunWorkflow', 'ReturnToBot'].includes(merged.terminalActionOverride) && !('inactivityChain' in current))
68
134
  throw new Error(`Terminal action ${merged.terminalActionOverride} needs a messaging build with the agent inactivity chain; this one does not report it. Use 0-5.`);
69
135
 
70
136
  const mode = argv['reassignment-mode'] ?? current.reassignmentMode ?? 'Observe';
@@ -79,6 +145,10 @@ export function buildPolicyUpdate(current, argv) {
79
145
  for (const f of patched) chain[CHAIN_FLAGS[f]] = argv[f];
80
146
  merged.inactivityChain = chain;
81
147
  }
148
+ // The read-side advanced block carries inheritedFrom; send only the explicit PUT block (or nothing).
149
+ delete merged.advanced;
150
+ const adv = buildAdvancedUpdate(current, argv);
151
+ if (adv) merged.advanced = adv;
82
152
  return merged;
83
153
  }
84
154
 
@@ -117,6 +187,7 @@ export function describeChain(p) {
117
187
  ` Terminal workflow: ${c.terminalWorkflowId ?? 'none'}`,
118
188
  );
119
189
  }
190
+ if ('advanced' in p) out.push(...describeAdvanced(p.advanced));
120
191
  return out;
121
192
  }
122
193
 
@@ -2,7 +2,7 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
4
  buildPolicyUpdate, validatePolicyUpdate, terminalForSave, terminalLabel,
5
- describeChain, describeSimulation, describeTransfers,
5
+ describeChain, describeSimulation, describeTransfers, buildAdvancedUpdate, validateAdvancedUpdate, describeAdvanced,
6
6
  } from './routing-inactivity.js';
7
7
 
8
8
  const current = {
@@ -15,7 +15,7 @@ test('terminal action accepts ints and names, rejects junk', () => {
15
15
  assert.equal(terminalForSave('7'), 'NotifyManager');
16
16
  assert.equal(terminalForSave('runworkflow'), 'RunWorkflow');
17
17
  assert.equal(terminalForSave(undefined), null);
18
- assert.throws(() => terminalForSave(9), /Unknown terminal action/);
18
+ assert.throws(() => terminalForSave(10), /Unknown terminal action/);
19
19
  assert.throws(() => terminalForSave('Bogus'), /Unknown terminal action/);
20
20
  assert.equal(terminalLabel(null), 'Default (Survey)');
21
21
  assert.equal(terminalLabel(8), 'RunWorkflow');
@@ -99,3 +99,46 @@ test('describe helpers render the chain, a simulation and a transfer count', ()
99
99
  assert.match(describeTransfers({ siteId: 1, days: 30, total: 2, byAgent: [{ agentId: 7, name: 'Sam', count: 2 }], byDay: [] }).join('\n'), /Sam \(id 7\): 2/);
100
100
  assert.deepEqual(describeTransfers({ note: 'No agent found' }), ['No agent found']);
101
101
  });
102
+
103
+ const withAdv = { ...current, advanced: { noTypingAfterSeconds: null, inheritedFrom: { noTypingAfterSeconds: 'Default' } } };
104
+
105
+ test('ReturnToBot is terminal action 9', () => {
106
+ assert.equal(terminalForSave(9), 'ReturnToBot');
107
+ assert.equal(terminalForSave('returntobot'), 'ReturnToBot');
108
+ });
109
+
110
+ test('advanced flags build a PUT block where untouched fields are null (unchanged)', () => {
111
+ const body = buildPolicyUpdate(withAdv, { 'no-typing-after': 90, 'no-typing-reroute': true, 'apply-to': 'queue' });
112
+ assert.equal(body.advanced.noTypingAfterSeconds, 90);
113
+ assert.equal(body.advanced.noTypingReroute, true);
114
+ assert.equal(body.advanced.applyToRoutingActions, 2);
115
+ assert.equal(body.advanced.escalationTeamId, null);
116
+ assert.equal(body.advanced.inheritedFrom, undefined);
117
+ });
118
+
119
+ test('no advanced flag means the read-side advanced block is not echoed back', () => {
120
+ const body = buildPolicyUpdate(withAdv, { 'max-reroutes': 4 });
121
+ assert.equal('advanced' in body, false);
122
+ });
123
+
124
+ test('advanced flags fail clearly on a messaging build without the block', () => {
125
+ assert.throws(() => buildPolicyUpdate(current, { 'no-typing-after': 90 }), /does not report the advanced/);
126
+ });
127
+
128
+ test('advanced validation mirrors the server rules', () => {
129
+ const adv = buildAdvancedUpdate(withAdv, { 'no-typing-after': 5, 'escalation-after': 10 });
130
+ const errs = validateAdvancedUpdate(adv, null, withAdv).join(' | ');
131
+ assert.match(errs, /--no-typing-after/);
132
+ assert.match(errs, /--escalation-after must be/);
133
+ assert.match(errs, /needs --escalation-team/);
134
+ assert.match(validateAdvancedUpdate(null, 'ReturnToBot', withAdv).join(' '), /needs --terminal-bot/);
135
+ assert.deepEqual(validateAdvancedUpdate(buildAdvancedUpdate(withAdv, { 'terminal-bot': 4 }), 'ReturnToBot', withAdv), []);
136
+ assert.throws(() => buildAdvancedUpdate(withAdv, { 'apply-to': 'both' }), /--apply-to/);
137
+ });
138
+
139
+ test('describeAdvanced shows the value and where it is inherited from', () => {
140
+ const text = describeAdvanced({ noTypingAfterSeconds: 90, inheritedFrom: { noTypingAfterSeconds: 'Team' } }).join('\n');
141
+ assert.match(text, /No typing after:\s+90s \[from Team\]/);
142
+ assert.match(describeAdvanced(undefined).join(' '), /does not report/);
143
+ assert.match(describeChain(withAdv).join('\n'), /Advanced \(stepped away/);
144
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "1.4.65",
3
+ "version": "1.4.67",
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": {