@velaro/mcp-server 0.6.99 → 0.6.101

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 +1 -1
  2. package/server.js +226 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.99",
3
+ "version": "0.6.101",
4
4
  "description": "Velaro MCP server — connect Claude and other AI agents directly to your Velaro account: KB, workflows, bots, conversations, contacts, routing, and more.",
5
5
  "type": "module",
6
6
  "bin": {
package/server.js CHANGED
@@ -842,18 +842,19 @@ const TOOLS = [
842
842
  groupBy: { type: 'string', description: '"month" or "week" (default month)' },
843
843
  deploymentId: { type: 'number', description: 'Optional website (deployment row id) filter' },
844
844
  teamId: { type: 'number', description: 'Optional team filter' },
845
+ includeSecondary: { type: 'boolean', description: 'Also return secondary topics per conversation (needs the site multi-topic setting on). Response becomes { report, secondary: [{ theme, conversations, avgConfidence }] }.' },
845
846
  },
846
847
  },
847
848
  },
848
849
  {
849
850
  name: 'canned_learning_suggestions',
850
851
  description: 'List AI-proposed canned message improvements awaiting human review (learned from low ratings and never-used messages). Each row shows currentText, proposedText, the reason, the signals it learned from, and a review state (pending_review, approved, rejected, rolled_back). Intelligence Suite entitlement required.',
851
- inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'pending_review (default), approved, rejected, rolled_back, or all' } } },
852
+ inputSchema: { type: 'object', properties: { state: { type: 'string', description: 'pending_review (default), approved, rejected, rolled_back, or all' }, teamId: { type: 'number', description: 'Only suggestions for canned responses on this team. Team managers only ever see their own teams.' } } },
852
853
  },
853
854
  {
854
855
  name: 'canned_learning_generate',
855
- description: 'Analyze this site canned message usage and ratings and queue AI-proposed improvements for human review. Nothing changes until approved. Capped at 5 messages per run and one run per day; text is PII-redacted before it reaches the model.',
856
- inputSchema: { type: 'object', properties: {} },
856
+ description: 'Analyze this site canned message usage and ratings and queue AI-proposed improvements for human review. Nothing changes until approved. Capped at 5 messages per run and one run per day (per team when teamId is given); text is PII-redacted before it reaches the model. Team managers must pass a teamId they manage.',
857
+ inputSchema: { type: 'object', properties: { teamId: { type: 'number', description: 'Only generate for canned responses on this team' } } },
857
858
  },
