@sendinel/mcp-server 1.0.145 → 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 (44) 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/lib/tags.d.ts +10 -0
  5. package/build/lib/tags.js +29 -0
  6. package/build/server.js +5 -3
  7. package/build/tools/ab-testing.js +4 -3
  8. package/build/tools/advisor.js +2 -2
  9. package/build/tools/ai-work.js +14 -13
  10. package/build/tools/analytics.js +26 -26
  11. package/build/tools/approvals.js +3 -1
  12. package/build/tools/automations.js +12 -11
  13. package/build/tools/brand-kit.js +4 -3
  14. package/build/tools/campaign-sets.js +11 -10
  15. package/build/tools/campaigns.js +73 -74
  16. package/build/tools/compound.js +7 -6
  17. package/build/tools/connector.js +4 -3
  18. package/build/tools/contacts.js +116 -86
  19. package/build/tools/content.js +16 -16
  20. package/build/tools/convert-imported.js +8 -7
  21. package/build/tools/data-proposals.js +8 -7
  22. package/build/tools/data.js +39 -38
  23. package/build/tools/delivery-ops.js +32 -32
  24. package/build/tools/drafts.js +10 -9
  25. package/build/tools/forms.js +5 -4
  26. package/build/tools/gdpr.js +10 -9
  27. package/build/tools/landing-pages.js +3 -3
  28. package/build/tools/lists.js +21 -20
  29. package/build/tools/newsletters.js +26 -25
  30. package/build/tools/org.js +26 -23
  31. package/build/tools/segments.js +4 -5
  32. package/build/tools/short-links.js +7 -6
  33. package/build/tools/signatures.js +10 -9
  34. package/build/tools/sites.js +9 -8
  35. package/build/tools/sms.js +14 -13
  36. package/build/tools/social-posts.js +20 -19
  37. package/build/tools/social-replies.js +7 -6
  38. package/build/tools/surveys.js +10 -9
  39. package/build/tools/templates.js +14 -13
  40. package/build/tools/warmup.js +5 -4
  41. package/build/tools/webhooks.js +5 -4
  42. package/package.json +1 -1
  43. package/build/lib/db-errors.d.ts +0 -12
  44. package/build/lib/db-errors.js +0 -20
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
5
  import { assertHttpUrl, RESERVED_LINK_SLUGS } from '../lib/url-validation.js';
6
+ import { dbToolError } from '../lib/db-error.js';
6
7
  const txt = (text) => ({ content: [{ type: 'text', text }] });
7
8
  const j = (data) => txt(JSON.stringify(data, null, 2));
8
9
  const VANITY_SLUG_RE = /^[a-zA-Z0-9-]{3,40}$/;
@@ -41,12 +42,12 @@ export function registerShortLinkTools(server) {
41
42
  destination = assertHttpUrl(destination_url);
42
43
  }
43
44
  catch (err) {
44
- return j({ error: err instanceof Error ? err.message : 'Invalid destination URL', code: 'validation_failed' });
45
+ return j({ error: err instanceof Error ? dbToolError(err).error : 'Invalid destination URL', code: 'validation_failed' });
45
46
  }
46
47
  if (campaign_id) {
47
48
  const { data: campaign, error } = await db().from('campaigns').select('id').eq('id', campaign_id).eq('project_id', projectId).maybeSingle();
48
49
  if (error)
49
- return j({ error: error.message, code: 'INTERNAL_ERROR' });
50
+ return j({ ...dbToolError(error), code: 'INTERNAL_ERROR' });
50
51
  if (!campaign)
51
52
  return j({ error: 'Campaign not found in this project', code: 'not_found' });
52
53
  }
@@ -81,7 +82,7 @@ export function registerShortLinkTools(server) {
81
82
  if (error?.code === '23505')
82
83
  return j({ error: 'slug already exists', code: 'conflict' });
83
84
  if (error)
84
- return j({ error: error.message, code: 'INTERNAL_ERROR' });
85
+ return j({ ...dbToolError(error), code: 'INTERNAL_ERROR' });
85
86
  return j({ short_link: data, short_url: `${appBaseUrl()}/s/${data.slug}` });
86
87
  });
