@velaro/mcp-server 0.6.104 → 1.0.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.104",
3
+ "version": "1.0.1",
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": {
@@ -45,6 +45,7 @@
45
45
  "tool-annotations.js",
46
46
  "log-search.js",
47
47
  "messaging-base.js",
48
+ "workflow-schedule.js",
48
49
  "server.json",
49
50
  "README.md"
50
51
  ],
package/server.js CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  } from '@modelcontextprotocol/sdk/types.js';
52
52
  import { withAnnotations } from './tool-annotations.js';
53
53
  import { resolveMessagingApi } from './messaging-base.js';
54
+ import { buildResolvePath, buildTimelinePath, buildOverrideBody, buildValidateBody, buildBannerPath, buildOverrideListPath, tolerant, VALIDATE_PATH, OVERRIDE_PATH } from './workflow-schedule.js';
54
55
 
55
56
  // Default was 'https://help.velaro.com'. Renamed to the recognizable admin API hostname for
56
57
  // clarity during the 2026-09-10 Donaldson bot Calendly status investigation
@@ -156,6 +157,25 @@ function extractLogEntries(response) {
156
157
  return [];
157
158
  }
158
159
 
160
+ /** Body for POST/PUT ApplePayPresetOrders: only passed fields; the server computes the total. */
161
+ function applePayPresetBody(args) {
162
+ const body = {};
163
+ for (const k of ['name', 'description', 'currencyCode', 'isActive', 'overrideRestrictedToManagers']) {
164
+ if (args[k] !== undefined) body[k] = args[k];
165
+ }
166
+ if (args.lineItems !== undefined) {
167
+ if (!Array.isArray(args.lineItems) || args.lineItems.length > 20) throw new Error('lineItems must be an array of at most 20 items');
168
+ body.lineItems = args.lineItems.map(li => ({ label: String(li?.label ?? ''), amount: Number(li?.amount) }));
169
+ }
170
+ return body;
171
+ }
172
+
173
+ function applePayPresetId(raw) {
174
+ const n = Number(raw);
175
+ if (!Number.isInteger(n) || n <= 0) throw new Error('id must be a positive whole number');
176
+ return n;
177
+ }
178
+
159
179
  /**
160
180
  * Body for PUT avatar/groups|workflows/{id}/override. Merge by default (only passed fields change, "" clears one);
161
181
  * `replace: true` replaces the whole override. Mirrors cli/lib/commands/avatar.js.
@@ -212,6 +232,65 @@ function execInsightsRange(days) {
212
232
 
213
233
  const messagingDel = (path) => messagingRequest('DELETE', path);
214
234
 
235
+ // Visitor link helpers (mirrors cli/lib/visitor-link.js; keep in sync, covered by cli/lib/visitor-link.test.js)
236
+
237
+ const MAX_TTL_MINUTES = 1440;
238
+
239
+ function decodeLinkToken(token) {
240
+ const out = { ok: false, problems: [] };
241
+ if (typeof token !== 'string' || !token.trim()) { out.problems.push('empty token'); return out; }
242
+ const parts = token.trim().split('.');
243
+ if (parts.length !== 3 || parts[0] !== 'vlk1') { out.problems.push('not a vlk1 link token (expected vlk1.<payload>.<signature>)'); return out; }
244
+ const payload = Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8');
245
+ const f = payload.split('|');
246
+ if (f.length < 6) { out.problems.push('payload has fewer than 6 fields'); return out; }
247
+ const [siteId, visitorId, conversationBaseId, scope, iat, exp] = f;
248
+ const toMs = (v) => { const n = Number(v); return Number.isFinite(n) && n > 0 ? (n > 1e12 ? n : n * 1000) : null; };
249
+ const issuedMs = toMs(iat);
250
+ const expMs = toMs(exp);
251
+ Object.assign(out, {
252
+ ok: true,
253
+ siteId: Number(siteId) || null,
254
+ visitorId,
255
+ conversationBaseId: Number(conversationBaseId) || null,
256
+ scope,
257
+ issuedAt: issuedMs ? new Date(issuedMs).toISOString() : null,
258
+ expiresAt: expMs ? new Date(expMs).toISOString() : null,
259
+ signaturePresent: parts[2].length > 0,
260
+ });
261
+ if (scope !== 'link') out.problems.push(`unexpected scope "${scope}" (v1 only issues "link")`);
262
+ if (!out.signaturePresent) out.problems.push('signature missing');
263
+ if (!expMs) out.problems.push('expiry not parseable');
264
+ return out;
265
+ }
266
+
267
+ function diagnoseLink({ token, siteId, settings, now = Date.now() }) {
268
+ const claims = token ? decodeLinkToken(token) : { ok: false, problems: [] };
269
+ const findings = [...claims.problems];
270
+ if (claims.ok) {
271
+ if (siteId && claims.siteId && Number(siteId) !== claims.siteId) findings.push(`token was minted for site ${claims.siteId}, not site ${siteId}: cross-site tokens always fail`);
272
+ if (claims.expiresAt && Date.parse(claims.expiresAt) <= now) findings.push(`token expired at ${claims.expiresAt}`);
273
+ if (!claims.conversationBaseId) findings.push('token carries no conversation id (visitor had not started a chat when it was minted)');
274
+ }
275
+ if (settings) {
276
+ if (!settings.enabled) findings.push('visitor linking is OFF for this site: mint returns linking_disabled');
277
+ if (settings.keyConfigured === false) findings.push('link signing key is not configured on the server: mint and resolve fail closed (503 key_not_configured)');
278
+ }
279
+ return { claims, settings: settings ?? null, findings, healthy: findings.length === 0 };
280
+ }
281
+
282
+ function validateSettingsArgs({ enabled, ttlMinutes }) {
283
+ if (enabled === undefined && ttlMinutes === undefined) throw new Error('Pass enabled and/or ttlMinutes.');
284
+ if (ttlMinutes !== undefined && (!Number.isInteger(ttlMinutes) || ttlMinutes < 1 || ttlMinutes > MAX_TTL_MINUTES)) {
285
+ throw new Error(`ttlMinutes must be a whole number from 1 to ${MAX_TTL_MINUTES}.`);
286
+ }
287
+ const body = {};
288
+ if (enabled !== undefined) body.enabled = !!enabled;
289
+ if (ttlMinutes !== undefined) body.ttlMinutes = ttlMinutes;
290
+ return body;
291
+ }
292
+
293
+
215
294
  // Skill-based routing CRUD helper for SkillRouting/{Skills|Assignments|Requirements}.
216
295
  async function skillRoutingCrud(resource, args, fields) {
217
296
  const body = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
@@ -1114,6 +1193,66 @@ const TOOLS = [
1114
1193
  required: ['name', 'trigger'],
1115
1194
  },
1116
1195
  },
1196
+ {
1197
+ name: 'workflow_schedule_resolve',
1198
+ description: 'Which workflow runs at a given date/time? Returns { mode, first (winner), gap, winnerOverrideId, active (ordered workflows with priority), trace } (workflow schedule priority; schedule-only preview, a workflow own trigger conditions are not evaluated). Site comes from auth. Answers "why did workflow X (not) fire at 6 PM Dec 25". Read-only.',
1199
+ inputSchema: { type: 'object', properties: {
1200
+ trigger: { type: 'string', description: 'Trigger type, e.g. "conversation_opened"' },
1201
+ channel: { type: 'string', description: 'Channel, e.g. "Chat", "Sms"' },
1202
+ at: { type: 'string', description: 'ISO date-time to evaluate (default now)' },
1203
+ tz: { type: 'string', description: 'IANA time zone, e.g. "America/Los_Angeles"' },
1204
+ }, required: ['trigger'] },
1205
+ },
1206
+ {
1207
+ name: 'workflow_schedule_timeline',
1208
+ description: 'Timeline of which workflow wins over a date range for a trigger/channel: returns { segments: [{ fromUtc, toUtc, workflowId, workflowName, gap }], gaps (count) }. Read-only.',
1209
+ inputSchema: { type: 'object', properties: {
1210
+ from: { type: 'string', description: 'ISO start with offset (default now)' },
1211
+ to: { type: 'string', description: 'ISO end with offset (default from + 14 days; range max 62 days)' },
1212
+ trigger: { type: 'string' },
1213
+ channel: { type: 'string' },
1214
+ }, required: ['trigger'] },
1215
+ },
1216
+ {
1217
+ name: 'workflow_schedule_validate',
1218
+ description: 'Dry-run one draft schedule override without saving: what would the next 400 days look like. Returns { ok, errors, warnings, gapCount }; same-priority overlap and a bad target block, gaps warn. Omit override to just list current gaps. Read-only.',
1219
+ inputSchema: { type: 'object', properties: {
1220
+ trigger: { type: 'string' },
1221
+ channel: { type: 'string' },
1222
+ override: { type: 'object', description: 'Draft override: { sharedScheduleId, name, startDate, endDate, action ("SwitchWorkflow" or 3), targetWorkflowId, recurringYearly, autoExpiresAt, isActive }' },
1223
+ }, required: ['trigger'] },
1224
+ },
1225
+ {
1226
+ name: 'workflow_schedule_banner',
1227
+ description: 'The first gap (no workflow scheduled to run) in the next 7 days for a trigger/channel: { hasGap, fromUtc, toUtc, message }. Read-only.',
1228
+ inputSchema: { type: 'object', properties: { trigger: { type: 'string' }, channel: { type: 'string' } } },
1229
+ },
1230
+ {
1231
+ name: 'workflow_schedule_override_list',
1232
+ description: 'List this site\'s schedule overrides, optionally only one action ("SwitchWorkflow" or 3). Read-only.',
1233
+ inputSchema: { type: 'object', properties: { action: { description: '"SwitchWorkflow" or an int action value' } } },
1234
+ },
1235
+ {
1236
+ name: 'workflow_schedule_override_delete',
1237
+ description: 'Delete a schedule override by id (site admins only). Writes: confirm with the user first.',
1238
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] },
1239
+ },
1240
+ {
1241
+ name: 'workflow_schedule_override_save',
1242
+ description: 'Create (omit id) or update (id) a schedule override. action accepts "SwitchWorkflow" (3) with targetWorkflowId to run a different workflow on those dates, alongside the existing actions. Writes: confirm the dates and target with the user first.',
1243
+ inputSchema: { type: 'object', properties: {
1244
+ id: { type: 'number', description: 'Existing override id to update; omit to create' },
1245
+ sharedScheduleId: { type: 'number' },
1246
+ name: { type: 'string' },
1247
+ startDate: { type: 'string', description: 'YYYY-MM-DD' },
1248
+ endDate: { type: 'string', description: 'YYYY-MM-DD, optional' },
1249
+ recurringYearly: { type: 'boolean' },
1250
+ autoExpiresAt: { type: 'string', description: 'ISO date-time with offset, optional' },
1251
+ isActive: { type: 'boolean', description: 'Default true' },
1252
+ action: { description: '"SwitchWorkflow" or its int value 3 (or an existing action value)' },
1253
+ targetWorkflowId: { type: 'number', description: 'Workflow to run when action is SwitchWorkflow' },
1254
+ }, required: ['action'] },
1255
+ },
1117
1256
  {
1118
1257
  name: 'workflow_update_node',
1119
1258
  description: `Update a single node in a workflow. Use workflow_get first to read current nodes, then pass the full updated node. The node must already exist.
@@ -1681,6 +1820,41 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1681
1820
  required: ['message'],
1682
1821
  },
1683
1822
  },
1823
+ {
1824
+ name: 'visitor_link_get_settings',
1825
+ description: 'Get the visitor-linking settings for this site: whether chat visitors can be linked to later form/survey submissions (default off), the link lifetime in minutes, and whether the signing key is configured. Site is resolved server-side.',
1826
+ inputSchema: { type: 'object', properties: {}, required: [] },
1827
+ },
1828
+ {
1829
+ name: 'visitor_link_set_settings',
1830
+ description: 'Turn visitor linking on or off for this site and/or set the link lifetime (1-1440 minutes). Only fields you pass change. Links are only created for visitors who have chatted and, when the site uses a consent banner, granted functional storage.',
1831
+ inputSchema: {
1832
+ type: 'object',
1833
+ properties: {
1834
+ enabled: { type: 'boolean', description: 'Enable or disable visitor linking' },
1835
+ ttlMinutes: { type: 'number', description: 'Link lifetime in minutes, 1-1440' },
1836
+ },
1837
+ required: [],
1838
+ },
1839
+ },
1840
+ {
1841
+ name: 'visitor_link_diagnose',
1842
+ description: 'Explain why visitor linking is or is not working for this site. Reads the current settings and, if you pass a vlk1 link token, decodes its claims (site, conversation, expiry) and reports cross-site, expired, missing-conversation and key problems. The signature is not verified here; use visitor_link_resolve for the real server-side check.',
1843
+ inputSchema: {
1844
+ type: 'object',
1845
+ properties: { token: { type: 'string', description: 'Optional vlk1 link token from a form hidden field' } },
1846
+ required: [],
1847
+ },
1848
+ },
1849
+ {
1850
+ name: 'visitor_link_resolve',
1851
+ description: 'Exchange a vlk1 link token for the visitor id, conversation id and contact id (what your own server does when a form arrives). Cross-site and expired tokens fail.',
1852
+ inputSchema: {
1853
+ type: 'object',
1854
+ properties: { token: { type: 'string', description: 'vlk1 link token' } },
1855
+ required: ['token'],
1856
+ },
1857
+ },
1684
1858
  {
1685
1859
  name: 'teams_push_settings_get',
1686
1860
  description: 'Get the Velaro Bot proactive-alert settings for this site: per-alert switches (new chat, missed chat, ticket created, ticket assigned, daily digest), the effective daily cap and the push count for today. Site is resolved server-side.',
@@ -3464,7 +3638,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3464
3638
  inputSchema: {
3465
3639
  type: 'object',
3466
3640
  properties: {
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'] },
3641
+ 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', 'bonus_insights', 'operations_briefing', 'entitled'] },
3468
3642
  from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
3469
3643
  to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
3470
3644
  groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
@@ -3481,6 +3655,13 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3481
3655
  timeZone: { type: 'string', description: 'demand_coverage: IANA time zone (default America/Los_Angeles).' },
3482
3656
  lowScore: { type: 'number', description: 'bot_containment: QA score below which a handed-off chat counts as regret (1-100, default 60).' },
3483
3657
  includeDemo: { type: 'boolean', description: 'true = show only the seeded sample (demo) data. Default false.' },
3658
+ name: { type: 'string', description: 'bonus_insights: which analysis: coverage_gap|next_best_fix|escalation_risk|skill_gap_matrix|handle_quality_outliers|abandon_causes.' },
3659
+ narrative: { type: 'boolean', description: 'operations_briefing: include the cached AI narrative (default true).' },
3660
+ minutesPerReview: { type: 'number', description: 'bonus_insights: assumed minutes per manual review (0-240).' },
3661
+ hourlyCost: { type: 'number', description: 'bonus_insights: assumed reviewer hourly cost (0-1000).' },
3662
+ deflectionPct: { type: 'number', description: 'bonus_insights: assumed deflection percent (0-100).' },
3663
+ costPerChat: { type: 'number', description: 'bonus_insights: assumed cost per chat (0-1000).' },
3664
+ waitMinutes: { type: 'number', description: 'bonus_insights: wait minutes that counts as abandoned (1-1440).' },
3484
3665
  promiseId: { type: 'string', description: 'promise_dismiss: the promise id to dismiss (managers only).' },
3485
3666
  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.' },
3486
3667
  },
@@ -3706,6 +3887,51 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3706
3887
  },
3707
3888
  },
3708
3889
 
3890
+ // ── Apple Pay pre-canned (preset) orders ───────────────────────────────────
3891
+ {
3892
+ name: 'apple_pay_preset_list',
3893
+ description: 'List the Apple Pay pre-canned (preset) orders for the authenticated site: name, line items (label + amount), currency, server-computed total, active flag, and whether only managers may change amounts at send time. Use the id or name with the AI skill (preset_order_id / preset_order_name) or the composer.',
3894
+ inputSchema: { type: 'object', properties: { activeOnly: { type: 'boolean', description: 'Only return active presets (default false).' } } },
3895
+ },
3896
+ {
3897
+ name: 'apple_pay_preset_create',
3898
+ description: 'Create an Apple Pay pre-canned order (up to 20 line items). The total is computed by the server from the line items; never send one.',
3899
+ inputSchema: {
3900
+ type: 'object',
3901
+ properties: {
3902
+ name: { type: 'string', description: 'Preset name, e.g. "Haircut and tip".' },
3903
+ description: { type: 'string', description: 'Optional description.' },
3904
+ currencyCode: { type: 'string', description: 'ISO currency code, default USD.' },
3905
+ lineItems: { type: 'array', maxItems: 20, description: 'Line items.', items: { type: 'object', properties: { label: { type: 'string' }, amount: { type: 'number' } }, required: ['label', 'amount'] } },
3906
+ isActive: { type: 'boolean', description: 'Available to agents and the AI (default true).' },
3907
+ overrideRestrictedToManagers: { type: 'boolean', description: 'Only managers can change amounts at send time.' },
3908
+ },
3909
+ required: ['name', 'lineItems'],
3910
+ },
3911
+ },
3912
+ {
3913
+ name: 'apple_pay_preset_update',
3914
+ description: 'Update an Apple Pay preset order. Only the fields you pass change; passing lineItems replaces the whole list.',
3915
+ inputSchema: {
3916
+ type: 'object',
3917
+ properties: {
3918
+ id: { type: 'number', description: 'Preset order id (from apple_pay_preset_list).' },
3919
+ name: { type: 'string' },
3920
+ description: { type: 'string' },
3921
+ currencyCode: { type: 'string' },
3922
+ lineItems: { type: 'array', maxItems: 20, items: { type: 'object', properties: { label: { type: 'string' }, amount: { type: 'number' } }, required: ['label', 'amount'] } },
3923
+ isActive: { type: 'boolean' },
3924
+ overrideRestrictedToManagers: { type: 'boolean' },
3925
+ },
3926
+ required: ['id'],
3927
+ },
3928
+ },
3929
+ {
3930
+ name: 'apple_pay_preset_delete',
3931
+ description: 'Delete an Apple Pay preset order by id.',
3932
+ inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Preset order id.' } }, required: ['id'] },
3933
+ },
3934
+
3709
3935
  // ── Velaro Avatar (first-party provider) — library, personalities, site/group inheritance ──
3710
3936
  {
3711
3937
  name: 'avatar_list_library',
@@ -4581,6 +4807,15 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4581
4807
  },
4582
4808
  },
4583
4809
 
4810
+ {
4811
+ name: 'netsuite_rollback_auth',
4812
+ description: 'Undo a NetSuite connection-type switch: sets a connection that was moved to OAuth 2.0 / M2M back to the legacy OAuth 1.0a credentials. Refused (409) once the old credentials were cleared. Nothing is deleted. Administrator only, audited. Lives in velaro-messaging - requires VELARO_MESSAGING_API.',
4813
+ inputSchema: {
4814
+ type: 'object',
4815
+ properties: { configId: { type: 'number', description: 'NetSuite config ID (see netsuite_list_configs).' } },
4816
+ required: ['configId'],
4817
+ },
4818
+ },
4584
4819
  {
4585
4820
  name: 'netsuite_suiteapp_download',
4586
4821
  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.',
@@ -4888,6 +5123,21 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
4888
5123
  // Distinct from the reusable coupon-CODE catalog, which lives in velaro-messaging
4889
5124
  // (superadmin/billing/coupons). Pass couponCode here only to record which catalog code a grant
4890
5125
  // corresponds to.
5126
+ {
5127
+ name: 'sign_versions_list',
5128
+ description: 'Velaro Sign: list every uploaded document version of a contract (owner-only; signers never see old versions). Newest first with sha256, pages, uploader, note.',
5129
+ inputSchema: { type: 'object', properties: { contractId: { type: 'number', description: 'Contract ID' } }, required: ['contractId'] },
5130
+ },
5131
+ {
5132
+ name: 'sign_versions_diff',
5133
+ description: 'Velaro Sign: text diff between two document versions of a contract (changed blocks only: removed vs added lines). Owner-only.',
5134
+ inputSchema: { type: 'object', properties: { contractId: { type: 'number' }, from: { type: 'number', description: 'Older version number' }, to: { type: 'number', description: 'Newer version number' } }, required: ['contractId', 'from', 'to'] },
5135
+ },
5136
+ {
5137
+ name: 'sign_versions_upload',
5138
+ description: 'Velaro Sign: upload a new PDF version of a contract document from a local file path. Allowed only while the contract is a draft; after send/sign, void and re-send.',
5139
+ inputSchema: { type: 'object', properties: { contractId: { type: 'number' }, filePath: { type: 'string', description: 'Local path of the PDF' }, note: { type: 'string', description: 'Change note' } }, required: ['contractId', 'filePath'] },
5140
+ },
4891
5141
  {
4892
5142
  name: 'coupon_grant_list',
4893
5143
  description: 'List per-account Stripe discount grants. Velaro billing staff only. Shows which accounts currently have a discount and its status.',
@@ -5013,6 +5263,51 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5013
5263
  required: ['id'],
5014
5264
  },
5015
5265
  },
5266
+ {
5267
+ name: 'usage_alerts',
5268
+ description: 'Plan-limit alerts for the authenticated site: every metered resource at or above its warning threshold (80%+), with used/limit, percent, level (warning | critical | limit) and whether overage is billed. Same rules as the usage-limit emails and the dashboard banner. Use to answer "are they near a limit?" or "why did they get a usage email?". Read-only. To raise a limit, a customer uses Billing > Contact Account Manager; staff use support_set_limit.',
5269
+ },
5270
+ {
5271
+ name: 'billing_notify_settings_get',
5272
+ description: 'Billing notifications: the global settings (receipts.card, receipts.ach, receipts.other, invoices.bcc, digest.recipients). Staff only.',
5273
+ inputSchema: { type: 'object', properties: {} },
5274
+ },
5275
+ {
5276
+ name: 'billing_notify_setting_set',
5277
+ description: 'Billing notifications: set one global setting. receipts.card|receipts.ach|receipts.other = never|send; invoices.bcc = comma-separated emails (blank = none); digest.recipients = comma-separated emails. Staff only.',
5278
+ inputSchema: {
5279
+ type: 'object',
5280
+ properties: {
5281
+ key: { type: 'string', enum: ['receipts.card', 'receipts.ach', 'receipts.other', 'invoices.bcc', 'digest.recipients'], description: 'Setting key' },
5282
+ value: { type: 'string', description: 'New value' },
5283
+ },
5284
+ required: ['key', 'value'],
5285
+ },
5286
+ },
5287
+ {
5288
+ name: 'billing_receipt_overrides_set',
5289
+ description: 'Billing notifications: set receipt overrides on one subscription (never|send per channel). Channels you omit keep their current override. Staff only.',
5290
+ inputSchema: {
5291
+ type: 'object',
5292
+ properties: {
5293
+ subscriptionId: { type: 'number', description: 'Billing subscription ID' },
5294
+ card: { type: 'string', enum: ['never', 'send'], description: 'Card receipts override' },
5295
+ ach: { type: 'string', enum: ['never', 'send'], description: 'ACH receipts override' },
5296
+ other: { type: 'string', enum: ['never', 'send'], description: 'Other (check, cash, wire) receipts override' },
5297
+ },
5298
+ required: ['subscriptionId'],
5299
+ },
5300
+ },
5301
+ {
5302
+ name: 'billing_digest_send_now',
5303
+ description: 'Billing notifications: send the daily billing digest now to digest.recipients (same email the 7am run sends). Staff only.',
5304
+ inputSchema: { type: 'object', properties: {} },
5305
+ },
5306
+ {
5307
+ name: 'billing_receipts_run_now',
5308
+ description: 'Billing notifications: decide pending payment receipts now instead of at :05. Returns the counts (sent, skipped, failed). Staff only.',
5309
+ inputSchema: { type: 'object', properties: {} },
5310
+ },
5016
5311
  {
5017
5312
  name: 'billing_plan_status',
5018
5313
  description: 'Returns the current subscription state for the authenticated site: active Enable* feature flags, licensed seat count, self-serve seats toggle, and recent Stripe payment session history. Use this to answer "what plan are they on?" or "which features are active?"',
@@ -5122,6 +5417,43 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5122
5417
  required: ['siteId', 'featureKey', 'value'],
5123
5418
  },
5124
5419
  },
5420
+ {
5421
+ name: 'entitlement_trial_get',
5422
+ description: 'Trial governance view for one site: trial status, days left, and the grant ledger (active/held/purchased/revoked). Superadmin only.',
5423
+ inputSchema: { type: 'object', properties: { siteId: { type: 'number', description: 'Site ID.' } }, required: ['siteId'] },
5424
+ },
5425
+ {
5426
+ name: 'entitlement_trial_recommend',
5427
+ description: 'Deterministic package recommendation for a trial site, from its recorded trial usage. Returns customer-safe text plus a staff-only rationale. Superadmin only.',
5428
+ inputSchema: { type: 'object', properties: { siteId: { type: 'number', description: 'Site ID.' } }, required: ['siteId'] },
5429
+ },
5430
+ {
5431
+ name: 'entitlement_trial_grant',
5432
+ description: 'Grant one feature to a trial site and record it in the trial ledger (who, when, why). Superadmin only; the site must be in a trial.',
5433
+ inputSchema: {
5434
+ type: 'object',
5435
+ properties: {
5436
+ siteId: { type: 'number', description: 'Site ID.' },
5437
+ featureKey: { type: 'string', description: 'Feature key, e.g. "EnableIvr".' },
5438
+ reason: { type: 'string', description: 'Why this feature is granted (required, recorded).' },
5439
+ },
5440
+ required: ['siteId', 'featureKey', 'reason'],
5441
+ },
5442
+ },
5443
+ {
5444
+ name: 'entitlement_trial_set_limit',
5445
+ description: 'Set a per-site trial cap (MaxIndexedChunkTotal, MaxKbArticles, MaxKnowledgeDocuments, MaxScraperPagesPerMonth). Whole number >= 0; audited. Superadmin only.',
5446
+ inputSchema: {
5447
+ type: 'object',
5448
+ properties: {
5449
+ siteId: { type: 'number', description: 'Site ID.' },
5450
+ key: { type: 'string', description: 'Cap name.' },
5451
+ value: { type: 'number', description: 'Whole number >= 0.' },
5452
+ reason: { type: 'string', description: 'Why (required, audited).' },
5453
+ },
5454
+ required: ['siteId', 'key', 'value', 'reason'],
5455
+ },
5456
+ },
5125
5457
  {
5126
5458
  name: 'entitlement_remove_override',
5127
5459
  description: 'Remove a per-site feature override, reverting the site to its plan default. Superadmin only.',
@@ -6197,7 +6529,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
6197
6529
  {"name":"skill_routing_assignments","description":"Skill-based routing: assign skills to agents with proficiency 1-5. action = list | create | update | delete.","inputSchema":{"type":"object","properties":{"action":{"type":"string"},"id":{"type":"number"},"workspaceUserId":{"type":"number"},"agentSkillId":{"type":"number"},"proficiency":{"type":"number"}},"required":["action"]}},
6198
6530
  {"name":"skill_routing_requirements","description":"Skill-based routing: required/preferred skills per deployment and/or team. action = list | create | update | delete. Update is partial. Requirements are disabled by default; set isEnabled true to turn on.","inputSchema":{"type":"object","properties":{"action":{"type":"string"},"id":{"type":"number"},"deploymentId":{"type":"number"},"teamId":{"type":"number"},"requiredSkillIds":{"type":"array","items":{"type":"number"}},"preferredSkillIds":{"type":"array","items":{"type":"number"}},"overflowWaitSeconds":{"type":"number"},"isEnabled":{"type":"boolean"}},"required":["action"]}},
6199
6531
  {"name":"skill_routing_simulate","description":"Dry-run skill routing for a team: shows which agents would be narrowed to and why. Never assigns a conversation or writes the decision log.","inputSchema":{"type":"object","properties":{"teamId":{"type":"number"},"waitedSeconds":{"type":"number"}}}},
6200
- {"name":"qa_rubric","description":"Conversation Quality QA rubrics. action = list | get | default | create | update | delete. Pass rubric fields in \"rubric\". Requires EnableConversationQuality.","inputSchema":{"type":"object","properties":{"action":{"type":"string"},"id":{"type":"number"},"rubric":{"type":"object"}},"required":["action"]}},
6532
+ {"name":"qa_rubric","description":"Conversation Quality QA rubrics. action = list | get | default | starter (create the starter rubric if the site has none) | create | update | clone (inactive copy; optional rubric.name) | activate | deactivate | rescore (re-score recent chats against the active rubric; pass days 1-90 and limit 1-500) | delete. Pass rubric fields in \"rubric\". Weights are relative and shown as percents; max 20 criteria. Requires EnableConversationQuality and the Administrator role for writes.","inputSchema":{"type":"object","properties":{"action":{"type":"string"},"id":{"type":"number"},"rubric":{"type":"object"},"days":{"type":"number"},"limit":{"type":"number"}},"required":["action"]}},
6201
6533
  {"name":"qa_low_scores","description":"Conversations scoring below the rubric threshold, for QA review.","inputSchema":{"type":"object","properties":{"threshold":{"type":"number"},"days":{"type":"number"},"agentId":{"type":"number"},"skip":{"type":"number"},"take":{"type":"number"}}}},
6202
6534
  {"name":"ai_field_extraction_get_config","description":"Get the AI field extraction config (opt-in, default off; monthly cap). Requires the ai_field_extraction entitlement.","inputSchema":{"type":"object","properties":{}}},
6203
6535
  {"name":"ai_field_extraction_update_config","description":"Update AI field extraction config (partial). enabled, monthlyCap (null = 500 default, hard ceiling 50000), createThemeTags.","inputSchema":{"type":"object","properties":{"enabled":{"type":"boolean"},"monthlyCap":{"type":"number"},"createThemeTags":{"type":"boolean"}}}},
@@ -6838,6 +7170,29 @@ async function handleTool(name, args) {
6838
7170
  return await rpc('conversation_search', args);
6839
7171
 
6840
7172
  // ── Workflows ──────────────────────────────────────────────────────────
7173
+ case 'workflow_schedule_resolve':
7174
+ return await tolerant(() => messagingGet(buildResolvePath(args)));
7175
+
7176
+ case 'workflow_schedule_timeline':
7177
+ return await tolerant(() => messagingGet(buildTimelinePath(args)));
7178
+
7179
+ case 'workflow_schedule_validate':
7180
+ return await tolerant(() => messagingPost(VALIDATE_PATH, buildValidateBody(args)));
7181
+
7182
+ case 'workflow_schedule_banner':
7183
+ return await tolerant(() => messagingGet(buildBannerPath(args)));
7184
+
7185
+ case 'workflow_schedule_override_list':
7186
+ return await tolerant(() => messagingGet(buildOverrideListPath(args)));
7187
+
7188
+ case 'workflow_schedule_override_delete':
7189
+ return await tolerant(async () => { await messagingDel(`${OVERRIDE_PATH}/${args.id}`); return { success: true, deleted: args.id }; });
7190
+
7191
+ case 'workflow_schedule_override_save':
7192
+ return await tolerant(() => args.id
7193
+ ? messagingPut(`${OVERRIDE_PATH}/${args.id}`, buildOverrideBody(args))
7194
+ : messagingPost(OVERRIDE_PATH, buildOverrideBody(args)));
7195
+
6841
7196
  case 'workflow_list':
6842
7197
  return await rpc('workflow_list', args);
6843
7198
 
@@ -7358,6 +7713,20 @@ reply: ${r?.reply}`;
7358
7713
  return `Sent to Teams webhook "${webhook.name}".`;
7359
7714
  }
7360
7715
 
7716
+ case 'visitor_link_get_settings':
7717
+ return await messagingGet('/SiteSettings/visitor-linking');
7718
+
7719
+ case 'visitor_link_set_settings':
7720
+ return await messagingPut('/SiteSettings/visitor-linking', validateSettingsArgs({ enabled: args.enabled, ttlMinutes: args.ttlMinutes }));
7721
+
7722
+ case 'visitor_link_diagnose': {
7723
+ const settings = await messagingGet('/SiteSettings/visitor-linking');
7724
+ return diagnoseLink({ token: args.token, settings });
7725
+ }
7726
+
7727
+ case 'visitor_link_resolve':
7728
+ return await messagingPost('/api/v1/visitor-links/resolve', { token: args.token });
7729
+
7361
7730
  case 'teams_push_settings_get':
7362
7731
  return await api('GET', '/TeamsBot/PushSettings');
7363
7732
 
@@ -9568,6 +9937,7 @@ reply: ${r?.reply}`;
9568
9937
  weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
9569
9938
  repeat_contact: 'repeat-contact', demand_coverage: 'demand-coverage', bot_containment: 'bot-containment',
9570
9939
  effort_index: 'effort-index', promise_ledger: 'promise-ledger', new_agent_ramp: 'new-agent-ramp',
9940
+ bonus_insights: 'bonus', operations_briefing: 'operations-briefing', entitled: 'entitled',
9571
9941
  };
9572
9942
  if (args.report === 'promise_dismiss') {
9573
9943
  if (!args.promiseId) throw new Error('promiseId is required for promise_dismiss');
@@ -9600,11 +9970,15 @@ reply: ${r?.reply}`;
9600
9970
  if (f > t) throw new Error('from must be on or before to');
9601
9971
  return JSON.stringify(await messagingPost('/api/enterprise-reports/year-in-review', { from: midnightIso(f), to: midnightIso(addDays(t, 1)) }), null, 2);
9602
9972
  }
9603
- const seg = ER_PATHS[args.report];
9973
+ let seg = ER_PATHS[args.report];
9604
9974
  if (!seg) throw new Error('report must be one of: ' + Object.keys(ER_PATHS).join(', ') + ', promise_dismiss, year_in_review');
9975
+ if (args.report === 'bonus_insights') {
9976
+ if (!args.name) throw new Error('name is required for bonus_insights (coverage_gap|next_best_fix|escalation_risk|skill_gap_matrix|handle_quality_outliers|abandon_causes)');
9977
+ seg = 'bonus/' + encodeURIComponent(String(args.name).replace(/-/g, '_'));
9978
+ }
9605
9979
  if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
9606
9980
  const qs = new URLSearchParams();
9607
- for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days', 'windowDays', 'weeks', 'handleMinutes', 'maxConcurrent', 'timeZone', 'lowScore', 'includeDemo']) {
9981
+ for (const k of ['from', 'to', 'groupBy', 'deploymentId', 'teamId', 'threshold', 'perAgent', 'limit', 'days', 'windowDays', 'weeks', 'handleMinutes', 'maxConcurrent', 'timeZone', 'lowScore', 'includeDemo', 'narrative', 'minutesPerReview', 'hourlyCost', 'deflectionPct', 'costPerChat', 'waitMinutes']) {
9608
9982
  if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
9609
9983
  }
9610
9984
  const q = qs.toString();
@@ -9803,6 +10177,15 @@ reply: ${r?.reply}`;
9803
10177
  return `✓ AI Video Avatar updated for site ${args.siteId}. Provider: ${res.providerSlug}, enabled: ${res.enabled}, maxAvatarMinutesPerMonth: ${res.maxAvatarMinutesPerMonth ?? 'n/a'}, maxAvatarTtsChars: ${res.maxAvatarTtsChars ?? 'n/a'}`;
9804
10178
  }
9805
10179
 
10180
+ case 'apple_pay_preset_list':
10181
+ return JSON.stringify(await messagingRequest('GET', `/ApplePayPresetOrders${args.activeOnly ? '?activeOnly=true' : ''}`), null, 2);
10182
+ case 'apple_pay_preset_create':
10183
+ return JSON.stringify(await messagingRequest('POST', '/ApplePayPresetOrders', applePayPresetBody(args)), null, 2);
10184
+ case 'apple_pay_preset_update':
10185
+ return JSON.stringify(await messagingRequest('PUT', `/ApplePayPresetOrders/${applePayPresetId(args.id)}`, applePayPresetBody(args)), null, 2);
10186
+ case 'apple_pay_preset_delete':
10187
+ return JSON.stringify(await messagingRequest('DELETE', `/ApplePayPresetOrders/${applePayPresetId(args.id)}`), null, 2);
10188
+
9806
10189
  case 'avatar_list_library':
9807
10190
  return JSON.stringify(await messagingRequest('GET', `/avatar/library${args.kind ? `?kind=${encodeURIComponent(args.kind)}` : ''}`), null, 2);
9808
10191
  case 'avatar_list_personalities':
@@ -10556,6 +10939,10 @@ reply: ${r?.reply}`;
10556
10939
  ? `Embed key exists but the SuiteApp is not connected (last polled ${d.lastPolledAt}, outside the connected window).`
10557
10940
  : 'Embed key exists but the SuiteApp has not polled yet.';
10558
10941
  }
10942
+ case 'netsuite_rollback_auth': {
10943
+ const r = await messagingPost('/NetSuite/Auth/RollbackAuthType', { id: args.configId });
10944
+ return `Config ${r?.id ?? args.configId} switched back to ${r?.authType ?? 'oauth1'}. Run a connection test to confirm.`;
10945
+ }
10559
10946
 
10560
10947
  case 'netsuite_m2m_init': {
10561
10948
  const r = await messagingPost('/NetSuite/M2M/Init', { id: args.configId ?? null, company: args.company, displayName: args.displayName });
@@ -10947,6 +11334,31 @@ ${JSON.stringify(result, null, 2)}`;
10947
11334
  }
10948
11335
 
10949
11336
  // ── Stripe discount grants ────────────────────────────────────────────────────
11337
+ case 'sign_versions_list': {
11338
+ const r = await api('GET', `/api/Contract/${args.contractId}/versions`);
11339
+ const rows = r.versions || [];
11340
+ if (!rows.length) return `Contract ${args.contractId}: no document versions yet.`;
11341
+ return rows.map(v => `v${v.versionNumber} ${String(v.uploadedAt||'').slice(0,19)} ${v.uploadedBy||'-'} ${v.fileName||'-'} ${v.pageCount??'?'}p sha256:${String(v.sha256||'').slice(0,12)}${v.note ? ' note: '+v.note : ''}`).join('\n');
11342
+ }
11343
+
11344
+ case 'sign_versions_diff': {
11345
+ const r = await api('GET', `/api/Contract/${args.contractId}/diff?from=${args.from}&to=${args.to}`);
11346
+ if (r.identical) return `v${r.from} and v${r.to} are byte-identical.`;
11347
+ return (r.blocks || []).map(b => [...(b.removed||[]).map(x => '- '+x), ...(b.added||[]).map(x => '+ '+x)].join('\n')).join('\n\n') || 'No text differences.';
11348
+ }
11349
+
11350
+ case 'sign_versions_upload': {
11351
+ const buf = _readFileSync(args.filePath);
11352
+ if (buf.subarray(0, 5).toString('latin1') !== '%PDF-') throw new Error('Only PDF files can be uploaded as a contract version.');
11353
+ const form = new FormData();
11354
+ form.append('file', new Blob([buf], { type: 'application/pdf' }), String(args.filePath).split(/[\/]/).pop());
11355
+ if (args.note) form.append('note', args.note);
11356
+ const res = await fetch(`${API_BASE}/api/Contract/${args.contractId}/document`, { method: 'POST', headers: { Authorization: authHeader() }, body: form, signal: AbortSignal.timeout(60000) });
11357
+ const t = await res.text();
11358
+ if (!res.ok) throw new Error(`Upload failed ${res.status}: ${t.slice(0,300)} (new versions only while draft; after send/sign void and re-send)`);
11359
+ return `Uploaded new version for contract ${args.contractId}.`;
11360
+ }
11361
+
10950
11362
  case 'coupon_grant_list': {
10951
11363
  const qs = new URLSearchParams();
10952
11364
  if (args.siteId !== undefined) qs.set('siteId', String(args.siteId));
@@ -11049,6 +11461,42 @@ ${JSON.stringify(result, null, 2)}`;
11049
11461
  return `✅ Billing contact ${args.id} removed.`;
11050
11462
  }
11051
11463
 
11464
+ case 'usage_alerts': {
11465
+ const r = await messagingApi('/Usage/alerts');
11466
+ if (!r.alerts?.length) return 'No plan-limit alerts. Every metered resource is under its warning threshold.';
11467
+ return r.alerts.map(a => `[${a.level.toUpperCase()}] ${a.displayName}: ${a.current} of ${a.limit} (${a.percent}%)${a.softLimit ? ' - overage is billed' : ''}`).join(' | ');
11468
+ }
11469
+ case 'billing_notify_settings_get': {
11470
+ const all = await api('GET', '/api/BillingNotifications/settings');
11471
+ return Object.entries(all).map(([k, v]) => `${k}: ${v ?? '(not set: code default)'}`).join('\n');
11472
+ }
11473
+
11474
+ case 'billing_notify_setting_set': {
11475
+ await api('PUT', `/api/BillingNotifications/settings/${encodeURIComponent(args.key)}`, { value: args.value });
11476
+ return `✅ ${args.key} set to ${args.value}.`;
11477
+ }
11478
+
11479
+ case 'billing_receipt_overrides_set': {
11480
+ // The API replaces all three channels, so keep the ones the caller did not pass.
11481
+ const sub = await api('GET', `/api/BillingSubscription/${args.subscriptionId}`);
11482
+ const res = await api('PUT', `/api/BillingNotifications/subscriptions/${args.subscriptionId}/receipts`, {
11483
+ card: args.card ?? sub.receiptCardOverride ?? null,
11484
+ ach: args.ach ?? sub.receiptAchOverride ?? null,
11485
+ other: args.other ?? sub.receiptOtherOverride ?? null,
11486
+ });
11487
+ return `✅ Subscription ${res.id} receipts: card=${res.card ?? 'inherit'} ach=${res.ach ?? 'inherit'} other=${res.other ?? 'inherit'}.`;
11488
+ }
11489
+
11490
+ case 'billing_digest_send_now': {
11491
+ const res = await api('POST', '/api/BillingNotifications/digest/send-now', {});
11492
+ return `✅ Digest sent to ${res.recipients.join(', ')}: ${res.subject}`;
11493
+ }
11494
+
11495
+ case 'billing_receipts_run_now': {
11496
+ const res = await api('POST', '/api/BillingNotifications/receipts/run-now', {});
11497
+ return `✅ Receipts run: ${res.summary}`;
11498
+ }
11499
+
11052
11500
  case 'billing_plan_status': {
11053
11501
  const s = await api('GET', '/api/BillingPayment/plan-status');
11054
11502
  const lines = [`Site ${s.siteId} — Subscription Status`];
@@ -11220,6 +11668,32 @@ ${JSON.stringify(result, null, 2)}`;
11220
11668
  return `✅ Site ${args.siteId}: ${args.featureKey} = ${args.value}${args.reason ? ` (${args.reason})` : ''}`;
11221
11669
  }
11222
11670
 
11671
+ case 'entitlement_trial_get': {
11672
+ if (!args.siteId) throw new Error('siteId is required');
11673
+ return JSON.stringify(await api('GET', `/Entitlements/${args.siteId}/trial`), null, 2);
11674
+ }
11675
+
11676
+ case 'entitlement_trial_recommend': {
11677
+ if (!args.siteId) throw new Error('siteId is required');
11678
+ return JSON.stringify(await api('GET', `/Entitlements/${args.siteId}/trial/recommendation`), null, 2);
11679
+ }
11680
+
11681
+ case 'entitlement_trial_grant': {
11682
+ if (!args.siteId) throw new Error('siteId is required');
11683
+ if (!args.featureKey) throw new Error('featureKey is required');
11684
+ if (!args.reason) throw new Error('reason is required');
11685
+ const res = await api('POST', `/Entitlements/${args.siteId}/trial/grant`, { featureKey: args.featureKey, reason: args.reason });
11686
+ return `Site ${args.siteId}: ${res.featureKey} granted for the trial (${res.state}).`;
11687
+ }
11688
+
11689
+ case 'entitlement_trial_set_limit': {
11690
+ if (!args.siteId) throw new Error('siteId is required');
11691
+ if (!Number.isInteger(args.value) || args.value < 0) throw new Error('value must be a whole number >= 0');
11692
+ if (!args.key || !args.reason) throw new Error('key and reason are required');
11693
+ const res = await api('POST', `/Entitlements/${args.siteId}/trial/limit`, { key: args.key, value: args.value, reason: args.reason });
11694
+ return `Site ${args.siteId}: ${res.key} ${res.old} -> ${res.value}.`;
11695
+ }
11696
+
11223
11697
  case 'entitlement_remove_override': {
11224
11698
  if (!args.siteId) throw new Error('siteId is required');
11225
11699
  if (!args.featureKey) throw new Error('featureKey is required');
@@ -12058,7 +12532,12 @@ ${JSON.stringify(result, null, 2)}`;
12058
12532
  if (a === 'create') return await messagingPost('/api/admin/QualityQa/rubrics', args.rubric ?? {});
12059
12533
  if (a === 'update') return await messagingPut('/api/admin/QualityQa/rubrics/' + args.id, args.rubric ?? {});
12060
12534
  if (a === 'delete') return await messagingDel('/api/admin/QualityQa/rubrics/' + args.id);
12061
- throw new Error('action must be list|get|default|create|update|delete');
12535
+ if (a === 'starter') return await messagingPost('/api/admin/QualityQa/rubrics/starter', {});
12536
+ if (a === 'clone') return await messagingPost('/api/admin/QualityQa/rubrics/' + args.id + '/clone', args.rubric ?? {});
12537
+ if (a === 'activate') return await messagingPut('/api/admin/QualityQa/rubrics/' + args.id, { isActive: true });
12538
+ if (a === 'deactivate') return await messagingPut('/api/admin/QualityQa/rubrics/' + args.id, { isActive: false });
12539
+ if (a === 'rescore') return await messagingPost('/api/admin/QualityQa/rescore?days=' + encodeURIComponent(args.days ?? 30) + '&limit=' + encodeURIComponent(args.limit ?? 200), {});
12540
+ throw new Error('action must be list|get|default|starter|create|update|clone|activate|deactivate|rescore|delete');
12062
12541
  }
12063
12542
  case 'qa_low_scores': {
12064
12543
  const qs = new URLSearchParams({ days: String(args.days ?? 30), skip: String(args.skip ?? 0), take: String(args.take ?? 50) });
@@ -12128,7 +12607,7 @@ const PROMPTS = [
12128
12607
  ];
12129
12608
 
12130
12609
  const server = new Server(
12131
- { name: 'velaro', version: '0.4.0' },
12610
+ { name: 'velaro', version: JSON.parse(_readFileSync(_join(_dirname(_ftu(import.meta.url)), 'package.json'), 'utf8')).version },
12132
12611
  { capabilities: { tools: {}, prompts: {} } }
12133
12612
  );
12134
12613
 
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "url": "https://github.com/velaro/velaro-admin",
9
9
  "source": "github"
10
10
  },
11
- "version": "0.6.0",
11
+ "version": "1.0.0",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "@velaro/mcp-server",
17
- "version": "0.6.0",
17
+ "version": "1.0.0",
18
18
  "transport": {
19
19
  "type": "stdio"
20
20
  },
@@ -0,0 +1,70 @@
1
+ // Shared helpers for the workflow schedule-priority tools (velaro MCP). Messaging owns the REST
2
+ // endpoints under /Workflows/schedule/*; the site always comes from auth, never a parameter.
3
+ // An older messaging without the endpoints answers 404/501 -> we return a friendly message.
4
+
5
+ export const SCHEDULE_NOT_AVAILABLE = "Workflow scheduling is not available on this site's version.";
6
+
7
+ export const SCHEDULE_ACTIONS = { SwitchWorkflow: 3 };
8
+
9
+ export function buildResolvePath({ trigger, channel, at, tz } = {}) {
10
+ const q = new URLSearchParams();
11
+ if (trigger) q.set('trigger', trigger);
12
+ if (channel) q.set('channel', channel);
13
+ if (at) q.set('at', at);
14
+ if (tz) q.set('tz', tz);
15
+ return `/Workflows/schedule/resolve?${q}`;
16
+ }
17
+
18
+ export function buildTimelinePath({ from, to, trigger, channel } = {}) {
19
+ const q = new URLSearchParams();
20
+ if (from) q.set('from', from);
21
+ if (to) q.set('to', to);
22
+ if (trigger) q.set('trigger', trigger);
23
+ if (channel) q.set('channel', channel);
24
+ return `/Workflows/schedule/timeline?${q}`;
25
+ }
26
+
27
+ export const VALIDATE_PATH = '/Workflows/schedule/validate';
28
+ export const BANNER_PATH = '/Workflows/schedule/banner';
29
+ export const OVERRIDE_PATH = '/ScheduleOverride';
30
+
31
+ // Normalizes the action to the int messaging expects (accepts 'SwitchWorkflow' or 3).
32
+ export function buildOverrideBody(args = {}) {
33
+ const { action, targetWorkflowId, ...rest } = args;
34
+ const body = { ...rest };
35
+ if (action !== undefined) body.action = typeof action === 'string' && action in SCHEDULE_ACTIONS ? SCHEDULE_ACTIONS[action] : action;
36
+ if (targetWorkflowId !== undefined) body.targetWorkflowId = targetWorkflowId;
37
+ return body;
38
+ }
39
+
40
+ // Messaging's POST /Workflows/schedule/validate takes { trigger, channel, override } (one draft override).
41
+ export function buildValidateBody({ trigger, channel, override } = {}) {
42
+ const body = { trigger: trigger || 'conversation_opened' };
43
+ if (channel) body.channel = channel;
44
+ if (override) body.override = buildOverrideBody(override);
45
+ return body;
46
+ }
47
+
48
+ export function buildBannerPath({ trigger, channel } = {}) {
49
+ const q = new URLSearchParams();
50
+ if (trigger) q.set('trigger', trigger);
51
+ if (channel) q.set('channel', channel);
52
+ return `${BANNER_PATH}?${q}`;
53
+ }
54
+
55
+ export function buildOverrideListPath({ action } = {}) {
56
+ if (action === undefined || action === '') return OVERRIDE_PATH;
57
+ const n = typeof action === 'string' && action in SCHEDULE_ACTIONS ? SCHEDULE_ACTIONS[action] : action;
58
+ return `${OVERRIDE_PATH}?action=${encodeURIComponent(n)}`;
59
+ }
60
+
61
+ // Runs fn(); a 404/501 (error message contains "-> 404:" / "-> 501:" or err.status) maps to the friendly string.
62
+ export async function tolerant(fn) {
63
+ try {
64
+ return await fn();
65
+ } catch (err) {
66
+ const status = err?.status ?? Number(/->\s*(\d{3})/.exec(err?.message ?? '')?.[1]);
67
+ if (status === 404 || status === 501) return SCHEDULE_NOT_AVAILABLE;
68
+ throw err;
69
+ }
70
+ }