@velaro/mcp-server 0.6.95 → 0.6.97

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 +268 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velaro/mcp-server",
3
- "version": "0.6.95",
3
+ "version": "0.6.97",
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
@@ -160,9 +160,17 @@ function avatarOverrideBody(args) {
160
160
  for (const k of ['providerSlug', 'avatarId', 'personalityPresetSlug', 'voiceId', 'displayName', 'backgroundMode', 'backgroundValue']) {
161
161
  if (args[k] !== undefined) body[k] = args[k];
162
162
  }
163
+ if (args.motionLevel !== undefined) body.motionLevel = parseAvatarMotionLevel(args.motionLevel);
163
164
  return body;
164
165
  }
165
166
 
167
+ /** Closed vocabulary for avatar body motion; "" resets/inherits. Fails readably instead of a bare 400. */
168
+ function parseAvatarMotionLevel(raw) {
169
+ const level = String(raw).trim().toLowerCase();
170
+ if (!['', 'off', 'subtle', 'natural', 'expressive'].includes(level)) throw new Error('motionLevel must be off, subtle, natural or expressive (empty string resets/inherits).');
171
+ return level;
172
+ }
173
+
166
174
  async function messagingRequest(method, path, body) {
167
175
  if (!MESSAGING_API) throw new Error(
168
176
  'VELARO_MESSAGING_API is not configured. Add "VELARO_MESSAGING_API": "https://velaro-messaging-api-staging.azurewebsites.net" to your MCP env config.'
@@ -1699,6 +1707,79 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
1699
1707
  },
1700
1708
 
1701
1709
  // ── Livefluence Migration ─────────────────────────────────────────────────
1710
+ {
1711
+ name: 'livefluence_site_workflows_list',
1712
+ description: 'Velaro staff only (superadmin). List ANOTHER v20 site\'s workflows: id, name, enabled, priority, action, team/AI target. Use after livefluence_migrate to see what exists on the destination site (e.g. 100140).',
1713
+ inputSchema: { type: 'object', properties: { site_id: { type: 'number', description: 'V20 site ID to inspect' } }, required: ['site_id'] },
1714
+ },
1715
+ {
1716
+ name: 'livefluence_site_workflows_set_enabled',
1717
+ description: 'Velaro staff only (superadmin). Enable or disable workflows on a v20 site by ids or names. Idempotent, audit-logged. Enabling REFUSES a workflow whose trigger is not fully converted from v10 unless force_partial=true; pass v10_site_id to check each against its same-named v10 rule. Use dry_run=true first.',
1718
+ inputSchema: {
1719
+ type: 'object',
1720
+ properties: {
1721
+ site_id: { type: 'number', description: 'V20 site ID' },
1722
+ workflow_ids: { type: 'array', items: { type: 'number' } },
1723
+ names: { type: 'array', items: { type: 'string' } },
1724
+ enabled: { type: 'boolean', description: 'true = enable, false = disable' },
1725
+ v10_site_id: { type: 'number', description: 'Verify conversion against this v10 site rules' },
1726
+ force_partial: { type: 'boolean', description: 'Enable even if v10 expression terms were not converted' },
1727
+ dry_run: { type: 'boolean' },
1728
+ },
1729
+ required: ['site_id', 'enabled'],
1730
+ },
1731
+ },
1732
+ {
1733
+ name: 'livefluence_site_workflow_create',
1734
+ description: 'Velaro staff only (superadmin). Create a workflow on a v20 site from a nodes-blob JSON (object of nodes with a trigger node). Disabled unless enabled=true. Idempotent by name; validates that referenced AI configs/teams exist on the site. Use dry_run=true first.',
1735
+ inputSchema: {
1736
+ type: 'object',
1737
+ properties: {
1738
+ site_id: { type: 'number' },
1739
+ name: { type: 'string' },
1740
+ nodes_blob: { type: 'string', description: 'JSON string of the nodes object' },
1741
+ trigger_type: { type: 'string', description: 'Default conversation_opened' },
1742
+ enabled: { type: 'boolean' },
1743
+ dry_run: { type: 'boolean' },
1744
+ },
1745
+ required: ['site_id', 'name', 'nodes_blob'],
1746
+ },
1747
+ },
1748
+ {
1749
+ name: 'livefluence_site_workflow_set_action',
1750
+ description: 'Velaro staff only (superadmin). Set a workflow action node on a v20 site to transferToAI {aiConfigId} or routeToTeam {teamId}. Validates the target exists on that site. Idempotent, audit-logged.',
1751
+ inputSchema: {
1752
+ type: 'object',
1753
+ properties: {
1754
+ site_id: { type: 'number' },
1755
+ workflow_id: { type: 'number' },
1756
+ action_type: { type: 'string', enum: ['transferToAI', 'routeToTeam'] },
1757
+ target_id: { type: 'number', description: 'aiConfigId for transferToAI, teamId for routeToTeam' },
1758
+ dry_run: { type: 'boolean' },
1759
+ },
1760
+ required: ['site_id', 'workflow_id', 'action_type', 'target_id'],
1761
+ },
1762
+ },
1763
+ {
1764
+ name: 'livefluence_site_rules_list',
1765
+ description: 'Velaro staff only (superadmin). List a v20 site Rules (e.g. the use-ai rules seeded by livefluence_seed_url_rules) with enabled state.',
1766
+ inputSchema: { type: 'object', properties: { site_id: { type: 'number' } }, required: ['site_id'] },
1767
+ },
1768
+ {
1769
+ name: 'livefluence_site_rules_set_enabled',
1770
+ description: 'Velaro staff only (superadmin). Switch v20 Rules off/on without deleting them (reversible), by rule ids or by trigger (e.g. use-ai). Use to turn off the url-rules seeded earlier so they do not double-route with migrated workflows. Idempotent, audit-logged.',
1771
+ inputSchema: {
1772
+ type: 'object',
1773
+ properties: {
1774
+ site_id: { type: 'number' },
1775
+ rule_ids: { type: 'array', items: { type: 'number' } },
1776
+ trigger: { type: 'string', description: 'Every rule with this trigger, e.g. use-ai' },
1777
+ enabled: { type: 'boolean' },
1778
+ dry_run: { type: 'boolean' },
1779
+ },
1780
+ required: ['site_id', 'enabled'],
1781
+ },
1782
+ },
1702
1783
  {
1703
1784
  name: 'livefluence_seed_url_rules',
1704
1785
  description: 'Velaro staff only. Seed v20 use-ai routing rules on a target site from v10 URL rules. Each rule matches "url contains <path>" with scheme/host stripped (path-only), so a demo page path triggers the same rule as the customer real URL. Idempotent. Domain-only URLs are skipped. Use dry_run=true first.',
@@ -2160,6 +2241,20 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
2160
2241
  required: ['jobId'],
2161
2242
  },
2162
2243
  },
