chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import type { DubBulkCreateLinksParams, DubBulkCreateLinksResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const bulkCreateLinksTool: ToolConfig<DubBulkCreateLinksParams, DubBulkCreateLinksResponse> =
|
||||
{
|
||||
id: 'dub_bulk_create_links',
|
||||
name: 'Dub Bulk Create Links',
|
||||
description:
|
||||
'Create up to 100 short links in a single request. Returns the created links alongside any per-link errors.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
links: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of link objects to create. Each object requires a "url" and may include domain, key, tagIds, and other link fields (max 100).',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dub.co/links/bulk',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const links = typeof params.links === 'string' ? JSON.parse(params.links) : params.links
|
||||
return Array.isArray(links) ? links : [links]
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to bulk create links')
|
||||
}
|
||||
|
||||
const results = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
const created = results.filter((item) => !item.error)
|
||||
const errors = results.filter((item) => item.error)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
created,
|
||||
errors,
|
||||
count: created.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
created: {
|
||||
type: 'json',
|
||||
description: 'Array of successfully created link objects',
|
||||
},
|
||||
errors: {
|
||||
type: 'json',
|
||||
description: 'Array of per-link errors ({ link, error, code }) for links that failed',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of links successfully created' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { DubBulkDeleteLinksParams, DubBulkDeleteLinksResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const bulkDeleteLinksTool: ToolConfig<DubBulkDeleteLinksParams, DubBulkDeleteLinksResponse> =
|
||||
{
|
||||
id: 'dub_bulk_delete_links',
|
||||
name: 'Dub Bulk Delete Links',
|
||||
description:
|
||||
'Delete up to 100 short links in a single request by their link IDs. Non-existing IDs are ignored.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
linkIds: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated link IDs to delete (max 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/links/bulk')
|
||||
const ids = params.linkIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
url.searchParams.set('linkIds', ids.join(','))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to bulk delete links')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deletedCount: data.deletedCount ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deletedCount: { type: 'number', description: 'Number of links that were deleted' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { DubBulkUpdateLinksParams, DubBulkUpdateLinksResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const bulkUpdateLinksTool: ToolConfig<DubBulkUpdateLinksParams, DubBulkUpdateLinksResponse> =
|
||||
{
|
||||
id: 'dub_bulk_update_links',
|
||||
name: 'Dub Bulk Update Links',
|
||||
description:
|
||||
'Apply the same set of field updates to up to 100 links at once, selected by link IDs or external IDs.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
linkIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated link IDs to update (max 100, takes precedence over externalIds)',
|
||||
},
|
||||
externalIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated external IDs to update (max 100)',
|
||||
},
|
||||
data: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON object of fields to apply to every selected link (e.g. { "archived": true, "tagIds": ["..."] })',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dub.co/links/bulk',
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const data = typeof params.data === 'string' ? JSON.parse(params.data) : params.data
|
||||
const linkIds = params.linkIds
|
||||
? params.linkIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
const externalIds = params.externalIds
|
||||
? params.externalIds
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
if (linkIds.length === 0 && externalIds.length === 0) {
|
||||
throw new Error(
|
||||
'Bulk Update Links requires at least one Link ID or External ID to select which links to update.'
|
||||
)
|
||||
}
|
||||
const body: Record<string, unknown> = { data: data ?? {} }
|
||||
if (linkIds.length > 0) {
|
||||
body.linkIds = linkIds
|
||||
} else {
|
||||
body.externalIds = externalIds
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to bulk update links')
|
||||
}
|
||||
|
||||
const updated = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
updated,
|
||||
count: updated.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
updated: {
|
||||
type: 'json',
|
||||
description: 'Array of updated link objects',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of links updated' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { DubCreateLinkParams, DubCreateLinkResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createLinkTool: ToolConfig<DubCreateLinkParams, DubCreateLinkResponse> = {
|
||||
id: 'dub_create_link',
|
||||
name: 'Dub Create Link',
|
||||
description:
|
||||
'Create a new short link with Dub. Supports custom domains, slugs, UTM parameters, and more.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The destination URL of the short link',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom domain for the short link (defaults to dub.sh)',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom slug for the short link (randomly generated if not provided)',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'External ID for the link in your database',
|
||||
},
|
||||
tenantId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tenant ID for grouping links created on behalf of a customer/tenant',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Folder ID to organize the link into',
|
||||
},
|
||||
trackConversion: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to track conversions (leads/sales) for the short link',
|
||||
},
|
||||
tagIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag IDs to assign to the link',
|
||||
},
|
||||
comments: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comments for the short link',
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Expiration date in ISO 8601 format',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Password to protect the short link',
|
||||
},
|
||||
rewrite: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable link cloaking',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to archive the link',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG title for the link preview',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG description for the link preview',
|
||||
},
|
||||
utm_source: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM source parameter',
|
||||
},
|
||||
utm_medium: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM medium parameter',
|
||||
},
|
||||
utm_campaign: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM campaign parameter',
|
||||
},
|
||||
utm_term: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM term parameter',
|
||||
},
|
||||
utm_content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM content parameter',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dub.co/links',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = { url: params.url }
|
||||
if (params.domain) body.domain = params.domain
|
||||
if (params.key) body.key = params.key
|
||||
if (params.externalId) body.externalId = params.externalId
|
||||
if (params.tenantId) body.tenantId = params.tenantId
|
||||
if (params.folderId) body.folderId = params.folderId
|
||||
if (params.trackConversion !== undefined) body.trackConversion = params.trackConversion
|
||||
if (params.tagIds) body.tagIds = params.tagIds.split(',').map((id) => id.trim())
|
||||
if (params.comments) body.comments = params.comments
|
||||
if (params.expiresAt) body.expiresAt = params.expiresAt
|
||||
if (params.password) body.password = params.password
|
||||
if (params.rewrite !== undefined) body.rewrite = params.rewrite
|
||||
if (params.archived !== undefined) body.archived = params.archived
|
||||
if (params.title) body.title = params.title
|
||||
if (params.description) body.description = params.description
|
||||
if (params.utm_source) body.utm_source = params.utm_source
|
||||
if (params.utm_medium) body.utm_medium = params.utm_medium
|
||||
if (params.utm_campaign) body.utm_campaign = params.utm_campaign
|
||||
if (params.utm_term) body.utm_term = params.utm_term
|
||||
if (params.utm_content) body.utm_content = params.utm_content
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to create link')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
domain: data.domain ?? '',
|
||||
key: data.key ?? '',
|
||||
url: data.url ?? '',
|
||||
shortLink: data.shortLink ?? '',
|
||||
qrCode: data.qrCode ?? '',
|
||||
archived: data.archived ?? false,
|
||||
externalId: data.externalId ?? null,
|
||||
title: data.title ?? null,
|
||||
description: data.description ?? null,
|
||||
tags: data.tags ?? [],
|
||||
folderId: data.folderId ?? null,
|
||||
tenantId: data.tenantId ?? null,
|
||||
trackConversion: data.trackConversion ?? false,
|
||||
clicks: data.clicks ?? 0,
|
||||
leads: data.leads ?? 0,
|
||||
conversions: data.conversions ?? 0,
|
||||
sales: data.sales ?? 0,
|
||||
saleAmount: data.saleAmount ?? 0,
|
||||
lastClicked: data.lastClicked ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
utm_source: data.utm_source ?? null,
|
||||
utm_medium: data.utm_medium ?? null,
|
||||
utm_campaign: data.utm_campaign ?? null,
|
||||
utm_term: data.utm_term ?? null,
|
||||
utm_content: data.utm_content ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique ID of the created link' },
|
||||
domain: { type: 'string', description: 'Domain of the short link' },
|
||||
key: { type: 'string', description: 'Slug of the short link' },
|
||||
url: { type: 'string', description: 'Destination URL' },
|
||||
shortLink: { type: 'string', description: 'Full short link URL' },
|
||||
qrCode: { type: 'string', description: 'QR code URL for the short link' },
|
||||
archived: { type: 'boolean', description: 'Whether the link is archived' },
|
||||
externalId: { type: 'string', description: 'External ID', optional: true },
|
||||
title: { type: 'string', description: 'OG title', optional: true },
|
||||
description: { type: 'string', description: 'OG description', optional: true },
|
||||
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
|
||||
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
|
||||
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
|
||||
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
|
||||
clicks: { type: 'number', description: 'Number of clicks' },
|
||||
leads: { type: 'number', description: 'Number of leads' },
|
||||
conversions: { type: 'number', description: 'Number of conversions' },
|
||||
sales: { type: 'number', description: 'Number of sales' },
|
||||
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
|
||||
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
utm_source: { type: 'string', description: 'UTM source parameter', optional: true },
|
||||
utm_medium: { type: 'string', description: 'UTM medium parameter', optional: true },
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign parameter', optional: true },
|
||||
utm_term: { type: 'string', description: 'UTM term parameter', optional: true },
|
||||
utm_content: { type: 'string', description: 'UTM content parameter', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { DubCreateTagParams, DubCreateTagResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createTagTool: ToolConfig<DubCreateTagParams, DubCreateTagResponse> = {
|
||||
id: 'dub_create_tag',
|
||||
name: 'Dub Create Tag',
|
||||
description: 'Create a new tag in the workspace for organizing and filtering short links.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the tag to create (1-50 characters)',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Tag color: red, yellow, green, blue, purple, brown, gray, or pink (random if omitted)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dub.co/tags',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = { name: params.name }
|
||||
if (params.color) body.color = params.color
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to create tag')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
name: data.name ?? '',
|
||||
color: data.color ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique ID of the created tag' },
|
||||
name: { type: 'string', description: 'Name of the tag' },
|
||||
color: { type: 'string', description: 'Color assigned to the tag' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { DubDeleteLinkParams, DubDeleteLinkResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteLinkTool: ToolConfig<DubDeleteLinkParams, DubDeleteLinkResponse> = {
|
||||
id: 'dub_delete_link',
|
||||
name: 'Dub Delete Link',
|
||||
description: 'Delete a short link by its link ID or external ID (prefixed with ext_).',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
linkId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The link ID or external ID prefixed with ext_',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.dub.co/links/${params.linkId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to delete link')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'ID of the deleted link' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { DubGetAnalyticsParams, DubGetAnalyticsResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getAnalyticsTool: ToolConfig<DubGetAnalyticsParams, DubGetAnalyticsResponse> = {
|
||||
id: 'dub_get_analytics',
|
||||
name: 'Dub Get Analytics',
|
||||
description:
|
||||
'Retrieve analytics for links including clicks, leads, and sales. Supports filtering by link, time range, and grouping by various dimensions.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
event: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event type: clicks (default), leads, sales, or composite',
|
||||
},
|
||||
groupBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Group results by: count (default), timeseries, countries, cities, devices, browsers, os, referers, top_links, top_urls',
|
||||
},
|
||||
linkId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by link ID',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by external ID (prefix with ext_)',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by domain',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Time interval: 24h (default), 7d, 30d, 90d, 1y, mtd, qtd, ytd, or all',
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start date/time in ISO 8601 format (overrides interval)',
|
||||
},
|
||||
end: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End date/time in ISO 8601 format (defaults to now)',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by country (ISO 3166-1 alpha-2 code)',
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'IANA timezone for timeseries data (defaults to UTC)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/analytics')
|
||||
if (params.event) url.searchParams.set('event', params.event)
|
||||
if (params.groupBy) url.searchParams.set('groupBy', params.groupBy)
|
||||
if (params.linkId) url.searchParams.set('linkId', params.linkId)
|
||||
if (params.externalId) url.searchParams.set('externalId', params.externalId)
|
||||
if (params.domain) url.searchParams.set('domain', params.domain)
|
||||
if (params.interval) url.searchParams.set('interval', params.interval)
|
||||
if (params.start) url.searchParams.set('start', params.start)
|
||||
if (params.end) url.searchParams.set('end', params.end)
|
||||
if (params.country) url.searchParams.set('country', params.country)
|
||||
if (params.timezone) url.searchParams.set('timezone', params.timezone)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get analytics')
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
clicks: 0,
|
||||
leads: 0,
|
||||
sales: 0,
|
||||
saleAmount: 0,
|
||||
data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
clicks: data.clicks ?? 0,
|
||||
leads: data.leads ?? 0,
|
||||
sales: data.sales ?? 0,
|
||||
saleAmount: data.saleAmount ?? 0,
|
||||
data: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
clicks: { type: 'number', description: 'Total number of clicks' },
|
||||
leads: { type: 'number', description: 'Total number of leads' },
|
||||
sales: { type: 'number', description: 'Total number of sales' },
|
||||
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
|
||||
data: {
|
||||
type: 'json',
|
||||
description: 'Grouped analytics data (timeseries, countries, devices, etc.)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { DubGetEventsParams, DubGetEventsResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEventsTool: ToolConfig<DubGetEventsParams, DubGetEventsResponse> = {
|
||||
id: 'dub_get_events',
|
||||
name: 'Dub List Events',
|
||||
description:
|
||||
'Retrieve a paginated stream of individual click, lead, and sale events for links, with filtering by link, time range, and location.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
event: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Event type: clicks (default), leads, or sales',
|
||||
},
|
||||
linkId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by link ID',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by external ID (prefix with ext_)',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by domain',
|
||||
},
|
||||
interval: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Time interval: 24h (default), 7d, 30d, 90d, 1y, mtd, qtd, ytd, or all',
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Start date/time in ISO 8601 format (overrides interval)',
|
||||
},
|
||||
end: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'End date/time in ISO 8601 format (defaults to now)',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by country (ISO 3166-1 alpha-2 code)',
|
||||
},
|
||||
timezone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'IANA timezone for event timestamps (defaults to UTC)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of events per page (default: 100, max: 1000)',
|
||||
},
|
||||
sortOrder: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort order: desc (default) or asc',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/events')
|
||||
if (params.event) url.searchParams.set('event', params.event)
|
||||
if (params.linkId) url.searchParams.set('linkId', params.linkId)
|
||||
if (params.externalId) url.searchParams.set('externalId', params.externalId)
|
||||
if (params.domain) url.searchParams.set('domain', params.domain)
|
||||
if (params.interval) url.searchParams.set('interval', params.interval)
|
||||
if (params.start) url.searchParams.set('start', params.start)
|
||||
if (params.end) url.searchParams.set('end', params.end)
|
||||
if (params.country) url.searchParams.set('country', params.country)
|
||||
if (params.timezone) url.searchParams.set('timezone', params.timezone)
|
||||
if (params.page) url.searchParams.set('page', String(params.page))
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to list events')
|
||||
}
|
||||
|
||||
const events = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
events,
|
||||
count: events.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
events: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of event objects (event, timestamp, click, link, and customer/sale data when applicable)',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of events returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { DubGetLinkParams, DubGetLinkResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getLinkTool: ToolConfig<DubGetLinkParams, DubGetLinkResponse> = {
|
||||
id: 'dub_get_link',
|
||||
name: 'Dub Get Link',
|
||||
description:
|
||||
'Retrieve information about a short link by its link ID, external ID, or domain + key combination.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
linkId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique ID of the short link',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The external ID of the link in your database',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain of the link (use with key)',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The slug of the link (use with domain)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/links/info')
|
||||
if (params.linkId) url.searchParams.set('linkId', params.linkId)
|
||||
if (params.externalId) url.searchParams.set('externalId', params.externalId)
|
||||
if (params.domain) url.searchParams.set('domain', params.domain)
|
||||
if (params.key) url.searchParams.set('key', params.key)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to get link')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
domain: data.domain ?? '',
|
||||
key: data.key ?? '',
|
||||
url: data.url ?? '',
|
||||
shortLink: data.shortLink ?? '',
|
||||
qrCode: data.qrCode ?? '',
|
||||
archived: data.archived ?? false,
|
||||
externalId: data.externalId ?? null,
|
||||
title: data.title ?? null,
|
||||
description: data.description ?? null,
|
||||
tags: data.tags ?? [],
|
||||
folderId: data.folderId ?? null,
|
||||
tenantId: data.tenantId ?? null,
|
||||
trackConversion: data.trackConversion ?? false,
|
||||
clicks: data.clicks ?? 0,
|
||||
leads: data.leads ?? 0,
|
||||
conversions: data.conversions ?? 0,
|
||||
sales: data.sales ?? 0,
|
||||
saleAmount: data.saleAmount ?? 0,
|
||||
lastClicked: data.lastClicked ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
utm_source: data.utm_source ?? null,
|
||||
utm_medium: data.utm_medium ?? null,
|
||||
utm_campaign: data.utm_campaign ?? null,
|
||||
utm_term: data.utm_term ?? null,
|
||||
utm_content: data.utm_content ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique ID of the link' },
|
||||
domain: { type: 'string', description: 'Domain of the short link' },
|
||||
key: { type: 'string', description: 'Slug of the short link' },
|
||||
url: { type: 'string', description: 'Destination URL' },
|
||||
shortLink: { type: 'string', description: 'Full short link URL' },
|
||||
qrCode: { type: 'string', description: 'QR code URL for the short link' },
|
||||
archived: { type: 'boolean', description: 'Whether the link is archived' },
|
||||
externalId: { type: 'string', description: 'External ID', optional: true },
|
||||
title: { type: 'string', description: 'OG title', optional: true },
|
||||
description: { type: 'string', description: 'OG description', optional: true },
|
||||
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
|
||||
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
|
||||
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
|
||||
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
|
||||
clicks: { type: 'number', description: 'Number of clicks' },
|
||||
leads: { type: 'number', description: 'Number of leads' },
|
||||
conversions: { type: 'number', description: 'Number of conversions' },
|
||||
sales: { type: 'number', description: 'Number of sales' },
|
||||
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
|
||||
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
utm_source: { type: 'string', description: 'UTM source parameter', optional: true },
|
||||
utm_medium: { type: 'string', description: 'UTM medium parameter', optional: true },
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign parameter', optional: true },
|
||||
utm_term: { type: 'string', description: 'UTM term parameter', optional: true },
|
||||
utm_content: { type: 'string', description: 'UTM content parameter', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { DubGetLinksCountParams, DubGetLinksCountResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getLinksCountTool: ToolConfig<DubGetLinksCountParams, DubGetLinksCountResponse> = {
|
||||
id: 'dub_get_links_count',
|
||||
name: 'Dub Count Links',
|
||||
description:
|
||||
'Retrieve the number of short links for the authenticated workspace, optionally filtered and grouped by domain, tag, user, or folder.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by domain',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query matched against the short link slug and destination URL',
|
||||
},
|
||||
tagIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag IDs to filter by',
|
||||
},
|
||||
tagNames: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag names to filter by (case-insensitive)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by folder ID',
|
||||
},
|
||||
showArchived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include archived links (defaults to false)',
|
||||
},
|
||||
groupBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Group counts by: domain, tagId, userId, or folderId',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/links/count')
|
||||
if (params.domain) url.searchParams.set('domain', params.domain)
|
||||
if (params.search) url.searchParams.set('search', params.search)
|
||||
if (params.tagIds) url.searchParams.set('tagIds', params.tagIds)
|
||||
if (params.tagNames) url.searchParams.set('tagNames', params.tagNames)
|
||||
if (params.folderId) url.searchParams.set('folderId', params.folderId)
|
||||
if (params.showArchived !== undefined)
|
||||
url.searchParams.set('showArchived', String(params.showArchived))
|
||||
if (params.groupBy) url.searchParams.set('groupBy', params.groupBy)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to count links')
|
||||
}
|
||||
|
||||
if (typeof data === 'number') {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
count: data,
|
||||
groups: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const groups = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
const count = groups.reduce((sum, group) => sum + (Number(group.count) || 0), 0)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
count,
|
||||
groups,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
count: { type: 'number', description: 'Total number of links matching the filters' },
|
||||
groups: {
|
||||
type: 'json',
|
||||
description: 'Per-group counts when groupBy is set (e.g. [{ domain, count }])',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { DubGetQrCodeParams, DubGetQrCodeResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getQrCodeTool: ToolConfig<DubGetQrCodeParams, DubGetQrCodeResponse> = {
|
||||
id: 'dub_get_qr_code',
|
||||
name: 'Dub Get QR Code',
|
||||
description:
|
||||
'Generate a customizable QR code (PNG) for a short link, with control over size, error correction, colors, and margin.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The short link URL to encode in the QR code',
|
||||
},
|
||||
logo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL of a custom logo to embed in the QR code (requires a paid Dub plan)',
|
||||
},
|
||||
size: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'QR code size in pixels (default: 600)',
|
||||
},
|
||||
level: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Error correction level: L (default), M, Q, or H',
|
||||
},
|
||||
fgColor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Foreground color in hex (default: #000000)',
|
||||
},
|
||||
bgColor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Background color in hex (default: #FFFFFF)',
|
||||
},
|
||||
hideLogo: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to hide the logo in the center of the QR code (default: false)',
|
||||
},
|
||||
margin: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Margin (quiet zone) around the QR code (default: 2)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/qr')
|
||||
url.searchParams.set('url', params.url.trim())
|
||||
if (params.logo) url.searchParams.set('logo', params.logo)
|
||||
if (params.size !== undefined) url.searchParams.set('size', String(params.size))
|
||||
if (params.level) url.searchParams.set('level', params.level)
|
||||
if (params.fgColor) url.searchParams.set('fgColor', params.fgColor)
|
||||
if (params.bgColor) url.searchParams.set('bgColor', params.bgColor)
|
||||
if (params.hideLogo !== undefined) url.searchParams.set('hideLogo', String(params.hideLogo))
|
||||
if (params.margin !== undefined) url.searchParams.set('margin', String(params.margin))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'image/png',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
let message = errorText || `Failed to generate QR code: ${response.status}`
|
||||
try {
|
||||
const parsed = JSON.parse(errorText)
|
||||
message = parsed.error?.message || parsed.error || message
|
||||
} catch {
|
||||
// Non-JSON error body; use the raw text
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
const mimeType = response.headers.get('content-type') || 'image/png'
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: 'qrcode.png',
|
||||
mimeType,
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
},
|
||||
content: buffer.toString('base64'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Generated QR code image stored in execution files',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Base64-encoded PNG image data',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { bulkCreateLinksTool } from '@/tools/dub/bulk_create_links'
|
||||
import { bulkDeleteLinksTool } from '@/tools/dub/bulk_delete_links'
|
||||
import { bulkUpdateLinksTool } from '@/tools/dub/bulk_update_links'
|
||||
import { createLinkTool } from '@/tools/dub/create_link'
|
||||
import { createTagTool } from '@/tools/dub/create_tag'
|
||||
import { deleteLinkTool } from '@/tools/dub/delete_link'
|
||||
import { getAnalyticsTool } from '@/tools/dub/get_analytics'
|
||||
import { getEventsTool } from '@/tools/dub/get_events'
|
||||
import { getLinkTool } from '@/tools/dub/get_link'
|
||||
import { getLinksCountTool } from '@/tools/dub/get_links_count'
|
||||
import { getQrCodeTool } from '@/tools/dub/get_qr_code'
|
||||
import { listDomainsTool } from '@/tools/dub/list_domains'
|
||||
import { listFoldersTool } from '@/tools/dub/list_folders'
|
||||
import { listLinksTool } from '@/tools/dub/list_links'
|
||||
import { listTagsTool } from '@/tools/dub/list_tags'
|
||||
import { updateLinkTool } from '@/tools/dub/update_link'
|
||||
import { upsertLinkTool } from '@/tools/dub/upsert_link'
|
||||
|
||||
export const dubCreateLinkTool = createLinkTool
|
||||
export const dubGetLinkTool = getLinkTool
|
||||
export const dubUpdateLinkTool = updateLinkTool
|
||||
export const dubUpsertLinkTool = upsertLinkTool
|
||||
export const dubDeleteLinkTool = deleteLinkTool
|
||||
export const dubListLinksTool = listLinksTool
|
||||
export const dubGetAnalyticsTool = getAnalyticsTool
|
||||
export const dubGetLinksCountTool = getLinksCountTool
|
||||
export const dubGetEventsTool = getEventsTool
|
||||
export const dubBulkCreateLinksTool = bulkCreateLinksTool
|
||||
export const dubBulkUpdateLinksTool = bulkUpdateLinksTool
|
||||
export const dubBulkDeleteLinksTool = bulkDeleteLinksTool
|
||||
export const dubGetQrCodeTool = getQrCodeTool
|
||||
export const dubListDomainsTool = listDomainsTool
|
||||
export const dubListTagsTool = listTagsTool
|
||||
export const dubCreateTagTool = createTagTool
|
||||
export const dubListFoldersTool = listFoldersTool
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { DubListDomainsParams, DubListDomainsResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listDomainsTool: ToolConfig<DubListDomainsParams, DubListDomainsResponse> = {
|
||||
id: 'dub_list_domains',
|
||||
name: 'Dub List Domains',
|
||||
description:
|
||||
'Retrieve the custom domains registered in the workspace, so links can be created against the right domain.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include archived domains (defaults to false)',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search by domain name',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of domains per page (default: 50, max: 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/domains')
|
||||
if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived))
|
||||
if (params.search) url.searchParams.set('search', params.search)
|
||||
if (params.page) url.searchParams.set('page', String(params.page))
|
||||
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to list domains')
|
||||
}
|
||||
|
||||
const domains = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
domains,
|
||||
count: domains.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
domains: {
|
||||
type: 'json',
|
||||
description: 'Array of domain objects (slug, verified, primary, archived)',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of domains returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { DubListFoldersParams, DubListFoldersResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listFoldersTool: ToolConfig<DubListFoldersParams, DubListFoldersResponse> = {
|
||||
id: 'dub_list_folders',
|
||||
name: 'Dub List Folders',
|
||||
description:
|
||||
'Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search by folder name',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of folders per page (default: 50, max: 50)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/folders')
|
||||
if (params.search) url.searchParams.set('search', params.search)
|
||||
if (params.page) url.searchParams.set('page', String(params.page))
|
||||
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to list folders')
|
||||
}
|
||||
|
||||
const folders = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
folders,
|
||||
count: folders.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
folders: {
|
||||
type: 'json',
|
||||
description: 'Array of folder objects (id, name, accessLevel)',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of folders returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { DubListLinksParams, DubListLinksResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listLinksTool: ToolConfig<DubListLinksParams, DubListLinksResponse> = {
|
||||
id: 'dub_list_links',
|
||||
name: 'Dub List Links',
|
||||
description:
|
||||
'Retrieve a paginated list of short links for the authenticated workspace. Supports filtering by domain, search query, tags, and sorting.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by domain',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search query matched against the short link slug and destination URL',
|
||||
},
|
||||
tagIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag IDs to filter by',
|
||||
},
|
||||
tenantId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by tenant ID',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by folder ID',
|
||||
},
|
||||
showArchived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include archived links (defaults to false)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number (deprecated by Dub in favor of startingAfter/endingBefore)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of links per page (default: 100, max: 100)',
|
||||
},
|
||||
startingAfter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor: fetch results after this link ID',
|
||||
},
|
||||
endingBefore: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor: fetch results before this link ID',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/links')
|
||||
if (params.domain) url.searchParams.set('domain', params.domain)
|
||||
if (params.search) url.searchParams.set('search', params.search)
|
||||
if (params.tagIds) url.searchParams.set('tagIds', params.tagIds)
|
||||
if (params.tenantId) url.searchParams.set('tenantId', params.tenantId)
|
||||
if (params.folderId) url.searchParams.set('folderId', params.folderId)
|
||||
if (params.showArchived !== undefined)
|
||||
url.searchParams.set('showArchived', String(params.showArchived))
|
||||
if (params.page) url.searchParams.set('page', String(params.page))
|
||||
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
|
||||
if (params.startingAfter) url.searchParams.set('startingAfter', params.startingAfter)
|
||||
if (params.endingBefore) url.searchParams.set('endingBefore', params.endingBefore)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to list links')
|
||||
}
|
||||
|
||||
const links = Array.isArray(data) ? data : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
links: links.map((link: Record<string, unknown>) => ({
|
||||
id: (link.id as string) ?? '',
|
||||
domain: (link.domain as string) ?? '',
|
||||
key: (link.key as string) ?? '',
|
||||
url: (link.url as string) ?? '',
|
||||
shortLink: (link.shortLink as string) ?? '',
|
||||
qrCode: (link.qrCode as string) ?? '',
|
||||
archived: (link.archived as boolean) ?? false,
|
||||
externalId: (link.externalId as string) ?? null,
|
||||
title: (link.title as string) ?? null,
|
||||
description: (link.description as string) ?? null,
|
||||
clicks: (link.clicks as number) ?? 0,
|
||||
leads: (link.leads as number) ?? 0,
|
||||
conversions: (link.conversions as number) ?? 0,
|
||||
sales: (link.sales as number) ?? 0,
|
||||
saleAmount: (link.saleAmount as number) ?? 0,
|
||||
lastClicked: (link.lastClicked as string) ?? null,
|
||||
createdAt: (link.createdAt as string) ?? '',
|
||||
updatedAt: (link.updatedAt as string) ?? '',
|
||||
tags: (link.tags as Array<{ id: string; name: string; color: string }>) ?? [],
|
||||
folderId: (link.folderId as string) ?? null,
|
||||
tenantId: (link.tenantId as string) ?? null,
|
||||
trackConversion: (link.trackConversion as boolean) ?? false,
|
||||
utm_source: (link.utm_source as string) ?? null,
|
||||
utm_medium: (link.utm_medium as string) ?? null,
|
||||
utm_campaign: (link.utm_campaign as string) ?? null,
|
||||
utm_term: (link.utm_term as string) ?? null,
|
||||
utm_content: (link.utm_content as string) ?? null,
|
||||
})),
|
||||
count: links.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
links: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Array of link objects (id, domain, key, url, shortLink, clicks, tags, createdAt)',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of links returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { DubListTagsParams, DubListTagsResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listTagsTool: ToolConfig<DubListTagsParams, DubListTagsResponse> = {
|
||||
id: 'dub_list_tags',
|
||||
name: 'Dub List Tags',
|
||||
description:
|
||||
'Retrieve the tags defined in the workspace, so the right tag IDs can be assigned to links.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search by tag name',
|
||||
},
|
||||
sortBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field to sort by: name (default) or createdAt',
|
||||
},
|
||||
sortOrder: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort order: asc (default) or desc',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of tags per page (default: 100, max: 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.dub.co/tags')
|
||||
if (params.search) url.searchParams.set('search', params.search)
|
||||
if (params.sortBy) url.searchParams.set('sortBy', params.sortBy)
|
||||
if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder)
|
||||
if (params.page) url.searchParams.set('page', String(params.page))
|
||||
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to list tags')
|
||||
}
|
||||
|
||||
const tags = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tags,
|
||||
count: tags.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tags: {
|
||||
type: 'json',
|
||||
description: 'Array of tag objects (id, name, color)',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of tags returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
interface DubBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface DubCreateLinkParams extends DubBaseParams {
|
||||
url: string
|
||||
domain?: string
|
||||
key?: string
|
||||
externalId?: string
|
||||
tenantId?: string
|
||||
folderId?: string
|
||||
trackConversion?: boolean
|
||||
tagIds?: string
|
||||
comments?: string
|
||||
expiresAt?: string
|
||||
password?: string
|
||||
rewrite?: boolean
|
||||
archived?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
utm_source?: string
|
||||
utm_medium?: string
|
||||
utm_campaign?: string
|
||||
utm_term?: string
|
||||
utm_content?: string
|
||||
}
|
||||
|
||||
export interface DubGetLinkParams extends DubBaseParams {
|
||||
linkId?: string
|
||||
externalId?: string
|
||||
domain?: string
|
||||
key?: string
|
||||
}
|
||||
|
||||
export interface DubUpdateLinkParams extends DubBaseParams {
|
||||
linkId: string
|
||||
url?: string
|
||||
domain?: string
|
||||
key?: string
|
||||
title?: string
|
||||
description?: string
|
||||
externalId?: string
|
||||
tenantId?: string
|
||||
folderId?: string
|
||||
trackConversion?: boolean
|
||||
tagIds?: string
|
||||
comments?: string
|
||||
expiresAt?: string
|
||||
password?: string
|
||||
rewrite?: boolean
|
||||
archived?: boolean
|
||||
utm_source?: string
|
||||
utm_medium?: string
|
||||
utm_campaign?: string
|
||||
utm_term?: string
|
||||
utm_content?: string
|
||||
}
|
||||
|
||||
export interface DubUpsertLinkParams extends DubBaseParams {
|
||||
url: string
|
||||
domain?: string
|
||||
key?: string
|
||||
externalId?: string
|
||||
tenantId?: string
|
||||
folderId?: string
|
||||
trackConversion?: boolean
|
||||
tagIds?: string
|
||||
comments?: string
|
||||
expiresAt?: string
|
||||
password?: string
|
||||
rewrite?: boolean
|
||||
archived?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
utm_source?: string
|
||||
utm_medium?: string
|
||||
utm_campaign?: string
|
||||
utm_term?: string
|
||||
utm_content?: string
|
||||
}
|
||||
|
||||
export interface DubDeleteLinkParams extends DubBaseParams {
|
||||
linkId: string
|
||||
}
|
||||
|
||||
export interface DubListLinksParams extends DubBaseParams {
|
||||
domain?: string
|
||||
search?: string
|
||||
tagIds?: string
|
||||
tenantId?: string
|
||||
folderId?: string
|
||||
showArchived?: boolean
|
||||
page?: number
|
||||
pageSize?: number
|
||||
startingAfter?: string
|
||||
endingBefore?: string
|
||||
}
|
||||
|
||||
export interface DubGetAnalyticsParams extends DubBaseParams {
|
||||
event?: string
|
||||
groupBy?: string
|
||||
linkId?: string
|
||||
externalId?: string
|
||||
domain?: string
|
||||
interval?: string
|
||||
start?: string
|
||||
end?: string
|
||||
country?: string
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
export interface DubGetLinksCountParams extends DubBaseParams {
|
||||
domain?: string
|
||||
search?: string
|
||||
tagIds?: string
|
||||
tagNames?: string
|
||||
folderId?: string
|
||||
showArchived?: boolean
|
||||
groupBy?: string
|
||||
}
|
||||
|
||||
export interface DubGetEventsParams extends DubBaseParams {
|
||||
event?: string
|
||||
linkId?: string
|
||||
externalId?: string
|
||||
domain?: string
|
||||
interval?: string
|
||||
start?: string
|
||||
end?: string
|
||||
country?: string
|
||||
timezone?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
sortOrder?: string
|
||||
}
|
||||
|
||||
export interface DubBulkCreateLinksParams extends DubBaseParams {
|
||||
links: unknown
|
||||
}
|
||||
|
||||
export interface DubBulkUpdateLinksParams extends DubBaseParams {
|
||||
linkIds?: string
|
||||
externalIds?: string
|
||||
data: unknown
|
||||
}
|
||||
|
||||
export interface DubBulkDeleteLinksParams extends DubBaseParams {
|
||||
linkIds: string
|
||||
}
|
||||
|
||||
export interface DubGetQrCodeParams extends DubBaseParams {
|
||||
url: string
|
||||
logo?: string
|
||||
size?: number
|
||||
level?: string
|
||||
fgColor?: string
|
||||
bgColor?: string
|
||||
hideLogo?: boolean
|
||||
margin?: number
|
||||
}
|
||||
|
||||
export interface DubListDomainsParams extends DubBaseParams {
|
||||
archived?: boolean
|
||||
search?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface DubListTagsParams extends DubBaseParams {
|
||||
search?: string
|
||||
sortBy?: string
|
||||
sortOrder?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface DubCreateTagParams extends DubBaseParams {
|
||||
name: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface DubListFoldersParams extends DubBaseParams {
|
||||
search?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
interface DubLink {
|
||||
id: string
|
||||
domain: string
|
||||
key: string
|
||||
url: string
|
||||
shortLink: string
|
||||
qrCode: string
|
||||
archived: boolean
|
||||
externalId: string | null
|
||||
title: string | null
|
||||
description: string | null
|
||||
tags: Array<{ id: string; name: string; color: string }>
|
||||
folderId: string | null
|
||||
tenantId: string | null
|
||||
trackConversion: boolean
|
||||
clicks: number
|
||||
leads: number
|
||||
conversions: number
|
||||
sales: number
|
||||
saleAmount: number
|
||||
lastClicked: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
utm_source: string | null
|
||||
utm_medium: string | null
|
||||
utm_campaign: string | null
|
||||
utm_term: string | null
|
||||
utm_content: string | null
|
||||
}
|
||||
|
||||
export interface DubCreateLinkResponse extends ToolResponse {
|
||||
output: DubLink
|
||||
}
|
||||
|
||||
export interface DubGetLinkResponse extends ToolResponse {
|
||||
output: DubLink
|
||||
}
|
||||
|
||||
export interface DubUpdateLinkResponse extends ToolResponse {
|
||||
output: DubLink
|
||||
}
|
||||
|
||||
export interface DubUpsertLinkResponse extends ToolResponse {
|
||||
output: DubLink
|
||||
}
|
||||
|
||||
export interface DubDeleteLinkResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubListLinksResponse extends ToolResponse {
|
||||
output: {
|
||||
links: DubLink[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubGetAnalyticsResponse extends ToolResponse {
|
||||
output: {
|
||||
clicks: number
|
||||
leads: number
|
||||
sales: number
|
||||
saleAmount: number
|
||||
data: Record<string, unknown>[] | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubGetLinksCountResponse extends ToolResponse {
|
||||
output: {
|
||||
count: number
|
||||
groups: Record<string, unknown>[] | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubGetEventsResponse extends ToolResponse {
|
||||
output: {
|
||||
events: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubBulkCreateLinksResponse extends ToolResponse {
|
||||
output: {
|
||||
created: Record<string, unknown>[]
|
||||
errors: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubBulkUpdateLinksResponse extends ToolResponse {
|
||||
output: {
|
||||
updated: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubBulkDeleteLinksResponse extends ToolResponse {
|
||||
output: {
|
||||
deletedCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubGetQrCodeResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubListDomainsResponse extends ToolResponse {
|
||||
output: {
|
||||
domains: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubListTagsResponse extends ToolResponse {
|
||||
output: {
|
||||
tags: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubCreateTagResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DubListFoldersResponse extends ToolResponse {
|
||||
output: {
|
||||
folders: Record<string, unknown>[]
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export type DubResponse =
|
||||
| DubCreateLinkResponse
|
||||
| DubGetLinkResponse
|
||||
| DubUpdateLinkResponse
|
||||
| DubUpsertLinkResponse
|
||||
| DubDeleteLinkResponse
|
||||
| DubListLinksResponse
|
||||
| DubGetAnalyticsResponse
|
||||
| DubGetLinksCountResponse
|
||||
| DubGetEventsResponse
|
||||
| DubBulkCreateLinksResponse
|
||||
| DubBulkUpdateLinksResponse
|
||||
| DubBulkDeleteLinksResponse
|
||||
| DubGetQrCodeResponse
|
||||
| DubListDomainsResponse
|
||||
| DubListTagsResponse
|
||||
| DubCreateTagResponse
|
||||
| DubListFoldersResponse
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { DubUpdateLinkParams, DubUpdateLinkResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateLinkTool: ToolConfig<DubUpdateLinkParams, DubUpdateLinkResponse> = {
|
||||
id: 'dub_update_link',
|
||||
name: 'Dub Update Link',
|
||||
description:
|
||||
'Update an existing short link. You can modify the destination URL, slug, metadata, UTM parameters, and more.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
linkId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The link ID or external ID prefixed with ext_',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New destination URL',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New custom domain',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New custom slug',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG description',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'External ID for the link',
|
||||
},
|
||||
tenantId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tenant ID for grouping links created on behalf of a customer/tenant',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Folder ID to organize the link into',
|
||||
},
|
||||
trackConversion: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to track conversions (leads/sales) for the short link',
|
||||
},
|
||||
tagIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag IDs',
|
||||
},
|
||||
comments: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comments for the short link',
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Expiration date in ISO 8601 format',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Password to protect the link',
|
||||
},
|
||||
rewrite: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable link cloaking',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to archive the link',
|
||||
},
|
||||
utm_source: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM source parameter',
|
||||
},
|
||||
utm_medium: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM medium parameter',
|
||||
},
|
||||
utm_campaign: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM campaign parameter',
|
||||
},
|
||||
utm_term: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM term parameter',
|
||||
},
|
||||
utm_content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM content parameter',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.dub.co/links/${params.linkId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.url) body.url = params.url
|
||||
if (params.domain) body.domain = params.domain
|
||||
if (params.key) body.key = params.key
|
||||
if (params.title) body.title = params.title
|
||||
if (params.description) body.description = params.description
|
||||
if (params.externalId) body.externalId = params.externalId
|
||||
if (params.tenantId) body.tenantId = params.tenantId
|
||||
if (params.folderId) body.folderId = params.folderId
|
||||
if (params.trackConversion !== undefined) body.trackConversion = params.trackConversion
|
||||
if (params.tagIds) body.tagIds = params.tagIds.split(',').map((id) => id.trim())
|
||||
if (params.comments) body.comments = params.comments
|
||||
if (params.expiresAt) body.expiresAt = params.expiresAt
|
||||
if (params.password) body.password = params.password
|
||||
if (params.rewrite !== undefined) body.rewrite = params.rewrite
|
||||
if (params.archived !== undefined) body.archived = params.archived
|
||||
if (params.utm_source) body.utm_source = params.utm_source
|
||||
if (params.utm_medium) body.utm_medium = params.utm_medium
|
||||
if (params.utm_campaign) body.utm_campaign = params.utm_campaign
|
||||
if (params.utm_term) body.utm_term = params.utm_term
|
||||
if (params.utm_content) body.utm_content = params.utm_content
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to update link')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
domain: data.domain ?? '',
|
||||
key: data.key ?? '',
|
||||
url: data.url ?? '',
|
||||
shortLink: data.shortLink ?? '',
|
||||
qrCode: data.qrCode ?? '',
|
||||
archived: data.archived ?? false,
|
||||
externalId: data.externalId ?? null,
|
||||
title: data.title ?? null,
|
||||
description: data.description ?? null,
|
||||
tags: data.tags ?? [],
|
||||
folderId: data.folderId ?? null,
|
||||
tenantId: data.tenantId ?? null,
|
||||
trackConversion: data.trackConversion ?? false,
|
||||
clicks: data.clicks ?? 0,
|
||||
leads: data.leads ?? 0,
|
||||
conversions: data.conversions ?? 0,
|
||||
sales: data.sales ?? 0,
|
||||
saleAmount: data.saleAmount ?? 0,
|
||||
lastClicked: data.lastClicked ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
utm_source: data.utm_source ?? null,
|
||||
utm_medium: data.utm_medium ?? null,
|
||||
utm_campaign: data.utm_campaign ?? null,
|
||||
utm_term: data.utm_term ?? null,
|
||||
utm_content: data.utm_content ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique ID of the updated link' },
|
||||
domain: { type: 'string', description: 'Domain of the short link' },
|
||||
key: { type: 'string', description: 'Slug of the short link' },
|
||||
url: { type: 'string', description: 'Destination URL' },
|
||||
shortLink: { type: 'string', description: 'Full short link URL' },
|
||||
qrCode: { type: 'string', description: 'QR code URL for the short link' },
|
||||
archived: { type: 'boolean', description: 'Whether the link is archived' },
|
||||
externalId: { type: 'string', description: 'External ID', optional: true },
|
||||
title: { type: 'string', description: 'OG title', optional: true },
|
||||
description: { type: 'string', description: 'OG description', optional: true },
|
||||
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
|
||||
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
|
||||
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
|
||||
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
|
||||
clicks: { type: 'number', description: 'Number of clicks' },
|
||||
leads: { type: 'number', description: 'Number of leads' },
|
||||
conversions: { type: 'number', description: 'Number of conversions' },
|
||||
sales: { type: 'number', description: 'Number of sales' },
|
||||
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
|
||||
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
utm_source: { type: 'string', description: 'UTM source parameter', optional: true },
|
||||
utm_medium: { type: 'string', description: 'UTM medium parameter', optional: true },
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign parameter', optional: true },
|
||||
utm_term: { type: 'string', description: 'UTM term parameter', optional: true },
|
||||
utm_content: { type: 'string', description: 'UTM content parameter', optional: true },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { DubUpsertLinkParams, DubUpsertLinkResponse } from '@/tools/dub/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const upsertLinkTool: ToolConfig<DubUpsertLinkParams, DubUpsertLinkResponse> = {
|
||||
id: 'dub_upsert_link',
|
||||
name: 'Dub Upsert Link',
|
||||
description:
|
||||
'Create or update a short link by its URL. If a link with the same URL already exists, update it. Otherwise, create a new link.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dub API key',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The destination URL of the short link',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom domain for the short link (defaults to dub.sh)',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom slug for the short link (randomly generated if not provided)',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'External ID for the link in your database',
|
||||
},
|
||||
tenantId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tenant ID for grouping links created on behalf of a customer/tenant',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Folder ID to organize the link into',
|
||||
},
|
||||
trackConversion: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to track conversions (leads/sales) for the short link',
|
||||
},
|
||||
tagIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tag IDs to assign to the link',
|
||||
},
|
||||
comments: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comments for the short link',
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Expiration date in ISO 8601 format',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Password to protect the short link',
|
||||
},
|
||||
rewrite: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable link cloaking',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to archive the link',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG title for the link preview',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom OG description for the link preview',
|
||||
},
|
||||
utm_source: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM source parameter',
|
||||
},
|
||||
utm_medium: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM medium parameter',
|
||||
},
|
||||
utm_campaign: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM campaign parameter',
|
||||
},
|
||||
utm_term: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM term parameter',
|
||||
},
|
||||
utm_content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UTM content parameter',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.dub.co/links/upsert',
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = { url: params.url }
|
||||
if (params.domain) body.domain = params.domain
|
||||
if (params.key) body.key = params.key
|
||||
if (params.externalId) body.externalId = params.externalId
|
||||
if (params.tenantId) body.tenantId = params.tenantId
|
||||
if (params.folderId) body.folderId = params.folderId
|
||||
if (params.trackConversion !== undefined) body.trackConversion = params.trackConversion
|
||||
if (params.tagIds) body.tagIds = params.tagIds.split(',').map((id) => id.trim())
|
||||
if (params.comments) body.comments = params.comments
|
||||
if (params.expiresAt) body.expiresAt = params.expiresAt
|
||||
if (params.password) body.password = params.password
|
||||
if (params.rewrite !== undefined) body.rewrite = params.rewrite
|
||||
if (params.archived !== undefined) body.archived = params.archived
|
||||
if (params.title) body.title = params.title
|
||||
if (params.description) body.description = params.description
|
||||
if (params.utm_source) body.utm_source = params.utm_source
|
||||
if (params.utm_medium) body.utm_medium = params.utm_medium
|
||||
if (params.utm_campaign) body.utm_campaign = params.utm_campaign
|
||||
if (params.utm_term) body.utm_term = params.utm_term
|
||||
if (params.utm_content) body.utm_content = params.utm_content
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || data.error || 'Failed to upsert link')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? '',
|
||||
domain: data.domain ?? '',
|
||||
key: data.key ?? '',
|
||||
url: data.url ?? '',
|
||||
shortLink: data.shortLink ?? '',
|
||||
qrCode: data.qrCode ?? '',
|
||||
archived: data.archived ?? false,
|
||||
externalId: data.externalId ?? null,
|
||||
title: data.title ?? null,
|
||||
description: data.description ?? null,
|
||||
tags: data.tags ?? [],
|
||||
folderId: data.folderId ?? null,
|
||||
tenantId: data.tenantId ?? null,
|
||||
trackConversion: data.trackConversion ?? false,
|
||||
clicks: data.clicks ?? 0,
|
||||
leads: data.leads ?? 0,
|
||||
conversions: data.conversions ?? 0,
|
||||
sales: data.sales ?? 0,
|
||||
saleAmount: data.saleAmount ?? 0,
|
||||
lastClicked: data.lastClicked ?? null,
|
||||
createdAt: data.createdAt ?? '',
|
||||
updatedAt: data.updatedAt ?? '',
|
||||
utm_source: data.utm_source ?? null,
|
||||
utm_medium: data.utm_medium ?? null,
|
||||
utm_campaign: data.utm_campaign ?? null,
|
||||
utm_term: data.utm_term ?? null,
|
||||
utm_content: data.utm_content ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique ID of the link' },
|
||||
domain: { type: 'string', description: 'Domain of the short link' },
|
||||
key: { type: 'string', description: 'Slug of the short link' },
|
||||
url: { type: 'string', description: 'Destination URL' },
|
||||
shortLink: { type: 'string', description: 'Full short link URL' },
|
||||
qrCode: { type: 'string', description: 'QR code URL for the short link' },
|
||||
archived: { type: 'boolean', description: 'Whether the link is archived' },
|
||||
externalId: { type: 'string', description: 'External ID', optional: true },
|
||||
title: { type: 'string', description: 'OG title', optional: true },
|
||||
description: { type: 'string', description: 'OG description', optional: true },
|
||||
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
|
||||
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
|
||||
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
|
||||
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
|
||||
clicks: { type: 'number', description: 'Number of clicks' },
|
||||
leads: { type: 'number', description: 'Number of leads' },
|
||||
conversions: { type: 'number', description: 'Number of conversions' },
|
||||
sales: { type: 'number', description: 'Number of sales' },
|
||||
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
|
||||
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp' },
|
||||
utm_source: { type: 'string', description: 'UTM source parameter', optional: true },
|
||||
utm_medium: { type: 'string', description: 'UTM medium parameter', optional: true },
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign parameter', optional: true },
|
||||
utm_term: { type: 'string', description: 'UTM term parameter', optional: true },
|
||||
utm_content: { type: 'string', description: 'UTM content parameter', optional: true },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user