feat: add @agentic/airtable Airtable package

old-agentic
Travis Fischer 2025-04-10 00:34:03 +07:00
rodzic 50f0626ac6
commit 1a4ef44f2b
16 zmienionych plików z 1247 dodań i 1 usunięć

Wyświetl plik

@ -55,6 +55,7 @@
{
"group": "Tools",
"pages": [
"tools/airtable",
"tools/apollo",
"tools/arxiv",
"tools/bing",

Wyświetl plik

@ -0,0 +1,48 @@
---
title: Airtable
description: Airtable is a no-code spreadsheets, CRM, and database.
---
- package: `@agentic/airtable`
- exports: `class AirtableClient`, `namespace airtable`
- env vars: `AIRTABLE_API_KEY`
- [source](https://github.com/transitive-bullshit/agentic/blob/main/packages/airtable/src/airtable-client.ts)
- [airtable api docs](https://airtable.com/developers/web/api/introduction)
## Install
<CodeGroup>
```bash npm
npm install @agentic/airtable
```
```bash yarn
yarn add @agentic/airtable
```
```bash pnpm
pnpm add @agentic/airtable
```
</CodeGroup>
## Usage
```ts
import { AirtableClient } from '@agentic/airtable'
const airtable = new AirtableClient()
const { bases } = await airtable.listBases()
console.log('bases', tables)
const baseId = bases[0]!.id
const tables = await airtable.listTables({ baseId })
console.log('tables', tables)
const searchResults = await airtable.searchRecords({
baseId,
tableId: tables[0]!.id,
searchTerm: 'Travis'
})
console.log('search results', searchResults)
```

Wyświetl plik

@ -0,0 +1,45 @@
{
"name": "@agentic/airtable",
"version": "7.6.3",
"description": "Agentic SDK for Airtable.",
"author": "Travis Fischer <travis@transitivebullsh.it>",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/transitive-bullshit/agentic.git",
"directory": "packages/airtable"
},
"type": "module",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"clean": "del dist",
"test": "run-s test:*",
"test:lint": "eslint .",
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/core": "workspace:*",
"ky": "catalog:",
"p-throttle": "catalog:"
},
"peerDependencies": {
"zod": "catalog:"
},
"publishConfig": {
"access": "public"
}
}

Wyświetl plik

@ -0,0 +1,24 @@
<p align="center">
<a href="https://agentic.so">
<img alt="Agentic" src="https://raw.githubusercontent.com/transitive-bullshit/agentic/main/docs/media/agentic-header.jpg" width="308">
</a>
</p>
<p align="center">
<em>AI agent stdlib that works with any LLM and TypeScript AI SDK.</em>
</p>
<p align="center">
<a href="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml"><img alt="Build Status" src="https://github.com/transitive-bullshit/agentic/actions/workflows/main.yml/badge.svg" /></a>
<a href="https://www.npmjs.com/package/@agentic/stdlib"><img alt="NPM" src="https://img.shields.io/npm/v/@agentic/stdlib.svg" /></a>
<a href="https://github.com/transitive-bullshit/agentic/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
<a href="https://prettier.io"><img alt="Prettier Code Formatting" src="https://img.shields.io/badge/code_style-prettier-brightgreen.svg" /></a>
</p>
# Agentic
**See the [github repo](https://github.com/transitive-bullshit/agentic) or [docs](https://agentic.so) for more info.**
## License
MIT © [Travis Fischer](https://x.com/transitive_bs)

Wyświetl plik

@ -0,0 +1,423 @@
import {
aiFunction,
AIFunctionsProvider,
assert,
getEnv,
sanitizeSearchParams
} from '@agentic/core'
import defaultKy, { type KyInstance } from 'ky'
import { z } from 'zod'
import { airtable } from './airtable'
/**
* Airtable API client.
*
* @see https://airtable.com/developers/web/api/introduction
*/
export class AirtableClient extends AIFunctionsProvider {
protected readonly ky: KyInstance
protected readonly apiKey: string
protected readonly apiBaseUrl: string
constructor({
apiKey = getEnv('AIRTABLE_API_KEY'),
apiBaseUrl = airtable.API_BASE_URL,
timeoutMs = 60_000,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
timeoutMs?: number
ky?: KyInstance
} = {}) {
assert(
apiKey,
`AirtableClient missing required "username" (defaults to "AIRTABLE_API_KEY")`
)
super()
this.apiKey = apiKey
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: apiBaseUrl,
timeout: timeoutMs,
headers: {
Authorization: `Bearer ${apiKey}`
}
})
}
/**
* Lists all of the bases that the user has access to.
*/
@aiFunction({
name: 'airtable_list_bases',
description: 'Lists all accessible Airtable bases.',
inputSchema: z.object({})
})
async listBases(): Promise<airtable.ListBasesResponse> {
return this.ky.get('v0/meta/bases').json<airtable.ListBasesResponse>()
}
/**
* Lists all of the tables in a base.
*/
@aiFunction({
name: 'airtable_list_tables',
description: 'Lists all of the tables in a base.',
inputSchema: airtable.ListTablesArgsSchema
})
async listTables<
TDetailLevel extends airtable.TableDetailLevel = 'full'
>(args: {
baseId: string
detailLevel?: TDetailLevel
}): Promise<Array<airtable.AirtableTableToDetailLevel<TDetailLevel>>> {
const { baseId, detailLevel = 'full' } = args
const res = await this.ky
.get(`/v0/meta/bases/${baseId}/tables`)
.json<airtable.BaseSchemaResponse>()
return res.tables.map((table) =>
transformTableDetailLevel<TDetailLevel>({
table,
detailLevel: detailLevel as TDetailLevel
})
)
}
/**
* Gets a single table's schema in a base.
*/
@aiFunction({
name: 'airtable_get_table',
description: "Gets a single table's schema in a base.",
inputSchema: airtable.DescribeTableArgsSchema
})
async getTable<
TDetailLevel extends airtable.TableDetailLevel = 'full'
>(args: {
baseId: string
tableId: string
detailLevel?: TDetailLevel
}): Promise<airtable.AirtableTableToDetailLevel<TDetailLevel>> {
const tables = await this.listTables<TDetailLevel>(args)
const table = tables.find((t) => t.id === args.tableId)
assert(table, `Table ${args.tableId} not found in base ${args.baseId}`)
return table
}
/**
* Lists records from a table.
*/
@aiFunction({
name: 'airtable_list_records',
description: 'Lists records from a table.',
inputSchema: airtable.ListRecordsArgsSchema
})
async listRecords(
args: airtable.ListRecordsArgs
): Promise<airtable.AirtableRecord[]> {
const { baseId, tableId, ...options } = args
return this.ky
.get(`/v0/${baseId}/${tableId}`, {
searchParams: sanitizeSearchParams(options)
})
.json<airtable.AirtableRecord[]>()
}
/**
* Lists all records from a table.
*/
@aiFunction({
name: 'airtable_list_all_records',
description: 'Lists all records from a table.',
inputSchema: airtable.ListRecordsArgsSchema
})
async listAllRecords(
args: airtable.ListRecordsArgs
): Promise<airtable.AirtableRecord[]> {
const allRecords: airtable.AirtableRecord[] = []
let offset = args.offset ?? 0
do {
const res = await this.listRecords({
...args,
offset
})
if (!res.length) {
break
}
allRecords.push(...res)
offset += res.length
} while (true)
return allRecords
}
/**
* Gets a single record from a table.
*/
@aiFunction({
name: 'airtable_get_record',
description: 'Gets a single record from a table.',
inputSchema: airtable.GetRecordArgsSchema
})
async getRecord(
args: airtable.GetRecordArgs
): Promise<airtable.AirtableRecord> {
const { baseId, tableId, recordId } = args
return this.ky
.get(`/v0/${baseId}/${tableId}/${recordId}`)
.json<airtable.AirtableRecord>()
}
/**
* Creates a record in a table.
*/
@aiFunction({
name: 'airtable_create_record',
description: 'Creates a record in a table.',
inputSchema: airtable.CreateRecordArgsSchema
})
async createRecord(
args: airtable.CreateRecordArgs
): Promise<airtable.AirtableRecord> {
const { baseId, tableId, ...body } = args
return this.ky
.post(`/v0/${baseId}/${tableId}`, {
json: body
})
.json<airtable.AirtableRecord>()
}
/**
* Updates records in a table.
*/
@aiFunction({
name: 'airtable_update_records',
description: 'Updates records in a table.',
inputSchema: airtable.UpdateRecordsArgsSchema
})
async updateRecords(
args: airtable.UpdateRecordsArgs
): Promise<airtable.AirtableRecord[]> {
const { baseId, tableId, ...body } = args
return this.ky
.patch(`/v0/${baseId}/${tableId}`, {
json: body
})
.json<airtable.AirtableRecord[]>()
}
/**
* Deletes records from a table.
*/
@aiFunction({
name: 'airtable_delete_records',
description: 'Deletes records from a table.',
inputSchema: airtable.DeleteRecordsArgsSchema
})
async deleteRecords(
args: airtable.DeleteRecordsArgs
): Promise<{ id: string }[]> {
const { baseId, tableId, recordIds } = args
const queryString = recordIds.map((id) => `records[]=${id}`).join('&')
const res = await this.ky
.delete(`/v0/${baseId}/${tableId}?${queryString}`)
.json<{ records: { id: string; deleted: boolean }[] }>()
return res.records.map(({ id }) => ({ id }))
}
/**
* Creates a table in a base.
*/
@aiFunction({
name: 'airtable_create_table',
description: 'Creates a table in a base.',
inputSchema: airtable.CreateTableArgsSchema
})
async createTable(args: airtable.CreateTableArgs): Promise<airtable.Table> {
const { baseId, ...body } = args
return this.ky
.post(`/v0/meta/bases/${baseId}/tables`, {
json: body
})
.json<airtable.Table>()
}
/**
* Updates a table in a base.
*/
@aiFunction({
name: 'airtable_update_table',
description: 'Updates a table in a base.',
inputSchema: airtable.UpdateTableArgsSchema
})
async updateTable(args: airtable.UpdateTableArgs): Promise<airtable.Table> {
const { baseId, tableId, ...body } = args
return this.ky
.patch(`/v0/meta/bases/${baseId}/tables/${tableId}`, {
json: body
})
.json<airtable.Table>()
}
/**
* Creates a field in a table.
*/
@aiFunction({
name: 'airtable_create_field',
description: 'Creates a field in a table.',
inputSchema: airtable.CreateFieldArgsSchema
})
async createField(args: airtable.CreateFieldArgs): Promise<airtable.Field> {
const { baseId, tableId, body } = args
return this.ky
.post(`/v0/meta/bases/${baseId}/tables/${tableId}/fields`, {
json: body.field
})
.json<airtable.Field>()
}
/**
* Updates a field in a table.
*/
@aiFunction({
name: 'airtable_update_field',
description: 'Updates a field in a table.',
inputSchema: airtable.UpdateFieldArgsSchema
})
async updateField(args: airtable.UpdateFieldArgs): Promise<airtable.Field> {
const { baseId, tableId, fieldId, ...body } = args
return this.ky
.patch(`/v0/meta/bases/${baseId}/tables/${tableId}/fields/${fieldId}`, {
json: body
})
.json<airtable.Field>()
}
/**
* Searches for records in a table which contain specific text.
*/
@aiFunction({
name: 'airtable_search_records',
description: 'Searches for records in a table which contain specific text.',
inputSchema: airtable.SearchRecordsArgsSchema
})
async searchRecords(
args: airtable.SearchRecordsArgs
): Promise<airtable.AirtableRecord[]> {
const { baseId, tableId, fieldIds, searchTerm, ...opts } = args
// Validate and get search fields
const searchFieldIds = await this.validateAndGetSearchFields({
baseId,
tableId,
fieldIds
})
// Escape the search term to prevent formula injection
const escapedSearchTerm = searchTerm.replaceAll(/["\\]/g, '\\$&')
// Build OR(FIND("term", field1), FIND("term", field2), ...)
const filterByFormula = `OR(${searchFieldIds
.map((fieldId) => `FIND("${escapedSearchTerm}", {${fieldId}})`)
.join(',')})`
return this.listRecords({ ...opts, baseId, tableId, filterByFormula })
}
/**
* Validates and gets the searchable text fields in a table.
*/
protected async validateAndGetSearchFields({
baseId,
tableId,
fieldIds
}: {
baseId: string
tableId: string
fieldIds?: string[]
}): Promise<string[]> {
const table = await this.getTable({ baseId, tableId })
const searchableFieldTypes = new Set([
'singleLineText',
'multilineText',
'richText',
'email',
'url',
'phoneNumber'
])
const searchableFieldIds = new Set(
table.fields
.filter((field) => searchableFieldTypes.has(field.type))
.map((field) => field.id)
)
if (!searchableFieldIds.size) {
throw new Error('No text fields available to search')
}
// If specific fields were requested, validate that they exist and are, in
// fact, valid searchable text fields.
if (fieldIds && fieldIds.length > 0) {
// Check if any requested fields were invalid
const invalidFieldIds = fieldIds.filter(
(fieldId) => !searchableFieldIds.has(fieldId)
)
if (invalidFieldIds.length > 0) {
throw new Error(
`Invalid fields requested: ${invalidFieldIds.join(', ')}`
)
}
return fieldIds
}
return Array.from(searchableFieldIds)
}
}
function transformTableDetailLevel<
T extends airtable.TableDetailLevel = 'full'
>({
table,
detailLevel
}: {
table: airtable.Table
detailLevel: T
}): airtable.AirtableTableToDetailLevel<T> {
switch (detailLevel) {
case 'tableIdentifiersOnly':
return {
id: table.id,
name: table.name
} as any
case 'identifiersOnly':
return {
id: table.id,
name: table.name,
fields: table.fields.map((field) => ({
id: field.id,
name: field.name
})),
views: table.views.map((view) => ({
id: view.id,
name: view.name
}))
} as any
default:
return table as any
}
}

Wyświetl plik

@ -0,0 +1,665 @@
import { z } from 'zod'
export namespace airtable {
export const API_BASE_URL = 'https://api.airtable.com'
export const BaseSchema = z.object({
id: z.string(),
name: z.string(),
permissionLevel: z.string()
})
export type Base = z.infer<typeof BaseSchema>
export const ListBasesResponseSchema = z.object({
bases: z.array(BaseSchema),
offset: z.string().optional()
})
export type ListBasesResponse = z.infer<typeof ListBasesResponseSchema>
export const FieldOptionsSchema = z
.object({
isReversed: z.boolean().optional(),
inverseLinkFieldId: z.string().optional(),
linkedTableId: z.string().optional(),
prefersSingleRecordLink: z.boolean().optional(),
color: z.string().optional(),
icon: z.string().optional()
})
.passthrough()
export type FieldOptions = z.infer<typeof FieldOptionsSchema>
export const FieldSchema = z
.object({
name: z.string(),
description: z.string().optional()
})
.and(
// Extracted from Airtable API docs
z.union([
z.object({ type: z.literal('autoNumber') }),
z.object({ type: z.literal('barcode') }),
z.object({ type: z.literal('button') }),
z
.object({
options: z.object({
color: z
.enum([
'greenBright',
'tealBright',
'cyanBright',
'blueBright',
'purpleBright',
'pinkBright',
'redBright',
'orangeBright',
'yellowBright',
'grayBright'
])
.describe('The color of the checkbox.'),
icon: z
.enum([
'check',
'xCheckbox',
'star',
'heart',
'thumbsUp',
'flag',
'dot'
])
.describe('The icon name of the checkbox.')
}),
type: z.literal('checkbox')
})
.describe(
"Bases on a free or plus plan are limited to using the `'check'` icon and `'greenBright'` color."
),
z.object({ type: z.literal('createdBy') }),
z.object({
options: z.object({
result: z
.union([
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'`format` is always provided when reading.\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
),
name: z.enum([
'local',
'friendly',
'us',
'european',
'iso'
])
})
}),
type: z.literal('date')
}),
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'`format` is always provided when reading.\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
),
name: z.enum([
'local',
'friendly',
'us',
'european',
'iso'
])
}),
timeFormat: z.object({
format: z.enum(['h:mma', 'HH:mm']),
name: z.enum(['12hour', '24hour'])
}),
timeZone: z.any()
}),
type: z.literal('dateTime')
})
])
.describe(
'This will always be a `date` or `dateTime` field config.'
)
.optional()
}),
type: z.literal('createdTime')
}),
z.object({
options: z.object({
isValid: z
.boolean()
.describe(
'`false` when recordLinkFieldId is null, e.g. the referenced column was deleted.'
),
recordLinkFieldId: z.union([z.string(), z.null()]).optional()
}),
type: z.literal('count')
}),
z.any(),
z.object({
options: z.object({
isValid: z
.boolean()
.describe(
'False if this formula/field configuation has an error'
),
referencedFieldIds: z
.union([z.array(z.string()), z.null()])
.describe('The fields to check the last modified time of'),
result: z
.union([
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'`format` is always provided when reading.\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
),
name: z.enum([
'local',
'friendly',
'us',
'european',
'iso'
])
})
}),
type: z.literal('date')
}),
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'`format` is always provided when reading.\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
),
name: z.enum([
'local',
'friendly',
'us',
'european',
'iso'
])
}),
timeFormat: z.object({
format: z.enum(['h:mma', 'HH:mm']),
name: z.enum(['12hour', '24hour'])
}),
timeZone: z.any()
}),
type: z.literal('dateTime')
}),
z.null()
])
.describe(
'This will always be a `date` or `dateTime` field config.'
)
}),
type: z.literal('lastModifiedTime')
}),
z.object({ type: z.literal('lastModifiedBy') }),
z.object({
options: z.object({
fieldIdInLinkedTable: z
.union([z.string(), z.null()])
.describe(
'The field in the linked table that this field is looking up.'
),
isValid: z
.boolean()
.describe(
'Is the field currently valid (e.g. false if the linked record field has\nbeen deleted)'
),
recordLinkFieldId: z
.union([z.string(), z.null()])
.describe('The linked record field in the current table.'),
result: z
.union([z.any(), z.null()])
.describe(
'The field type and options inside of the linked table. See other field\ntype configs on this page for the possible values. Can be null if invalid.'
)
}),
type: z.literal('lookup')
}),
z.object({
options: z.object({
precision: z
.number()
.describe(
'Indicates the number of digits shown to the right of the decimal point for this field. (0-8 inclusive)'
)
}),
type: z.literal('number')
}),
z.object({
options: z.object({
precision: z
.number()
.describe(
'Indicates the number of digits shown to the right of the decimal point for this field. (0-8 inclusive)'
)
}),
type: z.literal('percent')
}),
z.object({
options: z.object({
precision: z
.number()
.describe(
'Indicates the number of digits shown to the right of the decimal point for this field. (0-7 inclusive)'
),
symbol: z.string().describe('Currency symbol to use.')
}),
type: z.literal('currency')
}),
z.object({
options: z.object({
durationFormat: z.enum([
'h:mm',
'h:mm:ss',
'h:mm:ss.S',
'h:mm:ss.SS',
'h:mm:ss.SSS'
])
}),
type: z.literal('duration')
}),
z.object({ type: z.literal('multilineText') }),
z.object({ type: z.literal('phoneNumber') }),
z
.object({
options: z.object({
color: z
.enum([
'yellowBright',
'orangeBright',
'redBright',
'pinkBright',
'purpleBright',
'blueBright',
'cyanBright',
'tealBright',
'greenBright',
'grayBright'
])
.describe('The color of selected icons.'),
icon: z
.enum(['star', 'heart', 'thumbsUp', 'flag', 'dot'])
.describe('The icon name used to display the rating.'),
max: z
.number()
.describe(
'The maximum value for the rating, from 1 to 10 inclusive.'
)
}),
type: z.literal('rating')
})
.describe(
"Bases on a free or plus plan are limited to using the 'star' icon and 'yellowBright' color."
),
z.object({ type: z.literal('richText') }),
z.object({
options: z.object({
fieldIdInLinkedTable: z
.string()
.describe('The id of the field in the linked table')
.optional(),
isValid: z.boolean().optional(),
recordLinkFieldId: z
.string()
.describe('The linked field id')
.optional(),
referencedFieldIds: z
.array(z.string())
.describe(
'The ids of any fields referenced in the rollup formula'
)
.optional(),
result: z
.union([z.any(), z.null()])
.describe(
'The resulting field type and options for the rollup. See other field\ntype configs on this page for the possible values. Can be null if invalid.'
)
.optional()
}),
type: z.literal('rollup')
}),
z.object({ type: z.literal('singleLineText') }),
z.object({ type: z.literal('email') }),
z.object({ type: z.literal('url') }),
z.object({
options: z.object({
choices: z.array(
z.object({
color: z
.string()
.describe(
'Optional when the select field is configured to not use colors.\n\nAllowed values: "blueLight2", "cyanLight2", "tealLight2", "greenLight2", "yellowLight2", "orangeLight2", "redLight2", "pinkLight2", "purpleLight2", "grayLight2", "blueLight1", "cyanLight1", "tealLight1", "greenLight1", "yellowLight1", "orangeLight1", "redLight1", "pinkLight1", "purpleLight1", "grayLight1", "blueBright", "cyanBright", "tealBright", "greenBright", "yellowBright", "orangeBright", "redBright", "pinkBright", "purpleBright", "grayBright", "blueDark1", "cyanDark1", "tealDark1", "greenDark1", "yellowDark1", "orangeDark1", "redDark1", "pinkDark1", "purpleDark1", "grayDark1"'
)
.optional(),
id: z.string(),
name: z.string()
})
)
}),
type: z.literal('externalSyncSource')
}),
z.object({
options: z.object({
prompt: z
.array(
z.union([
z.string(),
z.object({ field: z.object({ fieldId: z.string() }) })
])
)
.describe(
'The prompt that is used to generate the results in the AI field, additional object\ntypes may be added in the future. Currently, this is an array of strings or objects that identify any fields interpolated into the prompt.\n\nThe prompt will not currently be provided if this field config is within another\nfields configuration (like a lookup field)'
)
.optional(),
referencedFieldIds: z
.array(z.string())
.describe(
'The other fields in the record that are used in the ai field\n\nThe referencedFieldIds will not currently be provided if this field config is within another\nfields configuration (like a lookup field)'
)
.optional()
}),
type: z.literal('aiText')
}),
z
.object({
options: z.object({
linkedTableId: z
.string()
.describe('The ID of the table this field links to'),
viewIdForRecordSelection: z
.string()
.describe(
'The ID of the view in the linked table\nto use when showing a list of records to select from'
)
.optional()
}),
type: z.literal('multipleRecordLinks')
})
.describe(
'Creating "multipleRecordLinks" fields is supported but updating options for\nexisting "multipleRecordLinks" fields is not supported.'
),
z.object({
options: z.object({
choices: z.array(
z.object({
color: z
.string()
.describe('Optional when creating an option.')
.optional(),
id: z
.string()
.describe(
'This is not specified when creating new options, useful when specifing existing\noptions (for example: reordering options, keeping old options and adding new ones, etc)'
)
.optional(),
name: z.string()
})
)
}),
type: z.literal('singleSelect')
}),
z.object({
options: z.object({
choices: z.array(
z.object({
color: z
.string()
.describe('Optional when creating an option.')
.optional(),
id: z
.string()
.describe(
'This is not specified when creating new options, useful when specifing existing\noptions (for example: reordering options, keeping old options and adding new ones, etc)'
)
.optional(),
name: z.string()
})
)
}),
type: z.literal('multipleSelects')
}),
z.object({
options: z.record(z.any()).optional(),
type: z.literal('singleCollaborator')
}),
z.object({
options: z.record(z.any()).optional(),
type: z.literal('multipleCollaborators')
}),
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'Format is optional when writing, but it must match\nthe corresponding name if provided.\n\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
)
.optional(),
name: z.enum(['local', 'friendly', 'us', 'european', 'iso'])
})
}),
type: z.literal('date')
}),
z.object({
options: z.object({
dateFormat: z.object({
format: z
.enum(['l', 'LL', 'M/D/YYYY', 'D/M/YYYY', 'YYYY-MM-DD'])
.describe(
'Format is optional when writing, but it must match\nthe corresponding name if provided.\n\n(`l` for local, `LL` for friendly, `M/D/YYYY` for us, `D/M/YYYY` for european, `YYYY-MM-DD` for iso)'
)
.optional(),
name: z.enum(['local', 'friendly', 'us', 'european', 'iso'])
}),
timeFormat: z.object({
format: z.enum(['h:mma', 'HH:mm']).optional(),
name: z.enum(['12hour', '24hour'])
}),
timeZone: z.any()
}),
type: z.literal('dateTime')
}),
z.object({
options: z.object({ isReversed: z.boolean() }).optional(),
type: z.literal('multipleAttachments')
})
])
)
.describe(
'The config of a field. NB: Formula fields cannot be created with this MCP due to a limitation in the Airtable API.'
)
export type Field = z.infer<typeof FieldSchema> & { id: string }
export const ViewSchema = z.object({
id: z.string(),
name: z.string(),
type: z.string()
})
export type View = z.infer<typeof ViewSchema>
export const TableSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
primaryFieldId: z.string(),
fields: z.array(FieldSchema.and(z.object({ id: z.string() }))),
views: z.array(ViewSchema)
})
export type Table = z.infer<typeof TableSchema>
export const BaseSchemaResponseSchema = z.object({
tables: z.array(TableSchema)
})
export type BaseSchemaResponse = z.infer<typeof BaseSchemaResponseSchema>
// Zod schemas for tool arguments
export const ListRecordsArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
offset: z
.number()
.optional()
.describe('Offset to start the list from. Defaults to 0.'),
maxRecords: z
.number()
.optional()
.describe('Maximum number of records to return. Defaults to 100.'),
filterByFormula: z
.string()
.optional()
.describe('Airtable formula to filter records')
})
export type ListRecordsArgs = z.infer<typeof ListRecordsArgsSchema>
export const SearchRecordsArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
searchTerm: z.string().describe('Text to search for in records'),
fieldIds: z
.array(z.string())
.optional()
.describe(
'Specific field ids to search in. If not provided, searches all text-based fields.'
),
maxRecords: z
.number()
.optional()
.describe('Maximum number of records to return. Defaults to 100.')
})
export type SearchRecordsArgs = z.infer<typeof SearchRecordsArgsSchema>
export const TableDetailLevelSchema = z.enum([
'tableIdentifiersOnly',
'identifiersOnly',
'full'
]).describe(`Detail level for table information:
- tableIdentifiersOnly: table IDs and names
- identifiersOnly: table, field, and view IDs and names
- full: complete details including field types, descriptions, and configurations
Note for LLMs: To optimize context window usage, request the minimum detail level needed:
- Use 'tableIdentifiersOnly' when you only need to list or reference tables
- Use 'identifiersOnly' when you need to work with field or view references
- Only use 'full' when you need field types, descriptions, or other detailed configuration
If you only need detailed information on a few tables in a base with many complex tables, it might be more efficient for you to use list_tables with tableIdentifiersOnly, then describe_table with full on the specific tables you want.`)
export type TableDetailLevel = z.infer<typeof TableDetailLevelSchema>
export const DescribeTableArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
detailLevel: TableDetailLevelSchema.optional().default('full')
})
export type DescribeTableArgs = z.infer<typeof DescribeTableArgsSchema>
export const ListTablesArgsSchema = z.object({
baseId: z.string(),
detailLevel: TableDetailLevelSchema.optional().default('full')
})
export type ListTablesArgs = z.infer<typeof ListTablesArgsSchema>
export const GetRecordArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
recordId: z.string()
})
export type GetRecordArgs = z.infer<typeof GetRecordArgsSchema>
export const CreateRecordArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
fields: z.record(z.any())
})
export type CreateRecordArgs = z.infer<typeof CreateRecordArgsSchema>
export const UpdateRecordsArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
records: z.array(
z.object({
id: z.string(),
fields: z.record(z.any())
})
)
})
export type UpdateRecordsArgs = z.infer<typeof UpdateRecordsArgsSchema>
export const DeleteRecordsArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
recordIds: z.array(z.string())
})
export type DeleteRecordsArgs = z.infer<typeof DeleteRecordsArgsSchema>
export const CreateTableArgsSchema = z.object({
baseId: z.string(),
name: z
.string()
.describe('Name for the new table. Must be unique in the base.'),
description: z.string().optional(),
fields: z.array(FieldSchema).describe(`Table fields. Rules:
- At least one field must be specified.
- The primary (first) field must be one of: single line text, long text, date, phone number, email, URL, number, currency, percent, duration, formula, autonumber, barcode.`)
})
export type CreateTableArgs = z.infer<typeof CreateTableArgsSchema>
export const UpdateTableArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
name: z.string().optional(),
description: z.string().optional()
})
export type UpdateTableArgs = z.infer<typeof UpdateTableArgsSchema>
export const CreateFieldArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
// This is used as a workaround for https://github.com/orgs/modelcontextprotocol/discussions/90
body: z.object({
field: FieldSchema
})
})
export type CreateFieldArgs = z.infer<typeof CreateFieldArgsSchema>
export const UpdateFieldArgsSchema = z.object({
baseId: z.string(),
tableId: z.string(),
fieldId: z.string(),
name: z.string().optional(),
description: z.string().optional()
})
export type UpdateFieldArgs = z.infer<typeof UpdateFieldArgsSchema>
export type FieldSet = Record<string, any>
export type AirtableRecord = { id: string; fields: FieldSet }
export type AirtableTableToDetailLevel<
TDetailLevel extends TableDetailLevel | undefined = 'full'
> = TDetailLevel extends undefined
? airtable.Table
: TDetailLevel extends 'full'
? airtable.Table
: TDetailLevel extends 'identifiersOnly'
? {
id: string
name: string
fields: { id: string; name: string }[]
views: { id: string; name: string }[]
}
: TDetailLevel extends 'tableIdentifiersOnly'
? { id: string; name: string }
: never
}

