@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.
- 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 +26 -26
- 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 +73 -74
- 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
|
@@ -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(
|
|
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.
|
|
291
|
+
return txt(`Error: ${dbToolError(directError).error}`);
|
|
292
292
|
if (ownerError)
|
|
293
|
-
return txt(`Error: ${ownerError.
|
|
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.
|
|
305
|
+
return txt(`Error: ${dbToolError(directProjectsError).error}`);
|
|
306
306
|
if (ownedProjectsError)
|
|
307
|
-
return txt(`Error: ${ownedProjectsError.
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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(
|
|
1157
|
+
return j(dbToolError(subscriptionRes.error));
|
|
1158
1158
|
if (invoiceRes.error)
|
|
1159
|
-
return j(
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
1279
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1280
1280
|
return j({ campaigns: data ?? [] });
|
|
1281
1281
|
});
|
|
1282
1282
|
}
|
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)
|