diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 2417e6acb58..fcdab73224d 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2439,6 +2439,21 @@ export function JiraIcon(props: SVGProps) { ) } +export function LinqIcon(props: SVGProps) { + return ( + + + + + ) +} + export function LinearIcon(props: React.SVGProps) { return ( ) { ) } +export function TogetherIcon(props: SVGProps) { + return ( + + + + + + ) +} + +export function BasetenIcon(props: SVGProps) { + return ( + + + + ) +} + export function MondayIcon(props: SVGProps) { return ( = { linear_v2: LinearIcon, linkedin: LinkedInIcon, linkup: LinkupIcon, + linq: LinqIcon, loops: LoopsIcon, luma: LumaIcon, mailchimp: MailchimpIcon, diff --git a/apps/docs/content/docs/en/tools/linq.mdx b/apps/docs/content/docs/en/tools/linq.mdx new file mode 100644 index 00000000000..3bb5f0994e5 --- /dev/null +++ b/apps/docs/content/docs/en/tools/linq.mdx @@ -0,0 +1,787 @@ +--- +title: Linq +description: Send iMessage, SMS, and RCS messages and manage conversations with Linq +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Linq](https://linqapp.com/) is an API-first messaging platform that lets you reach people on iMessage, SMS, and RCS through real conversations. Linq handles the messaging plumbing — Apple and carrier delivery, group chats, read receipts, typing indicators, reactions, and attachments — behind a single REST API designed for programmatic access. + +**Why Linq?** +- **iMessage, SMS, and RCS in one API:** Send and receive across all three channels from the same chats and phone numbers, with automatic delivery over the best available service. +- **Rich conversations:** Media, link previews, screen and bubble effects, tapback reactions, inline replies, voice memos, and editable messages — not just plain text. +- **Group chat management:** Create groups, add and remove participants, rename chats, update icons, and leave conversations. +- **Capability checks:** Verify whether an address supports iMessage or RCS before you send, so you pick the right channel every time. +- **Real-time webhooks:** Subscribe to message, reaction, participant, and call events with HMAC-SHA256 signature verification. + +**Using Linq in Sim** + +Sim's Linq integration connects your agentic workflows directly to Linq using an API key. With 34 operations spanning chats, messages, attachments, phone numbers, capability checks, contact cards, and webhook subscriptions, you can build conversational messaging automations without writing backend code. + +**Key benefits of using Linq in Sim:** +- **Conversational agents:** Send and read messages in iMessage, SMS, or RCS chats, react with tapbacks, and reply inline to build natural two-way conversations. +- **Reliable delivery:** Check iMessage/RCS capability and set a preferred service so each message goes out over the right channel. +- **Files and voice:** Upload attachments up to 100MB and send media or voice memos straight from your workflow. +- **Event-driven flows:** Manage webhook subscriptions so workflows can react to inbound messages, reactions, and participant changes. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Reach people on iMessage, SMS, and RCS through Linq. Start chats, send messages with media, links, effects, and replies, send voice memos, react with tapbacks, manage group participants, check iMessage/RCS capability, configure contact cards, and subscribe to webhook events — all through a single Linq API key. + + + +## Tools + +### `linq_add_participant` + +Add a participant to a group chat (3+ existing participants) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the group chat | +| `handle` | string | Yes | Phone number \(E.164 format\) or email address of the participant to add | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Human-readable status message | +| `status` | string | Queued action status | +| `traceId` | string | Trace ID for the queued action | + +### `linq_check_imessage` + +Check whether an address (phone number or email) supports iMessage + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `address` | string | Yes | Phone number \(E.164 format\) or email address to check | +| `from` | string | No | Sender phone number to check from \(defaults to an available number\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `address` | string | The address that was checked | +| `available` | boolean | Whether the address supports iMessage | + +### `linq_check_rcs` + +Check whether an address (phone number or email) supports RCS + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `address` | string | Yes | Phone number \(E.164 format\) or email address to check | +| `from` | string | No | Sender phone number to check from \(defaults to an available number\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `address` | string | The address that was checked | +| `available` | boolean | Whether the address supports RCS | + +### `linq_create_attachment` + +Upload a file to Linq as a reusable attachment (max 100MB) and get an attachment ID to send in messages + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `file` | file | No | File to upload \(a UserFile from a file-upload field or a previous block\) | +| `fileContent` | string | No | Legacy base64-encoded file content fallback | +| `filename` | string | No | Override the file name \(defaults to the uploaded file name\) | +| `contentType` | string | No | Override the MIME type \(defaults to the uploaded file type\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachmentId` | string | Reusable attachment ID to reference when sending messages or voice memos | +| `downloadUrl` | string | URL the attachment can be downloaded from | +| `filename` | string | File name | +| `contentType` | string | MIME type of the file | +| `sizeBytes` | number | File size in bytes | +| `status` | string | Upload status | + +### `linq_create_chat` + +Start a new iMessage, SMS, or RCS chat and send the first message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `from` | string | Yes | Sender phone number in E.164 format \(e.g. +14155551234\) | +| `to` | array | Yes | Recipient handles \(phone numbers in E.164 format or email addresses\) | +| `text` | string | No | Text content of the first message. Optional, but at least one of text, media, attachment, or link is required | +| `mediaUrl` | string | No | Optional publicly accessible HTTPS URL of an image, video, or file to attach | +| `attachmentId` | string | No | Optional ID of a pre-uploaded attachment to send instead of a media URL | +| `preferredService` | string | No | Preferred delivery service: iMessage, SMS, or RCS | +| `effectName` | string | No | Optional iMessage effect name \(e.g. confetti, fireworks, lasers\) | +| `effectType` | string | No | Optional effect type: screen or bubble | +| `replyToMessageId` | string | No | Optional message ID to reply to inline | +| `replyToPartIndex` | number | No | Optional part index of the message being replied to | +| `idempotencyKey` | string | No | Optional idempotency key to safely retry the request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chatId` | string | ID of the created chat | +| `displayName` | string | Display name of the chat | +| `isGroup` | boolean | Whether the chat is a group chat | +| `service` | string | Delivery service used \(iMessage, SMS, RCS\) | +| `handles` | json | Participant handles in the chat | +| `healthStatus` | json | Messaging line health status | +| `message` | json | The sent message object with parts and delivery info | + +### `linq_create_contact_card` + +Set up a contact card (Name and Photo Sharing) for a phone number + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `phoneNumber` | string | Yes | Phone number in E.164 format the card applies to | +| `firstName` | string | Yes | First name to display | +| `lastName` | string | No | Last name to display | +| `imageUrl` | string | No | Profile photo URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `phoneNumber` | string | Phone number the card applies to | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `imageUrl` | string | Profile photo URL | +| `isActive` | boolean | Whether the card is active | + +### `linq_create_webhook_subscription` + +Subscribe an HTTPS endpoint to Linq webhook events + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `targetUrl` | string | Yes | HTTPS endpoint that will receive webhook events | +| `subscribedEvents` | array | Yes | Event types to subscribe to \(e.g. message.sent, message.delivered\) | +| `phoneNumbers` | array | No | E.164 phone numbers to filter events by \(omit for all numbers\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Subscription ID | +| `targetUrl` | string | Endpoint that receives events | +| `subscribedEvents` | json | Subscribed event types | +| `phoneNumbers` | json | Filtered phone numbers \(null = all\) | +| `isActive` | boolean | Whether the subscription is active | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | +| `signingSecret` | string | HMAC-SHA256 signing secret. Store securely — it cannot be retrieved again | + +### `linq_delete_attachment` + +Permanently delete an attachment owned by your account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `attachmentId` | string | Yes | The unique identifier of the attachment to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the attachment was deleted | + +### `linq_delete_message` + +Delete a message from the Linq API only (does not unsend it; recipients still see it) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `messageId` | string | Yes | The unique identifier of the message to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the message was deleted | + +### `linq_delete_webhook_subscription` + +Delete a webhook subscription from your account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `subscriptionId` | string | Yes | The unique identifier of the webhook subscription to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the subscription was deleted | + +### `linq_edit_message` + +Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iMessage only) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `messageId` | string | Yes | The unique identifier of the message to edit | +| `text` | string | Yes | New text content for the message part | +| `partIndex` | number | No | Index of the message part to edit \(defaults to 0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Message ID | +| `chatId` | string | ID of the chat the message belongs to | +| `isFromMe` | boolean | Whether the message was sent by you | +| `isDelivered` | boolean | Whether the message was delivered | +| `isRead` | boolean | Whether the message was read | +| `service` | string | Delivery service \(iMessage, SMS, RCS\) | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | +| `sentAt` | string | ISO 8601 sent timestamp | +| `parts` | json | Updated message parts with reactions | +| `message` | json | The full updated message object | + +### `linq_get_attachment` + +Retrieve metadata for an attachment, including its download URL and status + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `attachmentId` | string | Yes | The unique identifier of the attachment | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Attachment ID | +| `filename` | string | File name | +| `contentType` | string | MIME type of the file | +| `sizeBytes` | number | File size in bytes | +| `status` | string | Upload status \(pending, complete, failed\) | +| `downloadUrl` | string | URL to download the file | +| `createdAt` | string | ISO 8601 creation timestamp | + +### `linq_get_chat` + +Retrieve a chat by ID, including participants and line health + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Chat ID | +| `displayName` | string | Display name of the chat | +| `isGroup` | boolean | Whether the chat is a group chat | +| `isArchived` | boolean | Whether the chat is archived | +| `service` | string | Delivery service \(iMessage, SMS, RCS\) | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | +| `handles` | json | Participant handles in the chat | +| `healthStatus` | json | Messaging line health status | + +### `linq_get_contact_card` + +Retrieve contact cards, optionally filtered by phone number + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `phoneNumber` | string | No | E.164 phone number to filter by \(omit to return all cards\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `contactCards` | array | Contact cards on the account | +| ↳ `phoneNumber` | string | Phone number in E.164 format | +| ↳ `firstName` | string | First name | +| ↳ `lastName` | string | Last name | +| ↳ `imageUrl` | string | Profile photo URL | +| ↳ `isActive` | boolean | Whether the card is active | + +### `linq_get_message` + +Retrieve a single message by ID, including parts, reactions, and delivery status + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `messageId` | string | Yes | The unique identifier of the message | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Message ID | +| `chatId` | string | ID of the chat the message belongs to | +| `isFromMe` | boolean | Whether the message was sent by you | +| `isDelivered` | boolean | Whether the message was delivered | +| `isRead` | boolean | Whether the message was read | +| `service` | string | Delivery service \(iMessage, SMS, RCS\) | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | +| `sentAt` | string | ISO 8601 sent timestamp | +| `parts` | json | Message parts \(text, media, link\) with reactions | +| `message` | json | The full message object | + +### `linq_get_webhook_subscription` + +Retrieve a webhook subscription by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `subscriptionId` | string | Yes | The unique identifier of the webhook subscription | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Subscription ID | +| `targetUrl` | string | Endpoint that receives events | +| `subscribedEvents` | json | Subscribed event types | +| `phoneNumbers` | json | Filtered phone numbers \(null = all\) | +| `isActive` | boolean | Whether the subscription is active | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | + +### `linq_leave_chat` + +Leave an iMessage group chat (4+ active participants; not supported for direct chats) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the group chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Human-readable status message | +| `status` | string | Queued action status \(e.g. accepted\) | +| `traceId` | string | Trace ID for the queued action | + +### `linq_list_chats` + +List chats, optionally filtered by sender or participant handle + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `from` | string | No | Filter by sender phone number in E.164 format | +| `to` | string | No | Filter by participant handle \(phone number or email\) | +| `limit` | number | No | Results per page \(default 20, max 100\) | +| `cursor` | string | No | Pagination cursor from a previous response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chats` | json | Array of chat objects | +| `nextCursor` | string | Cursor for the next page, or null if there are no more results | + +### `linq_list_messages` + +List messages in a chat with pagination + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | +| `limit` | number | No | Maximum number of messages to return | +| `cursor` | string | No | Pagination cursor from a previous response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messages` | json | Array of message objects with parts and reactions | +| `nextCursor` | string | Cursor for the next page, or null if there are no more results | + +### `linq_list_phone_numbers` + +List all phone numbers assigned to your partner account, with line health + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `phoneNumbers` | array | Phone numbers assigned to the account | +| ↳ `id` | string | Phone number ID | +| ↳ `phoneNumber` | string | Phone number in E.164 format | +| ↳ `healthStatus` | json | Line health status \(status, doc_url\) | + +### `linq_list_thread` + +List all messages in the thread that contains a given message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `messageId` | string | Yes | The ID of any message in the thread | +| `order` | string | No | Sort order: asc \(oldest first\) or desc \(newest first\) | +| `limit` | number | No | Maximum number of messages to return | +| `cursor` | string | No | Pagination cursor from a previous response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `messages` | json | Array of message objects in the thread | +| `nextCursor` | string | Cursor for the next page, or null if there are no more results | + +### `linq_list_webhook_events` + +List all webhook event types available to subscribe to + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | json | Available webhook event type names | +| `docUrl` | string | Documentation URL for webhook events | + +### `linq_list_webhook_subscriptions` + +List all webhook subscriptions on your account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscriptions` | array | Webhook subscriptions | +| ↳ `id` | string | Subscription ID | +| ↳ `targetUrl` | string | Endpoint that receives events | +| ↳ `subscribedEvents` | json | Subscribed event types | +| ↳ `phoneNumbers` | json | Filtered phone numbers \(null = all\) | +| ↳ `isActive` | boolean | Whether the subscription is active | +| ↳ `createdAt` | string | ISO 8601 creation timestamp | +| ↳ `updatedAt` | string | ISO 8601 update timestamp | + +### `linq_mark_chat_read` + +Mark all messages in a chat as read + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the chat was marked as read | + +### `linq_react_to_message` + +Add or remove a tapback reaction on a message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `messageId` | string | Yes | The unique identifier of the message to react to | +| `operation` | string | Yes | Whether to add or remove the reaction: add or remove | +| `type` | string | Yes | Reaction type: love, like, dislike, laugh, emphasize, question, custom, or sticker | +| `customEmoji` | string | No | Emoji to use when type is custom | +| `partIndex` | number | No | Index of the message part to react to \(defaults to the entire message\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Human-readable status message | +| `status` | string | Queued action status | +| `traceId` | string | Trace ID for the queued action | + +### `linq_remove_participant` + +Remove a participant from a group chat (minimum 3 participants must remain) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the group chat | +| `handle` | string | Yes | Phone number \(E.164 format\) or email address of the participant to remove | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Human-readable status message | +| `status` | string | Queued action status | +| `traceId` | string | Trace ID for the queued action | + +### `linq_send_message` + +Send a message to an existing chat, with optional media, link, effect, or reply + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | +| `text` | string | No | Text content of the message. Optional, but at least one of text, media, attachment, or link is required | +| `mediaUrl` | string | No | Optional publicly accessible HTTPS URL of an image, video, or file to attach | +| `attachmentId` | string | No | Optional ID of a pre-uploaded attachment to send instead of a media URL | +| `linkUrl` | string | No | Optional URL to send as a rich link preview. Linq requires a link to be its own message, so when set, text and media are ignored | +| `preferredService` | string | No | Preferred delivery service: iMessage, SMS, or RCS | +| `effectName` | string | No | Optional iMessage effect name \(e.g. confetti, fireworks, lasers\) | +| `effectType` | string | No | Optional effect type: screen or bubble | +| `replyToMessageId` | string | No | Optional message ID to reply to inline | +| `replyToPartIndex` | number | No | Optional part index of the message being replied to | +| `idempotencyKey` | string | No | Optional idempotency key to safely retry the request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chatId` | string | ID of the chat the message was sent to | +| `messageId` | string | ID of the sent message | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, failed\) | +| `sentAt` | string | ISO 8601 timestamp the message was sent | +| `service` | string | Delivery service \(iMessage, SMS, RCS\) | +| `message` | json | The full sent message object with parts | + +### `linq_send_voice_memo` + +Send a voice memo to a chat from a URL or a pre-uploaded attachment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | +| `voiceMemoUrl` | string | No | Publicly accessible HTTPS URL of the audio file \(MP3, M4A, AAC, CAF, WAV, AIFF, AMR\) | +| `attachmentId` | string | No | ID of a pre-uploaded audio attachment \(use instead of voiceMemoUrl\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ID of the sent voice memo message | +| `status` | string | Delivery status | +| `from` | string | Sender handle | +| `to` | json | Recipient handles | +| `service` | string | Delivery service \(iMessage, SMS, RCS\) | +| `voiceMemo` | json | Audio file metadata \(id, filename, mime_type, size_bytes, url, duration_ms\) | + +### `linq_share_contact_card` + +Share your configured contact card (Name and Photo Sharing) with a chat + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the contact card was shared | + +### `linq_start_typing` + +Show a typing indicator in a one-on-one chat (iMessage only, not group chats) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the typing indicator was sent | + +### `linq_stop_typing` + +Stop the typing indicator in a one-on-one chat (iMessage only, not group chats) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the typing indicator was stopped | + +### `linq_update_chat` + +Update chat properties such as group display name and icon + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `chatId` | string | Yes | The unique identifier of the chat | +| `displayName` | string | No | New display name for the group chat | +| `groupChatIcon` | string | No | New group chat icon \(publicly accessible image URL\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `chatId` | string | ID of the updated chat | +| `status` | string | Status of the queued update | + +### `linq_update_contact_card` + +Partially update an existing active contact card for a phone number + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `phoneNumber` | string | Yes | Phone number in E.164 format identifying the card to update | +| `firstName` | string | No | New first name | +| `lastName` | string | No | New last name | +| `imageUrl` | string | No | New profile photo URL | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `phoneNumber` | string | Phone number the card applies to | +| `firstName` | string | First name | +| `lastName` | string | Last name | +| `imageUrl` | string | Profile photo URL | +| `isActive` | boolean | Whether the card is active | + +### `linq_update_webhook_subscription` + +Update a webhook subscription (target URL, events, phone filter, or active state) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Linq API key | +| `subscriptionId` | string | Yes | The unique identifier of the webhook subscription | +| `targetUrl` | string | No | New HTTPS endpoint that will receive events | +| `subscribedEvents` | array | No | New set of event types to subscribe to | +| `phoneNumbers` | array | No | New set of E.164 phone numbers to filter by | +| `isActive` | boolean | No | Whether the subscription should be active | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Subscription ID | +| `targetUrl` | string | Endpoint that receives events | +| `subscribedEvents` | json | Subscribed event types | +| `phoneNumbers` | json | Filtered phone numbers \(null = all\) | +| `isActive` | boolean | Whether the subscription is active | +| `createdAt` | string | ISO 8601 creation timestamp | +| `updatedAt` | string | ISO 8601 update timestamp | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index dbc2ef5b7f7..a17c92d28e7 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -110,6 +110,7 @@ "linear", "linkedin", "linkup", + "linq", "loops", "luma", "mailchimp", diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 81c24aeadd8..ef0582c6f41 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -111,6 +111,7 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, + LinqIcon, LoopsIcon, LumaIcon, MailchimpIcon, @@ -323,6 +324,7 @@ export const blockTypeToIconMap: Record = { linear_v2: LinearIcon, linkedin: LinkedInIcon, linkup: LinkupIcon, + linq: LinqIcon, loops: LoopsIcon, luma: LumaIcon, mailchimp: MailchimpIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 8d7bec0c5ed..fd58672771b 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -8679,6 +8679,161 @@ "integrationTypes": ["search", "sales"], "tags": ["web-scraping", "enrichment"] }, + { + "type": "linq", + "slug": "linq", + "name": "Linq", + "description": "Send iMessage, SMS, and RCS messages and manage conversations with Linq", + "longDescription": "Reach people on iMessage, SMS, and RCS through Linq. Start chats, send messages with media, links, effects, and replies, send voice memos, react with tapbacks, manage group participants, check iMessage/RCS capability, configure contact cards, and subscribe to webhook events — all through a single Linq API key.", + "bgColor": "#000000", + "iconName": "LinqIcon", + "docsUrl": "https://docs.sim.ai/tools/linq", + "operations": [ + { + "name": "Send Message", + "description": "Send a message to an existing chat, with optional media, link, effect, or reply" + }, + { + "name": "Create Chat", + "description": "Start a new iMessage, SMS, or RCS chat and send the first message" + }, + { + "name": "List Chats", + "description": "List chats, optionally filtered by sender or participant handle" + }, + { + "name": "Get Chat", + "description": "Retrieve a chat by ID, including participants and line health" + }, + { + "name": "Update Chat", + "description": "Update chat properties such as group display name and icon" + }, + { + "name": "Mark Chat as Read", + "description": "Mark all messages in a chat as read" + }, + { + "name": "Leave Chat", + "description": "Leave an iMessage group chat (4+ active participants; not supported for direct chats)" + }, + { + "name": "Add Participant", + "description": "Add a participant to a group chat (3+ existing participants)" + }, + { + "name": "Remove Participant", + "description": "Remove a participant from a group chat (minimum 3 participants must remain)" + }, + { + "name": "Start Typing", + "description": "Show a typing indicator in a one-on-one chat (iMessage only, not group chats)" + }, + { + "name": "Stop Typing", + "description": "Stop the typing indicator in a one-on-one chat (iMessage only, not group chats)" + }, + { + "name": "Send Voice Memo", + "description": "Send a voice memo to a chat from a URL or a pre-uploaded attachment" + }, + { + "name": "Share Contact Card", + "description": "Share your configured contact card (Name and Photo Sharing) with a chat" + }, + { + "name": "List Messages", + "description": "List messages in a chat with pagination" + }, + { + "name": "List Thread", + "description": "List all messages in the thread that contains a given message" + }, + { + "name": "Get Message", + "description": "Retrieve a single message by ID, including parts, reactions, and delivery status" + }, + { + "name": "Edit Message", + "description": "Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iMessage only)" + }, + { + "name": "Delete Message", + "description": "Delete a message from the Linq API only (does not unsend it; recipients still see it)" + }, + { + "name": "React to Message", + "description": "Add or remove a tapback reaction on a message" + }, + { + "name": "Create Attachment", + "description": "Upload a file to Linq as a reusable attachment (max 100MB) and get an attachment ID to send in messages" + }, + { + "name": "Get Attachment", + "description": "Retrieve metadata for an attachment, including its download URL and status" + }, + { + "name": "Delete Attachment", + "description": "Permanently delete an attachment owned by your account" + }, + { + "name": "List Phone Numbers", + "description": "List all phone numbers assigned to your partner account, with line health" + }, + { + "name": "Check iMessage", + "description": "Check whether an address (phone number or email) supports iMessage" + }, + { + "name": "Check RCS", + "description": "Check whether an address (phone number or email) supports RCS" + }, + { + "name": "Get Contact Card", + "description": "Retrieve contact cards, optionally filtered by phone number" + }, + { + "name": "Create Contact Card", + "description": "Set up a contact card (Name and Photo Sharing) for a phone number" + }, + { + "name": "Update Contact Card", + "description": "Partially update an existing active contact card for a phone number" + }, + { + "name": "Create Webhook Subscription", + "description": "Subscribe an HTTPS endpoint to Linq webhook events" + }, + { + "name": "List Webhook Subscriptions", + "description": "List all webhook subscriptions on your account" + }, + { + "name": "Get Webhook Subscription", + "description": "Retrieve a webhook subscription by ID" + }, + { + "name": "Update Webhook Subscription", + "description": "Update a webhook subscription (target URL, events, phone filter, or active state)" + }, + { + "name": "Delete Webhook Subscription", + "description": "Delete a webhook subscription from your account" + }, + { + "name": "List Webhook Events", + "description": "List all webhook event types available to subscribe to" + } + ], + "operationCount": 34, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["communication", "developer-tools"], + "tags": ["messaging", "automation", "webhooks"] + }, { "type": "loops", "slug": "loops", diff --git a/apps/sim/app/api/tools/linq/upload/route.ts b/apps/sim/app/api/tools/linq/upload/route.ts new file mode 100644 index 00000000000..379b7665c0b --- /dev/null +++ b/apps/sim/app/api/tools/linq/upload/route.ts @@ -0,0 +1,161 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { linqUploadAttachmentContract } from '@/lib/api/contracts/tools/communication/messaging' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('LinqUploadAttachmentAPI') + +/** Linq pre-upload caps attachments at 100MB. */ +const MAX_SIZE_BYTES = 100 * 1024 * 1024 + +/** + * Upload a file to Linq as a reusable attachment. + * + * Linq uses a two-step pre-upload flow: register the attachment metadata to + * receive a presigned URL, then PUT the bytes to that URL with the exact + * headers Linq returns. The resulting `attachment_id` can be referenced when + * sending messages or voice memos. + */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized Linq upload attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest(linqUploadAttachmentContract, request, {}) + if (!parsed.success) return parsed.response + const { apiKey, file, fileContent, filename, contentType } = parsed.data.body + + let buffer: Buffer + let resolvedFilename = filename ?? '' + let resolvedContentType = contentType ?? '' + + if (file) { + const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'No valid file provided' }, + { status: 400 } + ) + } + const userFile = userFiles[0] + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + buffer = await downloadFileFromStorage(userFile, requestId, logger) + if (!resolvedFilename) resolvedFilename = userFile.name + if (!resolvedContentType) resolvedContentType = userFile.type || 'application/octet-stream' + } else if (fileContent) { + buffer = Buffer.from(fileContent, 'base64') + if (!resolvedFilename) resolvedFilename = 'file' + if (!resolvedContentType) resolvedContentType = 'application/octet-stream' + } else { + return NextResponse.json( + { success: false, error: 'A file is required to upload an attachment' }, + { status: 400 } + ) + } + + const sizeBytes = buffer.length + if (sizeBytes === 0) { + return NextResponse.json({ success: false, error: 'File is empty' }, { status: 400 }) + } + if (sizeBytes > MAX_SIZE_BYTES) { + return NextResponse.json( + { + success: false, + error: `File exceeds Linq's 100MB attachment limit (${(sizeBytes / (1024 * 1024)).toFixed(2)}MB)`, + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Registering Linq attachment`, { + filename: resolvedFilename, + contentType: resolvedContentType, + sizeBytes, + }) + + const registerResponse = await fetch(`${LINQ_API_BASE}/attachments`, { + method: 'POST', + headers: linqHeaders(apiKey), + body: JSON.stringify({ + filename: resolvedFilename, + content_type: resolvedContentType, + size_bytes: sizeBytes, + }), + }) + const registerData = await registerResponse.json().catch(() => null) + if (!registerResponse.ok) { + return NextResponse.json( + { success: false, error: extractLinqError(registerData, 'Failed to register attachment') }, + { status: registerResponse.status } + ) + } + + const uploadUrl: string | undefined = registerData?.upload_url + const attachmentId: string | undefined = registerData?.attachment_id + if (!uploadUrl || !attachmentId) { + return NextResponse.json( + { success: false, error: 'Linq did not return an upload URL or attachment ID' }, + { status: 502 } + ) + } + + const requiredHeaders: Record = registerData?.required_headers ?? { + 'Content-Type': resolvedContentType, + 'Content-Length': String(sizeBytes), + } + const uploadMethod: string = registerData?.http_method ?? 'PUT' + + logger.info(`[${requestId}] Uploading ${sizeBytes} bytes to presigned URL`) + const uploadResponse = await fetch(uploadUrl, { + method: uploadMethod, + headers: requiredHeaders, + body: new Uint8Array(buffer), + }) + if (!uploadResponse.ok) { + const uploadError = await uploadResponse.text().catch(() => '') + logger.error(`[${requestId}] Presigned upload failed: ${uploadResponse.status}`, uploadError) + return NextResponse.json( + { success: false, error: `Failed to upload file bytes to Linq (${uploadResponse.status})` }, + { status: 502 } + ) + } + + logger.info(`[${requestId}] Attachment uploaded`, { attachmentId }) + return NextResponse.json({ + success: true, + output: { + attachmentId, + downloadUrl: registerData?.download_url ?? null, + filename: resolvedFilename, + contentType: resolvedContentType, + sizeBytes, + status: 'complete', + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error uploading Linq attachment:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Unknown error occurred') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/linq.ts b/apps/sim/blocks/blocks/linq.ts new file mode 100644 index 00000000000..50e2cb8357e --- /dev/null +++ b/apps/sim/blocks/blocks/linq.ts @@ -0,0 +1,867 @@ +import { LinqIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' + +const CHAT_ID_OPS = [ + 'get_chat', + 'update_chat', + 'mark_chat_read', + 'leave_chat', + 'share_contact_card', + 'send_voice_memo', + 'add_participant', + 'remove_participant', + 'start_typing', + 'stop_typing', + 'send_message', + 'list_messages', +] as const + +const MESSAGE_ID_OPS = [ + 'get_message', + 'list_thread', + 'edit_message', + 'delete_message', + 'react_to_message', +] as const + +const ATTACHMENT_ID_OPS = ['get_attachment', 'delete_attachment'] as const + +const SUBSCRIPTION_ID_OPS = [ + 'get_webhook_subscription', + 'update_webhook_subscription', + 'delete_webhook_subscription', +] as const + +const MESSAGE_CONTENT_OPS = ['create_chat', 'send_message'] as const + +const CONTACT_CARD_OPS = ['get_contact_card', 'create_contact_card', 'update_contact_card'] as const + +const CONTACT_CARD_WRITE_OPS = ['create_contact_card', 'update_contact_card'] as const + +const WEBHOOK_WRITE_OPS = ['create_webhook_subscription', 'update_webhook_subscription'] as const + +const PAGINATION_OPS = ['list_chats', 'list_messages', 'list_thread'] as const + +const CAPABILITY_OPS = ['check_imessage', 'check_rcs'] as const + +const PARTICIPANT_OPS = ['add_participant', 'remove_participant'] as const + +const splitHandles = (value: unknown): string[] => + String(value) + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter(Boolean) + +export const LinqBlock: BlockConfig = { + type: 'linq', + name: 'Linq', + description: 'Send iMessage, SMS, and RCS messages and manage conversations with Linq', + longDescription: + 'Reach people on iMessage, SMS, and RCS through Linq. Start chats, send messages with media, links, effects, and replies, send voice memos, react with tapbacks, manage group participants, check iMessage/RCS capability, configure contact cards, and subscribe to webhook events — all through a single Linq API key.', + docsLink: 'https://docs.sim.ai/tools/linq', + category: 'tools', + integrationType: IntegrationType.Communication, + tags: ['messaging', 'automation', 'webhooks'], + bgColor: '#000000', + icon: LinqIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Message', id: 'send_message' }, + { label: 'Create Chat', id: 'create_chat' }, + { label: 'List Chats', id: 'list_chats' }, + { label: 'Get Chat', id: 'get_chat' }, + { label: 'Update Chat', id: 'update_chat' }, + { label: 'Mark Chat as Read', id: 'mark_chat_read' }, + { label: 'Leave Chat', id: 'leave_chat' }, + { label: 'Add Participant', id: 'add_participant' }, + { label: 'Remove Participant', id: 'remove_participant' }, + { label: 'Start Typing', id: 'start_typing' }, + { label: 'Stop Typing', id: 'stop_typing' }, + { label: 'Send Voice Memo', id: 'send_voice_memo' }, + { label: 'Share Contact Card', id: 'share_contact_card' }, + { label: 'List Messages', id: 'list_messages' }, + { label: 'List Thread', id: 'list_thread' }, + { label: 'Get Message', id: 'get_message' }, + { label: 'Edit Message', id: 'edit_message' }, + { label: 'Delete Message', id: 'delete_message' }, + { label: 'React to Message', id: 'react_to_message' }, + { label: 'Create Attachment', id: 'create_attachment' }, + { label: 'Get Attachment', id: 'get_attachment' }, + { label: 'Delete Attachment', id: 'delete_attachment' }, + { label: 'List Phone Numbers', id: 'list_phone_numbers' }, + { label: 'Check iMessage', id: 'check_imessage' }, + { label: 'Check RCS', id: 'check_rcs' }, + { label: 'Get Contact Card', id: 'get_contact_card' }, + { label: 'Create Contact Card', id: 'create_contact_card' }, + { label: 'Update Contact Card', id: 'update_contact_card' }, + { label: 'Create Webhook Subscription', id: 'create_webhook_subscription' }, + { label: 'List Webhook Subscriptions', id: 'list_webhook_subscriptions' }, + { label: 'Get Webhook Subscription', id: 'get_webhook_subscription' }, + { label: 'Update Webhook Subscription', id: 'update_webhook_subscription' }, + { label: 'Delete Webhook Subscription', id: 'delete_webhook_subscription' }, + { label: 'List Webhook Events', id: 'list_webhook_events' }, + ], + value: () => 'send_message', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Linq API key', + required: true, + password: true, + }, + + { + id: 'chatId', + title: 'Chat ID', + type: 'short-input', + placeholder: 'Chat UUID', + condition: { field: 'operation', value: [...CHAT_ID_OPS] }, + required: { field: 'operation', value: [...CHAT_ID_OPS] }, + }, + + // Create Chat - recipients + { + id: 'senderFrom', + title: 'From', + type: 'short-input', + placeholder: '+14155551234 (your sending number, E.164)', + condition: { field: 'operation', value: 'create_chat' }, + required: { field: 'operation', value: 'create_chat' }, + }, + { + id: 'recipients', + title: 'To', + type: 'long-input', + placeholder: 'Comma- or newline-separated handles (+14155550000, alice@example.com)', + condition: { field: 'operation', value: 'create_chat' }, + required: { field: 'operation', value: 'create_chat' }, + }, + + // Message content (create_chat + send_message) + { + id: 'messageText', + title: 'Message', + type: 'long-input', + placeholder: 'Message text (or leave blank to send only media, an attachment, or a link)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + wandConfig: { + enabled: true, + prompt: + 'Generate a friendly, concise message body suitable for iMessage or SMS. Return ONLY the message text - no explanations, no extra text.', + placeholder: 'Describe the message purpose and tone...', + }, + }, + { + id: 'mediaUrl', + title: 'Media URL', + type: 'short-input', + placeholder: 'https://cdn.example.com/image.png (optional)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'mediaAttachmentId', + title: 'Attachment ID', + type: 'short-input', + placeholder: 'Pre-uploaded attachment ID (optional)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'linkUrl', + title: 'Link URL', + type: 'short-input', + placeholder: 'https://example.com (sent as its own preview; ignores text/media)', + condition: { field: 'operation', value: 'send_message' }, + mode: 'advanced', + }, + { + id: 'preferredService', + title: 'Preferred Service', + type: 'dropdown', + options: [ + { label: 'Auto', id: '' }, + { label: 'iMessage', id: 'iMessage' }, + { label: 'SMS', id: 'SMS' }, + { label: 'RCS', id: 'RCS' }, + ], + value: () => '', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'effectName', + title: 'Effect Name', + type: 'short-input', + placeholder: 'confetti, fireworks, lasers (optional)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'effectType', + title: 'Effect Type', + type: 'dropdown', + options: [ + { label: 'None', id: '' }, + { label: 'Screen', id: 'screen' }, + { label: 'Bubble', id: 'bubble' }, + ], + value: () => '', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'replyToMessageId', + title: 'Reply to Message ID', + type: 'short-input', + placeholder: 'Message ID to reply to inline (optional)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'replyToPartIndex', + title: 'Reply Part Index', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + { + id: 'idempotencyKey', + title: 'Idempotency Key', + type: 'short-input', + placeholder: 'Unique key to safely retry (optional)', + condition: { field: 'operation', value: [...MESSAGE_CONTENT_OPS] }, + mode: 'advanced', + }, + + // Update Chat + { + id: 'displayName', + title: 'Display Name', + type: 'short-input', + placeholder: 'New group chat name', + condition: { field: 'operation', value: 'update_chat' }, + }, + { + id: 'groupChatIcon', + title: 'Group Chat Icon', + type: 'short-input', + placeholder: 'https://cdn.example.com/icon.png', + condition: { field: 'operation', value: 'update_chat' }, + mode: 'advanced', + }, + + // Participants + { + id: 'participantHandle', + title: 'Participant Handle', + type: 'short-input', + placeholder: '+14155550000 or alice@example.com', + condition: { field: 'operation', value: [...PARTICIPANT_OPS] }, + required: { field: 'operation', value: [...PARTICIPANT_OPS] }, + }, + + // Send Voice Memo (provide either a URL or a pre-uploaded attachment ID) + { + id: 'voiceMemoUrl', + title: 'Voice Memo URL', + type: 'short-input', + placeholder: 'https://cdn.example.com/memo.m4a (required unless an Attachment ID is set)', + condition: { field: 'operation', value: 'send_voice_memo' }, + }, + { + id: 'voiceAttachmentId', + title: 'Attachment ID', + type: 'short-input', + placeholder: 'Pre-uploaded audio attachment ID (use instead of a URL)', + condition: { field: 'operation', value: 'send_voice_memo' }, + mode: 'advanced', + }, + + // List Chats filters + { + id: 'filterFrom', + title: 'From', + type: 'short-input', + placeholder: 'Filter by sender number (E.164)', + condition: { field: 'operation', value: 'list_chats' }, + mode: 'advanced', + }, + { + id: 'filterTo', + title: 'To', + type: 'short-input', + placeholder: 'Filter by participant handle', + condition: { field: 'operation', value: 'list_chats' }, + mode: 'advanced', + }, + + // Message ID (message-level operations) + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + placeholder: 'Message UUID', + condition: { field: 'operation', value: [...MESSAGE_ID_OPS] }, + required: { field: 'operation', value: [...MESSAGE_ID_OPS] }, + }, + + // Edit Message + { + id: 'editText', + title: 'New Text', + type: 'long-input', + placeholder: 'Updated message text', + condition: { field: 'operation', value: 'edit_message' }, + required: { field: 'operation', value: 'edit_message' }, + }, + { + id: 'editPartIndex', + title: 'Part Index', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'edit_message' }, + mode: 'advanced', + }, + + // React to Message + { + id: 'reactionOperation', + title: 'Reaction Operation', + type: 'dropdown', + options: [ + { label: 'Add', id: 'add' }, + { label: 'Remove', id: 'remove' }, + ], + value: () => 'add', + condition: { field: 'operation', value: 'react_to_message' }, + required: { field: 'operation', value: 'react_to_message' }, + }, + { + id: 'reactionType', + title: 'Reaction Type', + type: 'dropdown', + options: [ + { label: 'Love', id: 'love' }, + { label: 'Like', id: 'like' }, + { label: 'Dislike', id: 'dislike' }, + { label: 'Laugh', id: 'laugh' }, + { label: 'Emphasize', id: 'emphasize' }, + { label: 'Question', id: 'question' }, + { label: 'Custom Emoji', id: 'custom' }, + { label: 'Sticker', id: 'sticker' }, + ], + value: () => 'love', + condition: { field: 'operation', value: 'react_to_message' }, + required: { field: 'operation', value: 'react_to_message' }, + }, + { + id: 'reactionCustomEmoji', + title: 'Custom Emoji', + type: 'short-input', + placeholder: '🎉 (required when type is Custom Emoji)', + condition: { field: 'operation', value: 'react_to_message' }, + mode: 'advanced', + }, + { + id: 'reactionPartIndex', + title: 'Part Index', + type: 'short-input', + placeholder: 'Defaults to the entire message', + condition: { field: 'operation', value: 'react_to_message' }, + mode: 'advanced', + }, + + // Create Attachment (file upload) + { + id: 'uploadFile', + title: 'File', + type: 'file-upload', + canonicalParamId: 'file', + placeholder: 'Upload a file to attach (max 100MB)', + multiple: false, + condition: { field: 'operation', value: 'create_attachment' }, + required: { field: 'operation', value: 'create_attachment' }, + mode: 'basic', + }, + { + id: 'fileRef', + title: 'File', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'Reference a file from a previous block (e.g. {{block.output.file}})', + condition: { field: 'operation', value: 'create_attachment' }, + required: { field: 'operation', value: 'create_attachment' }, + mode: 'advanced', + }, + { + id: 'attachmentFilename', + title: 'File Name', + type: 'short-input', + placeholder: 'Override the file name (optional)', + condition: { field: 'operation', value: 'create_attachment' }, + mode: 'advanced', + }, + { + id: 'attachmentContentType', + title: 'Content Type', + type: 'short-input', + placeholder: 'Override MIME type, e.g. image/png (optional)', + condition: { field: 'operation', value: 'create_attachment' }, + mode: 'advanced', + }, + + // Attachment ID (get/delete) + { + id: 'attachmentId', + title: 'Attachment ID', + type: 'short-input', + placeholder: 'Attachment UUID', + condition: { field: 'operation', value: [...ATTACHMENT_ID_OPS] }, + required: { field: 'operation', value: [...ATTACHMENT_ID_OPS] }, + }, + + // Capability checks + { + id: 'address', + title: 'Address', + type: 'short-input', + placeholder: '+14155550000 or alice@example.com', + condition: { field: 'operation', value: [...CAPABILITY_OPS] }, + required: { field: 'operation', value: [...CAPABILITY_OPS] }, + }, + { + id: 'capabilityFrom', + title: 'From', + type: 'short-input', + placeholder: 'Sender number to check from (optional)', + condition: { field: 'operation', value: [...CAPABILITY_OPS] }, + mode: 'advanced', + }, + + // Contact Card + { + id: 'contactPhoneNumber', + title: 'Phone Number', + type: 'short-input', + placeholder: '+14155551234', + condition: { field: 'operation', value: [...CONTACT_CARD_OPS] }, + required: { field: 'operation', value: [...CONTACT_CARD_WRITE_OPS] }, + }, + { + id: 'contactFirstName', + title: 'First Name', + type: 'short-input', + placeholder: 'Alice', + condition: { field: 'operation', value: [...CONTACT_CARD_WRITE_OPS] }, + required: { field: 'operation', value: 'create_contact_card' }, + }, + { + id: 'contactLastName', + title: 'Last Name', + type: 'short-input', + placeholder: 'Johnson', + condition: { field: 'operation', value: [...CONTACT_CARD_WRITE_OPS] }, + mode: 'advanced', + }, + { + id: 'contactImageUrl', + title: 'Profile Photo URL', + type: 'short-input', + placeholder: 'https://cdn.example.com/avatar.png', + condition: { field: 'operation', value: [...CONTACT_CARD_WRITE_OPS] }, + mode: 'advanced', + }, + + // Webhook Subscriptions + { + id: 'subscriptionId', + title: 'Subscription ID', + type: 'short-input', + placeholder: 'Subscription UUID', + condition: { field: 'operation', value: [...SUBSCRIPTION_ID_OPS] }, + required: { field: 'operation', value: [...SUBSCRIPTION_ID_OPS] }, + }, + { + id: 'webhookTargetUrl', + title: 'Target URL', + type: 'short-input', + placeholder: 'https://example.com/webhooks/linq', + condition: { field: 'operation', value: [...WEBHOOK_WRITE_OPS] }, + required: { field: 'operation', value: 'create_webhook_subscription' }, + }, + { + id: 'webhookEvents', + title: 'Subscribed Events', + type: 'long-input', + placeholder: 'Comma- or newline-separated (message.sent, message.delivered, reaction.added)', + condition: { field: 'operation', value: [...WEBHOOK_WRITE_OPS] }, + required: { field: 'operation', value: 'create_webhook_subscription' }, + }, + { + id: 'webhookPhoneNumbers', + title: 'Phone Numbers', + type: 'long-input', + placeholder: 'Comma- or newline-separated E.164 numbers (optional, omit for all)', + condition: { field: 'operation', value: [...WEBHOOK_WRITE_OPS] }, + mode: 'advanced', + }, + { + id: 'webhookIsActive', + title: 'Active', + type: 'dropdown', + options: [ + { label: 'No change', id: '' }, + { label: 'Active', id: 'true' }, + { label: 'Inactive', id: 'false' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_webhook_subscription' }, + mode: 'advanced', + }, + + // Pagination (list operations) + { + id: 'order', + title: 'Order', + type: 'dropdown', + options: [ + { label: 'Ascending (oldest first)', id: 'asc' }, + { label: 'Descending (newest first)', id: 'desc' }, + ], + value: () => 'asc', + condition: { field: 'operation', value: 'list_thread' }, + mode: 'advanced', + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '20', + condition: { field: 'operation', value: [...PAGINATION_OPS] }, + mode: 'advanced', + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Pagination cursor from a previous response', + condition: { field: 'operation', value: [...PAGINATION_OPS] }, + mode: 'advanced', + }, + ], + + tools: { + access: [ + 'linq_add_participant', + 'linq_check_imessage', + 'linq_check_rcs', + 'linq_create_attachment', + 'linq_create_chat', + 'linq_create_contact_card', + 'linq_create_webhook_subscription', + 'linq_delete_attachment', + 'linq_delete_message', + 'linq_delete_webhook_subscription', + 'linq_edit_message', + 'linq_get_attachment', + 'linq_get_chat', + 'linq_get_contact_card', + 'linq_get_message', + 'linq_get_webhook_subscription', + 'linq_leave_chat', + 'linq_list_chats', + 'linq_list_messages', + 'linq_list_phone_numbers', + 'linq_list_thread', + 'linq_list_webhook_events', + 'linq_list_webhook_subscriptions', + 'linq_mark_chat_read', + 'linq_react_to_message', + 'linq_remove_participant', + 'linq_send_message', + 'linq_send_voice_memo', + 'linq_share_contact_card', + 'linq_start_typing', + 'linq_stop_typing', + 'linq_update_chat', + 'linq_update_contact_card', + 'linq_update_webhook_subscription', + ], + config: { + tool: (params) => `linq_${params.operation || 'send_message'}`, + params: (params) => { + const { + operation, + senderFrom, + recipients, + messageText, + mediaAttachmentId, + linkUrl, + replyToPartIndex, + displayName, + groupChatIcon, + participantHandle, + voiceAttachmentId, + filterFrom, + filterTo, + editText, + editPartIndex, + reactionOperation, + reactionType, + reactionCustomEmoji, + reactionPartIndex, + file, + attachmentId, + attachmentFilename, + attachmentContentType, + capabilityFrom, + contactPhoneNumber, + contactFirstName, + contactLastName, + contactImageUrl, + webhookTargetUrl, + webhookEvents, + webhookPhoneNumbers, + webhookIsActive, + limit, + ...rest + } = params + + const toFiniteNumber = (value: unknown, field: string): number => { + const parsed = Number(value) + if (!Number.isFinite(parsed)) { + throw new Error(`Invalid numeric value for ${field}: ${String(value)}`) + } + return parsed + } + + if (operation === 'create_chat') { + if (senderFrom) rest.from = senderFrom + if (recipients !== undefined && recipients !== '') rest.to = splitHandles(recipients) + } + + if (operation === 'create_chat' || operation === 'send_message') { + if (messageText !== undefined) rest.text = messageText + if (mediaAttachmentId) rest.attachmentId = mediaAttachmentId + if (replyToPartIndex !== undefined && replyToPartIndex !== '') { + rest.replyToPartIndex = toFiniteNumber(replyToPartIndex, 'Reply Part Index') + } + } + + // Links are only valid on send_message — Linq rejects a link as the first + // message of a new chat, so it is never forwarded to create_chat. + if (operation === 'send_message' && linkUrl) { + rest.linkUrl = linkUrl + } + + if (operation === 'update_chat') { + if (displayName !== undefined && displayName !== '') rest.displayName = displayName + if (groupChatIcon !== undefined && groupChatIcon !== '') { + rest.groupChatIcon = groupChatIcon + } + } + + if (operation === 'add_participant' || operation === 'remove_participant') { + if (participantHandle) rest.handle = participantHandle + } + + if (operation === 'send_voice_memo' && voiceAttachmentId) { + rest.attachmentId = voiceAttachmentId + } + + if ( + ATTACHMENT_ID_OPS.includes(operation as (typeof ATTACHMENT_ID_OPS)[number]) && + attachmentId + ) { + rest.attachmentId = attachmentId + } + + if (operation === 'list_chats') { + if (filterFrom) rest.from = filterFrom + if (filterTo) rest.to = filterTo + } + + if (operation === 'edit_message') { + if (editText !== undefined) rest.text = editText + if (editPartIndex !== undefined && editPartIndex !== '') { + rest.partIndex = toFiniteNumber(editPartIndex, 'Part Index') + } + } + + if (operation === 'react_to_message') { + if (reactionOperation) rest.operation = reactionOperation + if (reactionType) rest.type = reactionType + if (reactionCustomEmoji) rest.customEmoji = reactionCustomEmoji + if (reactionPartIndex !== undefined && reactionPartIndex !== '') { + rest.partIndex = toFiniteNumber(reactionPartIndex, 'Part Index') + } + } + + if (operation === 'create_attachment') { + const normalizedFile = normalizeFileInput(file, { single: true }) + if (normalizedFile) rest.file = normalizedFile + if (attachmentFilename) rest.filename = attachmentFilename + if (attachmentContentType) rest.contentType = attachmentContentType + } + + if ((operation === 'check_imessage' || operation === 'check_rcs') && capabilityFrom) { + rest.from = capabilityFrom + } + + if (CONTACT_CARD_OPS.includes(operation as (typeof CONTACT_CARD_OPS)[number])) { + if (contactPhoneNumber) rest.phoneNumber = contactPhoneNumber + } + + if (CONTACT_CARD_WRITE_OPS.includes(operation as (typeof CONTACT_CARD_WRITE_OPS)[number])) { + if (contactFirstName !== undefined && contactFirstName !== '') { + rest.firstName = contactFirstName + } + if (contactLastName !== undefined && contactLastName !== '') { + rest.lastName = contactLastName + } + if (contactImageUrl !== undefined && contactImageUrl !== '') { + rest.imageUrl = contactImageUrl + } + } + + if (WEBHOOK_WRITE_OPS.includes(operation as (typeof WEBHOOK_WRITE_OPS)[number])) { + if (webhookTargetUrl !== undefined && webhookTargetUrl !== '') { + rest.targetUrl = webhookTargetUrl + } + if (webhookEvents !== undefined && webhookEvents !== '') { + rest.subscribedEvents = splitHandles(webhookEvents) + } + if (webhookPhoneNumbers !== undefined && webhookPhoneNumbers !== '') { + rest.phoneNumbers = splitHandles(webhookPhoneNumbers) + } + } + + if (operation === 'update_webhook_subscription' && webhookIsActive) { + rest.isActive = webhookIsActive === 'true' + } + + if ( + PAGINATION_OPS.includes(operation as (typeof PAGINATION_OPS)[number]) && + limit !== undefined && + limit !== '' + ) { + rest.limit = toFiniteNumber(limit, 'Limit') + } + + return rest + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Linq API key' }, + chatId: { type: 'string', description: 'Chat ID' }, + senderFrom: { type: 'string', description: 'Sender phone number for a new chat' }, + recipients: { type: 'string', description: 'Recipient handles for a new chat' }, + messageText: { type: 'string', description: 'Message text content' }, + mediaUrl: { type: 'string', description: 'Media URL to attach' }, + mediaAttachmentId: { type: 'string', description: 'Pre-uploaded attachment ID for a message' }, + linkUrl: { type: 'string', description: 'Rich link preview URL' }, + preferredService: { type: 'string', description: 'Preferred delivery service' }, + effectName: { type: 'string', description: 'iMessage effect name' }, + effectType: { type: 'string', description: 'iMessage effect type (screen or bubble)' }, + replyToMessageId: { type: 'string', description: 'Message ID to reply to' }, + replyToPartIndex: { type: 'string', description: 'Part index of the replied-to message' }, + idempotencyKey: { type: 'string', description: 'Idempotency key' }, + displayName: { type: 'string', description: 'Group chat display name' }, + groupChatIcon: { type: 'string', description: 'Group chat icon URL' }, + participantHandle: { type: 'string', description: 'Participant handle to add or remove' }, + voiceMemoUrl: { type: 'string', description: 'Voice memo audio URL' }, + voiceAttachmentId: { type: 'string', description: 'Pre-uploaded audio attachment ID' }, + filterFrom: { type: 'string', description: 'List chats: filter by sender number' }, + filterTo: { type: 'string', description: 'List chats: filter by participant handle' }, + messageId: { type: 'string', description: 'Message ID' }, + editText: { type: 'string', description: 'New text for an edited message' }, + editPartIndex: { type: 'string', description: 'Part index to edit' }, + reactionOperation: { type: 'string', description: 'Add or remove a reaction' }, + reactionType: { type: 'string', description: 'Reaction type' }, + reactionCustomEmoji: { type: 'string', description: 'Custom emoji for a reaction' }, + reactionPartIndex: { type: 'string', description: 'Part index to react to' }, + file: { type: 'json', description: 'File to upload as an attachment' }, + attachmentFilename: { type: 'string', description: 'Override the attachment file name' }, + attachmentContentType: { type: 'string', description: 'Override the attachment MIME type' }, + attachmentId: { type: 'string', description: 'Attachment ID' }, + address: { type: 'string', description: 'Address to check capability for' }, + capabilityFrom: { type: 'string', description: 'Sender number to check capability from' }, + contactPhoneNumber: { type: 'string', description: 'Contact card phone number' }, + contactFirstName: { type: 'string', description: 'Contact card first name' }, + contactLastName: { type: 'string', description: 'Contact card last name' }, + contactImageUrl: { type: 'string', description: 'Contact card profile photo URL' }, + subscriptionId: { type: 'string', description: 'Webhook subscription ID' }, + webhookTargetUrl: { type: 'string', description: 'Webhook target URL' }, + webhookEvents: { type: 'string', description: 'Webhook subscribed event types' }, + webhookPhoneNumbers: { type: 'string', description: 'Webhook phone number filter' }, + webhookIsActive: { type: 'string', description: 'Whether the webhook subscription is active' }, + order: { type: 'string', description: 'Thread sort order (asc or desc)' }, + limit: { type: 'string', description: 'Pagination limit' }, + cursor: { type: 'string', description: 'Pagination cursor' }, + }, + + outputs: { + chatId: { type: 'string', description: 'Chat ID' }, + displayName: { type: 'string', description: 'Chat display name' }, + isGroup: { type: 'boolean', description: 'Whether the chat is a group chat' }, + isArchived: { type: 'boolean', description: 'Whether the chat is archived' }, + service: { type: 'string', description: 'Delivery service (iMessage, SMS, RCS)' }, + handles: { type: 'json', description: 'Participant handles' }, + healthStatus: { type: 'json', description: 'Messaging line health status' }, + chats: { type: 'json', description: 'Array of chats (list operations)' }, + messages: { type: 'json', description: 'Array of messages (list operations)' }, + nextCursor: { type: 'string', description: 'Pagination cursor for the next page' }, + messageId: { type: 'string', description: 'Message ID' }, + deliveryStatus: { type: 'string', description: 'Message delivery status' }, + sentAt: { type: 'string', description: 'ISO 8601 sent timestamp' }, + message: { type: 'json', description: 'A message object with parts and metadata' }, + parts: { type: 'json', description: 'Message parts (text, media, link) with reactions' }, + isFromMe: { type: 'boolean', description: 'Whether the message was sent by you' }, + isDelivered: { type: 'boolean', description: 'Whether the message was delivered' }, + isRead: { type: 'boolean', description: 'Whether the message was read' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' }, + status: { type: 'string', description: 'Status field (varies by operation)' }, + success: { type: 'boolean', description: 'Whether the action succeeded' }, + traceId: { type: 'string', description: 'Trace ID for a queued action' }, + id: { type: 'string', description: 'ID of the primary resource returned' }, + from: { type: 'string', description: 'Sender handle' }, + to: { type: 'json', description: 'Recipient handles' }, + voiceMemo: { type: 'json', description: 'Voice memo audio metadata' }, + attachmentId: { type: 'string', description: 'Attachment ID' }, + downloadUrl: { type: 'string', description: 'Attachment download URL' }, + filename: { type: 'string', description: 'Attachment file name' }, + contentType: { type: 'string', description: 'Attachment MIME type' }, + sizeBytes: { type: 'number', description: 'Attachment size in bytes' }, + phoneNumbers: { type: 'json', description: 'Phone numbers' }, + address: { type: 'string', description: 'Address checked for capability' }, + available: { type: 'boolean', description: 'Whether the address supports the service' }, + contactCards: { type: 'json', description: 'Contact cards' }, + phoneNumber: { type: 'string', description: 'Contact card phone number' }, + firstName: { type: 'string', description: 'Contact card first name' }, + lastName: { type: 'string', description: 'Contact card last name' }, + imageUrl: { type: 'string', description: 'Contact card profile photo URL' }, + isActive: { type: 'boolean', description: 'Whether the resource is active' }, + subscriptions: { type: 'json', description: 'Webhook subscriptions' }, + targetUrl: { type: 'string', description: 'Webhook target URL' }, + subscribedEvents: { type: 'json', description: 'Subscribed webhook event types' }, + signingSecret: { type: 'string', description: 'Webhook signing secret (returned once)' }, + events: { type: 'json', description: 'Available webhook event types' }, + docUrl: { type: 'string', description: 'Documentation URL' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 14f3bda53fd..d13aa5d2bcc 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -118,6 +118,7 @@ import { LemlistBlock } from '@/blocks/blocks/lemlist' import { LinearBlock, LinearV2Block } from '@/blocks/blocks/linear' import { LinkedInBlock } from '@/blocks/blocks/linkedin' import { LinkupBlock } from '@/blocks/blocks/linkup' +import { LinqBlock } from '@/blocks/blocks/linq' import { LogsBlock } from '@/blocks/blocks/logs' import { LoopsBlock } from '@/blocks/blocks/loops' import { LumaBlock } from '@/blocks/blocks/luma' @@ -386,6 +387,7 @@ export const registry: Record = { linear_v2: LinearV2Block, linkedin: LinkedInBlock, linkup: LinkupBlock, + linq: LinqBlock, logs: LogsBlock, loops: LoopsBlock, luma: LumaBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index e2c0e8946a6..fcdab73224d 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2439,6 +2439,21 @@ export function JiraIcon(props: SVGProps) { ) } +export function LinqIcon(props: SVGProps) { + return ( + + + + + ) +} + export function LinearIcon(props: React.SVGProps) { return ( export type TelegramSendDocumentBody = ContractBodyInput export type TwilioGetRecordingBody = ContractBodyInput +export type LinqUploadAttachmentBody = ContractBodyInput export type SmsSendResponse = ContractJsonResponse export type TelegramSendDocumentResponse = ContractJsonResponse export type TwilioGetRecordingResponse = ContractJsonResponse +export type LinqUploadAttachmentResponse = ContractJsonResponse diff --git a/apps/sim/tools/linq/add_participant.ts b/apps/sim/tools/linq/add_participant.ts new file mode 100644 index 00000000000..df1e301cfc5 --- /dev/null +++ b/apps/sim/tools/linq/add_participant.ts @@ -0,0 +1,66 @@ +import type { LinqParticipantParams, LinqQueuedResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqAddParticipantTool: ToolConfig = { + id: 'linq_add_participant', + name: 'Add Participant', + description: 'Add a participant to a group chat (3+ existing participants)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the group chat', + }, + handle: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format) or email address of the participant to add', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/participants`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => ({ handle: params.handle }), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to add participant'), + output: { message: null, status: null, traceId: null }, + } + } + + return { + success: true, + output: { + message: data.message ?? null, + status: data.status ?? null, + traceId: data.trace_id ?? null, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Human-readable status message', optional: true }, + status: { type: 'string', description: 'Queued action status', optional: true }, + traceId: { type: 'string', description: 'Trace ID for the queued action', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/check_imessage.ts b/apps/sim/tools/linq/check_imessage.ts new file mode 100644 index 00000000000..cc44b5047b2 --- /dev/null +++ b/apps/sim/tools/linq/check_imessage.ts @@ -0,0 +1,70 @@ +import type { LinqCapabilityCheckParams, LinqCapabilityCheckResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqCheckImessageTool: ToolConfig< + LinqCapabilityCheckParams, + LinqCapabilityCheckResult +> = { + id: 'linq_check_imessage', + name: 'Check iMessage Capability', + description: 'Check whether an address (phone number or email) supports iMessage', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + address: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format) or email address to check', + }, + from: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sender phone number to check from (defaults to an available number)', + }, + }, + + request: { + url: `${LINQ_API_BASE}/capability/check_imessage`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { address: params.address } + if (params.from) body.from = params.from + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to check iMessage capability'), + output: { address: '', available: false }, + } + } + + return { + success: true, + output: { + address: data.address ?? '', + available: data.available ?? false, + }, + } + }, + + outputs: { + address: { type: 'string', description: 'The address that was checked' }, + available: { type: 'boolean', description: 'Whether the address supports iMessage' }, + }, +} diff --git a/apps/sim/tools/linq/check_rcs.ts b/apps/sim/tools/linq/check_rcs.ts new file mode 100644 index 00000000000..12c3808f871 --- /dev/null +++ b/apps/sim/tools/linq/check_rcs.ts @@ -0,0 +1,67 @@ +import type { LinqCapabilityCheckParams, LinqCapabilityCheckResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqCheckRcsTool: ToolConfig = { + id: 'linq_check_rcs', + name: 'Check RCS Capability', + description: 'Check whether an address (phone number or email) supports RCS', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + address: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format) or email address to check', + }, + from: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sender phone number to check from (defaults to an available number)', + }, + }, + + request: { + url: `${LINQ_API_BASE}/capability/check_rcs`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { address: params.address } + if (params.from) body.from = params.from + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to check RCS capability'), + output: { address: '', available: false }, + } + } + + return { + success: true, + output: { + address: data.address ?? '', + available: data.available ?? false, + }, + } + }, + + outputs: { + address: { type: 'string', description: 'The address that was checked' }, + available: { type: 'boolean', description: 'Whether the address supports RCS' }, + }, +} diff --git a/apps/sim/tools/linq/create_attachment.ts b/apps/sim/tools/linq/create_attachment.ts new file mode 100644 index 00000000000..944ba934b02 --- /dev/null +++ b/apps/sim/tools/linq/create_attachment.ts @@ -0,0 +1,107 @@ +import type { LinqCreateAttachmentParams, LinqCreateAttachmentResult } from '@/tools/linq/types' +import type { ToolConfig } from '@/tools/types' + +export const linqCreateAttachmentTool: ToolConfig< + LinqCreateAttachmentParams, + LinqCreateAttachmentResult +> = { + id: 'linq_create_attachment', + name: 'Upload Attachment', + description: + 'Upload a file to Linq as a reusable attachment (max 100MB) and get an attachment ID to send in messages', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + file: { + type: 'file', + required: false, + visibility: 'user-or-llm', + description: 'File to upload (a UserFile from a file-upload field or a previous block)', + }, + fileContent: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Legacy base64-encoded file content fallback', + }, + filename: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Override the file name (defaults to the uploaded file name)', + }, + contentType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Override the MIME type (defaults to the uploaded file type)', + }, + }, + + request: { + url: '/api/tools/linq/upload', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + apiKey: params.apiKey, + file: params.file, + fileContent: params.fileContent, + filename: params.filename, + contentType: params.contentType, + }), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok || !data?.success) { + return { + success: false, + error: data?.error ?? 'Failed to upload attachment', + output: { + attachmentId: '', + downloadUrl: null, + filename: '', + contentType: '', + sizeBytes: 0, + status: '', + }, + } + } + + const output = data.output ?? {} + return { + success: true, + output: { + attachmentId: output.attachmentId ?? '', + downloadUrl: output.downloadUrl ?? null, + filename: output.filename ?? '', + contentType: output.contentType ?? '', + sizeBytes: output.sizeBytes ?? 0, + status: output.status ?? '', + }, + } + }, + + outputs: { + attachmentId: { + type: 'string', + description: 'Reusable attachment ID to reference when sending messages or voice memos', + }, + downloadUrl: { + type: 'string', + description: 'URL the attachment can be downloaded from', + optional: true, + }, + filename: { type: 'string', description: 'File name' }, + contentType: { type: 'string', description: 'MIME type of the file' }, + sizeBytes: { type: 'number', description: 'File size in bytes' }, + status: { type: 'string', description: 'Upload status' }, + }, +} diff --git a/apps/sim/tools/linq/create_chat.ts b/apps/sim/tools/linq/create_chat.ts new file mode 100644 index 00000000000..5b57c44a167 --- /dev/null +++ b/apps/sim/tools/linq/create_chat.ts @@ -0,0 +1,153 @@ +import type { LinqCreateChatParams, LinqCreateChatResult } from '@/tools/linq/types' +import { + buildMessageContent, + extractLinqError, + LINQ_API_BASE, + linqHeaders, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqCreateChatTool: ToolConfig = { + id: 'linq_create_chat', + name: 'Create Chat', + description: 'Start a new iMessage, SMS, or RCS chat and send the first message', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + from: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Sender phone number in E.164 format (e.g. +14155551234)', + }, + to: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Recipient handles (phone numbers in E.164 format or email addresses)', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Text content of the first message. Optional, but at least one of text, media, attachment, or link is required', + }, + mediaUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional publicly accessible HTTPS URL of an image, video, or file to attach', + }, + attachmentId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional ID of a pre-uploaded attachment to send instead of a media URL', + }, + preferredService: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Preferred delivery service: iMessage, SMS, or RCS', + }, + effectName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional iMessage effect name (e.g. confetti, fireworks, lasers)', + }, + effectType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional effect type: screen or bubble', + }, + replyToMessageId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional message ID to reply to inline', + }, + replyToPartIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Optional part index of the message being replied to', + }, + idempotencyKey: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional idempotency key to safely retry the request', + }, + }, + + request: { + url: `${LINQ_API_BASE}/chats`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + if (params.linkUrl) { + throw new Error( + 'The first message of a new chat cannot be a link (Linq rejects it). Create the chat first, then send a link in a follow-up message.' + ) + } + return { + from: params.from, + to: params.to, + message: buildMessageContent(params), + } + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to create chat'), + output: { + chatId: '', + displayName: '', + isGroup: false, + service: null, + handles: [], + healthStatus: null, + message: null, + }, + } + } + + const chat = data.chat ?? {} + return { + success: true, + output: { + chatId: chat.id ?? '', + displayName: chat.display_name ?? '', + isGroup: chat.is_group ?? false, + service: chat.service ?? null, + handles: chat.handles ?? [], + healthStatus: chat.health_status ?? null, + message: chat.message ?? null, + }, + } + }, + + outputs: { + chatId: { type: 'string', description: 'ID of the created chat' }, + displayName: { type: 'string', description: 'Display name of the chat' }, + isGroup: { type: 'boolean', description: 'Whether the chat is a group chat' }, + service: { type: 'string', description: 'Delivery service used (iMessage, SMS, RCS)' }, + handles: { type: 'json', description: 'Participant handles in the chat' }, + healthStatus: { type: 'json', description: 'Messaging line health status', optional: true }, + message: { type: 'json', description: 'The sent message object with parts and delivery info' }, + }, +} diff --git a/apps/sim/tools/linq/create_contact_card.ts b/apps/sim/tools/linq/create_contact_card.ts new file mode 100644 index 00000000000..c9cebb7dac6 --- /dev/null +++ b/apps/sim/tools/linq/create_contact_card.ts @@ -0,0 +1,92 @@ +import type { LinqContactCardResult, LinqCreateContactCardParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqCreateContactCardTool: ToolConfig< + LinqCreateContactCardParams, + LinqContactCardResult +> = { + id: 'linq_create_contact_card', + name: 'Create Contact Card', + description: 'Set up a contact card (Name and Photo Sharing) for a phone number', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number in E.164 format the card applies to', + }, + firstName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'First name to display', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Last name to display', + }, + imageUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Profile photo URL', + }, + }, + + request: { + url: `${LINQ_API_BASE}/contact_card`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { + phone_number: params.phoneNumber, + first_name: params.firstName, + } + if (params.lastName !== undefined) body.last_name = params.lastName + if (params.imageUrl !== undefined) body.image_url = params.imageUrl + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to create contact card'), + output: { phoneNumber: '', firstName: '', lastName: null, imageUrl: null, isActive: false }, + } + } + + return { + success: true, + output: { + phoneNumber: data.phone_number ?? '', + firstName: data.first_name ?? '', + lastName: data.last_name ?? null, + imageUrl: data.image_url ?? null, + isActive: data.is_active ?? false, + }, + } + }, + + outputs: { + phoneNumber: { type: 'string', description: 'Phone number the card applies to' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name', optional: true }, + imageUrl: { type: 'string', description: 'Profile photo URL', optional: true }, + isActive: { type: 'boolean', description: 'Whether the card is active' }, + }, +} diff --git a/apps/sim/tools/linq/create_webhook_subscription.ts b/apps/sim/tools/linq/create_webhook_subscription.ts new file mode 100644 index 00000000000..3d51709ec95 --- /dev/null +++ b/apps/sim/tools/linq/create_webhook_subscription.ts @@ -0,0 +1,111 @@ +import type { + LinqCreateWebhookSubscriptionParams, + LinqCreateWebhookSubscriptionResult, +} from '@/tools/linq/types' +import { + extractLinqError, + LINQ_API_BASE, + linqHeaders, + mapWebhookSubscription, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqCreateWebhookSubscriptionTool: ToolConfig< + LinqCreateWebhookSubscriptionParams, + LinqCreateWebhookSubscriptionResult +> = { + id: 'linq_create_webhook_subscription', + name: 'Create Webhook Subscription', + description: 'Subscribe an HTTPS endpoint to Linq webhook events', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + targetUrl: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'HTTPS endpoint that will receive webhook events', + }, + subscribedEvents: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Event types to subscribe to (e.g. message.sent, message.delivered)', + }, + phoneNumbers: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'E.164 phone numbers to filter events by (omit for all numbers)', + }, + }, + + request: { + url: `${LINQ_API_BASE}/webhook-subscriptions`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { + target_url: params.targetUrl, + subscribed_events: params.subscribedEvents, + } + if (params.phoneNumbers && params.phoneNumbers.length > 0) { + body.phone_numbers = params.phoneNumbers + } + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to create webhook subscription'), + output: { + id: '', + targetUrl: '', + subscribedEvents: [], + phoneNumbers: null, + isActive: false, + createdAt: null, + updatedAt: null, + signingSecret: '', + }, + } + } + + return { + success: true, + output: { + ...mapWebhookSubscription(data), + signingSecret: data.signing_secret ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Subscription ID' }, + targetUrl: { type: 'string', description: 'Endpoint that receives events' }, + subscribedEvents: { type: 'json', description: 'Subscribed event types' }, + phoneNumbers: { + type: 'json', + description: 'Filtered phone numbers (null = all)', + optional: true, + }, + isActive: { type: 'boolean', description: 'Whether the subscription is active' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + signingSecret: { + type: 'string', + description: 'HMAC-SHA256 signing secret. Store securely — it cannot be retrieved again', + }, + }, +} diff --git a/apps/sim/tools/linq/delete_attachment.ts b/apps/sim/tools/linq/delete_attachment.ts new file mode 100644 index 00000000000..1557da8612e --- /dev/null +++ b/apps/sim/tools/linq/delete_attachment.ts @@ -0,0 +1,48 @@ +import type { LinqDeleteAttachmentParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqDeleteAttachmentTool: ToolConfig = { + id: 'linq_delete_attachment', + name: 'Delete Attachment', + description: 'Permanently delete an attachment owned by your account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the attachment to delete', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/attachments/${encodeURIComponent(params.attachmentId.trim())}`, + method: 'DELETE', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to delete attachment'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the attachment was deleted' }, + }, +} diff --git a/apps/sim/tools/linq/delete_message.ts b/apps/sim/tools/linq/delete_message.ts new file mode 100644 index 00000000000..1b4d43ca4c6 --- /dev/null +++ b/apps/sim/tools/linq/delete_message.ts @@ -0,0 +1,48 @@ +import type { LinqDeleteMessageParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqDeleteMessageTool: ToolConfig = { + id: 'linq_delete_message', + name: 'Delete Message', + description: + 'Delete a message from the Linq API only (does not unsend it; recipients still see it)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the message to delete', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/messages/${encodeURIComponent(params.messageId.trim())}`, + method: 'DELETE', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to delete message'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the message was deleted' }, + }, +} diff --git a/apps/sim/tools/linq/delete_webhook_subscription.ts b/apps/sim/tools/linq/delete_webhook_subscription.ts new file mode 100644 index 00000000000..6e5fd1184c1 --- /dev/null +++ b/apps/sim/tools/linq/delete_webhook_subscription.ts @@ -0,0 +1,51 @@ +import type { LinqDeleteWebhookSubscriptionParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqDeleteWebhookSubscriptionTool: ToolConfig< + LinqDeleteWebhookSubscriptionParams, + LinqSuccessResult +> = { + id: 'linq_delete_webhook_subscription', + name: 'Delete Webhook Subscription', + description: 'Delete a webhook subscription from your account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + subscriptionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the webhook subscription to delete', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/webhook-subscriptions/${encodeURIComponent(params.subscriptionId.trim())}`, + method: 'DELETE', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to delete webhook subscription'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the subscription was deleted' }, + }, +} diff --git a/apps/sim/tools/linq/edit_message.ts b/apps/sim/tools/linq/edit_message.ts new file mode 100644 index 00000000000..63772f04512 --- /dev/null +++ b/apps/sim/tools/linq/edit_message.ts @@ -0,0 +1,116 @@ +import type { LinqEditMessageParams, LinqMessageResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqEditMessageTool: ToolConfig = { + id: 'linq_edit_message', + name: 'Edit Message', + description: + 'Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iMessage only)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the message to edit', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New text content for the message part', + }, + partIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the message part to edit (defaults to 0)', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/messages/${encodeURIComponent(params.messageId.trim())}`, + method: 'PATCH', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { text: params.text } + if (typeof params.partIndex === 'number') body.part_index = params.partIndex + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to edit message'), + output: { + id: '', + chatId: '', + isFromMe: null, + isDelivered: null, + isRead: null, + service: null, + createdAt: null, + updatedAt: null, + sentAt: null, + parts: [], + message: {}, + }, + } + } + + return { + success: true, + output: { + id: data.id ?? '', + chatId: data.chat_id ?? '', + isFromMe: data.is_from_me ?? null, + isDelivered: data.is_delivered ?? null, + isRead: data.is_read ?? null, + service: data.service ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + sentAt: data.sent_at ?? null, + parts: data.parts ?? [], + message: data, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Message ID' }, + chatId: { type: 'string', description: 'ID of the chat the message belongs to' }, + isFromMe: { + type: 'boolean', + description: 'Whether the message was sent by you', + optional: true, + }, + isDelivered: { + type: 'boolean', + description: 'Whether the message was delivered', + optional: true, + }, + isRead: { type: 'boolean', description: 'Whether the message was read', optional: true }, + service: { + type: 'string', + description: 'Delivery service (iMessage, SMS, RCS)', + optional: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + sentAt: { type: 'string', description: 'ISO 8601 sent timestamp', optional: true }, + parts: { type: 'json', description: 'Updated message parts with reactions' }, + message: { type: 'json', description: 'The full updated message object' }, + }, +} diff --git a/apps/sim/tools/linq/get_attachment.ts b/apps/sim/tools/linq/get_attachment.ts new file mode 100644 index 00000000000..d958013a088 --- /dev/null +++ b/apps/sim/tools/linq/get_attachment.ts @@ -0,0 +1,75 @@ +import type { LinqAttachmentResult, LinqGetAttachmentParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqGetAttachmentTool: ToolConfig = { + id: 'linq_get_attachment', + name: 'Get Attachment', + description: 'Retrieve metadata for an attachment, including its download URL and status', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the attachment', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/attachments/${encodeURIComponent(params.attachmentId.trim())}`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to get attachment'), + output: { + id: '', + filename: '', + contentType: '', + sizeBytes: null, + status: '', + downloadUrl: null, + createdAt: null, + }, + } + } + + return { + success: true, + output: { + id: data.id ?? '', + filename: data.filename ?? '', + contentType: data.content_type ?? '', + sizeBytes: data.size_bytes ?? null, + status: data.status ?? '', + downloadUrl: data.download_url ?? null, + createdAt: data.created_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Attachment ID' }, + filename: { type: 'string', description: 'File name' }, + contentType: { type: 'string', description: 'MIME type of the file' }, + sizeBytes: { type: 'number', description: 'File size in bytes', optional: true }, + status: { type: 'string', description: 'Upload status (pending, complete, failed)' }, + downloadUrl: { type: 'string', description: 'URL to download the file', optional: true }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/get_chat.ts b/apps/sim/tools/linq/get_chat.ts new file mode 100644 index 00000000000..93cfdde22e1 --- /dev/null +++ b/apps/sim/tools/linq/get_chat.ts @@ -0,0 +1,84 @@ +import type { LinqChatResult, LinqGetChatParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqGetChatTool: ToolConfig = { + id: 'linq_get_chat', + name: 'Get Chat', + description: 'Retrieve a chat by ID, including participants and line health', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to get chat'), + output: { + id: '', + displayName: '', + isGroup: false, + isArchived: null, + service: null, + createdAt: null, + updatedAt: null, + handles: [], + healthStatus: null, + }, + } + } + + return { + success: true, + output: { + id: data.id ?? '', + displayName: data.display_name ?? '', + isGroup: data.is_group ?? false, + isArchived: data.is_archived ?? null, + service: data.service ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + handles: data.handles ?? [], + healthStatus: data.health_status ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Chat ID' }, + displayName: { type: 'string', description: 'Display name of the chat' }, + isGroup: { type: 'boolean', description: 'Whether the chat is a group chat' }, + isArchived: { type: 'boolean', description: 'Whether the chat is archived', optional: true }, + service: { + type: 'string', + description: 'Delivery service (iMessage, SMS, RCS)', + optional: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + handles: { type: 'json', description: 'Participant handles in the chat' }, + healthStatus: { type: 'json', description: 'Messaging line health status', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/get_contact_card.ts b/apps/sim/tools/linq/get_contact_card.ts new file mode 100644 index 00000000000..6aec63be1e0 --- /dev/null +++ b/apps/sim/tools/linq/get_contact_card.ts @@ -0,0 +1,81 @@ +import type { LinqGetContactCardParams, LinqGetContactCardResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqGetContactCardTool: ToolConfig< + LinqGetContactCardParams, + LinqGetContactCardResult +> = { + id: 'linq_get_contact_card', + name: 'Get Contact Card', + description: 'Retrieve contact cards, optionally filtered by phone number', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + phoneNumber: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'E.164 phone number to filter by (omit to return all cards)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.phoneNumber) query.set('phone_number', params.phoneNumber) + const qs = query.toString() + return `${LINQ_API_BASE}/contact_card${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to get contact card'), + output: { contactCards: [] }, + } + } + + return { + success: true, + output: { + contactCards: (data.contact_cards ?? []).map((card: Record) => ({ + phoneNumber: (card.phone_number as string) ?? '', + firstName: (card.first_name as string) ?? '', + lastName: (card.last_name as string | null) ?? null, + imageUrl: (card.image_url as string | null) ?? null, + isActive: (card.is_active as boolean) ?? false, + })), + }, + } + }, + + outputs: { + contactCards: { + type: 'array', + description: 'Contact cards on the account', + items: { + type: 'object', + properties: { + phoneNumber: { type: 'string', description: 'Phone number in E.164 format' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name', optional: true }, + imageUrl: { type: 'string', description: 'Profile photo URL', optional: true }, + isActive: { type: 'boolean', description: 'Whether the card is active' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/linq/get_message.ts b/apps/sim/tools/linq/get_message.ts new file mode 100644 index 00000000000..9ed38b36b00 --- /dev/null +++ b/apps/sim/tools/linq/get_message.ts @@ -0,0 +1,98 @@ +import type { LinqGetMessageParams, LinqMessageResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqGetMessageTool: ToolConfig = { + id: 'linq_get_message', + name: 'Get Message', + description: 'Retrieve a single message by ID, including parts, reactions, and delivery status', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the message', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/messages/${encodeURIComponent(params.messageId.trim())}`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to get message'), + output: { + id: '', + chatId: '', + isFromMe: null, + isDelivered: null, + isRead: null, + service: null, + createdAt: null, + updatedAt: null, + sentAt: null, + parts: [], + message: {}, + }, + } + } + + return { + success: true, + output: { + id: data.id ?? '', + chatId: data.chat_id ?? '', + isFromMe: data.is_from_me ?? null, + isDelivered: data.is_delivered ?? null, + isRead: data.is_read ?? null, + service: data.service ?? null, + createdAt: data.created_at ?? null, + updatedAt: data.updated_at ?? null, + sentAt: data.sent_at ?? null, + parts: data.parts ?? [], + message: data, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Message ID' }, + chatId: { type: 'string', description: 'ID of the chat the message belongs to' }, + isFromMe: { + type: 'boolean', + description: 'Whether the message was sent by you', + optional: true, + }, + isDelivered: { + type: 'boolean', + description: 'Whether the message was delivered', + optional: true, + }, + isRead: { type: 'boolean', description: 'Whether the message was read', optional: true }, + service: { + type: 'string', + description: 'Delivery service (iMessage, SMS, RCS)', + optional: true, + }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + sentAt: { type: 'string', description: 'ISO 8601 sent timestamp', optional: true }, + parts: { type: 'json', description: 'Message parts (text, media, link) with reactions' }, + message: { type: 'json', description: 'The full message object' }, + }, +} diff --git a/apps/sim/tools/linq/get_webhook_subscription.ts b/apps/sim/tools/linq/get_webhook_subscription.ts new file mode 100644 index 00000000000..02829efba4c --- /dev/null +++ b/apps/sim/tools/linq/get_webhook_subscription.ts @@ -0,0 +1,82 @@ +import type { + LinqGetWebhookSubscriptionParams, + LinqWebhookSubscriptionResult, +} from '@/tools/linq/types' +import { + extractLinqError, + LINQ_API_BASE, + linqHeaders, + mapWebhookSubscription, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqGetWebhookSubscriptionTool: ToolConfig< + LinqGetWebhookSubscriptionParams, + LinqWebhookSubscriptionResult +> = { + id: 'linq_get_webhook_subscription', + name: 'Get Webhook Subscription', + description: 'Retrieve a webhook subscription by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + subscriptionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the webhook subscription', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/webhook-subscriptions/${encodeURIComponent(params.subscriptionId.trim())}`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to get webhook subscription'), + output: { + id: '', + targetUrl: '', + subscribedEvents: [], + phoneNumbers: null, + isActive: false, + createdAt: null, + updatedAt: null, + }, + } + } + + return { + success: true, + output: mapWebhookSubscription(data), + } + }, + + outputs: { + id: { type: 'string', description: 'Subscription ID' }, + targetUrl: { type: 'string', description: 'Endpoint that receives events' }, + subscribedEvents: { type: 'json', description: 'Subscribed event types' }, + phoneNumbers: { + type: 'json', + description: 'Filtered phone numbers (null = all)', + optional: true, + }, + isActive: { type: 'boolean', description: 'Whether the subscription is active' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/index.ts b/apps/sim/tools/linq/index.ts new file mode 100644 index 00000000000..fb27ef97c7a --- /dev/null +++ b/apps/sim/tools/linq/index.ts @@ -0,0 +1,35 @@ +export { linqAddParticipantTool } from '@/tools/linq/add_participant' +export { linqCheckImessageTool } from '@/tools/linq/check_imessage' +export { linqCheckRcsTool } from '@/tools/linq/check_rcs' +export { linqCreateAttachmentTool } from '@/tools/linq/create_attachment' +export { linqCreateChatTool } from '@/tools/linq/create_chat' +export { linqCreateContactCardTool } from '@/tools/linq/create_contact_card' +export { linqCreateWebhookSubscriptionTool } from '@/tools/linq/create_webhook_subscription' +export { linqDeleteAttachmentTool } from '@/tools/linq/delete_attachment' +export { linqDeleteMessageTool } from '@/tools/linq/delete_message' +export { linqDeleteWebhookSubscriptionTool } from '@/tools/linq/delete_webhook_subscription' +export { linqEditMessageTool } from '@/tools/linq/edit_message' +export { linqGetAttachmentTool } from '@/tools/linq/get_attachment' +export { linqGetChatTool } from '@/tools/linq/get_chat' +export { linqGetContactCardTool } from '@/tools/linq/get_contact_card' +export { linqGetMessageTool } from '@/tools/linq/get_message' +export { linqGetWebhookSubscriptionTool } from '@/tools/linq/get_webhook_subscription' +export { linqLeaveChatTool } from '@/tools/linq/leave_chat' +export { linqListChatsTool } from '@/tools/linq/list_chats' +export { linqListMessagesTool } from '@/tools/linq/list_messages' +export { linqListPhoneNumbersTool } from '@/tools/linq/list_phone_numbers' +export { linqListThreadTool } from '@/tools/linq/list_thread' +export { linqListWebhookEventsTool } from '@/tools/linq/list_webhook_events' +export { linqListWebhookSubscriptionsTool } from '@/tools/linq/list_webhook_subscriptions' +export { linqMarkChatReadTool } from '@/tools/linq/mark_chat_read' +export { linqReactToMessageTool } from '@/tools/linq/react_to_message' +export { linqRemoveParticipantTool } from '@/tools/linq/remove_participant' +export { linqSendMessageTool } from '@/tools/linq/send_message' +export { linqSendVoiceMemoTool } from '@/tools/linq/send_voice_memo' +export { linqShareContactCardTool } from '@/tools/linq/share_contact_card' +export { linqStartTypingTool } from '@/tools/linq/start_typing' +export { linqStopTypingTool } from '@/tools/linq/stop_typing' +export * from '@/tools/linq/types' +export { linqUpdateChatTool } from '@/tools/linq/update_chat' +export { linqUpdateContactCardTool } from '@/tools/linq/update_contact_card' +export { linqUpdateWebhookSubscriptionTool } from '@/tools/linq/update_webhook_subscription' diff --git a/apps/sim/tools/linq/leave_chat.ts b/apps/sim/tools/linq/leave_chat.ts new file mode 100644 index 00000000000..ccabe9b32ef --- /dev/null +++ b/apps/sim/tools/linq/leave_chat.ts @@ -0,0 +1,59 @@ +import type { LinqChatActionParams, LinqQueuedResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqLeaveChatTool: ToolConfig = { + id: 'linq_leave_chat', + name: 'Leave Chat', + description: + 'Leave an iMessage group chat (4+ active participants; not supported for direct chats)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the group chat', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/leave`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to leave chat'), + output: { message: null, status: null, traceId: null }, + } + } + + return { + success: true, + output: { + message: data.message ?? null, + status: data.status ?? null, + traceId: data.trace_id ?? null, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Human-readable status message', optional: true }, + status: { type: 'string', description: 'Queued action status (e.g. accepted)', optional: true }, + traceId: { type: 'string', description: 'Trace ID for the queued action', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/list_chats.ts b/apps/sim/tools/linq/list_chats.ts new file mode 100644 index 00000000000..d57652faddc --- /dev/null +++ b/apps/sim/tools/linq/list_chats.ts @@ -0,0 +1,86 @@ +import type { LinqListChatsParams, LinqListChatsResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListChatsTool: ToolConfig = { + id: 'linq_list_chats', + name: 'List Chats', + description: 'List chats, optionally filtered by sender or participant handle', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + from: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by sender phone number in E.164 format', + }, + to: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by participant handle (phone number or email)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Results per page (default 20, max 100)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.from) query.set('from', params.from) + if (params.to) query.set('to', params.to) + if (typeof params.limit === 'number') query.set('limit', String(params.limit)) + if (params.cursor) query.set('cursor', params.cursor) + const qs = query.toString() + return `${LINQ_API_BASE}/chats${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list chats'), + output: { chats: [], nextCursor: null }, + } + } + + return { + success: true, + output: { + chats: data.chats ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + chats: { type: 'json', description: 'Array of chat objects' }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page, or null if there are no more results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/linq/list_messages.ts b/apps/sim/tools/linq/list_messages.ts new file mode 100644 index 00000000000..aff4b64125d --- /dev/null +++ b/apps/sim/tools/linq/list_messages.ts @@ -0,0 +1,80 @@ +import type { LinqListMessagesParams, LinqListMessagesResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListMessagesTool: ToolConfig = { + id: 'linq_list_messages', + name: 'List Messages', + description: 'List messages in a chat with pagination', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of messages to return', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (typeof params.limit === 'number') query.set('limit', String(params.limit)) + if (params.cursor) query.set('cursor', params.cursor) + const qs = query.toString() + return `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/messages${ + qs ? `?${qs}` : '' + }` + }, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list messages'), + output: { messages: [], nextCursor: null }, + } + } + + return { + success: true, + output: { + messages: data.messages ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + messages: { type: 'json', description: 'Array of message objects with parts and reactions' }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page, or null if there are no more results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/linq/list_phone_numbers.ts b/apps/sim/tools/linq/list_phone_numbers.ts new file mode 100644 index 00000000000..14f153b5d83 --- /dev/null +++ b/apps/sim/tools/linq/list_phone_numbers.ts @@ -0,0 +1,70 @@ +import type { + LinqHealthStatus, + LinqListPhoneNumbersParams, + LinqListPhoneNumbersResult, +} from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListPhoneNumbersTool: ToolConfig< + LinqListPhoneNumbersParams, + LinqListPhoneNumbersResult +> = { + id: 'linq_list_phone_numbers', + name: 'List Phone Numbers', + description: 'List all phone numbers assigned to your partner account, with line health', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + }, + + request: { + url: `${LINQ_API_BASE}/phone_numbers`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list phone numbers'), + output: { phoneNumbers: [] }, + } + } + + return { + success: true, + output: { + phoneNumbers: (data.phone_numbers ?? []).map((num: Record) => ({ + id: (num.id as string) ?? '', + phoneNumber: (num.phone_number as string) ?? '', + healthStatus: (num.health_status as LinqHealthStatus | undefined) ?? null, + })), + }, + } + }, + + outputs: { + phoneNumbers: { + type: 'array', + description: 'Phone numbers assigned to the account', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Phone number ID' }, + phoneNumber: { type: 'string', description: 'Phone number in E.164 format' }, + healthStatus: { type: 'json', description: 'Line health status (status, doc_url)' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/linq/list_thread.ts b/apps/sim/tools/linq/list_thread.ts new file mode 100644 index 00000000000..5d44bfb2612 --- /dev/null +++ b/apps/sim/tools/linq/list_thread.ts @@ -0,0 +1,87 @@ +import type { LinqListMessagesResult, LinqListThreadParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListThreadTool: ToolConfig = { + id: 'linq_list_thread', + name: 'List Thread Messages', + description: 'List all messages in the thread that contains a given message', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of any message in the thread', + }, + order: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc (oldest first) or desc (newest first)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of messages to return', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from a previous response', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.order) query.set('order', params.order) + if (typeof params.limit === 'number') query.set('limit', String(params.limit)) + if (params.cursor) query.set('cursor', params.cursor) + const qs = query.toString() + return `${LINQ_API_BASE}/messages/${encodeURIComponent(params.messageId.trim())}/thread${ + qs ? `?${qs}` : '' + }` + }, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list thread messages'), + output: { messages: [], nextCursor: null }, + } + } + + return { + success: true, + output: { + messages: data.messages ?? [], + nextCursor: data.next_cursor ?? null, + }, + } + }, + + outputs: { + messages: { type: 'json', description: 'Array of message objects in the thread' }, + nextCursor: { + type: 'string', + description: 'Cursor for the next page, or null if there are no more results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/linq/list_webhook_events.ts b/apps/sim/tools/linq/list_webhook_events.ts new file mode 100644 index 00000000000..e31cf7ec0b2 --- /dev/null +++ b/apps/sim/tools/linq/list_webhook_events.ts @@ -0,0 +1,53 @@ +import type { LinqListWebhookEventsParams, LinqListWebhookEventsResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListWebhookEventsTool: ToolConfig< + LinqListWebhookEventsParams, + LinqListWebhookEventsResult +> = { + id: 'linq_list_webhook_events', + name: 'List Webhook Events', + description: 'List all webhook event types available to subscribe to', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + }, + + request: { + url: `${LINQ_API_BASE}/webhook-events`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list webhook events'), + output: { events: [], docUrl: null }, + } + } + + return { + success: true, + output: { + events: data.events ?? [], + docUrl: data.doc_url ?? null, + }, + } + }, + + outputs: { + events: { type: 'json', description: 'Available webhook event type names' }, + docUrl: { type: 'string', description: 'Documentation URL for webhook events', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/list_webhook_subscriptions.ts b/apps/sim/tools/linq/list_webhook_subscriptions.ts new file mode 100644 index 00000000000..391c6680e73 --- /dev/null +++ b/apps/sim/tools/linq/list_webhook_subscriptions.ts @@ -0,0 +1,74 @@ +import type { + LinqListWebhookSubscriptionsParams, + LinqListWebhookSubscriptionsResult, +} from '@/tools/linq/types' +import { + extractLinqError, + LINQ_API_BASE, + linqHeaders, + mapWebhookSubscription, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqListWebhookSubscriptionsTool: ToolConfig< + LinqListWebhookSubscriptionsParams, + LinqListWebhookSubscriptionsResult +> = { + id: 'linq_list_webhook_subscriptions', + name: 'List Webhook Subscriptions', + description: 'List all webhook subscriptions on your account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + }, + + request: { + url: `${LINQ_API_BASE}/webhook-subscriptions`, + method: 'GET', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to list webhook subscriptions'), + output: { subscriptions: [] }, + } + } + + return { + success: true, + output: { + subscriptions: (data.subscriptions ?? []).map(mapWebhookSubscription), + }, + } + }, + + outputs: { + subscriptions: { + type: 'array', + description: 'Webhook subscriptions', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Subscription ID' }, + targetUrl: { type: 'string', description: 'Endpoint that receives events' }, + subscribedEvents: { type: 'json', description: 'Subscribed event types' }, + phoneNumbers: { type: 'json', description: 'Filtered phone numbers (null = all)' }, + isActive: { type: 'boolean', description: 'Whether the subscription is active' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/linq/mark_chat_read.ts b/apps/sim/tools/linq/mark_chat_read.ts new file mode 100644 index 00000000000..25ea3e16c74 --- /dev/null +++ b/apps/sim/tools/linq/mark_chat_read.ts @@ -0,0 +1,47 @@ +import type { LinqChatActionParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqMarkChatReadTool: ToolConfig = { + id: 'linq_mark_chat_read', + name: 'Mark Chat as Read', + description: 'Mark all messages in a chat as read', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/read`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to mark chat as read'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the chat was marked as read' }, + }, +} diff --git a/apps/sim/tools/linq/react_to_message.ts b/apps/sim/tools/linq/react_to_message.ts new file mode 100644 index 00000000000..a2ab9bc2893 --- /dev/null +++ b/apps/sim/tools/linq/react_to_message.ts @@ -0,0 +1,93 @@ +import type { LinqQueuedResult, LinqReactToMessageParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqReactToMessageTool: ToolConfig = { + id: 'linq_react_to_message', + name: 'React to Message', + description: 'Add or remove a tapback reaction on a message', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the message to react to', + }, + operation: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Whether to add or remove the reaction: add or remove', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Reaction type: love, like, dislike, laugh, emphasize, question, custom, or sticker', + }, + customEmoji: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Emoji to use when type is custom', + }, + partIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the message part to react to (defaults to the entire message)', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/messages/${encodeURIComponent(params.messageId.trim())}/reactions`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = { + operation: params.operation, + type: params.type, + } + if (params.customEmoji) body.custom_emoji = params.customEmoji + if (typeof params.partIndex === 'number') body.part_index = params.partIndex + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to react to message'), + output: { message: null, status: null, traceId: null }, + } + } + + return { + success: true, + output: { + message: data.message ?? null, + status: data.status ?? null, + traceId: data.trace_id ?? null, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Human-readable status message', optional: true }, + status: { type: 'string', description: 'Queued action status', optional: true }, + traceId: { type: 'string', description: 'Trace ID for the queued action', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/remove_participant.ts b/apps/sim/tools/linq/remove_participant.ts new file mode 100644 index 00000000000..9acf7611943 --- /dev/null +++ b/apps/sim/tools/linq/remove_participant.ts @@ -0,0 +1,66 @@ +import type { LinqParticipantParams, LinqQueuedResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqRemoveParticipantTool: ToolConfig = { + id: 'linq_remove_participant', + name: 'Remove Participant', + description: 'Remove a participant from a group chat (minimum 3 participants must remain)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the group chat', + }, + handle: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number (E.164 format) or email address of the participant to remove', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/participants`, + method: 'DELETE', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => ({ handle: params.handle }), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to remove participant'), + output: { message: null, status: null, traceId: null }, + } + } + + return { + success: true, + output: { + message: data.message ?? null, + status: data.status ?? null, + traceId: data.trace_id ?? null, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Human-readable status message', optional: true }, + status: { type: 'string', description: 'Queued action status', optional: true }, + traceId: { type: 'string', description: 'Trace ID for the queued action', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/send_message.ts b/apps/sim/tools/linq/send_message.ts new file mode 100644 index 00000000000..f7448ece0d7 --- /dev/null +++ b/apps/sim/tools/linq/send_message.ts @@ -0,0 +1,152 @@ +import type { LinqSendMessageParams, LinqSendMessageResult } from '@/tools/linq/types' +import { + buildMessageContent, + extractLinqError, + LINQ_API_BASE, + linqHeaders, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqSendMessageTool: ToolConfig = { + id: 'linq_send_message', + name: 'Send Message', + description: 'Send a message to an existing chat, with optional media, link, effect, or reply', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Text content of the message. Optional, but at least one of text, media, attachment, or link is required', + }, + mediaUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional publicly accessible HTTPS URL of an image, video, or file to attach', + }, + attachmentId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional ID of a pre-uploaded attachment to send instead of a media URL', + }, + linkUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Optional URL to send as a rich link preview. Linq requires a link to be its own message, so when set, text and media are ignored', + }, + preferredService: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Preferred delivery service: iMessage, SMS, or RCS', + }, + effectName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional iMessage effect name (e.g. confetti, fireworks, lasers)', + }, + effectType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional effect type: screen or bubble', + }, + replyToMessageId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional message ID to reply to inline', + }, + replyToPartIndex: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Optional part index of the message being replied to', + }, + idempotencyKey: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional idempotency key to safely retry the request', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/messages`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => ({ message: buildMessageContent(params) }), + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to send message'), + output: { + chatId: '', + messageId: '', + deliveryStatus: null, + sentAt: null, + service: null, + message: null, + }, + } + } + + const message = data.message ?? {} + return { + success: true, + output: { + chatId: data.chat_id ?? '', + messageId: message.id ?? '', + deliveryStatus: message.delivery_status ?? null, + sentAt: message.sent_at ?? null, + service: message.service ?? null, + message, + }, + } + }, + + outputs: { + chatId: { type: 'string', description: 'ID of the chat the message was sent to' }, + messageId: { type: 'string', description: 'ID of the sent message' }, + deliveryStatus: { + type: 'string', + description: 'Delivery status (pending, queued, sent, delivered, failed)', + optional: true, + }, + sentAt: { + type: 'string', + description: 'ISO 8601 timestamp the message was sent', + optional: true, + }, + service: { + type: 'string', + description: 'Delivery service (iMessage, SMS, RCS)', + optional: true, + }, + message: { type: 'json', description: 'The full sent message object with parts' }, + }, +} diff --git a/apps/sim/tools/linq/send_voice_memo.ts b/apps/sim/tools/linq/send_voice_memo.ts new file mode 100644 index 00000000000..d13efe6c5c9 --- /dev/null +++ b/apps/sim/tools/linq/send_voice_memo.ts @@ -0,0 +1,95 @@ +import type { LinqSendVoiceMemoParams, LinqSendVoiceMemoResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqSendVoiceMemoTool: ToolConfig = { + id: 'linq_send_voice_memo', + name: 'Send Voice Memo', + description: 'Send a voice memo to a chat from a URL or a pre-uploaded attachment', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + voiceMemoUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Publicly accessible HTTPS URL of the audio file (MP3, M4A, AAC, CAF, WAV, AIFF, AMR)', + }, + attachmentId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ID of a pre-uploaded audio attachment (use instead of voiceMemoUrl)', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/voicememo`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + if (!params.attachmentId && !params.voiceMemoUrl) { + throw new Error('Provide either a voice memo URL or a pre-uploaded attachment ID') + } + const body: Record = {} + if (params.attachmentId) body.attachment_id = params.attachmentId + else if (params.voiceMemoUrl) body.voice_memo_url = params.voiceMemoUrl + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to send voice memo'), + output: { id: '', status: null, from: null, to: [], service: null, voiceMemo: null }, + } + } + + const memo = data.voice_memo ?? {} + return { + success: true, + output: { + id: memo.id ?? '', + status: memo.status ?? null, + from: memo.from ?? null, + to: memo.to ?? [], + service: memo.service ?? null, + voiceMemo: memo.voice_memo ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'ID of the sent voice memo message' }, + status: { type: 'string', description: 'Delivery status', optional: true }, + from: { type: 'string', description: 'Sender handle', optional: true }, + to: { type: 'json', description: 'Recipient handles' }, + service: { + type: 'string', + description: 'Delivery service (iMessage, SMS, RCS)', + optional: true, + }, + voiceMemo: { + type: 'json', + description: 'Audio file metadata (id, filename, mime_type, size_bytes, url, duration_ms)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/linq/share_contact_card.ts b/apps/sim/tools/linq/share_contact_card.ts new file mode 100644 index 00000000000..94b795d1520 --- /dev/null +++ b/apps/sim/tools/linq/share_contact_card.ts @@ -0,0 +1,48 @@ +import type { LinqChatActionParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqShareContactCardTool: ToolConfig = { + id: 'linq_share_contact_card', + name: 'Share Contact Card', + description: 'Share your configured contact card (Name and Photo Sharing) with a chat', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/share_contact_card`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to share contact card'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the contact card was shared' }, + }, +} diff --git a/apps/sim/tools/linq/start_typing.ts b/apps/sim/tools/linq/start_typing.ts new file mode 100644 index 00000000000..ec43ea9f28a --- /dev/null +++ b/apps/sim/tools/linq/start_typing.ts @@ -0,0 +1,47 @@ +import type { LinqChatActionParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqStartTypingTool: ToolConfig = { + id: 'linq_start_typing', + name: 'Start Typing Indicator', + description: 'Show a typing indicator in a one-on-one chat (iMessage only, not group chats)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/typing`, + method: 'POST', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to start typing indicator'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the typing indicator was sent' }, + }, +} diff --git a/apps/sim/tools/linq/stop_typing.ts b/apps/sim/tools/linq/stop_typing.ts new file mode 100644 index 00000000000..22e00b95c39 --- /dev/null +++ b/apps/sim/tools/linq/stop_typing.ts @@ -0,0 +1,47 @@ +import type { LinqChatActionParams, LinqSuccessResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqStopTypingTool: ToolConfig = { + id: 'linq_stop_typing', + name: 'Stop Typing Indicator', + description: 'Stop the typing indicator in a one-on-one chat (iMessage only, not group chats)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}/typing`, + method: 'DELETE', + headers: (params) => linqHeaders(params.apiKey), + }, + + transformResponse: async (response): Promise => { + if (response.ok) { + return { success: true, output: { success: true } } + } + const data = await response.json().catch(() => null) + return { + success: false, + error: extractLinqError(data, 'Failed to stop typing indicator'), + output: { success: false }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the typing indicator was stopped' }, + }, +} diff --git a/apps/sim/tools/linq/types.ts b/apps/sim/tools/linq/types.ts new file mode 100644 index 00000000000..13f40bda431 --- /dev/null +++ b/apps/sim/tools/linq/types.ts @@ -0,0 +1,395 @@ +import type { ToolResponse } from '@/tools/types' + +/** Messaging service a chat or message is delivered over. */ +export type LinqServiceType = 'iMessage' | 'SMS' | 'RCS' + +/** Tapback / reaction types supported by the Linq API. */ +export type LinqReactionType = + | 'love' + | 'like' + | 'dislike' + | 'laugh' + | 'emphasize' + | 'question' + | 'custom' + | 'sticker' + +/** A participant handle within a chat. */ +export interface LinqChatHandle { + id: string + handle: string + joined_at: string + service: LinqServiceType + is_me?: boolean + left_at?: string | null + status?: 'active' | 'left' | 'removed' +} + +/** Health status of a chat or phone number line. */ +export interface LinqHealthStatus { + status: string + doc_url: string + updated_at?: string +} + +interface LinqBaseParams { + apiKey: string +} + +export interface LinqCreateChatParams extends LinqBaseParams { + from: string + to: string[] + text?: string + mediaUrl?: string + attachmentId?: string + linkUrl?: string + preferredService?: LinqServiceType + effectName?: string + effectType?: string + replyToMessageId?: string + replyToPartIndex?: number + idempotencyKey?: string +} + +export interface LinqCreateChatResult extends ToolResponse { + output: { + chatId: string + displayName: string + isGroup: boolean + service: string | null + handles: LinqChatHandle[] + healthStatus: LinqHealthStatus | null + message: Record | null + } +} + +export interface LinqListChatsParams extends LinqBaseParams { + cursor?: string + from?: string + to?: string + limit?: number +} + +export interface LinqListChatsResult extends ToolResponse { + output: { + chats: Array> + nextCursor: string | null + } +} + +export interface LinqGetChatParams extends LinqBaseParams { + chatId: string +} + +export interface LinqChatResult extends ToolResponse { + output: { + id: string + displayName: string + isGroup: boolean + isArchived: boolean | null + service: string | null + createdAt: string | null + updatedAt: string | null + handles: LinqChatHandle[] + healthStatus: LinqHealthStatus | null + } +} + +export interface LinqUpdateChatParams extends LinqBaseParams { + chatId: string + displayName?: string + groupChatIcon?: string +} + +export interface LinqUpdateChatResult extends ToolResponse { + output: { + chatId: string | null + status: string | null + } +} + +export interface LinqChatActionParams extends LinqBaseParams { + chatId: string +} + +/** Generic success-shaped response used by simple action endpoints. */ +export interface LinqSuccessResult extends ToolResponse { + output: { + success: boolean + } +} + +/** Queued-action response: { message, status, trace_id }. */ +export interface LinqQueuedResult extends ToolResponse { + output: { + message: string | null + status: string | null + traceId: string | null + } +} + +export interface LinqSendVoiceMemoParams extends LinqBaseParams { + chatId: string + voiceMemoUrl?: string + attachmentId?: string +} + +export interface LinqSendVoiceMemoResult extends ToolResponse { + output: { + id: string + status: string | null + from: string | null + to: string[] + service: string | null + voiceMemo: Record | null + } +} + +export interface LinqParticipantParams extends LinqBaseParams { + chatId: string + handle: string +} + +export interface LinqSendMessageParams extends LinqBaseParams { + chatId: string + text?: string + mediaUrl?: string + attachmentId?: string + linkUrl?: string + preferredService?: LinqServiceType + effectName?: string + effectType?: string + replyToMessageId?: string + replyToPartIndex?: number + idempotencyKey?: string +} + +export interface LinqSendMessageResult extends ToolResponse { + output: { + chatId: string + messageId: string + deliveryStatus: string | null + sentAt: string | null + service: string | null + message: Record | null + } +} + +export interface LinqListMessagesParams extends LinqBaseParams { + chatId: string + cursor?: string + limit?: number +} + +export interface LinqListThreadParams extends LinqBaseParams { + messageId: string + cursor?: string + limit?: number + order?: 'asc' | 'desc' +} + +export interface LinqListMessagesResult extends ToolResponse { + output: { + messages: Array> + nextCursor: string | null + } +} + +export interface LinqGetMessageParams extends LinqBaseParams { + messageId: string +} + +export interface LinqMessageResult extends ToolResponse { + output: { + id: string + chatId: string + isFromMe: boolean | null + isDelivered: boolean | null + isRead: boolean | null + service: string | null + createdAt: string | null + updatedAt: string | null + sentAt: string | null + parts: Array> + message: Record + } +} + +export interface LinqEditMessageParams extends LinqBaseParams { + messageId: string + text: string + partIndex?: number +} + +export interface LinqDeleteMessageParams extends LinqBaseParams { + messageId: string +} + +export interface LinqReactToMessageParams extends LinqBaseParams { + messageId: string + operation: 'add' | 'remove' + type: LinqReactionType + customEmoji?: string + partIndex?: number +} + +export interface LinqCreateAttachmentParams extends LinqBaseParams { + /** UserFile object (or reference) to upload. */ + file?: unknown + /** Legacy base64 file content fallback. */ + fileContent?: string + filename?: string + contentType?: string +} + +export interface LinqCreateAttachmentResult extends ToolResponse { + output: { + attachmentId: string + downloadUrl: string | null + filename: string + contentType: string + sizeBytes: number + status: string + } +} + +export interface LinqGetAttachmentParams extends LinqBaseParams { + attachmentId: string +} + +export interface LinqAttachmentResult extends ToolResponse { + output: { + id: string + filename: string + contentType: string + sizeBytes: number | null + status: string + downloadUrl: string | null + createdAt: string | null + } +} + +export interface LinqDeleteAttachmentParams extends LinqBaseParams { + attachmentId: string +} + +export interface LinqListPhoneNumbersParams extends LinqBaseParams {} + +export interface LinqListPhoneNumbersResult extends ToolResponse { + output: { + phoneNumbers: Array<{ + id: string + phoneNumber: string + healthStatus: LinqHealthStatus | null + }> + } +} + +export interface LinqCapabilityCheckParams extends LinqBaseParams { + address: string + from?: string +} + +export interface LinqCapabilityCheckResult extends ToolResponse { + output: { + address: string + available: boolean + } +} + +export interface LinqGetContactCardParams extends LinqBaseParams { + phoneNumber?: string +} + +export interface LinqGetContactCardResult extends ToolResponse { + output: { + contactCards: Array<{ + phoneNumber: string + firstName: string + lastName: string | null + imageUrl: string | null + isActive: boolean + }> + } +} + +export interface LinqCreateContactCardParams extends LinqBaseParams { + phoneNumber: string + firstName: string + lastName?: string + imageUrl?: string +} + +export interface LinqUpdateContactCardParams extends LinqBaseParams { + phoneNumber: string + firstName?: string + lastName?: string + imageUrl?: string +} + +export interface LinqContactCardResult extends ToolResponse { + output: { + phoneNumber: string + firstName: string + lastName: string | null + imageUrl: string | null + isActive: boolean + } +} + +export interface LinqWebhookSubscription { + id: string + targetUrl: string + subscribedEvents: string[] + phoneNumbers: string[] | null + isActive: boolean + createdAt: string | null + updatedAt: string | null +} + +export interface LinqCreateWebhookSubscriptionParams extends LinqBaseParams { + targetUrl: string + subscribedEvents: string[] + phoneNumbers?: string[] +} + +export interface LinqCreateWebhookSubscriptionResult extends ToolResponse { + output: LinqWebhookSubscription & { signingSecret: string } +} + +export interface LinqListWebhookSubscriptionsParams extends LinqBaseParams {} + +export interface LinqListWebhookSubscriptionsResult extends ToolResponse { + output: { + subscriptions: LinqWebhookSubscription[] + } +} + +export interface LinqGetWebhookSubscriptionParams extends LinqBaseParams { + subscriptionId: string +} + +export interface LinqWebhookSubscriptionResult extends ToolResponse { + output: LinqWebhookSubscription +} + +export interface LinqUpdateWebhookSubscriptionParams extends LinqBaseParams { + subscriptionId: string + targetUrl?: string + subscribedEvents?: string[] + phoneNumbers?: string[] + isActive?: boolean +} + +export interface LinqDeleteWebhookSubscriptionParams extends LinqBaseParams { + subscriptionId: string +} + +export interface LinqListWebhookEventsParams extends LinqBaseParams {} + +export interface LinqListWebhookEventsResult extends ToolResponse { + output: { + events: string[] + docUrl: string | null + } +} diff --git a/apps/sim/tools/linq/update_chat.ts b/apps/sim/tools/linq/update_chat.ts new file mode 100644 index 00000000000..9eef2ad93d7 --- /dev/null +++ b/apps/sim/tools/linq/update_chat.ts @@ -0,0 +1,74 @@ +import type { LinqUpdateChatParams, LinqUpdateChatResult } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqUpdateChatTool: ToolConfig = { + id: 'linq_update_chat', + name: 'Update Chat', + description: 'Update chat properties such as group display name and icon', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the chat', + }, + displayName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New display name for the group chat', + }, + groupChatIcon: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New group chat icon (publicly accessible image URL)', + }, + }, + + request: { + url: (params) => `${LINQ_API_BASE}/chats/${encodeURIComponent(params.chatId.trim())}`, + method: 'PUT', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.displayName !== undefined) body.display_name = params.displayName + if (params.groupChatIcon !== undefined) body.group_chat_icon = params.groupChatIcon + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to update chat'), + output: { chatId: null, status: null }, + } + } + + return { + success: true, + output: { + chatId: data.chat_id ?? null, + status: data.status ?? null, + }, + } + }, + + outputs: { + chatId: { type: 'string', description: 'ID of the updated chat', optional: true }, + status: { type: 'string', description: 'Status of the queued update', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/update_contact_card.ts b/apps/sim/tools/linq/update_contact_card.ts new file mode 100644 index 00000000000..6bc35ab5de1 --- /dev/null +++ b/apps/sim/tools/linq/update_contact_card.ts @@ -0,0 +1,91 @@ +import type { LinqContactCardResult, LinqUpdateContactCardParams } from '@/tools/linq/types' +import { extractLinqError, LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqUpdateContactCardTool: ToolConfig< + LinqUpdateContactCardParams, + LinqContactCardResult +> = { + id: 'linq_update_contact_card', + name: 'Update Contact Card', + description: 'Partially update an existing active contact card for a phone number', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + phoneNumber: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number in E.164 format identifying the card to update', + }, + firstName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New first name', + }, + lastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New last name', + }, + imageUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New profile photo URL', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/contact_card?phone_number=${encodeURIComponent(params.phoneNumber.trim())}`, + method: 'PATCH', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.firstName !== undefined) body.first_name = params.firstName + if (params.lastName !== undefined) body.last_name = params.lastName + if (params.imageUrl !== undefined) body.image_url = params.imageUrl + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to update contact card'), + output: { phoneNumber: '', firstName: '', lastName: null, imageUrl: null, isActive: false }, + } + } + + return { + success: true, + output: { + phoneNumber: data.phone_number ?? '', + firstName: data.first_name ?? '', + lastName: data.last_name ?? null, + imageUrl: data.image_url ?? null, + isActive: data.is_active ?? false, + }, + } + }, + + outputs: { + phoneNumber: { type: 'string', description: 'Phone number the card applies to' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name', optional: true }, + imageUrl: { type: 'string', description: 'Profile photo URL', optional: true }, + isActive: { type: 'boolean', description: 'Whether the card is active' }, + }, +} diff --git a/apps/sim/tools/linq/update_webhook_subscription.ts b/apps/sim/tools/linq/update_webhook_subscription.ts new file mode 100644 index 00000000000..099180ca672 --- /dev/null +++ b/apps/sim/tools/linq/update_webhook_subscription.ts @@ -0,0 +1,114 @@ +import type { + LinqUpdateWebhookSubscriptionParams, + LinqWebhookSubscriptionResult, +} from '@/tools/linq/types' +import { + extractLinqError, + LINQ_API_BASE, + linqHeaders, + mapWebhookSubscription, +} from '@/tools/linq/utils' +import type { ToolConfig } from '@/tools/types' + +export const linqUpdateWebhookSubscriptionTool: ToolConfig< + LinqUpdateWebhookSubscriptionParams, + LinqWebhookSubscriptionResult +> = { + id: 'linq_update_webhook_subscription', + name: 'Update Webhook Subscription', + description: 'Update a webhook subscription (target URL, events, phone filter, or active state)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Linq API key', + }, + subscriptionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The unique identifier of the webhook subscription', + }, + targetUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New HTTPS endpoint that will receive events', + }, + subscribedEvents: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New set of event types to subscribe to', + }, + phoneNumbers: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New set of E.164 phone numbers to filter by', + }, + isActive: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the subscription should be active', + }, + }, + + request: { + url: (params) => + `${LINQ_API_BASE}/webhook-subscriptions/${encodeURIComponent(params.subscriptionId.trim())}`, + method: 'PUT', + headers: (params) => linqHeaders(params.apiKey), + body: (params) => { + const body: Record = {} + if (params.targetUrl !== undefined) body.target_url = params.targetUrl + if (params.subscribedEvents !== undefined) body.subscribed_events = params.subscribedEvents + if (params.phoneNumbers !== undefined) body.phone_numbers = params.phoneNumbers + if (params.isActive !== undefined) body.is_active = params.isActive + return body + }, + }, + + transformResponse: async (response): Promise => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: extractLinqError(data, 'Failed to update webhook subscription'), + output: { + id: '', + targetUrl: '', + subscribedEvents: [], + phoneNumbers: null, + isActive: false, + createdAt: null, + updatedAt: null, + }, + } + } + + return { + success: true, + output: mapWebhookSubscription(data), + } + }, + + outputs: { + id: { type: 'string', description: 'Subscription ID' }, + targetUrl: { type: 'string', description: 'Endpoint that receives events' }, + subscribedEvents: { type: 'json', description: 'Subscribed event types' }, + phoneNumbers: { + type: 'json', + description: 'Filtered phone numbers (null = all)', + optional: true, + }, + isActive: { type: 'boolean', description: 'Whether the subscription is active' }, + createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'ISO 8601 update timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/linq/utils.ts b/apps/sim/tools/linq/utils.ts new file mode 100644 index 00000000000..f1ec9582b93 --- /dev/null +++ b/apps/sim/tools/linq/utils.ts @@ -0,0 +1,102 @@ +import type { LinqServiceType, LinqWebhookSubscription } from '@/tools/linq/types' + +/** Base URL for the Linq partner API. Operation paths are appended under `/v3`. */ +export const LINQ_API_BASE = 'https://api.linqapp.com/api/partner/v3' + +/** Authorization headers shared by every Linq request. */ +export function linqHeaders(apiKey: string): Record { + return { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + } +} + +/** Extract a human-readable error message from a Linq error response body. */ +export function extractLinqError(data: unknown, fallback: string): string { + if (data && typeof data === 'object') { + const record = data as Record + const error = record.error + if (error && typeof error === 'object') { + const message = (error as Record).message + if (typeof message === 'string' && message.length > 0) return message + } + if (typeof record.message === 'string' && record.message.length > 0) return record.message + } + return fallback +} + +interface MessageContentInput { + text?: string + mediaUrl?: string + attachmentId?: string + linkUrl?: string + preferredService?: LinqServiceType + effectName?: string + effectType?: string + replyToMessageId?: string + replyToPartIndex?: number + idempotencyKey?: string +} + +/** + * Build the `message` content object sent to chat create/send endpoints. + * Assembles the `parts` array from text, media, and link inputs, then layers + * on optional effect, reply, service preference, and idempotency fields. + */ +export function buildMessageContent(input: MessageContentInput): Record { + const parts: Array> = [] + + if (input.linkUrl) { + // Linq requires a link to be the only part in a message — it cannot be + // combined with text or media parts — so a link is sent on its own. + parts.push({ type: 'link', value: input.linkUrl }) + } else { + if (input.text && input.text.length > 0) { + parts.push({ type: 'text', value: input.text }) + } + if (input.attachmentId) { + parts.push({ type: 'media', attachment_id: input.attachmentId }) + } else if (input.mediaUrl) { + parts.push({ type: 'media', url: input.mediaUrl }) + } + } + + if (parts.length === 0) { + throw new Error('A message requires text, a media URL, an attachment ID, or a link URL') + } + + const message: Record = { parts } + + if (input.preferredService) { + message.preferred_service = input.preferredService + } + if (input.effectName || input.effectType) { + const effect: Record = {} + if (input.effectName) effect.name = input.effectName + if (input.effectType) effect.type = input.effectType + message.effect = effect + } + if (input.replyToMessageId) { + const replyTo: Record = { message_id: input.replyToMessageId } + if (typeof input.replyToPartIndex === 'number') replyTo.part_index = input.replyToPartIndex + message.reply_to = replyTo + } + if (input.idempotencyKey) { + message.idempotency_key = input.idempotencyKey + } + + return message +} + +/** Map a raw webhook subscription API object to the camelCase output shape. */ +export function mapWebhookSubscription(data: Record): LinqWebhookSubscription { + return { + id: (data.id as string) ?? '', + targetUrl: (data.target_url as string) ?? '', + subscribedEvents: (data.subscribed_events as string[]) ?? [], + phoneNumbers: (data.phone_numbers as string[] | null) ?? null, + isActive: (data.is_active as boolean) ?? false, + createdAt: (data.created_at as string | null) ?? null, + updatedAt: (data.updated_at as string | null) ?? null, + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bf90f1e477..596d394ddb6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1689,6 +1689,42 @@ import { } from '@/tools/linear' import { linkedInGetProfileTool, linkedInSharePostTool } from '@/tools/linkedin' import { linkupSearchTool } from '@/tools/linkup' +import { + linqAddParticipantTool, + linqCheckImessageTool, + linqCheckRcsTool, + linqCreateAttachmentTool, + linqCreateChatTool, + linqCreateContactCardTool, + linqCreateWebhookSubscriptionTool, + linqDeleteAttachmentTool, + linqDeleteMessageTool, + linqDeleteWebhookSubscriptionTool, + linqEditMessageTool, + linqGetAttachmentTool, + linqGetChatTool, + linqGetContactCardTool, + linqGetMessageTool, + linqGetWebhookSubscriptionTool, + linqLeaveChatTool, + linqListChatsTool, + linqListMessagesTool, + linqListPhoneNumbersTool, + linqListThreadTool, + linqListWebhookEventsTool, + linqListWebhookSubscriptionsTool, + linqMarkChatReadTool, + linqReactToMessageTool, + linqRemoveParticipantTool, + linqSendMessageTool, + linqSendVoiceMemoTool, + linqShareContactCardTool, + linqStartTypingTool, + linqStopTypingTool, + linqUpdateChatTool, + linqUpdateContactCardTool, + linqUpdateWebhookSubscriptionTool, +} from '@/tools/linq' import { llmChatTool } from '@/tools/llm' import { logsGetExecutionTool, logsGetTool, logsQueryTool } from '@/tools/logs' import { @@ -3508,6 +3544,40 @@ export const tools: Record = { ketch_set_consent: ketchSetConsentTool, ketch_set_subscriptions: ketchSetSubscriptionsTool, linkup_search: linkupSearchTool, + linq_add_participant: linqAddParticipantTool, + linq_check_imessage: linqCheckImessageTool, + linq_check_rcs: linqCheckRcsTool, + linq_create_attachment: linqCreateAttachmentTool, + linq_create_chat: linqCreateChatTool, + linq_create_contact_card: linqCreateContactCardTool, + linq_create_webhook_subscription: linqCreateWebhookSubscriptionTool, + linq_delete_attachment: linqDeleteAttachmentTool, + linq_delete_message: linqDeleteMessageTool, + linq_delete_webhook_subscription: linqDeleteWebhookSubscriptionTool, + linq_edit_message: linqEditMessageTool, + linq_get_attachment: linqGetAttachmentTool, + linq_get_chat: linqGetChatTool, + linq_get_contact_card: linqGetContactCardTool, + linq_get_message: linqGetMessageTool, + linq_get_webhook_subscription: linqGetWebhookSubscriptionTool, + linq_leave_chat: linqLeaveChatTool, + linq_list_chats: linqListChatsTool, + linq_list_messages: linqListMessagesTool, + linq_list_phone_numbers: linqListPhoneNumbersTool, + linq_list_thread: linqListThreadTool, + linq_list_webhook_events: linqListWebhookEventsTool, + linq_list_webhook_subscriptions: linqListWebhookSubscriptionsTool, + linq_mark_chat_read: linqMarkChatReadTool, + linq_react_to_message: linqReactToMessageTool, + linq_remove_participant: linqRemoveParticipantTool, + linq_send_message: linqSendMessageTool, + linq_send_voice_memo: linqSendVoiceMemoTool, + linq_share_contact_card: linqShareContactCardTool, + linq_start_typing: linqStartTypingTool, + linq_stop_typing: linqStopTypingTool, + linq_update_chat: linqUpdateChatTool, + linq_update_contact_card: linqUpdateContactCardTool, + linq_update_webhook_subscription: linqUpdateWebhookSubscriptionTool, logs_query: logsQueryTool, logs_get: logsGetTool, logs_get_execution: logsGetExecutionTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 6f63a524433..83cf378eb3a 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 761, - zodRoutes: 761, + totalRoutes: 762, + zodRoutes: 762, nonZodRoutes: 0, } as const