feat(tools): added airtable integration for oauth2 and tools (#214)

* feat(airtable): add Airtable integration with OAuth support ref #102

* added scope descriptions for airtable oauth

---------

Co-authored-by: rodrick-mpofu <rodrickmpofu@gmail.com>
This commit is contained in:
Waleed Latif
2025-04-01 13:53:28 -07:00
committed by GitHub
co-authored by rodrick-mpofu
parent f835441c50
commit 7dde4191c7
14 changed files with 816 additions and 26 deletions
@@ -59,6 +59,8 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'tweet.write': 'Post tweets on your behalf',
'users.read': 'Read your profile information',
'offline.access': 'Access your account when you are not using the application',
'data.records:read': 'Read your records',
'data.records:write': 'Write to your records',
}
// Convert OAuth scope to user-friendly description
+89
View File
@@ -0,0 +1,89 @@
/**
* @vitest-environment jsdom
*
* Airtable Block Integration Tests
*/
import { describe, expect, test } from 'vitest'
import { AirtableBlock } from './airtable'
describe('Airtable Block', () => {
test('should have correct configuration', () => {
expect(AirtableBlock.type).toBe('airtable')
expect(AirtableBlock.category).toBe('tools')
expect(AirtableBlock.tools.access).toContain('airtable_read')
expect(AirtableBlock.tools.access).toContain('airtable_write')
expect(AirtableBlock.tools.access).toContain('airtable_update')
})
test('should select correct tool based on operation', () => {
const config = AirtableBlock.tools.config
if (!config) {
throw new Error('Tool config is missing')
}
expect(config.tool({ operation: 'read' })).toBe('airtable_read')
expect(config.tool({ operation: 'write' })).toBe('airtable_write')
expect(config.tool({ operation: 'update' })).toBe('airtable_update')
expect(() => config.tool({ operation: 'invalid' })).toThrow('Invalid Airtable operation')
})
test('should transform parameters correctly', () => {
const config = AirtableBlock.tools.config
if (!config || !config.params) {
throw new Error('Tool config or params is missing')
}
// Test read operation
const readParams = config.params({
operation: 'read',
credential: 'oauth_token',
baseId: 'base123',
tableId: 'table456',
maxRecords: '100',
filterFormula: "Status='Active'"
})
expect(readParams.accessToken).toBe('oauth_token')
expect(readParams.baseId).toBe('base123')
expect(readParams.tableId).toBe('table456')
// Test write operation
const writeParams = config.params({
operation: 'write',
credential: 'oauth_token',
baseId: 'base123',
tableId: 'table456',
records: JSON.stringify([{ fields: { Name: 'Test' } }])
})
expect(writeParams.accessToken).toBe('oauth_token')
expect(writeParams.records).toEqual([{ fields: { Name: 'Test' } }])
// Test update operation
const updateParams = config.params({
operation: 'update',
credential: 'oauth_token',
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
records: JSON.stringify({ fields: { Name: 'Updated' } })
})
expect(updateParams.accessToken).toBe('oauth_token')
expect(updateParams.recordId).toBe('rec789')
expect(updateParams.fields).toEqual({ fields: { Name: 'Updated' } })
})
test('should have required OAuth configuration', () => {
const subBlocks = AirtableBlock.subBlocks
const credentialBlock = subBlocks.find(block => block.id === 'credential')
expect(credentialBlock).toBeDefined()
expect(credentialBlock?.type).toBe('oauth-input')
expect(credentialBlock?.provider).toBe('airtable')
expect(credentialBlock?.requiredScopes).toContain('data.records:read')
expect(credentialBlock?.requiredScopes).toContain('data.records:write')
})
})
+174
View File
@@ -0,0 +1,174 @@
import { AirtableIcon } from '@/components/icons'
import {
AirtableReadResponse,
AirtableWriteResponse,
AirtableUpdateResponse,
} from '@/tools/airtable/types'
import { BlockConfig } from '../types'
type AirtableResponse = AirtableReadResponse | AirtableWriteResponse | AirtableUpdateResponse
export const AirtableBlock: BlockConfig<AirtableResponse> = {
type: 'airtable',
name: 'Airtable',
description: 'Read, write, and update Airtable records',
longDescription:
'Integrate Airtable functionality to manage table records. Read data from existing tables, ' +
'write new records, and update existing ones using OAuth authentication. Supports table ' +
'selection and record operations with custom field mapping.',
category: 'tools',
bgColor: '#FCB400',
icon: AirtableIcon,
subBlocks: [
// Operation selector
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Read Records', id: 'read' },
{ label: 'Write Records', id: 'write' },
{ label: 'Update Records', id: 'update' },
],
},
// Airtable Credentials
{
id: 'credential',
title: 'Airtable Account',
type: 'oauth-input',
layout: 'full',
provider: 'airtable',
serviceId: 'airtable',
requiredScopes: ['data.records:read', 'data.records:write'],
placeholder: 'Select Airtable account',
},
// Base ID
{
id: 'baseId',
title: 'Base ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter your base ID (found in the API documentation)',
},
// Table Name/ID
{
id: 'tableId',
title: 'Table Name/ID',
type: 'short-input',
layout: 'full',
placeholder: 'Enter table name or ID',
},
// Read Operation Fields
{
id: 'maxRecords',
title: 'Max Records',
type: 'short-input',
layout: 'half',
placeholder: 'Maximum number of records to return',
condition: { field: 'operation', value: 'read' },
},
{
id: 'filterFormula',
title: 'Filter Formula',
type: 'long-input',
layout: 'full',
placeholder: 'Enter Airtable formula to filter records',
condition: { field: 'operation', value: 'read' },
},
// Write Operation Fields
{
id: 'records',
title: 'Records',
type: 'code',
layout: 'full',
placeholder: 'Enter records in JSON format',
condition: { field: 'operation', value: 'write' },
},
// Update Operation Fields
{
id: 'records',
title: 'Record Fields',
type: 'code',
layout: 'full',
placeholder: 'Enter record fields in JSON format',
condition: { field: 'operation', value: 'update' },
},
{
id: 'recordId',
title: 'Record ID',
type: 'short-input',
layout: 'full',
placeholder: 'ID of the record to update',
condition: { field: 'operation', value: 'update' },
},
],
tools: {
access: ['airtable_read', 'airtable_write', 'airtable_update'],
config: {
tool: (params) => {
switch (params.operation) {
case 'read':
return 'airtable_read'
case 'write':
return 'airtable_write'
case 'update':
return 'airtable_update'
default:
throw new Error(`Invalid Airtable operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, records, ...rest } = params
if (params.operation === 'update' && records) {
const parsedRecords = JSON.parse(records)
return {
accessToken: credential,
fields: parsedRecords,
...rest,
}
}
return {
accessToken: credential,
records: records ? JSON.parse(records) : undefined,
...rest,
}
},
},
},
inputs: {
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
baseId: { type: 'string', required: true },
tableId: { type: 'string', required: true },
// Read operation inputs
maxRecords: { type: 'number', required: false },
filterFormula: { type: 'string', required: false },
// Write/Update operation inputs
records: {
type: 'json',
required: false,
schema: {
type: 'object',
properties: {
fields: {
type: 'object',
additionalProperties: true,
},
},
required: ['fields'],
},
},
recordId: { type: 'string', required: false },
},
outputs: {
response: {
type: {
records: 'json',
metadata: 'json',
},
},
},
}
+3
View File
@@ -33,11 +33,13 @@ import { VisionBlock } from './blocks/vision'
import { WhatsAppBlock } from './blocks/whatsapp'
import { XBlock } from './blocks/x'
import { YouTubeBlock } from './blocks/youtube'
import { AirtableBlock } from './blocks/airtable'
import { BlockConfig } from './types'
// Export blocks for ease of use
export {
AgentBlock,
AirtableBlock,
ApiBlock,
FileBlock,
FunctionBlock,
@@ -76,6 +78,7 @@ export {
// Registry of all block configurations, alphabetically sorted
const blocks: Record<string, BlockConfig> = {
agent: AgentBlock,
airtable: AirtableBlock,
api: ApiBlock,
condition: ConditionBlock,
confluence: ConfluenceBlock,
+11 -25
View File
@@ -1516,33 +1516,19 @@ export function RedditIcon(props: SVGProps<SVGSVGElement>) {
export function AirtableIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
width="800px"
height="800px"
viewBox="0 -20.5 256 256"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
>
<g>
<path
d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695"
fill="#FFBF00"
></path>
<path
d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863"
fill="#26B5F8"
></path>
<path
d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355"
fill="#ED3049"
></path>
<path
d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355"
fillOpacity="0.25"
fill="#000000"
></path>
</g>
<path d="M4 9.5V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5.5" />
<path d="M6.5 16.5v-4.25c0-.41.34-.75.75-.75h9.5c.41 0 .75.34.75.75v4.25" />
<path d="M3 16.5h18" />
<path d="M4 21h16a1 1 0 0 0 1-1v-3.5H3V20a1 1 0 0 0 1 1z" />
<rect x="7" y="8" width="10" height="3" rx="0.5" />
</svg>
)
}
+49
View File
@@ -412,6 +412,55 @@ export const auth = betterAuth({
}
},
},
// Airtable provider
{
providerId: 'airtable',
clientId: process.env.AIRTABLE_CLIENT_ID as string,
clientSecret: process.env.AIRTABLE_CLIENT_SECRET as string,
authorizationUrl: 'https://airtable.com/oauth/authorize',
tokenUrl: 'https://airtable.com/oauth/token',
userInfoUrl: 'https://api.airtable.com/v0/meta/whoami',
scopes: ['data.records:read', 'data.records:write'],
responseType: 'code',
pkce: true,
accessType: 'offline',
prompt: 'consent',
redirectURI: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/airtable`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://api.airtable.com/v0/meta/whoami', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Error fetching Airtable user info:', {
status: response.status,
statusText: response.statusText,
})
return null
}
const profile = await response.json()
const now = new Date()
return {
id: profile.id,
name: profile.name || 'Airtable User',
email: profile.email,
image: null,
emailVerified: true,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Airtable getUserInfo:', { error })
return null
}
},
},
],
}),
],
+22 -1
View File
@@ -10,13 +10,14 @@ import {
GoogleSheetsIcon,
SupabaseIcon,
xIcon,
AirtableIcon,
} from '@/components/icons'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('OAuth')
// Define the base OAuth provider type
export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | string
export type OAuthProvider = 'google' | 'github' | 'x' | 'supabase' | 'confluence' | 'airtable' | string
export type OAuthService =
| 'google'
| 'google-email'
@@ -27,6 +28,7 @@ export type OAuthService =
| 'x'
| 'supabase'
| 'confluence'
| 'airtable'
// Define the interface for OAuth provider configuration
export interface OAuthProviderConfig {
@@ -193,6 +195,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'confluence',
},
airtable: {
id: 'airtable',
name: 'Airtable',
icon: (props) => AirtableIcon(props),
services: {
airtable: {
id: 'airtable',
name: 'Airtable',
description: 'Manage Airtable bases, tables, and records.',
providerId: 'airtable',
icon: (props) => AirtableIcon(props),
baseProviderIcon: (props) => AirtableIcon(props),
scopes: ['data.records:read', 'data.records:write'],
},
},
defaultService: 'airtable',
},
}
// Helper function to get a service by provider and service ID
@@ -243,6 +262,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[]
return 'x'
} else if (provider === 'confluence') {
return 'confluence'
} else if (provider === 'airtable') {
return 'airtable'
}
return providerConfig.defaultService
+174
View File
@@ -0,0 +1,174 @@
/**
* @vitest-environment jsdom
*
* Airtable Tools Integration Tests
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { ToolTester } from '../__test-utils__/test-tools'
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './index'
describe('Airtable Tools Integration', () => {
let tester: ToolTester
beforeEach(() => {
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
})
afterEach(() => {
vi.resetAllMocks()
delete process.env.NEXT_PUBLIC_APP_URL
})
describe('Airtable Read Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableReadTool)
})
test('should construct correct read request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
maxRecords: 100,
filterFormula: "Status='Active'"
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
expect(url).toContain('/base123/table456')
expect(url).toContain('maxRecords=100')
expect(url).toContain('filterByFormula=Status%3D%27Active%27')
expect(headers['Authorization']).toBe('Bearer token789')
})
test('should handle successful read response', async () => {
const mockData = {
records: [
{ id: 'rec1', fields: { Name: 'Test 1' } },
{ id: 'rec2', fields: { Name: 'Test 2' } }
],
offset: 'next_page_token'
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789'
})
expect(result.success).toBe(true)
expect(result.output.records).toHaveLength(2)
expect(result.output.metadata.offset).toBe('next_page_token')
expect(result.output.metadata.totalRecords).toBe(2)
})
})
describe('Airtable Write Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableWriteTool)
})
test('should construct correct write request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
records: [{ fields: { Name: 'New Record' } }]
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
const body = tester.getRequestBody(params)
expect(url).toContain('/base123/table456')
expect(headers['Authorization']).toBe('Bearer token789')
expect(body).toEqual({ records: [{ fields: { Name: 'New Record' } }] })
})
test('should handle successful write response', async () => {
const mockData = {
records: [
{ id: 'rec1', fields: { Name: 'New Record' } }
]
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'token789',
records: [{ fields: { Name: 'New Record' } }]
})
expect(result.success).toBe(true)
expect(result.output.records).toHaveLength(1)
expect(result.output.metadata.recordCount).toBe(1)
})
})
describe('Airtable Update Tool', () => {
beforeEach(() => {
tester = new ToolTester(airtableUpdateTool)
})
test('should construct correct update request', () => {
const params = {
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
fields: { Name: 'Updated Record' }
}
const url = tester.getRequestUrl(params)
const headers = tester.getRequestHeaders(params)
const body = tester.getRequestBody(params)
expect(url).toContain('/base123/table456/rec789')
expect(headers['Authorization']).toBe('Bearer token789')
expect(body).toEqual({ fields: { Name: 'Updated Record' } })
})
test('should handle successful update response', async () => {
const mockData = {
id: 'rec789',
fields: { Name: 'Updated Record' }
}
tester.setup(mockData)
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
recordId: 'rec789',
accessToken: 'token789',
fields: { Name: 'Updated Record' }
})
expect(result.success).toBe(true)
expect(result.output.records).toHaveLength(1)
expect(result.output.metadata.recordCount).toBe(1)
expect(result.output.metadata.updatedFields).toContain('Name')
})
})
test('should handle error responses', async () => {
tester = new ToolTester(airtableReadTool)
const errorMessage = 'Invalid API key'
tester.setup({ error: errorMessage }, { ok: false, status: 401 })
const result = await tester.execute({
baseId: 'base123',
tableId: 'table456',
accessToken: 'invalid_token'
})
expect(result.success).toBe(false)
expect(result.error).toContain('Failed to read Airtable records')
})
})
+7
View File
@@ -0,0 +1,7 @@
import { readTool } from './read'
import { writeTool } from './write'
import { updateTool } from './update'
export const airtableReadTool = readTool
export const airtableWriteTool = writeTool
export const airtableUpdateTool = updateTool
+79
View File
@@ -0,0 +1,79 @@
import { ToolConfig } from '../types'
import { AirtableReadParams, AirtableReadResponse } from './types'
export const readTool: ToolConfig<AirtableReadParams, AirtableReadResponse> = {
id: 'airtable_read',
name: 'Airtable Read Records',
description: 'Read records from an Airtable table',
version: '1.0.0',
oauth: {
required: true,
provider: 'airtable',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
description: 'ID or name of the table',
},
maxRecords: {
type: 'number',
required: false,
description: 'Maximum number of records to return',
},
filterFormula: {
type: 'string',
required: false,
description: 'Formula to filter records',
},
},
request: {
url: (params) => {
const url = `https://api.airtable.com/v0/${params.baseId}/${params.tableId}`
const queryParams = new URLSearchParams()
if (params.maxRecords) queryParams.append('maxRecords', params.maxRecords.toString())
if (params.filterFormula) {
const encodedFormula = encodeURIComponent(params.filterFormula).replace(/'/g, '%27')
queryParams.append('filterByFormula', encodedFormula)
}
const queryString = queryParams.toString()
return queryString ? `${url}?${queryString}` : url
},
method: 'GET',
headers: (params) => ({
'Authorization': `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
records: data.records,
metadata: {
offset: data.offset,
totalRecords: data.records.length,
},
},
}
},
transformError: (error) => {
return `Failed to read Airtable records: ${error.message}`
},
}
+69
View File
@@ -0,0 +1,69 @@
import { ToolResponse } from '../types'
// Common interfaces
interface AirtableRecord {
id: string
createdTime: string
fields: Record<string, any>
}
interface AirtableError {
type: string
message: string
}
// Response interfaces
export interface AirtableReadResponse extends ToolResponse {
output: {
records: AirtableRecord[]
metadata: {
offset?: string
totalRecords?: number
}
}
}
export interface AirtableWriteResponse extends ToolResponse {
output: {
records: AirtableRecord[]
metadata: {
recordCount: number
}
}
}
export interface AirtableUpdateResponse extends ToolResponse {
output: {
records: AirtableRecord[]
metadata: {
recordCount: number
updatedFields: string[]
}
}
}
// Request interfaces
export interface AirtableReadParams {
accessToken: string
baseId: string
tableId: string
maxRecords?: number
filterFormula?: string
}
export interface AirtableWriteParams {
accessToken: string
baseId: string
tableId: string
records: Array<{
fields: Record<string, any>
}>
}
export interface AirtableUpdateParams {
accessToken: string
baseId: string
tableId: string
recordId: string
fields: Record<string, any>
}
+68
View File
@@ -0,0 +1,68 @@
import { ToolConfig } from '../types'
import { AirtableUpdateParams, AirtableUpdateResponse } from './types'
export const updateTool: ToolConfig<AirtableUpdateParams, AirtableUpdateResponse> = {
id: 'airtable_update',
name: 'Airtable Update Records',
description: 'Update existing records in an Airtable table',
version: '1.0.0',
provider: 'airtable',
params: {
accessToken: {
type: 'string',
required: true,
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
description: 'ID or name of the table',
},
recordId: {
type: 'string',
required: true,
description: 'ID of the record to update',
},
fields: {
type: 'json',
required: true,
description: 'Fields to update',
},
},
request: {
url: (params) =>
`https://api.airtable.com/v0/${params.baseId}/${params.tableId}/${params.recordId}`,
method: 'PATCH',
headers: (params) => ({
'Authorization': `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({ fields: params.fields }),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
records: [data],
metadata: {
recordCount: 1,
updatedFields: Object.keys(data.fields),
},
},
}
},
transformError: (error) => {
return `Failed to update Airtable record: ${error.message}`
},
}
+65
View File
@@ -0,0 +1,65 @@
import { ToolConfig } from '../types'
import { AirtableWriteParams, AirtableWriteResponse } from './types'
export const writeTool: ToolConfig<AirtableWriteParams, AirtableWriteResponse> = {
id: 'airtable_write',
name: 'Airtable Write Records',
description: 'Write new records to an Airtable table',
version: '1.0.0',
oauth: {
required: true,
provider: 'airtable',
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'OAuth access token',
},
baseId: {
type: 'string',
required: true,
description: 'ID of the Airtable base',
},
tableId: {
type: 'string',
required: true,
description: 'ID or name of the table',
},
records: {
type: 'json',
required: true,
description: 'Array of records to create',
},
},
request: {
url: (params) =>
`https://api.airtable.com/v0/${params.baseId}/${params.tableId}`,
method: 'POST',
headers: (params) => ({
'Authorization': `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({ records: params.records }),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
records: data.records,
metadata: {
recordCount: data.records.length,
},
},
}
},
transformError: (error) => {
return `Failed to write Airtable records: ${error.message}`
},
}
+4
View File
@@ -44,6 +44,7 @@ import { visionTool } from './vision/vision'
import { whatsappSendMessageTool } from './whatsapp'
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
import { youtubeSearchTool } from './youtube/search'
import { airtableReadTool, airtableWriteTool, airtableUpdateTool } from './airtable'
const logger = createLogger('Tools')
@@ -110,6 +111,9 @@ export const tools: Record<string, ToolConfig> = {
confluence_list: confluenceListTool,
confluence_update: confluenceUpdateTool,
dalle_generate: dalleTool,
airtable_read: airtableReadTool,
airtable_write: airtableWriteTool,
airtable_update: airtableUpdateTool,
}
// Get a tool by its ID