2244
+ {
2245
+ name: 'index_create',
2246
+ description: 'Velaro staff only. Create a named virtual knowledge index on ANY site (e.g. "100140-troubleshooting", "100140-manual-catalog") so uploads/publishes with that indexName work. Idempotent: returns the existing index if the name already exists. Respects the site index quota. Wraps POST KnowledgeSetup/Staff/{siteId}/create-index.',
2247
+ inputSchema: {
2248
+ type: 'object',
2249
+ properties: {
2250
+ siteId: { type: 'number', description: 'Target site ID' },
2251
+ indexName: { type: 'string', description: 'Index name (lowercased, a-z 0-9 and dashes)' },
2252
+ purpose: { type: 'string', enum: ['general', 'troubleshooting', 'catalog'], description: 'Index purpose (default general)' },
2253
+ excludeFromAllSources: { type: 'boolean', description: 'Keep out of the ["*"] all-sources search' },
2254
+ },
2255
+ required: ['siteId', 'indexName'],
2256
+ },
2257
+ },
2163
2258
  {
2164
2259
  name: 'uploaded_file_reindex',
2165
2260
  description: 'Re-index an already-uploaded AI training file from blob storage — use when a file shows 0 chunks (e.g. PDF uploaded before PDF support was added). Reads from blob and re-runs full ingestion. Use index_status after ~30s to verify chunks appear.',
@@ -3299,7 +3394,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3299
3394
  // ── AI Video Avatar management (superadmin) ────────────────────────────────
3300
3395
  {
3301
3396
  name: 'avatar_get_config',
3302
- description: 'Get the AI Video Avatar (Simli/D-ID/HeyGen) configuration for any customer site — provider, avatar/face ID, voice ID, enabled state, "call me" option, callback confirm window. Velaro admin only. Never returns provider API keys (Velaro uses its own master keys, never per-site).',
3397
+ description: 'Get the AI Video Avatar (Simli/D-ID/HeyGen) configuration for any customer site — provider, avatar/face ID, voice ID, enabled state, "call me" option, callback confirm window, body motion level and strength. Velaro admin only. Never returns provider API keys (Velaro uses its own master keys, never per-site).',
3303
3398
  inputSchema: {
3304
3399
  type: 'object',
3305
3400
  properties: {
@@ -3325,6 +3420,8 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3325
3420
  showBadge: { type: 'boolean', description: 'Show the name badge over the avatar video. Default true.' },
3326
3421
  sizePx: { type: 'number', description: 'Max avatar panel width in px (160-640, square). Default 360.' },
3327
3422
  allowVisitorCollapse: { type: 'boolean', description: 'Let visitors hide or collapse the avatar panel (up/down arrow and X). Default true.' },
3423
+ motionLevel: { type: 'string', enum: ['', 'off', 'subtle', 'natural', 'expressive'], description: 'Body motion preset layered over the lip-sync (shoulders, breathing, head, brows). Empty string resets to the platform default (natural). Visitors with reduced-motion enabled always get a still avatar.' },
3424
+ motionIntensityPercent: { type: 'integer', minimum: 25, maximum: 150, description: 'Body motion strength, whole percent 25-150 (default 100); multiplies every motion amplitude.' },
3328
3425
  maxAvatarMinutesPerMonth: { type: 'number', description: 'Monthly avatar video minutes cap for the site. Whole number 1-100000; never unlimited (0/negative rejected).' },
3329
3426
  maxAvatarTtsChars: { type: 'number', description: 'Monthly avatar TTS character cap (avatar_tts_chars) for the site. Whole number 1-100000000; never unlimited (0/negative rejected). Overage pricing is a platform-wide ladder, not per site.' },
3330
3427
  },
@@ -3374,6 +3471,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3374
3471
  displayName: { type: 'string', description: 'Visitor-facing avatar name.' },
3375
3472
  backgroundMode: { type: 'string', description: 'none | color | gradient | image. "none" = explicitly no backdrop.' },
3376
3473
  backgroundValue: { type: 'string', description: 'Hex color, "#a,#b" gradient, or https image URL.' },
3474
+ motionLevel: { type: 'string', enum: ['', 'off', 'subtle', 'natural', 'expressive'], description: 'Body motion preset for this group (perWorkflow ?? perGroup ?? perSite ?? natural). Empty string inherits.' },
3377
3475
  replace: { type: 'boolean', description: 'true = replace the whole override (omitted fields inherit) instead of merging.' },
3378
3476
  },
3379
3477
  required: ['groupId'],
@@ -3403,6 +3501,7 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
3403
3501
  displayName: { type: 'string' },
3404
3502
  backgroundMode: { type: 'string', description: 'none | color | gradient | image' },
3405
3503
  backgroundValue: { type: 'string' },
3504
+ motionLevel: { type: 'string', enum: ['', 'off', 'subtle', 'natural', 'expressive'], description: 'Body motion preset for this workflow. Empty string inherits.' },
3406
3505
  replace: { type: 'boolean' },
3407
3506
  },
3408
3507
  required: ['workflowId'],
@@ -5246,6 +5345,11 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5246
5345
  description: 'Hosted public job pages for this site: is the package included, are the pages published, and the opaque board link, sitemap, Indeed feed and Google feed URLs (the feed URLs contain a secret key). Returns { entitled, enabled, boardUrl, sitemapUrl, indeedFeedUrl, googleFeedUrl, publicApplyEnabled, eeocSelfIdAvailable, liveListings }; { entitled:false } without the package.',
5247
5346
  inputSchema: { type: 'object', properties: {}, required: [] },
5248
5347
  },