Wyświetl plik

@ -0,0 +1,2 @@
export * from './airtable'
export * from './airtable-client'

Wyświetl plik

@ -0,0 +1,5 @@
{
"extends": "@fisch0920/config/tsconfig-node",
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

Wyświetl plik

@ -28,10 +28,12 @@ export class OpenMeteoClient extends AIFunctionsProvider {
constructor({
apiKey = getEnv('OPEN_METEO_API_KEY'),
apiBaseUrl = openmeteo.apiBaseUrl,
timeoutMs = 60_000,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
timeoutMs?: number
ky?: KyInstance
} = {}) {
super()
@ -41,6 +43,7 @@ export class OpenMeteoClient extends AIFunctionsProvider {
this.ky = ky.extend({
prefixUrl: apiBaseUrl,
timeout: timeoutMs,
...(apiKey
? {
headers: {

Wyświetl plik

@ -320,10 +320,12 @@ export class RedditClient extends AIFunctionsProvider {
constructor({
baseUrl = reddit.BASE_URL,
userAgent = 'agentic-reddit-client/1.0.0',
timeoutMs = 60_000,
ky = defaultKy
}: {
baseUrl?: string
userAgent?: string
timeoutMs?: number
ky?: KyInstance
} = {}) {
super()
@ -332,6 +334,7 @@ export class RedditClient extends AIFunctionsProvider {
this.ky = ky.extend({
prefixUrl: this.baseUrl,
timeout: timeoutMs,
headers: {
'User-Agent': userAgent
}

Wyświetl plik

@ -32,6 +32,7 @@
"test:typecheck": "tsc --noEmit"
},
"dependencies": {
"@agentic/airtable": "workspace:*",
"@agentic/apollo": "workspace:*",
"@agentic/arxiv": "workspace:*",
"@agentic/bing": "workspace:*",

Wyświetl plik

@ -1,3 +1,4 @@
export * from '@agentic/airtable'
export * from '@agentic/apollo'
export * from '@agentic/arxiv'
export * from '@agentic/bing'

Wyświetl plik

@ -98,11 +98,13 @@ export class TypeformClient extends AIFunctionsProvider {
constructor({
apiKey = getEnv('TYPEFORM_API_KEY'),
apiBaseUrl = typeform.API_BASE_URL,
timeoutMs = 60_000,
ky = defaultKy
}: {
/** Typeform Personal Access Token */
apiKey?: string
apiBaseUrl?: string
timeoutMs?: number
ky?: KyInstance
} = {}) {
assert(
@ -116,6 +118,7 @@ export class TypeformClient extends AIFunctionsProvider {
this.ky = ky.extend({
prefixUrl: this.apiBaseUrl,
timeout: timeoutMs,
headers: {
Authorization: `Bearer ${this.apiKey}`
}

Wyświetl plik

@ -97,10 +97,12 @@ export class YouTubeClient extends AIFunctionsProvider {
constructor({
apiKey = getEnv('YOUTUBE_API_KEY'),
apiBaseUrl = youtube.API_BASE_URL,
timeoutMs = 30_000,
ky = defaultKy
}: {
apiKey?: string
apiBaseUrl?: string
timeoutMs?: number
ky?: KyInstance
} = {}) {
assert(
@ -113,7 +115,8 @@ export class YouTubeClient extends AIFunctionsProvider {
this.apiBaseUrl = apiBaseUrl
this.ky = ky.extend({
prefixUrl: this.apiBaseUrl
prefixUrl: this.apiBaseUrl,
timeout: timeoutMs
})
}

Wyświetl plik

@ -445,6 +445,21 @@ importers:
specifier: 'catalog:'
version: 4.3.4(react@18.3.1)(zod@3.24.2)
packages/airtable:
dependencies:
'@agentic/core':
specifier: workspace:*
version: link:../core
ky:
specifier: 'catalog:'
version: 1.8.0
p-throttle:
specifier: 'catalog:'
version: 6.2.0
zod:
specifier: 'catalog:'
version: 3.24.2
packages/apollo:
dependencies:
'@agentic/core':
@ -1071,6 +1086,9 @@ importers:
packages/stdlib:
dependencies:
'@agentic/airtable':
specifier: workspace:*
version: link:../airtable
'@agentic/apollo':
specifier: workspace:*
version: link:../apollo

Wyświetl plik

@ -180,6 +180,7 @@ Full docs are available at [agentic.so](https://agentic.so).
| Service / Tool | Package | Docs | Description |
| ------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Airtable](https://airtable.com/developers/web/api/introduction) | `@agentic/airtable` | [docs](https://agentic.so/tools/airtable) | No-code spreadsheets, CRM, and database. |
| [Apollo](https://docs.apollo.io) | `@agentic/apollo` | [docs](https://agentic.so/tools/apollo) | B2B person and company enrichment API. |
| [ArXiv](https://arxiv.org) | `@agentic/arxiv` | [docs](https://agentic.so/tools/arxiv) | Search for research articles. |
| [Bing](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) | `@agentic/bing` | [docs](https://agentic.so/tools/bing) | Bing web search. |