Compare commits

...

6 Commits

Author SHA1 Message Date
waleed
eb493d94ed type fixes 2026-02-28 12:03:17 -08:00
Vasyl Abramovych
dd14f9d750 fix(lint): satisfy biome for short.io
Made-with: Cursor
2026-02-28 11:38:34 -08:00
Vasyl Abramovych
fc5e4237ab fix(short-io): address PR review feedback
- Change apiKey visibility from 'hidden' to 'user-only' in all 6 tools
- Simplify block tool selector to string interpolation
- Move QR code generation to server-side API route, return as file
  object (name, mimeType, data, size) matching standard file pattern
- Update block outputs and docs to reflect file type for QR code
2026-02-28 11:38:34 -08:00
Vasyl Abramovych
fcefd01f94 docs(short-io): add Short.io tool documentation
Add documentation page covering all 6 Short.io tools with input/output
parameter tables and usage instructions.
2026-02-28 11:38:34 -08:00
Vasyl Abramovych
2408b5af2d feat(blocks): add Short.io block and icon
Add Short.io block config with 6 operations (create link, list domains,
list links, delete link, get QR code, get analytics). Add ShortIoIcon
and register the block in the blocks registry.
2026-02-28 11:38:34 -08:00
Vasyl Abramovych
7327ec0058 feat(tools): add Short.io tools and registry
Add 6 Short.io tool implementations (create link, list domains, list
links, delete link, get QR code, get analytics) with shared types and
barrel export. Register all tools in the tools registry.
2026-02-28 11:38:34 -08:00
17 changed files with 1108 additions and 2 deletions

View File

@@ -76,7 +76,6 @@ export function ApiIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function ConditionalIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -6012,3 +6011,20 @@ export function HexIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function ShortIoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 64 65' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect width='64' height='65' fill='#FFFFFF' />
<path
d='M41.1 45.7c0 2-.8 3.5-2.5 4.6-1.6 1-3.8 1.6-6.5 1.6-3.4 0-6-.8-8-2.3-2-1.6-3-3.6-3.2-6.1l-16.3-.4c0 4.1 1.2 7.8 3.6 11.1A24 24 0 0 0 18 62c2.2 1 4.5 1.7 7 2.2l.4.1H0V.2h24.9A25.4 25.4 0 0 0 9.3 9.5C7.1 12.5 6 15.9 6 19.7c0 4.2.9 7.6 2.6 10.1 1.7 2.5 4 4.4 6.8 5.7 2.8 1.3 6.3 2.3 10.6 3.2 4.4.9 7.5 1.6 9.5 2.2 1.9.5 3.3 1.1 4.3 1.9.8.6 1.3 1.6 1.3 2.9Z'
fill='#0BB07D'
/>
<path d='M25.3 64.2h-.6l.1-.1.5.1Z' fill='#33333D' />
<path
d='M64 64.2H38.1a28 28 0 0 0 7.1-2.2 23 23 0 0 0 9.4-7.6c2.2-3.2 3.4-6.8 3.4-10.8a17 17 0 0 0-2.6-9.8c-1.7-2.4-4-4.3-6.9-5.5a54.4 54.4 0 0 0-10.8-3.1c-4.3-.8-7.3-1.5-9.2-2.1a12 12 0 0 1-4.2-1.8c-.9-.7-1.3-1.7-1.3-3 0-1.9.7-3.3 2.2-4.3 1.5-1 3.4-1.5 5.8-1.5 2.7 0 4.9.7 6.5 2.1a7.8 7.8 0 0 1 2.7 5.4h16.4c0-3.8-1.1-7.3-3.3-10.5a23 23 0 0 0-9.1-7.4c-2.1-1-4.4-1.7-6.8-2.1H64v64.2Z'
fill='#383738'
/>
</svg>
)
}

View File

