@velaro/mcp-server 0.6.54 → 0.6.55
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/README.md +5 -0
- package/package.json +1 -1
- package/server.js +205 -7
package/README.md
CHANGED
|
@@ -132,6 +132,11 @@ This works with any MCP client that supports HTTP/SSE transport — including Cl
|
|
|
132
132
|
| `team_list` | List all teams for this site |
|
|
133
133
|
| `routing_list` | List all routing rules ordered by priority |
|
|
134
134
|
| `routing_get` | Get routing rule details including rule and value JSON |
|
|
135
|
+
| `routing_get_policy` | Site auto-routing policy including reassignment mode and the agent inactivity chain |
|
|
136
|
+
| `routing_update_policy` | Update the policy, reassignment mode and inactivity chain (mode is always sent back) |
|
|
137
|
+
| `routing_simulate_inactivity` | DRY RUN of the inactivity chain for a mocked scenario, nothing is sent |
|
|
138
|
+
| `routing_explain_precedence` | Which routing rule wins when an agent goes silent, plus conflict warnings |
|
|
139
|
+
| `routing_inactivity_transfers` | Count agent inactivity transfers: total, per agent, per day |
|
|
135
140
|
|
|
136
141
|
### Site
|
|
137
142
|
| Tool | What It Does |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velaro/mcp-server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.55",
|
|
4
4
|
"description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro account: KB, workflows, bots, conversations, contacts, routing, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -1446,7 +1446,21 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1446
1446
|
preferPreviousAgentOnTimeout: { type: 'boolean', description: 'On timeout, prefer agent from visitor\'s prior session (loyalty routing)' },
|
|
1447
1447
|
agentFollowUpFlagEnabled: { type: 'boolean', description: 'Flag conversations that required multiple reroutes' },
|
|
1448
1448
|
queueUnavailableGracePeriodSeconds: { type: 'number', description: 'Wait before returning unavailable when agents are transiently busy (seconds)' },
|
|
1449
|
-
terminalActionOverride: { type: 'number', description: 'Terminal action: 0=Survey 1=CreateTicket 2=SurveyAndCreateTicket 3=Queue 4=OfflineMessage 5=NoOp. Null = default (Survey).' },
|
|
1449
|
+
terminalActionOverride: { type: 'number', description: 'Terminal action: 0=Survey 1=CreateTicket 2=SurveyAndCreateTicket 3=Queue 4=OfflineMessage 5=NoOp 6=EndChat 7=NotifyManager 8=RunWorkflow (needs inactivityTerminalWorkflowId). Null = default (Survey).' },
|
|
1450
|
+
exhaustionMessageTemplate: { type: 'string', description: 'Message sent to the visitor with the final action (max 500 characters).' },
|
|
1451
|
+
reassignmentMode: { type: 'string', enum: ['Observe','QueueOnly','Reassign'], description: "Observe (default) only records what would happen; QueueOnly returns a silent agent's chat to the group queue; Reassign hands it to the next agent. Backup group and no-agent-available steps need Reassign." },
|
|
1452
|
+
inactivityChainEnabled: { type: 'boolean', description: 'Master switch for the agent inactivity fallback chain. Off by default.' },
|
|
1453
|
+
inactivityBackupTeamId: { type: 'number', description: "Backup group to try when nobody in the chat's own group is available." },
|
|
1454
|
+
inactivityBackupTeamAfterSeconds: { type: 'number', description: 'Seconds with no available agent before the backup group is tried (0-7200).' },
|
|
1455
|
+
inactivityNoAgentActionAfterSeconds: { type: 'number', description: 'Seconds with no available agent before the final action runs (0-14400).' },
|
|
1456
|
+
inactivityTerminalWorkflowId: { type: 'number', description: 'Workflow to run when terminalActionOverride is 8.' },
|
|
1457
|
+
rerouteOnAgentUnavailable: { type: 'boolean', description: 'Presence trigger: also treat a chat as inactive when its agent is no longer Available.' },
|
|
1458
|
+
alertAvailableAgentsOnRelease: { type: 'boolean', description: 'Alert available agents when an inactive chat returns to the queue.' },
|
|
1459
|
+
notifyManagersOnInactivity: { type: 'boolean', description: 'Alert managers about inactive chats.' },
|
|
1460
|
+
rerouteCooldownSeconds: { type: 'number', description: 'Cooldown between moves of the same chat (0-3600).' },
|
|
1461
|
+
queueEscalationAfterMinutes: { type: 'number', description: 'Widen the queue alert to backup group and managers after this many minutes (1-240).' },
|
|
1462
|
+
inactivityVisitorText: { type: 'string', description: 'Message the visitor sees when the chat is moved (max 500 characters).' },
|
|
1463
|
+
inactivityAgentText: { type: 'string', description: 'Message agents see on a re-queued chat (max 500 characters).' },
|
|
1450
1464
|
},
|
|
1451
1465
|
required: [],
|
|
1452
1466
|
},
|
|
@@ -1462,6 +1476,41 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1462
1476
|
required: ['conversationId'],
|
|
1463
1477
|
},
|
|
1464
1478
|
},
|
|
1479
|
+
{
|
|
1480
|
+
name: 'routing_simulate_inactivity',
|
|
1481
|
+
description: 'DRY RUN: simulate what the agent inactivity chain would do for a mocked scenario, using the saved routing settings. Answers "what would happen if an agent goes silent and nobody is available for N minutes". Returns which rule applies, the safety-gate result (cooldown, duplicate, hop cap), the decision, the exact visitor and agent messages, and the rules that also matched but lost. Sends nothing and touches no live chat. Uses the same decision code the timer job runs.',
|
|
1482
|
+
inputSchema: {
|
|
1483
|
+
type: 'object',
|
|
1484
|
+
properties: {
|
|
1485
|
+
teamId: { type: 'number', description: 'Group id whose own policy to simulate. Omit for the site default.' },
|
|
1486
|
+
noAgentSeconds: { type: 'number', description: 'Seconds nobody has been available (0 = just now). Default 0.' },
|
|
1487
|
+
attemptsUsed: { type: 'number', description: 'Moves already used on the chat. Default 0.' },
|
|
1488
|
+
inactivityStep: { type: 'number', description: '0 = nothing tried, 1 = offered to backup group, 2 = final action applied. Default 0.' },
|
|
1489
|
+
secondsSinceLastTransfer: { type: 'number', description: 'Seconds since the chat was last moved. Omit if never moved.' },
|
|
1490
|
+
sameEpisodeAlreadyHandled: { type: 'boolean', description: 'True to simulate the same silence being delivered twice (duplicate guard).' },
|
|
1491
|
+
fromAgentName: { type: 'string', description: 'Name of the silent agent, used in the alert text.' },
|
|
1492
|
+
},
|
|
1493
|
+
required: [],
|
|
1494
|
+
},
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
name: 'routing_explain_precedence',
|
|
1498
|
+
description: 'Explain which routing rule wins when an agent goes silent: the fixed evaluation order (group policy over site default, kill switch, reassignment mode, detector, safety gate, same-group agents, backup group, final action, and the other mechanisms this policy does not control), plus warnings where one policy shadows or contradicts another. Use for "why did the site default not apply to team X" or "which rule wins".',
|
|
1499
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
name: 'routing_inactivity_transfers',
|
|
1503
|
+
description: 'Count and break down "agent inactivity transfers" (chats taken from an agent who stopped responding) for your site: total, per agent, per day. Pass agentName (display name contains) or agentId to answer "how many inactivity transfers for agent X". Days 1 to 92, default 30.',
|
|
1504
|
+
inputSchema: {
|
|
1505
|
+
type: 'object',
|
|
1506
|
+
properties: {
|
|
1507
|
+
days: { type: 'number', description: 'Lookback in days (1-92). Default 30.' },
|
|
1508
|
+
agentName: { type: 'string', description: 'Agent display name (partial match) to filter to.' },
|
|
1509
|
+
agentId: { type: 'number', description: 'Exact agent id to filter to.' },
|
|
1510
|
+
},
|
|
1511
|
+
required: [],
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1465
1514
|
{
|
|
1466
1515
|
name: 'routing_explain_decision',
|
|
1467
1516
|
description: 'Plain-English explanation of every routing step for a conversation. Returns a human-readable summary ("10:32 AM: Warning sent to visitor… 10:37 AM: Rerouted from Alex → Sarah…"), the policy settings that were active, and enriched events with agent names resolved. Use to answer "why did this chat route to X?" or "why was this chat missed?"',
|
|
@@ -4248,6 +4297,34 @@ function parseBusinessHours(input) {
|
|
|
4248
4297
|
|
|
4249
4298
|
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
4250
4299
|
|
|
4300
|
+
// -- Agent inactivity chain helpers (velaro-messaging PR #1471, v20 ONLY) ------------------------
|
|
4301
|
+
// Terminal action ints are persisted by messaging (append-only enum). The API returns/accepts the NAME.
|
|
4302
|
+
const TERMINAL_ACTION_NAMES = ['Survey','CreateTicket','SurveyAndCreateTicket','Queue','OfflineMessage','NoOp','EndChat','NotifyManager','RunWorkflow'];
|
|
4303
|
+
function terminalActionLabel(v) {
|
|
4304
|
+
if (v === null || v === undefined || v === '') return 'Default (Survey)';
|
|
4305
|
+
if (typeof v === 'number') return TERMINAL_ACTION_NAMES[v] ?? String(v);
|
|
4306
|
+
return String(v);
|
|
4307
|
+
}
|
|
4308
|
+
function terminalActionForSave(v) {
|
|
4309
|
+
if (v === null || v === undefined || v === '') return null;
|
|
4310
|
+
const valid = TERMINAL_ACTION_NAMES.map((n, i) => `${i}=${n}`).join(' ');
|
|
4311
|
+
// Numeric strings ("7") count as ints, the same as the CLI.
|
|
4312
|
+
if (typeof v === 'string' && /^\d+$/.test(v.trim())) v = Number(v.trim());
|
|
4313
|
+
if (typeof v === 'number') {
|
|
4314
|
+
const name = Number.isInteger(v) ? TERMINAL_ACTION_NAMES[v] : undefined;
|
|
4315
|
+
if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-${TERMINAL_ACTION_NAMES.length - 1}: ${valid}`);
|
|
4316
|
+
return name;
|
|
4317
|
+
}
|
|
4318
|
+
const hit = TERMINAL_ACTION_NAMES.find((n) => n.toLowerCase() === String(v).toLowerCase());
|
|
4319
|
+
if (!hit) throw new Error(`Unknown terminal action "${v}". Use one of: ${valid}`);
|
|
4320
|
+
return hit;
|
|
4321
|
+
}
|
|
4322
|
+
const CHAIN_FIELDS = [
|
|
4323
|
+
'enabled','backupTeamId','backupTeamAfterSeconds','noTargetActionAfterSeconds','rerouteOnAgentUnavailable',
|
|
4324
|
+
'alertAvailableAgentsOnRelease','notifyManagersOnInactivity','rerouteCooldownSeconds','queueEscalationAfterMinutes',
|
|
4325
|
+
'terminalWorkflowId','visitorStatusText','agentStatusText',
|
|
4326
|
+
];
|
|
4327
|
+
|
|
4251
4328
|
async function handleTool(name, args) {
|
|
4252
4329
|
switch (name) {
|
|
4253
4330
|
case 'case_list_types': {
|
|
@@ -5179,9 +5256,23 @@ async function handleTool(name, args) {
|
|
|
5179
5256
|
|
|
5180
5257
|
case 'routing_get_policy': {
|
|
5181
5258
|
const p = await messagingGet('/AutoRoutingPolicy');
|
|
5182
|
-
const terminalLabel = p.terminalActionOverride
|
|
5183
|
-
|
|
5184
|
-
|
|
5259
|
+
const terminalLabel = terminalActionLabel(p.terminalActionOverride);
|
|
5260
|
+
const chain = p.inactivityChain;
|
|
5261
|
+
const chainLines = chain ? [
|
|
5262
|
+
'',
|
|
5263
|
+
`Reassignment mode: ${p.reassignmentMode ?? 'Observe'}`,
|
|
5264
|
+
`Inactivity chain: ${chain.enabled ? 'ON' : 'off'}`,
|
|
5265
|
+
...(chain.enabled ? [
|
|
5266
|
+
` Backup group: ${chain.backupTeamId ?? 'none'}${chain.backupTeamId != null ? ` after ${chain.backupTeamAfterSeconds}s` : ''}`,
|
|
5267
|
+
` Final action after: ${chain.noTargetActionAfterSeconds != null ? `${chain.noTargetActionAfterSeconds}s with no agent available` : 'only when the hop cap is used up'}`,
|
|
5268
|
+
` Cooldown between moves: ${chain.rerouteCooldownSeconds}s`,
|
|
5269
|
+
` Presence trigger: ${chain.rerouteOnAgentUnavailable}`,
|
|
5270
|
+
` Alert agents on release: ${chain.alertAvailableAgentsOnRelease}`,
|
|
5271
|
+
` Notify managers: ${chain.notifyManagersOnInactivity}`,
|
|
5272
|
+
` Queue escalation: ${chain.queueEscalationAfterMinutes != null ? `${chain.queueEscalationAfterMinutes} min` : 'never'}`,
|
|
5273
|
+
` Terminal workflow: ${chain.terminalWorkflowId ?? 'none'}`,
|
|
5274
|
+
] : []),
|
|
5275
|
+
] : ['', '(This messaging build does not report the inactivity chain.)'];
|
|
5185
5276
|
return [
|
|
5186
5277
|
`Enabled: ${p.enabled}`,
|
|
5187
5278
|
`Initial response timeout: ${p.initialResponseTimeoutSeconds}s`,
|
|
@@ -5193,6 +5284,7 @@ async function handleTool(name, args) {
|
|
|
5193
5284
|
`Agent follow-up flag: ${p.agentFollowUpFlagEnabled}`,
|
|
5194
5285
|
`Queue unavailable grace: ${p.queueUnavailableGracePeriodSeconds}s`,
|
|
5195
5286
|
`Terminal action: ${terminalLabel}`,
|
|
5287
|
+
...chainLines,
|
|
5196
5288
|
].join('\n');
|
|
5197
5289
|
}
|
|
5198
5290
|
|
|
@@ -5209,8 +5301,46 @@ async function handleTool(name, args) {
|
|
|
5209
5301
|
for (const f of fields) {
|
|
5210
5302
|
if (args[f] !== undefined) merged[f] = args[f];
|
|
5211
5303
|
}
|
|
5212
|
-
|
|
5213
|
-
|
|
5304
|
+
// Terminal action travels as the enum name; the API returns the name too, so normalize either way.
|
|
5305
|
+
merged.terminalActionOverride = terminalActionForSave(merged.terminalActionOverride);
|
|
5306
|
+
// RunWorkflow needs a workflow whatever the chain switch says: the final action also runs at the hop cap.
|
|
5307
|
+
// An explicit null clears the workflow, so it must fail the guard (?? would have fallen back to the saved one).
|
|
5308
|
+
const effectiveWorkflowId = args.inactivityTerminalWorkflowId !== undefined
|
|
5309
|
+
? args.inactivityTerminalWorkflowId
|
|
5310
|
+
: current.inactivityChain?.terminalWorkflowId;
|
|
5311
|
+
if (merged.terminalActionOverride === 'RunWorkflow' && effectiveWorkflowId == null) {
|
|
5312
|
+
throw new Error('Terminal action 8 (RunWorkflow) needs inactivityTerminalWorkflowId.');
|
|
5313
|
+
}
|
|
5314
|
+
// Actions 6-8 exist only on a messaging build with the inactivity chain; an older one would store a different value.
|
|
5315
|
+
if (['EndChat', 'NotifyManager', 'RunWorkflow'].includes(merged.terminalActionOverride) && !('inactivityChain' in current)) {
|
|
5316
|
+
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.`);
|
|
5317
|
+
}
|
|
5318
|
+
if (args.exhaustionMessageTemplate !== undefined) merged.exhaustionMessageTemplate = args.exhaustionMessageTemplate;
|
|
5319
|
+
// Always send the mode back: an absent value is treated as Observe by the API.
|
|
5320
|
+
merged.reassignmentMode = args.reassignmentMode ?? current.reassignmentMode ?? 'Observe';
|
|
5321
|
+
const chainPatch = {
|
|
5322
|
+
enabled: args.inactivityChainEnabled,
|
|
5323
|
+
backupTeamId: args.inactivityBackupTeamId,
|
|
5324
|
+
backupTeamAfterSeconds: args.inactivityBackupTeamAfterSeconds,
|
|
5325
|
+
noTargetActionAfterSeconds: args.inactivityNoAgentActionAfterSeconds,
|
|
5326
|
+
terminalWorkflowId: args.inactivityTerminalWorkflowId,
|
|
5327
|
+
rerouteOnAgentUnavailable: args.rerouteOnAgentUnavailable,
|
|
5328
|
+
alertAvailableAgentsOnRelease: args.alertAvailableAgentsOnRelease,
|
|
5329
|
+
notifyManagersOnInactivity: args.notifyManagersOnInactivity,
|
|
5330
|
+
rerouteCooldownSeconds: args.rerouteCooldownSeconds,
|
|
5331
|
+
queueEscalationAfterMinutes: args.queueEscalationAfterMinutes,
|
|
5332
|
+
visitorStatusText: args.inactivityVisitorText,
|
|
5333
|
+
agentStatusText: args.inactivityAgentText,
|
|
5334
|
+
};
|
|
5335
|
+
if (current.inactivityChain || Object.values(chainPatch).some((v) => v !== undefined)) {
|
|
5336
|
+
const chain = { ...(current.inactivityChain ?? {}) };
|
|
5337
|
+
for (const f of CHAIN_FIELDS) if (chainPatch[f] !== undefined) chain[f] = chainPatch[f];
|
|
5338
|
+
merged.inactivityChain = chain;
|
|
5339
|
+
}
|
|
5340
|
+
await messagingPut('/AutoRoutingPolicy', merged);
|
|
5341
|
+
// PUT answers { updated: true }, not the policy: re-read so the reply reports what is really stored.
|
|
5342
|
+
const saved = await messagingGet('/AutoRoutingPolicy');
|
|
5343
|
+
return `Routing policy updated. Initial timeout: ${saved.initialResponseTimeoutSeconds}s, Idle timeout: ${saved.idleAfterResponseTimeoutSeconds}s, Max reroutes: ${saved.maxRerouteAttempts}, Reassignment mode: ${saved.reassignmentMode ?? 'n/a'}, Inactivity chain: ${saved.inactivityChain?.enabled ? 'ON' : 'off'}, Terminal action: ${terminalActionLabel(saved.terminalActionOverride)}.`;
|
|
5214
5344
|
}
|
|
5215
5345
|
|
|
5216
5346
|
case 'routing_get_decision_log': {
|
|
@@ -5241,13 +5371,81 @@ async function handleTool(name, args) {
|
|
|
5241
5371
|
if (result.policySnapshot) {
|
|
5242
5372
|
const p = result.policySnapshot;
|
|
5243
5373
|
lines.push('');
|
|
5244
|
-
lines.push(`Policy settings: initial timeout ${p.initialResponseTimeoutSeconds}s, idle timeout ${p.idleAfterResponseTimeoutSeconds}s, max reroutes ${p.maxRerouteAttempts}, terminal action ${p.terminalActionOverride
|
|
5374
|
+
lines.push(`Policy settings: initial timeout ${p.initialResponseTimeoutSeconds}s, idle timeout ${p.idleAfterResponseTimeoutSeconds}s, max reroutes ${p.maxRerouteAttempts}, terminal action ${terminalActionLabel(p.terminalActionOverride)}`);
|
|
5245
5375
|
if (!p.allowBounceBack) lines.push('Anti-bounce is ON — timed-out agents are excluded from re-routing for this conversation.');
|
|
5246
5376
|
if (p.preferPreviousAgentOnTimeout) lines.push('Loyalty preference is ON — on timeout, the engine prefers the agent from a prior session.');
|
|
5247
5377
|
}
|
|
5248
5378
|
return lines.join('\n');
|
|
5249
5379
|
}
|
|
5250
5380
|
|
|
5381
|
+
case 'routing_simulate_inactivity': {
|
|
5382
|
+
const body = {
|
|
5383
|
+
teamId: args.teamId ?? null,
|
|
5384
|
+
noAgentSeconds: args.noAgentSeconds ?? 0,
|
|
5385
|
+
attemptsUsed: args.attemptsUsed ?? 0,
|
|
5386
|
+
inactivityStep: args.inactivityStep ?? 0,
|
|
5387
|
+
secondsSinceLastTransfer: args.secondsSinceLastTransfer ?? null,
|
|
5388
|
+
sameEpisodeAlreadyHandled: args.sameEpisodeAlreadyHandled ?? false,
|
|
5389
|
+
fromAgentName: args.fromAgentName ?? null,
|
|
5390
|
+
};
|
|
5391
|
+
const r = await messagingPost('/Routing/Inactivity/simulate', body);
|
|
5392
|
+
const gate = {
|
|
5393
|
+
NotEnabled: "The inactivity chain is off for this rule, so today's behavior applies and nothing new happens.",
|
|
5394
|
+
Proceed: 'Allowed to move the chat.',
|
|
5395
|
+
SkipDedupe: 'Blocked: this exact silence was already handled (double-transfer guard).',
|
|
5396
|
+
SkipCooldown: 'Blocked: the chat was moved too recently (cooldown).',
|
|
5397
|
+
HopCapReached: 'Hop cap reached: no more moves, so the final action applies.',
|
|
5398
|
+
}[r.gate] ?? r.gate;
|
|
5399
|
+
const action = {
|
|
5400
|
+
LegacyDefer: 'Nothing new is configured, so the chat stays with its current owner.',
|
|
5401
|
+
Hold: 'Still inside the wait window, nothing happens yet.',
|
|
5402
|
+
WidenToBackupTeam: 'The chat is offered to the backup group.',
|
|
5403
|
+
ApplyTerminal: 'The final action runs.',
|
|
5404
|
+
}[r.noTargetAction] ?? r.noTargetAction;
|
|
5405
|
+
const lines = [
|
|
5406
|
+
'DRY RUN (nothing sent, no live chat touched)',
|
|
5407
|
+
`Settings used: ${r.ruleSource}`,
|
|
5408
|
+
`Safety gate: ${gate}`,
|
|
5409
|
+
`Decision: ${action}`,
|
|
5410
|
+
r.explanation ? `Why: ${r.explanation}` : null,
|
|
5411
|
+
r.nextStepAtSeconds != null ? `Next step after ${r.nextStepAtSeconds}s with no agent available.` : null,
|
|
5412
|
+
`Visitor would see: "${r.visitorMessage}"`,
|
|
5413
|
+
`Agents would see: "${r.queueAlert || r.agentStatus}"`,
|
|
5414
|
+
].filter(Boolean);
|
|
5415
|
+
for (const l of r.otherRulesThatMatchedButLost ?? []) lines.push(`Also matched but lost: ${l.rule} (${l.reason})`);
|
|
5416
|
+
return lines.join('\n');
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
case 'routing_explain_precedence': {
|
|
5420
|
+
const [defaults, conflicts] = await Promise.all([
|
|
5421
|
+
messagingGet('/Routing/Inactivity/defaults'),
|
|
5422
|
+
messagingGet('/Routing/Inactivity/conflicts'),
|
|
5423
|
+
]);
|
|
5424
|
+
const lines = ['Evaluation order (top to bottom):', ...(defaults.precedence ?? []).map((p) => ` ${p}`), ''];
|
|
5425
|
+
const list = conflicts.conflicts ?? [];
|
|
5426
|
+
if (!list.length) lines.push('Warnings: none. No group policy cancels out the site default.');
|
|
5427
|
+
else {
|
|
5428
|
+
lines.push('Warnings:');
|
|
5429
|
+
for (const c of list) lines.push(` [${String(c.severity).toUpperCase()}] ${c.teamId != null ? `group ${c.teamId}` : 'site default'}: ${c.message}`);
|
|
5430
|
+
}
|
|
5431
|
+
return lines.join('\n');
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5434
|
+
case 'routing_inactivity_transfers': {
|
|
5435
|
+
let path = `/RoutingDiagnostics/inactivity-transfers?days=${args.days ?? 30}`;
|
|
5436
|
+
if (args.agentId != null) path += `&agentId=${args.agentId}`;
|
|
5437
|
+
if (args.agentName) path += `&agentName=${encodeURIComponent(args.agentName)}`;
|
|
5438
|
+
const r = await api('GET', path);
|
|
5439
|
+
if (r.note && !r.total && !(r.byAgent ?? []).length) return r.note;
|
|
5440
|
+
const lines = [`Agent inactivity transfers, last ${r.days} days: ${r.total}${r.capped ? ' (capped at 5000)' : ''}`];
|
|
5441
|
+
if (r.note) lines.push(r.note);
|
|
5442
|
+
if (r.matchedAgentsCapped) lines.push('The name matched more agents than are counted here. Use a longer name or agentId.');
|
|
5443
|
+
for (const a of r.byAgent ?? []) lines.push(` ${a.name}: ${a.count}`);
|
|
5444
|
+
if ((r.byDay ?? []).length) lines.push('', 'By day (Pacific): ' + r.byDay.map((d) => `${d.date}=${d.count}`).join(', '));
|
|
5445
|
+
if (!r.total) lines.push('None. Check that the inactivity chain is on and reassignment mode is not Observe.');
|
|
5446
|
+
return lines.join('\n');
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5251
5449
|
// ── Livefluence Migration ──────────────────────────────────────────────
|
|
5252
5450
|
case 'livefluence_export': {
|
|
5253
5451
|
const qs = args.v20_site_id ? `?v20SiteId=${args.v20_site_id}` : '';
|