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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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,
},
},
}
+148
View File
@@ -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',
},
},
}
+82
View File
@@ -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' },
},
}
+91
View File
@@ -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' },
},
},
}
+13
View File
@@ -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,
},
},
}
+112
View File
@@ -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',
},
},
}
+262
View File
@@ -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,
},
}
+152
View File
@@ -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,
},
},
}