@@ -126,6 +126,7 @@ import {
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
ShortIoIcon,
SimilarwebIcon,
SlackIcon,
SmtpIcon,
@@ -283,6 +284,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
short_io: ShortIoIcon,
similarweb: SimilarwebIcon,
slack: SlackIcon,
smtp: SmtpIcon,

View File

@@ -122,6 +122,7 @@
"sftp",
"sharepoint",
"shopify",
"short_io",
"similarweb",
"slack",
"smtp",

View File

@@ -0,0 +1,173 @@
---
title: Short.io
description: Create and manage short links, domains, and analytics.
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="short_io"
color="#FFFFFF"
/>
{/* MANUAL-CONTENT-START:intro */}
[Short.io](https://short.io/) is a white-label URL shortener that lets you create branded short links on your own domain, track clicks, and manage links at scale. Short.io is designed for businesses that want professional short URLs, QR codes, and link analytics without relying on generic shorteners.
With Short.io in Sim, you can:
- **Create short links**: Generate branded short URLs from long URLs using your custom domain, with optional custom paths
- **List domains**: Retrieve all Short.io domains on your account to get domain IDs for listing links
- **List links**: List short links for a domain with pagination and optional date sort order
- **Delete links**: Remove a short link by its ID (e.g. lnk_abc123_abcdef)
- **Generate QR codes**: Create QR codes for any Short.io link with optional size, color, background color, and format (PNG or SVG); returns a base64 data URL
- **Get link statistics**: Fetch click analytics for a link including total clicks, human clicks, referrer/country/browser/OS/city breakdowns, UTM dimensions, time-series data, and date interval
These capabilities allow your Sim agents to automate link shortening, QR code generation, and analytics reporting directly in your workflows — from campaign tracking to link management and performance dashboards.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Short.io to generate branded short links, list domains and links, delete links, generate QR codes, and view link statistics. Requires your Short.io Secret API Key.
## Tools
### `short_io_create_link`
Create a short link using your Short.io custom domain.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
| `domain` | string | Yes | Your registered Short.io custom domain |
| `originalURL` | string | Yes | The long URL to shorten |
| `path` | string | No | Optional custom path for the short link |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `shortURL` | string | The generated short link URL |
| `idString` | string | The unique Short.io link ID string |
| `originalURL` | string | The original long URL |
| `path` | string | The path/slug of the short link |
| `createdAt` | string | ISO 8601 creation timestamp |
### `short_io_list_domains`
List Short.io domains. Returns domain IDs and details for use in List Links.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domains` | array | List of domain objects \(id, hostname, etc.\) |
| `count` | number | Number of domains |
### `short_io_list_links`
List short links for a domain. Requires domain_id (from List Domains or dashboard). Max 150 per request.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
| `domainId` | number | Yes | Domain ID \(from List Domains\) |
| `limit` | number | No | Max links to return \(1150\) |
| `pageToken` | string | No | Pagination token from previous response |
| `dateSortOrder` | string | No | Sort by date: asc or desc |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `links` | array | List of link objects \(idString, shortURL, originalURL, path, etc.\) |
| `count` | number | Number of links returned |
| `nextPageToken` | string | Token for next page |
### `short_io_delete_link`
Delete a short link by ID (e.g. lnk_abc123_abcdef). Rate limit 20/s.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
| `linkId` | string | Yes | Link ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `deleted` | boolean | Whether the link was deleted |
| `idString` | string | Deleted link ID |
### `short_io_get_qr_code`
Generate a QR code for a Short.io link (POST /links/qr/{linkIdString}).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
| `linkId` | string | Yes | Link ID \(e.g. lnk_abc123_abcdef\) |
| `color` | string | No | QR color hex \(e.g. 000000\) |
| `backgroundColor` | string | No | Background color hex \(e.g. FFFFFF\) |
| `size` | number | No | QR size 199 |
| `type` | string | No | Output format: png or svg |
| `useDomainSettings` | boolean | No | Use domain settings \(default true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | file | Generated QR code image file |
### `short_io_get_analytics`
Fetch click statistics for a Short.io link (Statistics API: totalClicks, humanClicks, referer, country, etc.).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Short.io Secret API Key |
| `linkId` | string | Yes | No description |
| `period` | string | Yes | Period: today, yesterday, last7, last30, total, week, month, lastmonth |
| `tz` | string | No | Timezone \(default UTC\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalClicks` | number | Total clicks |
| `humanClicks` | number | Human clicks |
| `totalClicksChange` | string | Change vs previous period |
| `humanClicksChange` | string | Human clicks change |
| `referer` | array | Referrer breakdown \(referer, score\) |
| `country` | array | Country breakdown \(countryName, country, score\) |
| `browser` | array | Browser breakdown \(browser, score\) |
| `os` | array | OS breakdown \(os, score\) |
| `city` | array | City breakdown \(city, name, countryCode, score\) |
| `device` | array | Device breakdown |
| `social` | array | Social source breakdown \(social, score\) |
| `utmMedium` | array | UTM medium breakdown |
| `utmSource` | array | UTM source breakdown |
| `utmCampaign` | array | UTM campaign breakdown |
| `clickStatistics` | object | Time-series click data \(datasets with x/y points per interval\) |
| `interval` | object | Date range \(startDate, endDate, prevStartDate, prevEndDate, tz\) |

View File

@@ -0,0 +1,101 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
export const dynamic = 'force-dynamic'
const logger = createLogger('ShortIoQrAPI')
const ShortIoQrSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
linkId: z.string().min(1, 'Link ID is required'),
color: z.string().optional(),
backgroundColor: z.string().optional(),
size: z.number().min(1).max(99).optional(),
type: z.enum(['png', 'svg']).optional(),
useDomainSettings: z.boolean().optional(),
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Short.io QR request: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const body = await request.json()
const validated = ShortIoQrSchema.parse(body)
const qrBody: Record<string, unknown> = {
useDomainSettings: validated.useDomainSettings ?? true,
}
if (validated.color) qrBody.color = validated.color
if (validated.backgroundColor) qrBody.backgroundColor = validated.backgroundColor
if (validated.size) qrBody.size = validated.size
if (validated.type) qrBody.type = validated.type
const response = await fetch(`https://api.short.io/links/qr/${validated.linkId}`, {
method: 'POST',
headers: {
Authorization: validated.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(qrBody),
})
if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText)
logger.error(`[${requestId}] Short.io QR API error: ${errorText}`)
return NextResponse.json(
{ success: false, error: `Short.io API error: ${errorText}` },
{ status: response.status }
)
}
const contentType = response.headers.get('Content-Type') ?? 'image/png'
const fileBuffer = Buffer.from(await response.arrayBuffer())
const mimeType = contentType.split(';')[0]?.trim() || 'image/png'
const ext = validated.type === 'svg' ? 'svg' : 'png'
const fileName = `qr-${validated.linkId}.${ext}`
logger.info(`[${requestId}] QR code generated`, {
linkId: validated.linkId,
size: fileBuffer.length,
mimeType,
})
return NextResponse.json({
success: true,
output: {
file: {
name: fileName,
mimeType,
data: fileBuffer.toString('base64'),
size: fileBuffer.length,
},
},
})
} catch (error: unknown) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: `Validation error: ${error.errors.map((e) => e.message).join(', ')}`,
},
{ status: 400 }
)
}
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error(`[${requestId}] Short.io QR error: ${message}`)
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,272 @@
import { ShortIoIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'
export const ShortIoBlock: BlockConfig<ToolResponse> = {
type: 'short_io',
name: 'Short.io',
description: 'Create and manage short links, domains, and analytics.',
authMode: AuthMode.ApiKey,
longDescription:
'Integrate Short.io to generate branded short links, list domains and links, delete links, generate QR codes, and view link statistics. Requires your Short.io Secret API Key.',
docsLink: 'https://docs.sim.ai/tools/short_io',
category: 'tools',
bgColor: '#FFFFFF',
icon: ShortIoIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create Link', id: 'create_link' },
{ label: 'List Domains', id: 'list_domains' },
{ label: 'List Links', id: 'list_links' },
{ label: 'Delete Link', id: 'delete_link' },
{ label: 'Get QR Code', id: 'get_qr_code' },
{ label: 'Get Link Statistics', id: 'get_analytics' },
],
value: () => 'create_link',
},
{
id: 'apiKey',
title: 'Secret API Key',
type: 'short-input',
mode: 'basic',
required: true,
password: true,
placeholder: 'sk_...',
},
{
id: 'domain',
title: 'Custom Domain',
type: 'short-input',
placeholder: 'link.yourbrand.com',
condition: { field: 'operation', value: 'create_link' },
required: true,
},
{
id: 'originalURL',
title: 'Original URL',
type: 'long-input',
placeholder: 'https://www.example.com/very/long/path/to/page',
condition: { field: 'operation', value: 'create_link' },
required: true,
},
{
id: 'path',
title: 'Custom Path (Optional)',
type: 'short-input',
placeholder: 'my-custom-path',
condition: { field: 'operation', value: 'create_link' },
required: false,
mode: 'advanced',
},
{
id: 'domainId',
title: 'Domain ID',
type: 'short-input',
placeholder: '12345',
condition: { field: 'operation', value: 'list_links' },
required: true,
},
{
id: 'limit',
title: 'Limit (1150)',
type: 'short-input',
placeholder: '50',
condition: { field: 'operation', value: 'list_links' },
required: false,
mode: 'advanced',
},
{
id: 'dateSortOrder',
title: 'Sort Order',
type: 'dropdown',
options: [
{ label: 'Descending', id: 'desc' },
{ label: 'Ascending', id: 'asc' },
],
condition: { field: 'operation', value: 'list_links' },
required: false,
mode: 'advanced',
value: () => 'desc',
},
{
id: 'pageToken',
title: 'Page Token',
type: 'short-input',
placeholder: 'Next page token',
condition: { field: 'operation', value: 'list_links' },
required: false,
mode: 'advanced',
},
{
id: 'linkId',
title: 'Short.io Link ID',
type: 'short-input',
placeholder: 'lnk_abc123_abcdef',
condition: {
field: 'operation',
value: ['get_qr_code', 'get_analytics', 'delete_link'],
},
required: true,
},
{
id: 'type',
title: 'QR Format',
type: 'dropdown',
options: [
{ label: 'PNG', id: 'png' },
{ label: 'SVG', id: 'svg' },
],
condition: { field: 'operation', value: 'get_qr_code' },
required: false,
value: () => 'png',
},
{
id: 'size',
title: 'QR Size (199)',
type: 'short-input',
placeholder: '10',
condition: { field: 'operation', value: 'get_qr_code' },
required: false,
mode: 'advanced',
},
{
id: 'color',
title: 'QR Color (hex)',
type: 'short-input',
placeholder: '000000',
condition: { field: 'operation', value: 'get_qr_code' },
required: false,
mode: 'advanced',
},
{
id: 'backgroundColor',
title: 'Background Color (hex)',
type: 'short-input',
placeholder: 'FFFFFF',
condition: { field: 'operation', value: 'get_qr_code' },
required: false,
mode: 'advanced',
},
{
id: 'period',
title: 'Statistics Period',
type: 'dropdown',
options: [
{ label: 'Today', id: 'today' },
{ label: 'Yesterday', id: 'yesterday' },
{ label: 'This Week', id: 'week' },
{ label: 'Last 7 Days', id: 'last_7_days' },
{ label: 'This Month', id: 'month' },
{ label: 'Last Month', id: 'lastmonth' },
{ label: 'Last 30 Days', id: 'last_30_days' },
{ label: 'All Time', id: 'all_time' },
],
condition: { field: 'operation', value: 'get_analytics' },
required: true,
value: () => 'last_30_days',
},
{
id: 'tz',
title: 'Timezone',
type: 'short-input',
placeholder: 'UTC',
condition: { field: 'operation', value: 'get_analytics' },
required: false,
mode: 'advanced',
},
],
tools: {
access: [
'short_io_create_link',
'short_io_list_domains',
'short_io_list_links',
'short_io_delete_link',
'short_io_get_qr_code',
'short_io_get_analytics',
],
config: {
tool: (params) => `short_io_${params.operation}`,
params: (params) => {
const { apiKey, operation, size, domainId, limit, dateSortOrder, ...rest } = params
const out: Record<string, unknown> = { ...rest, apiKey }
if (size !== undefined && size !== '') {
const n = Number(size)
if (!Number.isNaN(n) && n >= 1 && n <= 99) out.size = n
}
if (operation === 'list_links' && domainId !== undefined && domainId !== '') {
const d = Number(domainId)
if (!Number.isNaN(d)) out.domainId = d
}
if (operation === 'list_links' && limit !== undefined && limit !== '') {
const l = Number(limit)
if (!Number.isNaN(l) && l >= 1 && l <= 150) out.limit = l
}
if (operation === 'list_links' && dateSortOrder !== undefined && dateSortOrder !== '') {
out.dateSortOrder = dateSortOrder
}
return out
},
},
},
inputs: {
apiKey: { type: 'string', description: 'Secret API Key' },
operation: { type: 'string', description: 'Short.io operation to perform' },
domain: { type: 'string', description: 'Your registered Short.io custom domain' },
originalURL: { type: 'string', description: 'The original long URL to shorten' },
path: { type: 'string', description: 'Optional custom path for the short link' },
domainId: { type: 'number', description: 'Domain ID (from List Domains)' },
limit: { type: 'number', description: 'Max links to return (1150)' },
dateSortOrder: { type: 'string', description: 'Sort order: asc or desc' },
pageToken: { type: 'string', description: 'Pagination token for List Links' },
linkId: { type: 'string', description: 'The Short.io internal link ID string' },
type: { type: 'string', description: 'QR output format: png or svg' },
size: { type: 'number', description: 'QR size 199' },
color: { type: 'string', description: 'QR color hex' },
backgroundColor: { type: 'string', description: 'QR background color hex' },
period: {
type: 'string',
description: 'Statistics period (e.g. today, last_30_days, all_time)',
},
tz: { type: 'string', description: 'Timezone for statistics (e.g. UTC)' },
},
outputs: {
shortURL: { type: 'string', description: 'The generated short link' },
idString: { type: 'string', description: 'The Short.io link ID' },
originalURL: { type: 'string', description: 'The original long URL' },
path: { type: 'string', description: 'The path/slug of the short link' },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' },
domains: { type: 'array', description: 'List of domains (from List Domains)' },
count: { type: 'number', description: 'Number of domains or links returned' },
links: { type: 'array', description: 'List of links (from List Links)' },
nextPageToken: { type: 'string', description: 'Pagination token for next page' },
deleted: { type: 'boolean', description: 'Whether the link was deleted' },
file: { type: 'file', description: 'Generated QR code image file' },
totalClicks: { type: 'number', description: 'Total clicks' },
humanClicks: { type: 'number', description: 'Human clicks' },
totalClicksChange: { type: 'string', description: 'Change in total clicks vs previous period' },
humanClicksChange: { type: 'string', description: 'Change in human clicks vs previous period' },
referer: { type: 'array', description: 'Referrer breakdown (referer, score)' },
country: { type: 'array', description: 'Country breakdown (countryName, country, score)' },
browser: { type: 'array', description: 'Browser breakdown (browser, score)' },
os: { type: 'array', description: 'OS breakdown (os, score)' },
city: { type: 'array', description: 'City breakdown (city, name, countryCode, score)' },
device: { type: 'array', description: 'Device breakdown' },
social: { type: 'array', description: 'Social source breakdown (social, score)' },
utmMedium: { type: 'array', description: 'UTM medium breakdown' },
utmSource: { type: 'array', description: 'UTM source breakdown' },
utmCampaign: { type: 'array', description: 'UTM campaign breakdown' },
clickStatistics: {
type: 'json',
description: 'Time-series click data (datasets with x/y per interval)',
},
interval: {
type: 'json',
description: 'Date range (startDate, endDate, prevStartDate, prevEndDate, tz)',
},
},
}

