@velaro/mcp-server 0.6.54 → 0.6.56
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 +7 -3
- package/server.js +428 -9
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.56",
|
|
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": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"qs": "^6.16.0"
|
|
15
15
|
},
|
|
16
16
|
"overrides": {
|
|
17
|
-
"qs": "^6.16.0"
|
|
17
|
+
"qs": "^6.16.0",
|
|
18
|
+
"hono": "^4.13.5"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
21
|
"start": "node server.js"
|
|
@@ -43,5 +44,8 @@
|
|
|
43
44
|
"server.js",
|
|
44
45
|
"server.json",
|
|
45
46
|
"README.md"
|
|
46
|
-
]
|
|
47
|
+
],
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"license-checker-rseidelsohn": "^4.4.2"
|
|
50
|
+
}
|
|
47
51
|
}
|
package/server.js
CHANGED
|
@@ -62,6 +62,12 @@ const MESSAGING_API = process.env.VELARO_MESSAGING_API || null;
|
|
|
62
62
|
const MCP_KEY = process.env.VELARO_MCP_KEY;
|
|
63
63
|
const JWT = process.env.VELARO_JWT;
|
|
64
64
|
|
|
65
|
+
// mcp/staff-server.js defaults to staging instead -- the two servers' defaults intentionally
|
|
66
|
+
// differ, but a zero-result lookup gives no hint which environment was actually searched
|
|
67
|
+
// either way. See ENV_LABEL in staff-server.js for the incident this class of confusion caused
|
|
68
|
+
// (a real production customer read as "doesn't exist" from a staging-scoped search).
|
|
69
|
+
const ENV_LABEL = API_BASE.includes('staging') ? 'staging' : 'production';
|
|
70
|
+
|
|
65
71
|
// Known admin API hosts, for tools that let a caller explicitly target one environment
|
|
66
72
|
// regardless of what this server process's own API_BASE is configured to (see the `env` param
|
|
67
73
|
// on the calendly_* superadmin tools below). Deliberately NOT used as a default anywhere —
|
|
@@ -561,6 +567,18 @@ const TOOLS = [
|
|
|
561
567
|
},
|
|
562
568
|
},
|
|
563
569
|
|
|
570
|
+
{
|
|
571
|
+
name: 'site_scraper_usage',
|
|
572
|
+
description: 'Velaro staff only. Scraper/upload/chunk consumption for ANY site in one call: pages this month vs budget (both the admin and messaging ledgers), upload pages, OCR pages, jobs today vs daily cap, and chunks indexed vs MaxIndexedChunkTotal with chunks grouped by index name. Wraps GET ScraperJobs/StaffUsage/{siteId} (IsVelaroAdminAsync + super-admin enforced server-side). Read-only.',
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: 'object',
|
|
575
|
+
properties: {
|
|
576
|
+
siteId: { type: 'number', description: 'Target site ID (need not be your own)' },
|
|
577
|
+
},
|
|
578
|
+
required: ['siteId'],
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
|
|
564
582
|
// ── Startup migration diagnostics (AdminStartupDiagnostics canary table) ──
|
|
565
583
|
{
|
|
566
584
|
name: 'admin_startup_diagnostics',
|
|
@@ -1446,7 +1464,21 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1446
1464
|
preferPreviousAgentOnTimeout: { type: 'boolean', description: 'On timeout, prefer agent from visitor\'s prior session (loyalty routing)' },
|
|
1447
1465
|
agentFollowUpFlagEnabled: { type: 'boolean', description: 'Flag conversations that required multiple reroutes' },
|
|
1448
1466
|
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).' },
|
|
1467
|
+
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).' },
|
|
1468
|
+
exhaustionMessageTemplate: { type: 'string', description: 'Message sent to the visitor with the final action (max 500 characters).' },
|
|
1469
|
+
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." },
|
|
1470
|
+
inactivityChainEnabled: { type: 'boolean', description: 'Master switch for the agent inactivity fallback chain. Off by default.' },
|
|
1471
|
+
inactivityBackupTeamId: { type: 'number', description: "Backup group to try when nobody in the chat's own group is available." },
|
|
1472
|
+
inactivityBackupTeamAfterSeconds: { type: 'number', description: 'Seconds with no available agent before the backup group is tried (0-7200).' },
|
|
1473
|
+
inactivityNoAgentActionAfterSeconds: { type: 'number', description: 'Seconds with no available agent before the final action runs (0-14400).' },
|
|
1474
|
+
inactivityTerminalWorkflowId: { type: 'number', description: 'Workflow to run when terminalActionOverride is 8.' },
|
|
1475
|
+
rerouteOnAgentUnavailable: { type: 'boolean', description: 'Presence trigger: also treat a chat as inactive when its agent is no longer Available.' },
|
|
1476
|
+
alertAvailableAgentsOnRelease: { type: 'boolean', description: 'Alert available agents when an inactive chat returns to the queue.' },
|
|
1477
|
+
notifyManagersOnInactivity: { type: 'boolean', description: 'Alert managers about inactive chats.' },
|
|
1478
|
+
rerouteCooldownSeconds: { type: 'number', description: 'Cooldown between moves of the same chat (0-3600).' },
|
|
1479
|
+
queueEscalationAfterMinutes: { type: 'number', description: 'Widen the queue alert to backup group and managers after this many minutes (1-240).' },
|
|
1480
|
+
inactivityVisitorText: { type: 'string', description: 'Message the visitor sees when the chat is moved (max 500 characters).' },
|
|
1481
|
+
inactivityAgentText: { type: 'string', description: 'Message agents see on a re-queued chat (max 500 characters).' },
|
|
1450
1482
|
},
|
|
1451
1483
|
required: [],
|
|
1452
1484
|
},
|
|
@@ -1462,6 +1494,41 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1462
1494
|
required: ['conversationId'],
|
|
1463
1495
|
},
|
|
1464
1496
|
},
|
|
1497
|
+
{
|
|
1498
|
+
name: 'routing_simulate_inactivity',
|
|
1499
|
+
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.',
|
|
1500
|
+
inputSchema: {
|
|
1501
|
+
type: 'object',
|
|
1502
|
+
properties: {
|
|
1503
|
+
teamId: { type: 'number', description: 'Group id whose own policy to simulate. Omit for the site default.' },
|
|
1504
|
+
noAgentSeconds: { type: 'number', description: 'Seconds nobody has been available (0 = just now). Default 0.' },
|
|
1505
|
+
attemptsUsed: { type: 'number', description: 'Moves already used on the chat. Default 0.' },
|
|
1506
|
+
inactivityStep: { type: 'number', description: '0 = nothing tried, 1 = offered to backup group, 2 = final action applied. Default 0.' },
|
|
1507
|
+
secondsSinceLastTransfer: { type: 'number', description: 'Seconds since the chat was last moved. Omit if never moved.' },
|
|
1508
|
+
sameEpisodeAlreadyHandled: { type: 'boolean', description: 'True to simulate the same silence being delivered twice (duplicate guard).' },
|
|
1509
|
+
fromAgentName: { type: 'string', description: 'Name of the silent agent, used in the alert text.' },
|
|
1510
|
+
},
|
|
1511
|
+
required: [],
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
name: 'routing_explain_precedence',
|
|
1516
|
+
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".',
|
|
1517
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: 'routing_inactivity_transfers',
|
|
1521
|
+
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.',
|
|
1522
|
+
inputSchema: {
|
|
1523
|
+
type: 'object',
|
|
1524
|
+
properties: {
|
|
1525
|
+
days: { type: 'number', description: 'Lookback in days (1-92). Default 30.' },
|
|
1526
|
+
agentName: { type: 'string', description: 'Agent display name (partial match) to filter to.' },
|
|
1527
|
+
agentId: { type: 'number', description: 'Exact agent id to filter to.' },
|
|
1528
|
+
},
|
|
1529
|
+
required: [],
|
|
1530
|
+
},
|
|
1531
|
+
},
|
|
1465
1532
|
{
|
|
1466
1533
|
name: 'routing_explain_decision',
|
|
1467
1534
|
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?"',
|
|
@@ -1544,6 +1611,120 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1544
1611
|
description: 'Show what is currently indexed for the site: document counts per source type (kb_article, scraper_page, bigcommerce_product, etc). Use to verify ingestion completed and data is present.',
|
|
1545
1612
|
inputSchema: { type: 'object', properties: {} },
|
|
1546
1613
|
},
|
|
1614
|
+
{
|
|
1615
|
+
name: 'index_move_preview',
|
|
1616
|
+
description: 'Preview moving (or copying) documents between two knowledge indexes on the site. Writes nothing. Returns document/chunk/page counts, per-document method (copy_vectors, rechunk, already_in_target, blocked), quota effect, and whether it can run. No rescan and no OCR is ever needed. Moving into a troubleshooting index needs the EnableTroubleshootingKnowledge entitlement. Wraps POST IndexMove/Preview (velaro-messaging); the site comes from the caller auth, never an argument.',
|
|
1617
|
+
inputSchema: {
|
|
1618
|
+
type: 'object',
|
|
1619
|
+
properties: {
|
|
1620
|
+
fromIndex: { type: 'string', description: 'Source index name' },
|
|
1621
|
+
toIndex: { type: 'string', description: 'Target index name' },
|
|
1622
|
+
kind: { type: 'string', enum: ['move', 'copy'], description: 'Default move' },
|
|
1623
|
+
skipBlocked: { type: 'boolean', description: 'Leave blocked documents in place and move the rest' },
|
|
1624
|
+
selector: {
|
|
1625
|
+
type: 'object',
|
|
1626
|
+
description: 'What to move. Needs at least one field or all:true. Fields: all (bool), jobId, urlPrefix, sourceIds[], fileNames[], docIds[] (numbers), sourceType (website_page | video_transcript | uploaded_document | onedrive_file | custom_content).',
|
|
1627
|
+
},
|
|
1628
|
+
},
|
|
1629
|
+
required: ['fromIndex', 'toIndex', 'selector'],
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
name: 'index_move_execute',
|
|
1634
|
+
description: 'Run a move/copy between knowledge indexes. Always call index_move_preview first and check canExecute. Write-then-delete and resumable: content is never missing from both indexes. Returns a moveId; poll index_move_status. Wraps POST IndexMove/Execute.',
|
|
1635
|
+
inputSchema: {
|
|
1636
|
+
type: 'object',
|
|
1637
|
+
properties: {
|
|
1638
|
+
fromIndex: { type: 'string' },
|
|
1639
|
+
toIndex: { type: 'string' },
|
|
1640
|
+
kind: { type: 'string', enum: ['move', 'copy'] },
|
|
1641
|
+
skipBlocked: { type: 'boolean' },
|
|
1642
|
+
selector: {
|
|
1643
|
+
type: 'object',
|
|
1644
|
+
description: 'What to move. Needs at least one field or all:true. Fields: all (bool), jobId, urlPrefix, sourceIds[], fileNames[], docIds[] (numbers), sourceType (website_page | video_transcript | uploaded_document | onedrive_file | custom_content).',
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
required: ['fromIndex', 'toIndex', 'selector'],
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
name: 'index_split_preview',
|
|
1652
|
+
description: 'Detect manual-like documents in a general index and preview moving them into an existing troubleshooting-purpose index. Writes nothing. Wraps POST IndexMove/Split/Preview. Use index_split_execute to run it.',
|
|
1653
|
+
inputSchema: {
|
|
1654
|
+
type: 'object',
|
|
1655
|
+
properties: {
|
|
1656
|
+
fromIndex: { type: 'string' },
|
|
1657
|
+
toIndex: { type: 'string', description: 'Existing troubleshooting-purpose index' },
|
|
1658
|
+
docIds: { type: 'array', items: { type: 'number' }, description: 'Only these documents (default: every detected manual)' },
|
|
1659
|
+
skipBlocked: { type: 'boolean' },
|
|
1660
|
+
},
|
|
1661
|
+
required: ['fromIndex', 'toIndex'],
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
name: 'index_split_execute',
|
|
1666
|
+
description: 'Run a split: move detected manuals from a general index into an existing troubleshooting-purpose index. Call index_split_preview first. Resumable and undoable via index_move_control. Wraps POST IndexMove/Split/Execute.',
|
|
1667
|
+
inputSchema: {
|
|
1668
|
+
type: 'object',
|
|
1669
|
+
properties: {
|
|
1670
|
+
fromIndex: { type: 'string' },
|
|
1671
|
+
toIndex: { type: 'string' },
|
|
1672
|
+
docIds: { type: 'array', items: { type: 'number' } },
|
|
1673
|
+
skipBlocked: { type: 'boolean' },
|
|
1674
|
+
},
|
|
1675
|
+
required: ['fromIndex', 'toIndex'],
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: 'index_move_status',
|
|
1680
|
+
description: 'Status of one move (moveId) or the recent moves (omit moveId): status, per-document progress, canResume/canAbort/canUndo. Wraps GET IndexMove/Moves[/{id}].',
|
|
1681
|
+
inputSchema: {
|
|
1682
|
+
type: 'object',
|
|
1683
|
+
properties: { moveId: { type: 'string' } },
|
|
1684
|
+
},
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
name: 'index_move_control',
|
|
1688
|
+
description: 'Resume a stopped move, abort a queued/failed move that has not started removing content, or undo a completed move. Wraps POST IndexMove/Moves/{id}/Resume|Abort|Undo.',
|
|
1689
|
+
inputSchema: {
|
|
1690
|
+
type: 'object',
|
|
1691
|
+
properties: {
|
|
1692
|
+
moveId: { type: 'string' },
|
|
1693
|
+
action: { type: 'string', enum: ['resume', 'abort', 'undo'] },
|
|
1694
|
+
},
|
|
1695
|
+
required: ['moveId', 'action'],
|
|
1696
|
+
},
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
name: 'index_mixed_content',
|
|
1700
|
+
description: 'Check whether a general index holds manual-like documents that belong in a Troubleshooting Assistant index. Read-only. Wraps GET IndexMove/MixedContent.',
|
|
1701
|
+
inputSchema: {
|
|
1702
|
+
type: 'object',
|
|
1703
|
+
properties: { indexName: { type: 'string' } },
|
|
1704
|
+
required: ['indexName'],
|
|
1705
|
+
},
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
name: 'index_integrity_check',
|
|
1709
|
+
description: 'Read-only integrity report for a knowledge index: orphan chunks, records without chunks, count mismatches, duplicate records. Wraps GET IndexMove/Integrity.',
|
|
1710
|
+
inputSchema: {
|
|
1711
|
+
type: 'object',
|
|
1712
|
+
properties: { indexName: { type: 'string' } },
|
|
1713
|
+
required: ['indexName'],
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
name: 'index_integrity_repair',
|
|
1718
|
+
description: 'Repair a knowledge index. Additive and idempotent: corrects counts and adopts orphan chunks into tracking rows, never deletes vectors. removeDuplicateRecords also removes duplicate tracking rows. Run index_integrity_check first. Wraps POST IndexMove/Integrity/Repair.',
|
|
1719
|
+
inputSchema: {
|
|
1720
|
+
type: 'object',
|
|
1721
|
+
properties: {
|
|
1722
|
+
indexName: { type: 'string' },
|
|
1723
|
+
removeDuplicateRecords: { type: 'boolean' },
|
|
1724
|
+
},
|
|
1725
|
+
required: ['indexName'],
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1547
1728
|
{
|
|
1548
1729
|
name: 'index_ingest',
|
|
1549
1730
|
description: 'Trigger ingestion of a scraper job into the knowledge index. Fires in the background; use index_status after ~2 minutes to verify chunks appeared.',
|
|
@@ -2282,6 +2463,17 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2282
2463
|
required: ['siteId'],
|
|
2283
2464
|
},
|
|
2284
2465
|
},
|
|
2466
|
+
{
|
|
2467
|
+
name: 'tour_insights',
|
|
2468
|
+
description: 'Moshky setup-tour funnel and feedback (Velaro staff only). Without siteId: per-tour, per-step counts of offered/opened/step_viewed/arrived/stuck/completed/dismissed/load_error over the last N days plus recent scrubbed feedback. With siteId: that one site\'s tour event timeline and feedback. Feedback comments are PII-scrubbed and for staff eyes only.',
|
|
2469
|
+
inputSchema: {
|
|
2470
|
+
type: 'object',
|
|
2471
|
+
properties: {
|
|
2472
|
+
siteId: { type: 'number', description: 'Optional. When set, returns that site\'s tour timeline instead of the cross-site funnel.' },
|
|
2473
|
+
days: { type: 'number', description: 'Look-back window in days (default 30, max 365)' },
|
|
2474
|
+
},
|
|
2475
|
+
},
|
|
2476
|
+
},
|
|
2285
2477
|
{
|
|
2286
2478
|
name: 'support_extend_subscription',
|
|
2287
2479
|
description: 'Extend a customer site\'s subscription expiration by N days. SuperAdmin only.',
|
|
@@ -2886,6 +3078,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2886
3078
|
},
|
|
2887
3079
|
},
|
|
2888
3080
|
|
|
3081
|
+
{
|
|
3082
|
+
name: 'magento_diagnose',
|
|
3083
|
+
description: 'Run the Magento live-connection diagnostic for a site: config present, access token valid, /rest/V1 reachability, store code/base URL, and ACL scope probes. Calls messaging GET diagnostics/test-magento?siteId=<id>. Read-only.',
|
|
3084
|
+
inputSchema: {
|
|
3085
|
+
type: 'object',
|
|
3086
|
+
properties: {
|
|
3087
|
+
siteId: { type: 'number', description: 'Site to diagnose (e.g. 1032)' },
|
|
3088
|
+
},
|
|
3089
|
+
required: ['siteId'],
|
|
3090
|
+
},
|
|
3091
|
+
},
|
|
3092
|
+
|
|
2889
3093
|
// ── CallRail Attribution ──────────────────────────────────────────────────────
|
|
2890
3094
|
{
|
|
2891
3095
|
name: 'callrail_get_attributions',
|
|
@@ -4248,6 +4452,34 @@ function parseBusinessHours(input) {
|
|
|
4248
4452
|
|
|
4249
4453
|
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
4250
4454
|
|
|
4455
|
+
// -- Agent inactivity chain helpers (velaro-messaging PR #1471, v20 ONLY) ------------------------
|
|
4456
|
+
// Terminal action ints are persisted by messaging (append-only enum). The API returns/accepts the NAME.
|
|
4457
|
+
const TERMINAL_ACTION_NAMES = ['Survey','CreateTicket','SurveyAndCreateTicket','Queue','OfflineMessage','NoOp','EndChat','NotifyManager','RunWorkflow'];
|
|
4458
|
+
function terminalActionLabel(v) {
|
|
4459
|
+
if (v === null || v === undefined || v === '') return 'Default (Survey)';
|
|
4460
|
+
if (typeof v === 'number') return TERMINAL_ACTION_NAMES[v] ?? String(v);
|
|
4461
|
+
return String(v);
|
|
4462
|
+
}
|
|
4463
|
+
function terminalActionForSave(v) {
|
|
4464
|
+
if (v === null || v === undefined || v === '') return null;
|
|
4465
|
+
const valid = TERMINAL_ACTION_NAMES.map((n, i) => `${i}=${n}`).join(' ');
|
|
4466
|
+
// Numeric strings ("7") count as ints, the same as the CLI.
|
|
4467
|
+
if (typeof v === 'string' && /^\d+$/.test(v.trim())) v = Number(v.trim());
|
|
4468
|
+
if (typeof v === 'number') {
|
|
4469
|
+
const name = Number.isInteger(v) ? TERMINAL_ACTION_NAMES[v] : undefined;
|
|
4470
|
+
if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-${TERMINAL_ACTION_NAMES.length - 1}: ${valid}`);
|
|
4471
|
+
return name;
|
|
4472
|
+
}
|
|
4473
|
+
const hit = TERMINAL_ACTION_NAMES.find((n) => n.toLowerCase() === String(v).toLowerCase());
|
|
4474
|
+
if (!hit) throw new Error(`Unknown terminal action "${v}". Use one of: ${valid}`);
|
|
4475
|
+
return hit;
|
|
4476
|
+
}
|
|
4477
|
+
const CHAIN_FIELDS = [
|
|
4478
|
+
'enabled','backupTeamId','backupTeamAfterSeconds','noTargetActionAfterSeconds','rerouteOnAgentUnavailable',
|
|
4479
|
+
'alertAvailableAgentsOnRelease','notifyManagersOnInactivity','rerouteCooldownSeconds','queueEscalationAfterMinutes',
|
|
4480
|
+
'terminalWorkflowId','visitorStatusText','agentStatusText',
|
|
4481
|
+
];
|
|
4482
|
+
|
|
4251
4483
|
async function handleTool(name, args) {
|
|
4252
4484
|
switch (name) {
|
|
4253
4485
|
case 'case_list_types': {
|
|
@@ -4506,6 +4738,11 @@ async function handleTool(name, args) {
|
|
|
4506
4738
|
return JSON.stringify(result, null, 2);
|
|
4507
4739
|
}
|
|
4508
4740
|
|
|
4741
|
+
case 'site_scraper_usage': {
|
|
4742
|
+
const result = await api('GET', `ScraperJobs/StaffUsage/${encodeURIComponent(args.siteId)}`);
|
|
4743
|
+
return JSON.stringify(result, null, 2);
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4509
4746
|
case 'apple_health': {
|
|
4510
4747
|
const h = await messagingGet(`/SupportTools/sites/${args.siteId}/apple/health`);
|
|
4511
4748
|
return JSON.stringify(h, null, 2);
|
|
@@ -5179,9 +5416,23 @@ async function handleTool(name, args) {
|
|
|
5179
5416
|
|
|
5180
5417
|
case 'routing_get_policy': {
|
|
5181
5418
|
const p = await messagingGet('/AutoRoutingPolicy');
|
|
5182
|
-
const terminalLabel = p.terminalActionOverride
|
|
5183
|
-
|
|
5184
|
-
|
|
5419
|
+
const terminalLabel = terminalActionLabel(p.terminalActionOverride);
|
|
5420
|
+
const chain = p.inactivityChain;
|
|
5421
|
+
const chainLines = chain ? [
|
|
5422
|
+
'',
|
|
5423
|
+
`Reassignment mode: ${p.reassignmentMode ?? 'Observe'}`,
|
|
5424
|
+
`Inactivity chain: ${chain.enabled ? 'ON' : 'off'}`,
|
|
5425
|
+
...(chain.enabled ? [
|
|
5426
|
+
` Backup group: ${chain.backupTeamId ?? 'none'}${chain.backupTeamId != null ? ` after ${chain.backupTeamAfterSeconds}s` : ''}`,
|
|
5427
|
+
` Final action after: ${chain.noTargetActionAfterSeconds != null ? `${chain.noTargetActionAfterSeconds}s with no agent available` : 'only when the hop cap is used up'}`,
|
|
5428
|
+
` Cooldown between moves: ${chain.rerouteCooldownSeconds}s`,
|
|
5429
|
+
` Presence trigger: ${chain.rerouteOnAgentUnavailable}`,
|
|
5430
|
+
` Alert agents on release: ${chain.alertAvailableAgentsOnRelease}`,
|
|
5431
|
+
` Notify managers: ${chain.notifyManagersOnInactivity}`,
|
|
5432
|
+
` Queue escalation: ${chain.queueEscalationAfterMinutes != null ? `${chain.queueEscalationAfterMinutes} min` : 'never'}`,
|
|
5433
|
+
` Terminal workflow: ${chain.terminalWorkflowId ?? 'none'}`,
|
|
5434
|
+
] : []),
|
|
5435
|
+
] : ['', '(This messaging build does not report the inactivity chain.)'];
|
|
5185
5436
|
return [
|
|
5186
5437
|
`Enabled: ${p.enabled}`,
|
|
5187
5438
|
`Initial response timeout: ${p.initialResponseTimeoutSeconds}s`,
|
|
@@ -5193,6 +5444,7 @@ async function handleTool(name, args) {
|
|
|
5193
5444
|
`Agent follow-up flag: ${p.agentFollowUpFlagEnabled}`,
|
|
5194
5445
|
`Queue unavailable grace: ${p.queueUnavailableGracePeriodSeconds}s`,
|
|
5195
5446
|
`Terminal action: ${terminalLabel}`,
|
|
5447
|
+
...chainLines,
|
|
5196
5448
|
].join('\n');
|
|
5197
5449
|
}
|
|
5198
5450
|
|
|
@@ -5209,8 +5461,46 @@ async function handleTool(name, args) {
|
|
|
5209
5461
|
for (const f of fields) {
|
|
5210
5462
|
if (args[f] !== undefined) merged[f] = args[f];
|
|
5211
5463
|
}
|
|
5212
|
-
|
|
5213
|
-
|
|
5464
|
+
// Terminal action travels as the enum name; the API returns the name too, so normalize either way.
|
|
5465
|
+
merged.terminalActionOverride = terminalActionForSave(merged.terminalActionOverride);
|
|
5466
|
+
// RunWorkflow needs a workflow whatever the chain switch says: the final action also runs at the hop cap.
|
|
5467
|
+
// An explicit null clears the workflow, so it must fail the guard (?? would have fallen back to the saved one).
|
|
5468
|
+
const effectiveWorkflowId = args.inactivityTerminalWorkflowId !== undefined
|
|
5469
|
+
? args.inactivityTerminalWorkflowId
|
|
5470
|
+
: current.inactivityChain?.terminalWorkflowId;
|
|
5471
|
+
if (merged.terminalActionOverride === 'RunWorkflow' && effectiveWorkflowId == null) {
|
|
5472
|
+
throw new Error('Terminal action 8 (RunWorkflow) needs inactivityTerminalWorkflowId.');
|
|
5473
|
+
}
|
|
5474
|
+
// Actions 6-8 exist only on a messaging build with the inactivity chain; an older one would store a different value.
|
|
5475
|
+
if (['EndChat', 'NotifyManager', 'RunWorkflow'].includes(merged.terminalActionOverride) && !('inactivityChain' in current)) {
|
|
5476
|
+
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.`);
|
|
5477
|
+
}
|
|
5478
|
+
if (args.exhaustionMessageTemplate !== undefined) merged.exhaustionMessageTemplate = args.exhaustionMessageTemplate;
|
|
5479
|
+
// Always send the mode back: an absent value is treated as Observe by the API.
|
|
5480
|
+
merged.reassignmentMode = args.reassignmentMode ?? current.reassignmentMode ?? 'Observe';
|
|
5481
|
+
const chainPatch = {
|
|
5482
|
+
enabled: args.inactivityChainEnabled,
|
|
5483
|
+
backupTeamId: args.inactivityBackupTeamId,
|
|
5484
|
+
backupTeamAfterSeconds: args.inactivityBackupTeamAfterSeconds,
|
|
5485
|
+
noTargetActionAfterSeconds: args.inactivityNoAgentActionAfterSeconds,
|
|
5486
|
+
terminalWorkflowId: args.inactivityTerminalWorkflowId,
|
|
5487
|
+
rerouteOnAgentUnavailable: args.rerouteOnAgentUnavailable,
|
|
5488
|
+
alertAvailableAgentsOnRelease: args.alertAvailableAgentsOnRelease,
|
|
5489
|
+
notifyManagersOnInactivity: args.notifyManagersOnInactivity,
|
|
5490
|
+
rerouteCooldownSeconds: args.rerouteCooldownSeconds,
|
|
5491
|
+
queueEscalationAfterMinutes: args.queueEscalationAfterMinutes,
|
|
5492
|
+
visitorStatusText: args.inactivityVisitorText,
|
|
5493
|
+
agentStatusText: args.inactivityAgentText,
|
|
5494
|
+
};
|
|
5495
|
+
if (current.inactivityChain || Object.values(chainPatch).some((v) => v !== undefined)) {
|
|
5496
|
+
const chain = { ...(current.inactivityChain ?? {}) };
|
|
5497
|
+
for (const f of CHAIN_FIELDS) if (chainPatch[f] !== undefined) chain[f] = chainPatch[f];
|
|
5498
|
+
merged.inactivityChain = chain;
|
|
5499
|
+
}
|
|
5500
|
+
await messagingPut('/AutoRoutingPolicy', merged);
|
|
5501
|
+
// PUT answers { updated: true }, not the policy: re-read so the reply reports what is really stored.
|
|
5502
|
+
const saved = await messagingGet('/AutoRoutingPolicy');
|
|
5503
|
+
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
5504
|
}
|
|
5215
5505
|
|
|
5216
5506
|
case 'routing_get_decision_log': {
|
|
@@ -5241,13 +5531,81 @@ async function handleTool(name, args) {
|
|
|
5241
5531
|
if (result.policySnapshot) {
|
|
5242
5532
|
const p = result.policySnapshot;
|
|
5243
5533
|
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
|
|
5534
|
+
lines.push(`Policy settings: initial timeout ${p.initialResponseTimeoutSeconds}s, idle timeout ${p.idleAfterResponseTimeoutSeconds}s, max reroutes ${p.maxRerouteAttempts}, terminal action ${terminalActionLabel(p.terminalActionOverride)}`);
|
|
5245
5535
|
if (!p.allowBounceBack) lines.push('Anti-bounce is ON — timed-out agents are excluded from re-routing for this conversation.');
|
|
5246
5536
|
if (p.preferPreviousAgentOnTimeout) lines.push('Loyalty preference is ON — on timeout, the engine prefers the agent from a prior session.');
|
|
5247
5537
|
}
|
|
5248
5538
|
return lines.join('\n');
|
|
5249
5539
|
}
|
|
5250
5540
|
|
|
5541
|
+
case 'routing_simulate_inactivity': {
|
|
5542
|
+
const body = {
|
|
5543
|
+
teamId: args.teamId ?? null,
|
|
5544
|
+
noAgentSeconds: args.noAgentSeconds ?? 0,
|
|
5545
|
+
attemptsUsed: args.attemptsUsed ?? 0,
|
|
5546
|
+
inactivityStep: args.inactivityStep ?? 0,
|
|
5547
|
+
secondsSinceLastTransfer: args.secondsSinceLastTransfer ?? null,
|
|
5548
|
+
sameEpisodeAlreadyHandled: args.sameEpisodeAlreadyHandled ?? false,
|
|
5549
|
+
fromAgentName: args.fromAgentName ?? null,
|
|
5550
|
+
};
|
|
5551
|
+
const r = await messagingPost('/Routing/Inactivity/simulate', body);
|
|
5552
|
+
const gate = {
|
|
5553
|
+
NotEnabled: "The inactivity chain is off for this rule, so today's behavior applies and nothing new happens.",
|
|
5554
|
+
Proceed: 'Allowed to move the chat.',
|
|
5555
|
+
SkipDedupe: 'Blocked: this exact silence was already handled (double-transfer guard).',
|
|
5556
|
+
SkipCooldown: 'Blocked: the chat was moved too recently (cooldown).',
|
|
5557
|
+
HopCapReached: 'Hop cap reached: no more moves, so the final action applies.',
|
|
5558
|
+
}[r.gate] ?? r.gate;
|
|
5559
|
+
const action = {
|
|
5560
|
+
LegacyDefer: 'Nothing new is configured, so the chat stays with its current owner.',
|
|
5561
|
+
Hold: 'Still inside the wait window, nothing happens yet.',
|
|
5562
|
+
WidenToBackupTeam: 'The chat is offered to the backup group.',
|
|
5563
|
+
ApplyTerminal: 'The final action runs.',
|
|
5564
|
+
}[r.noTargetAction] ?? r.noTargetAction;
|
|
5565
|
+
const lines = [
|
|
5566
|
+
'DRY RUN (nothing sent, no live chat touched)',
|
|
5567
|
+
`Settings used: ${r.ruleSource}`,
|
|
5568
|
+
`Safety gate: ${gate}`,
|
|
5569
|
+
`Decision: ${action}`,
|
|
5570
|
+
r.explanation ? `Why: ${r.explanation}` : null,
|
|
5571
|
+
r.nextStepAtSeconds != null ? `Next step after ${r.nextStepAtSeconds}s with no agent available.` : null,
|
|
5572
|
+
`Visitor would see: "${r.visitorMessage}"`,
|
|
5573
|
+
`Agents would see: "${r.queueAlert || r.agentStatus}"`,
|
|
5574
|
+
].filter(Boolean);
|
|
5575
|
+
for (const l of r.otherRulesThatMatchedButLost ?? []) lines.push(`Also matched but lost: ${l.rule} (${l.reason})`);
|
|
5576
|
+
return lines.join('\n');
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
case 'routing_explain_precedence': {
|
|
5580
|
+
const [defaults, conflicts] = await Promise.all([
|
|
5581
|
+
messagingGet('/Routing/Inactivity/defaults'),
|
|
5582
|
+
messagingGet('/Routing/Inactivity/conflicts'),
|
|
5583
|
+
]);
|
|
5584
|
+
const lines = ['Evaluation order (top to bottom):', ...(defaults.precedence ?? []).map((p) => ` ${p}`), ''];
|
|
5585
|
+
const list = conflicts.conflicts ?? [];
|
|
5586
|
+
if (!list.length) lines.push('Warnings: none. No group policy cancels out the site default.');
|
|
5587
|
+
else {
|
|
5588
|
+
lines.push('Warnings:');
|
|
5589
|
+
for (const c of list) lines.push(` [${String(c.severity).toUpperCase()}] ${c.teamId != null ? `group ${c.teamId}` : 'site default'}: ${c.message}`);
|
|
5590
|
+
}
|
|
5591
|
+
return lines.join('\n');
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
case 'routing_inactivity_transfers': {
|
|
5595
|
+
let path = `/RoutingDiagnostics/inactivity-transfers?days=${args.days ?? 30}`;
|
|
5596
|
+
if (args.agentId != null) path += `&agentId=${args.agentId}`;
|
|
5597
|
+
if (args.agentName) path += `&agentName=${encodeURIComponent(args.agentName)}`;
|
|
5598
|
+
const r = await api('GET', path);
|
|
5599
|
+
if (r.note && !r.total && !(r.byAgent ?? []).length) return r.note;
|
|
5600
|
+
const lines = [`Agent inactivity transfers, last ${r.days} days: ${r.total}${r.capped ? ' (capped at 5000)' : ''}`];
|
|
5601
|
+
if (r.note) lines.push(r.note);
|
|
5602
|
+
if (r.matchedAgentsCapped) lines.push('The name matched more agents than are counted here. Use a longer name or agentId.');
|
|
5603
|
+
for (const a of r.byAgent ?? []) lines.push(` ${a.name}: ${a.count}`);
|
|
5604
|
+
if ((r.byDay ?? []).length) lines.push('', 'By day (Pacific): ' + r.byDay.map((d) => `${d.date}=${d.count}`).join(', '));
|
|
5605
|
+
if (!r.total) lines.push('None. Check that the inactivity chain is on and reassignment mode is not Observe.');
|
|
5606
|
+
return lines.join('\n');
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5251
5609
|
// ── Livefluence Migration ──────────────────────────────────────────────
|
|
5252
5610
|
case 'livefluence_export': {
|
|
5253
5611
|
const qs = args.v20_site_id ? `?v20SiteId=${args.v20_site_id}` : '';
|
|
@@ -5650,6 +6008,53 @@ async function handleTool(name, args) {
|
|
|
5650
6008
|
return lines.join('\n');
|
|
5651
6009
|
}
|
|
5652
6010
|
|
|
6011
|
+
case 'index_move_preview': {
|
|
6012
|
+
const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
|
|
6013
|
+
const r = await messagingPost('/IndexMove/Preview', { fromIndex, toIndex, kind, skipBlocked, selector });
|
|
6014
|
+
return JSON.stringify(r, null, 2);
|
|
6015
|
+
}
|
|
6016
|
+
case 'index_move_execute': {
|
|
6017
|
+
const { fromIndex, toIndex, kind, skipBlocked, selector } = args;
|
|
6018
|
+
const r = await messagingPost('/IndexMove/Execute', { fromIndex, toIndex, kind, skipBlocked, selector });
|
|
6019
|
+
return JSON.stringify(r, null, 2);
|
|
6020
|
+
}
|
|
6021
|
+
case 'index_split_preview': {
|
|
6022
|
+
const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
|
|
6023
|
+
const r = await messagingPost('/IndexMove/Split/Preview', body);
|
|
6024
|
+
return JSON.stringify(r, null, 2);
|
|
6025
|
+
}
|
|
6026
|
+
case 'index_split_execute': {
|
|
6027
|
+
const body = { fromIndex: args.fromIndex, toIndex: args.toIndex, docIds: args.docIds, skipBlocked: args.skipBlocked };
|
|
6028
|
+
const r = await messagingPost('/IndexMove/Split/Execute', body);
|
|
6029
|
+
return JSON.stringify(r, null, 2);
|
|
6030
|
+
}
|
|
6031
|
+
case 'index_move_status': {
|
|
6032
|
+
const r = await messagingGet(args.moveId
|
|
6033
|
+
? `/IndexMove/Moves/${encodeURIComponent(args.moveId)}`
|
|
6034
|
+
: '/IndexMove/Moves?take=25');
|
|
6035
|
+
return JSON.stringify(r, null, 2);
|
|
6036
|
+
}
|
|
6037
|
+
case 'index_move_control': {
|
|
6038
|
+
const verb = { resume: 'Resume', abort: 'Abort', undo: 'Undo' }[args.action];
|
|
6039
|
+
if (!verb) throw new Error('action must be resume, abort or undo');
|
|
6040
|
+
const r = await messagingPost(`/IndexMove/Moves/${encodeURIComponent(args.moveId)}/${verb}`);
|
|
6041
|
+
return JSON.stringify(r, null, 2);
|
|
6042
|
+
}
|
|
6043
|
+
case 'index_mixed_content': {
|
|
6044
|
+
const r = await messagingGet(`/IndexMove/MixedContent?index=${encodeURIComponent(args.indexName)}`);
|
|
6045
|
+
return JSON.stringify(r, null, 2);
|
|
6046
|
+
}
|
|
6047
|
+
case 'index_integrity_check': {
|
|
6048
|
+
const r = await messagingGet(`/IndexMove/Integrity?index=${encodeURIComponent(args.indexName)}`);
|
|
6049
|
+
return JSON.stringify(r, null, 2);
|
|
6050
|
+
}
|
|
6051
|
+
case 'index_integrity_repair': {
|
|
6052
|
+
const r = await messagingPost('/IndexMove/Integrity/Repair', {
|
|
6053
|
+
indexName: args.indexName,
|
|
6054
|
+
removeDuplicateRecords: !!args.removeDuplicateRecords,
|
|
6055
|
+
});
|
|
6056
|
+
return JSON.stringify(r, null, 2);
|
|
6057
|
+
}
|
|
5653
6058
|
case 'index_status': {
|
|
5654
6059
|
const sources = await api('GET', '/AzureIndexes/IntegrationSources');
|
|
5655
6060
|
if (!Array.isArray(sources) || !sources.length) return 'No indexed sources found. Run index_ingest to populate.';
|
|
@@ -6539,7 +6944,7 @@ async function handleTool(name, args) {
|
|
|
6539
6944
|
|
|
6540
6945
|
case 'support_user_lookup': {
|
|
6541
6946
|
const users = await api('GET', `/SupportTools/users/search?q=${encodeURIComponent(args.query)}`);
|
|
6542
|
-
if (!Array.isArray(users) || !users.length) return `No users found matching "${args.query}".`;
|
|
6947
|
+
if (!Array.isArray(users) || !users.length) return `No users found matching "${args.query}" in ${ENV_LABEL}. If this looks like a real customer, re-run with VELARO_ADMIN_API set to the other environment before concluding they don't exist.`;
|
|
6543
6948
|
return users.map(u => {
|
|
6544
6949
|
const roles = u.sites?.map(s =>
|
|
6545
6950
|
` site ${s.siteId} (${s.companyName}) — ${s.isAdministrator ? 'Admin' : s.isManager ? 'Manager' : 'Agent'}${s.isActive === false ? ' INACTIVE' : ''}`
|
|
@@ -6555,7 +6960,7 @@ async function handleTool(name, args) {
|
|
|
6555
6960
|
|
|
6556
6961
|
case 'support_site_lookup': {
|
|
6557
6962
|
const sites = await api('GET', `/SupportTools/sites/search?q=${encodeURIComponent(args.query)}`);
|
|
6558
|
-
if (!Array.isArray(sites) || !sites.length) return `No sites found matching "${args.query}".`;
|
|
6963
|
+
if (!Array.isArray(sites) || !sites.length) return `No sites found matching "${args.query}" in ${ENV_LABEL}. If this looks like a real customer, re-run with VELARO_ADMIN_API set to the other environment before concluding they don't exist.`;
|
|
6559
6964
|
return sites.map(s => {
|
|
6560
6965
|
const admins = s.admins?.map(a => ` ${a.userName} (${a.firstName} ${a.lastName})`).join('\n') ?? 'none';
|
|
6561
6966
|
return `[siteId=${s.siteId}] ${s.companyName}${s.isTestSite ? ' [TEST]' : ''}\n URL: ${s.companyURL ?? '—'} | Created: ${s.dateCreated ? s.dateCreated.slice(0,10) : '—'}\n Admins:\n${admins}`;
|
|
@@ -6652,6 +7057,14 @@ async function handleTool(name, args) {
|
|
|
6652
7057
|
return lines.join('\n');
|
|
6653
7058
|
}
|
|
6654
7059
|
|
|
7060
|
+
case 'tour_insights': {
|
|
7061
|
+
const days = Math.min(365, Math.max(1, args.days ?? 30));
|
|
7062
|
+
const path = args.siteId
|
|
7063
|
+
? `/SupportTools/sites/${args.siteId}/tour-events?days=${days}`
|
|
7064
|
+
: `/SupportTools/tour-insights?days=${days}`;
|
|
7065
|
+
return JSON.stringify(await api('GET', path), null, 2);
|
|
7066
|
+
}
|
|
7067
|
+
|
|
6655
7068
|
case 'support_extend_subscription': {
|
|
6656
7069
|
const res = await api('POST', `/SupportTools/sites/${args.siteId}/extend-subscription`, { days: args.days });
|
|
6657
7070
|
return res?.success
|
|
@@ -7231,6 +7644,12 @@ async function handleTool(name, args) {
|
|
|
7231
7644
|
return lines.join('\n');
|
|
7232
7645
|
}
|
|
7233
7646
|
|
|
7647
|
+
case 'magento_diagnose': {
|
|
7648
|
+
if (!args.siteId) return 'siteId is required.';
|
|
7649
|
+
const r = await api('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(args.siteId)}`);
|
|
7650
|
+
return JSON.stringify(r, null, 2);
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7234
7653
|
// ── CallRail Attribution ──────────────────────────────────────────────────
|
|
7235
7654
|
case 'callrail_get_attributions': {
|
|
7236
7655
|
const limit = Math.min(100, args.limit ?? 25);
|