@sendinel/mcp-server 1.0.146 → 1.0.147

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 (42) hide show
  1. package/build/lib/action-approvals.js +3 -1
  2. package/build/lib/db-error.d.ts +39 -6
  3. package/build/lib/db-error.js +221 -11
  4. package/build/server.js +5 -3
  5. package/build/tools/ab-testing.js +4 -3
  6. package/build/tools/advisor.js +2 -2
  7. package/build/tools/ai-work.js +14 -13
  8. package/build/tools/analytics.js +26 -26
  9. package/build/tools/approvals.js +3 -1
  10. package/build/tools/automations.js +12 -11
  11. package/build/tools/brand-kit.js +4 -3
  12. package/build/tools/campaign-sets.js +11 -10
  13. package/build/tools/campaigns.js +73 -74
  14. package/build/tools/compound.js +7 -6
  15. package/build/tools/connector.js +4 -3
  16. package/build/tools/contacts.js +44 -45
  17. package/build/tools/content.js +16 -16
  18. package/build/tools/convert-imported.js +8 -7
  19. package/build/tools/data-proposals.js +8 -7
  20. package/build/tools/data.js +39 -38
  21. package/build/tools/delivery-ops.js +32 -32
  22. package/build/tools/drafts.js +10 -9
  23. package/build/tools/forms.js +5 -4
  24. package/build/tools/gdpr.js +10 -9
  25. package/build/tools/landing-pages.js +3 -3
  26. package/build/tools/lists.js +21 -20
  27. package/build/tools/newsletters.js +26 -25
  28. package/build/tools/org.js +26 -23
  29. package/build/tools/segments.js +4 -5
  30. package/build/tools/short-links.js +7 -6
  31. package/build/tools/signatures.js +10 -9
  32. package/build/tools/sites.js +9 -8
  33. package/build/tools/sms.js +14 -13
  34. package/build/tools/social-posts.js +20 -19
  35. package/build/tools/social-replies.js +7 -6
  36. package/build/tools/surveys.js +10 -9
  37. package/build/tools/templates.js +14 -13
  38. package/build/tools/warmup.js +5 -4
  39. package/build/tools/webhooks.js +5 -4
  40. package/package.json +1 -1
  41. package/build/lib/db-errors.d.ts +0 -12
  42. package/build/lib/db-errors.js +0 -20
@@ -1,4 +1,3 @@
1
- import { dbErrorResult } from '../lib/db-errors.js';
2
1
  import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
3
2
  import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
4
3
  import { z } from 'zod';
@@ -13,7 +12,7 @@ import { ensureReplyActionSecretConfigured, mintReplyAction, } from '@sendinel/d
13
12
  import { finishCampaignEdit, resumeCampaignEdit, TRIGGER_EDIT_SELECT } from '../lib/campaign-trigger-approval.js';
14
13
  import { adminDb, db, requestContext } from '../db.js';
15
14
  import { getProjectId } from '../project.js';
16
- import { dbToolError, toolError } from '../lib/db-error.js';
15
+ import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
17
16
  import { checkPlanLimit } from '../plan-limits.js';
18
17
  import { buildVoicePrompt, buildEmailSystemPrompt, deriveTemplateVariables, validateTemplate } from '../lib/voice.js';
19
18
  import { fireWebhookEvent } from '../lib/webhook-events.js';