View File

@@ -141,6 +141,7 @@ import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
import { SftpBlock } from '@/blocks/blocks/sftp'
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
import { ShopifyBlock } from '@/blocks/blocks/shopify'
import { ShortIoBlock } from '@/blocks/blocks/short_io'
import { SimilarwebBlock } from '@/blocks/blocks/similarweb'
import { SlackBlock } from '@/blocks/blocks/slack'
import { SmtpBlock } from '@/blocks/blocks/smtp'
@@ -345,6 +346,7 @@ export const registry: Record<string, BlockConfig> = {
sftp: SftpBlock,
sharepoint: SharepointBlock,
shopify: ShopifyBlock,
short_io: ShortIoBlock,
similarweb: SimilarwebBlock,
slack: SlackBlock,
smtp: SmtpBlock,

View File

@@ -76,7 +76,6 @@ export function ApiIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function ConditionalIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
@@ -6012,3 +6011,20 @@ export function HexIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function ShortIoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 64 65' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect width='64' height='65' fill='#FFFFFF' />
<path
d='M41.1 45.7c0 2-.8 3.5-2.5 4.6-1.6 1-3.8 1.6-6.5 1.6-3.4 0-6-.8-8-2.3-2-1.6-3-3.6-3.2-6.1l-16.3-.4c0 4.1 1.2 7.8 3.6 11.1A24 24 0 0 0 18 62c2.2 1 4.5 1.7 7 2.2l.4.1H0V.2h24.9A25.4 25.4 0 0 0 9.3 9.5C7.1 12.5 6 15.9 6 19.7c0 4.2.9 7.6 2.6 10.1 1.7 2.5 4 4.4 6.8 5.7 2.8 1.3 6.3 2.3 10.6 3.2 4.4.9 7.5 1.6 9.5 2.2 1.9.5 3.3 1.1 4.3 1.9.8.6 1.3 1.6 1.3 2.9Z'
fill='#0BB07D'
/>
<path d='M25.3 64.2h-.6l.1-.1.5.1Z' fill='#33333D' />
<path
d='M64 64.2H38.1a28 28 0 0 0 7.1-2.2 23 23 0 0 0 9.4-7.6c2.2-3.2 3.4-6.8 3.4-10.8a17 17 0 0 0-2.6-9.8c-1.7-2.4-4-4.3-6.9-5.5a54.4 54.4 0 0 0-10.8-3.1c-4.3-.8-7.3-1.5-9.2-2.1a12 12 0 0 1-4.2-1.8c-.9-.7-1.3-1.7-1.3-3 0-1.9.7-3.3 2.2-4.3 1.5-1 3.4-1.5 5.8-1.5 2.7 0 4.9.7 6.5 2.1a7.8 7.8 0 0 1 2.7 5.4h16.4c0-3.8-1.1-7.3-3.3-10.5a23 23 0 0 0-9.1-7.4c-2.1-1-4.4-1.7-6.8-2.1H64v64.2Z'
fill='#383738'
/>
</svg>
)
}