87
88
  server.registerTool('get_short_link_stats', {
@@ -103,7 +104,7 @@ export function registerShortLinkTools(server) {
103
104
  .eq('project_id', projectId)
104
105
  .maybeSingle();
105
106
  if (linkError)
106
- return j({ error: linkError.message });
107
+ return j(dbToolError(linkError));
107
108
  if (!link)
108
109
  return j({ error: 'Short link not found', code: 'not_found' });
109
110
  const { data: clicks, error: clickError } = await db()
@@ -113,7 +114,7 @@ export function registerShortLinkTools(server) {
113
114
  .order('clicked_at', { ascending: false })
114
115
  .limit(500);
115
116
  if (clickError)
116
- return j({ error: clickError.message });
117
+ return j(dbToolError(clickError));
117
118
  const uniqueClicks = new Set((clicks ?? []).map((click) => click.ip_hash).filter(Boolean)).size;
118
119
  return j({
119
120
  short_link: link,
@@ -146,7 +147,7 @@ export function registerShortLinkTools(server) {
146
147
  normalizedUrl = assertHttpUrl(url);
147
148
  }
148
149
  catch (err) {
149
- return j({ error: err instanceof Error ? err.message : 'Invalid QR URL', code: 'validation_failed' });
150
+ return j({ error: err instanceof Error ? dbToolError(err).error : 'Invalid QR URL', code: 'validation_failed' });
150
151
  }
151
152
  const dataUri = await QRCode.toDataURL(normalizedUrl, {
152
153
  type: 'image/png',
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
5
  import { logAudit } from '../audit.js';
6
+ import { dbToolError } from '../lib/db-error.js';
6
7
  const txt = (text) => ({ content: [{ type: 'text', text }] });
7
8
  const j = (data) => txt(JSON.stringify(data, null, 2));
8
9
  const COLUMNS = 'id, site_id, name, html, is_default, created_at, updated_at';
@@ -23,7 +24,7 @@ export function registerSignatureTools(server) {
23
24
  query = query.eq('site_id', site_id);
24
25
  const { data, error } = await query;
25
26
  if (error)
26
- return j({ error: error.message });
27
+ return j(dbToolError(error));
27
28
  return j({ signatures: data ?? [], count: data?.length ?? 0 });
28
29
  });
29
30
  server.registerTool('create_signature', {
@@ -45,7 +46,7 @@ export function registerSignatureTools(server) {
45
46
  .eq('project_id', projectId)
46
47
  .maybeSingle();
47
48
  if (siteError)
48
- return j({ error: siteError.message });
49
+ return j(dbToolError(siteError));
49
50
  if (!site)
50
51
  return j({ error: 'Site not found', code: 'NOT_FOUND' });
51
52
  if (is_default) {
@@ -55,7 +56,7 @@ export function registerSignatureTools(server) {
55
56
  .eq('project_id', projectId)
56
57
  .eq('site_id', site_id);
57
58
  if (clearError)
58
- return j({ error: clearError.message });
59
+ return j(dbToolError(clearError));
59
60
  }
60
61
  const { data, error } = await db()
61
62
  .from('signature_blocks')
@@ -69,7 +70,7 @@ export function registerSignatureTools(server) {
69
70
  .select(COLUMNS)
70
71
  .single();
71
72
  if (error)
72
- return j({ error: error.message });
73
+ return j(dbToolError(error));
73
74
  await logAudit({
74
75
  action: 'signature.created',
75
76
  resourceType: 'signature',
@@ -97,7 +98,7 @@ export function registerSignatureTools(server) {
97
98
  .eq('project_id', projectId)
98
99
  .maybeSingle();
99
100
  if (existingError)
100
- return j({ error: existingError.message });
101
+ return j(dbToolError(existingError));
101
102
  if (!existing)
102
103
  return j({ error: 'Signature not found', code: 'NOT_FOUND' });
103
104
  if (is_default) {
@@ -108,7 +109,7 @@ export function registerSignatureTools(server) {
108
109
  .eq('site_id', existing.site_id)
109
110
  .neq('id', id);
110
111
  if (clearError)
111
- return j({ error: clearError.message });
112
+ return j(dbToolError(clearError));
112
113
  }
113
114
  const updates = { updated_at: new Date().toISOString() };
114
115
  if (name !== undefined)
@@ -125,7 +126,7 @@ export function registerSignatureTools(server) {
125
126
  .select(COLUMNS)
126
127
  .single();
127
128
  if (error)
128
- return j({ error: error.message });
129
+ return j(dbToolError(error));
129
130
  await logAudit({
130
131
  action: 'signature.updated',
131
132
  resourceType: 'signature',
@@ -148,7 +149,7 @@ export function registerSignatureTools(server) {
148
149
  .eq('project_id', projectId)
149
150
  .maybeSingle();
150
151
  if (existingError)
151
- return j({ error: existingError.message });
152
+ return j(dbToolError(existingError));
152
153
  if (!existing)
153
154
  return j({ error: 'Signature not found', code: 'NOT_FOUND' });
154
155
  const { error } = await db()
@@ -157,7 +158,7 @@ export function registerSignatureTools(server) {
157
158
  .eq('id', id)
158
159
  .eq('project_id', projectId);
159
160
  if (error)
160
- return j({ error: error.message });
161
+ return j(dbToolError(error));
161
162
  await logAudit({
162
163
  action: 'signature.deleted',
163
164
  resourceType: 'signature',
@@ -4,6 +4,7 @@ import { logAudit } from '../audit.js';
4
4
  import { db } from '../db.js';
5
5
  import { getProjectId } from '../project.js';
6
6
  import { checkPlanLimit } from '../plan-limits.js';
7
+ import { dbToolError } from '../lib/db-error.js';
7
8
  const txt = (text) => ({ content: [{ type: 'text', text }] });
8
9
  const j = (data) => txt(JSON.stringify(data, null, 2));
9
10
  const LOCAL_PART_RE = /^[a-z0-9._-]+$/;
@@ -17,7 +18,7 @@ async function resolveSenderDomain(input) {
17
18
  .eq('id', input.domain_id)
18
19
  .maybeSingle();
19
20
  if (error)
20
- return { error: error.message };
21
+ return { ...dbToolError(error) };
21
22
  if (!domain?.id || !domain.name)
22
23
  return { error: 'Domain not found.' };
23
24
  const localPart = input.local_part?.trim().toLowerCase();
@@ -44,7 +45,7 @@ async function resolveSenderDomain(input) {
44
45
  .eq('name', sendingDomain)
45
46
  .maybeSingle();
46
47
  if (existingError)
47
- return { error: existingError.message };
48
+ return { ...dbToolError(existingError) };
48
49
  if (existing?.id) {
49
50
  return {
50
51
  domain_id: String(existing.id),
@@ -59,7 +60,7 @@ async function resolveSenderDomain(input) {
59
60
  .select('id, name')
60
61
  .single();
61
62
  if (createError)
62
- return { error: createError.message };
63
+ return { ...dbToolError(createError) };
63
64
  return {
64
65
  domain_id: String(created.id),
65
66
  sending_domain: sendingDomain,
@@ -102,7 +103,7 @@ async function createSender({ name, slug, domain_id, local_part, sending_domain,
102
103
  .select()
103
104
  .single();
104
105
  if (error)
105
- return txt(`Error: ${error.message}`);
106
+ return txt(`Error: ${dbToolError(error).error}`);
106
107
  return j(data);
107
108
  }
108
109
  export function registerSiteTools(server) {
@@ -126,7 +127,7 @@ export function registerSiteTools(server) {
126
127
  .eq('id', site_id)
127
128
  .maybeSingle();
128
129
  if (error)
129
- return txt(`Error: ${error.message}`);
130
+ return txt(`Error: ${dbToolError(error).error}`);
130
131
  if (!data)
131
132
  return j({ error: 'Site not found', code: 'NOT_FOUND' });
132
133
  return j(data);
@@ -153,7 +154,7 @@ export function registerSiteTools(server) {
153
154
  .select('id, name, brand_voice, updated_at')
154
155
  .single();
155
156
  if (error)
156
- return txt(`Error: ${error.message}`);
157
+ return txt(`Error: ${dbToolError(error).error}`);
157
158
  return j(data);
158
159
  });
159
160
  server.registerTool('create_site', {
@@ -232,7 +233,7 @@ export function registerSiteTools(server) {
232
233
  .eq('project_id', projectId)
233
234
  .maybeSingle();
234
235
  if (error)
235
- return txt(`Error: ${error.message}`);
236
+ return txt(`Error: ${dbToolError(error).error}`);
236
237
  if (!current)
237
238
  return j({ error: 'Site not found', code: 'NOT_FOUND' });
238
239
  previousTemplate = current.unsub_footer_template;
@@ -249,7 +250,7 @@ export function registerSiteTools(server) {
249
250
  query = query.eq('unsub_footer_template', previousTemplate);
250
251
  const { data, error } = await query.select().maybeSingle();
251
252
  if (error)
252
- return txt(`Error: ${error.message}`);
253
+ return txt(`Error: ${dbToolError(error).error}`);
253
254
  if (!data)
254
255
  return j({ error: 'Site not found or footer changed during update. Read the site and retry.', code: 'SITE_CHANGED_OR_NOT_FOUND' });
255
256
  await logAudit({
@@ -5,6 +5,7 @@ import { z } from 'zod';
5
5
  import { decrypt } from '../crypto.js';
6
6
  import { adminDb, db } from '../db.js';
7
7
  import { getProjectId } from '../project.js';
8
+ import { dbToolError } from '../lib/db-error.js';
8
9
  const txt = (text) => ({ content: [{ type: 'text', text }] });
9
10
  const j = (data) => txt(JSON.stringify(data, null, 2));
10
11
  const GSM_BASIC = "@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ !\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà";
@@ -244,7 +245,7 @@ export function registerSmsTools(server) {
244
245
  query = query.eq('status', status);
245
246
  const { data, error } = await query;
246
247
  if (error)
247
- return j({ error: error.message });
248
+ return j(dbToolError(error));
248
249
  return j({ rows: data ?? [] });
249
250
  });
250
251
  server.registerTool('list_sms_inbound', {
@@ -271,7 +272,7 @@ export function registerSmsTools(server) {
271
272
  query = query.eq('interpreted_as', interpreted_as);
272
273
  const { data, error } = await query;
273
274
  if (error)
274
- return j({ error: error.message });
275
+ return j(dbToolError(error));
275
276
  return j({ rows: data ?? [] });
276
277
  });
277
278
  server.registerTool('get_sms_provider_status', {
@@ -291,7 +292,7 @@ export function registerSmsTools(server) {
291
292
  db().from('sms_log').select('status, created_at').eq('project_id', projectId).order('created_at', { ascending: false }).limit(50),
292
293
  ]);
293
294
  if (recent.error)
294
- return j({ error: recent.error.message });
295
+ return j(dbToolError(recent.error));
295
296
  const rows = recent.data ?? [];
296
297
  const failures = rows.filter((row) => row.status === 'failed' || row.status === 'undelivered').length;
297
298
  return j({
@@ -330,7 +331,7 @@ export function registerSmsTools(server) {
330
331
  .eq('id', contact_id)
331
332
  .maybeSingle();
332
333
  if (contactError)
333
- return j({ error: contactError.message });
334
+ return j(dbToolError(contactError));
334
335
  if (!contact?.phone_e164)
335
336
  return j({ error: 'Contact not found or missing phone number', code: 'NOT_FOUND' });
336
337
  const now = new Date().toISOString();
@@ -339,7 +340,7 @@ export function registerSmsTools(server) {
339
340
  : { sms_opt_in_status: 'opted_in', sms_opt_in_at: now, sms_opt_out_at: null };
340
341
  const { error: updateError } = await db().from('subscribers').update(patch).eq('project_id', projectId).eq('id', contact_id);
341
342
  if (updateError)
342
- return j({ error: updateError.message });
343
+ return j(dbToolError(updateError));
343
344
  const { error: consentError } = await db().from('consent_events').insert({
344
345
  org_id: orgId,
345
346
  project_id: projectId,
@@ -352,7 +353,7 @@ export function registerSmsTools(server) {
352
353
  metadata: {},
353
354
  });
354
355
  if (consentError)
355
- return j({ error: consentError.message });
356
+ return j(dbToolError(consentError));
356
357
  if (status === 'opt_out') {
357
358
  const { error: suppressionError } = await db().from('suppressions').upsert({
358
359
  org_id: orgId,
@@ -362,12 +363,12 @@ export function registerSmsTools(server) {
362
363
  reason: reason ?? 'manual_opt_out',
363
364
  }, { onConflict: 'project_id,channel,phone_e164' });
364
365
  if (suppressionError)
365
- return j({ error: suppressionError.message });
366
+ return j(dbToolError(suppressionError));
366
367
  }
367
368
  else {
368
369
  const { error: suppressionError } = await db().from('suppressions').delete().eq('project_id', projectId).eq('channel', 'sms').eq('phone_e164', contact.phone_e164);
369
370
  if (suppressionError)
370
- return j({ error: suppressionError.message });
371
+ return j(dbToolError(suppressionError));
371
372
  }
372
373
  return j({ ok: true, contact_id, phone_e164: contact.phone_e164, status });
373
374
  }));
@@ -432,7 +433,7 @@ export function registerSmsTools(server) {
432
433
  .select('id, name, type, status, channel, site_id, send_at')
433
434
  .single();
434
435
  if (campaignError || !campaign)
435
- return j({ error: campaignError?.message ?? 'Failed to create campaign' });
436
+ return j({ error: (campaignError ? dbToolError(campaignError).error : undefined) ?? 'Failed to create campaign' });
436
437
  const { data: step, error: stepError } = await db()
437
438
  .from('campaign_steps')
438
439
  .insert({
@@ -449,7 +450,7 @@ export function registerSmsTools(server) {
449
450
  .select('id, step_order, channel')
450
451
  .single();
451
452
  if (stepError)
452
- return j({ error: stepError.message, campaign });
453
+ return j({ ...dbToolError(stepError), campaign });
453
454
  return j({
454
455
  ok: true,
455
456
  campaign,
@@ -484,7 +485,7 @@ export function registerSmsTools(server) {
484
485
  if (contact_id && !targetNumber) {
485
486
  const { data: contact, error: contactError } = await db().from('subscribers').select('phone_e164').eq('project_id', projectId).eq('id', contact_id).maybeSingle();
486
487
  if (contactError)
487
- return j({ error: contactError.message });
488
+ return j(dbToolError(contactError));
488
489
  targetNumber = contact?.phone_e164 ?? null;
489
490
  }
490
491
  if (!targetNumber)
@@ -516,7 +517,7 @@ export function registerSmsTools(server) {
516
517
  .select('id')
517
518
  .single();
518
519
  if (logError || !logRow)
519
- return j({ error: logError?.message ?? 'Failed to create sms_log row' });
520
+ return j({ error: (logError ? dbToolError(logError).error : undefined) ?? 'Failed to create sms_log row' });
520
521
  const sendResult = await sendViaProvider(gate.provider.provider, gate.provider.credentials_enc, gate.provider.default_from_number, gate.normalizedPhone, body, `${process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'}/api/sms/inbound/${gate.provider.provider}`, trackingToken);
521
522
  const { error: updateError } = await db()
522
523
  .from('sms_log')
@@ -530,7 +531,7 @@ export function registerSmsTools(server) {
530
531
  })
531
532
  .eq('id', logRow.id);
532
533
  if (updateError)
533
- return j({ error: updateError.message });
534
+ return j(dbToolError(updateError));
534
535
  return j({
535
536
  ok: true,
536
537
  sms_log_id: logRow.id,
@@ -11,6 +11,7 @@ import { buildSocialSystemPrompt, buildSocialUserPrompt, parseSocialResponse, se
11
11
  import { fireWebhookEvent } from '../lib/webhook-events.js';
12
12
  import { resolveSurveyShortLink } from '../lib/survey-link-resolver.js';
13
13
  import { registerSocialReplyTools } from './social-replies.js';
14
+ import { dbToolError } from '../lib/db-error.js';
14
15
  const txt = (text) => ({ content: [{ type: 'text', text }] });
15
16
  const j = (data) => txt(JSON.stringify(data, null, 2));
16
17
  const socialPlatforms = ['instagram', 'linkedin', 'twitter', 'facebook', 'tiktok', 'youtube', 'pinterest', 'uploadpost', 'other'];
@@ -213,7 +214,7 @@ export function registerSocialPostTools(server) {
213
214
  .eq('id', projectId)
214
215
  .maybeSingle();
215
216
  if (projectError)
216
- return txt(`Error: ${projectError.message}`);
217
+ return txt(`Error: ${dbToolError(projectError).error}`);
217
218
  if (!project?.org_id)
218
219
  return txt('Error: Project organization not found.');
219
220
  const { data, error } = await db()
@@ -222,7 +223,7 @@ export function registerSocialPostTools(server) {
222
223
  .eq('org_id', project.org_id)
223
224
  .order('platform', { ascending: true });
224
225
  if (error)
225
- return txt(`Error: ${error.message}`);
226
+ return txt(`Error: ${dbToolError(error).error}`);
226
227
  const connections = (data ?? [])
227
228
  .map((row) => selectFields(row, SOCIAL_CONNECTION_COLUMNS));
228
229
  if (connections.length === 0) {
@@ -261,7 +262,7 @@ export function registerSocialPostTools(server) {
261
262
  query = query.eq('social_post_id', social_post_id);
262
263
  const { data, error } = await query;
263
264
  if (error)
264
- return txt(`Error: ${error.message}`);
265
+ return txt(`Error: ${dbToolError(error).error}`);
265
266
  const engagements = (data ?? [])
266
267
  .map((row) => selectFields(row, SOCIAL_ENGAGEMENT_LIST_COLUMNS));
267
268
  if (engagements.length === 0) {
@@ -287,7 +288,7 @@ export function registerSocialPostTools(server) {
287
288
  .eq('id', engagement_id)
288
289
  .maybeSingle();
289
290
  if (error)
290
- return txt(`Error: ${error.message}`);
291
+ return txt(`Error: ${dbToolError(error).error}`);
291
292
  if (!data)
292
293
  return j({ error: 'Social engagement not found', code: 'NOT_FOUND' });
293
294
  return j(selectFields(data, SOCIAL_ENGAGEMENT_DETAIL_COLUMNS));
@@ -321,7 +322,7 @@ export function registerSocialPostTools(server) {
321
322
  query = query.contains('platforms', [platform]);
322
323
  const { data, error } = await query;
323
324
  if (error)
324
- return txt(`Error: ${error.message}`);
325
+ return txt(`Error: ${dbToolError(error).error}`);
325
326
  return j({ posts: data ?? [] });
326
327
  });
327
328
  server.registerTool('get_social_post', {
@@ -342,7 +343,7 @@ export function registerSocialPostTools(server) {
342
343
  .eq('id', post_id)
343
344
  .maybeSingle();
344
345
  if (error)
345
- return txt(`Error: ${error.message}`);
346
+ return txt(`Error: ${dbToolError(error).error}`);
346
347
  if (!data)
347
348
  return j({ error: 'Social post not found', code: 'NOT_FOUND' });
348
349
  return j(data);
@@ -368,7 +369,7 @@ export function registerSocialPostTools(server) {
368
369
  db().from('projects').select('org_id').eq('id', projectId).maybeSingle(),
369
370
  ]);
370
371
  if (campaignError)
371
- return txt(`Error: ${campaignError.message}`);
372
+ return txt(`Error: ${dbToolError(campaignError).error}`);
372
373
  if (!campaign)
373
374
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
374
375
  const [{ data: site }, { data: projectSettingsRow }] = await Promise.all([
@@ -482,7 +483,7 @@ export function registerSocialPostTools(server) {
482
483
  await requireProjectCampaign(email_campaign_id);
483
484
  }
484
485
  catch (err) {
485
- return txt(`Error: ${err instanceof Error ? err.message : 'Invalid linked resource'}`);
486
+ return txt(`Error: ${err instanceof Error ? dbToolError(err).error : 'Invalid linked resource'}`);
486
487
  }
487
488
  let scheduledAt;
488
489
  try {
@@ -491,14 +492,14 @@ export function registerSocialPostTools(server) {
491
492
  return txt('Choose either a scheduled time or queue delivery, not both.');
492
493
  }
493
494
  catch (err) {
494
- return txt(`Error: ${err instanceof Error ? err.message : 'Invalid schedule'}`);
495
+ return txt(`Error: ${err instanceof Error ? dbToolError(err).error : 'Invalid schedule'}`);
495
496
  }
496
497
  let metadata;
497
498
  try {
498
499
  metadata = linkedInCompanyCrosspostMetadata(linkedin_company_crosspost, linkedin_company_page_id, platforms);
499
500
  }
500
501
  catch (err) {
501
- return txt(`Error: ${err instanceof Error ? err.message : 'Invalid LinkedIn cross-post settings'}`);
502
+ return txt(`Error: ${err instanceof Error ? dbToolError(err).error : 'Invalid LinkedIn cross-post settings'}`);
502
503
  }
503
504
  let resolvedDestinationUrl = destination_url;
504
505
  if (survey_id) {
@@ -530,7 +531,7 @@ export function registerSocialPostTools(server) {
530
531
  .select('id')
531
532
  .single();
532
533
  if (campaignError)
533
- return txt(`Error: ${campaignError.message}`);
534
+ return txt(`Error: ${dbToolError(campaignError).error}`);
534
535
  socialCampaignId = socialCampaign.id;
535
536
  }
536
537
  const status = statusFromSchedule(scheduledAt, Boolean(queue_requested));
@@ -558,7 +559,7 @@ export function registerSocialPostTools(server) {
558
559
  .select()
559
560
  .single();
560
561
  if (error)
561
- return txt(`Error: ${error.message}`);
562
+ return txt(`Error: ${dbToolError(error).error}`);
562
563
  fireWebhookEvent('social.post.created', { social_post_id: data.id, title: data.title, status: data.status, platforms: data.platforms }, projectId).catch(() => { });
563
564
  if (status === 'scheduled' || status === 'queued') {
564
565
  fireWebhookEvent('social.post.scheduled', { social_post_id: data.id, title: data.title, status: data.status, scheduled_at: data.scheduled_at, platforms: data.platforms }, projectId).catch(() => { });
@@ -614,7 +615,7 @@ export function registerSocialPostTools(server) {
614
615
  .select()
615
616
  .single();
616
617
  if (error)
617
- return txt(`Error: ${error.message}`);
618
+ return txt(`Error: ${dbToolError(error).error}`);
618
619
  fireWebhookEvent('social.post.updated', { social_post_id: data.id, title: data.title, status: data.status, fields: Object.keys(updates) }, getProjectId()).catch(() => { });
619
620
  if (data.status === 'published')
620
621
  fireWebhookEvent('social.post.published', { social_post_id: data.id, title: data.title, platforms: data.platforms, published_at: data.published_at }, getProjectId()).catch(() => { });
@@ -641,7 +642,7 @@ export function registerSocialPostTools(server) {
641
642
  .select()
642
643
  .single();
643
644
  if (error)
644
- return txt(`Error: ${error.message}`);
645
+ return txt(`Error: ${dbToolError(error).error}`);
645
646
  fireWebhookEvent('social.post.archived', { social_post_id: data.id, title: data.title }, getProjectId()).catch(() => { });
646
647
  return j(data);
647
648
  }));
@@ -682,7 +683,7 @@ export function registerSocialPostTools(server) {
682
683
  };
683
684
  const { data, error } = await db().from('social_campaigns').insert(insert).select().single();
684
685
  if (error)
685
- return txt(`Error: ${error.message}`);
686
+ return txt(`Error: ${dbToolError(error).error}`);
686
687
  return j({ id: data.id, tracked_url: data.tracked_url, campaign: data });
687
688
  });
688
689
  server.registerTool('schedule_social_post', {
@@ -715,7 +716,7 @@ export function registerSocialPostTools(server) {
715
716
  .eq('id', p.campaign_id)
716
717
  .maybeSingle();
717
718
  if (campaignError)
718
- return txt(`Error: ${campaignError.message}`);
719
+ return txt(`Error: ${dbToolError(campaignError).error}`);
719
720
  if (!campaign)
720
721
  return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
721
722
  const { data: project, error: projectError } = await admin
@@ -724,7 +725,7 @@ export function registerSocialPostTools(server) {
724
725
  .eq('id', campaign.project_id)
725
726
  .maybeSingle();
726
727
  if (projectError)
727
- return txt(`Error: ${projectError.message}`);
728
+ return txt(`Error: ${dbToolError(projectError).error}`);
728
729
  if (!project?.org_id)
729
730
  return j({ error: 'Project not found', code: 'NOT_FOUND' });
730
731
  const mockProfile = configuredSandboxSocialProfile(project, campaign.platform);
@@ -737,7 +738,7 @@ export function registerSocialPostTools(server) {
737
738
  .eq('platform', p.publisher)
738
739
  .maybeSingle();
739
740
  if (connectionError)
740
- return txt(`Error: ${connectionError.message}`);
741
+ return txt(`Error: ${dbToolError(connectionError).error}`);
741
742
  if (!mockProfile && !connection?.encrypted_access_token) {
742
743
  return j({ error: `No ${p.publisher} connection found. Connect it in Settings -> Integrations.`, code: 'NO_CONNECTION' });
743
744
  }
@@ -767,7 +768,7 @@ export function registerSocialPostTools(server) {
767
768
  .eq('project_id', projectId)
768
769
  .eq('id', p.campaign_id);
769
770
  if (updateError)
770
- return txt(`Error: ${updateError.message}`);
771
+ return txt(`Error: ${dbToolError(updateError).error}`);
771
772
  return j({ success: true, publisherPostId: result.publisherPostId, publish_status: publishStatus, post_url: result.postUrl ?? null });
772
773
  }
773
774
  catch (err) {
@@ -6,6 +6,7 @@ import { db, requestContext } from '../db.js';
6
6
  import { getProjectId } from '../project.js';
7
7
  import { getAnthropicModelId } from '../anthropic-model.js';
8
8
  import { buildVoicePrompt } from '../lib/voice.js';
9
+ import { dbToolError } from '../lib/db-error.js';
9
10
  const j = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value) }] });
10
11
  const replyText = z.string().min(1).max(1250).refine((value) => Boolean(value.trim()), 'Write a reply first.');
11
12
  const postTypes = /^(?:urn:li:(?:activity|share|ugcPost):\d+)$/;
@@ -15,7 +16,7 @@ async function pageEngagement(id) {
15
16
  .eq('project_id', getProjectId()).eq('platform', 'linkedin').eq('id', id)
16
17
  .not('page_id', 'is', null).maybeSingle();
17
18
  if (error)
18
- throw new Error(error.message);
19
+ throw error;
19
20
  if (!data)
20
21
  throw new Error('This Page comment was not found in the current project.');
21
22
  return data;
@@ -64,7 +65,7 @@ async function proposePersonal(kind, target, text, memberId) {
64
65
  comment_urn: commentUrn, reply_text: text, status: 'pending',
65
66
  }).select('id, status').single();
66
67
  if (error)
67
- return j({ error: `Could not save the proposal: ${error.message}` });
68
+ return j({ error: `Could not save the proposal: ${dbToolError(error).error}` });
68
69
  return j({ card_id: data.id, status: data.status, approve_link: `${(process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? process.env.APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '')}/dashboard/linkedin-proposals?proposal=${data.id}`, message: 'Waiting for the target member to Accept, Edit, or Skip. Nothing was posted.' });
69
70
  }
70
71
  /** These agent tools only read comments or create proposals; no provider posting call is reachable. */
@@ -87,7 +88,7 @@ export function registerSocialReplyTools(server) {
87
88
  else if (status && status !== 'all')
88
89
  query = query.eq('reply_status', status);
89
90
  const { data, error } = await query;
90
- return error ? j({ error: error.message }) : j({ comments: data ?? [] });
91
+ return error ? j(dbToolError(error)) : j({ comments: data ?? [] });
91
92
  });
92
93
  server.registerTool('draft_social_reply', {
93
94
  title: 'Draft LinkedIn Page Reply', description: 'Draft a Page reply in brand voice for human review; never posts.',
@@ -98,7 +99,7 @@ export function registerSocialReplyTools(server) {
98
99
  const engagement = await pageEngagement(engagement_id);
99
100
  const { data: project, error } = await db().from('projects').select('brand_settings').eq('id', getProjectId()).maybeSingle();
100
101
  if (error)
101
- throw new Error(error.message);
102
+ throw error;
102
103
  const { data: sites } = await db().from('sites').select('brand_voice').eq('project_id', getProjectId()).limit(1);
103
104
  if (!process.env.ANTHROPIC_API_KEY)
104
105
  return j({ error: 'AI drafting is unavailable.' });
@@ -134,7 +135,7 @@ export function registerSocialReplyTools(server) {
134
135
  .or('reply_status.is.null,reply_status.in.(pending,failed,skipped)')
135
136
  .select('id, reply_status');
136
137
  if (error)
137
- return j({ error: error.message });
138
+ return j(dbToolError(error));
138
139
  if (!data?.length)
139
140
  return j({ error: 'This reply is already posting or posted.' });
140
141
  return j({ card_id: engagement_id, status: 'pending', approve_link: `${(process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? process.env.APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '')}/dashboard?page=social&tab=inbox&reply_id=${engagement_id}`, message: 'Waiting for a person to Accept, Edit, or Skip. Nothing was posted.' });
@@ -260,7 +261,7 @@ export function registerSocialReplyTools(server) {
260
261
  status: 'pending',
261
262
  }).select('id, status').single();
262
263
  if (insertError || !proposal)
263
- return j({ error: `Could not save the proposal: ${insertError?.message ?? 'unknown'}` });
264
+ return j({ error: `Could not save the proposal: ${(insertError ? dbToolError(insertError).error : undefined) ?? 'unknown'}` });
264
265
  return j({
265
266
  card_id: proposal.id,
266
267
  status: proposal.status,
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { db } from '../db.js';
3
3
  import { getProjectId } from '../project.js';
4
+ import { dbToolError } from '../lib/db-error.js';
4
5
  const txt = (text) => ({ content: [{ type: 'text', text }] });
5
6
  const j = (data) => txt(JSON.stringify(data, null, 2));
6
7
  const QUESTION_SCHEMA = z.object({
@@ -98,11 +99,11 @@ export function registerSurveyTools(server) {
98
99
  .select()
99
100
  .single();
100
101
  if (error)
101
- return j({ error: error.message });
102
+ return j(dbToolError(error));
102
103
  const responseUrl = `${appBaseUrl()}/api/surveys/respond?survey_id=${survey.id}`;
103
104
  const { data: slugData, error: slugError } = await db().rpc('next_short_link_slug');
104
105
  if (slugError || !slugData)
105
- return j({ survey, response_url: responseUrl, short_link: null, warning: slugError?.message ?? 'short link slug unavailable' });
106
+ return j({ survey, response_url: responseUrl, short_link: null, warning: (slugError ? dbToolError(slugError).error : undefined) ?? 'short link slug unavailable' });
106
107
  const { data: shortLink, error: linkError } = await db()
107
108
  .from('short_links')
108
109
  .insert({
@@ -123,7 +124,7 @@ export function registerSurveyTools(server) {
123
124
  response_url: responseUrl,
124
125
  short_link: shortLink ?? null,
125
126
  short_url: shortLink?.slug ? `${appBaseUrl()}/s/${shortLink.slug}` : null,
126
- ...(linkError ? { warning: linkError.message } : {}),
127
+ ...(linkError ? { warning: dbToolError(linkError).error } : {}),
127
128
  });
128
129
  });
129
130
  server.registerTool('list_surveys', {
@@ -146,7 +147,7 @@ export function registerSurveyTools(server) {
146
147
  query = query.eq('status', status);
147
148
  const { data, error } = await query;
148
149
  if (error)
149
- return j({ error: error.message });
150
+ return j(dbToolError(error));
150
151
  return j({ surveys: data ?? [], count: data?.length ?? 0 });
151
152
  });
152
153
  server.registerTool('get_survey', {
@@ -163,7 +164,7 @@ export function registerSurveyTools(server) {
163
164
  const projectId = getProjectId();
164
165
  const { data: survey, error } = await db().from('surveys').select('*').eq('id', survey_id).eq('project_id', projectId).maybeSingle();
165
166
  if (error)
166
- return j({ error: error.message });
167
+ return j(dbToolError(error));
167
168
  if (!survey)
168
169
  return j({ error: 'Survey not found', code: 'NOT_FOUND' });
169
170
  const { data: shortLink } = survey.short_link_id
@@ -185,12 +186,12 @@ export function registerSurveyTools(server) {
185
186
  const projectId = getProjectId();
186
187
  const { data: survey, error: surveyError } = await db().from('surveys').select('id, title, questions').eq('id', survey_id).eq('project_id', projectId).maybeSingle();
187
188
  if (surveyError)
188
- return j({ error: surveyError.message });
189
+ return j(dbToolError(surveyError));
189
190
  if (!survey)
190
191
  return j({ error: 'Survey not found', code: 'NOT_FOUND' });
191
192
  const { data: responses, error: responseError } = await db().from('survey_responses').select('id, answers, submitted_at').eq('survey_id', survey_id).eq('project_id', projectId).order('submitted_at', { ascending: false });
192
193
  if (responseError)
193
- return j({ error: responseError.message });
194
+ return j(dbToolError(responseError));
194
195
  const questions = normalizeQuestions((survey.questions ?? []));
195
196
  return j({
196
197
  survey_id,
@@ -224,7 +225,7 @@ export function registerSurveyTools(server) {
224
225
  if (candidate === undefined) {
225
226
  const { data: existing, error } = await db().from('surveys').select('questions').eq('id', survey_id).eq('project_id', projectId).maybeSingle();
226
227
  if (error)
227
- return j({ error: error.message, code: 'INTERNAL_ERROR' });
228
+ return j({ ...dbToolError(error), code: 'INTERNAL_ERROR' });
228
229
  if (!existing)
229
230
  return j({ error: 'Survey not found', code: 'not_found' });
230
231
  candidate = existing.questions;
@@ -251,7 +252,7 @@ export function registerSurveyTools(server) {
251
252
  .select()
252
253
  .maybeSingle();
253
254
  if (error)
254
- return j({ error: error.message });
255
+ return j(dbToolError(error));
255
256
  if (!data)
256
257
  return j({ error: 'Survey not found', code: 'NOT_FOUND' });
257
258
  return j({ survey: data, ok: true });