@openpets/bento
Version:
Bento email marketing platform integration - manage subscribers, track events, send broadcasts, and analyze engagement
837 lines (794 loc) • 26.2 kB
text/typescript
import { z, createPlugin, createLogger, loadEnv, type ToolDefinition } from "openpets"
import { BentoAPI } from "./client"
const logger = createLogger("bento")
export const BentoPlugin = async () => {
const env = loadEnv("bento")
const publishableKey = env.BENTO_PUBLISHABLE_KEY
const secretKey = env.BENTO_SECRET_KEY
const siteUuid = env.BENTO_SITE_UUID
if (!publishableKey || !secretKey || !siteUuid) {
throw new Error(
"Missing required Bento credentials. Please set BENTO_PUBLISHABLE_KEY, BENTO_SECRET_KEY, and BENTO_SITE_UUID in your .env file."
)
}
const bento = new BentoAPI({
publishableKey,
secretKey,
siteUuid,
})
const tools: ToolDefinition[] = [
{
name: "add-subscriber",
description: "Add a new subscriber to Bento (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
firstName: z.string().optional().describe("First name"),
lastName: z.string().optional().describe("Last name"),
fields: z.object({}).passthrough().optional().describe("Additional custom fields"),
}),
execute: async (args) => {
try {
const fields = {
...(args.firstName && { firstName: args.firstName }),
...(args.lastName && { lastName: args.lastName }),
...args.fields,
}
const result = await bento.importEvents([{
email: args.email,
type: "$subscribe",
fields: Object.keys(fields).length > 0 ? fields : undefined,
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Subscriber ${args.email} added successfully. May take 1-3 minutes to appear.`
: "Failed to add subscriber",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "upsert-subscriber",
description: "Create or update a subscriber in Bento",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
fields: z.object({}).passthrough().optional().describe("Custom fields to set"),
tags: z.string().optional().describe("Comma-separated tags to add"),
removeTags: z.string().optional().describe("Comma-separated tags to remove"),
}),
execute: async (args) => {
try {
const subscriberData: any = {
email: args.email,
...args.fields,
}
if (args.tags) subscriberData.tags = args.tags
if (args.removeTags) subscriberData.remove_tags = args.removeTags
await bento.importSubscribers([subscriberData])
const subscriber = await bento.getSubscriber({ email: args.email })
return JSON.stringify({
success: true,
data: subscriber,
message: `Subscriber ${args.email} upserted successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-subscriber",
description: "Get subscriber details by email or UUID",
schema: z.object({
email: z.string().optional().describe("Subscriber email address"),
uuid: z.string().optional().describe("Subscriber UUID"),
}),
execute: async (args) => {
try {
const subscriber = await bento.getSubscriber(args)
if (!subscriber) {
return JSON.stringify({
success: false,
message: "Subscriber not found",
}, null, 2)
}
return JSON.stringify({
success: true,
data: subscriber,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "update-fields",
description: "Update custom fields for a subscriber (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
fields: z.object({}).passthrough().describe("Fields to update"),
}),
execute: async (args) => {
try {
const result = await bento.importEvents([{
email: args.email,
type: "$update_fields",
fields: args.fields,
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Fields updated for ${args.email}. May take 1-3 minutes to appear.`
: "Failed to update fields",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "tag-subscriber",
description: "Add a tag to a subscriber (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
tagName: z.string().describe("Tag name to add"),
}),
execute: async (args) => {
try {
const result = await bento.importEvents([{
email: args.email,
type: "$tag",
details: { tag: args.tagName },
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Tag "${args.tagName}" added to ${args.email}. May take 1-3 minutes to appear.`
: "Failed to add tag",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "remove-subscriber",
description: "Unsubscribe a subscriber (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
}),
execute: async (args) => {
try {
const result = await bento.importEvents([{
email: args.email,
type: "$unsubscribe",
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Subscriber ${args.email} unsubscribed. May take 1-3 minutes to appear.`
: "Failed to unsubscribe",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "track-event",
description: "Track a custom event for a subscriber (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
type: z.string().describe("Event type (e.g., $pageView, $login, custom-event)"),
details: z.object({}).passthrough().optional().describe("Event details/properties"),
fields: z.object({}).passthrough().optional().describe("Fields to update on subscriber"),
}),
execute: async (args) => {
try {
const result = await bento.importEvents([{
email: args.email,
type: args.type,
details: args.details,
fields: args.fields,
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Event "${args.type}" tracked for ${args.email}. May take 1-3 minutes to appear.`
: "Failed to track event",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "track-purchase",
description: "Track a purchase event to calculate subscriber LTV (TRIGGERS automations)",
schema: z.object({
email: z.string().email().describe("Subscriber email address"),
orderKey: z.string().describe("Unique order identifier"),
amount: z.number().describe("Purchase amount in cents (e.g., 1000 = $10.00)"),
currency: z.string().default("USD").describe("Currency code"),
cartItems: z.array(z.object({
productId: z.string(),
productName: z.string(),
quantity: z.number(),
price: z.number(),
})).optional().describe("Cart items"),
}),
execute: async (args) => {
try {
const result = await bento.importEvents([{
email: args.email,
type: "$purchase",
details: {
unique: { key: args.orderKey },
value: { amount: args.amount, currency: args.currency },
cart: args.cartItems ? {
items: args.cartItems.map(item => ({
product_id: item.productId,
product_name: item.productName,
quantity: item.quantity,
price: item.price,
})),
} : undefined,
},
}])
return JSON.stringify({
success: result === 1,
message: result === 1
? `Purchase tracked for ${args.email}. May take 1-3 minutes to appear.`
: "Failed to track purchase",
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "batch-import-subscribers",
description: "Import multiple subscribers in a single operation",
schema: z.object({
subscribers: z.array(z.object({
email: z.string().email(),
firstName: z.string().optional(),
lastName: z.string().optional(),
fields: z.object({}).passthrough().optional(),
})).describe("Array of subscribers to import"),
}),
execute: async (args) => {
try {
const subscribers = args.subscribers.map(sub => ({
email: sub.email,
...(sub.firstName && { firstName: sub.firstName }),
...(sub.lastName && { lastName: sub.lastName }),
...sub.fields,
}))
const result = await bento.importSubscribers(subscribers)
return JSON.stringify({
success: true,
count: result,
message: `${result} subscribers imported successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "send-transactional-email",
description: "Send transactional email(s) via Bento",
schema: z.object({
emails: z.array(z.object({
to: z.string().email().describe("Recipient email"),
from: z.string().email().describe("Sender email (must be existing Author)"),
subject: z.string().describe("Email subject (supports liquid templates)"),
htmlBody: z.string().describe("HTML email content (supports liquid templates)"),
transactional: z.boolean().default(false).describe("Send to unsubscribed users"),
personalizations: z.object({}).passthrough().optional().describe("Liquid template variables"),
})).min(1).max(100).describe("Array of 1-100 emails to send"),
}),
execute: async (args) => {
try {
const emails = args.emails.map(email => ({
to: email.to,
from: email.from,
subject: email.subject,
html_body: email.htmlBody,
transactional: email.transactional,
personalizations: email.personalizations,
}))
const result = await bento.sendTransactionalEmails(emails)
return JSON.stringify({
success: true,
queued: result,
message: `${result} email(s) queued successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-site-stats",
description: "Get overall site statistics including subscribers and engagement",
schema: z.object({}),
execute: async () => {
try {
const stats = await bento.getSiteStats()
return JSON.stringify({
success: true,
data: stats,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-segment-stats",
description: "Get statistics for a specific segment",
schema: z.object({
segmentId: z.string().describe("Segment ID to get stats for"),
}),
execute: async (args) => {
try {
const stats = await bento.getSegmentStats(args.segmentId)
return JSON.stringify({
success: true,
data: stats,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-report-stats",
description: "Get statistics for a specific broadcast report",
schema: z.object({
reportId: z.string().describe("Report ID to get stats for"),
}),
execute: async (args) => {
try {
const stats = await bento.getReportStats(args.reportId)
return JSON.stringify({
success: true,
data: stats,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-broadcasts",
description: "Retrieve all broadcast campaigns",
schema: z.object({}),
execute: async () => {
try {
const broadcasts = await bento.getBroadcasts()
return JSON.stringify({
success: true,
data: broadcasts,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "create-broadcast",
description: "Create new broadcast campaign(s)",
schema: z.object({
broadcasts: z.array(z.object({
name: z.string().describe("Broadcast name"),
subject: z.string().describe("Email subject"),
content: z.string().describe("HTML content"),
type: z.enum(["html", "plain"]).default("html").describe("Content type"),
fromName: z.string().describe("Sender name"),
fromEmail: z.string().email().describe("Sender email"),
inclusiveTags: z.string().optional().describe("Comma-separated tags to include"),
exclusiveTags: z.string().optional().describe("Comma-separated tags to exclude"),
segmentId: z.string().optional().describe("Segment ID to send to"),
batchSizePerHour: z.number().optional().describe("Throttle rate per hour"),
})).describe("Array of broadcasts to create"),
}),
execute: async (args) => {
try {
const broadcasts = args.broadcasts.map(b => ({
name: b.name,
subject: b.subject,
content: b.content,
type: b.type,
from: {
name: b.fromName,
email: b.fromEmail,
},
inclusive_tags: b.inclusiveTags,
exclusive_tags: b.exclusiveTags,
segment_id: b.segmentId,
batch_size_per_hour: b.batchSizePerHour,
}))
const result = await bento.createBroadcast(broadcasts)
return JSON.stringify({
success: true,
data: result,
message: `${broadcasts.length} broadcast(s) created successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-tags",
description: "Get all tags in the Bento account",
schema: z.object({}),
execute: async () => {
try {
const tags = await bento.getTags()
return JSON.stringify({
success: true,
data: tags,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "create-tag",
description: "Create a new tag in Bento",
schema: z.object({
name: z.string().describe("Tag name"),
}),
execute: async (args) => {
try {
const tag = await bento.createTag({ name: args.name })
return JSON.stringify({
success: true,
data: tag,
message: `Tag "${args.name}" created successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-fields",
description: "Get all custom fields defined in Bento",
schema: z.object({}),
execute: async () => {
try {
const fields = await bento.getFields()
return JSON.stringify({
success: true,
data: fields,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "create-field",
description: "Create a new custom field in Bento",
schema: z.object({
key: z.string().describe("Field key/name"),
}),
execute: async (args) => {
try {
const field = await bento.createField({ key: args.key })
return JSON.stringify({
success: true,
data: field,
message: `Field "${args.key}" created successfully`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "get-form-responses",
description: "Get responses for a specific form",
schema: z.object({
formId: z.string().describe("Form ID"),
}),
execute: async (args) => {
try {
const responses = await bento.getFormResponses(args.formId)
return JSON.stringify({
success: true,
data: responses,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "validate-email",
description: "Validate an email address (Experimental)",
schema: z.object({
email: z.string().email().describe("Email to validate"),
ip: z.string().optional().describe("IP address"),
name: z.string().optional().describe("Name"),
userAgent: z.string().optional().describe("User agent string"),
}),
execute: async (args) => {
try {
const result = await bento.validateEmail(args)
return JSON.stringify({
success: true,
data: result,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "geolocate-ip",
description: "Get geolocation information for an IP address (Experimental)",
schema: z.object({
ip: z.string().describe("IP address to geolocate"),
}),
execute: async (args) => {
try {
const location = await bento.geoLocateIP(args.ip)
return JSON.stringify({
success: true,
data: location,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "check-blacklist",
description: "Check if a domain or IP is blacklisted (Experimental)",
schema: z.object({
domain: z.string().optional().describe("Domain to check"),
ipAddress: z.string().optional().describe("IP address to check"),
}),
execute: async (args) => {
try {
const status = await bento.getBlacklistStatus(args)
return JSON.stringify({
success: true,
data: status,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "moderate-content",
description: "Perform content moderation on text (Experimental)",
schema: z.object({
content: z.string().describe("Content to moderate"),
}),
execute: async (args) => {
try {
const result = await bento.getContentModeration(args.content)
return JSON.stringify({
success: true,
data: result,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "guess-gender",
description: "Attempt to guess gender from a name (Experimental)",
schema: z.object({
name: z.string().describe("Name to analyze"),
}),
execute: async (args) => {
try {
const result = await bento.guessGender({ name: args.name })
return JSON.stringify({
success: true,
data: result,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "add-tag-no-automation",
description: "Add tag to subscriber WITHOUT triggering automations (Commands API)",
schema: z.object({
email: z.string().email().describe("Subscriber email"),
tagName: z.string().describe("Tag name"),
}),
execute: async (args) => {
try {
const result = await bento.addTag({
email: args.email,
tagName: args.tagName,
})
return JSON.stringify({
success: true,
data: result,
message: `Tag "${args.tagName}" added to ${args.email} (no automation triggered)`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "remove-tag-no-automation",
description: "Remove tag from subscriber WITHOUT triggering automations (Commands API)",
schema: z.object({
email: z.string().email().describe("Subscriber email"),
tagName: z.string().describe("Tag name"),
}),
execute: async (args) => {
try {
const result = await bento.removeTag({
email: args.email,
tagName: args.tagName,
})
return JSON.stringify({
success: true,
data: result,
message: `Tag "${args.tagName}" removed from ${args.email} (no automation triggered)`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "add-field-no-automation",
description: "Add field to subscriber WITHOUT triggering automations (Commands API)",
schema: z.object({
email: z.string().email().describe("Subscriber email"),
key: z.string().describe("Field key"),
value: z.unknown().describe("Field value"),
}),
execute: async (args) => {
try {
const result = await bento.addField({
email: args.email,
field: {
key: args.key,
value: args.value,
},
})
return JSON.stringify({
success: true,
data: result,
message: `Field "${args.key}" added to ${args.email} (no automation triggered)`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
{
name: "remove-field-no-automation",
description: "Remove field from subscriber WITHOUT triggering automations (Commands API)",
schema: z.object({
email: z.string().email().describe("Subscriber email"),
fieldName: z.string().describe("Field name to remove"),
}),
execute: async (args) => {
try {
const result = await bento.removeField({
email: args.email,
fieldName: args.fieldName,
})
return JSON.stringify({
success: true,
data: result,
message: `Field "${args.fieldName}" removed from ${args.email} (no automation triggered)`,
}, null, 2)
} catch (error: any) {
return JSON.stringify({
success: false,
error: error.message,
}, null, 2)
}
},
},
]
return createPlugin(tools)
}