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