@velaro/mcp-server 0.6.92 → 0.6.94

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/server.js +322 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.92",
3
+ "version": "0.6.94",
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": {
@@ -10,7 +10,7 @@
10
10
  "node": ">=18.0.0"
11
11
  },
12
12
  "dependencies": {
13
- "@modelcontextprotocol/sdk": "^1.30.0",
13
+ "@modelcontextprotocol/sdk": "^1.32.1",
14
14
  "qs": "^6.16.0"
15
15
  },
16
16
  "overrides": {
package/server.js CHANGED
@@ -151,6 +151,18 @@ function extractLogEntries(response) {
151
151
  return [];
152
152
  }
153
153
 
154
+ /**
155
+ * Body for PUT avatar/groups|workflows/{id}/override. Merge by default (only passed fields change, "" clears one);
156
+ * `replace: true` replaces the whole override. Mirrors cli/lib/commands/avatar.js.
157
+ */
158
+ function avatarOverrideBody(args) {
159
+ const body = { merge: args.replace !== true };
160
+ for (const k of ['providerSlug', 'avatarId', 'personalityPresetSlug', 'voiceId', 'displayName', 'backgroundMode', 'backgroundValue']) {
161
+ if (args[k] !== undefined) body[k] = args[k];
162
+ }
163
+ return body;
164
+ }
165
+
154
166
  async function messagingRequest(method, path, body) {
155
167
  if (!MESSAGING_API) throw new Error(
156
168
  'VELARO_MESSAGING_API is not configured. Add "VELARO_MESSAGING_API": "https://velaro-messaging-api-staging.azurewebsites.net" to your MCP env config.'
@@ -1048,6 +1060,7 @@ Key node shapes:
1048
1060
  propName:"myAnswerVar" } // variable that stores the result as JSON array string
1049
1061
  • condition: { type:"action", actionType:"condition", conditionOptions:[...] }
1050
1062
  • transferToAI: { type:"action", actionType:"transferToAI", aiConfigId:N }
1063
+ 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.
1051
1064
 
1052
1065
  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.`,
1053
1066
  inputSchema: {
@@ -1061,6 +1074,20 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1061
1074
  },
1062
1075
  },
1063
1076
 
1077
+ {
1078
+ name: 'ai_followups_site_get',
1079
+ description: "Read this account's default for AI follow-up suggestions (the AI Chatbot node's \"AI mode\": after each web-chat bot reply the bot offers 2-4 short tappable follow-up replies). A node's own suggestFollowUps setting wins over this default; platform default is off.",
1080
+ inputSchema: { type: 'object', properties: {}, required: [] },
1081
+ },
1082
+ {
1083
+ name: 'ai_followups_site_set',
1084
+ description: "Set this account's default for AI follow-up suggestions. enabled=true/false, or omit/null to clear (platform default: off). Per-node settings (workflow_update_node: suggestFollowUps) override this. Returns the value read back from the server.",
1085
+ inputSchema: {
1086
+ type: 'object',
1087
+ properties: { enabled: { type: ['boolean', 'null'], description: 'true = on, false = off, null/omitted = clear' } },
1088
+ required: [],
1089
+ },
1090
+ },
1064
1091
  // ── Surveys ──────────────────────────────────────────────────────────────
1065
1092
  {
1066
1093
  name: 'survey_list',
@@ -1594,10 +1621,28 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1594
1621
  },
1595
1622
  buttonLabel: { type: 'string', description: 'Optional action button label' },
1596
1623
  buttonUrl: { type: 'string', description: 'Optional action button URL (required if buttonLabel is set)' },
1624
+ mentions: { type: 'array', description: 'People to @mention (real Teams notification). Velaro staff only. Uses the ops Teams sender instead of a webhook.', items: { type: 'object', properties: { user: { type: 'string', description: 'Entra object id or UPN, e.g. andrea@velaro.com' }, name: { type: 'string', description: 'Display name shown in the @mention' } }, required: ['user'] } },
1625
+ destination: { type: 'string', description: 'Named channel in the DevOps team instead of General: prs, workflows, deploys, alerts, qa, general. Velaro staff only; uses the ops Teams sender instead of a webhook.' },
1597
1626
  },
1598
1627
  required: ['title'],
1599
1628
  },
1600
1629
  },
1630
+ {
1631
+ name: 'teams_send_direct',
1632
+ description: 'Send a 1:1 Teams message (Adaptive Card) directly to any user in the Velaro tenant by UPN/email or Entra object id. Velaro staff only. Graph chat first, Velaro Bot proactive fallback.',
1633
+ inputSchema: {
1634
+ type: 'object',
1635
+ properties: {
1636
+ user: { type: 'string', description: 'UPN/email (alex@velaro.com) or Entra object id' },
1637
+ title: { type: 'string', description: 'Card title' },
1638
+ message: { type: 'string', description: 'Message text' },
1639
+ fields: { type: 'array', description: 'Optional fact rows', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } }, required: ['label', 'value'] } },
1640
+ buttonLabel: { type: 'string', description: 'Optional button label' },
1641
+ buttonUrl: { type: 'string', description: 'Optional button URL' },
1642
+ },
1643
+ required: ['user', 'title'],
1644
+ },
1645
+ },
1601
1646
 
1602
1647
  // ── Slack ─────────────────────────────────────────────────────────────────
1603
1648
  {
@@ -1682,6 +1727,8 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1682
1727
  v20_site_id: { type: 'number', description: 'V20 site ID to migrate TO (defaults to the authenticated site)' },
1683
1728
  dry_run: { type: 'boolean', description: 'Preview what would be created without creating. Default: false.' },
1684
1729
  enable: { type: 'boolean', description: 'Enable created workflows immediately. Default: false.' },
1730
+ include_non_routing: { type: 'boolean', description: 'Also migrate v10 rules with IsRouting=false (group-assignment rules). Default: false.' },
1731
+ route_bot_groups_to_team: { type: 'boolean', description: 'Route to the v20 team named after the v10 group even if it had a v10 bot. Default: false.' },
1685
1732
  team_mappings: { type: 'array', description: 'Override auto-resolution: [{v10GroupId, v20TeamId}]', items: { type: 'object' } },
1686
1733
  ai_config_mappings: { type: 'array', description: 'Override auto-resolution: [{v10GroupId, v20AiConfigId}]', items: { type: 'object' } },
1687
1734
  },
@@ -1696,10 +1743,25 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1696
1743
  properties: {
1697
1744
  v10_site_id: { type: 'number', description: 'V10 (Livefluence) site ID' },
1698
1745
  v20_site_id: { type: 'number', description: 'V20 site ID to match against (for name suggestions)' },
1746
+ include_non_routing: { type: 'boolean', description: 'Also include v10 rules with IsRouting=false. Default: false.' },
1699
1747
  },
1700
1748
  required: ['v10_site_id'],
1701
1749
  },
1702
1750
  },
1751
+ {
1752
+ name: 'livefluence_create_teams',
1753
+ description: 'Copy v10 (Livefluence) groups to v20 teams on a site. v10 is read-only; idempotent by name; "Default Group" maps to the site default team; members are not copied. Use dry_run=true first.',
1754
+ inputSchema: {
1755
+ type: 'object',
1756
+ properties: {
1757
+ v10_site_id: { type: 'number', description: 'V10 (Livefluence) site ID to copy FROM' },
1758
+ v20_site_id: { type: 'number', description: 'V20 site ID to create teams on' },
1759
+ group_names: { type: 'array', items: { type: 'string' }, description: 'Group names to copy (default: every non-deleted v10 group)' },
1760
+ dry_run: { type: 'boolean', description: 'Preview without creating. Default: false.' },
1761
+ },
1762
+ required: ['v10_site_id', 'v20_site_id'],
1763
+ },
1764
+ },
1703
1765
 
1704
1766
  // ── Smart Routing Engine ──────────────────────────────────────────────────
1705
1767
  {
@@ -3057,7 +3119,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3057
3119
  inputSchema: {
3058
3120
  type: 'object',
3059
3121
  properties: {
3060
- report: { type: 'string', enum: ['program_breakdown', 'agent_scorecard', 'qa_coverage', 'coaching_queue', 'sentiment_breakdown', 'topic_radar', 'weekly_brief', 'weekly_brief_send_test'] },
3122
+ 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'] },
3061
3123
  from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
3062
3124
  to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
3063
3125
  groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
@@ -3067,6 +3129,14 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3067
3129
  perAgent: { type: 'number', description: 'coaching_queue: chats listed per agent (default 5).' },
3068
3130
  limit: { type: 'number', description: 'topic_radar: max topics (default 25).' },
3069
3131
  days: { type: 'number', description: 'weekly_brief: days to cover (default 7).' },
3132
+ windowDays: { type: 'number', description: 'repeat_contact: days a contact has to come back to count as a repeat (1-30, default 7).' },
3133
+ weeks: { type: 'number', description: 'demand_coverage: whole local weeks of history (1-12, default 6). from/to are IGNORED by demand_coverage.' },
3134
+ handleMinutes: { type: 'number', description: 'demand_coverage: assumed minutes per chat (1-120, default 10).' },
3135
+ maxConcurrent: { type: 'number', description: 'demand_coverage: assumed chats an agent handles at once (1-10, default 3).' },
3136
+ timeZone: { type: 'string', description: 'demand_coverage: IANA time zone (default America/Los_Angeles).' },
3137
+ lowScore: { type: 'number', description: 'bot_containment: QA score below which a handed-off chat counts as regret (1-100, default 60).' },
3138
+ includeDemo: { type: 'boolean', description: 'true = show only the seeded sample (demo) data. Default false.' },
3139
+ promiseId: { type: 'string', description: 'promise_dismiss: the promise id to dismiss (managers only).' },
3070
3140
  },
3071
3141
  required: ['report'],
3072
3142
  },
@@ -3287,19 +3357,24 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3287
3357
  },
3288
3358
  {
3289
3359
  name: 'avatar_get_group_effective',
3290
- description: 'Show the effective avatar settings for a group (group override field, else site default) and whether an override exists.',
3291
- inputSchema: { type: 'object', properties: { groupId: { type: 'number' } }, required: ['groupId'] },
3360
+ description: 'Show the effective avatar settings (person, personality, voice, display name, backdrop) for a group, layered perWorkflow ?? perGroup ?? perSite. Pass workflowId too to see what a conversation in that workflow inside that group gets. Also returns whether each override exists.',
3361
+ inputSchema: { type: 'object', properties: { groupId: { type: 'number', description: 'Team (group) id.' }, workflowId: { type: 'number', description: 'Optional workflow id to layer on top of the group.' } }, required: ['groupId'] },
3292
3362
  },
3293
3363
  {
3294
3364
  name: 'avatar_set_group_override',
3295
- description: 'Override avatar/personality/provider for one group. Omitted fields inherit the site default.',
3365
+ description: 'Override the avatar person/personality/voice/display name/backdrop for one group (a group is a Team). Only the fields you pass change (merge); pass an empty string to clear one field back to the site default. A library avatarId always uses the velaro provider. Backdrop: backgroundMode none|color|gradient|image with backgroundValue (hex, "#a,#b", or an https image URL).',
3296
3366
  inputSchema: {
3297
3367
  type: 'object',
3298
3368
  properties: {
3299
3369
  groupId: { type: 'number' },
3300
3370
  providerSlug: { type: 'string' },
3301
- avatarId: { type: 'string' },
3371
+ avatarId: { type: 'string', description: 'Library slug (see avatar_list_library), e.g. priya.' },
3302
3372
  personalityPresetSlug: { type: 'string' },
3373
+ voiceId: { type: 'string', description: 'ElevenLabs voice id.' },
3374
+ displayName: { type: 'string', description: 'Visitor-facing avatar name.' },
3375
+ backgroundMode: { type: 'string', description: 'none | color | gradient | image. "none" = explicitly no backdrop.' },
3376
+ backgroundValue: { type: 'string', description: 'Hex color, "#a,#b" gradient, or https image URL.' },
3377
+ replace: { type: 'boolean', description: 'true = replace the whole override (omitted fields inherit) instead of merging.' },
3303
3378
  },
3304
3379
  required: ['groupId'],
3305
3380
  },
@@ -3309,9 +3384,38 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3309
3384
  description: 'Reset one group to the site default avatar settings (removes its override).',
3310
3385
  inputSchema: { type: 'object', properties: { groupId: { type: 'number' } }, required: ['groupId'] },
3311
3386
  },
3387
+ {
3388
+ name: 'avatar_get_workflow_effective',
3389
+ description: 'Show the effective avatar settings for a workflow (workflow override, else its group, else site). Pass groupId to include a group override beneath the workflow.',
3390
+ inputSchema: { type: 'object', properties: { workflowId: { type: 'number' }, groupId: { type: 'number' } }, required: ['workflowId'] },
3391
+ },
3392
+ {
3393
+ name: 'avatar_set_workflow_override',
3394
+ description: 'Override the avatar person/personality/voice/display name/backdrop for one workflow (wins over its group and the site). Same fields and merge semantics as avatar_set_group_override. Example: library avatar priya with the Timken HR office backdrop https://admin.velaro.com/avatar-backdrops/timken-hr-office.png for the recruiter workflow.',
3395
+ inputSchema: {
3396
+ type: 'object',
3397
+ properties: {
3398
+ workflowId: { type: 'number' },
3399
+ providerSlug: { type: 'string' },
3400
+ avatarId: { type: 'string' },
3401
+ personalityPresetSlug: { type: 'string' },
3402
+ voiceId: { type: 'string' },
3403
+ displayName: { type: 'string' },
3404
+ backgroundMode: { type: 'string', description: 'none | color | gradient | image' },
3405
+ backgroundValue: { type: 'string' },
3406
+ replace: { type: 'boolean' },
3407
+ },
3408
+ required: ['workflowId'],
3409
+ },
3410
+ },
3411
+ {
3412
+ name: 'avatar_reset_workflow',
3413
+ description: 'Reset one workflow to its group / site default avatar settings (removes its override).',
3414
+ inputSchema: { type: 'object', properties: { workflowId: { type: 'number' } }, required: ['workflowId'] },
3415
+ },
3312
3416
  {
3313
3417
  name: 'avatar_reset_all_overrides',
3314
- description: 'Reset ALL group overrides for the calling site back to the site default.',
3418
+ description: 'Reset ALL group and workflow overrides for the calling site back to the site default.',
3315
3419
  inputSchema: { type: 'object', properties: {} },
3316
3420
  },
3317
3421
 
@@ -3750,6 +3854,15 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3750
3854
  required: [],
3751
3855
  },
3752
3856
  },
3857
+ {
3858
+ name: 'ai_federation',
3859
+ description: 'SuperAdmin: Anthropic Workload Identity Federation for the AI gateway. action=status shows enabled, circuit breaker state, last success/failure reason, federated vs API-key fallback counts and token expiry. action=test does a real token exchange plus a 1-token Haiku message and reports which path (federated, apikey, apikey-fallback) served it. action=reset_breaker closes the breaker so the next call retries federation.',
3860
+ inputSchema: {
3861
+ type: 'object',
3862
+ properties: { action: { type: 'string', enum: ['status', 'test', 'reset_breaker'], description: 'Default status.' } },
3863
+ required: [],
3864
+ },
3865
+ },
3753
3866
  {
3754
3867
  name: 'status_incident_list',
3755
3868
  description: 'Velaro staff only (SuperAdmin). Lists public status page incidents, including component overrides (IncidentType "Override").',
@@ -4998,6 +5111,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4998
5111
  properties: {
4999
5112
  publicApplyEnabled: { type: 'boolean' },
5000
5113
  autoCreateCaseTypeId: { type: 'number', description: 'Case type id; 0 clears the auto-create link' },
5114
+ defaultListingDays: { type: 'number', description: '1-365: how many days a new listing stays live when its feed gives no expiry. Requires the Job Board lifecycle package.' },
5001
5115
  },
5002
5116
  required: [],
5003
5117
  },
@@ -5103,6 +5217,68 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5103
5217
  required: ['sourceId', 'title'],
5104
5218
  },
5105
5219
  },
5220
+ // -- Job Board listing lifecycle (package "job-board-lifecycle", default OFF: 403 for a site without it) --
5221
+ {
5222
+ name: 'jobboard_lifecycle_status',
5223
+ description: 'Does this site have the Job Board listing lifecycle package (close/renew, expiry, JSON-LD listing pages), and what is its default listing duration? Returns { entitled, defaultListingDays }.',
5224
+ inputSchema: { type: 'object', properties: {}, required: [] },
5225
+ },
5226
+ {
5227
+ name: 'jobboard_close_listing',
5228
+ description: 'Close a job listing: it stops appearing in search and the public board, the public listing page answers 410, and applying to it answers 410. Idempotent. Requires the Job Board lifecycle package (403 otherwise); 404 for an id that is not on this site.',
5229
+ inputSchema: { type: 'object', properties: { listingId: { type: 'number' } }, required: ['listingId'] },
5230
+ },
5231
+ {
5232
+ name: 'jobboard_renew_listing',
5233
+ description: 'Renew a job listing: extends its expiry by `days` (default: the site\'s default listing days) from the later of its current expiry and now, increments its renewal count and republishes it if it was closed or expired. Requires the Job Board lifecycle package (403 otherwise).',
5234
+ inputSchema: {
5235
+ type: 'object',
5236
+ properties: {
5237
+ listingId: { type: 'number' },
5238
+ days: { type: 'number', description: '1-365; omit for the site default' },
5239
+ },
5240
+ required: ['listingId'],
5241
+ },
5242
+ },
5243
+ // -- Job Board hosted pages (package "job-board-hosted", default OFF: 403 for a site without it) --
5244
+ {
5245
+ name: 'jobboard_hosted_status',
5246
+ description: 'Hosted public job pages for this site: is the package included, are the pages published, and the opaque board link, sitemap, Indeed feed and Google feed URLs (the feed URLs contain a secret key). Returns { entitled, enabled, boardUrl, sitemapUrl, indeedFeedUrl, googleFeedUrl, publicApplyEnabled, eeocSelfIdAvailable, liveListings }; { entitled:false } without the package.',
5247
+ inputSchema: { type: 'object', properties: {}, required: [] },
5248
+ },
5249
+ {
5250
+ name: 'jobboard_hosted_set',
5251
+ description: 'Publish or unpublish the hosted job pages (apply-if-present). enabled=true issues the opaque board link and feed key the first time. regenerateLink=true issues a NEW board link and every old hosted URL answers 404 (administrators only). Requires the Job Board hosted pages package (403 otherwise).',
5252
+ inputSchema: {
5253
+ type: 'object',
5254
+ properties: {
5255
+ enabled: { type: 'boolean' },
5256
+ regenerateLink: { type: 'boolean', description: 'Retire the current board link and issue a new one (administrators only)' },
5257
+ },
5258
+ required: [],
5259
+ },
5260
+ },
5261
+ {
5262
+ name: 'jobboard_rotate_feed_key',
5263
+ description: 'Rotate the outbound feed key: the current Indeed and Google feed URLs stop working immediately and the response carries the new ones. Administrators only; requires the Job Board hosted pages package (403 otherwise).',
5264
+ inputSchema: { type: 'object', properties: {}, required: [] },
5265
+ },
5266
+ {
5267
+ name: 'jobboard_set_listing_pay',
5268
+ description: 'Set or clear the pay range of a job listing (pay transparency): shown on the hosted page and in its schema.org baseSalary. A manual value is never overwritten by a feed sync. Pass clear=true to remove it. Requires the Job Board hosted pages package (403 otherwise); 404 for an id that is not on this site.',
5269
+ inputSchema: {
5270
+ type: 'object',
5271
+ properties: {
5272
+ listingId: { type: 'number' },
5273
+ min: { type: 'number', description: 'Minimum (or only) amount, greater than 0' },
5274
+ max: { type: 'number', description: 'Maximum amount, at least min (optional)' },
5275
+ currency: { type: 'string', description: '3-letter ISO code, default USD' },
5276
+ period: { type: 'string', description: 'Hour | Day | Week | Month | Year' },
5277
+ clear: { type: 'boolean', description: 'Remove the stated pay' },
5278
+ },
5279
+ required: ['listingId'],
5280
+ },
5281
+ },
5106
5282
  {
5107
5283
  name: 'jobboard_move_application_stage',
5108
5284
  description: 'Move a candidate application to a new HR/agency-side pipeline stage on the applicant Kanban board (Applied, Screened, Interview, Offer, Hired, Rejected).',
@@ -5994,6 +6170,17 @@ async function handleTool(name, args) {
5994
6170
  // [Authorize] does not accept vel_live_* service keys (only a handful of controllers opt
5995
6171
  // into that manually) — these tools need VELARO_JWT, same requirement as the existing
5996
6172
  // notifications_get/ai_config_staff_get tools they're modeled on.
6173
+ case 'ai_followups_site_get': {
6174
+ const r = await messagingGet('/SiteSettings/AiFollowUps');
6175
+ return JSON.stringify({ enabled: r?.enabled ?? null }, null, 2);
6176
+ }
6177
+
6178
+ case 'ai_followups_site_set': {
6179
+ await messagingPut('/SiteSettings/AiFollowUps', { enabled: typeof args.enabled === 'boolean' ? args.enabled : null });
6180
+ const r = await messagingGet('/SiteSettings/AiFollowUps');
6181
+ return JSON.stringify({ enabled: r?.enabled ?? null }, null, 2);
6182
+ }
6183
+
5997
6184
  case 'survey_list': {
5998
6185
  const surveys = (await messagingGet('/Surveys/list')) ?? [];
5999
6186
  if (!surveys.length) return 'No surveys found.';
@@ -6492,7 +6679,26 @@ async function handleTool(name, args) {
6492
6679
  return await api('PUT', '/TeamsBot/ChannelConfig', body);
6493
6680
  }
6494
6681
 
6682
+ case 'teams_send_direct': {
6683
+ const r = await api('POST', '/TeamsMessaging/Direct', {
6684
+ user: args.user, title: args.title, message: args.message || '',
6685
+ facts: (args.fields || []).map(f => ({ label: f.label, value: f.value })),
6686
+ actionUrl: args.buttonUrl, actionLabel: args.buttonLabel,
6687
+ });
6688
+ return r?.sent ? `Direct message sent to ${args.user} via ${r.path}.` : `Send failed: ${r?.error || 'unknown error'} ${(r?.warnings || []).join('; ')}`;
6689
+ }
6690
+
6495
6691
  case 'teams_notify_card': {
6692
+ if ((args.mentions && args.mentions.length) || args.destination) {
6693
+ const r = await api('POST', '/TeamsMessaging/Card', {
6694
+ title: args.title,
6695
+ facts: (args.fields || []).map(f => ({ label: f.label, value: f.value })),
6696
+ actionUrl: args.buttonUrl, actionLabel: args.buttonLabel,
6697
+ destination: args.destination,
6698
+ mentions: (args.mentions || []).map(m => ({ user: m.user, name: m.name })),
6699
+ });
6700
+ return r?.sent ? `Card sent via ${r.path}${r.warnings?.length ? ' (' + r.warnings.join('; ') + ')' : ''}.` : `Send failed: ${r?.error || 'unknown error'}`;
6701
+ }
6496
6702
  const webhooks = await api('GET', '/TeamsNotification');
6497
6703
  if (!Array.isArray(webhooks) || !webhooks.length)
6498
6704
  return 'No Teams webhooks configured. Go to Integrations → Microsoft Teams to add one, or call velaro teams create <name> <webhookUrl> via CLI.';
@@ -6820,7 +7026,10 @@ async function handleTool(name, args) {
6820
7026
 
6821
7027
  // ── Livefluence Migration ──────────────────────────────────────────────
6822
7028
  case 'livefluence_export': {
6823
- const qs = args.v20_site_id ? `?v20SiteId=${args.v20_site_id}` : '';
7029
+ const params = [];
7030
+ if (args.v20_site_id) params.push(`v20SiteId=${args.v20_site_id}`);
7031
+ if (args.include_non_routing) params.push('includeNonRouting=true');
7032
+ const qs = params.length ? `?${params.join('&')}` : '';
6824
7033
  const data = await api('GET', `/DatabaseTool/V10Export/${args.v10_site_id}${qs}`);
6825
7034
  const lines = [
6826
7035
  `V10 Site ${data.v10SiteId} — ${data.ruleCount} routing rule(s), ${data.groupCount} group(s)`,
@@ -6845,6 +7054,16 @@ async function handleTool(name, args) {
6845
7054
  return lines.join('\n');
6846
7055
  }
6847
7056
 
7057
+ case 'livefluence_create_teams': {
7058
+ const result = await api('POST', '/DatabaseTool/V10CreateTeams', {
7059
+ v10SiteId: args.v10_site_id,
7060
+ v20SiteId: args.v20_site_id,
7061
+ dryRun: args.dry_run ?? false,
7062
+ groupNames: args.group_names ?? [],
7063
+ });
7064
+ return JSON.stringify(result, null, 2);
7065
+ }
7066
+
6848
7067
  case 'livefluence_seed_url_rules': {
6849
7068
  const result = await api('POST', '/DatabaseTool/V10SeedUrlRules', {
6850
7069
  siteId: args.site_id, aiConfigId: args.ai_config_id, dryRun: !!args.dry_run, rules: args.rules,
@@ -6858,6 +7077,8 @@ async function handleTool(name, args) {
6858
7077
  v20SiteId: args.v20_site_id ?? null,
6859
7078
  dryRun: args.dry_run ?? false,
6860
7079
  enable: args.enable ?? false,
7080
+ includeNonRouting: args.include_non_routing ?? false,
7081
+ routeBotGroupsToTeam: args.route_bot_groups_to_team ?? false,
6861
7082
  teamMappings: args.team_mappings ?? [],
6862
7083
  aiConfigMappings: args.ai_config_mappings ?? [],
6863
7084
  });
@@ -6873,6 +7094,7 @@ async function handleTool(name, args) {
6873
7094
  for (const item of result.createdItems) {
6874
7095
  lines.push(` [#${item.priority}] ${item.ruleName}${item.workflowId ? ` → id=${item.workflowId}` : ''}`);
6875
7096
  lines.push(` Action: ${item.action}${item.v2AiConfigId ? `, aiConfigId=${item.v2AiConfigId}` : ''}${item.v2TeamId ? `, teamId=${item.v2TeamId}` : ''}`);
7097
+ for (const u of item.unconverted ?? []) lines.push(` not converted: ${u}`);
6876
7098
  }
6877
7099
  lines.push('');
6878
7100
  }
@@ -8485,12 +8707,18 @@ async function handleTool(name, args) {
8485
8707
  program_breakdown: 'program-breakdown', agent_scorecard: 'agent-scorecard', qa_coverage: 'qa-coverage',
8486
8708
  coaching_queue: 'coaching-queue', sentiment_breakdown: 'sentiment-breakdown', topic_radar: 'topic-radar',
8487
8709
  weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
8710
+ repeat_contact: 'repeat-contact', demand_coverage: 'demand-coverage', bot_containment: 'bot-containment',
8711
+ effort_index: 'effort-index', promise_ledger: 'promise-ledger',
8488
8712
  };
8713
+ if (args.report === 'promise_dismiss') {
8714
+ if (!args.promiseId) throw new Error('promiseId is required for promise_dismiss');
8715
+ return JSON.stringify(await messagingPost('/api/enterprise-reports/promise-ledger/' + encodeURIComponent(String(args.promiseId)) + '/dismiss', {}), null, 2);
8716
+ }
8489
8717
  const seg = ER_PATHS[args.report];
8490
8718
  if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', '));