5348
+ {
5349
+ name: 'jobboard_applicant_accounts_status',
5350
+ description: 'Applicant accounts on the hosted job board (applicant sign-in with an emailed one-time code, my applications, saved jobs, resume library). Returns { entitled, hostedEnabled, signInUrl, accounts, signedInLast30Days, deletionPending, savedJobs, linkedApplications } (counts only, never emails or resumes); { entitled:false } without the Job Board applicant accounts package.',
5351
+ inputSchema: { type: 'object', properties: {}, required: [] },
5352
+ },
5249
5353
  {
5250
5354
  name: 'jobboard_hosted_set',
5251
5355
  description: 'Publish or unpublish the hosted job pages (apply-if-present). enabled=true issues the opaque board link and feed key the first time. regenerateLink=true issues a NEW board link and every old hosted URL answers 404 (administrators only). Requires the Job Board hosted pages package (403 otherwise).',
@@ -5292,6 +5396,60 @@ For multiSelect dynamic options: the variable named in dynamicOptionsVariable mu
5292
5396
  required: ['applicationId', 'stage'],
5293
5397
  },
5294
5398
  },
5399
+ // -- Job Board employers + moderation (package "job-board-employers", default OFF: 403 job_board_employers_not_entitled without it) --
5400
+ {
5401
+ name: 'jobboard_employers_list',
5402
+ description: 'List the employers on this site\'s job board with status, trusted flag (first posting approved), member count, live/total listings and how many listings wait for moderation. Requires the Job board employers package (403 otherwise).',
5403
+ inputSchema: { type: 'object', properties: {}, required: [] },
5404
+ },
5405
+ {
5406
+ name: 'jobboard_employer_create',
5407
+ description: 'Create an employer on the job board (name 2-200 characters, unique per site; optional verified email domain). Administrator only. Invite its first member with jobboard_employer_invite.',
5408
+ inputSchema: {
5409
+ type: 'object',
5410
+ properties: { name: { type: 'string' }, verifiedDomain: { type: 'string', description: 'Optional, e.g. example.com' } },
5411
+ required: ['name'],
5412
+ },
5413
+ },
5414
+ {
5415
+ name: 'jobboard_employer_invite',
5416
+ description: 'Email a single-use invitation (valid 7 days) to join an employer as Owner, Recruiter (default) or Viewer. Employers manage only their own listings and applicants and never see EEO data. Administrator only; 409 if the email already belongs to another employer or the hosted board is off.',
5417
+ inputSchema: {
5418
+ type: 'object',
5419
+ properties: {
5420
+ employerId: { type: 'number' },
5421
+ email: { type: 'string' },
5422
+ displayName: { type: 'string' },
5423
+ role: { type: 'string', description: 'Owner | Recruiter | Viewer (default Recruiter)' },
5424
+ },
5425
+ required: ['employerId', 'email'],
5426
+ },
5427
+ },
5428
+ {
5429
+ name: 'jobboard_employer_suspend',
5430
+ description: 'Suspend an employer: its members are signed out at once and every published listing is paused (off the public board). A reason of 3-500 characters is required. Administrator only.',
5431
+ inputSchema: { type: 'object', properties: { employerId: { type: 'number' }, reason: { type: 'string' } }, required: ['employerId', 'reason'] },
5432
+ },
5433
+ {
5434
+ name: 'jobboard_employer_reactivate',
5435
+ description: 'Reactivate a suspended employer. Its paused listings stay paused until the employer resumes them. Administrator only.',
5436
+ inputSchema: { type: 'object', properties: { employerId: { type: 'number' } }, required: ['employerId'] },
5437
+ },
5438
+ {
5439
+ name: 'jobboard_moderation_list',
5440
+ description: 'The moderation queue: employer listings waiting for a decision (an employer\'s first posting, or any posting that matched a scam signal such as fee requests, SSN/bank requests, gift cards, wire transfers, WhatsApp-only contact). Each item carries the matched flags. Pending listings are invisible on the public board.',
5441
+ inputSchema: { type: 'object', properties: {}, required: [] },
5442
+ },
5443
+ {
5444
+ name: 'jobboard_moderation_approve',
5445
+ description: 'Approve a pending employer listing: it goes live, the employer becomes trusted (later clean postings publish at once), and the decision is audited. Administrator only; 409 unless the listing is waiting for moderation.',
5446
+ inputSchema: { type: 'object', properties: { listingId: { type: 'number' } }, required: ['listingId'] },
5447
+ },
5448
+ {
5449
+ name: 'jobboard_moderation_reject',
5450
+ description: 'Reject a pending employer listing with a reason (3-500 characters) the employer will see; the listing stays hidden and the decision is audited. Administrator only.',
5451
+ inputSchema: { type: 'object', properties: { listingId: { type: 'number' }, reason: { type: 'string' } }, required: ['listingId', 'reason'] },
5452
+ },
5295
5453
  // -- Job Board pipeline (package "job-board-pipeline", default OFF: 403 job_board_pipeline_not_entitled without it, except jobboard_pipeline_get) --
