@velaro/cli 1.4.67 → 1.4.69

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.
@@ -220,19 +220,39 @@ const triageRulesCommand = {
220
220
  };
221
221
 
222
222
  const setTriageRuleCommand = {
223
- command: 'set-triage-rule <label> <enabled>',
224
- describe: `Enable or disable one fixed AI triage label (${VALID_LABELS.join(', ')})`,
223
+ command: 'set-triage-rule [label] [enabled]',
224
+ describe: `Enable/disable one fixed AI triage label (${VALID_LABELS.join(', ')}) and/or set --triage-mode`,
225
225
  builder: (y) => y
226
226
  .positional('label', { type: 'string', describe: `Label: ${VALID_LABELS.join(', ')}` })
227
- .positional('enabled', { type: 'string', describe: 'true or false', choices: ['true', 'false'] }),
227
+ .positional('enabled', { type: 'string', describe: 'true or false', choices: ['true', 'false'] })
228
+ .option('triage-mode', { type: 'string', choices: ['off', 'economy'], describe: 'off (default) or economy (skip AI call for obvious messages)' }),
228
229
  handler: async (argv) => {
229
230
  const { label, enabled } = argv;
230
- if (!VALID_LABELS.includes(label)) {
231
- console.error(`Invalid label "${label}". Valid values: ${VALID_LABELS.join(', ')}`);
231
+ const mode = argv['triage-mode'];
232
+ if (!label && !mode) {
233
+ console.error('Provide <label> <enabled> and/or --triage-mode <off|economy>.');
232
234
  process.exit(1);
233
235
  }
234
- const result = await messagingPut('/Focus/triage-rules', [{ Label: label, Enabled: enabled === 'true' }]);
235
- console.log(`\n ${GRN}✓${RESET} Label "${label}" ${enabled === 'true' ? 'enabled' : 'disabled'}. Effective labels: ${(result?.effectiveLabels ?? []).join(', ')}\n`);
236
+ let labels;
237
+ if (label) {
238
+ if (!VALID_LABELS.includes(label)) {
239
+ console.error(`Invalid label "${label}". Valid values: ${VALID_LABELS.join(', ')}`);
240
+ process.exit(1);
241
+ }
242
+ if (enabled !== 'true' && enabled !== 'false') {
243
+ console.error('<enabled> must be true or false.');
244
+ process.exit(1);
245
+ }
246
+ labels = [{ Label: label, Enabled: enabled === 'true' }];
247
+ }
248
+ const body = mode ? { ...(labels ? { labels } : {}), triageMode: mode } : labels;
249
+ const result = await messagingPut('/Focus/triage-rules', body);
250
+ if (label) console.log(`
251
+ ${GRN}✓${RESET} Label "${label}" ${enabled === 'true' ? 'enabled' : 'disabled'}.`);
252
+ if (mode) console.log(`
253
+ ${GRN}✓${RESET} Triage mode set to ${mode}.`);
254
+ console.log(` Effective labels: ${(result?.effectiveLabels ?? []).join(', ')}
255
+ `);
236
256
  },
237
257
  };
238
258
 
@@ -1,4 +1,5 @@
1
1
  import { messagingGet, messagingPut, messagingPost, messagingDel, get as adminGet } from '../api.js';
2
+ import { teamConfigPath, buildTeamConfigBody, parseExtraJson } from '../routing-team-config.js';
2
3
  import {
3
4
  TERMINAL_ACTION_NAMES, buildPolicyUpdate, validatePolicyUpdate, describeChain,
4
5
  describeSimulation, describeTransfers, validateAdvancedUpdate,
@@ -391,6 +392,69 @@ const inactivityTransfersCommand = {
391
392
  },
392
393
  };
393
394
 
395
+ // ── velaro routing team-config get|set ────────────────────────────────────────
396
+
397
+ const teamConfigGetCommand = {
398
+ command: 'get',
399
+ describe: 'Show team-level routing config (omit --team for the site default row)',
400
+ builder: (y) => y.option('team', { type: 'number', describe: 'Team (group) id' }),
401
+ handler: async (argv) => {
402
+ const r = await messagingGet(teamConfigPath(argv.team));
403
+ console.log(JSON.stringify(r, null, 2));
404
+ },
405
+ };
406
+
407
+ const teamConfigSetCommand = {
408
+ command: 'set',
409
+ describe: 'Partially update team-level routing config (only passed fields change)',
410
+ builder: (y) => y
411
+ .option('team', { type: 'number', describe: 'Team (group) id (omit for the site default row)' })
412
+ .option('routing-type', { type: 'string', describe: 'roundRobin | leastBusy | longestIdle (not validated here)' })
413
+ .option('routing-action', { type: 'string', describe: 'Routing action (server-validated)' })
414
+ .option('queue-timeout-enabled', { type: 'boolean' })
415
+ .option('queue-timeout-minutes', { type: 'number' })
416
+ .option('prioritize-agents', { type: 'boolean' })
417
+ .option('idle-alert-enabled', { type: 'boolean' })
418
+ .option('idle-alert-after', { type: 'number', describe: 'Minutes' })
419
+ .option('idle-alert-repeat', { type: 'number', describe: 'Minutes' })
420
+ .option('idle-alert-max', { type: 'number', describe: 'Max alerts per conversation' })
421
+ .option('idle-alert-title', { type: 'string' })
422
+ .option('idle-alert-body', { type: 'string' })
423
+ .option('schedule-aware', { type: 'boolean' })
424
+ .option('extra', { type: 'string', describe: 'JSON object of any other RoutingConfig fields, passed through as-is' }),
425
+ handler: async (argv) => {
426
+ let body;
427
+ try {
428
+ body = buildTeamConfigBody({
429
+ routingType: argv['routing-type'], routingAction: argv['routing-action'],
430
+ queueTimeoutEnabled: argv['queue-timeout-enabled'], queueTimeoutMinutes: argv['queue-timeout-minutes'],
431
+ prioritizeAgents: argv['prioritize-agents'],
432
+ idleQueueAlertEnabled: argv['idle-alert-enabled'], idleQueueAlertAfterMinutes: argv['idle-alert-after'],
433
+ idleQueueAlertRepeatMinutes: argv['idle-alert-repeat'], idleQueueAlertMaxPerConversation: argv['idle-alert-max'],
434
+ idleQueueAlertTitle: argv['idle-alert-title'], idleQueueAlertBody: argv['idle-alert-body'],
435
+ scheduleAwareRoutingEnabled: argv['schedule-aware'],
436
+ }, parseExtraJson(argv.extra));
437
+ } catch (e) {
438
+ console.error(` ${RED}x${RESET} ${e.message}`);
439
+ process.exitCode = 1;
440
+ return;
441
+ }
442
+ await messagingPut(teamConfigPath(argv.team), body);
443
+ console.log(`\n ${GRN}✓${RESET} Team routing config updated.\n`);
444
+ },
445
+ };
446
+
447
+ const teamConfigCommand = {
448
+ command: 'team-config <subcommand>',
449
+ describe: 'Get/update team-level routing config (routing type, queue timeout, idle alerts)',
450
+ builder: (y) => y
451
+ .command(teamConfigGetCommand)
452
+ .command(teamConfigSetCommand)
453
+ .demandCommand(1, 'Specify a subcommand: get, set')
454
+ .strict(),
455
+ handler: () => {},
456
+ };
457
+
394
458
  // ── top-level routing command ─────────────────────────────────────────────────
395
459
 
396
460
  export const routingCommand = {
@@ -400,12 +464,13 @@ export const routingCommand = {
400
464
  .command(engineCommand)
401
465
  .command(circuitBreakerCommand)
402
466
  .command(policyCommand)
467
+ .command(teamConfigCommand)
403
468
  .command(decisionLogCommand)
404
469
  .command(explainCommand)
405
470
  .command(simulateCommand)
406
471
  .command(precedenceCommand)
407
472
  .command(inactivityTransfersCommand)
408
- .demandCommand(1, 'Specify a subcommand: engine, circuit-breaker, policy, decision-log, explain, simulate, precedence, inactivity-transfers')
473
+ .demandCommand(1, 'Specify a subcommand: engine, circuit-breaker, policy, team-config, decision-log, explain, simulate, precedence, inactivity-transfers')
409
474
  .strict(),
410
475
  handler: () => {},
411
476
  };
@@ -0,0 +1,37 @@
1
+ // Team-level RoutingConfig helpers (REST: messaging GET/PUT /RoutingConfig?teamId=).
2
+ // Partial update, apply-if-present. Unknown fields pass through so new server fields
3
+ // (e.g. routingType "longestIdle", queue-broadcast, CRM-owner) work without a CLI release.
4
+
5
+ const KNOWN = [
6
+ 'routingType', 'routingAction', 'queueTimeoutEnabled', 'queueTimeoutMinutes', 'prioritizeAgents',
7
+ 'idleQueueAlertEnabled', 'idleQueueAlertAfterMinutes', 'idleQueueAlertRepeatMinutes',
8
+ 'idleQueueAlertMaxPerConversation', 'idleQueueAlertTitle', 'idleQueueAlertBody',
9
+ 'scheduleAwareRoutingEnabled',
10
+ ];
11
+
12
+ export function teamConfigPath(teamId) {
13
+ if (teamId === undefined || teamId === null || teamId === '') return '/RoutingConfig';
14
+ const n = Number(teamId);
15
+ if (!Number.isInteger(n) || n <= 0) throw new Error('teamId must be a positive integer.');
16
+ return `/RoutingConfig?teamId=${n}`;
17
+ }
18
+
19
+ // Build the PUT body: known fields from args + any extra object, dropping undefined.
20
+ // null is kept (it means "inherit" server-side). No enum validation on purpose.
21
+ export function buildTeamConfigBody(args = {}, extra = {}) {
22
+ const body = {};
23
+ for (const k of KNOWN) if (args[k] !== undefined) body[k] = args[k];
24
+ for (const [k, v] of Object.entries(extra || {})) if (v !== undefined) body[k] = v;
25
+ if (!Object.keys(body).length) throw new Error('No fields to update. Pass at least one field.');
26
+ return body;
27
+ }
28
+
29
+ export function parseExtraJson(text) {
30
+ if (text === undefined) return {};
31
+ let v;
32
+ try { v = JSON.parse(text); } catch (e) { throw new Error(`--extra must be valid JSON: ${e.message}`); }
33
+ if (!v || typeof v !== 'object' || Array.isArray(v)) throw new Error('--extra must be a JSON object.');
34
+ return v;
35
+ }
36
+
37
+ export const TEAM_CONFIG_FIELDS = KNOWN;
@@ -0,0 +1,23 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { teamConfigPath, buildTeamConfigBody, parseExtraJson } from './routing-team-config.js';
4
+
5
+ test('path scopes by teamId and rejects junk', () => {
6
+ assert.equal(teamConfigPath(), '/RoutingConfig');
7
+ assert.equal(teamConfigPath(7), '/RoutingConfig?teamId=7');
8
+ assert.throws(() => teamConfigPath('abc'));
9
+ assert.throws(() => teamConfigPath(0));
10
+ });
11
+
12
+ test('body is partial, keeps null, passes new values and unknown fields through', () => {
13
+ const b = buildTeamConfigBody({ routingType: 'longestIdle', queueTimeoutMinutes: null }, { queueBroadcast: true });
14
+ assert.deepEqual(b, { routingType: 'longestIdle', queueTimeoutMinutes: null, queueBroadcast: true });
15
+ assert.throws(() => buildTeamConfigBody({}));
16
+ });
17
+
18
+ test('extra json must be an object', () => {
19
+ assert.deepEqual(parseExtraJson(undefined), {});
20
+ assert.deepEqual(parseExtraJson('{"a":1}'), { a: 1 });
21
+ assert.throws(() => parseExtraJson('[1]'));
22
+ assert.throws(() => parseExtraJson('{'));
23
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/cli",
3
- "version": "1.4.67",
3
+ "version": "1.4.69",
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": {