d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
480 lines
16 KiB
TypeScript
480 lines
16 KiB
TypeScript
import { filterUndefined, isRecordLike } from '@sim/utils/object'
|
|
import type {
|
|
EmailBisonBaseParams,
|
|
EmailBisonCampaign,
|
|
EmailBisonCampaignTag,
|
|
EmailBisonLead,
|
|
EmailBisonLeadStats,
|
|
EmailBisonReply,
|
|
EmailBisonReplyAddress,
|
|
EmailBisonReplyAttachment,
|
|
EmailBisonTag,
|
|
} from '@/tools/emailbison/types'
|
|
import type { OutputProperty, ToolConfig } from '@/tools/types'
|
|
|
|
type QueryValue = string | number | boolean | Array<string | number> | undefined | null
|
|
|
|
interface EmailBisonEnvelope<T> {
|
|
data?: T
|
|
}
|
|
|
|
export function emailBisonHeaders(params: EmailBisonBaseParams): Record<string, string> {
|
|
return {
|
|
Authorization: `Bearer ${params.apiKey.trim()}`,
|
|
'Content-Type': 'application/json',
|
|
}
|
|
}
|
|
|
|
export const emailBisonBaseParamFields = {
|
|
apiKey: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'user-only',
|
|
description: 'Email Bison API token',
|
|
},
|
|
apiBaseUrl: {
|
|
type: 'string',
|
|
required: true,
|
|
visibility: 'user-only',
|
|
description: 'Email Bison instance URL that issued the token',
|
|
},
|
|
} satisfies ToolConfig['params']
|
|
|
|
export function emailBisonUrl(
|
|
path: string,
|
|
query: Record<string, QueryValue>,
|
|
baseUrl: string
|
|
): string {
|
|
const url = new URL(path, normalizeEmailBisonBaseUrl(baseUrl))
|
|
|
|
Object.entries(query).forEach(([key, value]) => {
|
|
if (value === undefined || value === null || value === '') return
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item) => {
|
|
url.searchParams.append(key, String(item))
|
|
})
|
|
return
|
|
}
|
|
|
|
url.searchParams.set(key, String(value))
|
|
})
|
|
|
|
return url.toString()
|
|
}
|
|
|
|
function normalizeEmailBisonBaseUrl(baseUrl: string): string {
|
|
const trimmedBaseUrl = baseUrl.trim()
|
|
if (!trimmedBaseUrl) {
|
|
throw new Error('Email Bison Instance URL is required')
|
|
}
|
|
|
|
const rawBaseUrl = /^https?:\/\//i.test(trimmedBaseUrl)
|
|
? trimmedBaseUrl
|
|
: `https://${trimmedBaseUrl}`
|
|
const parsed = new URL(rawBaseUrl)
|
|
|
|
if (parsed.protocol !== 'https:') {
|
|
throw new Error('Email Bison Instance URL must use HTTPS')
|
|
}
|
|
|
|
return parsed.origin
|
|
}
|
|
|
|
export function jsonBody(fields: Record<string, unknown>): Record<string, unknown> {
|
|
return filterUndefined(fields)
|
|
}
|
|
|
|
async function emailBisonData<T>(response: Response): Promise<T | null> {
|
|
const payload = (await response.json()) as EmailBisonEnvelope<T>
|
|
return payload.data ?? null
|
|
}
|
|
|
|
export async function emailBisonArrayData(response: Response, label: string): Promise<unknown[]> {
|
|
const data = await emailBisonData<unknown[]>(response)
|
|
if (!Array.isArray(data)) {
|
|
throw new Error(`Email Bison response did not include a valid ${label} array`)
|
|
}
|
|
return data
|
|
}
|
|
|
|
export async function emailBisonRecordData(response: Response, label: string): Promise<unknown> {
|
|
const data = await emailBisonData<unknown>(response)
|
|
if (!isRecordLike(data)) {
|
|
throw new Error(`Email Bison response did not include a valid ${label} object`)
|
|
}
|
|
return data
|
|
}
|
|
|
|
export function mapLead(value: unknown): EmailBisonLead {
|
|
const record = toRecord(value)
|
|
const stats = toRecord(record.overall_stats)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
first_name: toStringOrNull(record.first_name),
|
|
last_name: toStringOrNull(record.last_name),
|
|
email: toStringOrNull(record.email),
|
|
title: toStringOrNull(record.title),
|
|
company: toStringOrNull(record.company),
|
|
notes: toStringOrNull(record.notes),
|
|
status: toStringOrNull(record.status),
|
|
custom_variables: toArray(record.custom_variables).map((item) => {
|
|
const variable = toRecord(item)
|
|
return {
|
|
name: toStringOrNull(variable.name),
|
|
value: toStringOrNull(variable.value),
|
|
}
|
|
}),
|
|
lead_campaign_data: toArray(record.lead_campaign_data),
|
|
overall_stats: mapLeadStats(stats),
|
|
created_at: toStringOrNull(record.created_at),
|
|
updated_at: toStringOrNull(record.updated_at),
|
|
}
|
|
}
|
|
|
|
export function mapCampaign(value: unknown): EmailBisonCampaign {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
uuid: toStringOrNull(record.uuid),
|
|
name: toStringOrNull(record.name),
|
|
type: toStringOrNull(record.type),
|
|
status: toStringOrNull(record.status),
|
|
emails_sent: toNullableNumber(record.emails_sent),
|
|
opened: toNullableNumber(record.opened),
|
|
unique_opens: toNullableNumber(record.unique_opens),
|
|
replied: toNullableNumber(record.replied),
|
|
unique_replies: toNullableNumber(record.unique_replies),
|
|
bounced: toNullableNumber(record.bounced),
|
|
unsubscribed: toNullableNumber(record.unsubscribed),
|
|
interested: toNullableNumber(record.interested),
|
|
total_leads_contacted: toNullableNumber(record.total_leads_contacted),
|
|
total_leads: toNullableNumber(record.total_leads),
|
|
max_emails_per_day: toNullableNumber(record.max_emails_per_day),
|
|
max_new_leads_per_day: toNullableNumber(record.max_new_leads_per_day),
|
|
plain_text: toNullableBoolean(record.plain_text),
|
|
open_tracking: toNullableBoolean(record.open_tracking),
|
|
can_unsubscribe: toNullableBoolean(record.can_unsubscribe),
|
|
unsubscribe_text: toStringOrNull(record.unsubscribe_text),
|
|
...(record.sequence_prioritization !== undefined && {
|
|
sequence_prioritization: toStringOrNull(record.sequence_prioritization),
|
|
}),
|
|
tags: toArray(record.tags).map(mapCampaignTag),
|
|
created_at: toStringOrNull(record.created_at),
|
|
updated_at: toStringOrNull(record.updated_at),
|
|
}
|
|
}
|
|
|
|
function mapCampaignTag(value: unknown): EmailBisonCampaignTag {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
name: toStringOrNull(record.name),
|
|
default: toNullableBoolean(record.default),
|
|
}
|
|
}
|
|
|
|
export function mapTag(value: unknown): EmailBisonTag {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
name: toStringOrNull(record.name),
|
|
default: toNullableBoolean(record.default),
|
|
created_at: toStringOrNull(record.created_at),
|
|
updated_at: toStringOrNull(record.updated_at),
|
|
}
|
|
}
|
|
|
|
export function mapReply(value: unknown): EmailBisonReply {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
uuid: toStringOrNull(record.uuid),
|
|
folder: toStringOrNull(record.folder),
|
|
subject: toStringOrNull(record.subject),
|
|
read: toNullableBoolean(record.read),
|
|
interested: toNullableBoolean(record.interested),
|
|
automated_reply: toNullableBoolean(record.automated_reply),
|
|
html_body: toStringOrNull(record.html_body),
|
|
text_body: toStringOrNull(record.text_body),
|
|
raw_body: toStringOrNull(record.raw_body),
|
|
headers: toStringOrNull(record.headers),
|
|
date_received: toStringOrNull(record.date_received),
|
|
type: toStringOrNull(record.type),
|
|
tracked_reply: toNullableBoolean(record.tracked_reply),
|
|
scheduled_email_id: toStringNumberOrNull(record.scheduled_email_id),
|
|
campaign_id: toStringNumberOrNull(record.campaign_id),
|
|
lead_id: toNullableNumber(record.lead_id),
|
|
sender_email_id: toNullableNumber(record.sender_email_id),
|
|
raw_message_id: toStringOrNull(record.raw_message_id),
|
|
from_name: toStringOrNull(record.from_name),
|
|
from_email_address: toStringOrNull(record.from_email_address),
|
|
primary_to_email_address: toStringOrNull(record.primary_to_email_address),
|
|
to: toArray(record.to).map(mapReplyAddress),
|
|
cc: toStringOrNull(record.cc),
|
|
bcc: toStringOrNull(record.bcc),
|
|
parent_id: toStringNumberOrNull(record.parent_id),
|
|
attachments: toArray(record.attachments).map(mapReplyAttachment),
|
|
created_at: toStringOrNull(record.created_at),
|
|
updated_at: toStringOrNull(record.updated_at),
|
|
}
|
|
}
|
|
|
|
export function actionOutput(value: unknown): { success: boolean; message: string | null } {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
success: record.success === true,
|
|
message: toStringOrNull(record.message),
|
|
}
|
|
}
|
|
|
|
export const leadOutputs = {
|
|
id: { type: 'number', description: 'Lead ID' },
|
|
first_name: { type: 'string', description: 'Lead first name' },
|
|
last_name: { type: 'string', description: 'Lead last name' },
|
|
email: { type: 'string', description: 'Lead email address' },
|
|
title: { type: 'string', description: 'Lead title', optional: true },
|
|
company: { type: 'string', description: 'Lead company', optional: true },
|
|
notes: { type: 'string', description: 'Lead notes', optional: true },
|
|
status: { type: 'string', description: 'Lead status', optional: true },
|
|
custom_variables: {
|
|
type: 'array',
|
|
description: 'Lead custom variables',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string', description: 'Custom variable name' },
|
|
value: { type: 'string', description: 'Custom variable value', optional: true },
|
|
},
|
|
},
|
|
},
|
|
lead_campaign_data: {
|
|
type: 'array',
|
|
description: 'Lead campaign data returned by Email Bison',
|
|
},
|
|
overall_stats: {
|
|
type: 'object',
|
|
description: 'Lead engagement stats',
|
|
properties: {
|
|
emails_sent: { type: 'number', description: 'Emails sent' },
|
|
opens: { type: 'number', description: 'Email opens' },
|
|
replies: { type: 'number', description: 'Replies' },
|
|
unique_replies: { type: 'number', description: 'Unique replies' },
|
|
unique_opens: { type: 'number', description: 'Unique opens' },
|
|
},
|
|
},
|
|
created_at: { type: 'string', description: 'Lead creation timestamp', optional: true },
|
|
updated_at: { type: 'string', description: 'Lead update timestamp', optional: true },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
const tagProperties = {
|
|
id: { type: 'number', description: 'Tag ID' },
|
|
name: { type: 'string', description: 'Tag name' },
|
|
default: { type: 'boolean', description: 'Whether this is a default tag' },
|
|
created_at: { type: 'string', description: 'Tag creation timestamp', optional: true },
|
|
updated_at: { type: 'string', description: 'Tag update timestamp', optional: true },
|
|
} satisfies Record<string, OutputProperty>
|
|
|
|
const campaignTagProperties = {
|
|
id: { type: 'number', description: 'Tag ID' },
|
|
name: { type: 'string', description: 'Tag name' },
|
|
default: { type: 'boolean', description: 'Whether this is a default tag' },
|
|
} satisfies Record<string, OutputProperty>
|
|
|
|
export const listLeadsOutputs = {
|
|
leads: {
|
|
type: 'array',
|
|
description: 'List of leads',
|
|
items: {
|
|
type: 'object',
|
|
properties: leadOutputs,
|
|
},
|
|
},
|
|
count: { type: 'number', description: 'Number of leads returned' },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const campaignOutputs = {
|
|
id: { type: 'number', description: 'Campaign ID' },
|
|
uuid: { type: 'string', description: 'Campaign UUID', optional: true },
|
|
name: { type: 'string', description: 'Campaign name' },
|
|
type: { type: 'string', description: 'Campaign type', optional: true },
|
|
status: { type: 'string', description: 'Campaign status', optional: true },
|
|
emails_sent: { type: 'number', description: 'Emails sent' },
|
|
opened: { type: 'number', description: 'Total opens' },
|
|
unique_opens: { type: 'number', description: 'Unique opens' },
|
|
replied: { type: 'number', description: 'Total replies' },
|
|
unique_replies: { type: 'number', description: 'Unique replies' },
|
|
bounced: { type: 'number', description: 'Bounces' },
|
|
unsubscribed: { type: 'number', description: 'Unsubscribes' },
|
|
interested: { type: 'number', description: 'Interested replies' },
|
|
total_leads_contacted: { type: 'number', description: 'Total leads contacted' },
|
|
total_leads: { type: 'number', description: 'Total leads' },
|
|
max_emails_per_day: { type: 'number', description: 'Maximum emails per day', optional: true },
|
|
max_new_leads_per_day: {
|
|
type: 'number',
|
|
description: 'Maximum new leads per day',
|
|
optional: true,
|
|
},
|
|
plain_text: {
|
|
type: 'boolean',
|
|
description: 'Whether campaign emails are plain text',
|
|
optional: true,
|
|
},
|
|
open_tracking: {
|
|
type: 'boolean',
|
|
description: 'Whether open tracking is enabled',
|
|
optional: true,
|
|
},
|
|
can_unsubscribe: {
|
|
type: 'boolean',
|
|
description: 'Whether recipients can unsubscribe',
|
|
optional: true,
|
|
},
|
|
unsubscribe_text: { type: 'string', description: 'Unsubscribe text', optional: true },
|
|
tags: {
|
|
type: 'array',
|
|
description: 'Campaign tags',
|
|
items: { type: 'object', properties: campaignTagProperties },
|
|
},
|
|
created_at: { type: 'string', description: 'Campaign creation timestamp', optional: true },
|
|
updated_at: { type: 'string', description: 'Campaign update timestamp', optional: true },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const listCampaignsOutputs = {
|
|
campaigns: {
|
|
type: 'array',
|
|
description: 'List of campaigns',
|
|
items: {
|
|
type: 'object',
|
|
properties: campaignOutputs,
|
|
},
|
|
},
|
|
count: { type: 'number', description: 'Number of campaigns returned' },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const actionOutputs = {
|
|
success: { type: 'boolean', description: 'Whether the action succeeded' },
|
|
message: { type: 'string', description: 'Action message', optional: true },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const listRepliesOutputs = {
|
|
replies: {
|
|
type: 'array',
|
|
description: 'List of replies',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'number', description: 'Reply ID' },
|
|
subject: { type: 'string', description: 'Reply subject', optional: true },
|
|
text_body: { type: 'string', description: 'Reply text body', optional: true },
|
|
from_email_address: { type: 'string', description: 'Sender email', optional: true },
|
|
primary_to_email_address: {
|
|
type: 'string',
|
|
description: 'Primary recipient',
|
|
optional: true,
|
|
},
|
|
date_received: { type: 'string', description: 'Date received', optional: true },
|
|
interested: { type: 'boolean', description: 'Whether the reply is marked interested' },
|
|
read: { type: 'boolean', description: 'Whether the reply is read' },
|
|
},
|
|
},
|
|
},
|
|
count: { type: 'number', description: 'Number of replies returned' },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const tagOutputs = {
|
|
id: { type: 'number', description: 'Tag ID' },
|
|
name: { type: 'string', description: 'Tag name' },
|
|
default: { type: 'boolean', description: 'Whether this is a default tag' },
|
|
created_at: { type: 'string', description: 'Tag creation timestamp', optional: true },
|
|
updated_at: { type: 'string', description: 'Tag update timestamp', optional: true },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
export const listTagsOutputs = {
|
|
tags: {
|
|
type: 'array',
|
|
description: 'List of tags',
|
|
items: {
|
|
type: 'object',
|
|
properties: tagProperties,
|
|
},
|
|
},
|
|
count: { type: 'number', description: 'Number of tags returned' },
|
|
} satisfies NonNullable<ToolConfig['outputs']>
|
|
|
|
function mapLeadStats(record: Record<string, unknown>): EmailBisonLeadStats {
|
|
return {
|
|
emails_sent: toNullableNumber(record.emails_sent),
|
|
opens: toNullableNumber(record.opens),
|
|
replies: toNullableNumber(record.replies),
|
|
unique_replies: toNullableNumber(record.unique_replies),
|
|
unique_opens: toNullableNumber(record.unique_opens),
|
|
}
|
|
}
|
|
|
|
function mapReplyAddress(value: unknown): EmailBisonReplyAddress {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
name: toStringOrNull(record.name),
|
|
address: toStringOrNull(record.address),
|
|
}
|
|
}
|
|
|
|
function mapReplyAttachment(value: unknown): EmailBisonReplyAttachment {
|
|
const record = toRecord(value)
|
|
|
|
return {
|
|
id: toNullableNumber(record.id),
|
|
uuid: toStringOrNull(record.uuid),
|
|
reply_id: toNullableNumber(record.reply_id),
|
|
file_name: toStringOrNull(record.file_name),
|
|
download_url: toStringOrNull(record.download_url),
|
|
created_at: toStringOrNull(record.created_at),
|
|
updated_at: toStringOrNull(record.updated_at),
|
|
}
|
|
}
|
|
|
|
function toRecord(value: unknown): Record<string, unknown> {
|
|
return isRecordLike(value) ? value : {}
|
|
}
|
|
|
|
function toArray(value: unknown): unknown[] {
|
|
return Array.isArray(value) ? value : []
|
|
}
|
|
|
|
function toStringOrNull(value: unknown): string | null {
|
|
if (value === undefined || value === null) return null
|
|
return String(value)
|
|
}
|
|
|
|
function toStringNumberOrNull(value: unknown): number | string | null {
|
|
if (typeof value === 'number' || typeof value === 'string') return value
|
|
return null
|
|
}
|
|
|
|
function toNumber(value: unknown): number | null {
|
|
if (typeof value === 'number') return value
|
|
if (typeof value === 'string') {
|
|
const parsed = Number(value)
|
|
return Number.isFinite(parsed) ? parsed : null
|
|
}
|
|
return null
|
|
}
|
|
|
|
function toNullableNumber(value: unknown): number | null {
|
|
if (value === undefined || value === null) return null
|
|
return toNumber(value)
|
|
}
|
|
|
|
function toNullableBoolean(value: unknown): boolean | null {
|
|
if (value === undefined || value === null) return null
|
|
return typeof value === 'boolean' ? value : null
|
|
}
|