@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
@@ -229,7 +229,7 @@ export function registerAnalyticsTools(server) {
229
229
  query = query.eq('site_id', site_id);
230
230
  const { data, error } = await query;
231
231
  if (error)
232
- return j({ error: error.message });
232
+ return j(dbToolError(error));
233
233
  return j({ events: data ?? [] });
234
234
  });
235
235
  server.registerTool('get_portfolio_analytics', {
@@ -288,9 +288,9 @@ export function registerAnalyticsTools(server) {
288
288
  .eq('role', 'owner'),
289
289
  ]);
290
290
  if (directError)
291
- return txt(`Error: ${directError.message}`);
291
+ return txt(`Error: ${dbToolError(directError).error}`);
292
292
  if (ownerError)
293
- return txt(`Error: ${ownerError.message}`);
293
+ return txt(`Error: ${dbToolError(ownerError).error}`);
294
294
  const directProjectIds = Array.from(new Set((directAccess ?? []).map((row) => row.project_id).filter(Boolean)));
295
295
  const ownedOrgIds = Array.from(new Set((ownedOrgs ?? []).map((row) => row.org_id).filter(Boolean)));
296
296
  const [{ data: directProjects, error: directProjectsError }, { data: ownedProjects, error: ownedProjectsError }] = await Promise.all([
@@ -302,9 +302,9 @@ export function registerAnalyticsTools(server) {
302
302
  : Promise.resolve({ data: [], error: null }),
303
303
  ]);
304
304
  if (directProjectsError)
305
- return txt(`Error: ${directProjectsError.message}`);
305
+ return txt(`Error: ${dbToolError(directProjectsError).error}`);
306
306
  if (ownedProjectsError)
307
- return txt(`Error: ${ownedProjectsError.message}`);
307
+ return txt(`Error: ${dbToolError(ownedProjectsError).error}`);
308
308
  const byId = new Map();
309
309
  for (const project of [...(directProjects ?? []), ...(ownedProjects ?? [])]) {
310
310
  if (ctx.invitationProjectIds != null && !ctx.invitationProjectIds.includes(project.id))
@@ -316,7 +316,7 @@ export function registerAnalyticsTools(server) {
316
316
  ? await adminDb().from('organizations').select('id, name').in('id', orgIds)
317
317
  : { data: [], error: null };
318
318
  if (organizationsError)
319
- return txt(`Error: ${organizationsError.message}`);
319
+ return txt(`Error: ${dbToolError(organizationsError).error}`);
320
320
  const orgNames = new Map((organizations ?? []).map((org) => [org.id, org.name]));
321
321
  return j({
322
322
  projects: Array.from(byId.values())
@@ -353,7 +353,7 @@ export function registerAnalyticsTools(server) {
353
353
  .eq('org_id', project.org_id)
354
354
  .order('name');
355
355
  if (listErr)
356
- return txt(`Error: ${listErr.message}`);
356
+ return txt(`Error: ${dbToolError(listErr).error}`);
357
357
  // Get org info
358
358
  const { data: org } = await adminDb()
359
359
  .from('organizations')
@@ -396,7 +396,7 @@ export function registerAnalyticsTools(server) {
396
396
  p_project_id: project_id,
397
397
  });
398
398
  if (accessError)
399
- return j({ error: accessError.message });
399
+ return j(dbToolError(accessError));
400
400
  if (!hasAccess)
401
401
  return j({ error: 'You do not have access to this project.', code: 'PROJECT_ACCESS_DENIED' });
402
402
  if (ctx.invitationProjectIds != null && !ctx.invitationProjectIds.includes(project_id.toLowerCase())) {
@@ -408,7 +408,7 @@ export function registerAnalyticsTools(server) {
408
408
  .update({ active_project_id: project_id })
409
409
  .eq('id', ctx.oauthTokenId);
410
410
  if (updateError)
411
- return j({ error: updateError.message });
411
+ return j(dbToolError(updateError));
412
412
  const { data: project } = await adminDb()
413
413
  .from('projects')
414
414
  .select('id, name, org_id')
@@ -445,7 +445,7 @@ export function registerAnalyticsTools(server) {
445
445
  .eq('project_id', getProjectId())
446
446
  .order('name');
447
447
  if (error)
448
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
448
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
449
449
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
450
450
  });
451
451
  server.registerTool('get_stats', {
@@ -591,7 +591,7 @@ export function registerAnalyticsTools(server) {
591
591
  .order('check_date', { ascending: false })
592
592
  .limit(days);
593
593
  if (error)
594
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
594
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
595
595
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
596
596
  });
597
597
  server.registerTool('get_engagement_insights', {
@@ -611,7 +611,7 @@ export function registerAnalyticsTools(server) {
611
611
  const { data, error } = await db()
612
612
  .rpc('get_engagement_summary', site_id ? { p_project_id: projectId, p_site_id: site_id } : { p_project_id: projectId });
613
613
  if (error)
614
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
614
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
615
615
  let trackingEnabled = true;
616
616
  if (site_id) {
617
617
  const { data: site, error: siteError } = await db()
@@ -621,7 +621,7 @@ export function registerAnalyticsTools(server) {
621
621
  .eq('id', site_id)
622
622
  .maybeSingle();
623
623
  if (siteError)
624
- return { content: [{ type: 'text', text: `Error: ${siteError.message}` }] };
624
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(siteError).error}` }] };
625
625
  trackingEnabled = site?.tracking_enabled ?? false;
626
626
  }
627
627
  else {
@@ -630,7 +630,7 @@ export function registerAnalyticsTools(server) {
630
630
  .select('tracking_enabled')
631
631
  .eq('project_id', projectId);
632
632
  if (sitesError)
633
- return { content: [{ type: 'text', text: `Error: ${sitesError.message}` }] };
633
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(sitesError).error}` }] };
634
634
  trackingEnabled = (sites ?? []).some((site) => site.tracking_enabled);
635
635
  }
636
636
  const since30d = new Date(Date.now() - 30 * 86_400_000).toISOString();
@@ -644,7 +644,7 @@ export function registerAnalyticsTools(server) {
644
644
  q = q.eq('site_id', site_id);
645
645
  const { data: logs, error: logsError } = await q;
646
646
  if (logsError)
647
- return { content: [{ type: 'text', text: `Error: ${logsError.message}` }] };
647
+ return { content: [{ type: 'text', text: `Error: ${dbToolError(logsError).error}` }] };
648
648
  const rows = (logs ?? []);
649
649
  const metrics30d = computeAnalyticsMetrics(rows);
650
650
  const metrics7d = computeAnalyticsMetrics(rows.filter(row => row.sent_at && row.sent_at >= since7d));
@@ -714,7 +714,7 @@ export function registerAnalyticsTools(server) {
714
714
  .eq('project_id', projectId)
715
715
  .maybeSingle();
716
716
  if (siteErr || !site)
717
- return txt(`Error: Site not found — ${siteErr?.message ?? 'not found'}`);
717
+ return txt(`Error: Site not found — ${(siteErr ? dbToolError(siteErr).error : undefined) ?? 'not found'}`);
718
718
  const domain = site.sending_domain;
719
719
  const dnsResults = { spf: null, dkim: null, dmarc: null };
720
720
  let verificationSource = 'dns_lookup';
@@ -884,7 +884,7 @@ export function registerAnalyticsTools(server) {
884
884
  q = q.eq('campaign_id', campaign_id);
885
885
  const { data: logs, error } = await q;
886
886
  if (error)
887
- return txt(`Error: ${error.message}`);
887
+ return txt(`Error: ${dbToolError(error).error}`);
888
888
  let periodCampaigns;
889
889
  try {
890
890
  periodCampaigns = campaign_id || group_by === 'campaign' ? await readPeriodCampaigns(db(), projectId, { siteId: site_id, campaignId: campaign_id, start: since }) : new Map();
@@ -898,7 +898,7 @@ export function registerAnalyticsTools(server) {
898
898
  return j({ message: `No email activity in the last ${days} days.`, total: 0 });
899
899
  const { data: attributionRows, error: attributionError } = await readRevenue({ sb: db(), projectId, siteId: site_id, campaignId: campaign_id, start: since });
900
900
  if (attributionError)
901
- return txt(`Error: ${attributionError.message}`);
901
+ return txt(`Error: ${dbToolError(attributionError).error}`);
902
902
  const scopedAttributionRows = attributionRows ?? [];
903
903
  const computeMetrics = (set, counts) => {
904
904
  const sharedCounts = counts ?? countEmailRows(set);
@@ -1154,9 +1154,9 @@ export function registerAnalyticsTools(server) {
1154
1154
  }
1155
1155
  const [subscriptionRes, invoiceRes] = await Promise.all([subscriptionQuery, invoiceQuery]);
1156
1156
  if (subscriptionRes.error)
1157
- return j({ error: subscriptionRes.error.message });
1157
+ return j(dbToolError(subscriptionRes.error));
1158
1158
  if (invoiceRes.error)
1159
- return j({ error: invoiceRes.error.message });
1159
+ return j(dbToolError(invoiceRes.error));
1160
1160
  return j(buildRevenueAnalytics({
1161
1161
  days,
1162
1162
  start,
@@ -1185,14 +1185,14 @@ export function registerAnalyticsTools(server) {
1185
1185
  .eq('project_id', getProjectId())
1186
1186
  .maybeSingle();
1187
1187
  if (siteErr || !site)
1188
- return j({ error: siteErr?.message ?? 'Site not found', code: 'NOT_FOUND' });
1188
+ return j({ error: (siteErr ? dbToolError(siteErr).error : undefined) ?? 'Site not found', code: 'NOT_FOUND' });
1189
1189
  const { data, error } = await db()
1190
1190
  .from('tenant_email_metrics')
1191
1191
  .select('site_id, window_30d_sent, window_30d_complaints, window_30d_opens, complaint_rate, pause_level, paused_at, pause_reason, updated_at')
1192
1192
  .eq('site_id', siteId)
1193
1193
  .maybeSingle();
1194
1194
  if (error)
1195
- return j({ error: error.message });
1195
+ return j(dbToolError(error));
1196
1196
  const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
1197
1197
  const { count: reputationSignals30d, error: reputationErr } = await db()
1198
1198
  .from('email_events')
@@ -1202,7 +1202,7 @@ export function registerAnalyticsTools(server) {
1202
1202
  .eq('bounce_category', 'reputation_signal')
1203
1203
  .gte('occurred_at', since);
1204
1204
  if (reputationErr)
1205
- return j({ error: reputationErr.message });
1205
+ return j(dbToolError(reputationErr));
1206
1206
  return j({
1207
1207
  site_id: siteId,
1208
1208
  metrics: data ?? null,
@@ -1231,7 +1231,7 @@ export function registerAnalyticsTools(server) {
1231
1231
  .eq('id', site_id)
1232
1232
  .maybeSingle();
1233
1233
  if (siteError)
1234
- return txt(`Error: ${siteError.message}`);
1234
+ return txt(`Error: ${dbToolError(siteError).error}`);
1235
1235
  if (!site)
1236
1236
  return j({ error: 'Site not found', code: 'NOT_FOUND' });
1237
1237
  let q = db()
@@ -1245,7 +1245,7 @@ export function registerAnalyticsTools(server) {
1245
1245
  q = q.is('dismissed_at', null);
1246
1246
  const { data, error } = await q;
1247
1247
  if (error)
1248
- return txt(`Error: ${error.message}`);
1248
+ return txt(`Error: ${dbToolError(error).error}`);
1249
1249
  const rank = { critical: 0, warning: 1, info: 2 };
1250
1250
  return j((data ?? []).sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)));
1251
1251
  });
@@ -1276,7 +1276,7 @@ export function registerAnalyticsTools(server) {
1276
1276
  q = q.eq('status', p.status);
1277
1277
  const { data, error } = await q;
1278
1278
  if (error)
1279
- return txt(`Error: ${error.message}`);
1279
+ return txt(`Error: ${dbToolError(error).error}`);
1280
1280
  return j({ campaigns: data ?? [] });
1281
1281
  });
1282
1282
  }
@@ -53,7 +53,9 @@ export function registerApprovalTools(server, options) {
53
53
  },
54
54
  }, async ({ approval_id }) => {
55
55
  const result = await checkApproval(approval_id);
56
- return j(result);
56
+ return result.status === 'not_found'
57
+ ? toolError({ ...result, code: 'not_found' })
58
+ : j(result);
57
59
  });
58
60
  server.registerTool('list_pending_approvals', {
59
61
  title: 'List Pending Approvals',
@@ -8,6 +8,7 @@ import { buildVoicePrompt, buildEmailSystemPrompt } from '../lib/voice.js';
8
8
  import { getAnthropicModelId } from '../anthropic-model.js';
9
9
  // Shared with the dashboard, including trigger_config.where event-data filters (#3689).
10
10
  import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText, campaignMatchesTrigger, normalizeTriggerList as normalizeList } from '@sendinel/domain';
11
+ import { dbToolError } from '../lib/db-error.js';
11
12
  const txt = (text) => ({ content: [{ type: 'text', text }] });
12
13
  const j = (data) => txt(JSON.stringify(data, null, 2));
13
14
  async function enrollSubscriberIntoTriggeredCampaigns(input) {
@@ -22,7 +23,7 @@ async function enrollSubscriberIntoTriggeredCampaigns(input) {
22
23
  .eq('project_id', projectId)
23
24
  .maybeSingle();
24
25
  if (subscriberError)
25
- return txt(`Error: ${subscriberError.message}`);
26
+ return txt(`Error: ${dbToolError(subscriberError).error}`);
26
27
  if (!subscriber)
27
28
  return j({ error: 'Subscriber not found', code: 'NOT_FOUND' });
28
29
  if (subscriber.global_unsubscribed)
@@ -52,7 +53,7 @@ async function enrollSubscriberIntoTriggeredCampaigns(input) {
52
53
  }
53
54
  const { data: candidateCampaigns, error: campaignError } = await query;
54
55
  if (campaignError)
55
- return txt(`Error: ${campaignError.message}`);
56
+ return txt(`Error: ${dbToolError(campaignError).error}`);
56
57
  const campaigns = campaign_id
57
58
  ? (candidateCampaigns ?? [])
58
59
  : (candidateCampaigns ?? []).filter((campaign) => triggerEvents.some(event => campaignMatchesTrigger(campaign, { event, tags: triggerTags, data: context ?? null }))
@@ -74,7 +75,7 @@ async function enrollSubscriberIntoTriggeredCampaigns(input) {
74
75
  .upsert(rows, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true })
75
76
  .select('id, campaign_id');
76
77
  if (enrollError)
77
- return txt(`Error: ${enrollError.message}`);
78
+ return txt(`Error: ${dbToolError(enrollError).error}`);
78
79
  if (inserted?.length) {
79
80
  for (const campaign of campaigns) {
80
81
  const count = inserted.filter((row) => row.campaign_id === campaign.id).length;
@@ -121,7 +122,7 @@ export function registerAutomationTools(server) {
121
122
  q = q.eq('active', true);
122
123
  const { data, error } = await q;
123
124
  if (error)
124
- return txt(`Error: ${error.message}`);
125
+ return txt(`Error: ${dbToolError(error).error}`);
125
126
  return j(data ?? []);
126
127
  });
127
128
  server.registerTool('enroll_automation', {
@@ -175,7 +176,7 @@ export function registerAutomationTools(server) {
175
176
  .eq('project_id', projectId)
176
177
  .maybeSingle();
177
178
  if (campaignError)
178
- return txt(`Error: ${campaignError.message}`);
179
+ return txt(`Error: ${dbToolError(campaignError).error}`);
179
180
  if (!campaign)
180
181
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
181
182
  if (campaign.type !== 'triggered')
@@ -196,7 +197,7 @@ export function registerAutomationTools(server) {
196
197
  .select('id, campaign_id, step_index, wait_hours, channel, action_type, content_ref, response_gate')
197
198
  .order('step_index', { ascending: true });
198
199
  if (error)
199
- return txt(`Error: ${error.message}`);
200
+ return txt(`Error: ${dbToolError(error).error}`);
200
201
  return j({
201
202
  campaign_id,
202
203
  campaign_name: campaign.name,
@@ -231,7 +232,7 @@ export function registerAutomationTools(server) {
231
232
  .limit(1)
232
233
  .maybeSingle();
233
234
  if (gateError)
234
- return txt(`Error: ${gateError.message}`);
235
+ return txt(`Error: ${dbToolError(gateError).error}`);
235
236
  if (!gate)
236
237
  return j({ enrolled: 0, skipped: 'no_escalation_ladder_steps' });
237
238
  return enrollSubscriberIntoTriggeredCampaigns({
@@ -262,7 +263,7 @@ export function registerAutomationTools(server) {
262
263
  .eq('project_id', projectId)
263
264
  .maybeSingle();
264
265
  if (campaignError)
265
- return txt(`Error: ${campaignError.message}`);
266
+ return txt(`Error: ${dbToolError(campaignError).error}`);
266
267
  if (!campaign)
267
268
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
268
269
  const [{ data: steps, error: stepsError }, { data: enrollments, error: enrollmentsError }] = await Promise.all([
@@ -278,9 +279,9 @@ export function registerAutomationTools(server) {
278
279
  .order('enrolled_at', { ascending: false }),
279
280
  ]);
280
281
  if (stepsError)
281
- return txt(`Error: ${stepsError.message}`);
282
+ return txt(`Error: ${dbToolError(stepsError).error}`);
282
283
  if (enrollmentsError)
283
- return txt(`Error: ${enrollmentsError.message}`);
284
+ return txt(`Error: ${dbToolError(enrollmentsError).error}`);
284
285
  const contactIds = [...new Set((enrollments ?? []).map((row) => row.contact_id))];
285
286
  let contactsById = new Map();
286
287
  if (contactIds.length) {
@@ -523,7 +524,7 @@ End with a CTA to view the full changelog on GitHub.`;
523
524
  .select('id')
524
525
  .single();
525
526
  if (draftErr)
526
- return txt(`Failed to save draft: ${draftErr.message}`);
527
+ return txt(`Failed to save draft: ${dbToolError(draftErr).error}`);
527
528
  await db()
528
529
  .from('content_sources')
529
530
  .update({ last_polled_at: new Date().toISOString(), items_generated: (source.items_generated ?? 0) + 1 })
@@ -5,6 +5,7 @@ import { getProjectId } from '../project.js';
5
5
  import { downloadPublicAsset } from '../lib/ssrf-pinned-fetch.js';
6
6
  import { callSendinelV1 } from '../lib/v1-client.js';
7
7
  import { BRAND_ASSET_KINDS, isValidKind, persistBrandAsset, resolveKind, resolveMime, } from '../lib/brand-asset-persist.js';
8
+ import { dbToolError } from '../lib/db-error.js';
8
9
  const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
9
10
  // import_brand_kit_text parses free-form Brand Core/Brand Brain/Content Library
10
11
  // markdown into voice fields, colors, templates, and social posts — a different,
@@ -99,7 +100,7 @@ export function registerBrandKitTools(server) {
99
100
  .eq('review_status', 'confirmed')
100
101
  .order('created_at', { ascending: false });
101
102
  if (error)
102
- return j({ error: error.message });
103
+ return j(dbToolError(error));
103
104
  // Project-wide across ALL brand kits — deliberately not scoped to a single
104
105
  // kit_id, so projects with more than one kit aren't silently truncated.
105
106
  let rows = (data ?? []);
@@ -214,7 +215,7 @@ export function registerBrandKitTools(server) {
214
215
  });
215
216
  }
216
217
  catch (error) {
217
- return j({ error: error instanceof Error ? error.message : 'Upload failed' });
218
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Upload failed' });
218
219
  }
219
220
  });
220
221
  server.registerTool('ingest_brand_assets_by_url', {
@@ -249,7 +250,7 @@ export function registerBrandKitTools(server) {
249
250
  }
250
251
  }
251
252
  catch (error) {
252
- return j({ error: error instanceof Error ? error.message : 'Project lookup failed' });
253
+ return j({ error: error instanceof Error ? dbToolError(error).error : 'Project lookup failed' });
253
254
  }
254
255
  const results = [];
255
256
  for (const asset of assets) {
@@ -12,6 +12,7 @@ import { logAudit } from '../audit.js';
12
12
  import { applyCampaignTargetingFields } from './campaigns.js';
13
13
  import { checkCampaignContent, contentNotReadyResponse, contentWarningView, summarizeContentWarnings } from '../lib/content-sources.js';
14
14
  import { captureBroadcastAudience } from '../lib/approval-snapshot.js';
15
+ import { dbToolError } from '../lib/db-error.js';
15
16
  /**
16
17
  * Campaign sets (#3669): build a whole set of triggered campaigns as drafts in one call, then
17
18
  * activate the set behind one human approval. Built for Cyber Wingman's lifecycle emails
@@ -148,7 +149,7 @@ export async function writeCampaignDrafts(projectId, siteId, drafts, source) {
148
149
  return { error: { error: targetingError, code: 'INVALID_FIELD', campaign: c.name, created_before_failure: createdIds } };
149
150
  const { data: campaign, error } = await db().from('campaigns').insert(insertPayload).select('id').single();
150
151
  if (error || !campaign)
151
- return { error: { error: error?.message ?? 'Failed to create campaign', code: 'CAMPAIGN_CREATE_FAILED', campaign: c.name, created_before_failure: createdIds } };
152
+ return { error: { error: (error ? dbToolError(error).error : undefined) ?? 'Failed to create campaign', code: 'CAMPAIGN_CREATE_FAILED', campaign: c.name, created_before_failure: createdIds } };
152
153
  createdIds.push(campaign.id);
153
154
  const rows = c.emails.map((e, k) => ({
154
155
  campaign_id: campaign.id,
@@ -163,7 +164,7 @@ export async function writeCampaignDrafts(projectId, siteId, drafts, source) {
163
164
  }));
164
165
  const { error: stepError } = await db().from('campaign_steps').insert(rows);
165
166
  if (stepError)
166
- return { error: { error: stepError.message, code: 'STEP_CREATE_FAILED', campaign: c.name, created_before_failure: createdIds } };
167
+ return { error: { ...dbToolError(stepError), code: 'STEP_CREATE_FAILED', campaign: c.name, created_before_failure: createdIds } };
167
168
  }
168
169
  return { setId, createdIds };
169
170
  }
@@ -181,7 +182,7 @@ export function registerCampaignSetTools(server) {
181
182
  // Sender: the given one, or the only one. Never guessed between several.
182
183
  const { data: sites, error: sitesErr } = await db().from('sites').select('id, name, from_email, unsub_link_enabled').eq('project_id', projectId);
183
184
  if (sitesErr)
184
- return j({ error: sitesErr.message, code: 'SITE_LOOKUP_FAILED' });
185
+ return j({ ...dbToolError(sitesErr), code: 'SITE_LOOKUP_FAILED' });
185
186
  const site = site_id ? (sites ?? []).find((s) => s.id === site_id) : (sites?.length === 1 ? sites[0] : null);
186
187
  if (!site) {
187
188
  return j({
@@ -227,7 +228,7 @@ export function registerCampaignSetTools(server) {
227
228
  email = { subject: e.subject ?? g.subject, body_html: g.body_html, body_text: g.body_text, blocks: g.blocks, delay_days: e.delay_days ?? 0, delay_hours: e.delay_hours ?? 0 };
228
229
  }
229
230
  catch (error) {
230
- return j({ error: `Nothing was created. Drafting campaigns[${i}].emails[${k}] failed: ${error instanceof Error ? error.message : String(error)}`, code: 'DRAFT_FAILED' });
231
+ return j({ error: `Nothing was created. Drafting campaigns[${i}].emails[${k}] failed: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'DRAFT_FAILED' });
231
232
  }
232
233
  }
233
234
  else {
@@ -263,7 +264,7 @@ export function registerCampaignSetTools(server) {
263
264
  preview = previewOf(loaded.campaigns, loaded.steps);
264
265
  }
265
266
  catch (error) {
266
- return j({ campaign_set_id: setId, campaign_ids: createdIds, status: 'draft', warning: `Created, but the preview could not be read back: ${error instanceof Error ? error.message : String(error)}` });
267
+ return j({ campaign_set_id: setId, campaign_ids: createdIds, status: 'draft', warning: `Created, but the preview could not be read back: ${error instanceof Error ? dbToolError(error).error : String(error)}` });
267
268
  }
268
269
  return j({
269
270
  campaign_set_id: setId,
@@ -292,7 +293,7 @@ export function registerCampaignSetTools(server) {
292
293
  return j({ error: `Call activate_campaigns with only approval_id. Remove: ${extras.join(', ')}.`, code: 'APPROVAL_CALL_TAKES_ONLY_APPROVAL_ID' });
293
294
  const { data: row, error: rowErr } = await db().from('action_approvals').select('params').eq('id', approval_id).eq('project_id', projectId).eq('action', 'activate_campaigns').maybeSingle();
294
295
  if (rowErr)
295
- return j({ error: rowErr.message, code: 'APPROVAL_LOOKUP_FAILED' });
296
+ return j({ ...dbToolError(rowErr), code: 'APPROVAL_LOOKUP_FAILED' });
296
297
  if (!row)
297
298
  return j({ error: `Approval ${approval_id} is not_found. Request a new approval.`, code: 'not_found' });
298
299
  const stored = (row.params ?? {});
@@ -303,7 +304,7 @@ export function registerCampaignSetTools(server) {
303
304
  current = await loadSet(projectId, ids);
304
305
  }
305
306
  catch (error) {
306
- return j({ error: `Could not load the campaigns: ${error instanceof Error ? error.message : String(error)}`, code: 'SET_LOOKUP_FAILED' });
307
+ return j({ error: `Could not load the campaigns: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'SET_LOOKUP_FAILED' });
307
308
  }
308
309
  if (current.campaigns.length !== ids.length || campaignSetContentHash(current.campaigns, current.steps) !== stored.content_hash) {
309
310
  return j({ error: 'A campaign or email changed (or was deleted) after approval. Nothing was activated. Request a new approval.', code: 'CONTENT_CHANGED_SINCE_APPROVAL' });
@@ -325,7 +326,7 @@ export function registerCampaignSetTools(server) {
325
326
  .eq('project_id', projectId)
326
327
  .in('status', ['draft', 'paused']);
327
328
  if (error) {
328
- failed.push({ campaign_id: c.id, error: error.message });
329
+ failed.push({ campaign_id: c.id, ...dbToolError(error) });
329
330
  continue;
330
331
  }
331
332
  activated.push(c.id);
@@ -351,7 +352,7 @@ export function registerCampaignSetTools(server) {
351
352
  if (args.campaign_set_id) {
352
353
  const { data, error } = await db().from('campaigns').select('id').eq('project_id', projectId).eq('metadata->>campaign_set_id', args.campaign_set_id);
353
354
  if (error)
354
- return j({ error: error.message, code: 'SET_LOOKUP_FAILED' });
355
+ return j({ ...dbToolError(error), code: 'SET_LOOKUP_FAILED' });
355
356
  ids = [...new Set([...ids, ...(data ?? []).map((r) => r.id)])];
356
357
  }
357
358
  if (!ids.length)
@@ -361,7 +362,7 @@ export function registerCampaignSetTools(server) {
361
362
  set = await loadSet(projectId, ids);
362
363
  }
363
364
  catch (error) {
364
- return j({ error: `Could not load the campaigns: ${error instanceof Error ? error.message : String(error)}`, code: 'SET_LOOKUP_FAILED' });
365
+ return j({ error: `Could not load the campaigns: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'SET_LOOKUP_FAILED' });
365
366
  }
366
367
  const missing = ids.filter((id) => !set.campaigns.some((c) => c.id === id));
367
368
  if (missing.length)