@velaro/mcp-server 0.6.102 → 0.6.104
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 +6 -0
- package/log-search.js +252 -0
- package/messaging-base.js +21 -0
- package/package.json +5 -2
- package/server.js +519 -45
- package/tool-annotations.js +138 -0
package/server.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* VELARO_MCP_MODE=http PORT=3000 node server.js
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
-
import { describeRawMeta, fetchRetry429 } from './log-search.js';
|
|
43
|
+
import { buildRawLogQuery, describeRawMeta, fetchRetry429, resolveLogWindow } from './log-search.js';
|
|
44
44
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
45
45
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
46
46
|
import {
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
GetPromptRequestSchema,
|
|
50
50
|
ListPromptsRequestSchema,
|
|
51
51
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
52
|
+
import { withAnnotations } from './tool-annotations.js';
|
|
53
|
+
import { resolveMessagingApi } from './messaging-base.js';
|
|
52
54
|
|
|
53
55
|
// Default was 'https://help.velaro.com'. Renamed to the recognizable admin API hostname for
|
|
54
56
|
// clarity during the 2026-09-10 Donaldson bot Calendly status investigation
|
|
@@ -59,7 +61,7 @@ import {
|
|
|
59
61
|
// caused that investigation's status/reality contradiction, it was NOT this default resolving to a
|
|
60
62
|
// broken or different host; that theory is disproven. Left as a naming clarity improvement only.
|
|
61
63
|
const API_BASE = process.env.VELARO_ADMIN_API || 'https://api-admin-us-east.velaro.com';
|
|
62
|
-
const MESSAGING_API = process.env.VELARO_MESSAGING_API
|
|
64
|
+
const MESSAGING_API = resolveMessagingApi(process.env.VELARO_MESSAGING_API, API_BASE);
|
|
63
65
|
const MCP_KEY = process.env.VELARO_MCP_KEY;
|
|
64
66
|
const JWT = process.env.VELARO_JWT;
|
|
65
67
|
|
|
@@ -320,7 +322,7 @@ async function gatewayInit() {
|
|
|
320
322
|
// Tools served by velaro-messaging's admin-tools gateway (McpAdminGatewayController, POST /mcp/v1/admin-tools/call,
|
|
321
323
|
// no session). The admin /mcp gateway only serves kb_* tools, so these used to fail there.
|
|
322
324
|
const MESSAGING_ADMIN_TOOLS = new Set([
|
|
323
|
-
'workflow_create', 'workflow_list', 'workflow_get', 'workflow_toggle',
|
|
325
|
+
'workflow_create', 'workflow_list', 'workflow_get', 'workflow_toggle', 'workflow_run_now',
|
|
324
326
|
'bot_create', 'bot_list', 'bot_get', 'bot_update_prompt', 'bot_set_knowledge',
|
|
325
327
|
'ai_config_attach_index', 'ai_config_set_reply_format', 'call_pop_settings_get', 'call_pop_settings_set',
|
|
326
328
|
'crm_mobile_fields_get', 'crm_mobile_fields_set',
|
|
@@ -488,7 +490,7 @@ const TOOLS = [
|
|
|
488
490
|
},
|
|
489
491
|
{
|
|
490
492
|
name: 'notifications_get',
|
|
491
|
-
description: "Get the calling agent's own notification/SMS alert settings (new conversation, missed chat, unassign warning, text-alert toggles, SMS phone number).",
|
|
493
|
+
description: "Get the calling agent's own notification/SMS alert settings (new conversation, missed chat, unassign warning, text-alert toggles, SMS phone number) plus alertRouting: which Channel (web, sms, whatsapp, appleMessages, email, phone) x Handling (mine, unassigned, bot, teammates) x Kind (conversations, tickets, calls, visitors) alerts fire, and ivrBotAlerts (opt-in for inbound IVR bot chats).",
|
|
492
494
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
493
495
|
},
|
|
494
496
|
{
|
|
@@ -508,6 +510,35 @@ const TOOLS = [
|
|
|
508
510
|
smsOnUnassignWarning: { type: 'boolean', description: 'Send an SMS on unassign warning' },
|
|
509
511
|
useProfilePhone: { type: 'boolean', description: 'Use the profile phone number for SMS alerts' },
|
|
510
512
|
smsAlertPhone: { type: 'string', description: 'Phone number for SMS alerts (E.164, e.g. +14155551234)' },
|
|
513
|
+
alertRouting: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
description: 'Alert routing, partial update (omitted keys unchanged). An alert fires only if its channel, handling and kind are all on. Example: {"channels":{"sms":true,"web":false},"handling":{"mine":true,"unassigned":false},"kinds":{"tickets":false},"ivrBotAlerts":true}',
|
|
516
|
+
properties: {
|
|
517
|
+
channels: { type: 'object', description: 'Keys: web, sms, whatsapp, appleMessages, email, phone (booleans)' },
|
|
518
|
+
handling: { type: 'object', description: 'Keys: mine, unassigned, bot, teammates (booleans, multi-select)' },
|
|
519
|
+
kinds: { type: 'object', description: 'Keys: conversations, tickets, calls, visitors (booleans)' },
|
|
520
|
+
ivrBotAlerts: { type: 'boolean', description: 'Alert for inbound IVR / phone bot chats (default off)' },
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: 'signature_policy_get',
|
|
528
|
+
description: "Get the company email signature policy for the calling admin's site: the company-wide row (teamId 0), any per-team overrides, and the suggested legal disclaimer text. Modes: locked (users cannot use personal signatures), company-default (company signature is the default, personal allowed), personal-only (the default when nothing is set). The legal disclaimer is off unless turned on. Admin only.",
|
|
529
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: 'signature_policy_set',
|
|
533
|
+
description: "Update the company email signature policy (admin only). Only the fields passed are changed; the current row is read first and merged. teamId 0 (default) is the company-wide policy; a team id sets a per-team override. Pass mode 'default' or null to clear a value back to following the company-wide setting. Use signature_policy_get first.",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
type: 'object',
|
|
536
|
+
properties: {
|
|
537
|
+
teamId: { type: 'integer', description: '0 = company-wide (default), otherwise the team id for a per-team override' },
|
|
538
|
+
mode: { type: 'string', enum: ['locked', 'company-default', 'personal-only', 'default'], description: 'locked | company-default | personal-only | default (follow company-wide)' },
|
|
539
|
+
corporateSignature: { type: ['string', 'null'], description: 'Company signature HTML, max 4000 characters. Empty or null clears it.' },
|
|
540
|
+
disclaimerEnabled: { type: ['boolean', 'null'], description: 'Turn the legal disclaimer on or off. null follows the company-wide choice. Off by default.' },
|
|
541
|
+
disclaimerText: { type: ['string', 'null'], description: 'Legal disclaimer text, max 2000 characters. Empty or null clears it.' },
|
|
511
542
|
},
|
|
512
543
|
},
|
|
513
544
|
},
|
|
@@ -1058,6 +1089,18 @@ const TOOLS = [
|
|
|
1058
1089
|
required: ['id', 'enabled'],
|
|
1059
1090
|
},
|
|
1060
1091
|
},
|
|
1092
|
+
{
|
|
1093
|
+
name: 'workflow_run_now',
|
|
1094
|
+
description: 'Queue an enabled workflow to run now, optionally seeded with a shallow JSON context object (max 50 keys, 4KB) that becomes the run\'s workflow attributes.',
|
|
1095
|
+
inputSchema: {
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
properties: {
|
|
1098
|
+
id: { type: 'number', description: 'Workflow ID' },
|
|
1099
|
+
context: { type: 'object', description: 'Optional shallow object of values to seed the run, e.g. {"customerId":"123"}' },
|
|
1100
|
+
},
|
|
1101
|
+
required: ['id'],
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1061
1104
|
{
|
|
1062
1105
|
name: 'workflow_create',
|
|
1063
1106
|
description: 'Create a new workflow with optional pre-built nodes. Use this to build a workflow for the user from a description. Pass initialNodesBlob as a JSON string of the nodes object (keys are string IDs "1","2","3"..., values are node objects). For integration AI workflows, use 3 nodes: trigger (type:"trigger", trigger:"conversation_opened", overrideRouting:true, channels:[], triggerExpression:null), sendMessage (type:"action", actionType:"sendMessage", text:"greeting", child:3), and the AI node (type:"action", actionType:"dynamicsAI"|"netsuiteAI"|"hubSpotAI"|"shopifyAI"|"bigCommerceAI"|"woocommerceAI", configId fields null, aiConfigId:null, mcpServerIds:[]). The workflow is created disabled — activate it separately with workflow_toggle.',
|
|
@@ -1087,6 +1130,8 @@ Key node shapes:
|
|
|
1087
1130
|
propName:"myAnswerVar" } // variable that stores the result as JSON array string
|
|
1088
1131
|
• condition: { type:"action", actionType:"condition", conditionOptions:[...] }
|
|
1089
1132
|
• transferToAI: { type:"action", actionType:"transferToAI", aiConfigId:N }
|
|
1133
|
+
• sendSurvey: { type:"action", actionType:"sendSurvey", sendSurveyId:N, surveyDeliveryMode:"conversational"|"form", closeAfterSurvey:true|false, child:N }
|
|
1134
|
+
closeAfterSurvey (optional, default false): when true the chat closes once the visitor completes the survey (use for Unavailable/after-hours surveys that replace a live chat). Omit to keep the old behaviour.
|
|
1090
1135
|
AI follow-up suggestions ("AI mode"): add suggestFollowUps:true|false (omit = use the account default, set via ai_followups_site_set), suggestFollowUpCount:2-4, suggestFollowUpInstruction:"<=300 chars". After each web-chat bot reply the bot offers short tappable follow-up replies; typing stays available.
|
|
1091
1136
|
|
|
1092
1137
|
For multiSelect dynamic options: the variable named in dynamicOptionsVariable must contain a JSON array like [{"label":"Red","value":"red"},{"label":"Blue","value":"blue"}]. Populate it via an HTTP Request node before the ask node.`,
|
|
@@ -1471,6 +1516,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1471
1516
|
required: ['id'],
|
|
1472
1517
|
},
|
|
1473
1518
|
},
|
|
1519
|
+
{
|
|
1520
|
+
name: 'copilot_studio_ask',
|
|
1521
|
+
description: 'Live diagnostic: ask a connected Copilot Studio / Azure AI agent a real question and classify the reply as answered, vendor_limit (usage limit / unavailable) or no_answer (fallback topic). Use to prove the agent actually answers, not just that credentials work.',
|
|
1522
|
+
inputSchema: {
|
|
1523
|
+
type: 'object',
|
|
1524
|
+
properties: {
|
|
1525
|
+
id: { type: 'number', description: 'Config ID from copilot_studio_list' },
|
|
1526
|
+
question: { type: 'string', description: 'A real data-bound question for the agent' },
|
|
1527
|
+
},
|
|
1528
|
+
required: ['id', 'question'],
|
|
1529
|
+
},
|
|
1530
|
+
},
|
|
1474
1531
|
|
|
1475
1532
|
// ── Monday.com field mapping / CRM search ────────────────────────────────
|
|
1476
1533
|
{
|
|
@@ -1661,6 +1718,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1661
1718
|
welcomeMessage: { type: 'string', description: 'Sent to the Teams user when their message opens a new conversation. Use {{user.name}} for their display name. Max 1000 characters.' },
|
|
1662
1719
|
teamId: { type: 'number', description: 'Route new conversations to this team id (must be a real, non-deleted team on this site)' },
|
|
1663
1720
|
clearTeamId: { type: 'boolean', description: 'Clear the team override and fall back to the site default team' },
|
|
1721
|
+
prechatEnabled: { type: 'boolean', description: 'Ask the Teams user prechat questions before a conversation opens' },
|
|
1722
|
+
prechatFields: { type: 'array', description: 'Replaces the prechat questions (max 6). Key "email" is validated as an email.', items: { type: 'object', properties: { key: { type: 'string' }, label: { type: 'string' }, required: { type: 'boolean' } }, required: ['key', 'label'] } },
|
|
1723
|
+
quickReplies: { type: 'array', description: 'Replaces the quick-reply buttons (max 6, 40 chars each); empty array clears them', items: { type: 'string' } },
|
|
1724
|
+
postChatEnabled:{ type: 'boolean', description: 'Send postChatMessage when an agent closes the conversation' },
|
|
1725
|
+
postChatMessage:{ type: 'string', description: 'Post-chat text, {{user.name}} = first name, max 1000 chars; empty string clears it' },
|
|
1664
1726
|
},
|
|
1665
1727
|
required: [],
|
|
1666
1728
|
},
|
|
@@ -2381,6 +2443,24 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2381
2443
|
required: ['siteId', 'indexName'],
|
|
2382
2444
|
},
|
|
2383
2445
|
},
|
|
2446
|
+
{
|
|
2447
|
+
name: 'index_set_profile',
|
|
2448
|
+
description: 'Velaro staff only. Set the ingestion profile of ONE named index on ANY site (chunk strategy, chunk size/overlap, purpose, search size, excludeFromAllSources). Apply-if-present: only fields you pass change. Changing the profile versions the content hash so the next ingest re-chunks. Wraps PUT KnowledgeSetup/Staff/{siteId}/index-profile (velaro-messaging).',
|
|
2449
|
+
inputSchema: {
|
|
2450
|
+
type: 'object',
|
|
2451
|
+
properties: {
|
|
2452
|
+
siteId: { type: 'number', description: 'Target site ID' },
|
|
2453
|
+
indexName: { type: 'string', description: 'Index name on that site, e.g. "100140-troubleshooting"' },
|
|
2454
|
+
chunkStrategy: { type: 'string', enum: ['procedure', 'section'], description: 'Chunk strategy' },
|
|
2455
|
+
chunkSize: { type: 'number', description: 'Chunk size' },
|
|
2456
|
+
chunkOverlap: { type: 'number', description: 'Chunk overlap' },
|
|
2457
|
+
purpose: { type: 'string', enum: ['general', 'troubleshooting', 'catalog'], description: 'Index purpose' },
|
|
2458
|
+
searchSize: { type: 'number', description: 'Results per search' },
|
|
2459
|
+
excludeFromAllSources: { type: 'boolean', description: 'Keep out of the ["*"] all-sources search' },
|
|
2460
|
+
},
|
|
2461
|
+
required: ['siteId', 'indexName'],
|
|
2462
|
+
},
|
|
2463
|
+
},
|
|
2384
2464
|
{
|
|
2385
2465
|
name: 'uploaded_file_reindex',
|
|
2386
2466
|
description: 'Re-index an already-uploaded AI training file from blob storage — use when a file shows 0 chunks (e.g. PDF uploaded before PDF support was added). Reads from blob and re-runs full ingestion. Use index_status after ~30s to verify chunks appear.',
|
|
@@ -2626,6 +2706,29 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2626
2706
|
required: ['startDate', 'endDate'],
|
|
2627
2707
|
},
|
|
2628
2708
|
},
|
|
2709
|
+
{
|
|
2710
|
+
name: 'report_performance_by_country_get',
|
|
2711
|
+
description: 'Performance by Country report (US vs Canada vs Europe vs Other): engagements (handled, missed, abandoned), performance (first response, handle time, bot containment, transfers, service level) and scores (CSAT, sentiment) per country bucket and per agent. attribution: visitor (visitor GeoIP country, default) | agent (country on the agent profile) | group (the site saved group-to-bucket map).',
|
|
2712
|
+
inputSchema: {
|
|
2713
|
+
type: 'object',
|
|
2714
|
+
properties: {
|
|
2715
|
+
startDate: { type: 'string', description: 'ISO 8601 start. Default: 30 days ago' },
|
|
2716
|
+
endDate: { type: 'string', description: 'ISO 8601 end. Default: now' },
|
|
2717
|
+
attribution: { type: 'string', enum: ['visitor', 'agent', 'group'], description: 'Default: visitor' },
|
|
2718
|
+
channels: { type: 'array', items: { type: 'string' }, description: 'Optional ConversationSource filter, e.g. ["Web","TwilioSms"]. Default: all channels' },
|
|
2719
|
+
},
|
|
2720
|
+
},
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
name: 'report_performance_by_country_set_groups',
|
|
2724
|
+
description: 'Read or set the per-site mapping of Velaro groups (teams) to Performance by Country buckets. With no map, returns the site groups and the current mapping. With map ({"<teamId>": "US|Canada|Europe|any label"}) it REPLACES the mapping; unmapped groups report as Other. Only the site own groups are kept.',
|
|
2725
|
+
inputSchema: {
|
|
2726
|
+
type: 'object',
|
|
2727
|
+
properties: {
|
|
2728
|
+
map: { type: 'object', description: 'teamId to bucket label. Omit to just read.', additionalProperties: { type: 'string' } },
|
|
2729
|
+
},
|
|
2730
|
+
},
|
|
2731
|
+
},
|
|
2629
2732
|
{
|
|
2630
2733
|
name: 'report_campaign_performance',
|
|
2631
2734
|
description: 'Email campaign performance: open rate, click rate, delivery rate, complaint rate per campaign + aggregate KPIs.',
|
|
@@ -2906,7 +3009,9 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
2906
3009
|
platform: { type: 'string', enum: ['v10', 'v20', 'admin'], description: 'Filter by platform' },
|
|
2907
3010
|
environment: { type: 'string', enum: ['Production', 'Staging', 'Development'] },
|
|
2908
3011
|
correlationId: { type: 'string', description: 'Exact correlation/trace ID filter' },
|
|
2909
|
-
last: { type: 'string', description: 'Time window: 30m, 2h, 1d (default: 2h)' },
|
|
3012
|
+
last: { type: 'string', description: 'Time window ending at `to`/now: 30m, 2h, 1d (default: 2h)' },
|
|
3013
|
+
from: { type: 'string', description: 'Window start: ISO timestamp (2026-10-20T00:00:00Z) or relative (3h). Overrides `last`.' },
|
|
3014
|
+
to: { type: 'string', description: 'Window end: ISO timestamp or relative (1h = 1h ago). Default: now.' },
|
|
2910
3015
|
take: { type: 'number', description: 'Max results (default: 50, max: 500)' },
|
|
2911
3016
|
regex: { type: 'boolean', description: 'Treat q/logger as a regex pattern (default: false)' },
|
|
2912
3017
|
},
|
|
@@ -3355,11 +3460,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3355
3460
|
},
|
|
3356
3461
|
{
|
|
3357
3462
|
name: 'enterprise_reports',
|
|
3358
|
-
description: 'Enterprise Reports package (enterprise-internal, default OFF; every report returns 403 when the site does not hold that package item). report: program_breakdown (compare websites, teams, platforms, devices or entry pages: volume, missed, transfers, wait, first reply, QA score and coverage, sentiment), agent_scorecard (per agent by name: resolution, transfers, handle time, AI score, supervisor-reviewed score, sentiment impact), qa_coverage (share of resolved chats AI-scored and supervisor-reviewed by week and program), coaching_queue (low-scoring chats nobody reviewed, per agent), sentiment_breakdown (by website, team, agent or topic with change vs prior period), topic_radar (topics by volume, growth, transfer rate), weekly_brief (preview of the Monday brief sections), weekly_brief_send_test (emails this week\'s brief to the signed-in user). Scoped to the caller site.',
|
|
3463
|
+
description: 'Enterprise Reports package (enterprise-internal, default OFF; every report returns 403 when the site does not hold that package item). report: program_breakdown (compare websites, teams, platforms, devices or entry pages: volume, missed, transfers, wait, first reply, QA score and coverage, sentiment), agent_scorecard (per agent by name: resolution, transfers, handle time, AI score, supervisor-reviewed score, sentiment impact), qa_coverage (share of resolved chats AI-scored and supervisor-reviewed by week and program), coaching_queue (low-scoring chats nobody reviewed, per agent), sentiment_breakdown (by website, team, agent or topic with change vs prior period), topic_radar (topics by volume, growth, transfer rate), weekly_brief (preview of the Monday brief sections), weekly_brief_send_test (emails this week\'s brief to the signed-in user), year_in_review (builds the 12-month Year in review document in the background: from/to yyyy-MM-dd inclusive, default the last 12 whole Pacific months, returns a build id; call again with buildId to get status and the download link). Scoped to the caller site.',
|
|
3359
3464
|
inputSchema: {
|
|
3360
3465
|
type: 'object',
|
|
3361
3466
|
properties: {
|
|
3362
|
-
report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test', 'repeat_contact', 'demand_coverage', 'bot_containment', 'effort_index', 'promise_ledger', 'promise_dismiss'] },
|
|
3467
|
+
report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test', 'repeat_contact', 'demand_coverage', 'bot_containment', 'effort_index', 'promise_ledger', 'promise_dismiss', 'new_agent_ramp', 'year_in_review'] },
|
|
3363
3468
|
from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
|
|
3364
3469
|
to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
|
|
3365
3470
|
groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
|
|
@@ -3377,22 +3482,24 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3377
3482
|
lowScore: { type: 'number', description: 'bot_containment: QA score below which a handed-off chat counts as regret (1-100, default 60).' },
|
|
3378
3483
|
includeDemo: { type: 'boolean', description: 'true = show only the seeded sample (demo) data. Default false.' },
|
|
3379
3484
|
promiseId: { type: 'string', description: 'promise_dismiss: the promise id to dismiss (managers only).' },
|
|
3485
|
+
buildId: { type: 'string', description: 'year_in_review: the build id returned by a start call; when given, returns that build\'s status (queued|building|completed|failed) and download link instead of starting a new build.' },
|
|
3380
3486
|
},
|
|
3381
3487
|
required: ['report'],
|
|
3382
3488
|
},
|
|
3383
3489
|
},
|
|
3384
3490
|
{
|
|
3385
3491
|
name: 'report_backfill',
|
|
3386
|
-
description: 'Backfill report data for the caller site. Preview by default (reports "would fill N", writes nothing); apply=true writes. Idempotent. type "csat" fills missing survey ratings (SurveySubmission.Rating) from existing data; it does NOT touch ticket CSAT. Site managers only; scoped to the caller site.',
|
|
3492
|
+
description: 'Backfill report data for the caller site. Preview by default (reports "would fill N", writes nothing); apply=true writes. Idempotent. type "csat" fills missing survey ratings (SurveySubmission.Rating) from existing data; it does NOT touch ticket CSAT. type "csat-demo" adds tagged demo CSAT assigned to the demo agents (allow-listed demo sites only); type "csat-demo-undo" over the same from/to deletes exactly those tagged rows and nothing else. type "history-classification-sample" tags 20 recent conversations and reports the line-of-business and intent mix it found (the proposed tags); type "history-classification" then tags and sentiment-scores past resolved conversations in the range (up to 100 per run, run again to continue) so Enterprise Reports and Year in Review show classified history instead of Unclassified / Insufficient data. For both history types, apply=true is QUEUED (the server returns 202 with a runId and runs it in the background; one run per site at a time, a repeat call returns the active run); pass status=true to read the latest run (queued, running, completed with result, or failed). Site managers only; scoped to the caller site.',
|
|
3387
3493
|
inputSchema: {
|
|
3388
3494
|
type: 'object',
|
|
3389
3495
|
properties: {
|
|
3390
|
-
type: { type: 'string', description: 'Backfill type,
|
|
3391
|
-
from: { type: 'string', description: 'yyyy-MM-dd start.' },
|
|
3392
|
-
to: { type: 'string', description: 'yyyy-MM-dd end.' },
|
|
3393
|
-
apply: { type: 'boolean', description: 'true = write. Default false (preview only).' },
|
|
3496
|
+
type: { type: 'string', description: 'Backfill type: csat, csat-demo, csat-demo-undo, history-classification-sample or history-classification.' },
|
|
3497
|
+
from: { type: 'string', description: 'yyyy-MM-dd start (required unless status=true).' },
|
|
3498
|
+
to: { type: 'string', description: 'yyyy-MM-dd end (required unless status=true).' },
|
|
3499
|
+
apply: { type: 'boolean', description: 'true = write. Default false (preview only). For history types this queues a background run.' },
|
|
3500
|
+
status: { type: 'boolean', description: 'true = return the latest queued/running/finished apply run for this type instead of starting anything.' },
|
|
3394
3501
|
},
|
|
3395
|
-
required: ['type'
|
|
3502
|
+
required: ['type'],
|
|
3396
3503
|
},
|
|
3397
3504
|
},
|
|
3398
3505
|
// -- Staff: schedule adherence by site + role (SupportTools-style: explicit siteId, never auth-derived) --
|
|
@@ -3560,6 +3667,17 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3560
3667
|
required: ['siteId'],
|
|
3561
3668
|
},
|
|
3562
3669
|
},
|
|
3670
|
+
{
|
|
3671
|
+
name: 'avatar_session_readiness',
|
|
3672
|
+
description: 'Velaro staff diagnostic: runs the real avatar session gates for a site (plan, config, provider key, billing authority, concurrency) without claiming a slot or calling the provider, so it costs nothing. Use when a visitor avatar session returns 503 or will not start. Calls velaro-messaging GET /avatar/diagnostics/session-readiness (staff only).',
|
|
3673
|
+
inputSchema: {
|
|
3674
|
+
type: 'object',
|
|
3675
|
+
properties: {
|
|
3676
|
+
siteId: { type: 'number', description: 'Site ID to check' },
|
|
3677
|
+
},
|
|
3678
|
+
required: ['siteId'],
|
|
3679
|
+
},
|
|
3680
|
+
},
|
|
3563
3681
|
{
|
|
3564
3682
|
name: 'avatar_set_config',
|
|
3565
3683
|
description: 'Set up or update the AI Video Avatar for a customer site (partial update — only send the fields you want to change). Velaro admin only.',
|
|
@@ -3572,11 +3690,13 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3572
3690
|
avatarId: { type: 'string', description: 'Avatar ID. For provider "velaro" this is a Velaro Avatar library slug. Other providers: their own face/avatar ID. Omit for provider default.' },
|
|
3573
3691
|
voiceId: { type: 'string', description: 'ElevenLabs voice ID override. Omit to use the site default.' },
|
|
3574
3692
|
showCallMeOption: { type: 'boolean', description: 'Show the "Call me instead" option (requires EnableClickToCall).' },
|
|
3693
|
+
enabledOnAgentTransfer: { type: 'boolean', description: 'Keep the avatar running after a human agent joins the conversation (default off).' },
|
|
3575
3694
|
callbackConfirmWindowMinutes: { type: 'number', description: 'Minutes to confirm a callback request before it expires. Default 10.' },
|
|
3576
3695
|
displayName: { type: 'string', description: 'Visitor-facing avatar name shown on the badge (e.g. "Moshky"). Empty string resets to "AI".' },
|
|
3577
3696
|
showBadge: { type: 'boolean', description: 'Show the name badge over the avatar video. Default true.' },
|
|
3578
3697
|
sizePx: { type: 'number', description: 'Max avatar panel width in px (160-640, square). Default 360.' },
|
|
3579
3698
|
allowVisitorCollapse: { type: 'boolean', description: 'Let visitors hide or collapse the avatar panel (up/down arrow and X). Default true.' },
|
|
3699
|
+
heyGenEnabled: { type: 'boolean', description: 'Grant or revoke the HeyGen provider entitlement (EnableAvatarHeyGen) for this site. Off by default; HeyGen is hidden from the customer UI until granted.' },
|
|
3580
3700
|
motionLevel: { type: 'string', enum: ['', 'off', 'subtle', 'natural', 'expressive'], description: 'Body motion preset layered over the lip-sync (shoulders, breathing, head, brows). Empty string resets to the platform default (natural). Visitors with reduced-motion enabled always get a still avatar.' },
|
|
3581
3701
|
motionIntensityPercent: { type: 'integer', minimum: 25, maximum: 150, description: 'Body motion strength, whole percent 25-150 (default 100); multiplies every motion amplitude.' },
|
|
3582
3702
|
maxAvatarMinutesPerMonth: { type: 'number', description: 'Monthly avatar video minutes cap for the site. Whole number 1-100000; never unlimited (0/negative rejected).' },
|
|
@@ -3674,6 +3794,31 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3674
3794
|
description: 'Reset ALL group and workflow overrides for the calling site back to the site default.',
|
|
3675
3795
|
inputSchema: { type: 'object', properties: {} },
|
|
3676
3796
|
},
|
|
3797
|
+
{
|
|
3798
|
+
name: 'avatar_set_workflow_switch',
|
|
3799
|
+
description: 'Switch the AI avatar on, off, or back to the site rules for ONE workflow of the calling site. mode "on" shows the avatar, "off" hides it, "rules" clears the switch so the site rules decide. Only the switch changes; the rest of the workflow override is kept. Velaro admin or site auth.',
|
|
3800
|
+
inputSchema: {
|
|
3801
|
+
type: 'object',
|
|
3802
|
+
properties: {
|
|
3803
|
+
workflowId: { type: 'number', description: 'Workflow id (must belong to the calling site).' },
|
|
3804
|
+
mode: { type: 'string', enum: ['on', 'off', 'rules'], description: 'on | off | rules' },
|
|
3805
|
+
},
|
|
3806
|
+
required: ['workflowId', 'mode'],
|
|
3807
|
+
},
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
name: 'avatar_bulk_set_switch',
|
|
3811
|
+
description: 'Set the AI avatar switch (on | off | rules) for many workflows and/or teams of the calling site in one call. Select rows with workflowIds and/or groupIds (at least one; max 500 rows). Each changed row is audited. Velaro admin or site auth.',
|
|
3812
|
+
inputSchema: {
|
|
3813
|
+
type: 'object',
|
|
3814
|
+
properties: {
|
|
3815
|
+
mode: { type: 'string', enum: ['on', 'off', 'rules'], description: 'on | off | rules' },
|
|
3816
|
+
workflowIds: { type: 'array', items: { type: 'number' }, description: 'Workflow ids of the calling site.' },
|
|
3817
|
+
groupIds: { type: 'array', items: { type: 'number' }, description: 'Team (group) ids of the calling site.' },
|
|
3818
|
+
},
|
|
3819
|
+
required: ['mode'],
|
|
3820
|
+
},
|
|
3821
|
+
},
|
|
3677
3822
|
|
|
3678
3823
|
// ── Calendly integration management (superadmin) ──────────────────────────
|
|
3679
3824
|
{
|
|
@@ -4027,6 +4172,26 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4027
4172
|
required: ['shop'],
|
|
4028
4173
|
},
|
|
4029
4174
|
},
|
|
4175
|
+
{
|
|
4176
|
+
name: 'shopify_get_write_policy',
|
|
4177
|
+
description: 'Get the Shopify write-tier policy for the site: enableShopifyWrite (master toggle for the 11 Shopify write skills; read skills are unaffected), requireConfirmation, and the effective caps maxRefundAmount, maxDiscountPercent, maxDiscountFixedAmount (defaults filled in: 250 / 20 / 50). Site is resolved server-side from the caller\'s auth context. Lives in velaro-messaging (GET /api/Shopify/WritePolicy) - requires VELARO_MESSAGING_API.',
|
|
4178
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
4179
|
+
},
|
|
4180
|
+
{
|
|
4181
|
+
name: 'shopify_set_write_policy',
|
|
4182
|
+
description: 'Update the Shopify write-tier policy for the site (apply-if-present, partial: omitted fields keep their stored value). Pass null for a cap to return it to the default. enableShopifyWrite=false hides the 11 Shopify write skills (refund, discount, etc.) without touching the read skills. Validated server-side. Lives in velaro-messaging (PUT /api/Shopify/WritePolicy) - requires VELARO_MESSAGING_API.',
|
|
4183
|
+
inputSchema: {
|
|
4184
|
+
type: 'object',
|
|
4185
|
+
properties: {
|
|
4186
|
+
enableShopifyWrite: { type: 'boolean', description: 'Master toggle for the Shopify write skills.' },
|
|
4187
|
+
requireConfirmation: { type: 'boolean', description: 'Require customer confirmation before a write skill executes.' },
|
|
4188
|
+
maxRefundAmount: { type: ['number', 'null'], description: 'Max refund amount per call. null = default (250).' },
|
|
4189
|
+
maxDiscountPercent: { type: ['number', 'null'], description: 'Max percentage discount. null = default (20).' },
|
|
4190
|
+
maxDiscountFixedAmount: { type: ['number', 'null'], description: 'Max fixed-amount discount. null = default (50).' },
|
|
4191
|
+
},
|
|
4192
|
+
required: [],
|
|
4193
|
+
},
|
|
4194
|
+
},
|
|
4030
4195
|
{
|
|
4031
4196
|
name: 'appointments_manage',
|
|
4032
4197
|
description: 'Velaro Appointments — reschedules or cancels an existing appointment that was booked via appointments_book. Requires the provider slug and event_id from the original booking. Set intent="reschedule" with a new start (and optional duration) to move it, or intent="cancel" to remove it. Cancel is destructive and is additionally gated by the EnableDestructiveAiSkills subscription flag. Marketing page: /appointments.',
|
|
@@ -4219,8 +4384,9 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4219
4384
|
properties: {
|
|
4220
4385
|
storeUrl: { type: 'string', description: 'Magento store base URL, e.g. "https://mystore.com"' },
|
|
4221
4386
|
accessToken: { type: 'string', description: 'Magento REST API bearer token (Admin Integration token)' },
|
|
4387
|
+
configId: { type: 'number', description: 'Test an already-connected store with its stored token instead (storeUrl/accessToken then not needed)' },
|
|
4222
4388
|
},
|
|
4223
|
-
required: [
|
|
4389
|
+
required: [],
|
|
4224
4390
|
},
|
|
4225
4391
|
},
|
|
4226
4392
|
{
|
|
@@ -4295,6 +4461,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4295
4461
|
},
|
|
4296
4462
|
|
|
4297
4463
|
// ── Business actions + SAP (one definition, many surfaces) ─────────────────────
|
|
4464
|
+
{
|
|
4465
|
+
name: 'salesforce_diagnose',
|
|
4466
|
+
description: 'Run the Salesforce live diagnostic for a site: configuration completeness, OAuth token acquisition and, when deep, a read-only SOQL probe per object the skills use (contacts, leads, accounts, tasks, notes, email history, opportunities, cases, products, orders) with its package tier (Core/Sales/Service/Commerce). Never writes; write capabilities are never reported as verified; secrets are never returned. Calls messaging GET diagnostics/test-salesforce?siteId=<id>&deep=<bool>. Velaro staff only.',
|
|
4467
|
+
inputSchema: {
|
|
4468
|
+
type: 'object',
|
|
4469
|
+
properties: {
|
|
4470
|
+
siteId: { type: 'number', description: 'Site to diagnose (e.g. 1032)' },
|
|
4471
|
+
deep: { type: 'boolean', description: 'Probe every object (default true). false = configuration + token only.' },
|
|
4472
|
+
},
|
|
4473
|
+
required: ['siteId'],
|
|
4474
|
+
},
|
|
4475
|
+
},
|
|
4298
4476
|
{
|
|
4299
4477
|
name: 'sap_diagnose',
|
|
4300
4478
|
description: 'Run the SAP live diagnostic for a site: configuration completeness, authentication (real HTTP status) and, when deep, a per-capability read-only probe compared with what the capability matrix documents (S/4HANA on-prem/private/public, Business One, ECC via gateway). Never writes; a write capability is never reported as verified; secrets are never returned. Calls messaging GET diagnostics/test-sap?siteId=<id>&deep=<bool>. Velaro staff only.',
|
|
@@ -4403,6 +4581,35 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4403
4581
|
},
|
|
4404
4582
|
},
|
|
4405
4583
|
|
|
4584
|
+
{
|
|
4585
|
+
name: 'netsuite_suiteapp_download',
|
|
4586
|
+
description: 'Download the NetSuite screen-pop SuiteApp (SDF project zip) to a local file. With includeKey=true (default) a new crm-embed-only key is minted and baked into the zip, so NO NetSuite API Secret and no key paste is needed. With includeKey=false the zip is key-less and the admin must create a NetSuite API Secret: type ID velaro_api_key in the form (NetSuite adds the prefix, giving custsecret_velaro_api_key). Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
|
|
4587
|
+
inputSchema: { type: 'object', properties: { includeKey: { type: 'boolean', description: 'Default true.' }, outputPath: { type: 'string', description: 'Local file path to write (default: temp dir).' } }, required: [] },
|
|
4588
|
+
},
|
|
4589
|
+
{
|
|
4590
|
+
name: 'netsuite_config_delete',
|
|
4591
|
+
description: 'Delete a NetSuite connection (config) from the site. Removes the stored credentials; mappings/workflows referencing it will stop working. Destructive. Lives in velaro-messaging (DELETE Netsuite/Integrations/{id}) - requires VELARO_MESSAGING_API.',
|
|
4592
|
+
inputSchema: {
|
|
4593
|
+
type: 'object',
|
|
4594
|
+
properties: { configId: { type: 'number', description: 'NetSuite config ID (see netsuite_list_configs).' } },
|
|
4595
|
+
required: ['configId'],
|
|
4596
|
+
},
|
|
4597
|
+
},
|
|
4598
|
+
{
|
|
4599
|
+
name: 'netsuite_m2m_clear_legacy',
|
|
4600
|
+
description: 'Disconnect the old consumer-key/token credentials from a NetSuite config that already runs on a saved M2M certificate (explicit, never automatic; the connection row, mappings and workflows are untouched). Fails if M2M is not saved/active. Lives in velaro-messaging (POST NetSuite/M2M/ClearLegacy) - requires VELARO_MESSAGING_API.',
|
|
4601
|
+
inputSchema: {
|
|
4602
|
+
type: 'object',
|
|
4603
|
+
properties: { configId: { type: 'number', description: 'NetSuite config ID (see netsuite_list_configs).' } },
|
|
4604
|
+
required: ['configId'],
|
|
4605
|
+
},
|
|
4606
|
+
},
|
|
4607
|
+
{
|
|
4608
|
+
name: 'netsuite_embed_status',
|
|
4609
|
+
description: 'NetSuite SuiteApp call-pop (CRM embed) status for the site: is a crm_embed API key set, and the last heartbeat (when it last polled, connected or not) per key. Lives in velaro-messaging (GET api/v1/CrmEmbed/embed-status; fields hasEmbedKey, connected, lastPolledAt) - requires VELARO_MESSAGING_API.',
|
|
4610
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
4611
|
+
},
|
|
4612
|
+
|
|
4406
4613
|
{
|
|
4407
4614
|
name: 'netsuite_m2m_init',
|
|
4408
4615
|
description: 'Start NetSuite machine-to-machine (OAuth 2.0 Client Credentials) setup: generates the Velaro public certificate PEM to upload in NetSuite (Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup) and returns the steps. Omit configId for a new connection (company then required). Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
|
|
@@ -4896,6 +5103,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4896
5103
|
description: 'Show the Enterprise Reports package (enterprise-internal, default OFF): every item with its feature key, what it builds on, and, when siteId is given, whether it is ON for that site. Superadmin only.',
|
|
4897
5104
|
inputSchema: { type: 'object', properties: { siteId: { type: 'number', description: 'Optional site ID to show which items are on' } } },
|
|
4898
5105
|
},
|
|
5106
|
+
{
|
|
5107
|
+
name: 'entitlement_report_packages',
|
|
5108
|
+
description: 'Show the five report category packages (Conversations & Channels, Workforce & Operations, Customer Experience, AI & Intelligence, Business & Executive) nested under the top Enterprise Reports package (default OFF): every report with its package slug and entitlement key and, when siteId is given, whether each report and each whole category is ON for that site. Superadmin only.',
|
|
5109
|
+
inputSchema: { type: 'object', properties: { siteId: { type: 'number', description: 'Optional site ID to show which reports are on' } } },
|
|
5110
|
+
},
|
|
4899
5111
|
{
|
|
4900
5112
|
name: 'entitlement_set_override',
|
|
4901
5113
|
description: 'Set a per-site feature override — enable or disable a specific feature for one site, independent of their plan. Superadmin only. Use entitlement_list_features to find valid featureKey values.',
|
|
@@ -5090,11 +5302,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
5090
5302
|
},
|
|
5091
5303
|
{
|
|
5092
5304
|
name: 'focus_update_triage_rules',
|
|
5093
|
-
description: 'Enable or disable one or more of the 5 fixed AI triage labels (urgent, needs_reply, offer, fyi, waiting) for this site. Apply-if-present -- only labels included are changed. Rejects any label name outside the fixed set. Disabling all 5 fails safe back to all-enabled server-side rather than breaking triage. Optionally set triageMode: "economy" classifies obvious messages (auto-replies, clear urgency) without an AI call, lowering AI usage; "off"
|
|
5305
|
+
description: 'Enable or disable one or more of the 5 fixed AI triage labels (urgent, needs_reply, offer, fyi, waiting) for this site. Apply-if-present -- only labels included are changed. Rejects any label name outside the fixed set. Disabling all 5 fails safe back to all-enabled server-side rather than breaking triage. Optionally set triageMode: "economy" classifies obvious messages (auto-replies, clear urgency) without an AI call, lowering AI usage; "off" makes this site always use the AI call (platform default is economy). Unspecified fields are preserved.',
|
|
5094
5306
|
inputSchema: {
|
|
5095
5307
|
type: 'object',
|
|
5096
5308
|
properties: {
|
|
5097
|
-
triageMode: { type: 'string', enum: ['off', 'economy'], description: 'Optional. "economy" skips the AI call for obvious messages; "off"
|
|
5309
|
+
triageMode: { type: 'string', enum: ['off', 'economy'], description: 'Optional. "economy" skips the AI call for obvious messages; "off" makes this site always use the AI call. Unset inherits the platform default, which is economy.' },
|
|
5098
5310
|
labels: {
|
|
5099
5311
|
type: 'array',
|
|
5100
5312
|
description: 'Labels to update.',
|
|
@@ -5200,7 +5412,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
5200
5412
|
type: 'object',
|
|
5201
5413
|
properties: {
|
|
5202
5414
|
start: { type: 'string', description: 'ISO 8601 start of range.' },
|
|
5203
|
-
end: { type: 'string', description: '
|
|
5415
|
+
end: { type: 'string', description: 'End of range. A plain YYYY-MM-DD date includes that whole day (the server end is exclusive, so it is sent as the start of the next day); a full ISO 8601 datetime is an exact exclusive instant. Must be at most 90 days after start.' },
|
|
5204
5416
|
},
|
|
5205
5417
|
required: ['start', 'end'],
|
|
5206
5418
|
},
|
|
@@ -5330,6 +5542,28 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
5330
5542
|
},
|
|
5331
5543
|
},
|
|
5332
5544
|
|
|
5545
|
+
// -- Apple Pay display settings --
|
|
5546
|
+
// Backend: velaro-messaging ApplePayConfigsController (GET/POST ApplePayConfig, PUT ApplePayConfig/{id}).
|
|
5547
|
+
// Display + test mode only; merchant id / certificate (which affect the charge) are intentionally not settable here.
|
|
5548
|
+
{
|
|
5549
|
+
name: 'apple_pay_get_config',
|
|
5550
|
+
description: 'Get this site\'s Apple Pay settings: enabled, test mode (sandbox: Velaro Stripe test account, no real money, real Wallet cards decline), business name shown on the Apple Pay sheet, whether a custom payment-bubble logo is set, and whether the site uses Velaro\'s platform merchant. id is 0 when the site has no row yet.',
|
|
5551
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
5552
|
+
},
|
|
5553
|
+
{
|
|
5554
|
+
name: 'apple_pay_update_config',
|
|
5555
|
+
description: 'Create or update this site\'s Apple Pay DISPLAY settings and test mode. Apply-if-present: omit a field to leave it unchanged. These never affect the charge (the merchant id and certificate do, and are managed by secure Velaro admin provisioning). Creates the row when the site has none.',
|
|
5556
|
+
inputSchema: {
|
|
5557
|
+
type: 'object',
|
|
5558
|
+
properties: {
|
|
5559
|
+
merchantDisplayName: { type: 'string', description: 'Business name shown on the Apple Pay sheet' },
|
|
5560
|
+
isTestMode: { type: 'boolean', description: 'Sandbox mode: Velaro Stripe test account, no real money moves, real Wallet cards decline' },
|
|
5561
|
+
bubbleLogoBase64: { type: 'string', description: 'PNG/JPEG base64 (max 500 KB) for the payment bubble. Empty string clears it back to the default image.' },
|
|
5562
|
+
},
|
|
5563
|
+
required: [],
|
|
5564
|
+
},
|
|
5565
|
+
},
|
|
5566
|
+
|
|
5333
5567
|
// -- Recruiting Bot --
|
|
5334
5568
|
// Backend: server/Velaro.Messaging/Controllers/RecruitingController.cs.
|
|
5335
5569
|
// [EnableRecruitingBot] gated server-side; siteId always derives from the
|
|
@@ -6035,6 +6269,14 @@ function parseBusinessHours(input) {
|
|
|
6035
6269
|
|
|
6036
6270
|
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
6037
6271
|
|
|
6272
|
+
// Report end params are exclusive server-side: a plain YYYY-MM-DD end means "through that whole
|
|
6273
|
+
// day", so send the start of the next day (mirrors cli/lib/report-range.js exclusiveEndParam).
|
|
6274
|
+
function exclusiveEnd(raw) {
|
|
6275
|
+
if (raw == null || raw === '') return raw;
|
|
6276
|
+
const d = new Date(raw);
|
|
6277
|
+
if (Number.isNaN(d.getTime()) || !/^\d{4}-\d{2}-\d{2}$/.test(String(raw).trim())) return raw;
|
|
6278
|
+
return new Date(d.getTime() + 24 * 60 * 60 * 1000).toISOString();
|
|
6279
|
+
}
|
|
6038
6280
|
// -- Agent inactivity chain helpers (velaro-messaging PR #1471, v20 ONLY) ------------------------
|
|
6039
6281
|
// Terminal action ints are persisted by messaging (append-only enum). The API returns/accepts the NAME.
|
|
6040
6282
|
const TERMINAL_ACTION_NAMES = ['Survey','CreateTicket','SurveyAndCreateTicket','Queue','OfflineMessage','NoOp','EndChat','NotifyManager','RunWorkflow','ReturnToBot'];
|
|
@@ -6165,7 +6407,9 @@ async function handleTool(name, args) {
|
|
|
6165
6407
|
}
|
|
6166
6408
|
|
|
6167
6409
|
case 'notifications_get': {
|
|
6168
|
-
|
|
6410
|
+
const base = await messagingGet('/NotificationSettings');
|
|
6411
|
+
const routing = await messagingGet('/api/v1/alert-routing').catch(() => null);
|
|
6412
|
+
return JSON.stringify({ ...base, alertRouting: routing?.data ?? null }, null, 2);
|
|
6169
6413
|
}
|
|
6170
6414
|
|
|
6171
6415
|
case 'notifications_set': {
|
|
@@ -6179,7 +6423,47 @@ async function handleTool(name, args) {
|
|
|
6179
6423
|
if (args[f] !== undefined) current[f] = args[f];
|
|
6180
6424
|
}
|
|
6181
6425
|
await messagingPost('/NotificationSettings', current);
|
|
6182
|
-
|
|
6426
|
+
if (args.alertRouting && typeof args.alertRouting === 'object') {
|
|
6427
|
+
await messagingPut('/api/v1/alert-routing', args.alertRouting);
|
|
6428
|
+
}
|
|
6429
|
+
const routing = await messagingGet('/api/v1/alert-routing').catch(() => null);
|
|
6430
|
+
return JSON.stringify({ ...(await messagingGet('/NotificationSettings')), alertRouting: routing?.data ?? null }, null, 2);
|
|
6431
|
+
}
|
|
6432
|
+
|
|
6433
|
+
case 'signature_policy_get': {
|
|
6434
|
+
return JSON.stringify(await messagingGet('/Signatures/policy'), null, 2);
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6437
|
+
case 'signature_policy_set': {
|
|
6438
|
+
// PUT replaces the row and silently turns an unknown mode into "follow company-wide", so validate and merge here.
|
|
6439
|
+
// Same rules as cli/lib/signature-policy.js (keep the two in step).
|
|
6440
|
+
const modes = { 'locked': 'admin_locked', 'company-default': 'company_default', 'personal-only': 'personal_only' };
|
|
6441
|
+
const teamId = args.teamId === undefined || args.teamId === null ? 0 : args.teamId;
|
|
6442
|
+
if (!Number.isInteger(teamId) || teamId < 0) throw new Error('teamId must be 0 (company-wide) or a team id');
|
|
6443
|
+
const data = await messagingGet('/Signatures/policy');
|
|
6444
|
+
const cur = (Array.isArray(data?.policies) ? data.policies : []).find((p) => p.teamId === teamId) ?? {};
|
|
6445
|
+
const body = {
|
|
6446
|
+
teamId,
|
|
6447
|
+
mode: cur.mode ?? null,
|
|
6448
|
+
corporateSignature: cur.corporateSignature || null,
|
|
6449
|
+
disclaimerEnabled: cur.disclaimerEnabled ?? null,
|
|
6450
|
+
disclaimerText: cur.disclaimerText || null,
|
|
6451
|
+
};
|
|
6452
|
+
if (args.mode !== undefined) {
|
|
6453
|
+
if (args.mode === null || args.mode === 'default') body.mode = null;
|
|
6454
|
+
else if (modes[args.mode]) body.mode = modes[args.mode];
|
|
6455
|
+
else throw new Error('mode must be locked, company-default, personal-only or default');
|
|
6456
|
+
}
|
|
6457
|
+
if (args.corporateSignature !== undefined) body.corporateSignature = args.corporateSignature || null;
|
|
6458
|
+
if (args.disclaimerEnabled !== undefined) {
|
|
6459
|
+
if (args.disclaimerEnabled !== null && typeof args.disclaimerEnabled !== 'boolean') throw new Error('disclaimerEnabled must be true, false or null');
|
|
6460
|
+
body.disclaimerEnabled = args.disclaimerEnabled;
|
|
6461
|
+
}
|
|
6462
|
+
if (args.disclaimerText !== undefined) body.disclaimerText = args.disclaimerText || null;
|
|
6463
|
+
if (body.corporateSignature && body.corporateSignature.length > 4000) throw new Error('corporateSignature is limited to 4000 characters');
|
|
6464
|
+
if (body.disclaimerText && body.disclaimerText.length > 2000) throw new Error('disclaimerText is limited to 2000 characters');
|
|
6465
|
+
await messagingPut('/Signatures/policy', body);
|
|
6466
|
+
return JSON.stringify(await messagingGet('/Signatures/policy'), null, 2);
|
|
6183
6467
|
}
|
|
6184
6468
|
|
|
6185
6469
|
case 'admin_startup_diagnostics': {
|
|
@@ -6468,7 +6752,7 @@ async function handleTool(name, args) {
|
|
|
6468
6752
|
case 'chat_intelligence_themes': {
|
|
6469
6753
|
const qs = new URLSearchParams();
|
|
6470
6754
|
if (args.startDate) qs.set('startDate', args.startDate);
|
|
6471
|
-
if (args.endDate) qs.set('endDate', args.endDate);
|
|
6755
|
+
if (args.endDate) qs.set('endDate', exclusiveEnd(args.endDate));
|
|
6472
6756
|
qs.set('groupBy', args.groupBy ?? 'month');
|
|
6473
6757
|
if (args.deploymentId != null) qs.set('deploymentId', String(args.deploymentId));
|
|
6474
6758
|
if (args.teamId != null) qs.set('teamId', String(args.teamId));
|
|
@@ -6562,6 +6846,8 @@ async function handleTool(name, args) {
|
|
|
6562
6846
|
|
|
6563
6847
|
case 'workflow_toggle':
|
|
6564
6848
|
return await rpc('workflow_toggle', args);
|
|
6849
|
+
case 'workflow_run_now':
|
|
6850
|
+
return await rpc('workflow_run_now', args);
|
|
6565
6851
|
|
|
6566
6852
|
case 'workflow_create':
|
|
6567
6853
|
return await rpc('workflow_create', args);
|
|
@@ -6941,6 +7227,13 @@ async function handleTool(name, args) {
|
|
|
6941
7227
|
return `Connection test FAILED for agent ${args.id}: ${result?.error ?? 'unknown error'}`;
|
|
6942
7228
|
}
|
|
6943
7229
|
|
|
7230
|
+
case 'copilot_studio_ask': {
|
|
7231
|
+
if (!args.id || !args.question) return 'id and question are required.';
|
|
7232
|
+
const r = await api('POST', '/CopilotStudio/message', { configId: args.id, message: args.question });
|
|
7233
|
+
return `kind: ${r?.kind}
|
|
7234
|
+
reply: ${r?.reply}`;
|
|
7235
|
+
}
|
|
7236
|
+
|
|
6944
7237
|
// ── Monday.com field mapping / CRM search ──────────────────────────────
|
|
6945
7238
|
// Wired to MondayController.cs actions: GetBoardColumns, GetFieldMapping,
|
|
6946
7239
|
// UpdateFieldMapping, SearchContacts (routes under /Monday/...).
|
|
@@ -7093,7 +7386,7 @@ async function handleTool(name, args) {
|
|
|
7093
7386
|
|
|
7094
7387
|
case 'teams_channel_config_update': {
|
|
7095
7388
|
const body = {};
|
|
7096
|
-
for (const k of ['enabled','welcomeMessage','teamId','clearTeamId'])
|
|
7389
|
+
for (const k of ['enabled','welcomeMessage','teamId','clearTeamId','prechatEnabled','prechatFields','quickReplies','postChatEnabled','postChatMessage'])
|
|
7097
7390
|
if (args[k] !== undefined) body[k] = args[k];
|
|
7098
7391
|
return await api('PUT', '/TeamsBot/ChannelConfig', body);
|
|
7099
7392
|
}
|
|
@@ -8065,6 +8358,17 @@ async function handleTool(name, args) {
|
|
|
8065
8358
|
return `Index ready on site ${args.siteId}: ${idx?.name} (id=${idx?.id}, purpose=${idx?.purpose}, excludeFromAllSources=${!!idx?.excludeFromAllSources}).`;
|
|
8066
8359
|
}
|
|
8067
8360
|
|
|
8361
|
+
case 'index_set_profile': {
|
|
8362
|
+
if (!args.siteId || !args.indexName) return 'siteId and indexName are required.';
|
|
8363
|
+
const body = { indexName: args.indexName };
|
|
8364
|
+
for (const k of ['chunkStrategy', 'chunkSize', 'chunkOverlap', 'purpose', 'searchSize', 'excludeFromAllSources']) {
|
|
8365
|
+
if (args[k] !== undefined) body[k] = args[k];
|
|
8366
|
+
}
|
|
8367
|
+
const idx = await messagingRequest('PUT', `/KnowledgeSetup/Staff/${encodeURIComponent(args.siteId)}/index-profile`, body);
|
|
8368
|
+
if (idx?.error) return `Set index profile error: ${idx.error}`;
|
|
8369
|
+
return `Profile updated on site ${args.siteId}: ${idx?.name} (purpose=${idx?.purpose}, strategy=${idx?.chunkStrategy}, size=${idx?.chunkSize}, overlap=${idx?.chunkOverlap}, searchSize=${idx?.searchSize}, excludeFromAllSources=${!!idx?.excludeFromAllSources}). Re-ingest to apply to existing content.`;
|
|
8370
|
+
}
|
|
8371
|
+
|
|
8068
8372
|
case 'uploaded_files_list': {
|
|
8069
8373
|
const files = await messagingRequest('GET', '/uploadedFiles/list/AI');
|
|
8070
8374
|
if (!Array.isArray(files) || !files.length) return 'No AI training files uploaded for this site.';
|
|
@@ -8088,7 +8392,7 @@ async function handleTool(name, args) {
|
|
|
8088
8392
|
case 'service_level_report': {
|
|
8089
8393
|
const qs = [
|
|
8090
8394
|
args.startDate ? `startDate=${args.startDate}` : '',
|
|
8091
|
-
args.endDate ? `endDate=${args.endDate}` : '',
|
|
8395
|
+
args.endDate ? `endDate=${encodeURIComponent(exclusiveEnd(args.endDate))}` : '',
|
|
8092
8396
|
args.granularity ? `granularity=${args.granularity}` : '',
|
|
8093
8397
|
].filter(Boolean).join('&');
|
|
8094
8398
|
const r = await api('GET', `/ServiceLevel${qs ? '?' + qs : ''}`);
|
|
@@ -8138,7 +8442,7 @@ async function handleTool(name, args) {
|
|
|
8138
8442
|
case 'report_agent_utilization': {
|
|
8139
8443
|
const qs = [
|
|
8140
8444
|
args.startDate ? `startDate=${args.startDate}` : '',
|
|
8141
|
-
args.endDate ? `endDate=${args.endDate}` : '',
|
|
8445
|
+
args.endDate ? `endDate=${encodeURIComponent(exclusiveEnd(args.endDate))}` : '',
|
|
8142
8446
|
].filter(Boolean).join('&');
|
|
8143
8447
|
const rows = await api('GET', `/Reports/AgentUtilization${qs ? '?' + qs : ''}`);
|
|
8144
8448
|
if (!Array.isArray(rows) || !rows.length) return 'No agent utilization data for this period.';
|
|
@@ -8259,10 +8563,36 @@ async function handleTool(name, args) {
|
|
|
8259
8563
|
return `Export queued (id ${result.exportId}). ${result.message}`;
|
|
8260
8564
|
}
|
|
8261
8565
|
|
|
8566
|
+
case 'report_performance_by_country_get': {
|
|
8567
|
+
const end = args.endDate ? new Date(args.endDate) : new Date();
|
|
8568
|
+
const start = args.startDate ? new Date(args.startDate) : new Date(end.getTime() - 30 * 86400000);
|
|
8569
|
+
if (isNaN(start) || isNaN(end)) return 'Error: startDate/endDate must be valid ISO 8601 dates.';
|
|
8570
|
+
const r = await messagingPost('/reports/performance-by-country/query', {
|
|
8571
|
+
start: start.toISOString(), end: end.toISOString(),
|
|
8572
|
+
attribution: args.attribution || 'visitor', channels: Array.isArray(args.channels) ? args.channels : [],
|
|
8573
|
+
});
|
|
8574
|
+
const f = (v, s = '') => (v == null ? '-' : `${v}${s}`);
|
|
8575
|
+
const row = x => `${(x.agentName || x.bucket).padEnd(24)} eng ${x.engagements} | handled ${x.handled} | missed ${x.missed} | abandoned ${x.abandoned} | bot containment ${f(x.botContainmentPct, '%')} | transfers ${x.transfersToAgent} | 1st resp ${f(x.avgFirstResponseSeconds, 's')} | handle ${f(x.avgHandleTimeMinutes, 'm')} | SL ${f(x.serviceLevelPct, '%')} | CSAT ${f(x.csatAvg)} (${x.csatCount}) | positive ${f(x.sentimentPositivePct, '%')}`;
|
|
8576
|
+
return [`=== Performance by Country (${r.attribution}) ===`, ...(r.buckets || []).map(row), '', 'By agent:',
|
|
8577
|
+
...((r.agents || []).map(a => row(a) + (a.agentCountryNotSet ? ' [agent country not set]' : '')))].join('\n');
|
|
8578
|
+
}
|
|
8579
|
+
|
|
8580
|
+
case 'report_performance_by_country_set_groups': {
|
|
8581
|
+
const groups = await messagingGet('/reports/performance-by-country/groups');
|
|
8582
|
+
let current;
|
|
8583
|
+
if (args.map && typeof args.map === 'object') {
|
|
8584
|
+
current = await messagingPut('/reports/performance-by-country/group-map', { map: args.map });
|
|
8585
|
+
} else {
|
|
8586
|
+
current = await messagingGet('/reports/performance-by-country/group-map');
|
|
8587
|
+
}
|
|
8588
|
+
const map = current?.map ?? {};
|
|
8589
|
+
return ['Groups:', ...(Array.isArray(groups) ? groups : []).map(g => ` ${g.id} ${g.name} -> ${map[g.id] ?? '(unmapped, reports as Other)'}`)].join('\n');
|
|
8590
|
+
}
|
|
8591
|
+
|
|
8262
8592
|
case 'call_center_report': {
|
|
8263
8593
|
const qs = [
|
|
8264
8594
|
args.startDate ? `start=${args.startDate}` : '',
|
|
8265
|
-
args.endDate ? `end=${args.endDate}` : '',
|
|
8595
|
+
args.endDate ? `end=${encodeURIComponent(exclusiveEnd(args.endDate))}` : '',
|
|
8266
8596
|
].filter(Boolean).join('&');
|
|
8267
8597
|
const r = await api('GET', `/CallCenterAnalytics/Report${qs ? '?' + qs : ''}`);
|
|
8268
8598
|
const v = r.volume ?? {};
|
|
@@ -8300,7 +8630,7 @@ async function handleTool(name, args) {
|
|
|
8300
8630
|
case 'call_center_team_breakdown': {
|
|
8301
8631
|
const qs = [
|
|
8302
8632
|
args.startDate ? `start=${args.startDate}` : '',
|
|
8303
|
-
args.endDate ? `end=${args.endDate}` : '',
|
|
8633
|
+
args.endDate ? `end=${encodeURIComponent(exclusiveEnd(args.endDate))}` : '',
|
|
8304
8634
|
args.slaThresholdSeconds ? `slaThresholdSeconds=${args.slaThresholdSeconds}` : '',
|
|
8305
8635
|
].filter(Boolean).join('&');
|
|
8306
8636
|
let r;
|
|
@@ -8323,7 +8653,7 @@ async function handleTool(name, args) {
|
|
|
8323
8653
|
case 'call_center_channel_breakdown': {
|
|
8324
8654
|
const qs = [
|
|
8325
8655
|
args.startDate ? `start=${args.startDate}` : '',
|
|
8326
|
-
args.endDate ? `end=${args.endDate}` : '',
|
|
8656
|
+
args.endDate ? `end=${encodeURIComponent(exclusiveEnd(args.endDate))}` : '',
|
|
8327
8657
|
args.slaThresholdSeconds ? `slaThresholdSeconds=${args.slaThresholdSeconds}` : '',
|
|
8328
8658
|
].filter(Boolean).join('&');
|
|
8329
8659
|
let r;
|
|
@@ -8830,23 +9160,13 @@ async function handleTool(name, args) {
|
|
|
8830
9160
|
}
|
|
8831
9161
|
|
|
8832
9162
|
case 'velaro_logs_search': {
|
|
8833
|
-
const from
|
|
8834
|
-
const params =
|
|
8835
|
-
if (args.q) params.set('q', args.q);
|
|
8836
|
-
if (args.level) params.set('level', args.level.toUpperCase());
|
|
8837
|
-
if (args.logger) params.set('logger', args.logger);
|
|
8838
|
-
if (args.integration) params.set('integration', args.integration);
|
|
8839
|
-
if (args.siteId) params.set('siteId', String(args.siteId));
|
|
8840
|
-
if (args.source) params.set('source', args.source);
|
|
8841
|
-
if (args.platform) params.set('platform', args.platform);
|
|
8842
|
-
if (args.environment) params.set('environment', args.environment);
|
|
8843
|
-
if (args.correlationId) params.set('correlationId', args.correlationId);
|
|
8844
|
-
if (args.regex) params.set('regex', 'true');
|
|
9163
|
+
const { from, to } = resolveLogWindow(args);
|
|
9164
|
+
const params = buildRawLogQuery(args, from, to);
|
|
8845
9165
|
const resp = await messagingApi(`/superadmin/logs/search?${params}`, { timeoutMs: 40000 });
|
|
8846
9166
|
const entries = extractLogEntries(resp);
|
|
8847
9167
|
const meta = describeRawMeta(resp); // searchPath/scannedRows/truncated: a 0 or short list is never ambiguous
|
|
8848
9168
|
if (!entries.length) return `No results. ${meta}`;
|
|
8849
|
-
const lines = [`${entries.length} result(s) from ${from.toISOString().slice(0, 16)} UTC. ${meta}\n`];
|
|
9169
|
+
const lines = [`${entries.length} result(s) from ${from.toISOString().slice(0, 16)} to ${to.toISOString().slice(0, 16)} UTC. ${meta}\n`];
|
|
8850
9170
|
for (const e of entries) {
|
|
8851
9171
|
const ts = new Date(e.timestamp ?? e.LogTimestamp).toISOString().replace('T', ' ').slice(0, 19);
|
|
8852
9172
|
const lvl = (e.Level ?? e.level ?? '?').padEnd(5);
|
|
@@ -9247,14 +9567,41 @@ async function handleTool(name, args) {
|
|
|
9247
9567
|
coaching_queue: 'coaching-queue', sentiment_breakdown: 'sentiment-breakdown', topic_radar: 'topic-radar',
|
|
9248
9568
|
weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
|
|
9249
9569
|
repeat_contact: 'repeat-contact', demand_coverage: 'demand-coverage', bot_containment: 'bot-containment',
|
|
9250
|
-
effort_index: 'effort-index', promise_ledger: 'promise-ledger',
|
|
9570
|
+
effort_index: 'effort-index', promise_ledger: 'promise-ledger', new_agent_ramp: 'new-agent-ramp',
|
|
9251
9571
|
};
|
|
9252
9572
|
if (args.report === 'promise_dismiss') {
|
|
9253
9573
|
if (!args.promiseId) throw new Error('promiseId is required for promise_dismiss');
|
|
9254
9574
|
return JSON.stringify(await messagingPost('/api/enterprise-reports/promise-ledger/' + encodeURIComponent(String(args.promiseId)) + '/dismiss', {}), null, 2);
|
|
9255
9575
|
}
|
|
9576
|
+
if (args.report === 'year_in_review') {
|
|
9577
|
+
// POST starts a build (202 {id}); GET .../{id} -> {status, downloadUrl, error}. 403 when EnableEnterpriseReportsYearInReview is off.
|
|
9578
|
+
if (args.buildId) return JSON.stringify(await messagingGet('/api/enterprise-reports/year-in-review/' + encodeURIComponent(String(args.buildId))), null, 2);
|
|
9579
|
+
const zone = 'America/Los_Angeles';
|
|
9580
|
+
const keyRe = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
9581
|
+
const addDays = (k, n) => { const m = keyRe.exec(k); return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3] + n)).toISOString().slice(0, 10); };
|
|
9582
|
+
const offsetMs = (ms) => {
|
|
9583
|
+
const ps = new Intl.DateTimeFormat('en-US', { timeZone: zone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }).formatToParts(new Date(ms));
|
|
9584
|
+
const g = (t) => Number(ps.find((p) => p.type === t).value);
|
|
9585
|
+
return Date.UTC(g('year'), g('month') - 1, g('day'), g('hour'), g('minute'), g('second')) - Math.floor(ms / 1000) * 1000;
|
|
9586
|
+
};
|
|
9587
|
+
const midnightIso = (k) => {
|
|
9588
|
+
if (!keyRe.test(k || '')) throw new Error('from/to must be yyyy-MM-dd');
|
|
9589
|
+
const m = keyRe.exec(k);
|
|
9590
|
+
const wall = Date.UTC(+m[1], +m[2] - 1, +m[3]);
|
|
9591
|
+
let t = wall - offsetMs(wall);
|
|
9592
|
+
t = wall - offsetMs(t);
|
|
9593
|
+
return new Date(t).toISOString();
|
|
9594
|
+
};
|
|
9595
|
+
const today = new Intl.DateTimeFormat('en-CA', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date());
|
|
9596
|
+
const defFrom = String(Number(today.slice(0, 4)) - 1).padStart(4, '0') + '-' + today.slice(5, 7) + '-01';
|
|
9597
|
+
const defTo = addDays(today.slice(0, 7) + '-01', -1);
|
|
9598
|
+
const f = args.from || defFrom;
|
|
9599
|
+
const t = args.to || defTo;
|
|
9600
|
+
if (f > t) throw new Error('from must be on or before to');
|
|
9601
|
+
return JSON.stringify(await messagingPost('/api/enterprise-reports/year-in-review', { from: midnightIso(f), to: midnightIso(addDays(t, 1)) }), null, 2);
|
|
9602
|
+
}
|
|
9256
9603
|
const seg = ER_PATHS[args.report];
|
|
9257
|
-
if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', '));
|
|
9604
|
+
if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', ') + ', promise_dismiss, year_in_review');
|
|
9258
9605
|
if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
|
|
9259
9606
|
const qs = new URLSearchParams();
|
|
9260
9607
|
for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days', 'windowDays', 'weeks', 'handleMinutes', 'maxConcurrent', 'timeZone', 'lowScore', 'includeDemo']) {
|
|
@@ -9297,8 +9644,11 @@ async function handleTool(name, args) {
|
|
|
9297
9644
|
}
|
|
9298
9645
|
|
|
9299
9646
|
case 'report_backfill': {
|
|
9647
|
+
if (args.status === true) return JSON.stringify(await messagingGet(`/Reports/backfill/${encodeURIComponent(args.type)}/status`), null, 2);
|
|
9648
|
+
if (!args.from || !args.to) throw new Error('from and to are required (yyyy-MM-dd) unless status=true');
|
|
9300
9649
|
const qs = new URLSearchParams({ from: String(args.from), to: String(args.to), apply: String(args.apply === true) });
|
|
9301
9650
|
const r = await messagingPost(`/Reports/backfill/${encodeURIComponent(args.type)}?${qs}`);
|
|
9651
|
+
if (args.apply === true && r && r.runId) return (r.alreadyRunning ? `A run is already ${r.status} for this site; not starting another.\n` : 'Backfill QUEUED (runs in the background; call again with status=true to read progress)\n') + JSON.stringify(r, null, 2);
|
|
9302
9652
|
return (args.apply === true ? 'Backfill APPLIED\n' : 'Backfill PREVIEW (nothing written; pass apply=true to write)\n') + JSON.stringify(r, null, 2);
|
|
9303
9653
|
}
|
|
9304
9654
|
|
|
@@ -9393,15 +9743,25 @@ async function handleTool(name, args) {
|
|
|
9393
9743
|
].join('\n');
|
|
9394
9744
|
}
|
|
9395
9745
|
|
|
9746
|
+
case 'avatar_session_readiness': {
|
|
9747
|
+
const data = await messagingRequest('GET', `/avatar/diagnostics/session-readiness?siteId=${encodeURIComponent(args.siteId)}`);
|
|
9748
|
+
return JSON.stringify(data, null, 2);
|
|
9749
|
+
}
|
|
9750
|
+
|
|
9396
9751
|
case 'avatar_get_config': {
|
|
9397
9752
|
const data = await api('GET', `/Avatar/superadmin/status?siteId=${args.siteId}`);
|
|
9398
|
-
|
|
9753
|
+
let heyGenLine = 'unknown';
|
|
9754
|
+
try { const ent = await api('GET', `/Entitlements/${args.siteId}`); heyGenLine = ent?.features?.EnableAvatarHeyGen ? 'yes' : 'no'; } catch { /* entitlement lookup is best-effort */ }
|
|
9755
|
+
if (!data.configured) return `Site ${args.siteId}: AI Video Avatar NOT configured (no saved config). HeyGen entitled: ${heyGenLine}`;
|
|
9399
9756
|
return [
|
|
9400
9757
|
`Site ${args.siteId}: AI Video Avatar ${data.enabled ? 'ENABLED' : 'configured but DISABLED'}`,
|
|
9401
9758
|
` Provider: ${data.providerSlug}`,
|
|
9402
9759
|
` Avatar ID: ${data.avatarId || '(provider default)'}`,
|
|
9760
|
+
` Face per provider: ${data.avatarIdByProvider && Object.keys(data.avatarIdByProvider).length ? JSON.stringify(data.avatarIdByProvider) : '(none saved)'} (read-only)`,
|
|
9761
|
+
` Keep running on agent join: ${data.enabledOnAgentTransfer ? 'yes' : 'no'}`,
|
|
9403
9762
|
` Voice ID: ${data.voiceId || '(site default)'}`,
|
|
9404
9763
|
` Enabled: ${data.enabled}`,
|
|
9764
|
+
` HeyGen entitled: ${heyGenLine}`,
|
|
9405
9765
|
` "Call me": ${data.showCallMeOption}`,
|
|
9406
9766
|
` Callback window: ${data.callbackConfirmWindowMinutes} min`,
|
|
9407
9767
|
` Display name: ${data.displayName || '(AI)'}`,
|
|
@@ -9418,12 +9778,17 @@ async function handleTool(name, args) {
|
|
|
9418
9778
|
if (args.voiceId !== undefined) body.voiceId = args.voiceId;
|
|
9419
9779
|
if (args.enabled !== undefined) body.enabled = args.enabled;
|
|
9420
9780
|
if (args.showCallMeOption !== undefined) body.showCallMeOption = args.showCallMeOption;
|
|
9781
|
+
if (args.enabledOnAgentTransfer !== undefined) body.enabledOnAgentTransfer = args.enabledOnAgentTransfer;
|
|
9421
9782
|
if (args.callbackConfirmWindowMinutes !== undefined) body.callbackConfirmWindowMinutes = args.callbackConfirmWindowMinutes;
|
|
9422
9783
|
if (args.displayName !== undefined) body.displayName = args.displayName;
|
|
9423
9784
|
if (args.showBadge !== undefined) body.showBadge = args.showBadge;
|
|
9424
9785
|
if (args.sizePx !== undefined) body.sizePx = args.sizePx;
|
|
9425
9786
|
if (args.allowVisitorCollapse !== undefined) body.allowVisitorCollapse = args.allowVisitorCollapse;
|
|
9787
|
+
if (args.heyGenEnabled !== undefined) {
|
|
9788
|
+
await api('POST', `/Entitlements/${args.siteId}/override`, { featureKey: 'EnableAvatarHeyGen', value: args.heyGenEnabled, reason: 'avatar_set_config heyGenEnabled' });
|
|
9789
|
+
}
|
|
9426
9790
|
if (args.motionLevel !== undefined) body.motionLevel = parseAvatarMotionLevel(args.motionLevel);
|
|
9791
|
+
if (args.heyGenEnabled !== undefined && !Object.keys(body).length && args.motionIntensityPercent === undefined && args.maxAvatarMinutesPerMonth === undefined && args.maxAvatarTtsChars === undefined) return `Site ${args.siteId}: EnableAvatarHeyGen=${args.heyGenEnabled}`;
|
|
9427
9792
|
if (args.motionIntensityPercent !== undefined) {
|
|
9428
9793
|
if (!Number.isInteger(args.motionIntensityPercent) || args.motionIntensityPercent < 25 || args.motionIntensityPercent > 150) throw new Error('motionIntensityPercent must be a whole number between 25 and 150.');
|
|
9429
9794
|
body.motionIntensityPercent = args.motionIntensityPercent;
|
|
@@ -9457,6 +9822,22 @@ async function handleTool(name, args) {
|
|
|
9457
9822
|
return JSON.stringify(await messagingRequest('PUT', `/avatar/workflows/${Number(args.workflowId)}/override`, avatarOverrideBody(args)), null, 2);
|
|
9458
9823
|
case 'avatar_reset_workflow':
|
|
9459
9824
|
return JSON.stringify(await messagingRequest('DELETE', `/avatar/workflows/${Number(args.workflowId)}/override`), null, 2);
|
|
9825
|
+
case 'avatar_set_workflow_switch': {
|
|
9826
|
+
const mode = String(args.mode ?? '').trim().toLowerCase();
|
|
9827
|
+
if (!['on', 'off', 'rules'].includes(mode)) throw new Error('mode must be one of: on, off, rules');
|
|
9828
|
+
const id = Number(args.workflowId);
|
|
9829
|
+
if (!Number.isInteger(id) || id <= 0) throw new Error('workflowId must be a positive whole number');
|
|
9830
|
+
return JSON.stringify(await messagingRequest('PUT', `/avatar/workflows/${id}/enabled`, { mode }), null, 2);
|
|
9831
|
+
}
|
|
9832
|
+
case 'avatar_bulk_set_switch': {
|
|
9833
|
+
const mode = String(args.mode ?? '').trim().toLowerCase();
|
|
9834
|
+
if (!['on', 'off', 'rules'].includes(mode)) throw new Error('mode must be one of: on, off, rules');
|
|
9835
|
+
const workflowIds = (args.workflowIds ?? []).map(Number);
|
|
9836
|
+
const groupIds = (args.groupIds ?? []).map(Number);
|
|
9837
|
+
if (workflowIds.length + groupIds.length === 0) throw new Error('Pass workflowIds and/or groupIds');
|
|
9838
|
+
if (workflowIds.length + groupIds.length > 500) throw new Error('Too many rows in one call (max 500)');
|
|
9839
|
+
return JSON.stringify(await messagingRequest('PUT', '/avatar/overrides/enabled', { mode, workflowIds, groupIds }), null, 2);
|
|
9840
|
+
}
|
|
9460
9841
|
case 'avatar_reset_all_overrides':
|
|
9461
9842
|
return JSON.stringify(await messagingRequest('POST', '/avatar/group-overrides/reset-all', {}), null, 2);
|
|
9462
9843
|
|
|
@@ -9980,7 +10361,11 @@ async function handleTool(name, args) {
|
|
|
9980
10361
|
}
|
|
9981
10362
|
|
|
9982
10363
|
case 'magento_test_connection': {
|
|
9983
|
-
|
|
10364
|
+
if (!args.configId && (!args.storeUrl || !args.accessToken)) return 'Provide configId, or both storeUrl and accessToken.';
|
|
10365
|
+
const r = args.configId
|
|
10366
|
+
? await api('POST', `/Magento/Configs/${encodeURIComponent(args.configId)}/Test`)
|
|
10367
|
+
: await api('POST', '/Magento/Configs/Test', { storeUrl: args.storeUrl, accessToken: args.accessToken });
|
|
10368
|
+
if (r && r.valid === false) return `❌ Connection failed: ${r.error}`;
|
|
9984
10369
|
return `✅ Connected to "${r.storeName}" (${r.edition} ${r.version})`;
|
|
9985
10370
|
}
|
|
9986
10371
|
|
|
@@ -10046,6 +10431,13 @@ async function handleTool(name, args) {
|
|
|
10046
10431
|
}
|
|
10047
10432
|
|
|
10048
10433
|
// ── Business actions + SAP ────────────────────────────────────────────────
|
|
10434
|
+
case 'salesforce_diagnose': {
|
|
10435
|
+
if (!args.siteId) return 'siteId is required.';
|
|
10436
|
+
const deep = args.deep === false ? 'false' : 'true';
|
|
10437
|
+
const r = await messagingRequest('GET', `/diagnostics/test-salesforce?siteId=${encodeURIComponent(args.siteId)}&deep=${deep}`);
|
|
10438
|
+
return JSON.stringify(r, null, 2);
|
|
10439
|
+
}
|
|
10440
|
+
|
|
10049
10441
|
case 'sap_diagnose': {
|
|
10050
10442
|
if (!args.siteId) return 'siteId is required.';
|
|
10051
10443
|
const deep = args.deep === false ? 'false' : 'true';
|
|
@@ -10134,6 +10526,37 @@ async function handleTool(name, args) {
|
|
|
10134
10526
|
return `CTI token generated/rotated.\nDial URL: ${urls.dialUrlTemplate}\nScreen-pop URL: ${urls.screenPopUrl}`;
|
|
10135
10527
|
}
|
|
10136
10528
|
|
|
10529
|
+
case 'netsuite_suiteapp_download': {
|
|
10530
|
+
const includeKey = args.includeKey !== false;
|
|
10531
|
+
const r = await messagingGet(`/api/v1/CrmEmbed/suiteapp-package?includeKey=${includeKey}&format=json`);
|
|
10532
|
+
const d = r?.data ?? r;
|
|
10533
|
+
if (!d?.contentBase64) throw new Error('SuiteApp package was not returned.');
|
|
10534
|
+
const { writeFileSync } = await import('node:fs');
|
|
10535
|
+
const { tmpdir } = await import('node:os');
|
|
10536
|
+
const out = args.outputPath || _join(tmpdir(), d.fileName || 'velaro-netsuite-screenpop-suiteapp.zip');
|
|
10537
|
+
writeFileSync(out, Buffer.from(d.contentBase64, 'base64'));
|
|
10538
|
+
return `Saved ${out}. ` + (d.keyIncluded
|
|
10539
|
+
? 'Your Velaro key is already inside: no NetSuite API Secret and nothing to paste. Deploy with suitecloud (see SETUP.txt in the zip). Keep the zip private.'
|
|
10540
|
+
: `Key-less zip. In NetSuite create an API Secret: type ID ${d.secretFormId} (NetSuite adds the prefix, giving ${d.secretFullId}), paste your crm_embed_ key as Password.`);
|
|
10541
|
+
}
|
|
10542
|
+
case 'netsuite_config_delete': {
|
|
10543
|
+
await messagingDel(`/Netsuite/Integrations/${args.configId}`);
|
|
10544
|
+
return `Deleted NetSuite config ${args.configId} (a missing config is a no-op).`;
|
|
10545
|
+
}
|
|
10546
|
+
case 'netsuite_m2m_clear_legacy': {
|
|
10547
|
+
const r = await messagingPost('/NetSuite/M2M/ClearLegacy', { id: args.configId });
|
|
10548
|
+
return `Legacy consumer secret/token cleared for config ${r?.id ?? args.configId} (authType ${r?.authType ?? 'm2m'}).`;
|
|
10549
|
+
}
|
|
10550
|
+
case 'netsuite_embed_status': {
|
|
10551
|
+
const r = await messagingGet('/api/v1/CrmEmbed/embed-status');
|
|
10552
|
+
const d = r?.data ?? r;
|
|
10553
|
+
if (!d?.hasEmbedKey) return 'No embed key exists for this site yet. The SuiteApp cannot connect until one is created.';
|
|
10554
|
+
if (d.connected) return `SuiteApp connected. Last polled ${d.lastPolledAt}.`;
|
|
10555
|
+
return d.lastPolledAt
|
|
10556
|
+
? `Embed key exists but the SuiteApp is not connected (last polled ${d.lastPolledAt}, outside the connected window).`
|
|
10557
|
+
: 'Embed key exists but the SuiteApp has not polled yet.';
|
|
10558
|
+
}
|
|
10559
|
+
|
|
10137
10560
|
case 'netsuite_m2m_init': {
|
|
10138
10561
|
const r = await messagingPost('/NetSuite/M2M/Init', { id: args.configId ?? null, company: args.company, displayName: args.displayName });
|
|
10139
10562
|
return `Config ${r.id ?? args.configId ?? 'new'}. Upload this certificate in NetSuite (Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup):\n${r.certificatePem ?? '(none returned)'}\n${Array.isArray(r.steps) ? r.steps.map((x, i) => `${i + 1}. ${x}`).join('\n') : ''}`;
|
|
@@ -10182,6 +10605,20 @@ async function handleTool(name, args) {
|
|
|
10182
10605
|
return `${result.success ? 'Success' : 'Failed'}: ${result.message ?? '(no message)'}`;
|
|
10183
10606
|
}
|
|
10184
10607
|
|
|
10608
|
+
// ── Shopify write policy (velaro-messaging /api/Shopify/WritePolicy) ───────
|
|
10609
|
+
case 'shopify_get_write_policy': {
|
|
10610
|
+
return JSON.stringify(await messagingGet('/Shopify/WritePolicy'), null, 2);
|
|
10611
|
+
}
|
|
10612
|
+
case 'shopify_set_write_policy': {
|
|
10613
|
+
const body = {};
|
|
10614
|
+
for (const k of ['enableShopifyWrite', 'requireConfirmation', 'maxRefundAmount', 'maxDiscountPercent', 'maxDiscountFixedAmount']) {
|
|
10615
|
+
if (args[k] !== undefined) body[k] = args[k];
|
|
10616
|
+
}
|
|
10617
|
+
if (!Object.keys(body).length) throw new Error('Provide at least one field to change.');
|
|
10618
|
+
const result = await messagingPut('/Shopify/WritePolicy', body);
|
|
10619
|
+
return `Shopify write policy updated.
|
|
10620
|
+
${JSON.stringify(result, null, 2)}`;
|
|
10621
|
+
}
|
|
10185
10622
|
// ── Webflow (config CRUD) ──────────────────────────────────────────────────
|
|
10186
10623
|
case 'webflow_get_config': {
|
|
10187
10624
|
if (args.configId) {
|
|
@@ -10742,6 +11179,19 @@ async function handleTool(name, args) {
|
|
|
10742
11179
|
return lines.join(String.fromCharCode(10));
|
|
10743
11180
|
}
|
|
10744
11181
|
|
|
11182
|
+
case 'entitlement_report_packages': {
|
|
11183
|
+
const q = args && args.siteId ? `?siteId=${encodeURIComponent(args.siteId)}` : '';
|
|
11184
|
+
const pkg = await api('GET', `/Entitlements/report-packages${q}`);
|
|
11185
|
+
const lines = [`${pkg.top.name} (${pkg.top.slug}) - plan ${pkg.plan}, default OFF${pkg.siteId ? `, site ${pkg.siteId}${pkg.top.enabled ? ' [ALL ON]' : ''}` : ''}`, ` ${pkg.top.tagline}`];
|
|
11186
|
+
for (const c of pkg.categories) {
|
|
11187
|
+
lines.push('');
|
|
11188
|
+
lines.push(`${c.name} (${c.slug})${pkg.siteId ? ` [${c.enabled ? 'ON ' : 'off'}] ${c.enabledCount}/${c.reportCount} reports` : ` ${c.reportCount} reports`}`);
|
|
11189
|
+
lines.push(` ${c.tagline}`);
|
|
11190
|
+
for (const r of c.reports) lines.push(` ${pkg.siteId ? (r.enabled ? '[ON ] ' : '[off] ') : ''}${r.name} (${r.id}) - package ${r.packageSlug}, key ${r.packageKey}`);
|
|
11191
|
+
}
|
|
11192
|
+
return lines.join(String.fromCharCode(10));
|
|
11193
|
+
}
|
|
11194
|
+
|
|
10745
11195
|
case 'entitlement_list_features': {
|
|
10746
11196
|
const features = await api('GET', '/Entitlements/features');
|
|
10747
11197
|
const byCategory = {};
|
|
@@ -10946,7 +11396,12 @@ async function handleTool(name, args) {
|
|
|
10946
11396
|
throw new Error('Date range too large. Worked Hours can be queried for at most 90 days at a time.');
|
|
10947
11397
|
}
|
|
10948
11398
|
}
|
|
10949
|
-
|
|
11399
|
+
// Server End is exclusive: a date-only end means "through the end of that day".
|
|
11400
|
+
let endValue = args.end;
|
|
11401
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(String(args.end).trim()) && !Number.isNaN(endMs)) {
|
|
11402
|
+
endValue = new Date(endMs + 24 * 60 * 60 * 1000).toISOString();
|
|
11403
|
+
}
|
|
11404
|
+
return await messagingPost('/Reports/worked-hours-breakdown', { Start: args.start, End: endValue });
|
|
10950
11405
|
}
|
|
10951
11406
|
|
|
10952
11407
|
// -- Pickup & Reminder Texting --
|
|
@@ -10994,6 +11449,25 @@ async function handleTool(name, args) {
|
|
|
10994
11449
|
return JSON.stringify(result, null, 2);
|
|
10995
11450
|
}
|
|
10996
11451
|
|
|
11452
|
+
case 'apple_pay_get_config': {
|
|
11453
|
+
const config = await messagingGet('/ApplePayConfig');
|
|
11454
|
+
return JSON.stringify(config, null, 2);
|
|
11455
|
+
}
|
|
11456
|
+
|
|
11457
|
+
case 'apple_pay_update_config': {
|
|
11458
|
+
// Field names must match ApplePayConfigUpdateRequest exactly (apply-if-present: undefined is dropped by JSON).
|
|
11459
|
+
const body = {
|
|
11460
|
+
merchantDisplayName: args.merchantDisplayName,
|
|
11461
|
+
isTestMode: args.isTestMode,
|
|
11462
|
+
bubbleLogoBase64: args.bubbleLogoBase64,
|
|
11463
|
+
};
|
|
11464
|
+
const existing = await messagingGet('/ApplePayConfig');
|
|
11465
|
+
const result = existing && existing.id > 0
|
|
11466
|
+
? await messagingPut(`/ApplePayConfig/${existing.id}`, body)
|
|
11467
|
+
: await messagingPost('/ApplePayConfig', body);
|
|
11468
|
+
return JSON.stringify(result, null, 2);
|
|
11469
|
+
}
|
|
11470
|
+
|
|
10997
11471
|
// -- Payment Recovery --
|
|
10998
11472
|
case 'payment_recovery_send': {
|
|
10999
11473
|
// Field names must match SendPaymentReminderRequest exactly.
|
|
@@ -11658,7 +12132,7 @@ const server = new Server(
|
|
|
11658
12132
|
{ capabilities: { tools: {}, prompts: {} } }
|
|
11659
12133
|
);
|
|
11660
12134
|
|
|
11661
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
12135
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS.map(withAnnotations) }));
|
|
11662
12136
|
|
|
11663
12137
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
|
|
11664
12138
|
|