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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+73
View File
@@ -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' },
},
}
+62
View File
@@ -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' },
},
}
+102
View File
@@ -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' },
},
}
+242
View File
@@ -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 },
},
}
+68
View File
@@ -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' },
},
}
+52
View File
@@ -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' },
},
}
+146
View File
@@ -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,
},
},
}
+142
View File
@@ -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' },
},
}
+130
View File
@@ -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 },
},
}
+119
View File
@@ -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,
},
},
}
+129
View File
@@ -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',
},
},
}
+35
View File
@@ -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
+85
View File
@@ -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' },
},
}
+78
View File
@@ -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' },
},
}
+160
View File
@@ -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',
},
},
}
+92
View File
@@ -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' },
},
}
+352
View File
@@ -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
+249
View File
@@ -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 },
},
}
+242
View File
@@ -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 },
},
}