5296
5454
  {
5297
5455
  name: 'jobboard_pipeline_get',
@@ -7145,6 +7303,47 @@ async function handleTool(name, args) {
7145
7303
  return JSON.stringify(result, null, 2);
7146
7304
  }
7147
7305
 
7306
+ case 'livefluence_site_workflows_list': {
7307
+ const result = await api('GET', `/DatabaseTool/SiteWorkflows/${args.site_id}`);
7308
+ return JSON.stringify(result, null, 2);
7309
+ }
7310
+
7311
+ case 'livefluence_site_workflows_set_enabled': {
7312
+ const result = await api('POST', '/DatabaseTool/SiteWorkflows/SetEnabled', {
7313
+ siteId: args.site_id, workflowIds: args.workflow_ids ?? [], names: args.names ?? [],
7314
+ enabled: !!args.enabled, dryRun: !!args.dry_run, forcePartial: !!args.force_partial, v10SiteId: args.v10_site_id ?? null,
7315
+ });
7316
+ return JSON.stringify(result, null, 2);
7317
+ }
7318
+
7319
+ case 'livefluence_site_workflow_create': {
7320
+ const result = await api('POST', '/DatabaseTool/SiteWorkflows/Create', {
7321
+ siteId: args.site_id, name: args.name, triggerType: args.trigger_type ?? 'conversation_opened',
7322
+ nodesBlob: typeof args.nodes_blob === 'string' ? args.nodes_blob : JSON.stringify(args.nodes_blob),
7323
+ enabled: !!args.enabled, dryRun: !!args.dry_run,
7324
+ });
7325
+ return JSON.stringify(result, null, 2);
7326
+ }
7327
+
7328
+ case 'livefluence_site_workflow_set_action': {
7329
+ const result = await api('POST', '/DatabaseTool/SiteWorkflows/SetAction', {
7330
+ siteId: args.site_id, workflowId: args.workflow_id, actionType: args.action_type, targetId: args.target_id, dryRun: !!args.dry_run,
7331
+ });
7332
+ return JSON.stringify(result, null, 2);
7333
+ }
7334
+
7335
+ case 'livefluence_site_rules_list': {
7336
+ const result = await api('GET', `/DatabaseTool/SiteRules/${args.site_id}`);
7337
+ return JSON.stringify(result, null, 2);
7338
+ }
7339
+
7340
+ case 'livefluence_site_rules_set_enabled': {
7341
+ const result = await api('POST', '/DatabaseTool/SiteRules/SetEnabled', {
7342
+ siteId: args.site_id, ruleIds: args.rule_ids ?? [], trigger: args.trigger ?? null, enabled: !!args.enabled, dryRun: !!args.dry_run,
7343
+ });
7344
+ return JSON.stringify(result, null, 2);
7345
+ }
7346
+
7148
7347
  case 'livefluence_seed_url_rules': {
7149
7348
  const result = await api('POST', '/DatabaseTool/V10SeedUrlRules', {
7150
7349
  siteId: args.site_id, aiConfigId: args.ai_config_id, dryRun: !!args.dry_run, rules: args.rules,
@@ -7626,6 +7825,15 @@ async function handleTool(name, args) {
7626
7825
  return lines.join('\n');
7627
7826
  }
7628
7827
 
7828
+ case 'index_create': {
7829
+ if (!args.siteId || !args.indexName) return 'siteId and indexName are required.';
7830
+ const body = { indexName: args.indexName, purpose: args.purpose ?? 'general' };
7831
+ if (args.excludeFromAllSources !== undefined) body.excludeFromAllSources = args.excludeFromAllSources;
7832
+ const idx = await messagingRequest('POST', `/KnowledgeSetup/Staff/${encodeURIComponent(args.siteId)}/create-index`, body);
7833
+ if (idx?.error) return `Create index error: ${idx.error}`;
7834
+ return `Index ready on site ${args.siteId}: ${idx?.name} (id=${idx?.id}, purpose=${idx?.purpose}, excludeFromAllSources=${!!idx?.excludeFromAllSources}).`;
7835
+ }
7836
+
7629
7837
  case 'uploaded_files_list': {
7630
7838
  const files = await messagingRequest('GET', '/uploadedFiles/list/AI');
7631
7839
  if (!Array.isArray(files) || !files.length) return 'No AI training files uploaded for this site.';
@@ -8937,6 +9145,7 @@ async function handleTool(name, args) {
8937
9145
  ` Display name: ${data.displayName || '(AI)'}`,
8938
9146
  ` Badge shown: ${data.showBadge}`,
8939
9147
  ` Size: ${data.sizePx}px`,
9148
+ ` Body motion: ${data.motionLevel || '(default: natural)'} @ ${data.motionIntensityPercent ?? 100}%`,
8940
9149
  ].join('\n');
8941
9150
  }
8942
9151
 
@@ -8952,6 +9161,11 @@ async function handleTool(name, args) {
8952
9161
  if (args.showBadge !== undefined) body.showBadge = args.showBadge;
8953
9162
  if (args.sizePx !== undefined) body.sizePx = args.sizePx;
8954
9163
  if (args.allowVisitorCollapse !== undefined) body.allowVisitorCollapse = args.allowVisitorCollapse;
9164
+ if (args.motionLevel !== undefined) body.motionLevel = parseAvatarMotionLevel(args.motionLevel);
9165
+ if (args.motionIntensityPercent !== undefined) {
9166
+ if (!Number.isInteger(args.motionIntensityPercent) || args.motionIntensityPercent < 25 || args.motionIntensityPercent > 150) throw new Error('motionIntensityPercent must be a whole number between 25 and 150.');
9167
+ body.motionIntensityPercent = args.motionIntensityPercent;
9168
+ }
8955
9169
  for (const [arg, max] of [['maxAvatarMinutesPerMonth', 100000], ['maxAvatarTtsChars', 100000000]]) {
8956
9170
  if (args[arg] === undefined) continue;
8957
9171
  if (!Number.isInteger(args[arg]) || args[arg] < 1 || args[arg] > max) throw new Error(`${arg} must be a whole number between 1 and ${max} (never unlimited).`);
@@ -10720,6 +10934,10 @@ async function handleTool(name, args) {
10720
10934
  const r = await messagingGet('/Recruiting/Hosted');
10721
10935
  return JSON.stringify(r, null, 2);
10722
10936
  }
10937
+ case 'jobboard_applicant_accounts_status': {
10938
+ const r = await messagingGet('/Recruiting/Applicants');
10939
+ return JSON.stringify(r, null, 2);
10940
+ }
10723
10941
  case 'jobboard_hosted_set': {
10724
10942
  const body = {};
10725
10943
  if (args.enabled !== undefined) body.enabled = Boolean(args.enabled);
@@ -10758,6 +10976,55 @@ async function handleTool(name, args) {
10758
10976
  return JSON.stringify(r, null, 2);
10759
10977
  }
10760
10978
 
10979
+ case 'jobboard_employers_list': {
10980
+ return JSON.stringify(await messagingGet('/Recruiting/Employers'), null, 2);
10981
+ }
10982
+ case 'jobboard_employer_create': {
10983
+ const name = String(args.name ?? '').trim();
10984
+ if (name.length < 2 || name.length > 200) throw new Error('name must be 2 to 200 characters.');
10985
+ const body = { name };
10986
+ if (args.verifiedDomain !== undefined && String(args.verifiedDomain).trim()) body.verifiedDomain = String(args.verifiedDomain).trim();
10987
+ return JSON.stringify(await messagingPost('/Recruiting/Employers', body), null, 2);
10988
+ }
10989
+ case 'jobboard_employer_invite': {
10990
+ const id = Number(args.employerId);
10991
+ if (!Number.isInteger(id) || id <= 0) throw new Error('employerId must be a positive integer.');
10992
+ const email = String(args.email ?? '').trim();
10993
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(email)) throw new Error('email must be a valid email address.');
10994
+ const role = ['Owner', 'Recruiter', 'Viewer'].find((r) => r.toLowerCase() === String(args.role ?? 'Recruiter').trim().toLowerCase());
10995
+ if (!role) throw new Error('role must be Owner, Recruiter or Viewer.');
10996
+ const body = { email, role };
10997
+ if (args.displayName !== undefined && String(args.displayName).trim()) body.displayName = String(args.displayName).trim();
10998
+ return JSON.stringify(await messagingPost(`/Recruiting/Employers/${id}/Invite`, body), null, 2);
10999
+ }
11000
+ case 'jobboard_employer_suspend': {
11001
+ const id = Number(args.employerId);
11002
+ if (!Number.isInteger(id) || id <= 0) throw new Error('employerId must be a positive integer.');
11003
+ const reason = String(args.reason ?? '').trim();
11004
+ if (reason.length < 3 || reason.length > 500) throw new Error('reason must be 3 to 500 characters.');
11005
+ return JSON.stringify(await messagingPost(`/Recruiting/Employers/${id}/Suspend`, { reason }), null, 2);
11006
+ }
11007
+ case 'jobboard_employer_reactivate': {
11008
+ const id = Number(args.employerId);
11009
+ if (!Number.isInteger(id) || id <= 0) throw new Error('employerId must be a positive integer.');
11010
+ return JSON.stringify(await messagingPost(`/Recruiting/Employers/${id}/Reactivate`, {}), null, 2);
11011
+ }
11012
+ case 'jobboard_moderation_list': {
11013
+ return JSON.stringify(await messagingGet('/Recruiting/Moderation'), null, 2);
11014
+ }
11015
+ case 'jobboard_moderation_approve': {
11016
+ const id = Number(args.listingId);
11017
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
11018
+ return JSON.stringify(await messagingPost(`/Recruiting/Moderation/${id}/Approve`, {}), null, 2);
11019
+ }
11020
+ case 'jobboard_moderation_reject': {
11021
+ const id = Number(args.listingId);
11022
+ if (!Number.isInteger(id) || id <= 0) throw new Error('listingId must be a positive integer.');
11023
+ const reason = String(args.reason ?? '').trim();
11024
+ if (reason.length < 3 || reason.length > 500) throw new Error('reason must be 3 to 500 characters.');
11025
+ return JSON.stringify(await messagingPost(`/Recruiting/Moderation/${id}/Reject`, { reason }), null, 2);
11026
+ }
11027
+
10761
11028
  case 'jobboard_move_application_stage': {
10762
11029
  const id = Number(args.applicationId);
10763
11030
  if (!Number.isInteger(id) || id <= 0) throw new Error('applicationId must be a positive integer.');