UNPKG

@openpets/bento

Version:

Bento email marketing platform integration - manage subscribers, track events, send broadcasts, and analyze engagement

837 lines (794 loc) 26.2 kB
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) }