858
859
  {
859
860
  name: 'canned_learning_approve',
@@ -868,6 +869,11 @@ const TOOLS = [
868
869
  {
869
870
  name: 'canned_learning_edit_summary',
870
871
  description: 'Which canned messages agents edit most before sending (last 90 days): canned response id, shortcut, edit count and average similarity of the sent text to the original. This is the edited-before-send evidence the weekly canned message learning job uses. Intelligence Suite entitlement required.',
872
+ inputSchema: { type: 'object', properties: { teamId: { type: 'number', description: 'Only canned responses on this team' } } },
873
+ },
874
+ {
875
+ name: 'canned_learning_teams',
876
+ description: 'Teams the caller can review canned message learning for: every site team for a site supervisor, only managed teams for a team manager. Use the ids as teamId on the other canned_learning tools.',
871
877
  inputSchema: { type: 'object', properties: {} },
872
878
  },
873
879
  {
@@ -890,6 +896,16 @@ const TOOLS = [
890
896
  description: 'Replace the active topic list (max 60 topics, 60 chars each, duplicates removed). Used for new classifications only; existing history is untouched. Optionally load a template first with industry.',
891
897
  inputSchema: { type: 'object', properties: { topics: { type: 'array', items: { type: 'string' } }, industry: { type: 'string', description: 'Load a template instead: insurance, healthcare, retail, ecommerce, financial, saas, government, manufacturing' } } },
892
898
  },
899
+ {
900
+ name: 'chat_topics_settings_get',
901
+ description: 'Chat Intelligence multi-topic site setting: { themeMultiTopicEnabled (default off), themeReEvalEveryNMessages (1-50, default 5) }. Customer-writable by site supervisors.',
902
+ inputSchema: { type: 'object', properties: {} },
903
+ },
904
+ {
905
+ name: 'chat_topics_settings_set',
906
+ description: 'Turn multi-topic tracking on or off for this site and/or set how many new visitor messages trigger a topic re-check (1-50). Partial and idempotent: omit a field to leave it unchanged. Supervisor only.',
907
+ inputSchema: { type: 'object', properties: { themeMultiTopicEnabled: { type: 'boolean' }, themeReEvalEveryNMessages: { type: 'number' } } },
908
+ },
893
909
  {
894
910
  name: 'chat_topics_edit',
895
911
  description: 'Merge, rename, retire or group topics. Applied when reports are read, so history follows the new names with zero AI cost. op = merge|rename|retire|group; from = topic names; to = target (merge/rename); group = group name (empty clears).',
@@ -1134,10 +1150,25 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1134
1150
  },
1135
1151
  {
1136
1152
  name: 'survey_upsert',
1137
- description: 'Create (omit id or set id=0) or update (set id) a survey. body must be the full SurveyViewModel JSON shape (id, name, questions[], each question with type/text/options/required/variable/etc.). Use survey_get on an existing survey to fetch this full shape as a template rather than guessing the question schema from scratch.',
1138
- inputSchema: {
1139
- type: 'object',
1140
- properties: { body: { type: 'object', description: 'Full SurveyViewModel JSON' } },
1153
+ description: 'Create (omit id or set id=0) or update (set id) a survey. body must be the full SurveyViewModel JSON shape (id, name, questions[], each question with type/text/options/required/variable/etc.). Use survey_get on an existing survey to fetch this full shape as a template rather than guessing the question schema from scratch. Each question also accepts isThemeAnchor (boolean, default false: this answer sets the Chat Intelligence topic) and themeMapping (string: JSON object mapping answer text to a site topic, e.g. "{\\"Pay my bill\\":\\"Billing Questions\\"}"). Both only take effect when the site has the multi-topic setting on.',
1154
+ inputSchema: {
1155
+ type: 'object',
1156
+ properties: { body: {
1157
+ type: 'object',
1158
+ description: 'Full SurveyViewModel JSON',
1159
+ properties: {
1160
+ questions: {
1161
+ type: 'array',
1162
+ items: {
1163
+ type: 'object',
1164
+ properties: {
1165
+ isThemeAnchor: { type: 'boolean', description: 'Use the answer to this question as the Chat Intelligence topic (default false)' },
1166
+ themeMapping: { type: 'string', description: 'JSON object string mapping answer text to a site topic; invalid or empty is stored as null' },
1167
+ },
1168
+ },
1169
+ },
1170
+ },
1171
+ } },
1141
1172
  required: ['body'],
1142
1173
  },
1143
1174
  },
@@ -1557,6 +1588,23 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1557
1588
  },
1558
1589
  },
1559
1590
 
1591
+ // ── Dynamics per-team overrides (effective = team override ?? connection default) ──
1592
+ {
1593
+ name: 'dynamics_team_overrides_get',
1594
+ description: 'List per-team (group) Dynamics overrides of custom field mappings and activity type for a Dynamics config, plus the config\'s own "apply to all groups" defaults. Site is resolved server-side.',
1595
+ inputSchema: { type: 'object', properties: { configId: { type: 'number' } }, required: ['configId'] },
1596
+ },
1597
+ {
1598
+ name: 'dynamics_team_overrides_set',
1599
+ description: 'Create/update one team\'s Dynamics override. Apply-if-present: omitted fields are unchanged; an empty string clears that field back to the default. Fields: activityLogicalName (e.g. phonecall, task), customFieldMappingsJson (JSON array string).',
1600
+ inputSchema: { type: 'object', properties: { configId: { type: 'number' }, teamId: { type: 'number' }, activityLogicalName: { type: 'string' }, customFieldMappingsJson: { type: 'string' } }, required: ['configId', 'teamId'] },
1601
+ },
1602
+ {
1603
+ name: 'dynamics_team_overrides_reset',
1604
+ description: 'Reset a team to the Dynamics default (removes its override).',
1605
+ inputSchema: { type: 'object', properties: { configId: { type: 'number' }, teamId: { type: 'number' } }, required: ['configId', 'teamId'] },
1606
+ },
1607
+
1560
1608
  // ── Teams ─────────────────────────────────────────────────────────────────
1561
1609
  {
1562
1610
  name: 'team_list',
@@ -2155,6 +2203,62 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2155
2203
  required: ['fromIndex', 'toIndex'],
2156
2204
  },
2157
2205
  },
2206
+ {
2207
+ name: 'safety_events_list',
2208
+ description: "List the site's safety events (emergency, emergency_unconfirmed, therapy_setting_change, fail_closed, output_guardrail_block, visitor_report, off_topic_attempt). Needs the EnableTroubleshootingKnowledge entitlement. Wraps GET SafetyEvents; the site comes from the caller auth, never an argument.",
2209
+ inputSchema: {
2210
+ type: 'object',
2211
+ properties: {
2212
+ from: { type: 'string', description: 'ISO start date/time' },
2213
+ to: { type: 'string', description: 'ISO end date/time' },
2214
+ type: { type: 'string', description: 'One event type' },
2215
+ status: { type: 'string', enum: ['open', 'acknowledged'] },
2216
+ take: { type: 'number', description: 'Page size, default 25' },
2217
+ skip: { type: 'number', description: 'Rows to skip, default 0' },
2218
+ },
2219
+ },
2220
+ },
2221
+ {
2222
+ name: 'safety_events_summary',
2223
+ description: 'Per-day and total safety event counts by type for the last N days (default 7). Wraps GET SafetyEvents/summary.',
2224
+ inputSchema: { type: 'object', properties: { days: { type: 'number', description: 'Default 7, e.g. 30' } } },
2225
+ },
2226
+ {
2227
+ name: 'safety_event_ack',
2228
+ description: 'Mark one safety event as reviewed. Wraps POST SafetyEvents/{id}/acknowledge.',
2229
+ inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
2230
+ },
2231
+ {
2232
+ name: 'safety_settings_get',
2233
+ description: 'Get the safety alert settings: enabled, emailRecipients, smsRecipients, immediateTypes, digest (off|daily|weekly), threshold {count, windowMinutes}. Wraps GET SafetyEvents/settings.',
2234
+ inputSchema: { type: 'object', properties: {} },
2235
+ },
2236
+ {
2237
+ name: 'safety_settings_set',
2238
+ description: 'Change the safety alert settings. Only the fields you pass change; the rest keep their current value. Wraps PUT SafetyEvents/settings (reads the current settings first).',
2239
+ inputSchema: {
2240
+ type: 'object',
2241
+ properties: {
2242
+ enabled: { type: 'boolean' },
2243
+ emailRecipients: { type: 'array', items: { type: 'string' } },
2244
+ smsRecipients: { type: 'array', items: { type: 'string' }, description: 'E.164 numbers, e.g. +15551234567' },
2245
+ immediateTypes: { type: 'array', items: { type: 'string' }, description: 'Event types that alert right away' },
2246
+ digest: { type: 'string', enum: ['off', 'daily', 'weekly'] },
2247
+ thresholdCount: { type: 'number', description: 'Alert when this many events happen inside the window. 0 turns it off.' },
2248
+ thresholdWindowMinutes: { type: 'number', description: '1 to 10080' },
2249
+ },
2250
+ },
2251
+ },
2252
+ {
2253
+ name: 'safety_settings_test',
2254
+ description: 'Send a test safety alert to the configured recipients. Wraps POST SafetyEvents/settings/test.',
2255
+ inputSchema: { type: 'object', properties: {} },
2256
+ },
2257
+ {
2258
+ name: 'safety_need_help',
2259
+ description: 'Tell the Velaro team the bot is going off the rails and you need help now. Wraps POST SafetyEvents/need-help.',
2260
+ inputSchema: { type: 'object', properties: { message: { type: 'string', description: 'What is happening' } }, required: ['message'] },
2261
+ },
2158
2262
  {
2159
2263
  name: 'index_move_status',
2160
2264
  description: 'Status of one move (moveId) or the recent moves (omit moveId): status, per-document progress, canResume/canAbort/canUndo. Wraps GET IndexMove/Moves[/{id}].',
@@ -2867,6 +2971,25 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2867
2971
  },
2868
2972
  },
