kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add @agentic/airtable Airtable package
rodzic
50f0626ac6
commit
1a4ef44f2b
|
@ -55,6 +55,7 @@
|
|||
{
|
||||
"group": "Tools",
|
||||
"pages": [
|
||||
"tools/airtable",
|
||||
"tools/apollo",
|
||||
"tools/arxiv",
|
||||
"tools/bing",
|
||||
|
|
|
@ -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)
|
||||
```
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './airtable'
|
||||
export * from './airtable-client'
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@fisch0920/config/tsconfig-node",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"test:typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentic/airtable": "workspace:*",
|
||||
"@agentic/apollo": "workspace:*",
|
||||
"@agentic/arxiv": "workspace:*",
|
||||
"@agentic/bing": "workspace:*",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from '@agentic/airtable'
|
||||
export * from '@agentic/apollo'
|
||||
export * from '@agentic/arxiv'
|
||||
export * from '@agentic/bing'
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
Ładowanie…
Reference in New Issue