chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,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 },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user