2869
2973
 
2974
+ // ── Chat Window launcher copy (per-page launcher label + greeting) ─────────
2975
+ {
2976
+ name: 'chat_window_list',
2977
+ description: 'List chat windows for this site with the launcher label (launcherText) and greeting. Point a Select Chat Window workflow node or Determine Experience Bundle rule at a window to show its label and greeting on specific pages.',
2978
+ inputSchema: { type: 'object', properties: {}, required: [] },
2979
+ },
2980
+ {
2981
+ name: 'chat_window_set_launcher_copy',
2982
+ description: 'Set the launcher label (the text pill on the chat button, max 60 chars) and/or the opening greeting (max 256 chars) on one chat window. Omitted fields are left unchanged; an empty string clears the field and the widget falls back to its default. For per-page copy, create one chat window per page type (and per language) and select it with a Select Chat Window workflow node.',
2983
+ inputSchema: {
2984
+ type: 'object',
2985
+ properties: {
2986
+ id: { type: 'number', description: 'Chat window ID (see chat_window_list)' },
2987
+ launcherText: { type: 'string', description: 'Launcher label, e.g. "Troubleshoot my device"' },
2988
+ greeting: { type: 'string', description: 'Opening greeting shown when the window opens' },
2989
+ },
2990
+ required: ['id'],
2991
+ },
2992
+ },
2870
2993
  // ── Page Widget Containers ────────────────────────────────────────────────