View File

@@ -1744,6 +1744,14 @@ import {
shopifyUpdateOrderTool,
shopifyUpdateProductTool,
} from '@/tools/shopify'
import {
shortIoCreateLinkTool,
shortIoDeleteLinkTool,
shortIoGetAnalyticsTool,
shortIoGetQrCodeTool,
shortIoListDomainsTool,
shortIoListLinksTool,
} from '@/tools/short_io'
import {
similarwebBounceRateTool,
similarwebPagesPerVisitTool,
@@ -2573,6 +2581,12 @@ export const tools: Record<string, ToolConfig> = {
tavily_extract: tavilyExtractTool,
tavily_crawl: tavilyCrawlTool,
tavily_map: tavilyMapTool,
short_io_create_link: shortIoCreateLinkTool,
short_io_list_domains: shortIoListDomainsTool,
short_io_list_links: shortIoListLinksTool,
short_io_delete_link: shortIoDeleteLinkTool,
short_io_get_qr_code: shortIoGetQrCodeTool,
short_io_get_analytics: shortIoGetAnalyticsTool,
supabase_query: supabaseQueryTool,
supabase_insert: supabaseInsertTool,
supabase_get_row: supabaseGetRowTool,

View File

@@ -0,0 +1,82 @@
import type { ShortIoCreateLinkParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const shortIoCreateLinkTool: ToolConfig<ShortIoCreateLinkParams, ToolResponse> = {
id: 'short_io_create_link',
name: 'Short.io Create Link',
description: 'Create a short link using your Short.io custom domain.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your registered Short.io custom domain',
},
originalURL: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The long URL to shorten',
},
path: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional custom path for the short link',
},
},
request: {
url: 'https://api.short.io/links',
method: 'POST',
headers: (params) => ({
Authorization: params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const bodyData: Record<string, string> = {
domain: params.domain,
originalURL: params.originalURL,
}
if (params.path) {
bodyData.path = params.path
}
return bodyData
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText)
return {
success: false,
output: { shortURL: '', idString: '', originalURL: '', path: null, createdAt: null },
error: `Failed to create short link: ${errorText}`,
}
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
shortURL: data.shortURL,
idString: data.idString,
originalURL: data.originalURL,
path: data.path ?? null,
createdAt: data.createdAt ?? null,
},
}
},
outputs: {
shortURL: { type: 'string', description: 'The generated short link URL' },
idString: { type: 'string', description: 'The unique Short.io link ID string' },
originalURL: { type: 'string', description: 'The original long URL' },
path: { type: 'string', description: 'The path/slug of the short link', optional: true },
createdAt: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true },
},
}