8491
8719
  if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
8492
8720
  const qs = new URLSearchParams();
8493
- for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days']) {
8721
+ for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days', 'windowDays', 'weeks', 'handleMinutes', 'maxConcurrent', 'timeZone', 'lowScore', 'includeDemo']) {
8494
8722
  if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
8495
8723
  }
8496
8724
  const q = qs.toString();
@@ -8661,12 +8889,17 @@ async function handleTool(name, args) {
8661
8889
  return JSON.stringify(await messagingRequest('POST', '/avatar/site-default', {
8662
8890
  providerSlug: args.providerSlug, avatarId: args.avatarId, personalityPresetSlug: args.personalityPresetSlug }), null, 2);
8663
8891
  case 'avatar_get_group_effective':
8664
- return JSON.stringify(await messagingRequest('GET', `/avatar/groups/${Number(args.groupId)}/effective`), null, 2);
8892
+ return JSON.stringify(await messagingRequest('GET', `/avatar/groups/${Number(args.groupId)}/effective${args.workflowId != null ? `?workflowId=${Number(args.workflowId)}` : ''}`), null, 2);
8665
8893
  case 'avatar_set_group_override':
8666
- return JSON.stringify(await messagingRequest('PUT', `/avatar/groups/${Number(args.groupId)}/override`, {
8667
- providerSlug: args.providerSlug, avatarId: args.avatarId, personalityPresetSlug: args.personalityPresetSlug }), null, 2);
8894
+ return JSON.stringify(await messagingRequest('PUT', `/avatar/groups/${Number(args.groupId)}/override`, avatarOverrideBody(args)), null, 2);
8668
8895
  case 'avatar_reset_group':
8669
8896
  return JSON.stringify(await messagingRequest('DELETE', `/avatar/groups/${Number(args.groupId)}/override`), null, 2);
8897
+ case 'avatar_get_workflow_effective':
8898
+ return JSON.stringify(await messagingRequest('GET', `/avatar/workflows/${Number(args.workflowId)}/effective${args.groupId != null ? `?groupId=${Number(args.groupId)}` : ''}`), null, 2);
8899
+ case 'avatar_set_workflow_override':
8900
+ return JSON.stringify(await messagingRequest('PUT', `/avatar/workflows/${Number(args.workflowId)}/override`, avatarOverrideBody(args)), null, 2);
8901
+ case 'avatar_reset_workflow':
8902
+ return JSON.stringify(await messagingRequest('DELETE', `/avatar/workflows/${Number(args.workflowId)}/override`), null, 2);
8670
8903
  case 'avatar_reset_all_overrides':
8671
8904
  return JSON.stringify(await messagingRequest('POST', '/avatar/group-overrides/reset-all', {}), null, 2);
8672
8905
 
@@ -8908,6 +9141,13 @@ async function handleTool(name, args) {
8908
9141
  return JSON.stringify(data, null, 2);
8909
9142
  }
8910
9143
 
9144
+ case 'ai_federation': {
9145
+ const action = args.action || 'status';
9146
+ if (action === 'test') return JSON.stringify(await api('POST', '/AiFederation/test', {}), null, 2);
9147
+ if (action === 'reset_breaker') return JSON.stringify(await api('POST', '/AiFederation/reset-breaker', {}), null, 2);
9148
+ return JSON.stringify(await api('GET', '/AiFederation/status'), null, 2);
9149
+ }
9150
+
8911
9151
  case 'status_incident_list': {
8912
9152
  const rows = await messagingRequest('GET', `/superadmin/status-incidents?filter=${encodeURIComponent(args.filter || 'active')}`);
8913
9153
  return JSON.stringify(rows, null, 2);
@@ -10330,7 +10570,12 @@ async function handleTool(name, args) {
10330
10570
  if (!Number.isInteger(n) || n < 0) throw new Error('autoCreateCaseTypeId must be an integer >= 0 (0 clears).');
10331
10571
  body.autoCreateCaseTypeId = n;
10332
10572
  }
10333
- if (!Object.keys(body).length) throw new Error('Specify publicApplyEnabled and/or autoCreateCaseTypeId.');
10573
+ if (args.defaultListingDays !== undefined) {
10574
+ const d = Number(args.defaultListingDays);
10575
+ if (!Number.isInteger(d) || d < 1 || d > 365) throw new Error('defaultListingDays must be an integer from 1 to 365.');
10576
+ body.defaultListingDays = d;
10577
+ }
10578
+ if (!Object.keys(body).length) throw new Error('Specify publicApplyEnabled, autoCreateCaseTypeId and/or defaultListingDays.');
10334
10579
  const r = await messagingPut('/Recruiting/ApplySettings', body);
10335
10580
  return JSON.stringify(r, null, 2);
10336
10581
  }
@@ -10367,6 +10612,71 @@ async function handleTool(name, args) {
10367
10612
  }, null, 2);