2871
2994
  {
2872
2995
  name: 'widget_container_list',
@@ -3394,6 +3517,18 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3394
3517
  },
3395
3518
  },
3396
3519
 
3520
+ // -- Image Analysis (analyze_image) limits (superadmin) --
3521
+ {
3522
+ name: 'image_analysis_limits',
3523
+ description: 'Show a customer site Image Analysis (photo analysis) add-on limits: entitlement, monthly cap (MaxImageAnalysesPerMonth), usage this month, per-chat cap and extra-block size. Velaro admin only. To change the cap use support_set_limit with key MaxImageAnalysesPerMonth.',
3524
+ inputSchema: {
3525
+ type: 'object',
3526
+ properties: {
3527
+ siteId: { type: 'number', description: 'Site ID to check' },
3528
+ },
3529
+ required: ['siteId'],
3530
+ },
3531
+ },
3397
3532
  // ── AI Video Avatar management (superadmin) ────────────────────────────────
3398
3533
  {
3399
3534
  name: 'avatar_get_config',
@@ -6318,6 +6453,7 @@ async function handleTool(name, args) {
6318
6453
  qs.set('groupBy', args.groupBy ?? 'month');
6319
6454
  if (args.deploymentId != null) qs.set('deploymentId', String(args.deploymentId));
6320
6455
  if (args.teamId != null) qs.set('teamId', String(args.teamId));
6456
+ if (args.includeSecondary === true) qs.set('includeSecondary', 'true');
6321
6457
  return await messagingGet(`/Reports/chat-intelligence/themes?${qs.toString()}`);
6322
6458
  }
6323
6459
  case 'chat_topics_get':
@@ -6326,6 +6462,13 @@ async function handleTool(name, args) {
6326
6462
  return args.industry
6327
6463
  ? await messagingPost('/ChatIntelligenceTopics/template', { industry: args.industry })
6328
6464
  : await messagingPut('/ChatIntelligenceTopics', { topics: args.topics ?? [] });
6465
+ case 'chat_topics_settings_get':
6466
+ return await messagingGet('/ChatIntelligenceTopics/settings');
6467
+ case 'chat_topics_settings_set':
6468
+ return await messagingPut('/ChatIntelligenceTopics/settings', {
6469
+ themeMultiTopicEnabled: typeof args.themeMultiTopicEnabled === 'boolean' ? args.themeMultiTopicEnabled : null,
6470
+ themeReEvalEveryNMessages: typeof args.themeReEvalEveryNMessages === 'number' ? args.themeReEvalEveryNMessages : null,
6471
+ });
6329
6472
  case 'chat_topics_edit':
6330
6473
  return await messagingPost('/ChatIntelligenceTopics/mapping', { ops: [{ op: args.op, from: args.from ?? [], to: args.to ?? null, group: args.group ?? null }] });
6331
6474
  case 'chat_topics_group_suggestions':
@@ -6343,15 +6486,17 @@ async function handleTool(name, args) {
6343
6486
  if (args.confirm) return await messagingPost('/ChatIntelligenceTopics/analysis', { confirm: true });
6344
6487
  return { estimate: await messagingGet('/ChatIntelligenceTopics/analysis/estimate'), cached: await messagingGet('/ChatIntelligenceTopics/analysis') };
6345
6488
  case 'canned_learning_suggestions':
6346
- return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}`);
6489
+ return await messagingGet(`/CannedLearning/suggestions?state=${encodeURIComponent(args.state ?? 'pending_review')}${args.teamId != null ? `&teamId=${encodeURIComponent(args.teamId)}` : ''}`);
6347
6490
  case 'canned_learning_generate':
6348
- return await messagingPost('/CannedLearning/generate', {});
6491
+ return await messagingPost('/CannedLearning/generate', { teamId: args.teamId ?? null });
6492
+ case 'canned_learning_teams':
6493
+ return await messagingGet('/CannedLearning/teams');
6349
6494
  case 'canned_learning_approve':
6350
6495
  return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/approve`, { editedText: args.editedText ?? null });
6351
6496
  case 'canned_learning_reject':
6352
6497
  return await messagingPost(`/CannedLearning/suggestions/${args.suggestionId}/reject`, {});
6353
6498
  case 'canned_learning_edit_summary':
6354
- return await messagingGet('/CannedLearning/edit-events');
6499
+ return await messagingGet(`/CannedLearning/edit-events${args.teamId != null ? `?teamId=${encodeURIComponent(args.teamId)}` : ''}`);
6355
6500
  case 'canned_learning_record_edit':
6356
6501
  return await messagingPost('/CannedLearning/edit-events', { cannedResponseId: args.cannedResponseId, finalText: args.finalText });
6357
6502
  case 'canned_learning_rollback':
@@ -6911,6 +7056,19 @@ async function handleTool(name, args) {
6911
7056
  return await api('PUT', '/TeamsBot/PushSettings', body);
6912
7057
  }
6913
7058
 
7059
+ case 'dynamics_team_overrides_get':
7060
+ return await api('GET', `/Dynamics/Configs/${Number(args.configId)}/team-overrides`);
7061
+
7062
+ case 'dynamics_team_overrides_set': {
7063
+ const body = {};
7064
+ for (const k of ['activityLogicalName', 'customFieldMappingsJson'])
7065
+ if (args[k] !== undefined) body[k] = args[k];
7066
+ return await api('PUT', `/Dynamics/Configs/${Number(args.configId)}/team-overrides/${Number(args.teamId)}`, body);
7067
+ }
7068
+
7069
+ case 'dynamics_team_overrides_reset':
7070
+ return await api('DELETE', `/Dynamics/Configs/${Number(args.configId)}/team-overrides/${Number(args.teamId)}`);
7071
+
6914
7072
  case 'teams_channel_config_get':
6915
7073
  return await api('GET', '/TeamsBot/ChannelConfig');
6916
7074
 
@@ -7769,6 +7927,35 @@ async function handleTool(name, args) {
7769
7927
  const r = await messagingPost('/IndexMove/Split/Execute', body);
7770
7928
  return JSON.stringify(r, null, 2);
7771
7929
  }
7930
+ case 'safety_events_list': {
7931
+ const q = new URLSearchParams();
7932
+ for (const k of ['from', 'to', 'type', 'status']) if (args[k]) q.set(k, String(args[k]));
7933
+ q.set('take', String(args.take ?? 25));
7934
+ q.set('skip', String(args.skip ?? 0));
7935
+ return JSON.stringify(await messagingGet(`/SafetyEvents?${q}`), null, 2);
7936
+ }
7937
+ case 'safety_events_summary':
7938
+ return JSON.stringify(await messagingGet(`/SafetyEvents/summary?days=${Number(args.days) || 7}`), null, 2);
7939
+ case 'safety_event_ack':
7940
+ return JSON.stringify(await messagingPost(`/SafetyEvents/${encodeURIComponent(args.id)}/acknowledge`), null, 2);
7941
+ case 'safety_settings_get':
7942
+ return JSON.stringify(await messagingGet('/SafetyEvents/settings'), null, 2);
7943
+ case 'safety_settings_set': {
7944
+ const cur = (await messagingGet('/SafetyEvents/settings')) ?? {};
7945
+ const next = { ...cur };
7946
+ for (const k of ['enabled', 'emailRecipients', 'smsRecipients', 'immediateTypes', 'digest']) if (args[k] !== undefined) next[k] = args[k];
7947
+ if (args.thresholdCount !== undefined || args.thresholdWindowMinutes !== undefined) {
7948
+ next.threshold = {
7949
+ count: args.thresholdCount ?? cur.threshold?.count ?? 0,
7950
+ windowMinutes: args.thresholdWindowMinutes ?? cur.threshold?.windowMinutes ?? 60,
7951
+ };
7952
+ }
7953
+ return JSON.stringify(await messagingPut('/SafetyEvents/settings', next), null, 2);
7954
+ }
7955
+ case 'safety_settings_test':
7956
+ return JSON.stringify(await messagingPost('/SafetyEvents/settings/test'), null, 2);
7957
+ case 'safety_need_help':
7958
+ return JSON.stringify(await messagingPost('/SafetyEvents/need-help', { message: args.message }), null, 2);
7772
7959
  case 'index_move_status': {
7773
7960
  const r = await messagingGet(args.moveId
7774
7961
  ? `/IndexMove/Moves/${encodeURIComponent(args.moveId)}`
@@ -8723,6 +8910,23 @@ async function handleTool(name, args) {
8723
8910
  ).join('\n');
8724
8911
  }
8725
8912
 
8913
+ case 'chat_window_list': {
8914
+ const r = (await messagingGet('/ChatWindow/list')) ?? [];
8915
+ return JSON.stringify(r.map((w) => ({
8916
+ id: w.id, name: w.name, language: w.languageConfig?.language,
8917
+ launcherText: w.styleConfig?.launcherText ?? null, greeting: w.languageConfig?.greeting ?? null,
8918
+ })), null, 2);
8919
+ }
8920
+
8921
+ case 'chat_window_set_launcher_copy': {
8922
+ if (!args.id) throw new Error('id is required');
8923
+ const body = {};
8924
+ if (args.launcherText !== undefined) body.launcherText = args.launcherText;
8925
+ if (args.greeting !== undefined) body.greeting = args.greeting;
8926
+ if (!Object.keys(body).length) throw new Error('provide launcherText and/or greeting');
8927
+ return JSON.stringify(await messagingPut(`/ChatWindow/${args.id}/launcher-copy`, body), null, 2);
8928
+ }
8929
+
8726
8930
  case 'widget_container_get': {
8727
8931
  const c = await api('GET', `/PageWidgetContainers/${args.id}`);
8728
8932
  return JSON.stringify(c, null, 2);
@@ -9147,6 +9351,18 @@ async function handleTool(name, args) {
9147
9351
  }
9148
9352
 
9149
9353
  // ── AI Video Avatar ────────────────────────────────────────────────────
9354
+ // -- Image Analysis limits --
9355
+ case 'image_analysis_limits': {
9356
+ const d = await api('GET', `/ImageAnalysisLimits/superadmin?siteId=${args.siteId}`);
9357
+ return [
9358
+ `Site ${args.siteId}: Image Analysis ${d.entitled === null ? 'entitlement unknown' : d.entitled ? 'ENTITLED' : 'NOT entitled'}`,
9359
+ ` Monthly cap: ${d.monthlyCap} (effective ${d.effectiveMonthlyCap}, hard cap, no automatic overage)`,
9360
+ ` Used this month: ${d.usedThisMonth === null ? 'unknown' : d.usedThisMonth}`,
9361
+ ` Per-chat cap: ${d.perConversationCap}`,
9362
+ ` Extra block size: ${d.blockSize}`,
9363
+ ].join('\n');
9364
+ }
9365
+
9150
9366
  case 'avatar_get_config': {
9151
9367
  const data = await api('GET', `/Avatar/superadmin/status?siteId=${args.siteId}`);
9152
9368
  if (!data.configured) return `Site ${args.siteId}: AI Video Avatar NOT configured (no saved config).`;