chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import type {
|
||||
LoopsCheckContactSuppressionParams,
|
||||
LoopsCheckContactSuppressionResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsCheckContactSuppressionTool: ToolConfig<
|
||||
LoopsCheckContactSuppressionParams,
|
||||
LoopsCheckContactSuppressionResponse
|
||||
> = {
|
||||
id: 'loops_check_contact_suppression',
|
||||
name: 'Loops Check Contact Suppression',
|
||||
description:
|
||||
'Check whether a Loops contact is on the suppression list (bounced, complained, or unsubscribed) by email address or userId.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The contact email address to check (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact userId to check (at least one of email or userId is required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to check suppression status')
|
||||
}
|
||||
const base = 'https://app.loops.so/api/v1/contacts/suppression'
|
||||
if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}`
|
||||
return `${base}?userId=${encodeURIComponent(params.userId!.trim())}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (data.isSuppressed == null) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
contactId: null,
|
||||
email: null,
|
||||
userId: null,
|
||||
isSuppressed: false,
|
||||
removalQuotaLimit: null,
|
||||
removalQuotaRemaining: null,
|
||||
},
|
||||
error: data.message ?? 'Failed to check contact suppression status',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
contactId: (data.contact?.id as string) ?? null,
|
||||
email: (data.contact?.email as string) ?? null,
|
||||
userId: (data.contact?.userId as string) ?? null,
|
||||
isSuppressed: (data.isSuppressed as boolean) ?? false,
|
||||
removalQuotaLimit: (data.removalQuota?.limit as number) ?? null,
|
||||
removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
contactId: { type: 'string', description: 'The Loops-assigned contact ID', optional: true },
|
||||
email: { type: 'string', description: 'The contact email address', optional: true },
|
||||
userId: { type: 'string', description: 'The contact userId', optional: true },
|
||||
isSuppressed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the contact is on the suppression list',
|
||||
},
|
||||
removalQuotaLimit: {
|
||||
type: 'number',
|
||||
description: 'Total suppression-removal quota for the team',
|
||||
optional: true,
|
||||
},
|
||||
removalQuotaRemaining: {
|
||||
type: 'number',
|
||||
description: 'Remaining suppression-removal quota for the team',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { LoopsCreateContactParams, LoopsCreateContactResponse } from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsCreateContactTool: ToolConfig<
|
||||
LoopsCreateContactParams,
|
||||
LoopsCreateContactResponse
|
||||
> = {
|
||||
id: 'loops_create_contact',
|
||||
name: 'Loops Create Contact',
|
||||
description:
|
||||
'Create a new contact in your Loops audience with an email address and optional properties like name, user group, and mailing list subscriptions.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The email address for the new contact',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact last name',
|
||||
},
|
||||
source: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom source value replacing the default "API"',
|
||||
},
|
||||
subscribed: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the contact receives campaign emails (defaults to true)',
|
||||
},
|
||||
userGroup: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Group to segment the contact into (one group per contact)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unique user identifier from your application',
|
||||
},
|
||||
mailingLists: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
|
||||
},
|
||||
customProperties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Custom contact properties as key-value pairs (string, number, boolean, or date values)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/contacts/create',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
// Apply custom properties first so standard fields always take precedence
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
if (params.customProperties) {
|
||||
const props =
|
||||
typeof params.customProperties === 'string'
|
||||
? JSON.parse(params.customProperties)
|
||||
: params.customProperties
|
||||
Object.assign(body, props)
|
||||
}
|
||||
|
||||
body.email = params.email.trim()
|
||||
if (params.firstName) body.firstName = params.firstName.trim()
|
||||
if (params.lastName) body.lastName = params.lastName.trim()
|
||||
if (params.source) body.source = params.source.trim()
|
||||
if (params.subscribed != null) body.subscribed = params.subscribed
|
||||
if (params.userGroup) body.userGroup = params.userGroup.trim()
|
||||
if (params.userId) body.userId = params.userId.trim()
|
||||
|
||||
if (params.mailingLists) {
|
||||
body.mailingLists =
|
||||
typeof params.mailingLists === 'string'
|
||||
? JSON.parse(params.mailingLists)
|
||||
: params.mailingLists
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
id: null,
|
||||
},
|
||||
error: data.message ?? 'Failed to create contact',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
id: data.id ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the contact was created successfully' },
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The Loops-assigned ID of the created contact',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
LoopsCreateContactPropertyParams,
|
||||
LoopsCreateContactPropertyResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsCreateContactPropertyTool: ToolConfig<
|
||||
LoopsCreateContactPropertyParams,
|
||||
LoopsCreateContactPropertyResponse
|
||||
> = {
|
||||
id: 'loops_create_contact_property',
|
||||
name: 'Loops Create Contact Property',
|
||||
description:
|
||||
'Create a new custom contact property in your Loops account. The property name must be in camelCase format.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The property name in camelCase format (e.g., "favoriteColor")',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The property data type (e.g., "string", "number", "boolean", "date")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/contacts/properties',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => ({
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: data.message ?? 'Failed to create contact property',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the contact property was created successfully',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { LoopsDeleteContactParams, LoopsDeleteContactResponse } from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsDeleteContactTool: ToolConfig<
|
||||
LoopsDeleteContactParams,
|
||||
LoopsDeleteContactResponse
|
||||
> = {
|
||||
id: 'loops_delete_contact',
|
||||
name: 'Loops Delete Contact',
|
||||
description:
|
||||
'Delete a contact from Loops by email address or userId. At least one identifier must be provided.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The email address of the contact to delete (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The userId of the contact to delete (at least one of email or userId is required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/contacts/delete',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to delete a contact')
|
||||
}
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.email) body.email = params.email.trim()
|
||||
if (params.userId) body.userId = params.userId.trim()
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
message: data.message ?? 'Failed to delete contact',
|
||||
},
|
||||
error: data.message ?? 'Failed to delete contact',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
message: data.message ?? 'Contact deleted.',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the contact was deleted successfully' },
|
||||
message: { type: 'string', description: 'Status message from the API' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { LoopsFindContactParams, LoopsFindContactResponse } from '@/tools/loops/types'
|
||||
import { LOOPS_CONTACT_OUTPUT_PROPERTIES } from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsFindContactTool: ToolConfig<LoopsFindContactParams, LoopsFindContactResponse> = {
|
||||
id: 'loops_find_contact',
|
||||
name: 'Loops Find Contact',
|
||||
description:
|
||||
'Find a contact in Loops by email address or userId. Returns an array of matching contacts with all their properties including name, subscription status, user group, and mailing lists.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The contact email address to search for (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact userId to search for (at least one of email or userId is required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to find a contact')
|
||||
}
|
||||
const base = 'https://app.loops.so/api/v1/contacts/find'
|
||||
if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}`
|
||||
return `${base}?userId=${encodeURIComponent(params.userId!.trim())}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
contacts: [],
|
||||
},
|
||||
error: data.message ?? 'Failed to find contact',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
contacts: data.map((contact: Record<string, unknown>) => ({
|
||||
id: (contact.id as string) ?? '',
|
||||
email: (contact.email as string) ?? '',
|
||||
firstName: (contact.firstName as string) ?? null,
|
||||
lastName: (contact.lastName as string) ?? null,
|
||||
source: (contact.source as string) ?? null,
|
||||
subscribed: (contact.subscribed as boolean) ?? false,
|
||||
userGroup: (contact.userGroup as string) ?? null,
|
||||
userId: (contact.userId as string) ?? null,
|
||||
mailingLists: (contact.mailingLists as Record<string, boolean>) ?? {},
|
||||
optInStatus: (contact.optInStatus as string) ?? null,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
contacts: {
|
||||
type: 'array',
|
||||
description: 'Array of matching contact objects (empty array if no match found)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: LOOPS_CONTACT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
LoopsGetTransactionalEmailParams,
|
||||
LoopsGetTransactionalEmailResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsGetTransactionalEmailTool: ToolConfig<
|
||||
LoopsGetTransactionalEmailParams,
|
||||
LoopsGetTransactionalEmailResponse
|
||||
> = {
|
||||
id: 'loops_get_transactional_email',
|
||||
name: 'Loops Get Transactional Email',
|
||||
description:
|
||||
'Retrieve a single transactional email template from your Loops account by its ID, including its data variables and draft/published message IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
transactionalId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the transactional email template to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://app.loops.so/api/v1/transactional-emails/${encodeURIComponent(params.transactionalId.trim())}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.id) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: null,
|
||||
name: null,
|
||||
draftEmailMessageId: null,
|
||||
publishedEmailMessageId: null,
|
||||
transactionalGroupId: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
dataVariables: [],
|
||||
},
|
||||
error: data.message ?? 'Failed to get transactional email',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: (data.id as string) ?? null,
|
||||
name: (data.name as string) ?? null,
|
||||
draftEmailMessageId: (data.draftEmailMessageId as string) ?? null,
|
||||
publishedEmailMessageId: (data.publishedEmailMessageId as string) ?? null,
|
||||
transactionalGroupId: (data.transactionalGroupId as string) ?? null,
|
||||
createdAt: (data.createdAt as string) ?? null,
|
||||
updatedAt: (data.updatedAt as string) ?? null,
|
||||
dataVariables: (data.dataVariables as string[]) ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'The transactional email template ID', optional: true },
|
||||
name: { type: 'string', description: 'The template name', optional: true },
|
||||
draftEmailMessageId: {
|
||||
type: 'string',
|
||||
description: 'ID of the draft email message, if any',
|
||||
optional: true,
|
||||
},
|
||||
publishedEmailMessageId: {
|
||||
type: 'string',
|
||||
description: 'ID of the published email message, if any',
|
||||
optional: true,
|
||||
},
|
||||
transactionalGroupId: {
|
||||
type: 'string',
|
||||
description: 'ID of the transactional group this template belongs to, if any',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)', optional: true },
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
description: 'Last updated timestamp (ISO 8601)',
|
||||
optional: true,
|
||||
},
|
||||
dataVariables: {
|
||||
type: 'array',
|
||||
description: 'Template data variable names',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export { loopsCheckContactSuppressionTool } from '@/tools/loops/check_contact_suppression'
|
||||
export { loopsCreateContactTool } from '@/tools/loops/create_contact'
|
||||
export { loopsCreateContactPropertyTool } from '@/tools/loops/create_contact_property'
|
||||
export { loopsDeleteContactTool } from '@/tools/loops/delete_contact'
|
||||
export { loopsFindContactTool } from '@/tools/loops/find_contact'
|
||||
export { loopsGetTransactionalEmailTool } from '@/tools/loops/get_transactional_email'
|
||||
export { loopsListContactPropertiesTool } from '@/tools/loops/list_contact_properties'
|
||||
export { loopsListMailingListsTool } from '@/tools/loops/list_mailing_lists'
|
||||
export { loopsListTransactionalEmailsTool } from '@/tools/loops/list_transactional_emails'
|
||||
export { loopsRemoveContactSuppressionTool } from '@/tools/loops/remove_contact_suppression'
|
||||
export { loopsSendEventTool } from '@/tools/loops/send_event'
|
||||
export { loopsSendTransactionalEmailTool } from '@/tools/loops/send_transactional_email'
|
||||
export { loopsUpdateContactTool } from '@/tools/loops/update_contact'
|
||||
@@ -0,0 +1,87 @@
|
||||
import type {
|
||||
LoopsListContactPropertiesParams,
|
||||
LoopsListContactPropertiesResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsListContactPropertiesTool: ToolConfig<
|
||||
LoopsListContactPropertiesParams,
|
||||
LoopsListContactPropertiesResponse
|
||||
> = {
|
||||
id: 'loops_list_contact_properties',
|
||||
name: 'Loops List Contact Properties',
|
||||
description:
|
||||
'Retrieve a list of contact properties from your Loops account. Returns each property with its key, label, and data type. Can filter to show all properties or only custom ones.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
list: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter type: "all" for all properties (default) or "custom" for custom properties only',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const base = 'https://app.loops.so/api/v1/contacts/properties'
|
||||
if (params.list) return `${base}?list=${encodeURIComponent(params.list)}`
|
||||
return base
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
properties: [],
|
||||
},
|
||||
error: data.message ?? 'Failed to list contact properties',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
properties: data.map((prop: Record<string, unknown>) => ({
|
||||
key: (prop.key as string) ?? '',
|
||||
label: (prop.label as string) ?? '',
|
||||
type: (prop.type as string) ?? '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
properties: {
|
||||
type: 'array',
|
||||
description: 'Array of contact property objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'The property key (camelCase identifier)' },
|
||||
label: { type: 'string', description: 'The property display label' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The property data type (string, number, boolean, date)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type {
|
||||
LoopsListMailingListsParams,
|
||||
LoopsListMailingListsResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsListMailingListsTool: ToolConfig<
|
||||
LoopsListMailingListsParams,
|
||||
LoopsListMailingListsResponse
|
||||
> = {
|
||||
id: 'loops_list_mailing_lists',
|
||||
name: 'Loops List Mailing Lists',
|
||||
description:
|
||||
'Retrieve all mailing lists from your Loops account. Returns each list with its ID, name, description, and public/private status.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/lists',
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
mailingLists: [],
|
||||
},
|
||||
error: data.message ?? 'Failed to list mailing lists',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
mailingLists: data.map((list: Record<string, unknown>) => ({
|
||||
id: (list.id as string) ?? '',
|
||||
name: (list.name as string) ?? '',
|
||||
description: (list.description as string) ?? null,
|
||||
isPublic: (list.isPublic as boolean) ?? false,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
mailingLists: {
|
||||
type: 'array',
|
||||
description: 'Array of mailing list objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The mailing list ID' },
|
||||
name: { type: 'string', description: 'The mailing list name' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'The mailing list description (null if not set)',
|
||||
optional: true,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the list is public or private',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type {
|
||||
LoopsListTransactionalEmailsParams,
|
||||
LoopsListTransactionalEmailsResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsListTransactionalEmailsTool: ToolConfig<
|
||||
LoopsListTransactionalEmailsParams,
|
||||
LoopsListTransactionalEmailsResponse
|
||||
> = {
|
||||
id: 'loops_list_transactional_emails',
|
||||
name: 'Loops List Transactional Emails',
|
||||
description:
|
||||
'Retrieve a list of published transactional email templates from your Loops account. Returns each template with its ID, name, created/updated timestamps, and data variables.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
perPage: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page (10-50, default: 20)',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from a previous response to fetch the next page',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const base = 'https://app.loops.so/api/v1/transactional-emails'
|
||||
const queryParams: string[] = []
|
||||
if (params.perPage) queryParams.push(`perPage=${encodeURIComponent(params.perPage)}`)
|
||||
if (params.cursor) queryParams.push(`cursor=${encodeURIComponent(params.cursor)}`)
|
||||
return queryParams.length > 0 ? `${base}?${queryParams.join('&')}` : base
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.data && !Array.isArray(data)) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
transactionalEmails: [],
|
||||
pagination: {
|
||||
totalResults: 0,
|
||||
returnedResults: 0,
|
||||
perPage: 0,
|
||||
totalPages: 0,
|
||||
nextCursor: null,
|
||||
nextPage: null,
|
||||
},
|
||||
},
|
||||
error: data.message ?? 'Failed to list transactional emails',
|
||||
}
|
||||
}
|
||||
|
||||
const emails = data.data ?? data ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
transactionalEmails: emails.map((email: Record<string, unknown>) => ({
|
||||
id: (email.id as string) ?? '',
|
||||
name: (email.name as string) ?? '',
|
||||
createdAt: (email.createdAt as string) ?? '',
|
||||
updatedAt: (email.updatedAt as string) ?? '',
|
||||
// Deprecated alias of updatedAt, kept for backwards compatibility with the old
|
||||
// (now-removed) /api/v1/transactional list endpoint, which returned this field.
|
||||
lastUpdated: (email.updatedAt as string) ?? '',
|
||||
dataVariables: (email.dataVariables as string[]) ?? [],
|
||||
})),
|
||||
pagination: {
|
||||
totalResults: (data.pagination?.totalResults as number) ?? emails.length,
|
||||
returnedResults: (data.pagination?.returnedResults as number) ?? emails.length,
|
||||
perPage: (data.pagination?.perPage as number) ?? 20,
|
||||
totalPages: (data.pagination?.totalPages as number) ?? 1,
|
||||
nextCursor: (data.pagination?.nextCursor as string) ?? null,
|
||||
nextPage: (data.pagination?.nextPage as string) ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
transactionalEmails: {
|
||||
type: 'array',
|
||||
description: 'Array of published transactional email templates',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The transactional email template ID' },
|
||||
name: { type: 'string', description: 'The template name' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' },
|
||||
lastUpdated: {
|
||||
type: 'string',
|
||||
description: 'Deprecated alias of updatedAt, kept for backwards compatibility',
|
||||
},
|
||||
dataVariables: {
|
||||
type: 'array',
|
||||
description: 'Template data variable names',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
totalResults: { type: 'number', description: 'Total number of results' },
|
||||
returnedResults: { type: 'number', description: 'Number of results returned' },
|
||||
perPage: { type: 'number', description: 'Results per page' },
|
||||
totalPages: { type: 'number', description: 'Total number of pages' },
|
||||
nextCursor: {
|
||||
type: 'string',
|
||||
description: 'Cursor for next page (null if no more pages)',
|
||||
optional: true,
|
||||
},
|
||||
nextPage: {
|
||||
type: 'string',
|
||||
description: 'URL for next page (null if no more pages)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
LoopsRemoveContactSuppressionParams,
|
||||
LoopsRemoveContactSuppressionResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsRemoveContactSuppressionTool: ToolConfig<
|
||||
LoopsRemoveContactSuppressionParams,
|
||||
LoopsRemoveContactSuppressionResponse
|
||||
> = {
|
||||
id: 'loops_remove_contact_suppression',
|
||||
name: 'Loops Remove Contact Suppression',
|
||||
description:
|
||||
'Remove a Loops contact from the suppression list by email address or userId, allowing them to receive emails again. Subject to a team removal quota.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The contact email address to remove from suppression (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'The contact userId to remove from suppression (at least one of email or userId is required)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to remove suppression')
|
||||
}
|
||||
const base = 'https://app.loops.so/api/v1/contacts/suppression'
|
||||
if (params.email) return `${base}?email=${encodeURIComponent(params.email.trim())}`
|
||||
return `${base}?userId=${encodeURIComponent(params.userId!.trim())}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
message: data.message ?? 'Failed to remove contact suppression',
|
||||
removalQuotaLimit: (data.removalQuota?.limit as number) ?? null,
|
||||
removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null,
|
||||
},
|
||||
error: data.message ?? 'Failed to remove contact suppression',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
message: data.message ?? 'Contact removed from suppression list.',
|
||||
removalQuotaLimit: (data.removalQuota?.limit as number) ?? null,
|
||||
removalQuotaRemaining: (data.removalQuota?.remaining as number) ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the contact was removed from suppression successfully',
|
||||
},
|
||||
message: { type: 'string', description: 'Status message from the API', optional: true },
|
||||
removalQuotaLimit: {
|
||||
type: 'number',
|
||||
description: 'Total suppression-removal quota for the team',
|
||||
optional: true,
|
||||
},
|
||||
removalQuotaRemaining: {
|
||||
type: 'number',
|
||||
description: 'Remaining suppression-removal quota for the team',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { LoopsSendEventParams, LoopsSendEventResponse } from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsSendEventTool: ToolConfig<LoopsSendEventParams, LoopsSendEventResponse> = {
|
||||
id: 'loops_send_event',
|
||||
name: 'Loops Send Event',
|
||||
description:
|
||||
'Send an event to Loops to trigger automated email sequences for a contact. Identify the contact by email or userId and include optional event properties and mailing list changes.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The email address of the contact (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The userId of the contact (at least one of email or userId is required)',
|
||||
},
|
||||
eventName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the event to trigger',
|
||||
},
|
||||
eventProperties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event data as key-value pairs (string, number, boolean, or date values)',
|
||||
},
|
||||
mailingLists: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/events/send',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to send an event')
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
eventName: params.eventName.trim(),
|
||||
}
|
||||
|
||||
if (params.email) body.email = params.email.trim()
|
||||
if (params.userId) body.userId = params.userId.trim()
|
||||
|
||||
if (params.eventProperties) {
|
||||
body.eventProperties =
|
||||
typeof params.eventProperties === 'string'
|
||||
? JSON.parse(params.eventProperties)
|
||||
: params.eventProperties
|
||||
}
|
||||
|
||||
if (params.mailingLists) {
|
||||
body.mailingLists =
|
||||
typeof params.mailingLists === 'string'
|
||||
? JSON.parse(params.mailingLists)
|
||||
: params.mailingLists
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: data.message ?? 'Failed to send event',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the event was sent successfully' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type {
|
||||
LoopsSendTransactionalEmailParams,
|
||||
LoopsSendTransactionalEmailResponse,
|
||||
} from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsSendTransactionalEmailTool: ToolConfig<
|
||||
LoopsSendTransactionalEmailParams,
|
||||
LoopsSendTransactionalEmailResponse
|
||||
> = {
|
||||
id: 'loops_send_transactional_email',
|
||||
name: 'Loops Send Transactional Email',
|
||||
description:
|
||||
'Send a transactional email to a recipient using a Loops template. Supports dynamic data variables for personalization and optionally adds the recipient to your audience.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The email address of the recipient',
|
||||
},
|
||||
transactionalId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the transactional email template to send',
|
||||
},
|
||||
dataVariables: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Template data variables as key-value pairs (string or number values)',
|
||||
},
|
||||
addToAudience: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether to create the recipient as a contact if they do not already exist (default: false)',
|
||||
},
|
||||
attachments: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of file attachments. Each object must have filename (string), contentType (MIME type string), and data (base64-encoded string).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/transactional',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
email: params.email.trim(),
|
||||
transactionalId: params.transactionalId.trim(),
|
||||
}
|
||||
|
||||
if (params.dataVariables) {
|
||||
body.dataVariables =
|
||||
typeof params.dataVariables === 'string'
|
||||
? JSON.parse(params.dataVariables)
|
||||
: params.dataVariables
|
||||
}
|
||||
|
||||
if (params.addToAudience != null) {
|
||||
body.addToAudience = params.addToAudience
|
||||
}
|
||||
|
||||
if (params.attachments) {
|
||||
body.attachments =
|
||||
typeof params.attachments === 'string'
|
||||
? JSON.parse(params.attachments)
|
||||
: params.attachments
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
},
|
||||
error: data.message ?? 'Failed to send transactional email',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the transactional email was sent successfully',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface LoopsBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface LoopsCreateContactParams extends LoopsBaseParams {
|
||||
email: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
source?: string
|
||||
subscribed?: boolean
|
||||
userGroup?: string
|
||||
userId?: string
|
||||
mailingLists?: string | Record<string, boolean>
|
||||
customProperties?: string | Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface LoopsUpdateContactParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
source?: string
|
||||
subscribed?: boolean
|
||||
userGroup?: string
|
||||
mailingLists?: string | Record<string, boolean>
|
||||
customProperties?: string | Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface LoopsFindContactParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export interface LoopsDeleteContactParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export interface LoopsSendTransactionalEmailParams extends LoopsBaseParams {
|
||||
email: string
|
||||
transactionalId: string
|
||||
dataVariables?: string | Record<string, string | number>
|
||||
addToAudience?: boolean
|
||||
attachments?: string | { filename: string; contentType: string; data: string }[]
|
||||
}
|
||||
|
||||
export interface LoopsSendEventParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
eventName: string
|
||||
eventProperties?: string | Record<string, string | number | boolean>
|
||||
mailingLists?: string | Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface LoopsListMailingListsParams extends LoopsBaseParams {}
|
||||
|
||||
export interface LoopsListTransactionalEmailsParams extends LoopsBaseParams {
|
||||
perPage?: string
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export interface LoopsCreateContactPropertyParams extends LoopsBaseParams {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface LoopsListContactPropertiesParams extends LoopsBaseParams {
|
||||
list?: string
|
||||
}
|
||||
|
||||
export interface LoopsCheckContactSuppressionParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export interface LoopsRemoveContactSuppressionParams extends LoopsBaseParams {
|
||||
email?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
export interface LoopsGetTransactionalEmailParams extends LoopsBaseParams {
|
||||
transactionalId: string
|
||||
}
|
||||
|
||||
interface LoopsContact {
|
||||
id: string
|
||||
email: string
|
||||
firstName: string | null
|
||||
lastName: string | null
|
||||
source: string | null
|
||||
subscribed: boolean
|
||||
userGroup: string | null
|
||||
userId: string | null
|
||||
mailingLists: Record<string, boolean>
|
||||
optInStatus: string | null
|
||||
}
|
||||
|
||||
export interface LoopsCreateContactResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
id: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsUpdateContactResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
id: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsFindContactResponse extends ToolResponse {
|
||||
output: {
|
||||
contacts: LoopsContact[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsDeleteContactResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
message: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsSendTransactionalEmailResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsSendEventResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsListMailingListsResponse extends ToolResponse {
|
||||
output: {
|
||||
mailingLists: {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
isPublic: boolean
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsListTransactionalEmailsResponse extends ToolResponse {
|
||||
output: {
|
||||
transactionalEmails: {
|
||||
id: string
|
||||
name: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** @deprecated Alias of updatedAt, kept for backwards compatibility */
|
||||
lastUpdated: string
|
||||
dataVariables: string[]
|
||||
}[]
|
||||
pagination: {
|
||||
totalResults: number
|
||||
returnedResults: number
|
||||
perPage: number
|
||||
totalPages: number
|
||||
nextCursor: string | null
|
||||
nextPage: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsCreateContactPropertyResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsListContactPropertiesResponse extends ToolResponse {
|
||||
output: {
|
||||
properties: {
|
||||
key: string
|
||||
label: string
|
||||
type: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsCheckContactSuppressionResponse extends ToolResponse {
|
||||
output: {
|
||||
contactId: string | null
|
||||
email: string | null
|
||||
userId: string | null
|
||||
isSuppressed: boolean
|
||||
removalQuotaLimit: number | null
|
||||
removalQuotaRemaining: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsRemoveContactSuppressionResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
message: string | null
|
||||
removalQuotaLimit: number | null
|
||||
removalQuotaRemaining: number | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoopsGetTransactionalEmailResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string | null
|
||||
name: string | null
|
||||
draftEmailMessageId: string | null
|
||||
publishedEmailMessageId: string | null
|
||||
transactionalGroupId: string | null
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
dataVariables: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type LoopsResponse =
|
||||
| LoopsCreateContactResponse
|
||||
| LoopsUpdateContactResponse
|
||||
| LoopsFindContactResponse
|
||||
| LoopsDeleteContactResponse
|
||||
| LoopsSendTransactionalEmailResponse
|
||||
| LoopsSendEventResponse
|
||||
| LoopsListMailingListsResponse
|
||||
| LoopsListTransactionalEmailsResponse
|
||||
| LoopsCreateContactPropertyResponse
|
||||
| LoopsListContactPropertiesResponse
|
||||
| LoopsCheckContactSuppressionResponse
|
||||
| LoopsRemoveContactSuppressionResponse
|
||||
| LoopsGetTransactionalEmailResponse
|
||||
|
||||
export const LOOPS_CONTACT_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string' as const, description: 'Loops-assigned contact ID' },
|
||||
email: { type: 'string' as const, description: 'Contact email address' },
|
||||
firstName: { type: 'string' as const, description: 'Contact first name', optional: true },
|
||||
lastName: { type: 'string' as const, description: 'Contact last name', optional: true },
|
||||
source: {
|
||||
type: 'string' as const,
|
||||
description: 'Source the contact was created from',
|
||||
optional: true,
|
||||
},
|
||||
subscribed: {
|
||||
type: 'boolean' as const,
|
||||
description: 'Whether the contact receives campaign emails',
|
||||
},
|
||||
userGroup: { type: 'string' as const, description: 'Contact user group', optional: true },
|
||||
userId: { type: 'string' as const, description: 'External user identifier', optional: true },
|
||||
mailingLists: {
|
||||
type: 'object' as const,
|
||||
description: 'Mailing list IDs mapped to subscription status',
|
||||
optional: true,
|
||||
},
|
||||
optInStatus: {
|
||||
type: 'string' as const,
|
||||
description: 'Double opt-in status: pending, accepted, rejected, or null',
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { LoopsUpdateContactParams, LoopsUpdateContactResponse } from '@/tools/loops/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const loopsUpdateContactTool: ToolConfig<
|
||||
LoopsUpdateContactParams,
|
||||
LoopsUpdateContactResponse
|
||||
> = {
|
||||
id: 'loops_update_contact',
|
||||
name: 'Loops Update Contact',
|
||||
description:
|
||||
'Update an existing contact in Loops by email or userId. Creates a new contact if no match is found (upsert). Can update name, subscription status, user group, mailing lists, and custom properties.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Loops API key for authentication',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact email address (at least one of email or userId is required)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact userId (at least one of email or userId is required)',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact first name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The contact last name',
|
||||
},
|
||||
source: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom source value replacing the default "API"',
|
||||
},
|
||||
subscribed: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether the contact receives campaign emails (sending true re-subscribes unsubscribed contacts)',
|
||||
},
|
||||
userGroup: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Group to segment the contact into (one group per contact)',
|
||||
},
|
||||
mailingLists: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Mailing list IDs mapped to boolean values (true to subscribe, false to unsubscribe)',
|
||||
},
|
||||
customProperties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom contact properties as key-value pairs (send null to reset a property)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://app.loops.so/api/v1/contacts/update',
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.email && !params.userId) {
|
||||
throw new Error('At least one of email or userId is required to update a contact')
|
||||
}
|
||||
|
||||
// Apply custom properties first so standard fields always take precedence
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
if (params.customProperties) {
|
||||
const props =
|
||||
typeof params.customProperties === 'string'
|
||||
? JSON.parse(params.customProperties)
|
||||
: params.customProperties
|
||||
Object.assign(body, props)
|
||||
}
|
||||
|
||||
if (params.email) body.email = params.email.trim()
|
||||
if (params.userId) body.userId = params.userId.trim()
|
||||
if (params.firstName) body.firstName = params.firstName.trim()
|
||||
if (params.lastName) body.lastName = params.lastName.trim()
|
||||
if (params.source) body.source = params.source.trim()
|
||||
if (params.subscribed != null) body.subscribed = params.subscribed
|
||||
if (params.userGroup) body.userGroup = params.userGroup.trim()
|
||||
|
||||
if (params.mailingLists) {
|
||||
body.mailingLists =
|
||||
typeof params.mailingLists === 'string'
|
||||
? JSON.parse(params.mailingLists)
|
||||
: params.mailingLists
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
success: false,
|
||||
id: null,
|
||||
},
|
||||
error: data.message ?? 'Failed to update contact',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
id: data.id ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the contact was updated successfully' },
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The Loops-assigned ID of the updated or created contact',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user