10368
10613
  }
10369
10614
 
10615
+ case 'jobboard_lifecycle_status': {
10616
+ const r = await messagingGet('/Recruiting/Lifecycle');
10617
+ return JSON.stringify(r, null, 2);
10618
+ }
10619
+ case 'jobboard_close_listing': {
10620
+ const id = Number(args.listingId);
10621
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
10622
+ const r = await messagingPost(`/Recruiting/Jobs/${id}/close`, {});
10623
+ return JSON.stringify(r, null, 2);
10624
+ }
10625
+ case 'jobboard_renew_listing': {
10626
+ const id = Number(args.listingId);
10627
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
10628
+ const body = {};
10629
+ if (args.days !== undefined) {
10630
+ const d = Number(args.days);
10631
+ if (!Number.isInteger(d) || d < 1 || d > 365) throw new Error('days must be an integer from 1 to 365.');
10632
+ body.days = d;
10633
+ }
10634
+ const r = await messagingPost(`/Recruiting/Jobs/${id}/renew`, body);
10635
+ return JSON.stringify(r, null, 2);
10636
+ }
10637
+
10638
+ case 'jobboard_hosted_status': {
10639
+ const r = await messagingGet('/Recruiting/Hosted');
10640
+ return JSON.stringify(r, null, 2);
10641
+ }
10642
+ case 'jobboard_hosted_set': {
10643
+ const body = {};
10644
+ if (args.enabled !== undefined) body.enabled = Boolean(args.enabled);
10645
+ if (args.regenerateLink) body.regenerateSlug = true;
10646
+ if (!Object.keys(body).length) throw new Error('Specify enabled and/or regenerateLink (use jobboard_hosted_status to read).');
10647
+ const r = await messagingPut('/Recruiting/Hosted', body);
10648
+ return JSON.stringify(r, null, 2);
10649
+ }
10650
+ case 'jobboard_rotate_feed_key': {
10651
+ const r = await messagingPost('/Recruiting/Hosted/FeedKey/Rotate', {});
10652
+ return JSON.stringify(r, null, 2);
10653
+ }
10654
+ case 'jobboard_set_listing_pay': {
10655
+ const id = Number(args.listingId);
10656
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
10657
+ let body;
10658
+ if (args.clear) {
10659
+ body = { clear: true };
10660
+ } else {
10661
+ const lo = Number(args.min);
10662
+ if (args.min === undefined || !Number.isFinite(lo) || lo <= 0) throw new Error('min must be a number greater than 0 (or pass clear=true).');
10663
+ body = { salaryMin: lo };
10664
+ if (args.max !== undefined) {
10665
+ const hi = Number(args.max);
10666
+ if (!Number.isFinite(hi) || hi < lo) throw new Error('max must be a number greater than or equal to min.');
10667
+ body.salaryMax = hi;
10668
+ }
10669
+ const cur = String(args.currency ?? 'USD').trim().toUpperCase();
10670
+ if (!/^[A-Z]{3}$/.test(cur)) throw new Error('currency must be a 3-letter ISO code such as USD.');
10671
+ body.salaryCurrency = cur;
10672
+ const per = ['Hour', 'Day', 'Week', 'Month', 'Year'].find((p) => p.toLowerCase() === String(args.period ?? '').trim().toLowerCase());
10673
+ if (!per) throw new Error('period must be one of: Hour, Day, Week, Month, Year.');
10674
+ body.salaryPeriod = per;
10675
+ }
10676
+ const r = await messagingPut(`/Recruiting/Jobs/${id}/Pay`, body);
10677
+ return JSON.stringify(r, null, 2);
10678
+ }
10679
+
10370
10680
  case 'jobboard_move_application_stage': {
10371
10681
  const app = await messagingPut(`/Recruiting/Applications/${args.applicationId}/Stage`, { stage: args.stage });
10372
10682
  return JSON.stringify(app, null, 2);