@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/compound.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getProjectId } from '../project.js';
|
|
|
6
6
|
import { buildVoicePrompt, deriveTemplateVariables } from '../lib/voice.js';
|
|
7
7
|
import { parseModelJson, getAnthropicFastModelId } from '../anthropic-model.js';
|
|
8
8
|
import { BLOCK_GENERATION_SCHEMA, blocksToHtml, blocksToText, normalizeComposerBlocks } from '../lib/composer-blocks.js';
|
|
9
|
+
import { dbToolError } from '../lib/db-error.js';
|
|
9
10
|
// Haiku 5.5 (fast role) thinks before answering; thinking counts toward max_tokens (#4193).
|
|
10
11
|
const FAST_MIN_MAX_TOKENS = 8000;
|
|
11
12
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -128,13 +129,13 @@ Respond with a JSON array.`,
|
|
|
128
129
|
.select('id')
|
|
129
130
|
.single();
|
|
130
131
|
if (templateErr)
|
|
131
|
-
return j({ error: `Template creation failed: ${templateErr.
|
|
132
|
+
return j({ error: `Template creation failed: ${dbToolError(templateErr).error}` });
|
|
132
133
|
const { data: campaign, error: campaignErr } = await sb.from('campaigns')
|
|
133
134
|
.insert({ name: campaign_name, site_id, project_id: projectId, type: campaign_type, status: 'draft' })
|
|
134
135
|
.select('id, name, status')
|
|
135
136
|
.single();
|
|
136
137
|
if (campaignErr)
|
|
137
|
-
return j({ error: `Campaign creation failed: ${campaignErr.
|
|
138
|
+
return j({ error: `Campaign creation failed: ${dbToolError(campaignErr).error}` });
|
|
138
139
|
const createdSteps = [];
|
|
139
140
|
for (let i = 0; i < steps.length; i++) {
|
|
140
141
|
const step = steps[i];
|
|
@@ -153,7 +154,7 @@ Respond with a JSON array.`,
|
|
|
153
154
|
.select('id, step_order, subject')
|
|
154
155
|
.single();
|
|
155
156
|
if (error)
|
|
156
|
-
return j({ error: `Step ${i + 1} creation failed: ${error.
|
|
157
|
+
return j({ error: `Step ${i + 1} creation failed: ${dbToolError(error).error}`, campaign_id: campaign.id });
|
|
157
158
|
createdSteps.push(data);
|
|
158
159
|
}
|
|
159
160
|
return j({
|
|
@@ -236,7 +237,7 @@ Respond with JSON: { "subject": "...", "blocks": [...], "body_text": "..." }`,
|
|
|
236
237
|
.select('id, name, subject_hint, created_at')
|
|
237
238
|
.single();
|
|
238
239
|
if (error)
|
|
239
|
-
return j(
|
|
240
|
+
return j(dbToolError(error));
|
|
240
241
|
return j({ ok: true, template, preview: generated });
|
|
241
242
|
});
|
|
242
243
|
server.registerTool('diagnose_delivery_issue', {
|
|
@@ -354,7 +355,7 @@ Identify the most likely root cause and give 3-5 concise actionable next steps.`
|
|
|
354
355
|
.select('id, name, provider, status, dns_records')
|
|
355
356
|
.single();
|
|
356
357
|
if (domainErr)
|
|
357
|
-
return j({ error: `Domain registration failed: ${domainErr.
|
|
358
|
+
return j({ error: `Domain registration failed: ${dbToolError(domainErr).error}` });
|
|
358
359
|
const { data: site, error: siteErr } = await sb.from('sites')
|
|
359
360
|
.insert({
|
|
360
361
|
project_id: projectId,
|
|
@@ -372,7 +373,7 @@ Identify the most likely root cause and give 3-5 concise actionable next steps.`
|
|
|
372
373
|
.select('id, name, slug, sending_domain, from_name, from_email, provider, created_at')
|
|
373
374
|
.single();
|
|
374
375
|
if (siteErr)
|
|
375
|
-
return j({ error: `Site creation failed: ${siteErr.
|
|
376
|
+
return j({ error: `Site creation failed: ${dbToolError(siteErr).error}` });
|
|
376
377
|
const warmupPlan = [];
|
|
377
378
|
let volume = Math.max(10, Math.floor(daily_send_volume * 0.05));
|
|
378
379
|
for (let day = 1; day <= 14; day += 3) {
|
package/build/tools/connector.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getProjectId } from '../project.js';
|
|
|
4
4
|
import { listConnectorTablesFromDb } from '../lib/connector-table-discovery.js';
|
|
5
5
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
6
6
|
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, } from '../lib/action-approvals.js';
|
|
7
|
+
import { dbToolError } from '../lib/db-error.js';
|
|
7
8
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
8
9
|
const SECRET_RESPONSE_KEYS = new Set([
|
|
9
10
|
'connector_supabase_key_enc',
|
|
@@ -144,7 +145,7 @@ export function registerConnectorTools(server) {
|
|
|
144
145
|
data = await getConnectorStatus();
|
|
145
146
|
}
|
|
146
147
|
catch (error) {
|
|
147
|
-
return j({ error: error instanceof Error ? error.
|
|
148
|
+
return j({ error: error instanceof Error ? dbToolError(error).error : 'Connector status query failed' });
|
|
148
149
|
}
|
|
149
150
|
if (!data || typeof data !== 'object' || typeof data.viewInstalled !== 'boolean') {
|
|
150
151
|
return j({ error: 'Connector status did not include the required viewInstalled boolean.' });
|
|
@@ -166,7 +167,7 @@ export function registerConnectorTools(server) {
|
|
|
166
167
|
const { data: project, error } = await adminDb().schema('email').from('projects')
|
|
167
168
|
.select('connector_type').eq('id', projectId).maybeSingle();
|
|
168
169
|
if (error)
|
|
169
|
-
return j(
|
|
170
|
+
return j(dbToolError(error));
|
|
170
171
|
if (project?.connector_type !== 'byod') {
|
|
171
172
|
return j({ error: 'Connect your Supabase database in Settings before importing from it.' });
|
|
172
173
|
}
|
|
@@ -174,7 +175,7 @@ export function registerConnectorTools(server) {
|
|
|
174
175
|
return j({ tables: await listConnectorTablesFromDb(db()) });
|
|
175
176
|
}
|
|
176
177
|
catch (tableError) {
|
|
177
|
-
return j({ error: tableError instanceof Error ? tableError.
|
|
178
|
+
return j({ error: tableError instanceof Error ? dbToolError(tableError).error : 'Could not list database tables' });
|
|
178
179
|
}
|
|
179
180
|
});
|
|
180
181
|
server.registerTool('install_connector_schema', {
|
package/build/tools/contacts.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { normalizeTagInput, tagInputWarnings } from '../lib/tags.js';
|
|
2
|
-
import { dbErrorResult } from '../lib/db-errors.js';
|
|
3
2
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
3
|
import { CONTACT_ENRICHMENT_PROVIDERS } from '@desler/models';
|
|
5
4
|
import { createContactEnrichmentAdapters, enrichContactFromConnections, } from '@desler/models/contact-enrichment';
|
|
@@ -8,7 +7,7 @@ import { z } from 'zod';
|
|
|
8
7
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
9
8
|
import { db, projectClientForWriteback } from '../db.js';
|
|
10
9
|
import { getProjectId } from '../project.js';
|
|
11
|
-
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
10
|
+
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
12
11
|
import { resolveSegmentContactIds } from '../lib/send-audience.js';
|
|
13
12
|
import { checkPlanLimit, contactLimitMessage } from '../plan-limits.js';
|
|
14
13
|
import { logConsent, logAudit } from '../audit.js';
|
|
@@ -336,7 +335,7 @@ async function resolveSubscriberViewSiteId(projectId, siteParam) {
|
|
|
336
335
|
.eq('active', true)
|
|
337
336
|
.order('name');
|
|
338
337
|
if (error)
|
|
339
|
-
return { error
|
|
338
|
+
return { ...dbToolError(error) };
|
|
340
339
|
const all = (sites ?? []);
|
|
341
340
|
if (SUBSCRIBER_VIEW_UUID_RE.test(siteParam)) {
|
|
342
341
|
return { siteId: all.some((site) => site.id === siteParam) ? siteParam : undefined };
|
|
@@ -426,7 +425,7 @@ function isMissingRelationshipError(error) {
|
|
|
426
425
|
return false;
|
|
427
426
|
if (error.code === 'PGRST200')
|
|
428
427
|
return true;
|
|
429
|
-
return /could not find a relationship/i.test(error.
|
|
428
|
+
return /could not find a relationship/i.test((error ? dbToolError(error).error : undefined) ?? '');
|
|
430
429
|
}
|
|
431
430
|
async function hasCompaniesEmbed(projectId) {
|
|
432
431
|
const cached = companiesEmbedCache.get(projectId);
|
|
@@ -575,7 +574,7 @@ async function exportSubscriberData(projectId, contactId) {
|
|
|
575
574
|
.eq('project_id', projectId)
|
|
576
575
|
.maybeSingle();
|
|
577
576
|
if (contactError)
|
|
578
|
-
return {
|
|
577
|
+
return { ...dbToolError(contactError) };
|
|
579
578
|
if (!contact)
|
|
580
579
|
return { error: 'Subscriber not found', code: 'NOT_FOUND' };
|
|
581
580
|
const { data: emailHistory, error: logError } = await db()
|
|
@@ -586,7 +585,7 @@ async function exportSubscriberData(projectId, contactId) {
|
|
|
586
585
|
.not('to_email', 'like', 'sha256:%')
|
|
587
586
|
.order('sent_at', { ascending: false });
|
|
588
587
|
if (logError)
|
|
589
|
-
return {
|
|
588
|
+
return { ...dbToolError(logError) };
|
|
590
589
|
const { data: suppression, error: suppressionError } = await db()
|
|
591
590
|
.from('suppressions')
|
|
592
591
|
.select('reason, created_at')
|
|
@@ -595,7 +594,7 @@ async function exportSubscriberData(projectId, contactId) {
|
|
|
595
594
|
.not('email', 'like', 'sha256:%')
|
|
596
595
|
.maybeSingle();
|
|
597
596
|
if (suppressionError)
|
|
598
|
-
return {
|
|
597
|
+
return { ...dbToolError(suppressionError) };
|
|
599
598
|
return {
|
|
600
599
|
contact: {
|
|
601
600
|
id: contact.id,
|
|
@@ -714,7 +713,7 @@ export function registerContactTools(server) {
|
|
|
714
713
|
}, async () => {
|
|
715
714
|
const { data, error } = await db().from('hygiene_config').select('*').eq('project_id', getProjectId()).maybeSingle();
|
|
716
715
|
if (error)
|
|
717
|
-
return j(
|
|
716
|
+
return j(dbToolError(error));
|
|
718
717
|
return j(data ?? { enabled: false, inactive_days: 90, action: 'tag', tag_name: 'cold', suppress_after_days: 180, activity_property: null, last_run_at: null });
|
|
719
718
|
});
|
|
720
719
|
server.registerTool('update_hygiene_config', {
|
|
@@ -747,7 +746,7 @@ export function registerContactTools(server) {
|
|
|
747
746
|
row.activity_property = activityProperty;
|
|
748
747
|
const { error } = await db().from('hygiene_config').upsert(row, { onConflict: 'project_id' });
|
|
749
748
|
if (error)
|
|
750
|
-
return j(
|
|
749
|
+
return j(dbToolError(error));
|
|
751
750
|
return j({ updated: true });
|
|
752
751
|
});
|
|
753
752
|
server.registerTool('run_list_hygiene', {
|
|
@@ -767,7 +766,7 @@ export function registerContactTools(server) {
|
|
|
767
766
|
// read past 1000 is now safe as a follow-up.
|
|
768
767
|
const { data: contacts, error: readError } = await db().from('subscribers').select('id, email, first_name, last_name, custom_properties, tags, last_engaged_at').eq('project_id', projectId).lt('last_engaged_at', cutoff).limit(1000);
|
|
769
768
|
if (readError)
|
|
770
|
-
return j(
|
|
769
|
+
return j(dbToolError(readError));
|
|
771
770
|
let tagged = 0;
|
|
772
771
|
let skipped = 0;
|
|
773
772
|
let failed = 0;
|
|
@@ -908,7 +907,7 @@ export function registerContactTools(server) {
|
|
|
908
907
|
q = q.contains('tags', [`segment:${segmentId}`]);
|
|
909
908
|
const { data: sample, error } = await q;
|
|
910
909
|
if (error)
|
|
911
|
-
return txt(`Error: ${error.
|
|
910
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
912
911
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
913
912
|
if (!apiKey)
|
|
914
913
|
return j({ error: 'ANTHROPIC_API_KEY not configured', code: 'AI_UNAVAILABLE' });
|
|
@@ -984,7 +983,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
984
983
|
.eq('project_id', projectId)
|
|
985
984
|
.maybeSingle();
|
|
986
985
|
if (viewError)
|
|
987
|
-
return txt(`Error: ${viewError.
|
|
986
|
+
return txt(`Error: ${dbToolError(viewError).error}`);
|
|
988
987
|
if (!view)
|
|
989
988
|
return txt('Error: View not found');
|
|
990
989
|
viewFilters = normalizeSubscriberViewFilters(view.filters);
|
|
@@ -1050,7 +1049,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1050
1049
|
.select('contact_id, subscribed')
|
|
1051
1050
|
.eq('site_id', statusSiteId);
|
|
1052
1051
|
if (siteSubscriptionsError)
|
|
1053
|
-
return txt(`Error: ${siteSubscriptionsError.
|
|
1052
|
+
return txt(`Error: ${dbToolError(siteSubscriptionsError).error}`);
|
|
1054
1053
|
if (viewStatus === 'not_subscribed') {
|
|
1055
1054
|
const subscribedIds = (siteSubscriptions ?? [])
|
|
1056
1055
|
.map((subscription) => subscription.contact_id)
|
|
@@ -1082,7 +1081,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1082
1081
|
}
|
|
1083
1082
|
const { data, error } = await q;
|
|
1084
1083
|
if (error)
|
|
1085
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1084
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1086
1085
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1087
1086
|
});
|
|
1088
1087
|
server.registerTool('list_subscriber_views', {
|
|
@@ -1102,7 +1101,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1102
1101
|
.eq('project_id', getProjectId())
|
|
1103
1102
|
.order('created_at', { ascending: true });
|
|
1104
1103
|
if (error)
|
|
1105
|
-
return txt(`Error: ${error.
|
|
1104
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1106
1105
|
return j({ views: data ?? [] });
|
|
1107
1106
|
});
|
|
1108
1107
|
server.registerTool('create_subscriber_view', {
|
|
@@ -1220,7 +1219,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1220
1219
|
.select('id')
|
|
1221
1220
|
.maybeSingle();
|
|
1222
1221
|
if (error)
|
|
1223
|
-
return txt(`Error: ${error.
|
|
1222
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1224
1223
|
if (!data)
|
|
1225
1224
|
return txt('View not found');
|
|
1226
1225
|
return j({ deleted: true, view_id });
|
|
@@ -1251,7 +1250,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1251
1250
|
}
|
|
1252
1251
|
const { data: companies, error } = await query;
|
|
1253
1252
|
if (error)
|
|
1254
|
-
return txt(`Error: ${error.
|
|
1253
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1255
1254
|
const rows = companies ?? [];
|
|
1256
1255
|
if (!rows.length)
|
|
1257
1256
|
return j([]);
|
|
@@ -1266,7 +1265,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1266
1265
|
.order('id')
|
|
1267
1266
|
.range(offset, offset + 999);
|
|
1268
1267
|
if (subscriberError)
|
|
1269
|
-
return txt(`Error: ${subscriberError.
|
|
1268
|
+
return txt(`Error: ${dbToolError(subscriberError).error}`);
|
|
1270
1269
|
for (const subscriber of subscribers ?? []) {
|
|
1271
1270
|
const companyId = typeof subscriber.company_id === 'string' ? subscriber.company_id : null;
|
|
1272
1271
|
if (!companyId)
|
|
@@ -1531,7 +1530,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1531
1530
|
.upsert(rows, { onConflict: 'project_id,email', ignoreDuplicates: false })
|
|
1532
1531
|
.select('id, email, first_name, last_name, custom_properties');
|
|
1533
1532
|
if (uErr)
|
|
1534
|
-
return { content: [{ type: 'text', text: `Error: ${uErr.
|
|
1533
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(uErr).error}` }] };
|
|
1535
1534
|
const contactRows = upserted ?? [];
|
|
1536
1535
|
const subRows = contactRows.map((c) => ({ contact_id: c.id, site_id, subscribed: true }));
|
|
1537
1536
|
if (subRows.length) {
|
|
@@ -1564,7 +1563,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1564
1563
|
.select('id, email, first_name, last_name, custom_properties')
|
|
1565
1564
|
.single();
|
|
1566
1565
|
if (error)
|
|
1567
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1566
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1568
1567
|
if (data) {
|
|
1569
1568
|
// Cascade to site subscriptions
|
|
1570
1569
|
await db()
|
|
@@ -1637,7 +1636,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1637
1636
|
q = q.ilike('subject', `%${subject_contains.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
1638
1637
|
const { data, error } = await q;
|
|
1639
1638
|
if (error)
|
|
1640
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1639
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1641
1640
|
const fetched = (data ?? []);
|
|
1642
1641
|
const hasMore = fetched.length > pageSize;
|
|
1643
1642
|
// source: where the email came from. A campaign id wins; otherwise the stored category (transactional, test, marketing).
|
|
@@ -1672,7 +1671,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1672
1671
|
q = q.eq('reason', reason);
|
|
1673
1672
|
const { data, error } = await q;
|
|
1674
1673
|
if (error)
|
|
1675
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1674
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1676
1675
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1677
1676
|
});
|
|
1678
1677
|
server.registerTool('add_suppression', {
|
|
@@ -1696,7 +1695,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1696
1695
|
.select()
|
|
1697
1696
|
.single();
|
|
1698
1697
|
if (error)
|
|
1699
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1698
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1700
1699
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1701
1700
|
});
|
|
1702
1701
|
server.registerTool('list_subscribers_by_segment', {
|
|
@@ -1735,7 +1734,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1735
1734
|
segmentIds = await resolveSegmentContactIds(projectId, segment.rules);
|
|
1736
1735
|
}
|
|
1737
1736
|
catch (error) {
|
|
1738
|
-
return toolError(isSegmentRulesError(error) ? { error
|
|
1737
|
+
return toolError(isSegmentRulesError(error) ? { ...dbToolError(error), code: 'validation_failed' } : { error: 'Unable to evaluate segment', code: 'internal_error' });
|
|
1739
1738
|
}
|
|
1740
1739
|
if (!segmentIds.length)
|
|
1741
1740
|
return j([]);
|
|
@@ -2045,7 +2044,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2045
2044
|
return txt('Provide either contact_id or email.');
|
|
2046
2045
|
const { data, error } = await q.maybeSingle();
|
|
2047
2046
|
if (error)
|
|
2048
|
-
return txt(`Error: ${error.
|
|
2047
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2049
2048
|
return j(data);
|
|
2050
2049
|
});
|
|
2051
2050
|
server.registerTool('enrich_subscriber', {
|
|
@@ -2071,7 +2070,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2071
2070
|
.eq('id', contact_id)
|
|
2072
2071
|
.maybeSingle();
|
|
2073
2072
|
if (error)
|
|
2074
|
-
return txt(`Error: ${error.
|
|
2073
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2075
2074
|
if (!contact?.email)
|
|
2076
2075
|
return j({ error: 'Subscriber not found', code: 'NOT_FOUND' });
|
|
2077
2076
|
const enriched = await enrichContactFromProviders(projectId, contact.email, provider);
|
|
@@ -2109,7 +2108,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2109
2108
|
.select(embedOk ? '*, company:companies(id, name, domain)' : '*')
|
|
2110
2109
|
.single();
|
|
2111
2110
|
if (updateError)
|
|
2112
|
-
return txt(`Error: ${updateError.
|
|
2111
|
+
return txt(`Error: ${dbToolError(updateError).error}`);
|
|
2113
2112
|
const appliedFields = [];
|
|
2114
2113
|
if (!contact.first_name && enriched.firstName)
|
|
2115
2114
|
appliedFields.push('first_name');
|
|
@@ -2355,7 +2354,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2355
2354
|
}
|
|
2356
2355
|
catch (error) {
|
|
2357
2356
|
if (isSegmentRulesError(error))
|
|
2358
|
-
return txt(`Error: ${error.
|
|
2357
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2359
2358
|
throw error;
|
|
2360
2359
|
}
|
|
2361
2360
|
}
|
|
@@ -2380,7 +2379,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2380
2379
|
}
|
|
2381
2380
|
const { data, error } = await q.order('engagement_score', { ascending: false }).limit(sampleSize);
|
|
2382
2381
|
if (error)
|
|
2383
|
-
return txt(`Error: ${error.
|
|
2382
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2384
2383
|
// Get full count separately
|
|
2385
2384
|
let countQ = db()
|
|
2386
2385
|
.from('subscribers')
|
|
@@ -2466,7 +2465,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2466
2465
|
.order('email')
|
|
2467
2466
|
.range(offset, offset + 999);
|
|
2468
2467
|
if (suppressionError)
|
|
2469
|
-
return txt(`Error: ${suppressionError.
|
|
2468
|
+
return txt(`Error: ${dbToolError(suppressionError).error}`);
|
|
2470
2469
|
for (const suppression of suppressed ?? [])
|
|
2471
2470
|
suppressedSet.add(suppression.email);
|
|
2472
2471
|
if ((suppressed ?? []).length < 1000)
|
|
@@ -2474,7 +2473,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2474
2473
|
}
|
|
2475
2474
|
const { data: contacts, error } = await q.order('engagement_score', { ascending: true }).limit(sampleLimit);
|
|
2476
2475
|
if (error)
|
|
2477
|
-
return txt(`Error: ${error.
|
|
2476
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2478
2477
|
const inactive = (contacts ?? []).filter((c) => !suppressedSet.has(c.email));
|
|
2479
2478
|
if (!dry_run && inactive.length > 0) {
|
|
2480
2479
|
const channelContext = currentChannelContext();
|
|
@@ -2506,7 +2505,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2506
2505
|
.from('suppressions')
|
|
2507
2506
|
.upsert(suppressionRows, { onConflict: 'project_id,email', ignoreDuplicates: true });
|
|
2508
2507
|
if (supErr)
|
|
2509
|
-
return txt(`Error suppressing: ${supErr.
|
|
2508
|
+
return txt(`Error suppressing: ${dbToolError(supErr).error}`);
|
|
2510
2509
|
return j({ suppressed: inactive.length, days_threshold: days, sample: inactive });
|
|
2511
2510
|
}
|
|
2512
2511
|
return j({
|
|
@@ -2542,7 +2541,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2542
2541
|
.select()
|
|
2543
2542
|
.single();
|
|
2544
2543
|
if (error)
|
|
2545
|
-
return txt(`Error: ${error.
|
|
2544
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2546
2545
|
if (!data)
|
|
2547
2546
|
return txt(`No suppression found for ${email}.`);
|
|
2548
2547
|
return j({ removed: true, email, previous_reason: data.reason });
|
|
@@ -2625,7 +2624,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2625
2624
|
for (const ids of chunkIds(siteIds)) {
|
|
2626
2625
|
const part = await scanChunk(ids);
|
|
2627
2626
|
if (!part.ok)
|
|
2628
|
-
return txt(`Error: ${part.error.
|
|
2627
|
+
return txt(`Error: ${dbToolError(part.error).error}`);
|
|
2629
2628
|
all.push(...part.rows);
|
|
2630
2629
|
total = total === null || part.total === null ? null : total + part.total;
|
|
2631
2630
|
}
|
|
@@ -2637,7 +2636,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2637
2636
|
scan = await scanChunk(siteIds);
|
|
2638
2637
|
}
|
|
2639
2638
|
if (!scan.ok)
|
|
2640
|
-
return txt(`Error: ${scan.error.
|
|
2639
|
+
return txt(`Error: ${dbToolError(scan.error).error}`);
|
|
2641
2640
|
const contacts = scan.rows;
|
|
2642
2641
|
// #3604: say when the scan stopped at the limit instead of presenting a partial scan as complete.
|
|
2643
2642
|
const scanned = contacts?.length ?? 0;
|
|
@@ -2713,7 +2712,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2713
2712
|
p_source_ids: source_ids,
|
|
2714
2713
|
});
|
|
2715
2714
|
if (error)
|
|
2716
|
-
return txt(`Error: ${error.
|
|
2715
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2717
2716
|
logAudit({ action: 'contacts.merged', resourceType: 'contact', resourceId: target_id, details: { source_ids, result: data } });
|
|
2718
2717
|
return j(data);
|
|
2719
2718
|
});
|
|
@@ -2737,7 +2736,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2737
2736
|
...(siteScope ? { p_site_id: siteScope } : {}),
|
|
2738
2737
|
});
|
|
2739
2738
|
if (error)
|
|
2740
|
-
return { error: `Error: ${error.
|
|
2739
|
+
return { error: `Error: ${dbToolError(error).error}` };
|
|
2741
2740
|
const result = (Array.isArray(data) ? data[0] : data);
|
|
2742
2741
|
if (typeof result?.has_membership !== 'boolean' || !Array.isArray(result.keys)) {
|
|
2743
2742
|
return { error: 'Error: get_contact_schema_keys returned an unreadable result' };
|
|
@@ -2842,7 +2841,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2842
2841
|
if (error) {
|
|
2843
2842
|
if (error.code === '23505')
|
|
2844
2843
|
return contactInputError('conflict', `Error: A custom field with key "${key}" already exists.`);
|
|
2845
|
-
return j({ error: `Error: ${error.
|
|
2844
|
+
return j({ error: `Error: ${dbToolError(error).error}`, code: 'INTERNAL_ERROR' });
|
|
2846
2845
|
}
|
|
2847
2846
|
await logAudit({
|
|
2848
2847
|
action: 'custom_field.created',
|
|
@@ -2873,7 +2872,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2873
2872
|
.eq('project_id', projectId)
|
|
2874
2873
|
.maybeSingle();
|
|
2875
2874
|
if (currentError)
|
|
2876
|
-
return txt(`Error: ${currentError.
|
|
2875
|
+
return txt(`Error: ${dbToolError(currentError).error}`);
|
|
2877
2876
|
if (!current)
|
|
2878
2877
|
return j({ error: 'Custom field not found', code: 'NOT_FOUND' });
|
|
2879
2878
|
const nextType = (type ?? current.type);
|
|
@@ -2899,7 +2898,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2899
2898
|
.select('id, key, label, type, required, options, display_order')
|
|
2900
2899
|
.maybeSingle();
|
|
2901
2900
|
if (error)
|
|
2902
|
-
return txt(`Error: ${error.
|
|
2901
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2903
2902
|
if (!data)
|
|
2904
2903
|
return j({ error: 'Custom field not found', code: 'NOT_FOUND' });
|
|
2905
2904
|
await logAudit({
|
|
@@ -2925,7 +2924,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2925
2924
|
.select('id, key')
|
|
2926
2925
|
.maybeSingle();
|
|
2927
2926
|
if (error)
|
|
2928
|
-
return txt(`Error: ${error.
|
|
2927
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2929
2928
|
if (!data)
|
|
2930
2929
|
return j({ error: 'Custom field not found', code: 'NOT_FOUND' });
|
|
2931
2930
|
await logAudit({
|
|
@@ -2952,7 +2951,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2952
2951
|
}, async ({ contact_id, window_hours }) => {
|
|
2953
2952
|
const { data, error } = await getContactNeighborhood(db(), getProjectId(), contact_id, window_hours);
|
|
2954
2953
|
if (error)
|
|
2955
|
-
return txt(`Error: ${error.
|
|
2954
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2956
2955
|
if (data == null)
|
|
2957
2956
|
return j({ error: 'Subscriber not found', code: 'NOT_FOUND' });
|
|
2958
2957
|
return j({ ...data, attention: buildPreviewAttentionEnvelope() });
|
|
@@ -2973,7 +2972,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2973
2972
|
}, async ({ contact_id, window_hours }) => {
|
|
2974
2973
|
const { data, error } = await getScheduledTouches(db(), getProjectId(), contact_id, window_hours);
|
|
2975
2974
|
if (error)
|
|
2976
|
-
return txt(`Error: ${error.
|
|
2975
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2977
2976
|
if (data == null)
|
|
2978
2977
|
return j({ error: 'Subscriber not found', code: 'NOT_FOUND' });
|
|
2979
2978
|
return j({ ...data, attention: buildPreviewAttentionEnvelope() });
|
|
@@ -3000,7 +2999,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
3000
2999
|
enrollmentId: enrollment_id,
|
|
3001
3000
|
});
|
|
3002
3001
|
if (error)
|
|
3003
|
-
return txt(`Error: ${error.
|
|
3002
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
3004
3003
|
if (data == null)
|
|
3005
3004
|
return j({ error: 'Not found', code: 'NOT_FOUND' });
|
|
3006
3005
|
return j(data);
|
|
@@ -3025,7 +3024,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
3025
3024
|
}
|
|
3026
3025
|
const { data, error } = await getContactNeighborhood(db(), getProjectId(), contactId);
|
|
3027
3026
|
const payload = error
|
|
3028
|
-
? { error
|
|
3027
|
+
? { ...dbToolError(error) }
|
|
3029
3028
|
: data == null
|
|
3030
3029
|
? { error: 'Subscriber not found', code: 'NOT_FOUND' }
|
|
3031
3030
|
: data;
|
package/build/tools/content.js
CHANGED
|
@@ -133,7 +133,7 @@ export function registerContentTools(server) {
|
|
|
133
133
|
if (error?.code === '23505')
|
|
134
134
|
return j({ error: 'slug already exists', code: 'conflict' });
|
|
135
135
|
if (error)
|
|
136
|
-
return j({ error
|
|
136
|
+
return j({ ...dbToolError(error), code: error.code });
|
|
137
137
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai';
|
|
138
138
|
return j({ asset_link: { ...data, url: `${appUrl.replace(/\/$/, '')}/a/${data.slug}` } });
|
|
139
139
|
});
|
|
@@ -160,7 +160,7 @@ export function registerContentTools(server) {
|
|
|
160
160
|
query = query.eq('status', status);
|
|
161
161
|
const { data, error } = await query;
|
|
162
162
|
if (error)
|
|
163
|
-
return j(
|
|
163
|
+
return j(dbToolError(error));
|
|
164
164
|
const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
165
165
|
return j({ asset_links: (data ?? []).map((row) => ({ ...row, url: `${appUrl}/a/${row.slug}` })) });
|
|
166
166
|
});
|
|
@@ -192,7 +192,7 @@ export function registerContentTools(server) {
|
|
|
192
192
|
.order('viewed_at', { ascending: false })
|
|
193
193
|
.limit(limit);
|
|
194
194
|
if (error)
|
|
195
|
-
return j(
|
|
195
|
+
return j(dbToolError(error));
|
|
196
196
|
return j({ views: data ?? [] });
|
|
197
197
|
});
|
|
198
198
|
server.registerTool('list_content_blocks', {
|
|
@@ -211,7 +211,7 @@ export function registerContentTools(server) {
|
|
|
211
211
|
query = query.eq('category', category);
|
|
212
212
|
const { data, error } = await query;
|
|
213
213
|
if (error)
|
|
214
|
-
return j(
|
|
214
|
+
return j(dbToolError(error));
|
|
215
215
|
return j({ blocks: data ?? [] });
|
|
216
216
|
});
|
|
217
217
|
server.registerTool('create_content_block', {
|
|
@@ -258,7 +258,7 @@ export function registerContentTools(server) {
|
|
|
258
258
|
}, withApproval('delete_content_block', async ({ block_id }) => {
|
|
259
259
|
const { error } = await db().from('content_blocks').delete().eq('project_id', getProjectId()).eq('id', block_id);
|
|
260
260
|
if (error)
|
|
261
|
-
return j(
|
|
261
|
+
return j(dbToolError(error));
|
|
262
262
|
return j({ deleted: true });
|
|
263
263
|
}));
|
|
264
264
|
server.registerTool('list_content_sources', {
|
|
@@ -277,7 +277,7 @@ export function registerContentTools(server) {
|
|
|
277
277
|
.eq('project_id', getProjectId())
|
|
278
278
|
.order('created_at', { ascending: false });
|
|
279
279
|
if (error)
|
|
280
|
-
return j(
|
|
280
|
+
return j(dbToolError(error));
|
|
281
281
|
return j({ sources: (data ?? []).map((row) => ({ id: row.id, name: row.name, type: row.source_type, url: row.feed_url, active: row.active, last_fetched: row.last_polled_at, draft_count: row.items_generated ?? 0 })) });
|
|
282
282
|
});
|
|
283
283
|
server.registerTool('trigger_content_pipeline', {
|
|
@@ -296,7 +296,7 @@ export function registerContentTools(server) {
|
|
|
296
296
|
query = query.eq('id', source_id);
|
|
297
297
|
const { error } = await query;
|
|
298
298
|
if (error)
|
|
299
|
-
return j(
|
|
299
|
+
return j(dbToolError(error));
|
|
300
300
|
return j({ drafts_created: 0, queued_for_pipeline: true });
|
|
301
301
|
});
|
|
302
302
|
// Blogs (#2033): the primary agent door for ingesting a blog written by the
|
|
@@ -350,7 +350,7 @@ export function registerContentTools(server) {
|
|
|
350
350
|
.eq('project_id', getProjectId())
|
|
351
351
|
.eq('is_active', true);
|
|
352
352
|
if (destErr)
|
|
353
|
-
return j(
|
|
353
|
+
return j(dbToolError(destErr));
|
|
354
354
|
const target = publish_to.trim().toLowerCase();
|
|
355
355
|
const match = (dests ?? []).find((d) => d.id === publish_to || String(d.name ?? '').toLowerCase() === target);
|
|
356
356
|
if (!match) {
|
|
@@ -382,7 +382,7 @@ export function registerContentTools(server) {
|
|
|
382
382
|
.select('id, title, slug, status, source, scheduled_at, created_at')
|
|
383
383
|
.single();
|
|
384
384
|
if (error)
|
|
385
|
-
return j({ error
|
|
385
|
+
return j({ ...dbToolError(error), code: error.code });
|
|
386
386
|
return j({ blog_post: data, ...(auto_publish ? { note: scheduled_at ? 'Scheduled — the publish worker will send it at scheduled_at.' : 'Queued for immediate publish — the worker publishes it within a few minutes.' } : {}) });
|
|
387
387
|
}));
|
|
388
388
|
server.registerTool('list_blog_posts', {
|
|
@@ -410,7 +410,7 @@ export function registerContentTools(server) {
|
|
|
410
410
|
query = query.neq('status', 'archived');
|
|
411
411
|
const { data, error } = await query;
|
|
412
412
|
if (error)
|
|
413
|
-
return j(
|
|
413
|
+
return j(dbToolError(error));
|
|
414
414
|
return j({ blog_posts: data ?? [] });
|
|
415
415
|
});
|
|
416
416
|
server.registerTool('get_blog_post', {
|
|
@@ -432,7 +432,7 @@ export function registerContentTools(server) {
|
|
|
432
432
|
.eq('id', blog_post_id)
|
|
433
433
|
.maybeSingle();
|
|
434
434
|
if (error)
|
|
435
|
-
return j(
|
|
435
|
+
return j(dbToolError(error));
|
|
436
436
|
if (!data)
|
|
437
437
|
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
438
438
|
return j({ blog_post: data });
|
|
@@ -460,7 +460,7 @@ export function registerContentTools(server) {
|
|
|
460
460
|
.eq('id', blog_post_id)
|
|
461
461
|
.maybeSingle();
|
|
462
462
|
if (error)
|
|
463
|
-
return j(
|
|
463
|
+
return j(dbToolError(error));
|
|
464
464
|
if (!post)
|
|
465
465
|
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
466
466
|
const tags = Array.isArray(post.tags) ? post.tags.map((tag) => String(tag).toLowerCase()) : [];
|
|
@@ -499,7 +499,7 @@ export function registerContentTools(server) {
|
|
|
499
499
|
.eq('id', blog_post_id)
|
|
500
500
|
.maybeSingle();
|
|
501
501
|
if (error)
|
|
502
|
-
return j(
|
|
502
|
+
return j(dbToolError(error));
|
|
503
503
|
if (!post)
|
|
504
504
|
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
505
505
|
const scene = BLOG_IMAGE_SCENES.find((s) => s.id === scene_id);
|
|
@@ -542,7 +542,7 @@ export function registerContentTools(server) {
|
|
|
542
542
|
.select('id, title, cover_image_url, updated_at')
|
|
543
543
|
.maybeSingle();
|
|
544
544
|
if (updateErr)
|
|
545
|
-
return j({
|
|
545
|
+
return j({ ...dbToolError(updateErr), code: updateErr.code });
|
|
546
546
|
await adminDb().from('audit_log').insert({
|
|
547
547
|
project_id: projectId,
|
|
548
548
|
org_id: post.org_id ?? null,
|
|
@@ -556,7 +556,7 @@ export function registerContentTools(server) {
|
|
|
556
556
|
return j({ scene_id, format: chosenFormat, asset: generated.asset, prompt: generated.prompt, blog_post: updated });
|
|
557
557
|
}
|
|
558
558
|
catch (err) {
|
|
559
|
-
return j({ error: err instanceof Error ? err.
|
|
559
|
+
return j({ error: err instanceof Error ? dbToolError(err).error : 'Image generation failed', code: 'IMAGE_GENERATION_FAILED' });
|
|
560
560
|
}
|
|
561
561
|
});
|
|
562
562
|
server.registerTool('list_blog_destinations', {
|
|
@@ -576,7 +576,7 @@ export function registerContentTools(server) {
|
|
|
576
576
|
.eq('is_active', true)
|
|
577
577
|
.order('created_at', { ascending: false });
|
|
578
578
|
if (error)
|
|
579
|
-
return j(
|
|
579
|
+
return j(dbToolError(error));
|
|
580
580
|
return j({ destinations: data ?? [] });
|
|
581
581
|
});
|
|
582
582
|
}
|