@velaro/mcp-server 1.0.0 → 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 +1 -1
- package/server.js +192 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velaro/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
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": {
|
package/server.js
CHANGED
|
@@ -232,6 +232,65 @@ function execInsightsRange(days) {
|
|
|
232
232
|
|
|
233
233
|
const messagingDel = (path) => messagingRequest('DELETE', path);
|
|
234
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
|
+
|
|
235
294
|
// Skill-based routing CRUD helper for SkillRouting/{Skills|Assignments|Requirements}.
|
|
236
295
|
async function skillRoutingCrud(resource, args, fields) {
|
|
237
296
|
const body = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
@@ -1761,6 +1820,41 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
1761
1820
|
required: ['message'],
|
|
1762
1821
|
},
|
|
1763
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
|
+
},
|
|
1764
1858
|
{
|
|
1765
1859
|
name: 'teams_push_settings_get',
|
|
1766
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.',
|
|
@@ -3544,7 +3638,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3544
3638
|
inputSchema: {
|
|
3545
3639
|
type: 'object',
|
|
3546
3640
|
properties: {
|
|
3547
|
-
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'] },
|
|
3548
3642
|
from: { type: 'string', description: 'yyyy-MM-dd start (default: 30 days ago).' },
|
|
3549
3643
|
to: { type: 'string', description: 'yyyy-MM-dd end, inclusive (default: today).' },
|
|
3550
3644
|
groupBy: { type: 'string', description: 'program_breakdown: website|team|platform|device|entrypage. sentiment_breakdown: website|team|agent|theme.' },
|
|
@@ -3561,6 +3655,13 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
3561
3655
|
timeZone: { type: 'string', description: 'demand_coverage: IANA time zone (default America/Los_Angeles).' },
|
|
3562
3656
|
lowScore: { type: 'number', description: 'bot_containment: QA score below which a handed-off chat counts as regret (1-100, default 60).' },
|
|
3563
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).' },
|
|
3564
3665
|
promiseId: { type: 'string', description: 'promise_dismiss: the promise id to dismiss (managers only).' },
|
|
3565
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.' },
|
|
3566
3667
|
},
|
|
@@ -4706,6 +4807,15 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
4706
4807
|
},
|
|
4707
4808
|
},
|
|
4708
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
|
+
},
|
|
4709
4819
|
{
|
|
4710
4820
|
name: 'netsuite_suiteapp_download',
|
|
4711
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.',
|
|
@@ -5013,6 +5123,21 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
5013
5123
|
// Distinct from the reusable coupon-CODE catalog, which lives in velaro-messaging
|
|
5014
5124
|
// (superadmin/billing/coupons). Pass couponCode here only to record which catalog code a grant
|
|
5015
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
|
+
},
|
|
5016
5141
|
{
|
|
5017
5142
|
name: 'coupon_grant_list',
|
|
5018
5143
|
description: 'List per-account Stripe discount grants. Velaro billing staff only. Shows which accounts currently have a discount and its status.',
|
|
@@ -5138,6 +5263,10 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
5138
5263
|
required: ['id'],
|
|
5139
5264
|
},
|
|
5140
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
|
+
},
|
|
5141
5270
|
{
|
|
5142
5271
|
name: 'billing_notify_settings_get',
|
|
5143
5272
|
description: 'Billing notifications: the global settings (receipts.card, receipts.ach, receipts.other, invoices.bcc, digest.recipients). Staff only.',
|
|
@@ -6400,7 +6529,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
|
|
|
6400
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"]}},
|
|
6401
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"]}},
|
|
6402
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"}}}},
|
|
6403
|
-
{"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"]}},
|
|
6404
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"}}}},
|
|
6405
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":{}}},
|
|
6406
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"}}}},
|
|
@@ -7584,6 +7713,20 @@ reply: ${r?.reply}`;
|
|
|
7584
7713
|
return `Sent to Teams webhook "${webhook.name}".`;
|
|
7585
7714
|
}
|
|
7586
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
|
+
|
|
7587
7730
|
case 'teams_push_settings_get':
|
|
7588
7731
|
return await api('GET', '/TeamsBot/PushSettings');
|
|
7589
7732
|
|
|
@@ -9794,6 +9937,7 @@ reply: ${r?.reply}`;
|
|
|
9794
9937
|
weekly_brief: 'weekly-brief', weekly_brief_send_test: 'weekly-brief/send-test',
|
|
9795
9938
|
repeat_contact: 'repeat-contact', demand_coverage: 'demand-coverage', bot_containment: 'bot-containment',
|
|
9796
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',
|
|
9797
9941
|
};
|
|
9798
9942
|
if (args.report === 'promise_dismiss') {
|
|
9799
9943
|
if (!args.promiseId) throw new Error('promiseId is required for promise_dismiss');
|
|
@@ -9826,11 +9970,15 @@ reply: ${r?.reply}`;
|
|
|
9826
9970
|
if (f > t) throw new Error('from must be on or before to');
|
|
9827
9971
|
return JSON.stringify(await messagingPost('/api/enterprise-reports/year-in-review', { from: midnightIso(f), to: midnightIso(addDays(t, 1)) }), null, 2);
|
|
9828
9972
|
}
|
|
9829
|
-
|
|
9973
|
+
let seg = ER_PATHS[args.report];
|
|
9830
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
|
+
}
|
|
9831
9979
|
if (args.report === 'weekly_brief_send_test') return JSON.stringify(await messagingPost('/api/enterprise-reports/' + seg, {}), null, 2);
|
|
9832
9980
|
const qs = new URLSearchParams();
|
|
9833
|
-
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']) {
|
|
9834
9982
|
if (args[k] != null && args[k] !== '') qs.set(k, String(args[k]));
|
|
9835
9983
|
}
|
|
9836
9984
|
const q = qs.toString();
|
|
@@ -10791,6 +10939,10 @@ reply: ${r?.reply}`;
|
|
|
10791
10939
|
? `Embed key exists but the SuiteApp is not connected (last polled ${d.lastPolledAt}, outside the connected window).`
|
|
10792
10940
|
: 'Embed key exists but the SuiteApp has not polled yet.';
|
|
10793
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
|
+
}
|
|
10794
10946
|
|
|
10795
10947
|
case 'netsuite_m2m_init': {
|
|
10796
10948
|
const r = await messagingPost('/NetSuite/M2M/Init', { id: args.configId ?? null, company: args.company, displayName: args.displayName });
|
|
@@ -11182,6 +11334,31 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
11182
11334
|
}
|
|
11183
11335
|
|
|
11184
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
|
+
|
|
11185
11362
|
case 'coupon_grant_list': {
|
|
11186
11363
|
const qs = new URLSearchParams();
|
|
11187
11364
|
if (args.siteId !== undefined) qs.set('siteId', String(args.siteId));
|
|
@@ -11284,6 +11461,11 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
11284
11461
|
return `✅ Billing contact ${args.id} removed.`;
|
|
11285
11462
|
}
|
|
11286
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
|
+
}
|
|
11287
11469
|
case 'billing_notify_settings_get': {
|
|
11288
11470
|
const all = await api('GET', '/api/BillingNotifications/settings');
|
|
11289
11471
|
return Object.entries(all).map(([k, v]) => `${k}: ${v ?? '(not set: code default)'}`).join('\n');
|
|
@@ -12350,7 +12532,12 @@ ${JSON.stringify(result, null, 2)}`;
|
|
|
12350
12532
|
if (a === 'create') return await messagingPost('/api/admin/QualityQa/rubrics', args.rubric ?? {});
|
|
12351
12533
|
if (a === 'update') return await messagingPut('/api/admin/QualityQa/rubrics/' + args.id, args.rubric ?? {});
|
|
12352
12534
|
if (a === 'delete') return await messagingDel('/api/admin/QualityQa/rubrics/' + args.id);
|
|
12353
|
-
|
|
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');
|
|
12354
12541
|
}
|
|
12355
12542
|
case 'qa_low_scores': {
|
|
12356
12543
|
const qs = new URLSearchParams({ days: String(args.days ?? 30), skip: String(args.skip ?? 0), take: String(args.take ?? 50) });
|