Skip to content

Commit be8f442

Browse files
authored
feat(twilio-block): added twilio block/tools (#201)
* Add twilio block base * Edit fields * Add twilio tool * Fix Serialization of body in request. Add check for form-url-encoded. * Remove Test file * Remove execute in types
1 parent 6abd26b commit be8f442

File tree

8 files changed

+239
-3
lines changed

8 files changed

+239
-3
lines changed

sim/blocks/blocks/twilio.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { TwilioIcon } from '@/components/icons'
2+
import { BlockCategory, BlockConfig, BlockIcon } from '../types'
3+
import { TwilioSMSBlockOutput } from '@/tools/twilio/types'
4+
5+
export const TwilioSMSBlock: BlockConfig<TwilioSMSBlockOutput> = {
6+
type: 'twilio_sms',
7+
name: 'Twilio SMS',
8+
description: 'Send SMS messages via Twilio',
9+
longDescription:
10+
'Send text messages to single or multiple recipients using the Twilio API.',
11+
category: 'tools',
12+
bgColor: '#F22F46', // Twilio brand color
13+
icon: TwilioIcon,
14+
subBlocks: [
15+
{
16+
id: 'phoneNumbers',
17+
title: 'Recipient Phone Numbers',
18+
type: 'long-input',
19+
layout: 'full',
20+
placeholder: 'Enter phone numbers with country code (one per line, e.g., +1234567890)',
21+
},
22+
{
23+
id: 'message',
24+
title: 'Message',
25+
type: 'long-input',
26+
layout: 'full',
27+
placeholder: 'e.g. "Hello! This is a test message."',
28+
},
29+
{
30+
id: 'accountSid',
31+
title: 'Twilio Account SID',
32+
type: 'short-input',
33+
layout: 'full',
34+
placeholder: 'Your Twilio Account SID',
35+
},
36+
{
37+
id: 'authToken',
38+
title: 'Auth Token',
39+
type: 'short-input',
40+
layout: 'full',
41+
placeholder: 'Your Twilio Auth Token',
42+
password: true,
43+
},
44+
{
45+
id: 'fromNumber',
46+
title: 'From Twilio Phone Number',
47+
type: 'short-input',
48+
layout: 'full',
49+
placeholder: 'e.g. +1234567890',
50+
}
51+
],
52+
tools: {
53+
access: ['twilio_send_sms'],
54+
config: {
55+
tool: () => 'twilio_send_sms'
56+
},
57+
},
58+
inputs: {
59+
phoneNumbers: { type: 'string', required: true },
60+
message: { type: 'string', required: true },
61+
accountSid: { type: 'string', required: true },
62+
authToken: { type: 'string', required: true },
63+
fromNumber: { type: 'string', required: true }
64+
},
65+
outputs: {
66+
response: {
67+
type: {
68+
success: 'boolean',
69+
messageId: 'string',
70+
status: 'string',
71+
error: 'string'
72+
},
73+
},
74+
},
75+
}

sim/blocks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { XBlock } from './blocks/x'
3535
import { YouTubeBlock } from './blocks/youtube'
3636
import { AirtableBlock } from './blocks/airtable'
3737
import { BlockConfig } from './types'
38+
import { TwilioSMSBlock } from './blocks/twilio'
3839

3940
// Export blocks for ease of use
4041
export {
@@ -71,6 +72,7 @@ export {
7172
GoogleSheetsBlock,
7273
PerplexityBlock,
7374
ConfluenceBlock,
75+
TwilioSMSBlock,
7476
ImageGeneratorBlock,
7577
TypeformBlock,
7678
}
@@ -107,6 +109,7 @@ const blocks: Record<string, BlockConfig> = {
107109
supabase: SupabaseBlock,
108110
tavily: TavilyBlock,
109111
translate: TranslateBlock,
112+
twilio_sms: TwilioSMSBlock,
110113
typeform: TypeformBlock,
111114
vision: VisionBlock,
112115
whatsapp: WhatsAppBlock,

sim/components/icons.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,26 @@ export function ConfluenceIcon(props: SVGProps<SVGSVGElement>) {
17341734
)
17351735
}
17361736

1737+
export function TwilioIcon(props: SVGProps<SVGSVGElement>) {
1738+
return (
1739+
<svg
1740+
{...props}
1741+
xmlns="http://www.w3.org/2000/svg"
1742+
width="24"
1743+
height="24"
1744+
viewBox="0 0 256 256"
1745+
fill="none"
1746+
aria-hidden="true"
1747+
>
1748+
<circle cx="128" cy="128" r="128" fill="none" stroke="white" strokeWidth="21"/>
1749+
<circle cx="85" cy="85" r="21" fill="white"/>
1750+
<circle cx="171" cy="85" r="21" fill="white"/>
1751+
<circle cx="85" cy="171" r="21" fill="white"/>
1752+
<circle cx="171" cy="171" r="21" fill="white"/>
1753+
</svg>
1754+
)
1755+
}
1756+
17371757
export function ImageIcon(props: SVGProps<SVGSVGElement>) {
17381758
return (
17391759
<svg

sim/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { visionTool } from './vision/vision'
4444
import { whatsappSendMessageTool } from './whatsapp'
4545
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
4646
import { youtubeSearchTool } from './youtube/search'
47+
import { sendSMSTool } from './twilio/sendSMS'
4748
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable'
4849

4950
const logger = createLogger('Tools')
@@ -110,6 +111,7 @@ export const tools: Record<string, ToolConfig> = {
110111
confluence_retrieve: confluenceRetrieveTool,
111112
confluence_list: confluenceListTool,
112113
confluence_update: confluenceUpdateTool,
114+
twilio_send_sms: sendSMSTool,
113115
dalle_generate: dalleTool,
114116
airtable_read: airtableReadTool,
115117
airtable_write: airtableWriteTool,

sim/tools/twilio/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { sendSMSTool } from './sendSMS'
2+
3+
export const twilioSendSMSTool = sendSMSTool

sim/tools/twilio/sendSMS.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createLogger } from '@/lib/logs/console-logger'
2+
import { ToolConfig } from '../types'
3+
import { TwilioSMSBlockOutput, TwilioSendSMSParams } from './types'
4+
5+
const logger = createLogger('Twilio Send SMS Tool')
6+
7+
export const sendSMSTool: ToolConfig<TwilioSendSMSParams, TwilioSMSBlockOutput> = {
8+
id: 'twilio_send_sms',
9+
name: 'Twilio Send SMS',
10+
description: 'Send text messages to single or multiple recipients using the Twilio API.',
11+
version: '1.0.0',
12+
13+
params: {
14+
phoneNumbers: {
15+
type: 'string',
16+
required: true,
17+
description: 'Phone numbers to send the message to, separated by newlines'
18+
},
19+
message: {
20+
type: 'string',
21+
required: true,
22+
description: 'Message to send'
23+
},
24+
accountSid: {
25+
type: 'string',
26+
required: true,
27+
description: 'Twilio Account SID',
28+
requiredForToolCall: true
29+
},
30+
authToken: {
31+
type: 'string',
32+
required: true,
33+
description: 'Twilio Auth Token',
34+
requiredForToolCall: true
35+
},
36+
fromNumber: {
37+
type: 'string',
38+
required: true,
39+
description: 'Twilio phone number to send the message from'
40+
}
41+
},
42+
43+
request: {
44+
url: (params) => {
45+
if (!params.accountSid) {
46+
throw new Error('Twilio Account SID is required')
47+
}
48+
const url = `https://api.twilio.com/2010-04-01/Accounts/${params.accountSid}/Messages.json`;
49+
return url
50+
},
51+
method: 'POST',
52+
headers: (params) => {
53+
if (!params.accountSid || !params.authToken) {
54+
throw new Error('Twilio credentials are required')
55+
}
56+
// Use Buffer instead of btoa for Node.js compatibility
57+
const authToken = Buffer.from(`${params.accountSid}:${params.authToken}`).toString('base64')
58+
const headers = {
59+
Authorization: `Basic ${authToken}`,
60+
'Content-Type': 'application/x-www-form-urlencoded'
61+
}
62+
return headers
63+
},
64+
body: (params) => {
65+
if (!params.phoneNumbers) {
66+
throw new Error('Phone numbers are required but not provided')
67+
}
68+
if (!params.message) {
69+
throw new Error('Message content is required but not provided')
70+
}
71+
if (!params.fromNumber) {
72+
throw new Error('From number is required but not provided')
73+
}
74+
75+
// Get first phone number if multiple are provided
76+
const toNumber = params.phoneNumbers.split('\n')[0].trim()
77+
78+
// Create a URLSearchParams object and convert to string
79+
const formData = new URLSearchParams()
80+
formData.append('To', toNumber)
81+
formData.append('From', params.fromNumber)
82+
formData.append('Body', params.message)
83+
84+
const formDataString = formData.toString()
85+
return { body: formDataString }
86+
}
87+
},
88+
89+
transformResponse: async (response) => {
90+
const data = await response.json()
91+
92+
if (!response.ok) {
93+
const errorMessage = data.error?.message || data.message || `Failed to send SMS (HTTP ${response.status})`
94+
logger.error('Twilio API error:', data)
95+
throw new Error(errorMessage)
96+
}
97+
98+
logger.info('Twilio Response:', data)
99+
logger.info('Twilio Response type:', typeof data)
100+
return {
101+
success: true,
102+
output: {
103+
success: true,
104+
messageId: data.sid,
105+
status: data.status
106+
},
107+
error: undefined
108+
}
109+
},
110+
111+
transformError: (error) => {
112+
logger.error('Twilio tool error:', { error })
113+
return `SMS sending failed: ${error.message || 'Unknown error occurred'}`
114+
}
115+
}

sim/tools/twilio/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ToolResponse } from '../types'
2+
3+
export interface TwilioSendSMSParams {
4+
phoneNumbers: string
5+
message: string
6+
accountSid: string
7+
authToken: string
8+
fromNumber: string
9+
}
10+
11+
export interface TwilioSMSBlockOutput extends ToolResponse {
12+
output: {
13+
success: boolean
14+
messageId?: string
15+
status?: string
16+
error?: string
17+
}
18+
}

sim/tools/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ export function formatRequestParams(tool: ToolConfig, params: Record<string, any
4747
const hasBody = method !== 'GET' && method !== 'HEAD' && !!tool.request.body
4848
const bodyResult = tool.request.body ? tool.request.body(params) : undefined
4949

50-
// Special handling for NDJSON content type
51-
const isNDJSON = headers['Content-Type'] === 'application/x-ndjson'
50+
// Special handling for NDJSON content type or 'application/x-www-form-urlencoded'
51+
const isPreformattedContent = headers['Content-Type'] === 'application/x-ndjson' || headers['Content-Type'] === 'application/x-www-form-urlencoded'
5252
const body = hasBody
53-
? isNDJSON && bodyResult
53+
? isPreformattedContent && bodyResult
5454
? bodyResult.body
5555
: JSON.stringify(bodyResult)
5656
: undefined

0 commit comments

Comments
 (0)