View File

@@ -0,0 +1,48 @@
import type { ShortIoDeleteLinkParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const shortIoDeleteLinkTool: ToolConfig<ShortIoDeleteLinkParams, ToolResponse> = {
id: 'short_io_delete_link',
name: 'Short.io Delete Link',
description: 'Delete a short link by ID (e.g. lnk_abc123_abcdef). Rate limit 20/s.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
linkId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Link ID to delete',
},
},
request: {
url: (params) => `https://api.short.io/links/${encodeURIComponent(params.linkId.trim())}`,
method: 'DELETE',
headers: (params) => ({
Authorization: params.apiKey,
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const err = await response.text().catch(() => response.statusText)
return { success: false, output: { deleted: false, idString: '' }, error: err }
}
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
deleted: data.success === true,
idString: data.idString ?? '',
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the link was deleted' },
idString: { type: 'string', description: 'Deleted link ID', optional: true },
},
}

View File

@@ -0,0 +1,112 @@
import type { ShortIoGetAnalyticsParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const STATS_PERIOD_MAP: Record<string, string> = {
today: 'today',
yesterday: 'yesterday',
last_7_days: 'last7',
last_30_days: 'last30',
all_time: 'total',
week: 'week',
month: 'month',
lastmonth: 'lastmonth',
}
export const shortIoGetAnalyticsTool: ToolConfig<ShortIoGetAnalyticsParams, ToolResponse> = {
id: 'short_io_get_analytics',
name: 'Short.io Get Link Statistics',
description:
'Fetch click statistics for a Short.io link (Statistics API: totalClicks, humanClicks, referer, country, etc.).',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
linkId: { type: 'string', required: true, visibility: 'user-or-llm', description: 'Link ID' },
period: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Period: today, yesterday, last7, last30, total, week, month, lastmonth',
},
tz: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Timezone (default UTC)',
},
},
request: {
url: (params) => {
const base = `https://statistics.short.io/statistics/link/${encodeURIComponent(params.linkId.trim())}`
const period = STATS_PERIOD_MAP[params.period] ?? params.period ?? 'last30'
const q = new URLSearchParams({ period })
if (params.tz) q.set('tz', params.tz)
return `${base}?${q.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: params.apiKey,
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const err = await response.text().catch(() => response.statusText)
return { success: false, output: { totalClicks: 0, humanClicks: 0 }, error: err }
}
const data = await response.json().catch(() => ({}))
const totalClicks = data.totalClicks ?? 0
const humanClicks = data.humanClicks ?? totalClicks
return {
success: true,
output: {
totalClicks,
humanClicks,
totalClicksChange: data.totalClicksChange ?? null,
humanClicksChange: data.humanClicksChange ?? null,
referer: data.referer ?? [],
country: data.country ?? [],
browser: data.browser ?? [],
os: data.os ?? [],
city: data.city ?? [],
device: data.device ?? [],
social: data.social ?? [],
utmMedium: data.utm_medium ?? [],
utmSource: data.utm_source ?? [],
utmCampaign: data.utm_campaign ?? [],
clickStatistics: data.clickStatistics ?? null,
interval: data.interval ?? null,
},
}
},
outputs: {
totalClicks: { type: 'number', description: 'Total clicks' },
humanClicks: { type: 'number', description: 'Human clicks' },
totalClicksChange: { type: 'string', description: 'Change vs previous period', optional: true },
humanClicksChange: { type: 'string', description: 'Human clicks change', optional: true },
referer: { type: 'array', description: 'Referrer breakdown (referer, score)' },
country: { type: 'array', description: 'Country breakdown (countryName, country, score)' },
browser: { type: 'array', description: 'Browser breakdown (browser, score)' },
os: { type: 'array', description: 'OS breakdown (os, score)' },
city: { type: 'array', description: 'City breakdown (city, name, countryCode, score)' },
device: { type: 'array', description: 'Device breakdown' },
social: { type: 'array', description: 'Social source breakdown (social, score)' },
utmMedium: { type: 'array', description: 'UTM medium breakdown' },
utmSource: { type: 'array', description: 'UTM source breakdown' },
utmCampaign: { type: 'array', description: 'UTM campaign breakdown' },
clickStatistics: {
type: 'object',
description: 'Time-series click data (datasets with x/y points per interval)',
optional: true,
},
interval: {
type: 'object',
description: 'Date range (startDate, endDate, prevStartDate, prevEndDate, tz)',
optional: true,
},
},
}

View File

@@ -0,0 +1,93 @@
import type { ShortIoGetQrParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const shortIoGetQrCodeTool: ToolConfig<ShortIoGetQrParams, ToolResponse> = {
id: 'short_io_get_qr_code',
name: 'Short.io Generate QR Code',
description: 'Generate a QR code for a Short.io link (POST /links/qr/{linkIdString}).',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
linkId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Link ID (e.g. lnk_abc123_abcdef)',
},
color: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'QR color hex (e.g. 000000)',
},
backgroundColor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Background color hex (e.g. FFFFFF)',
},
size: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'QR size 199',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Output format: png or svg',
},
useDomainSettings: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Use domain settings (default true)',
},
},
request: {
url: '/api/tools/short_io/qr',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
apiKey: params.apiKey,
linkId: params.linkId,
useDomainSettings: params.useDomainSettings ?? true,
}
if (params.color != null && params.color !== '') body.color = params.color
if (params.backgroundColor != null && params.backgroundColor !== '')
body.backgroundColor = params.backgroundColor
if (params.size != null && params.size >= 1 && params.size <= 99) body.size = params.size
if (params.type === 'svg' || params.type === 'png') body.type = params.type
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json().catch(() => ({}))
if (!response.ok || !data.success) {
return {
success: false,
output: {},
error: data.error || response.statusText,
}
}
return {
success: true,
output: data.output,
}
},
outputs: {
file: {
type: 'file',
description: 'Generated QR code image file',
},
},
}

View File

@@ -0,0 +1,7 @@
export { shortIoCreateLinkTool } from '@/tools/short_io/create_link'
export { shortIoDeleteLinkTool } from '@/tools/short_io/delete_link'
export { shortIoGetAnalyticsTool } from '@/tools/short_io/get_analytics'
export { shortIoGetQrCodeTool } from '@/tools/short_io/get_qr_code'
export { shortIoListDomainsTool } from '@/tools/short_io/list_domains'
export { shortIoListLinksTool } from '@/tools/short_io/list_links'
export * from './types'

View File

@@ -0,0 +1,41 @@
import type { ShortIoListDomainsParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const shortIoListDomainsTool: ToolConfig<ShortIoListDomainsParams, ToolResponse> = {
id: 'short_io_list_domains',
name: 'Short.io List Domains',
description: 'List Short.io domains. Returns domain IDs and details for use in List Links.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
},
request: {
url: 'https://api.short.io/api/domains',
method: 'GET',
headers: (params) => ({
Authorization: params.apiKey,
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const err = await response.text().catch(() => response.statusText)
return { success: false, output: { domains: [], count: 0 }, error: err }
}
const data = await response.json().catch(() => ({}))
const list = Array.isArray(data) ? data : (data.domains ?? data.list ?? [])
return {
success: true,
output: { domains: list, count: list.length },
}
},
outputs: {
domains: { type: 'array', description: 'List of domain objects (id, hostname, etc.)' },
count: { type: 'number', description: 'Number of domains' },
},
}

View File

@@ -0,0 +1,86 @@
import type { ShortIoListLinksParams } from '@/tools/short_io/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const shortIoListLinksTool: ToolConfig<ShortIoListLinksParams, ToolResponse> = {
id: 'short_io_list_links',
name: 'Short.io List Links',
description:
'List short links for a domain. Requires domain_id (from List Domains or dashboard). Max 150 per request.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Short.io Secret API Key',
},
domainId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'Domain ID (from List Domains)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Max links to return (1150)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token from previous response',
},
dateSortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort by date: asc or desc',
},
},
request: {
url: (params) => {
const u = new URL('https://api.short.io/api/links')
u.searchParams.set('domain_id', String(params.domainId))
if (params.limit != null && params.limit >= 1 && params.limit <= 150) {
u.searchParams.set('limit', String(params.limit))
}
if (params.pageToken) u.searchParams.set('pageToken', params.pageToken)
if (params.dateSortOrder === 'asc' || params.dateSortOrder === 'desc') {
u.searchParams.set('dateSortOrder', params.dateSortOrder)
}
return u.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: params.apiKey,
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const err = await response.text().catch(() => response.statusText)
return { success: false, output: { links: [], count: 0 }, error: err }
}
const data = await response.json().catch(() => ({}))
const links = data.links ?? []
const count = data.count ?? links.length
return {
success: true,
output: {
links,
count,
nextPageToken: data.nextPageToken ?? undefined,
},
}
},
outputs: {
links: {
type: 'array',
description: 'List of link objects (idString, shortURL, originalURL, path, etc.)',
},
count: { type: 'number', description: 'Number of links returned' },
nextPageToken: { type: 'string', description: 'Token for next page', optional: true },
},
}

View File

@@ -0,0 +1,40 @@
export interface ShortIoCreateLinkParams {
apiKey: string
domain: string
originalURL: string
path?: string
}
export interface ShortIoListDomainsParams {
apiKey: string
}
export interface ShortIoListLinksParams {
apiKey: string
domainId: number
limit?: number
pageToken?: string
dateSortOrder?: 'asc' | 'desc'
}
export interface ShortIoDeleteLinkParams {
apiKey: string
linkId: string
}
export interface ShortIoGetQrParams {
apiKey: string
linkId: string
color?: string
backgroundColor?: string
size?: number
type?: 'png' | 'svg'
useDomainSettings?: boolean
}
export interface ShortIoGetAnalyticsParams {
apiKey: string
linkId: string
period: string
tz?: string
}