chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+66
View File
@@ -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<LinqParticipantParams, LinqQueuedResult> = {
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<LinqQueuedResult> => {
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 },
},
}
+70
View File
@@ -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<string, unknown> = { address: params.address }
if (params.from) body.from = params.from
return body
},
},
transformResponse: async (response): Promise<LinqCapabilityCheckResult> => {
const data = await response.json().catch(() => null)
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' },
},
}
+67
View File
@@ -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<LinqCapabilityCheckParams, LinqCapabilityCheckResult> = {
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) 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<string, unknown> = { address: params.address }
if (params.from) body.from = params.from
return body
},
},
transformResponse: async (response): Promise<LinqCapabilityCheckResult> => {
const data = await response.json().catch(() => null)
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' },
},
}
+107
View File
@@ -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<LinqCreateAttachmentResult> => {
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' },
},
}
+153
View File
@@ -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<LinqCreateChatParams, LinqCreateChatResult> = {
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<LinqCreateChatResult> => {
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' },
},
}
@@ -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<string, unknown> = {
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<LinqContactCardResult> => {
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' },
},
}
@@ -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<string, unknown> = {
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<LinqCreateWebhookSubscriptionResult> => {
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',
},
},
}
+48
View File
@@ -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<LinqDeleteAttachmentParams, LinqSuccessResult> = {
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<LinqSuccessResult> => {
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' },
},
}
+48
View File
@@ -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<LinqDeleteMessageParams, LinqSuccessResult> = {
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<LinqSuccessResult> => {
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' },
},
}
@@ -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<LinqSuccessResult> => {
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' },
},
}
+130
View File
@@ -0,0 +1,130 @@
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<LinqEditMessageParams, LinqMessageResult> = {
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<string, unknown> = { text: params.text }
if (typeof params.partIndex === 'number') body.part_index = params.partIndex
return body
},
},
transformResponse: async (response): Promise<LinqMessageResult> => {
// Linq returns 200 with the updated Message, or 204 No Content when the edit
// is accepted for an already-deleted message — guard the empty-body case.
const data = await response.json().catch(() => null)
if (!response.ok) {
return {
success: false,
error: extractLinqError(data, 'Failed to edit message'),
output: {
id: '',
chatId: '',
isFromMe: null,
deliveryStatus: null,
isDelivered: null,
isRead: null,
service: null,
createdAt: null,
updatedAt: null,
sentAt: null,
parts: [],
message: {},
},
}
}
const message = data ?? {}
return {
success: true,
output: {
id: message.id ?? '',
chatId: message.chat_id ?? '',
isFromMe: message.is_from_me ?? null,
deliveryStatus: message.delivery_status ?? null,
isDelivered: message.is_delivered ?? null,
isRead: message.is_read ?? null,
service: message.service ?? null,
createdAt: message.created_at ?? null,
updatedAt: message.updated_at ?? null,
sentAt: message.sent_at ?? null,
parts: message.parts ?? [],
message,
},
}
},
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,
},
deliveryStatus: {
type: 'string',
description: 'Delivery status (pending, queued, sent, delivered, received, read, failed)',
optional: true,
},
isDelivered: {
type: 'boolean',
description: 'Whether the message was delivered (deprecated; use deliveryStatus)',
optional: true,
},
isRead: {
type: 'boolean',
description: 'Whether the message was read (deprecated; use deliveryStatus)',
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' },
},
}
+75
View File
@@ -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<LinqGetAttachmentParams, LinqAttachmentResult> = {
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<LinqAttachmentResult> => {
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 },
},
}
+84
View File
@@ -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<LinqGetChatParams, LinqChatResult> = {
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<LinqChatResult> => {
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 },
},
}
+81
View File
@@ -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<LinqGetContactCardResult> => {
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<string, unknown>) => ({
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' },
},
},
},
},
}
+109
View File
@@ -0,0 +1,109 @@
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<LinqGetMessageParams, LinqMessageResult> = {
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<LinqMessageResult> => {
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: extractLinqError(data, 'Failed to get message'),
output: {
id: '',
chatId: '',
isFromMe: null,
deliveryStatus: 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,
deliveryStatus: data.delivery_status ?? 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,
},
deliveryStatus: {
type: 'string',
description: 'Delivery status (pending, queued, sent, delivered, received, read, failed)',
optional: true,
},
isDelivered: {
type: 'boolean',
description: 'Whether the message was delivered (deprecated; use deliveryStatus)',
optional: true,
},
isRead: {
type: 'boolean',
description: 'Whether the message was read (deprecated; use deliveryStatus)',
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' },
},
}
@@ -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<LinqWebhookSubscriptionResult> => {
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 },
},
}
+35
View File
@@ -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'
+59
View File
@@ -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<LinqChatActionParams, LinqQueuedResult> = {
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<LinqQueuedResult> => {
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 },
},
}
+86
View File
@@ -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<LinqListChatsParams, LinqListChatsResult> = {
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<LinqListChatsResult> => {
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,
},
},
}
+80
View File
@@ -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<LinqListMessagesParams, LinqListMessagesResult> = {
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<LinqListMessagesResult> => {
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,
},
},
}
+81
View File
@@ -0,0 +1,81 @@
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<LinqListPhoneNumbersResult> => {
const data = await response.json().catch(() => null)
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<string, unknown>) => ({
id: (num.id as string) ?? '',
phoneNumber: (num.phone_number as string) ?? '',
forwardingNumber: (num.forwarding_number as string | null) ?? null,
healthStatus:
((num.reputation ?? 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' },
forwardingNumber: {
type: 'string',
description: 'Forwarding number in E.164 format, or null',
nullable: true,
},
healthStatus: {
type: 'json',
description: 'Line reputation/health status (status, doc_url)',
nullable: true,
},
},
},
},
},
}
+87
View File
@@ -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<LinqListThreadParams, LinqListMessagesResult> = {
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<LinqListMessagesResult> => {
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,
},
},
}
@@ -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<LinqListWebhookEventsResult> => {
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 },
},
}
@@ -0,0 +1,86 @@
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<LinqListWebhookSubscriptionsResult> => {
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)',
nullable: true,
},
isActive: { type: 'boolean', description: 'Whether the subscription is active' },
createdAt: {
type: 'string',
description: 'ISO 8601 creation timestamp',
nullable: true,
},
updatedAt: {
type: 'string',
description: 'ISO 8601 update timestamp',
nullable: true,
},
},
},
},
},
}
+48
View File
@@ -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 linqMarkChatReadTool: ToolConfig<LinqChatActionParams, LinqSuccessResult> = {
id: 'linq_mark_chat_read',
name: 'Mark Chat as Read',
description:
'Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on 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())}/read`,
method: 'POST',
headers: (params) => linqHeaders(params.apiKey),
},
transformResponse: async (response): Promise<LinqSuccessResult> => {
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' },
},
}
+93
View File
@@ -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<LinqReactToMessageParams, LinqQueuedResult> = {
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<string, unknown> = {
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<LinqQueuedResult> => {
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 },
},
}
+66
View File
@@ -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<LinqParticipantParams, LinqQueuedResult> = {
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<LinqQueuedResult> => {
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 },
},
}
+153
View File
@@ -0,0 +1,153 @@
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<LinqSendMessageParams, LinqSendMessageResult> = {
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<LinqSendMessageResult> => {
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 ?? message.from_handle?.service ?? message.preferred_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, received, read, 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' },
},
}
+95
View File
@@ -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<LinqSendVoiceMemoParams, LinqSendVoiceMemoResult> = {
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<string, unknown> = {}
if (params.attachmentId) body.attachment_id = params.attachmentId
else if (params.voiceMemoUrl) body.voice_memo_url = params.voiceMemoUrl
return body
},
},
transformResponse: async (response): Promise<LinqSendVoiceMemoResult> => {
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,
},
},
}
+48
View File
@@ -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<LinqChatActionParams, LinqSuccessResult> = {
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<LinqSuccessResult> => {
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' },
},
}
+47
View File
@@ -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<LinqChatActionParams, LinqSuccessResult> = {
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<LinqSuccessResult> => {
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' },
},
}
+47
View File
@@ -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<LinqChatActionParams, LinqSuccessResult> = {
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<LinqSuccessResult> => {
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' },
},
}
+397
View File
@@ -0,0 +1,397 @@
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<string, unknown> | null
}
}
export interface LinqListChatsParams extends LinqBaseParams {
cursor?: string
from?: string
to?: string
limit?: number
}
export interface LinqListChatsResult extends ToolResponse {
output: {
chats: Array<Record<string, unknown>>
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<string, unknown> | 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<string, unknown> | 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<Record<string, unknown>>
nextCursor: string | null
}
}
export interface LinqGetMessageParams extends LinqBaseParams {
messageId: string
}
export interface LinqMessageResult extends ToolResponse {
output: {
id: string
chatId: string
isFromMe: boolean | null
deliveryStatus: string | null
isDelivered: boolean | null
isRead: boolean | null
service: string | null
createdAt: string | null
updatedAt: string | null
sentAt: string | null
parts: Array<Record<string, unknown>>
message: Record<string, unknown>
}
}
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
forwardingNumber: string | null
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
}
}
+74
View File
@@ -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<LinqUpdateChatParams, LinqUpdateChatResult> = {
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<string, unknown> = {}
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<LinqUpdateChatResult> => {
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 },
},
}
@@ -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<string, unknown> = {}
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<LinqContactCardResult> => {
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' },
},
}
@@ -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<string, unknown> = {}
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<LinqWebhookSubscriptionResult> => {
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 },
},
}
+102
View File
@@ -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<string, string> {
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<string, unknown>
const error = record.error
if (error && typeof error === 'object') {
const message = (error as Record<string, unknown>).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<string, unknown> {
const parts: Array<Record<string, unknown>> = []
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<string, unknown> = { parts }
if (input.preferredService) {
message.preferred_service = input.preferredService
}
if (input.effectName || input.effectType) {
const effect: Record<string, unknown> = {}
if (input.effectName) effect.name = input.effectName
if (input.effectType) effect.type = input.effectType
message.effect = effect
}
if (input.replyToMessageId) {
const replyTo: Record<string, unknown> = { 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<string, unknown>): 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,
}
}