@sendinel/mcp-server 1.0.146 → 1.0.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/action-approvals.js +3 -1
- package/build/lib/db-error.d.ts +39 -6
- package/build/lib/db-error.js +221 -11
- package/build/server.js +5 -3
- package/build/tools/ab-testing.js +4 -3
- package/build/tools/advisor.js +2 -2
- package/build/tools/ai-work.js +14 -13
- package/build/tools/analytics.js +33 -32
- package/build/tools/approvals.js +3 -1
- package/build/tools/automations.js +12 -11
- package/build/tools/brand-kit.js +4 -3
- package/build/tools/campaign-sets.js +11 -10
- package/build/tools/campaigns.js +81 -79
- package/build/tools/compound.js +7 -6
- package/build/tools/connector.js +4 -3
- package/build/tools/contacts.js +44 -45
- package/build/tools/content.js +16 -16
- package/build/tools/convert-imported.js +8 -7
- package/build/tools/data-proposals.js +8 -7
- package/build/tools/data.js +39 -38
- package/build/tools/delivery-ops.js +32 -32
- package/build/tools/drafts.js +10 -9
- package/build/tools/forms.js +5 -4
- package/build/tools/gdpr.js +10 -9
- package/build/tools/landing-pages.js +3 -3
- package/build/tools/lists.js +21 -20
- package/build/tools/newsletters.js +26 -25
- package/build/tools/org.js +26 -23
- package/build/tools/segments.js +4 -5
- package/build/tools/short-links.js +7 -6
- package/build/tools/signatures.js +10 -9
- package/build/tools/sites.js +9 -8
- package/build/tools/sms.js +14 -13
- package/build/tools/social-posts.js +20 -19
- package/build/tools/social-replies.js +7 -6
- package/build/tools/surveys.js +10 -9
- package/build/tools/templates.js +14 -13
- package/build/tools/warmup.js +5 -4
- package/build/tools/webhooks.js +5 -4
- package/package.json +1 -1
- package/build/lib/db-errors.d.ts +0 -12
- package/build/lib/db-errors.js +0 -20
package/build/tools/analytics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSentRow } from "@sendinel/domain";
|
|
1
2
|
import { readPeriodCampaigns, campaignPeriodFields, filterPlanContacts } from "@sendinel/domain";
|
|
2
3
|
import { isTrueConversionEvent, TRUE_CONVERSION_DEFINITION } from "@sendinel/domain";
|
|
3
4
|
import { percentRate, groupMetricRows, readRevenue, isComplainedRow, isOpenedRow, countPeople, isClickedRow, summarizeEmailRows, countEmailRows, isDeliveredRow, isBouncedRow, fetchAllRows, readAllRows } from "@sendinel/domain";
|
|
@@ -42,7 +43,7 @@ export function computeSenderHealthScore(metrics, trackingEnabled) {
|
|
|
42
43
|
function sendVolumeByDay(rows) {
|
|
43
44
|
const byDay = new Map();
|
|
44
45
|
for (const row of rows) {
|
|
45
|
-
if (!row.sent_at)
|
|
46
|
+
if (!row.sent_at || !isSentRow(row))
|
|
46
47
|
continue;
|
|
47
48
|
const date = new Date(row.sent_at).toISOString().slice(0, 10);
|
|
48
49
|
byDay.set(date, (byDay.get(date) ?? 0) + 1);
|
|
@@ -133,12 +134,12 @@ function distinctEventCount(events, eventType, messageIds) {
|
|
|
133
134
|
.map(event => event.provider_message_id)).size;
|
|
134
135
|
}
|
|
135
136
|
function buildTrueConversionAnalytics(params) {
|
|
136
|
-
const sent = params.logs.
|
|
137
|
+
const sent = countEmailRows(params.logs).sent;
|
|
137
138
|
const delivered = params.logs.filter(isDeliveredRow).length;
|
|
138
139
|
const trueRows = params.attributionRows.filter(row => isTrueConversionEvent(row.conversion_event));
|
|
139
140
|
const campaignDenominators = new Map();
|
|
140
141
|
for (const row of params.logs) {
|
|
141
|
-
if (typeof row.campaign_id !== 'string')
|
|
142
|
+
if (typeof row.campaign_id !== 'string' || !isSentRow(row))
|
|
142
143
|
continue;
|
|
143
144
|
campaignDenominators.set(row.campaign_id, (campaignDenominators.get(row.campaign_id) ?? 0) + 1);
|
|
144
145
|
}
|
|
@@ -229,7 +230,7 @@ export function registerAnalyticsTools(server) {
|
|
|
229
230
|
query = query.eq('site_id', site_id);
|
|
230
231
|
const { data, error } = await query;
|
|
231
232
|
if (error)
|
|
232
|
-
return j(
|
|
233
|
+
return j(dbToolError(error));
|
|
233
234
|
return j({ events: data ?? [] });
|
|
234
235
|
});
|
|
235
236
|
server.registerTool('get_portfolio_analytics', {
|
|
@@ -288,9 +289,9 @@ export function registerAnalyticsTools(server) {
|
|
|
288
289
|
.eq('role', 'owner'),
|
|
289
290
|
]);
|
|
290
291
|
if (directError)
|
|
291
|
-
return txt(`Error: ${directError.
|
|
292
|
+
return txt(`Error: ${dbToolError(directError).error}`);
|
|
292
293
|
if (ownerError)
|
|
293
|
-
return txt(`Error: ${ownerError.
|
|
294
|
+
return txt(`Error: ${dbToolError(ownerError).error}`);
|
|
294
295
|
const directProjectIds = Array.from(new Set((directAccess ?? []).map((row) => row.project_id).filter(Boolean)));
|
|
295
296
|
const ownedOrgIds = Array.from(new Set((ownedOrgs ?? []).map((row) => row.org_id).filter(Boolean)));
|
|
296
297
|
const [{ data: directProjects, error: directProjectsError }, { data: ownedProjects, error: ownedProjectsError }] = await Promise.all([
|
|
@@ -302,9 +303,9 @@ export function registerAnalyticsTools(server) {
|
|
|
302
303
|
: Promise.resolve({ data: [], error: null }),
|
|
303
304
|
]);
|
|
304
305
|
if (directProjectsError)
|
|
305
|
-
return txt(`Error: ${directProjectsError.
|
|
306
|
+
return txt(`Error: ${dbToolError(directProjectsError).error}`);
|
|
306
307
|
if (ownedProjectsError)
|
|
307
|
-
return txt(`Error: ${ownedProjectsError.
|
|
308
|
+
return txt(`Error: ${dbToolError(ownedProjectsError).error}`);
|
|
308
309
|
const byId = new Map();
|
|
309
310
|
for (const project of [...(directProjects ?? []), ...(ownedProjects ?? [])]) {
|
|
310
311
|
if (ctx.invitationProjectIds != null && !ctx.invitationProjectIds.includes(project.id))
|
|
@@ -316,7 +317,7 @@ export function registerAnalyticsTools(server) {
|
|
|
316
317
|
? await adminDb().from('organizations').select('id, name').in('id', orgIds)
|
|
317
318
|
: { data: [], error: null };
|
|
318
319
|
if (organizationsError)
|
|
319
|
-
return txt(`Error: ${organizationsError.
|
|
320
|
+
return txt(`Error: ${dbToolError(organizationsError).error}`);
|
|
320
321
|
const orgNames = new Map((organizations ?? []).map((org) => [org.id, org.name]));
|
|
321
322
|
return j({
|
|
322
323
|
projects: Array.from(byId.values())
|
|
@@ -353,7 +354,7 @@ export function registerAnalyticsTools(server) {
|
|
|
353
354
|
.eq('org_id', project.org_id)
|
|
354
355
|
.order('name');
|
|
355
356
|
if (listErr)
|
|
356
|
-
return txt(`Error: ${listErr.
|
|
357
|
+
return txt(`Error: ${dbToolError(listErr).error}`);
|
|
357
358
|
// Get org info
|
|
358
359
|
const { data: org } = await adminDb()
|
|
359
360
|
.from('organizations')
|
|
@@ -396,7 +397,7 @@ export function registerAnalyticsTools(server) {
|
|
|
396
397
|
p_project_id: project_id,
|
|
397
398
|
});
|
|
398
399
|
if (accessError)
|
|
399
|
-
return j(
|
|
400
|
+
return j(dbToolError(accessError));
|
|
400
401
|
if (!hasAccess)
|
|
401
402
|
return j({ error: 'You do not have access to this project.', code: 'PROJECT_ACCESS_DENIED' });
|
|
402
403
|
if (ctx.invitationProjectIds != null && !ctx.invitationProjectIds.includes(project_id.toLowerCase())) {
|
|
@@ -408,7 +409,7 @@ export function registerAnalyticsTools(server) {
|
|
|
408
409
|
.update({ active_project_id: project_id })
|
|
409
410
|
.eq('id', ctx.oauthTokenId);
|
|
410
411
|
if (updateError)
|
|
411
|
-
return j(
|
|
412
|
+
return j(dbToolError(updateError));
|
|
412
413
|
const { data: project } = await adminDb()
|
|
413
414
|
.from('projects')
|
|
414
415
|
.select('id, name, org_id')
|
|
@@ -445,7 +446,7 @@ export function registerAnalyticsTools(server) {
|
|
|
445
446
|
.eq('project_id', getProjectId())
|
|
446
447
|
.order('name');
|
|
447
448
|
if (error)
|
|
448
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
449
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
449
450
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
450
451
|
});
|
|
451
452
|
server.registerTool('get_stats', {
|
|
@@ -591,7 +592,7 @@ export function registerAnalyticsTools(server) {
|
|
|
591
592
|
.order('check_date', { ascending: false })
|
|
592
593
|
.limit(days);
|
|
593
594
|
if (error)
|
|
594
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
595
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
595
596
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
596
597
|
});
|
|
597
598
|
server.registerTool('get_engagement_insights', {
|
|
@@ -611,7 +612,7 @@ export function registerAnalyticsTools(server) {
|
|
|
611
612
|
const { data, error } = await db()
|
|
612
613
|
.rpc('get_engagement_summary', site_id ? { p_project_id: projectId, p_site_id: site_id } : { p_project_id: projectId });
|
|
613
614
|
if (error)
|
|
614
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
615
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
615
616
|
let trackingEnabled = true;
|
|
616
617
|
if (site_id) {
|
|
617
618
|
const { data: site, error: siteError } = await db()
|
|
@@ -621,7 +622,7 @@ export function registerAnalyticsTools(server) {
|
|
|
621
622
|
.eq('id', site_id)
|
|
622
623
|
.maybeSingle();
|
|
623
624
|
if (siteError)
|
|
624
|
-
return { content: [{ type: 'text', text: `Error: ${siteError.
|
|
625
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(siteError).error}` }] };
|
|
625
626
|
trackingEnabled = site?.tracking_enabled ?? false;
|
|
626
627
|
}
|
|
627
628
|
else {
|
|
@@ -630,7 +631,7 @@ export function registerAnalyticsTools(server) {
|
|
|
630
631
|
.select('tracking_enabled')
|
|
631
632
|
.eq('project_id', projectId);
|
|
632
633
|
if (sitesError)
|
|
633
|
-
return { content: [{ type: 'text', text: `Error: ${sitesError.
|
|
634
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(sitesError).error}` }] };
|
|
634
635
|
trackingEnabled = (sites ?? []).some((site) => site.tracking_enabled);
|
|
635
636
|
}
|
|
636
637
|
const since30d = new Date(Date.now() - 30 * 86_400_000).toISOString();
|
|
@@ -644,7 +645,7 @@ export function registerAnalyticsTools(server) {
|
|
|
644
645
|
q = q.eq('site_id', site_id);
|
|
645
646
|
const { data: logs, error: logsError } = await q;
|
|
646
647
|
if (logsError)
|
|
647
|
-
return { content: [{ type: 'text', text: `Error: ${logsError.
|
|
648
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(logsError).error}` }] };
|
|
648
649
|
const rows = (logs ?? []);
|
|
649
650
|
const metrics30d = computeAnalyticsMetrics(rows);
|
|
650
651
|
const metrics7d = computeAnalyticsMetrics(rows.filter(row => row.sent_at && row.sent_at >= since7d));
|
|
@@ -714,7 +715,7 @@ export function registerAnalyticsTools(server) {
|
|
|
714
715
|
.eq('project_id', projectId)
|
|
715
716
|
.maybeSingle();
|
|
716
717
|
if (siteErr || !site)
|
|
717
|
-
return txt(`Error: Site not found — ${siteErr
|
|
718
|
+
return txt(`Error: Site not found — ${(siteErr ? dbToolError(siteErr).error : undefined) ?? 'not found'}`);
|
|
718
719
|
const domain = site.sending_domain;
|
|
719
720
|
const dnsResults = { spf: null, dkim: null, dmarc: null };
|
|
720
721
|
let verificationSource = 'dns_lookup';
|
|
@@ -815,7 +816,7 @@ export function registerAnalyticsTools(server) {
|
|
|
815
816
|
.eq('site_id', site_id)
|
|
816
817
|
.gte('sent_at', sevenDaysAgo);
|
|
817
818
|
const logs = recentLogs ?? [];
|
|
818
|
-
const totalSent = logs.
|
|
819
|
+
const totalSent = countEmailRows(logs).sent;
|
|
819
820
|
const bounced = logs.filter((l) => isBouncedRow(l)).length;
|
|
820
821
|
const complained = logs.filter(isComplainedRow).length;
|
|
821
822
|
const bounceRate = percentRate(bounced, totalSent, 8) ?? 0;
|
|
@@ -884,7 +885,7 @@ export function registerAnalyticsTools(server) {
|
|
|
884
885
|
q = q.eq('campaign_id', campaign_id);
|
|
885
886
|
const { data: logs, error } = await q;
|
|
886
887
|
if (error)
|
|
887
|
-
return txt(`Error: ${error.
|
|
888
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
888
889
|
let periodCampaigns;
|
|
889
890
|
try {
|
|
890
891
|
periodCampaigns = campaign_id || group_by === 'campaign' ? await readPeriodCampaigns(db(), projectId, { siteId: site_id, campaignId: campaign_id, start: since }) : new Map();
|
|
@@ -898,7 +899,7 @@ export function registerAnalyticsTools(server) {
|
|
|
898
899
|
return j({ message: `No email activity in the last ${days} days.`, total: 0 });
|
|
899
900
|
const { data: attributionRows, error: attributionError } = await readRevenue({ sb: db(), projectId, siteId: site_id, campaignId: campaign_id, start: since });
|
|
900
901
|
if (attributionError)
|
|
901
|
-
return txt(`Error: ${attributionError.
|
|
902
|
+
return txt(`Error: ${dbToolError(attributionError).error}`);
|
|
902
903
|
const scopedAttributionRows = attributionRows ?? [];
|
|
903
904
|
const computeMetrics = (set, counts) => {
|
|
904
905
|
const sharedCounts = counts ?? countEmailRows(set);
|
|
@@ -1012,7 +1013,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1012
1013
|
if (firstHalf.length > 0 && secondHalf.length > 0) {
|
|
1013
1014
|
trend.open_rate = `${(parseRate(secondMetrics.open_rate) - parseRate(firstMetrics.open_rate) >= 0 ? '+' : '')}${(parseRate(secondMetrics.open_rate) - parseRate(firstMetrics.open_rate)).toFixed(1)}pp`;
|
|
1014
1015
|
trend.click_rate = `${(parseRate(secondMetrics.click_rate) - parseRate(firstMetrics.click_rate) >= 0 ? '+' : '')}${(parseRate(secondMetrics.click_rate) - parseRate(firstMetrics.click_rate)).toFixed(1)}pp`;
|
|
1015
|
-
trend.volume = `${
|
|
1016
|
+
trend.volume = `${firstMetrics.total_sent} → ${secondMetrics.total_sent} emails`;
|
|
1016
1017
|
}
|
|
1017
1018
|
return j({
|
|
1018
1019
|
period: `Last ${days} days`,
|
|
@@ -1077,7 +1078,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1077
1078
|
.eq('project_id', proj.id)
|
|
1078
1079
|
.gte('sent_at', since);
|
|
1079
1080
|
const rows = logs ?? [];
|
|
1080
|
-
const sent = rows.
|
|
1081
|
+
const sent = countEmailRows(rows).sent;
|
|
1081
1082
|
const delivered = rows.filter(isDeliveredRow).length;
|
|
1082
1083
|
const opened = countPeople(rows, isOpenedRow);
|
|
1083
1084
|
const clicked = countPeople(rows, isClickedRow);
|
|
@@ -1154,9 +1155,9 @@ export function registerAnalyticsTools(server) {
|
|
|
1154
1155
|
}
|
|
1155
1156
|
const [subscriptionRes, invoiceRes] = await Promise.all([subscriptionQuery, invoiceQuery]);
|
|
1156
1157
|
if (subscriptionRes.error)
|
|
1157
|
-
return j(
|
|
1158
|
+
return j(dbToolError(subscriptionRes.error));
|
|
1158
1159
|
if (invoiceRes.error)
|
|
1159
|
-
return j(
|
|
1160
|
+
return j(dbToolError(invoiceRes.error));
|
|
1160
1161
|
return j(buildRevenueAnalytics({
|
|
1161
1162
|
days,
|
|
1162
1163
|
start,
|
|
@@ -1185,14 +1186,14 @@ export function registerAnalyticsTools(server) {
|
|
|
1185
1186
|
.eq('project_id', getProjectId())
|
|
1186
1187
|
.maybeSingle();
|
|
1187
1188
|
if (siteErr || !site)
|
|
1188
|
-
return j({ error: siteErr
|
|
1189
|
+
return j({ error: (siteErr ? dbToolError(siteErr).error : undefined) ?? 'Site not found', code: 'NOT_FOUND' });
|
|
1189
1190
|
const { data, error } = await db()
|
|
1190
1191
|
.from('tenant_email_metrics')
|
|
1191
1192
|
.select('site_id, window_30d_sent, window_30d_complaints, window_30d_opens, complaint_rate, pause_level, paused_at, pause_reason, updated_at')
|
|
1192
1193
|
.eq('site_id', siteId)
|
|
1193
1194
|
.maybeSingle();
|
|
1194
1195
|
if (error)
|
|
1195
|
-
return j(
|
|
1196
|
+
return j(dbToolError(error));
|
|
1196
1197
|
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
1197
1198
|
const { count: reputationSignals30d, error: reputationErr } = await db()
|
|
1198
1199
|
.from('email_events')
|
|
@@ -1202,7 +1203,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1202
1203
|
.eq('bounce_category', 'reputation_signal')
|
|
1203
1204
|
.gte('occurred_at', since);
|
|
1204
1205
|
if (reputationErr)
|
|
1205
|
-
return j(
|
|
1206
|
+
return j(dbToolError(reputationErr));
|
|
1206
1207
|
return j({
|
|
1207
1208
|
site_id: siteId,
|
|
1208
1209
|
metrics: data ?? null,
|
|
@@ -1231,7 +1232,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1231
1232
|
.eq('id', site_id)
|
|
1232
1233
|
.maybeSingle();
|
|
1233
1234
|
if (siteError)
|
|
1234
|
-
return txt(`Error: ${siteError.
|
|
1235
|
+
return txt(`Error: ${dbToolError(siteError).error}`);
|
|
1235
1236
|
if (!site)
|
|
1236
1237
|
return j({ error: 'Site not found', code: 'NOT_FOUND' });
|
|
1237
1238
|
let q = db()
|
|
@@ -1245,7 +1246,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1245
1246
|
q = q.is('dismissed_at', null);
|
|
1246
1247
|
const { data, error } = await q;
|
|
1247
1248
|
if (error)
|
|
1248
|
-
return txt(`Error: ${error.
|
|
1249
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1249
1250
|
const rank = { critical: 0, warning: 1, info: 2 };
|
|
1250
1251
|
return j((data ?? []).sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)));
|
|
1251
1252
|
});
|
|
@@ -1276,7 +1277,7 @@ export function registerAnalyticsTools(server) {
|
|
|
1276
1277
|
q = q.eq('status', p.status);
|
|
1277
1278
|
const { data, error } = await q;
|
|
1278
1279
|
if (error)
|
|
1279
|
-
return txt(`Error: ${error.
|
|
1280
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1280
1281
|
return j({ campaigns: data ?? [] });
|
|
1281
1282
|
});
|
|
1282
1283
|
}
|
package/build/tools/approvals.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
282
|
+
return txt(`Error: ${dbToolError(stepsError).error}`);
|
|
282
283
|
if (enrollmentsError)
|
|
283
|
-
return txt(`Error: ${enrollmentsError.
|
|
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.
|
|
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 })
|
package/build/tools/brand-kit.js
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
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
|
|
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: {
|
|
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({
|
|
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.
|
|
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.
|
|
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({
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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)
|