@@ -296,7 +295,7 @@ export async function applyCampaignTargetingFields(updates, input, projectId, ex
296
295
  .eq('project_id', projectId)
297
296
  .maybeSingle();
298
297
  if (error)
299
- return error.message;
298
+ return dbToolError(error).error;
300
299
  if (!segment)
301
300
  return 'Segment not found';
302
301
  updates.segment_id = input.segment_id;
@@ -436,7 +435,7 @@ async function executeBroadcast(input) {
436
435
  .order('created_at', { ascending: false })
437
436
  .limit(1);
438
437
  if (matchErr) {
439
- return { ok: false, error: matchErr.message, code: 'ONE_SEND_LOOKUP_FAILED' };
438
+ return { ok: false, ...dbToolError(matchErr), code: 'ONE_SEND_LOOKUP_FAILED' };
440
439
  }
441
440
  const match = (matches ?? [])[0];
442
441
  if (match)
@@ -463,7 +462,7 @@ async function executeBroadcast(input) {
463
462
  .select('id')
464
463
  .single();
465
464
  if (campaignErr || !campaign) {
466
- return { ok: false, error: campaignErr?.message ?? 'Failed to create campaign', code: 'CAMPAIGN_CREATE_FAILED' };
465
+ return { ok: false, error: (campaignErr ? dbToolError(campaignErr).error : undefined) ?? 'Failed to create campaign', code: 'CAMPAIGN_CREATE_FAILED' };
467
466
  }
468
467
  const { data: step, error: stepErr } = await db()
469
468
  .from('campaign_steps')
@@ -480,7 +479,7 @@ async function executeBroadcast(input) {
480
479
  .select('id')
481
480
  .single();
482
481
  if (stepErr || !step) {
483
- return { ok: false, error: stepErr?.message ?? 'Campaign created but failed to add broadcast step', code: 'STEP_CREATE_FAILED', campaign_id: campaign.id };
482
+ return { ok: false, error: (stepErr ? dbToolError(stepErr).error : undefined) ?? 'Campaign created but failed to add broadcast step', code: 'STEP_CREATE_FAILED', campaign_id: campaign.id };
484
483
  }
485
484
  await recordBrandAssetUsage({
486
485
  client: db(),
@@ -504,7 +503,7 @@ async function executeBroadcast(input) {
504
503
  .from('campaign_enrollments')
505
504
  .upsert(batch, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true });
506
505
  if (enrollErr) {
507
- return { ok: false, error: enrollErr.message, code: 'ENROLLMENT_FAILED', campaign_id: campaign.id, enrolled_before_failure: i };
506
+ return { ok: false, ...dbToolError(enrollErr), code: 'ENROLLMENT_FAILED', campaign_id: campaign.id, enrolled_before_failure: i };
508
507
  }
509
508
  }
510
509
  const { error: activateErr } = await db()
@@ -513,7 +512,7 @@ async function executeBroadcast(input) {
513
512
  .eq('id', campaign.id)
514
513
  .eq('project_id', input.projectId);
515
514
  if (activateErr) {
516
- return { ok: false, error: activateErr.message, code: 'ACTIVATION_FAILED', campaign_id: campaign.id, enrolled: input.contactIds.length };
515
+ return { ok: false, ...dbToolError(activateErr), code: 'ACTIVATION_FAILED', campaign_id: campaign.id, enrolled: input.contactIds.length };
517
516
  }
518
517
  return { ok: true, campaignId: campaign.id, stepId: step.id, enrolled: input.contactIds.length, nextSendAt };
519
518
  }
@@ -532,7 +531,7 @@ async function attachBroadcastToExisting(input, match) {
532
531
  // One enrollment per campaign/contact, and ids has at most 200 entries.
533
532
  .limit(200);
534
533
  if (enrolledErr)
535
- return { ok: false, error: enrolledErr.message, code: 'ONE_SEND_LOOKUP_FAILED', campaign_id: match.id };
534
+ return { ok: false, ...dbToolError(enrolledErr), code: 'ONE_SEND_LOOKUP_FAILED', campaign_id: match.id };
536
535
  for (const row of enrolledRows ?? [])
537
536
  skip.add(row.contact_id);
538
537
  let logRows;
@@ -565,7 +564,7 @@ async function attachBroadcastToExisting(input, match) {
565
564
  .upsert(batch, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true })
566
565
  .select('contact_id');
567
566
  if (enrollErr) {
568
- return { ok: false, error: enrollErr.message, code: 'ENROLLMENT_FAILED', campaign_id: match.id, enrolled_before_failure: enrolled };
567
+ return { ok: false, ...dbToolError(enrollErr), code: 'ENROLLMENT_FAILED', campaign_id: match.id, enrolled_before_failure: enrolled };
569
568
  }
570
569
  enrolled += (inserted ?? []).length;
571
570
  }
@@ -585,7 +584,7 @@ async function attachBroadcastToExisting(input, match) {
585
584
  .eq('id', match.id)
586
585
  .eq('project_id', input.projectId);
587
586
  if (updateErr) {
588
- return { ok: false, error: updateErr.message, code: 'ACTIVATION_FAILED', campaign_id: match.id, enrolled };
587
+ return { ok: false, ...dbToolError(updateErr), code: 'ACTIVATION_FAILED', campaign_id: match.id, enrolled };
589
588
  }
590
589
  }
591
590
  const { data: steps } = await db()
@@ -674,7 +673,7 @@ export function registerCampaignTools(server) {
674
673
  query = query.eq('site_id', site_id);
675
674
  const { data, error } = await query;
676
675
  if (error)
677
- return j({ error: error.message });
676
+ return j(dbToolError(error));
678
677
  const days = {};
679
678
  for (const campaign of data ?? []) {
680
679
  if (campaign.type === 'triggered') {
@@ -745,7 +744,7 @@ export function registerCampaignTools(server) {
745
744
  q = q.eq('status', status);
746
745
  const { data, error } = await q;
747
746
  if (error)
748
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
747
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
749
748
  // total_opened / total_clicked stay as the raw event counters (unchanged); unique_* are people
750
749
  // counts that cannot exceed total_sent. null = the people-count lookup failed (unknown, not 0).
751
750
  const rows = data ?? [];
@@ -791,7 +790,7 @@ export function registerCampaignTools(server) {
791
790
  .eq('campaigns.site_id', site_id)
792
791
  .order('next_send_at', { ascending: true });
793
792
  if (error)
794
- return txt(`Error: ${error.message}`);
793
+ return txt(`Error: ${dbToolError(error).error}`);
795
794
  const campaigns = groupScheduleByCampaign((data ?? []));
796
795
  const total_sends_scheduled = campaigns.reduce((sum, campaign) => sum + campaign.total_sends_scheduled, 0);
797
796
  return j({
@@ -862,7 +861,7 @@ export function registerCampaignTools(server) {
862
861
  .select()
863
862
  .single();
864
863
  if (error)
865
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
864
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
866
865
  return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION, fires_on: buildCampaignFiresOn(data), trigger_diagnostics: buildCampaignTriggerDiagnostics(data) }, null, 2) }] };
867
866
  });
868
867
  const propertyConditions = z.array(z.union([
@@ -980,7 +979,7 @@ export function registerCampaignTools(server) {
980
979
  .is('delay_days', null)
981
980
  .is('delay_hours', null);
982
981
  if (stepsErr)
983
- return j({ error: `Could not check the campaign's emails: ${stepsErr.message}`, code: 'STEP_LOOKUP_FAILED' });
982
+ return j({ error: `Could not check the campaign's emails: ${dbToolError(stepsErr).error}`, code: 'STEP_LOOKUP_FAILED' });
984
983
  if (unsetSteps?.length) {
985
984
  return j({
986
985
  error: 'Some emails have no wait set. Set how long to wait before each one, then try again.',
@@ -996,7 +995,7 @@ export function registerCampaignTools(server) {
996
995
  .eq('project_id', getProjectId())
997
996
  .maybeSingle();
998
997
  if (campErr)
999
- return j({ error: `Could not check the campaign: ${campErr.message}`, code: 'CAMPAIGN_LOOKUP_FAILED' });
998
+ return j({ error: `Could not check the campaign: ${dbToolError(campErr).error}`, code: 'CAMPAIGN_LOOKUP_FAILED' });
1000
999
  if (campRow?.type === 'triggered' && campaignTriggerKind(campRow) === 'manual' && !campRow.segment_id) {
1001
1000
  return j({ error: 'This triggered campaign has no trigger. Set an event, tag, date, contact event, or segment before activating it.', code: 'validation_failed' });
1002
1001
  }
@@ -1007,7 +1006,7 @@ export function registerCampaignTools(server) {
1007
1006
  .select('campaign_id, step_order, subject, body_html, body_text')
1008
1007
  .eq('campaign_id', campaign_id);
1009
1008
  if (contentErr)
1010
- return j({ error: `Could not check the campaign's emails: ${contentErr.message}`, code: 'STEP_LOOKUP_FAILED' });
1009
+ return j({ error: `Could not check the campaign's emails: ${dbToolError(contentErr).error}`, code: 'STEP_LOOKUP_FAILED' });
1011
1010
  const readiness = await checkCampaignContent(getProjectId(), [campRow], contentSteps ?? []);
1012
1011
  if (readiness.blocking.length)
1013
1012
  return j(contentNotReadyResponse(readiness.blocking));
@@ -1049,7 +1048,7 @@ export function registerCampaignTools(server) {
1049
1048
  .select()
1050
1049
  .single();
1051
1050
  if (error)
1052
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
1051
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
1053
1052
  // Fire webhook for status changes
1054
1053
  const eventMap = { active: 'campaign.activated', completed: 'campaign.completed', paused: 'campaign.paused' };
1055
1054
  if (eventMap[status]) {
@@ -1084,7 +1083,7 @@ export function registerCampaignTools(server) {
1084
1083
  .eq('project_id', getProjectId())
1085
1084
  .maybeSingle();
1086
1085
  if (contactError)
1087
- return txt(`Error: ${contactError.message}`);
1086
+ return txt(`Error: ${dbToolError(contactError).error}`);
1088
1087
  if (!contact)
1089
1088
  return j({ error: 'Contact not found', code: 'NOT_FOUND' });
1090
1089
  if (contact.global_unsubscribed)
@@ -1097,7 +1096,7 @@ export function registerCampaignTools(server) {
1097
1096
  .select()
1098
1097
  .single();
1099
1098
  if (error)
1100
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
1099
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
1101
1100
  // Update campaign enrolled counter
1102
1101
  const { data: campaign } = await db().from('campaigns').select('total_enrolled').eq('id', campaign_id).maybeSingle();
1103
1102
  if (campaign) {
@@ -1135,9 +1134,9 @@ export function registerCampaignTools(server) {
1135
1134
  .maybeSingle(),
1136
1135
  ]);
1137
1136
  if (campaignRes.error)
1138
- return txt(`Error: ${campaignRes.error.message}`);
1137
+ return txt(`Error: ${dbToolError(campaignRes.error).error}`);
1139
1138
  if (contactRes.error)
1140
- return txt(`Error: ${contactRes.error.message}`);
1139
+ return txt(`Error: ${dbToolError(contactRes.error).error}`);
1141
1140
  if (!campaignRes.data || !contactRes.data)
1142
1141
  return j({ error: 'Campaign or contact not found', code: 'NOT_FOUND' });
1143
1142
  const { data: enrollment, error } = await db()
@@ -1147,7 +1146,7 @@ export function registerCampaignTools(server) {
1147
1146
  .eq('contact_id', contact_id)
1148
1147
  .maybeSingle();
1149
1148
  if (error)
1150
- return txt(`Error: ${error.message}`);
1149
+ return txt(`Error: ${dbToolError(error).error}`);
1151
1150
  if (!enrollment) {
1152
1151
  return j({ enrolled: false, enrollment: null, campaign: campaignRes.data, contact: contactRes.data });
1153
1152
  }
@@ -1169,9 +1168,9 @@ export function registerCampaignTools(server) {
1169
1168
  .maybeSingle(),
1170
1169
  ]);
1171
1170
  if (stepsSentError)
1172
- return txt(`Error: ${stepsSentError.message}`);
1171
+ return txt(`Error: ${dbToolError(stepsSentError).error}`);
1173
1172
  if (currentStepResult.error)
1174
- return txt(`Error: ${currentStepResult.error.message}`);
1173
+ return txt(`Error: ${dbToolError(currentStepResult.error).error}`);
1175
1174
  return j({
1176
1175
  enrolled: true,
1177
1176
  campaign: campaignRes.data,
@@ -1207,7 +1206,7 @@ export function registerCampaignTools(server) {
1207
1206
  .eq('site_id', site_id)
1208
1207
  .eq('subscribed', true);
1209
1208
  if (subErr)
1210
- return { content: [{ type: 'text', text: `Error: ${subErr.message}` }] };
1209
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(subErr).error}` }] };
1211
1210
  if (!subs?.length)
1212
1211
  return { content: [{ type: 'text', text: 'No active subscribers for this site.' }] };
1213
1212
  const ids = subs.map((s) => s.contact_id);
@@ -1227,7 +1226,7 @@ export function registerCampaignTools(server) {
1227
1226
  .from('campaign_enrollments')
1228
1227
  .upsert(rows, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true });
1229
1228
  if (enrollErr)
1230
- return { content: [{ type: 'text', text: `Error: ${enrollErr.message}` }] };
1229
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(enrollErr).error}` }] };
1231
1230
  // Update campaign enrolled counter
1232
1231
  const { data: campaign } = await db().from('campaigns').select('total_enrolled').eq('id', campaign_id).maybeSingle();
1233
1232
  if (campaign) {
@@ -1275,7 +1274,7 @@ export function registerCampaignTools(server) {
1275
1274
  .eq('project_id', projectId)
1276
1275
  .maybeSingle();
1277
1276
  if (siteErr || !site)
1278
- return j({ error: siteErr?.message ?? 'Site not found', code: 'SITE_NOT_FOUND' });
1277
+ return j({ error: (siteErr ? dbToolError(siteErr).error : undefined) ?? 'Site not found', code: 'SITE_NOT_FOUND' });
1279
1278
  const { data: segment, error: segmentErr } = await db()
1280
1279
  .from('segments')
1281
1280
  .select('id, name, rules, site_id')
@@ -1283,7 +1282,7 @@ export function registerCampaignTools(server) {
1283
1282
  .eq('project_id', projectId)
1284
1283
  .maybeSingle();
1285
1284
  if (segmentErr || !segment)
1286
- return j({ error: segmentErr?.message ?? 'Segment not found', code: 'SEGMENT_NOT_FOUND' });
1285
+ return j({ error: (segmentErr ? dbToolError(segmentErr).error : undefined) ?? 'Segment not found', code: 'SEGMENT_NOT_FOUND' });
1287
1286
  const rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
1288
1287
  // #3669: the same audience rules as send_broadcast and the dashboard: paged, signed up to this
1289
1288
  // sender, consent gate applied. The count shown is the count enrolled.
@@ -1295,7 +1294,7 @@ export function registerCampaignTools(server) {
1295
1294
  // #4270: a malformed segment enrolls nobody and says why.
1296
1295
  if (isSegmentRulesError(error))
1297
1296
  return j({ error: error.message, code: 'INVALID_SEGMENT_RULES', errors: error.errors });
1298
- return j({ error: `Could not load this audience: ${error instanceof Error ? error.message : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
1297
+ return j({ error: `Could not load this audience: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
1299
1298
  }
1300
1299
  const contactIds = audience.contactIds;
1301
1300
  if (!confirmed) {
@@ -1437,7 +1436,7 @@ export function registerCampaignTools(server) {
1437
1436
  .eq('action', 'send_broadcast')
1438
1437
  .maybeSingle();
1439
1438
  if (rowErr)
1440
- return j({ error: rowErr.message, code: 'APPROVAL_LOOKUP_FAILED' });
1439
+ return j({ ...dbToolError(rowErr), code: 'APPROVAL_LOOKUP_FAILED' });
1441
1440
  if (!row)
1442
1441
  return txt(`Approval ${approval_id} is not_found. Request a new approval.`);
1443
1442
  const stored = (row.params ?? {});
@@ -1517,7 +1516,7 @@ export function registerCampaignTools(server) {
1517
1516
  .select('id, name, from_email')
1518
1517
  .eq('project_id', projectId);
1519
1518
  if (sitesErr)
1520
- return j({ error: sitesErr.message, code: 'SITE_LOOKUP_FAILED' });
1519
+ return j({ ...dbToolError(sitesErr), code: 'SITE_LOOKUP_FAILED' });
1521
1520
  const site = site_id ? (sites ?? []).find((s) => s.id === site_id) : (sites?.length === 1 ? sites[0] : null);
1522
1521
  if (!site) {
1523
1522
  return j({
@@ -1540,7 +1539,7 @@ export function registerCampaignTools(server) {
1540
1539
  .eq('project_id', projectId)
1541
1540
  .maybeSingle();
1542
1541
  if (segmentErr)
1543
- return j({ error: segmentErr.message, code: 'SEGMENT_LOOKUP_FAILED' });
1542
+ return j({ ...dbToolError(segmentErr), code: 'SEGMENT_LOOKUP_FAILED' });
1544
1543
  if (!segment)
1545
1544
  return j({ error: 'Segment not found', code: 'SEGMENT_NOT_FOUND' });
1546
1545
  rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
@@ -1551,7 +1550,7 @@ export function registerCampaignTools(server) {
1551
1550
  rules = await parseAudienceRules(anthropic, audience);
1552
1551
  }
1553
1552
  catch (error) {
1554
- return j({ error: error instanceof Error ? error.message : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
1553
+ return j({ error: error instanceof Error ? dbToolError(error).error : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
1555
1554
  }
1556
1555
  segmentName = audience;
1557
1556
  }
@@ -1564,7 +1563,7 @@ export function registerCampaignTools(server) {
1564
1563
  // #4270: malformed segment or parsed audience rules send to nobody and say why.
1565
1564
  if (isSegmentRulesError(error))
1566
1565
  return j({ error: error.message, code: 'INVALID_SEGMENT_RULES', errors: error.errors, rules });
1567
- return j({ error: `Could not load this audience: ${error instanceof Error ? error.message : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
1566
+ return j({ error: `Could not load this audience: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
1568
1567
  }
1569
1568
  if (resolved.contactIds.length === 0) {
1570
1569
  await logRefusedSend('send_broadcast', site.id, sendClass, resolved, { audience: segment_id ? { segment_id } : { inline_rules: true } });
@@ -1577,7 +1576,7 @@ export function registerCampaignTools(server) {
1577
1576
  content = { subject: subject ?? generated.subject, body_html: generated.body_html, body_text: generated.body_text, blocks: generated.blocks };
1578
1577
  }
1579
1578
  catch (error) {
1580
- return j({ error: error instanceof Error ? error.message : String(error), code: 'DRAFT_FAILED' });
1579
+ return j({ error: error instanceof Error ? dbToolError(error).error : String(error), code: 'DRAFT_FAILED' });
1581
1580
  }
1582
1581
  }
1583
1582
  else {
@@ -1717,7 +1716,7 @@ export function registerCampaignTools(server) {
1717
1716
  query = query.eq('category', category);
1718
1717
  const { data, error } = await query;
1719
1718
  if (error)
1720
- return txt(`Error: ${error.message}`);
1719
+ return txt(`Error: ${dbToolError(error).error}`);
1721
1720
  return j(data ?? []);
1722
1721
  });
1723
1722
  server.registerTool('clone_sequence_template', {
@@ -1742,7 +1741,7 @@ export function registerCampaignTools(server) {
1742
1741
  .eq('id', template_id)
1743
1742
  .maybeSingle();
1744
1743
  if (templateError)
1745
- return txt(`Error: ${templateError.message}`);
1744
+ return txt(`Error: ${dbToolError(templateError).error}`);
1746
1745
  if (!template)
1747
1746
  return txt('Error: Template not found');
1748
1747
  const limit = await checkPlanLimit('active_campaigns');
@@ -1757,7 +1756,7 @@ export function registerCampaignTools(server) {
1757
1756
  .eq('project_id', projectId)
1758
1757
  .maybeSingle();
1759
1758
  if (targetSiteError)
1760
- return txt(`Error: ${targetSiteError.message}`);
1759
+ return txt(`Error: ${dbToolError(targetSiteError).error}`);
1761
1760
  if (!targetSite)
1762
1761
  return txt('Error: Site not found for this project');
1763
1762
  }
@@ -1771,7 +1770,7 @@ export function registerCampaignTools(server) {
1771
1770
  .limit(1)
1772
1771
  .maybeSingle();
1773
1772
  if (siteError)
1774
- return txt(`Error: ${siteError.message}`);
1773
+ return txt(`Error: ${dbToolError(siteError).error}`);
1775
1774
  targetSiteId = site?.id ?? null;
1776
1775
  }
1777
1776
  if (!targetSiteId) {
@@ -1790,7 +1789,7 @@ export function registerCampaignTools(server) {
1790
1789
  .select()
1791
1790
  .single();
1792
1791
  if (campaignError)
1793
- return txt(`Error creating campaign: ${campaignError.message}`);
1792
+ return txt(`Error creating campaign: ${dbToolError(campaignError).error}`);
1794
1793
  const steps = Array.isArray(template.template?.steps) ? template.template.steps : [];
1795
1794
  if (steps.length > 0) {
1796
1795
  const { error: stepsError } = await db()
@@ -1807,7 +1806,7 @@ export function registerCampaignTools(server) {
1807
1806
  condition_lookback_hours: step.condition_lookback_hours ?? null,
1808
1807
  })));
1809
1808
  if (stepsError)
1810
- return txt(`Campaign created but steps failed: ${stepsError.message}`);
1809
+ return txt(`Campaign created but steps failed: ${dbToolError(stepsError).error}`);
1811
1810
  }
1812
1811
  await logAudit({
1813
1812
  action: 'sequence_template.cloned',
@@ -2026,7 +2025,7 @@ Adapt your tone accordingly:
2026
2025
  .select('id')
2027
2026
  .single();
2028
2027
  if (segErr)
2029
- return txt(`Error creating segment: ${segErr.message}`);
2028
+ return txt(`Error creating segment: ${dbToolError(segErr).error}`);
2030
2029
  segId = seg.id;
2031
2030
  }
2032
2031
  const campLimit = await checkPlanLimit('active_campaigns');
@@ -2038,7 +2037,7 @@ Adapt your tone accordingly:
2038
2037
  .select('id')
2039
2038
  .single();
2040
2039
  if (campErr)
2041
- return txt(`Error creating campaign: ${campErr.message}`);
2040
+ return txt(`Error creating campaign: ${dbToolError(campErr).error}`);
2042
2041
  const steps = [];
2043
2042
  for (let i = 0; i < plan.steps.length; i++) {
2044
2043
  const planned = plan.steps[i];
@@ -2053,7 +2052,7 @@ Adapt your tone accordingly:
2053
2052
  .select('id, step_order, subject')
2054
2053
  .single();
2055
2054
  if (stepErr)
2056
- return txt(`Error adding step ${i}: ${stepErr.message}`);
2055
+ return txt(`Error adding step ${i}: ${dbToolError(stepErr).error}`);
2057
2056
  steps.push(step);
2058
2057
  }
2059
2058
  const enrollRows = plan.contact_ids.map((id) => ({ campaign_id: campaign.id, contact_id: id, next_send_at: new Date().toISOString() }));
@@ -2087,7 +2086,7 @@ Adapt your tone accordingly:
2087
2086
  .eq('action', 'launch_campaign')
2088
2087
  .maybeSingle();
2089
2088
  if (rowErr)
2090
- return txt(`Error reading approval ${approval_id}: ${rowErr.message}`);
2089
+ return txt(`Error reading approval ${approval_id}: ${dbToolError(rowErr).error}`);
2091
2090
  if (!row)
2092
2091
  return txt(`Approval ${approval_id} is not_found. Request a new approval.`);
2093
2092
  const stored = (row.params ?? {});
@@ -2682,7 +2681,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2682
2681
  ensureReplyActionSecretConfigured();
2683
2682
  }
2684
2683
  catch (err) {
2685
- return txt(`Error: ${err instanceof Error ? err.message : 'Reply action secret is not configured'}`);
2684
+ return txt(`Error: ${err instanceof Error ? dbToolError(err).error : 'Reply action secret is not configured'}`);
2686
2685
  }
2687
2686
  // Suppression check
2688
2687
  const { data: suppression } = await db()
@@ -2732,7 +2731,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2732
2731
  .eq('project_id', projectId)
2733
2732
  .maybeSingle();
2734
2733
  if (templateError)
2735
- return txt(`Error: ${templateError.message}`);
2734
+ return txt(`Error: ${dbToolError(templateError).error}`);
2736
2735
  if (!template)
2737
2736
  return txt('Error: Template not found');
2738
2737
  const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
@@ -2814,7 +2813,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2814
2813
  }
2815
2814
  }
2816
2815
  if (logError || !logRow?.id) {
2817
- return txt(`Error: ${logError?.message ?? 'Failed to create email log row'}`);
2816
+ return txt(`Error: ${(logError ? dbToolError(logError).error : undefined) ?? 'Failed to create email log row'}`);
2818
2817
  }
2819
2818
  let minted;
2820
2819
  try {
@@ -2853,7 +2852,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2853
2852
  }
2854
2853
  catch (error) {
2855
2854
  await db().from('email_log').update({ status: 'failed' }).eq('id', logRow.id);
2856
- return txt(`Error: Send failed — ${error instanceof Error ? error.message : 'unknown provider error'}`);
2855
+ return txt(`Error: Send failed — ${error instanceof Error ? dbToolError(error).error : 'unknown provider error'}`);
2857
2856
  }
2858
2857
  await db().from('email_log').update({
2859
2858
  status: 'sent',
@@ -2896,7 +2895,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2896
2895
  .limit(1)
2897
2896
  .maybeSingle();
2898
2897
  if (error)
2899
- return txt(`Error: ${error.message}`);
2898
+ return txt(`Error: ${dbToolError(error).error}`);
2900
2899
  if (!data)
2901
2900
  return j({ found: false, correlation_id });
2902
2901
  const outcome = data.status === 'pending' ? 'awaiting'
@@ -2929,7 +2928,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2929
2928
  .order('created_at', { ascending: false })
2930
2929
  .limit(limit);
2931
2930
  if (error)
2932
- return txt(`Error: ${error.message}`);
2931
+ return txt(`Error: ${dbToolError(error).error}`);
2933
2932
  const items = (data ?? []).map((row) => ({
2934
2933
  ...row,
2935
2934
  outcome: row.status === 'pending' ? 'awaiting'
@@ -3230,7 +3229,7 @@ Return ONLY valid JSON with this exact shape:
3230
3229
  .select()
3231
3230
  .single();
3232
3231
  if (campErr)
3233
- return txt(`Failed to create campaign: ${campErr.message}`);
3232
+ return txt(`Failed to create campaign: ${dbToolError(campErr).error}`);
3234
3233
  // 2. Add steps
3235
3234
  const stepInserts = steps.map((s, i) => ({
3236
3235
  campaign_id: campaign.id,
@@ -3246,7 +3245,7 @@ Return ONLY valid JSON with this exact shape:
3246
3245
  .from('campaign_steps')
3247
3246
  .insert(stepInserts);
3248
3247
  if (stepErr)
3249
- return txt(`Campaign created (${campaign.id}) but failed to add steps: ${stepErr.message}`);
3248
+ return txt(`Campaign created (${campaign.id}) but failed to add steps: ${dbToolError(stepErr).error}`);
3250
3249
  // 3. Enroll segment if provided
3251
3250
  let enrolled = 0;
3252
3251
  if (segment_id) {
@@ -3335,14 +3334,14 @@ Return ONLY valid JSON with this exact shape:
3335
3334
  .eq('project_id', projectId)
3336
3335
  .maybeSingle();
3337
3336
  if (campaignErr || !campaign)
3338
- return j({ error: campaignErr?.message ?? 'Campaign not found', code: 'NOT_FOUND' });
3337
+ return j({ error: (campaignErr ? dbToolError(campaignErr).error : undefined) ?? 'Campaign not found', code: 'NOT_FOUND' });
3339
3338
  const { data, error } = await db()
3340
3339
  .from('campaign_steps')
3341
3340
  .select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, created_at, updated_at')
3342
3341
  .eq('campaign_id', campaign_id)
3343
3342
  .order('step_order', { ascending: true });
3344
3343
  if (error)
3345
- return j({ error: error.message });
3344
+ return j(dbToolError(error));
3346
3345
  return j({ steps: data ?? [], count: data?.length ?? 0 });
3347
3346
  });
3348
3347
  server.registerTool('update_campaign_step', {
@@ -3391,7 +3390,7 @@ Return ONLY valid JSON with this exact shape:
3391
3390
  .eq('id', step_id)
3392
3391
  .maybeSingle();
3393
3392
  if (stepErr || !step)
3394
- return j({ error: stepErr?.message ?? 'Step not found', code: 'NOT_FOUND' });
3393
+ return j({ error: (stepErr ? dbToolError(stepErr).error : undefined) ?? 'Step not found', code: 'NOT_FOUND' });
3395
3394
  const { data: campaign } = await db()
3396
3395
  .from('campaigns')
3397
3396
  .select('id')
@@ -3424,7 +3423,7 @@ Return ONLY valid JSON with this exact shape:
3424
3423
  .select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, updated_at')
3425
3424
  .single();
3426
3425
  if (error)
3427
- return j({ error: error.message });
3426
+ return j(dbToolError(error));
3428
3427
  await logAudit({ action: 'campaign.step_updated', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
3429
3428
  return j({ step: data, ok: true });
3430
3429
  });
@@ -3463,7 +3462,7 @@ Return ONLY valid JSON with this exact shape:
3463
3462
  .delete()
3464
3463
  .eq('id', step_id);
3465
3464
  if (error)
3466
- return j({ error: error.message });
3465
+ return j(dbToolError(error));
3467
3466
  return j({ ok: true, deleted: step_id });
3468
3467
  }));
3469
3468
  server.registerTool('delete_campaign', {
@@ -3495,7 +3494,7 @@ Return ONLY valid JSON with this exact shape:
3495
3494
  .eq('id', campaign_id)
3496
3495
  .eq('project_id', projectId);
3497
3496
  if (error)
3498
- return j({ error: error.message });
3497
+ return j(dbToolError(error));
3499
3498
  return j({ success: true, deleted: campaign.name });
3500
3499
  }));
3501
3500
  server.registerTool('set_campaign_trigger', {
@@ -3524,7 +3523,7 @@ Return ONLY valid JSON with this exact shape:
3524
3523
  .select(TRIGGER_EDIT_SELECT)
3525
3524
  .eq('id', args.campaign_id).eq('project_id', projectId).maybeSingle();
3526
3525
  if (loadError)
3527
- return j({ error: loadError.message });
3526
+ return j(dbToolError(loadError));
3528
3527
  if (!existing)
3529
3528
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
3530
3529
  if (!['draft', 'active', 'paused'].includes(existing.status)) {
@@ -3618,7 +3617,7 @@ Return ONLY valid JSON with this exact shape:
3618
3617
  if (send_at !== undefined && send_at !== null) {
3619
3618
  const { data: existing, error: fetchError } = await getExisting();
3620
3619
  if (fetchError)
3621
- return j({ error: fetchError.message });
3620
+ return j(dbToolError(fetchError));
3622
3621
  if (!existing)
3623
3622
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
3624
3623
  campaignType = existing.type;
@@ -3626,7 +3625,7 @@ Return ONLY valid JSON with this exact shape:
3626
3625
  if (type !== undefined) {
3627
3626
  const { data: existing, error: fetchError } = await getExisting();
3628
3627
  if (fetchError)
3629
- return j({ error: fetchError.message });
3628
+ return j(dbToolError(fetchError));
3630
3629
  if (!existing)
3631
3630
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
3632
3631
  if (type !== existing.type) {
@@ -3641,7 +3640,7 @@ Return ONLY valid JSON with this exact shape:
3641
3640
  if (hasTriggerUpdate) {
3642
3641
  const { data: existing, error: fetchError } = await getExisting();
3643
3642
  if (fetchError)
3644
- return j({ error: fetchError.message });
3643
+ return j(dbToolError(fetchError));
3645
3644
  if (!existing)
3646
3645
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
3647
3646
  existingCampaignForTargeting = existing;
@@ -3697,7 +3696,7 @@ Return ONLY valid JSON with this exact shape:
3697
3696
  const result = await db().from('campaigns').update({ ...updates, updated_at: new Date().toISOString() })
3698
3697
  .eq('id', campaign_id).eq('project_id', projectId).select().single();
3699
3698
  if (result.error)
3700
- return j({ error: result.error.message });
3699
+ return j(dbToolError(result.error));
3701
3700
  data = result.data;
3702
3701
  await logAudit({ action: 'campaign.updated', resourceType: 'campaign', resourceId: campaign_id, details: { updates } });
3703
3702
  }
@@ -3730,7 +3729,7 @@ Return ONLY valid JSON with this exact shape:
3730
3729
  .eq('project_id', projectId)
3731
3730
  .maybeSingle();
3732
3731
  if (campErr || !campaign)
3733
- return j({ error: campErr?.message ?? 'Campaign not found', code: 'NOT_FOUND' });
3732
+ return j({ error: (campErr ? dbToolError(campErr).error : undefined) ?? 'Campaign not found', code: 'NOT_FOUND' });
3734
3733
  const [{ data: steps, error: stepsError }, { count: enrollCount, error: enrollError }, { count: sentCount, error: sentError }, { count: queuedEmailCount, error: queuedEmailError }, { count: failedSendCount, error: failedSendError }, { data: queueRows, error: queueError }, { count: dlqUnresolvedCount, error: dlqError },] = await Promise.all([
3735
3734
  db()
3736
3735
  .from('campaign_steps')
@@ -3775,7 +3774,7 @@ Return ONLY valid JSON with this exact shape:
3775
3774
  ]);
3776
3775
  const visibilityError = stepsError ?? enrollError ?? sentError ?? queuedEmailError ?? failedSendError ?? queueError ?? dlqError;
3777
3776
  if (visibilityError)
3778
- return j({ error: visibilityError.message, code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
3777
+ return j({ ...dbToolError(visibilityError), code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
3779
3778
  const periods = await readCampaignPeriods(db(), projectId, campaign);
3780
3779
  const outstandingRows = queueRows ?? [];
3781
3780
  const pendingOrRetryRows = outstandingRows.filter((row) => row.status !== 'processing');
@@ -3840,7 +3839,7 @@ Return ONLY valid JSON with this exact shape:
3840
3839
  .eq('campaign_id', campaign_id)
3841
3840
  .eq('contact_id', contact_id);
3842
3841
  if (error)
3843
- return j({ error: error.message });
3842
+ return j(dbToolError(error));
3844
3843
  return j({ success: true });
3845
3844
  });
3846
3845
  // #3348 - campaign branching and goals were fully built (schema, engine, dashboard API)
@@ -3869,7 +3868,7 @@ Return ONLY valid JSON with this exact shape:
3869
3868
  query = query.eq('from_step_id', step_id);
3870
3869
  const { data, error } = await query;
3871
3870
  if (error)
3872
- return j({ error: error.message });
3871
+ return j(dbToolError(error));
3873
3872
  return j({ campaign_id, branches: data ?? [], count: (data ?? []).length });
3874
3873
  });
3875
3874
  server.registerTool('add_campaign_branch', {
@@ -3941,7 +3940,7 @@ Return ONLY valid JSON with this exact shape:
3941
3940
  .eq('id', branch_id)
3942
3941
  .eq('campaign_id', campaign_id);
3943
3942
  if (error)
3944
- return j({ error: error.message });
3943
+ return j(dbToolError(error));
3945
3944
  await logAudit({ action: 'remove_campaign_branch', resourceType: 'campaign', resourceId: campaign_id, details: { branch_id } }).catch(() => { });
3946
3945
  return j({ success: true, removed: branch_id });
3947
3946
  });
@@ -3959,7 +3958,7 @@ Return ONLY valid JSON with this exact shape:
3959
3958
  .eq('campaign_id', campaign_id)
3960
3959
  .order('created_at', { ascending: true });
3961
3960
  if (error)
3962
- return j({ error: error.message });
3961
+ return j(dbToolError(error));
3963
3962
  return j({ campaign_id, goals: data ?? [], count: (data ?? []).length });
3964
3963
  });
3965
3964
  server.registerTool('set_campaign_goal', {
@@ -3995,7 +3994,7 @@ Return ONLY valid JSON with this exact shape:
3995
3994
  .select('id, goal_type, goal_value, action, target_campaign_id')
3996
3995
  .single();
3997
3996
  if (error)
3998
- return j({ error: error.message });
3997
+ return j(dbToolError(error));
3999
3998
  await logAudit({ action: 'set_campaign_goal', resourceType: 'campaign', resourceId: campaign_id, details: { goal_id: data?.id, goal_type, goal_action: resolvedAction } }).catch(() => { });
4000
3999
  return j({ success: true, goal: data });
4001
4000
  });
@@ -4013,7 +4012,7 @@ Return ONLY valid JSON with this exact shape:
4013
4012
  .eq('id', goal_id)
4014
4013
  .eq('campaign_id', campaign_id);
4015
4014
  if (error)
4016
- return j({ error: error.message });
4015
+ return j(dbToolError(error));
4017
4016
  await logAudit({ action: 'remove_campaign_goal', resourceType: 'campaign', resourceId: campaign_id, details: { goal_id } }).catch(() => { });
4018
4017
  return j({ success: true, removed: goal_id });
4019
4018
  });