chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import type { NotionAddDatabaseRowParams } from '@/tools/notion/types'
|
||||
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionAddDatabaseRowResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
url: string
|
||||
title: string
|
||||
created_time: string
|
||||
last_edited_time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const notionAddDatabaseRowTool: ToolConfig<
|
||||
NotionAddDatabaseRowParams,
|
||||
NotionAddDatabaseRowResponse
|
||||
> = {
|
||||
id: 'notion_add_database_row',
|
||||
name: 'Add Notion Database Row',
|
||||
description: 'Add a new row to a Notion database with specified properties',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
databaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the database to add the row to',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Row properties as JSON object matching the database schema (e.g., {"Name": {"title": [{"text": {"content": "Task 1"}}]}, "Status": {"select": {"name": "Done"}}})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.notion.com/v1/pages',
|
||||
method: 'POST',
|
||||
headers: (params: NotionAddDatabaseRowParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionAddDatabaseRowParams) => {
|
||||
return {
|
||||
parent: {
|
||||
type: 'database_id',
|
||||
database_id: params.databaseId.trim(),
|
||||
},
|
||||
properties: params.properties,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract title from properties if available
|
||||
let rowTitle = 'Untitled'
|
||||
for (const [, value] of Object.entries(data.properties || {})) {
|
||||
const prop = value as any
|
||||
if (prop.type === 'title' && prop.title?.length > 0) {
|
||||
rowTitle = prop.title.map((t: any) => t.plain_text || '').join('')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
url: data.url,
|
||||
title: rowTitle,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: PAGE_OUTPUT_PROPERTIES.id,
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
title: { type: 'string', description: 'Row title' },
|
||||
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
|
||||
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionAddDatabaseRowV2Tool: ToolConfig<
|
||||
NotionAddDatabaseRowParams,
|
||||
NotionAddDatabaseRowResponse
|
||||
> = {
|
||||
...notionAddDatabaseRowTool,
|
||||
id: 'notion_add_database_row_v2',
|
||||
version: '2.0.0',
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { NotionAppendBlocksParams } from '@/tools/notion/types'
|
||||
import { BLOCK_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionAppendBlocksResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce the children param into a block array, accepting either a JSON string
|
||||
* (when called directly by an agent) or an already-parsed array.
|
||||
*/
|
||||
function parseChildren(children: any[] | string): any[] {
|
||||
if (Array.isArray(children)) return children
|
||||
if (typeof children === 'string') {
|
||||
const parsed = JSON.parse(children)
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('children must be a JSON array of Notion block objects')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
throw new Error('children must be a JSON array of Notion block objects')
|
||||
}
|
||||
|
||||
export const notionAppendBlocksTool: ToolConfig<
|
||||
NotionAppendBlocksParams,
|
||||
NotionAppendBlocksResponse
|
||||
> = {
|
||||
id: 'notion_append_blocks',
|
||||
name: 'Notion Append Block Children',
|
||||
description: 'Append new block children (content) to a Notion page or block',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the page or block to append children to',
|
||||
},
|
||||
children: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of Notion block objects to append (max 100)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UUID of an existing block to append the new children after',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionAppendBlocksParams) =>
|
||||
`https://api.notion.com/v1/blocks/${params.blockId.trim()}/children`,
|
||||
method: 'PATCH',
|
||||
headers: (params: NotionAppendBlocksParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionAppendBlocksParams) => {
|
||||
const body: any = { children: parseChildren(params.children) }
|
||||
if (params.after) body.after = params.after.trim()
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
has_more: data.has_more ?? false,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: BLOCK_LIST_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionAppendBlocksV2Tool: ToolConfig<
|
||||
NotionAppendBlocksParams,
|
||||
NotionAppendBlocksResponse
|
||||
> = {
|
||||
id: 'notion_append_blocks_v2',
|
||||
name: 'Notion Append Block Children',
|
||||
description: 'Append new block children (content) to a Notion page or block',
|
||||
version: '2.0.0',
|
||||
oauth: notionAppendBlocksTool.oauth,
|
||||
params: notionAppendBlocksTool.params,
|
||||
request: notionAppendBlocksTool.request,
|
||||
transformResponse: notionAppendBlocksTool.transformResponse,
|
||||
outputs: notionAppendBlocksTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { NotionCreateCommentParams } from '@/tools/notion/types'
|
||||
import { RICH_TEXT_ARRAY_OUTPUT } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionCreateCommentResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
discussion_id: string
|
||||
created_time: string
|
||||
content: string
|
||||
rich_text: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export const notionCreateCommentTool: ToolConfig<
|
||||
NotionCreateCommentParams,
|
||||
NotionCreateCommentResponse
|
||||
> = {
|
||||
id: 'notion_create_comment',
|
||||
name: 'Notion Create Comment',
|
||||
description: 'Create a comment on a Notion page or within an existing discussion thread',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UUID of the page to comment on (provide either pageId or discussionId)',
|
||||
},
|
||||
discussionId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'UUID of an existing discussion thread to reply to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The text content of the comment',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.notion.com/v1/comments',
|
||||
method: 'POST',
|
||||
headers: (params: NotionCreateCommentParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionCreateCommentParams) => {
|
||||
const pageId = params.pageId?.trim()
|
||||
const discussionId = params.discussionId?.trim()
|
||||
|
||||
if (!pageId && !discussionId) {
|
||||
throw new Error('Either pageId or discussionId is required to create a comment')
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
rich_text: [{ type: 'text', text: { content: params.content } }],
|
||||
}
|
||||
|
||||
if (discussionId) {
|
||||
body.discussion_id = discussionId
|
||||
} else {
|
||||
body.parent = { page_id: pageId }
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const richText = data.rich_text ?? []
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
id: data.id,
|
||||
discussion_id: data.discussion_id ?? '',
|
||||
created_time: data.created_time ?? '',
|
||||
content: richText.map((t: any) => t.plain_text ?? '').join(''),
|
||||
rich_text: richText,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Comment UUID' },
|
||||
discussion_id: { type: 'string', description: 'UUID of the discussion thread' },
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
content: { type: 'string', description: 'Plain text content of the comment' },
|
||||
rich_text: RICH_TEXT_ARRAY_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionCreateCommentV2Tool: ToolConfig<
|
||||
NotionCreateCommentParams,
|
||||
NotionCreateCommentResponse
|
||||
> = {
|
||||
id: 'notion_create_comment_v2',
|
||||
name: 'Notion Create Comment',
|
||||
description: 'Create a comment on a Notion page or within an existing discussion thread',
|
||||
version: '2.0.0',
|
||||
oauth: notionCreateCommentTool.oauth,
|
||||
params: notionCreateCommentTool.params,
|
||||
request: notionCreateCommentTool.request,
|
||||
transformResponse: notionCreateCommentTool.transformResponse,
|
||||
outputs: notionCreateCommentTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import type { NotionCreateDatabaseParams, NotionResponse } from '@/tools/notion/types'
|
||||
import { DATABASE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionCreateDatabaseTool: ToolConfig<NotionCreateDatabaseParams, NotionResponse> = {
|
||||
id: 'notion_create_database',
|
||||
name: 'Create Notion Database',
|
||||
description: 'Create a new database in Notion with custom properties',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
parentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the parent page where the database will be created',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title for the new database',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Database properties as JSON object (optional, will create a default "Name" property if empty)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.notion.com/v1/databases',
|
||||
method: 'POST',
|
||||
headers: (params: NotionCreateDatabaseParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionCreateDatabaseParams) => {
|
||||
// Use provided properties or default to Name property
|
||||
const properties =
|
||||
params.properties && Object.keys(params.properties).length > 0
|
||||
? params.properties
|
||||
: { Name: { title: {} } }
|
||||
|
||||
const body = {
|
||||
parent: {
|
||||
type: 'page_id',
|
||||
page_id: params.parentId.trim(),
|
||||
},
|
||||
title: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: params.title,
|
||||
},
|
||||
},
|
||||
],
|
||||
properties,
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract database title
|
||||
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
|
||||
|
||||
// Extract properties for display
|
||||
const properties = data.properties || {}
|
||||
const propertyList = Object.entries(properties)
|
||||
.map(([name, prop]: [string, any]) => ` ${name}: ${prop.type}`)
|
||||
.join('\n')
|
||||
|
||||
const content = [
|
||||
`Database "${title}" created successfully!`,
|
||||
'',
|
||||
'Properties:',
|
||||
propertyList,
|
||||
'',
|
||||
`Database ID: ${data.id}`,
|
||||
`URL: ${data.url}`,
|
||||
].join('\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
metadata: {
|
||||
id: data.id,
|
||||
title,
|
||||
url: data.url,
|
||||
createdTime: data.created_time,
|
||||
properties: data.properties,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Success message with database details and properties list',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Database metadata including ID, title, URL, creation time, and properties schema',
|
||||
properties: {
|
||||
id: DATABASE_OUTPUT_PROPERTIES.id,
|
||||
title: { type: 'string', description: 'Database title' },
|
||||
url: DATABASE_OUTPUT_PROPERTIES.url,
|
||||
createdTime: DATABASE_OUTPUT_PROPERTIES.created_time,
|
||||
properties: DATABASE_OUTPUT_PROPERTIES.properties,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionCreateDatabaseV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
created_time: string
|
||||
properties: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export const notionCreateDatabaseV2Tool: ToolConfig<
|
||||
NotionCreateDatabaseParams,
|
||||
NotionCreateDatabaseV2Response
|
||||
> = {
|
||||
id: 'notion_create_database_v2',
|
||||
name: 'Create Notion Database',
|
||||
description: 'Create a new database in Notion with custom properties',
|
||||
version: '2.0.0',
|
||||
oauth: notionCreateDatabaseTool.oauth,
|
||||
params: notionCreateDatabaseTool.params,
|
||||
request: notionCreateDatabaseTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
title,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
properties: data.properties || {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: DATABASE_OUTPUT_PROPERTIES.id,
|
||||
title: { type: 'string', description: 'Database title' },
|
||||
url: DATABASE_OUTPUT_PROPERTIES.url,
|
||||
created_time: DATABASE_OUTPUT_PROPERTIES.created_time,
|
||||
properties: DATABASE_OUTPUT_PROPERTIES.properties,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import type { NotionCreatePageParams, NotionResponse } from '@/tools/notion/types'
|
||||
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResponse> = {
|
||||
id: 'notion_create_page',
|
||||
name: 'Notion Page Creator',
|
||||
description: 'Create a new page in Notion',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
parentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the parent Notion page where this page will be created',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the new page',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional content to add to the page upon creation',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.notion.com/v1/pages',
|
||||
method: 'POST',
|
||||
headers: (params: NotionCreatePageParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionCreatePageParams) => {
|
||||
const body: any = {
|
||||
parent: {
|
||||
type: 'page_id',
|
||||
page_id: params.parentId.trim(),
|
||||
},
|
||||
}
|
||||
|
||||
if (params.title) {
|
||||
body.properties = {
|
||||
title: {
|
||||
type: 'title',
|
||||
title: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: params.title,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
body.properties = {}
|
||||
}
|
||||
|
||||
if (params.content) {
|
||||
body.children = [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: params.content,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: `Successfully created page "${pageTitle}"`,
|
||||
metadata: {
|
||||
title: pageTitle,
|
||||
pageId: data.id,
|
||||
url: data.url,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
createdTime: data.created_time,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Success message confirming page creation',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Page metadata including title, page ID, URL, and timestamps',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
pageId: PAGE_OUTPUT_PROPERTIES.id,
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
lastEditedTime: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
createdTime: PAGE_OUTPUT_PROPERTIES.created_time,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionCreatePageV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
created_time: string
|
||||
last_edited_time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const notionCreatePageV2Tool: ToolConfig<
|
||||
NotionCreatePageParams,
|
||||
NotionCreatePageV2Response
|
||||
> = {
|
||||
id: 'notion_create_page_v2',
|
||||
name: 'Notion Page Creator',
|
||||
description: 'Create a new page in Notion',
|
||||
version: '2.0.0',
|
||||
oauth: notionCreatePageTool.oauth,
|
||||
params: notionCreatePageTool.params,
|
||||
request: notionCreatePageTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
title: pageTitle,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: PAGE_OUTPUT_PROPERTIES.id,
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
|
||||
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { NotionDeleteBlockParams } from '@/tools/notion/types'
|
||||
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionDeleteBlockResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
archived: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const notionDeleteBlockTool: ToolConfig<NotionDeleteBlockParams, NotionDeleteBlockResponse> =
|
||||
{
|
||||
id: 'notion_delete_block',
|
||||
name: 'Notion Delete Block',
|
||||
description: 'Delete (move to trash) a single Notion block',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the block to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionDeleteBlockParams) =>
|
||||
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params: NotionDeleteBlockParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
id: data.id,
|
||||
archived: data.archived ?? true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: BLOCK_OUTPUT_PROPERTIES.id,
|
||||
archived: { type: 'boolean', description: 'Whether the block was archived (moved to trash)' },
|
||||
},
|
||||
}
|
||||
|
||||
export const notionDeleteBlockV2Tool: ToolConfig<
|
||||
NotionDeleteBlockParams,
|
||||
NotionDeleteBlockResponse
|
||||
> = {
|
||||
id: 'notion_delete_block_v2',
|
||||
name: 'Notion Delete Block',
|
||||
description: 'Delete (move to trash) a single Notion block',
|
||||
version: '2.0.0',
|
||||
oauth: notionDeleteBlockTool.oauth,
|
||||
params: notionDeleteBlockTool.params,
|
||||
request: notionDeleteBlockTool.request,
|
||||
transformResponse: notionDeleteBlockTool.transformResponse,
|
||||
outputs: notionDeleteBlockTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
notionAddDatabaseRowTool,
|
||||
notionAddDatabaseRowV2Tool,
|
||||
} from '@/tools/notion/add_database_row'
|
||||
import { notionAppendBlocksTool, notionAppendBlocksV2Tool } from '@/tools/notion/append_blocks'
|
||||
import { notionCreateCommentTool, notionCreateCommentV2Tool } from '@/tools/notion/create_comment'
|
||||
import {
|
||||
notionCreateDatabaseTool,
|
||||
notionCreateDatabaseV2Tool,
|
||||
} from '@/tools/notion/create_database'
|
||||
import { notionCreatePageTool, notionCreatePageV2Tool } from '@/tools/notion/create_page'
|
||||
import { notionDeleteBlockTool, notionDeleteBlockV2Tool } from '@/tools/notion/delete_block'
|
||||
import { notionListCommentsTool, notionListCommentsV2Tool } from '@/tools/notion/list_comments'
|
||||
import { notionListUsersTool, notionListUsersV2Tool } from '@/tools/notion/list_users'
|
||||
import { notionQueryDatabaseTool, notionQueryDatabaseV2Tool } from '@/tools/notion/query_database'
|
||||
import { notionReadTool, notionReadV2Tool } from '@/tools/notion/read'
|
||||
import { notionReadDatabaseTool, notionReadDatabaseV2Tool } from '@/tools/notion/read_database'
|
||||
import { notionRetrieveBlockTool, notionRetrieveBlockV2Tool } from '@/tools/notion/retrieve_block'
|
||||
import {
|
||||
notionRetrieveBlockChildrenTool,
|
||||
notionRetrieveBlockChildrenV2Tool,
|
||||
} from '@/tools/notion/retrieve_block_children'
|
||||
import { notionRetrieveUserTool, notionRetrieveUserV2Tool } from '@/tools/notion/retrieve_user'
|
||||
import { notionSearchTool, notionSearchV2Tool } from '@/tools/notion/search'
|
||||
import { notionUpdateBlockTool, notionUpdateBlockV2Tool } from '@/tools/notion/update_block'
|
||||
import { notionUpdatePageTool, notionUpdatePageV2Tool } from '@/tools/notion/update_page'
|
||||
import { notionWriteTool, notionWriteV2Tool } from '@/tools/notion/write'
|
||||
|
||||
export {
|
||||
// Legacy tools
|
||||
notionReadTool,
|
||||
notionReadDatabaseTool,
|
||||
notionWriteTool,
|
||||
notionCreatePageTool,
|
||||
notionUpdatePageTool,
|
||||
notionQueryDatabaseTool,
|
||||
notionSearchTool,
|
||||
notionCreateDatabaseTool,
|
||||
notionAddDatabaseRowTool,
|
||||
notionAppendBlocksTool,
|
||||
notionRetrieveBlockTool,
|
||||
notionRetrieveBlockChildrenTool,
|
||||
notionUpdateBlockTool,
|
||||
notionDeleteBlockTool,
|
||||
notionCreateCommentTool,
|
||||
notionListCommentsTool,
|
||||
notionListUsersTool,
|
||||
notionRetrieveUserTool,
|
||||
// V2 tools
|
||||
notionReadV2Tool,
|
||||
notionReadDatabaseV2Tool,
|
||||
notionWriteV2Tool,
|
||||
notionCreatePageV2Tool,
|
||||
notionUpdatePageV2Tool,
|
||||
notionQueryDatabaseV2Tool,
|
||||
notionSearchV2Tool,
|
||||
notionCreateDatabaseV2Tool,
|
||||
notionAddDatabaseRowV2Tool,
|
||||
notionAppendBlocksV2Tool,
|
||||
notionRetrieveBlockV2Tool,
|
||||
notionRetrieveBlockChildrenV2Tool,
|
||||
notionUpdateBlockV2Tool,
|
||||
notionDeleteBlockV2Tool,
|
||||
notionCreateCommentV2Tool,
|
||||
notionListCommentsV2Tool,
|
||||
notionListUsersV2Tool,
|
||||
notionRetrieveUserV2Tool,
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { NotionListCommentsParams } from '@/tools/notion/types'
|
||||
import { COMMENT_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import { clampNotionPageSize } from '@/tools/notion/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionListCommentsResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const notionListCommentsTool: ToolConfig<
|
||||
NotionListCommentsParams,
|
||||
NotionListCommentsResponse
|
||||
> = {
|
||||
id: 'notion_list_comments',
|
||||
name: 'Notion List Comments',
|
||||
description: 'List unresolved comments on a Notion page or block',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the page or block whose comments to list',
|
||||
},
|
||||
startCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned by a previous request',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (1-100, default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionListCommentsParams) => {
|
||||
const url = new URL('https://api.notion.com/v1/comments')
|
||||
url.searchParams.set('block_id', params.blockId.trim())
|
||||
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
|
||||
const pageSize = clampNotionPageSize(params.pageSize)
|
||||
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: NotionListCommentsParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
has_more: data.has_more ?? false,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: COMMENT_LIST_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionListCommentsV2Tool: ToolConfig<
|
||||
NotionListCommentsParams,
|
||||
NotionListCommentsResponse
|
||||
> = {
|
||||
id: 'notion_list_comments_v2',
|
||||
name: 'Notion List Comments',
|
||||
description: 'List unresolved comments on a Notion page or block',
|
||||
version: '2.0.0',
|
||||
oauth: notionListCommentsTool.oauth,
|
||||
params: notionListCommentsTool.params,
|
||||
request: notionListCommentsTool.request,
|
||||
transformResponse: notionListCommentsTool.transformResponse,
|
||||
outputs: notionListCommentsTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { NotionListUsersParams } from '@/tools/notion/types'
|
||||
import { PAGINATION_OUTPUT_PROPERTIES, USER_LIST_RESULTS_OUTPUT } from '@/tools/notion/types'
|
||||
import { clampNotionPageSize } from '@/tools/notion/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionListUsersResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const notionListUsersTool: ToolConfig<NotionListUsersParams, NotionListUsersResponse> = {
|
||||
id: 'notion_list_users',
|
||||
name: 'Notion List Users',
|
||||
description: 'List all users (members and bots) in the Notion workspace',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
startCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned by a previous request',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (1-100, default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionListUsersParams) => {
|
||||
const url = new URL('https://api.notion.com/v1/users')
|
||||
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
|
||||
const pageSize = clampNotionPageSize(params.pageSize)
|
||||
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: NotionListUsersParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
has_more: data.has_more ?? false,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: USER_LIST_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionListUsersV2Tool: ToolConfig<NotionListUsersParams, NotionListUsersResponse> = {
|
||||
id: 'notion_list_users_v2',
|
||||
name: 'Notion List Users',
|
||||
description: 'List all users (members and bots) in the Notion workspace',
|
||||
version: '2.0.0',
|
||||
oauth: notionListUsersTool.oauth,
|
||||
params: notionListUsersTool.params,
|
||||
request: notionListUsersTool.request,
|
||||
transformResponse: notionListUsersTool.transformResponse,
|
||||
outputs: notionListUsersTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import type { NotionQueryDatabaseParams, NotionResponse } from '@/tools/notion/types'
|
||||
import { DATABASE_QUERY_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import { extractTitle, formatPropertyValue } from '@/tools/notion/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionQueryDatabaseTool: ToolConfig<NotionQueryDatabaseParams, NotionResponse> = {
|
||||
id: 'notion_query_database',
|
||||
name: 'Query Notion Database',
|
||||
description: 'Query and filter Notion database entries with advanced filtering',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
databaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the Notion database to query',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter conditions as JSON (optional)',
|
||||
},
|
||||
sorts: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort criteria as JSON array (optional)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default: 100, max: 100)',
|
||||
},
|
||||
startCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned by a previous request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionQueryDatabaseParams) => {
|
||||
return `https://api.notion.com/v1/databases/${params.databaseId.trim()}/query`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: NotionQueryDatabaseParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionQueryDatabaseParams) => {
|
||||
const body: any = {}
|
||||
|
||||
// Add filter if provided
|
||||
if (params.filter) {
|
||||
try {
|
||||
body.filter = JSON.parse(params.filter)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid filter JSON: ${toError(error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add sorts if provided
|
||||
if (params.sorts) {
|
||||
try {
|
||||
body.sorts = JSON.parse(params.sorts)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid sorts JSON: ${toError(error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add page size if provided
|
||||
if (params.pageSize) {
|
||||
body.page_size = Math.min(Number(params.pageSize), 100)
|
||||
}
|
||||
|
||||
// Add pagination cursor if provided
|
||||
if (params.startCursor) {
|
||||
body.start_cursor = params.startCursor.trim()
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const results = data.results || []
|
||||
|
||||
// Format the results into readable content
|
||||
const content = results
|
||||
.map((page: any, index: number) => {
|
||||
const properties = page.properties || {}
|
||||
const title = extractTitle(properties)
|
||||
const propertyValues = Object.entries(properties)
|
||||
.map(([key, value]: [string, any]) => {
|
||||
const formattedValue = formatPropertyValue(value)
|
||||
return ` ${key}: ${formattedValue}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `Entry ${index + 1}${title ? ` - ${title}` : ''}:\n${propertyValues}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: content || 'No results found',
|
||||
metadata: {
|
||||
totalResults: results.length,
|
||||
hasMore: data.has_more || false,
|
||||
nextCursor: data.next_cursor || null,
|
||||
results: results,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Formatted list of database entries with their properties',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Query metadata including total results count, pagination info, and raw results array',
|
||||
properties: {
|
||||
totalResults: { type: 'number', description: 'Number of results returned' },
|
||||
hasMore: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
nextCursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
results: DATABASE_QUERY_RESULTS_OUTPUT,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionQueryDatabaseV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
total_results: number
|
||||
}
|
||||
}
|
||||
|
||||
export const notionQueryDatabaseV2Tool: ToolConfig<
|
||||
NotionQueryDatabaseParams,
|
||||
NotionQueryDatabaseV2Response
|
||||
> = {
|
||||
id: 'notion_query_database_v2',
|
||||
name: 'Query Notion Database',
|
||||
description: 'Query and filter Notion database entries with advanced filtering',
|
||||
version: '2.0.0',
|
||||
oauth: notionQueryDatabaseTool.oauth,
|
||||
params: notionQueryDatabaseTool.params,
|
||||
request: notionQueryDatabaseTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const results = data.results || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
has_more: data.has_more || false,
|
||||
next_cursor: data.next_cursor || null,
|
||||
total_results: results.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: DATABASE_QUERY_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
total_results: { type: 'number', description: 'Number of results returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import type { NotionReadParams, NotionResponse } from '@/tools/notion/types'
|
||||
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
|
||||
id: 'notion_read',
|
||||
name: 'Notion Reader',
|
||||
description: 'Read content from a Notion page',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the Notion page to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionReadParams) => {
|
||||
return `https://api.notion.com/v1/pages/${params.pageId.trim()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: NotionReadParams) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: NotionReadParams) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
// Extract title from properties
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
// Now fetch the page content using blocks endpoint
|
||||
const pageId = params?.pageId?.trim()
|
||||
const accessToken = params?.accessToken
|
||||
|
||||
if (!pageId || !accessToken) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
metadata: {
|
||||
title: pageTitle,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
createdTime: data.created_time,
|
||||
url: data.url,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch page content using blocks endpoint
|
||||
const blocksResponse = await fetch(
|
||||
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!blocksResponse.ok) {
|
||||
// If we can't get blocks, still return the page metadata
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
metadata: {
|
||||
title: pageTitle,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
createdTime: data.created_time,
|
||||
url: data.url,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const blocksData = await blocksResponse.json()
|
||||
|
||||
// Extract text content from blocks
|
||||
const blocks = blocksData.results || []
|
||||
const content = blocks
|
||||
.map((block: any) => {
|
||||
if (block.type === 'paragraph') {
|
||||
return block.paragraph.rich_text.map((text: any) => text.plain_text).join('')
|
||||
}
|
||||
if (block.type === 'heading_1') {
|
||||
return `# ${block.heading_1.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'heading_2') {
|
||||
return `## ${block.heading_2.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'heading_3') {
|
||||
return `### ${block.heading_3.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'bulleted_list_item') {
|
||||
return `• ${block.bulleted_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'numbered_list_item') {
|
||||
return `1. ${block.numbered_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'to_do') {
|
||||
const checked = block.to_do.checked ? '[x]' : '[ ]'
|
||||
return `${checked} ${block.to_do.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: content,
|
||||
metadata: {
|
||||
title: pageTitle,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
createdTime: data.created_time,
|
||||
url: data.url,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Page content in markdown format with headers, paragraphs, lists, and todos',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Page metadata including title, URL, and timestamps',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
lastEditedTime: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
createdTime: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
url: { type: 'string', description: 'Notion page URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionReadV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
content: string
|
||||
title: string
|
||||
url: string
|
||||
created_time: string
|
||||
last_edited_time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const notionReadV2Tool: ToolConfig<NotionReadParams, NotionReadV2Response> = {
|
||||
id: 'notion_read_v2',
|
||||
name: 'Notion Reader',
|
||||
description: 'Read content from a Notion page',
|
||||
version: '2.0.0',
|
||||
oauth: notionReadTool.oauth,
|
||||
params: notionReadTool.params,
|
||||
request: notionReadTool.request,
|
||||
|
||||
transformResponse: async (response: Response, params?: NotionReadParams) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
const pageId = params?.pageId?.trim()
|
||||
const accessToken = params?.accessToken
|
||||
|
||||
if (!pageId || !accessToken) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
title: pageTitle,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const blocksResponse = await fetch(
|
||||
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!blocksResponse.ok) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
title: pageTitle,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const blocksData = await blocksResponse.json()
|
||||
const blocks = blocksData.results || []
|
||||
const content = blocks
|
||||
.map((block: any) => {
|
||||
if (block.type === 'paragraph') {
|
||||
return block.paragraph.rich_text.map((text: any) => text.plain_text).join('')
|
||||
}
|
||||
if (block.type === 'heading_1') {
|
||||
return `# ${block.heading_1.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'heading_2') {
|
||||
return `## ${block.heading_2.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'heading_3') {
|
||||
return `### ${block.heading_3.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'bulleted_list_item') {
|
||||
return `• ${block.bulleted_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'numbered_list_item') {
|
||||
return `1. ${block.numbered_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
if (block.type === 'to_do') {
|
||||
const checked = block.to_do.checked ? '[x]' : '[ ]'
|
||||
return `${checked} ${block.to_do.rich_text.map((text: any) => text.plain_text).join('')}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
title: pageTitle,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Page content in markdown format' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
|
||||
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { NotionResponse } from '@/tools/notion/types'
|
||||
import { DATABASE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface NotionReadDatabaseParams {
|
||||
databaseId: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export const notionReadDatabaseTool: ToolConfig<NotionReadDatabaseParams, NotionResponse> = {
|
||||
id: 'notion_read_database',
|
||||
name: 'Read Notion Database',
|
||||
description: 'Read database information and structure from Notion',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
databaseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the Notion database to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionReadDatabaseParams) => {
|
||||
return `https://api.notion.com/v1/databases/${params.databaseId.trim()}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: NotionReadDatabaseParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Extract database title
|
||||
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
|
||||
|
||||
// Extract properties for display
|
||||
const properties = data.properties || {}
|
||||
const propertyList = Object.entries(properties)
|
||||
.map(([name, prop]: [string, any]) => ` ${name}: ${prop.type}`)
|
||||
.join('\n')
|
||||
|
||||
const content = [
|
||||
`Database: ${title}`,
|
||||
'',
|
||||
'Properties:',
|
||||
propertyList,
|
||||
'',
|
||||
`Database ID: ${data.id}`,
|
||||
`URL: ${data.url}`,
|
||||
`Created: ${data.created_time ? new Date(data.created_time).toLocaleDateString() : 'Unknown'}`,
|
||||
`Last edited: ${data.last_edited_time ? new Date(data.last_edited_time).toLocaleDateString() : 'Unknown'}`,
|
||||
].join('\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
metadata: {
|
||||
title,
|
||||
url: data.url,
|
||||
id: data.id,
|
||||
createdTime: data.created_time,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
properties: data.properties,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Database information including title, properties schema, and metadata',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Database metadata including title, ID, URL, timestamps, and properties schema',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Database title' },
|
||||
url: DATABASE_OUTPUT_PROPERTIES.url,
|
||||
id: DATABASE_OUTPUT_PROPERTIES.id,
|
||||
createdTime: DATABASE_OUTPUT_PROPERTIES.created_time,
|
||||
lastEditedTime: DATABASE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
properties: DATABASE_OUTPUT_PROPERTIES.properties,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
interface NotionReadDatabaseV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
created_time: string
|
||||
last_edited_time: string
|
||||
properties: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export const notionReadDatabaseV2Tool: ToolConfig<
|
||||
NotionReadDatabaseParams,
|
||||
NotionReadDatabaseV2Response
|
||||
> = {
|
||||
id: 'notion_read_database_v2',
|
||||
name: 'Read Notion Database',
|
||||
description: 'Read database information and structure from Notion',
|
||||
version: '2.0.0',
|
||||
oauth: notionReadDatabaseTool.oauth,
|
||||
params: notionReadDatabaseTool.params,
|
||||
request: notionReadDatabaseTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
title,
|
||||
url: data.url,
|
||||
created_time: data.created_time,
|
||||
last_edited_time: data.last_edited_time,
|
||||
properties: data.properties || {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: DATABASE_OUTPUT_PROPERTIES.id,
|
||||
title: { type: 'string', description: 'Database title' },
|
||||
url: DATABASE_OUTPUT_PROPERTIES.url,
|
||||
created_time: DATABASE_OUTPUT_PROPERTIES.created_time,
|
||||
last_edited_time: DATABASE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
properties: DATABASE_OUTPUT_PROPERTIES.properties,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export interface NotionRetrieveBlockParams {
|
||||
blockId: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
interface NotionRetrieveBlockResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
type: string
|
||||
has_children: boolean
|
||||
archived: boolean
|
||||
block: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export const notionRetrieveBlockTool: ToolConfig<
|
||||
NotionRetrieveBlockParams,
|
||||
NotionRetrieveBlockResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_block',
|
||||
name: 'Notion Retrieve Block',
|
||||
description: 'Retrieve a single Notion block by its UUID, including its type-specific content',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the block to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionRetrieveBlockParams) =>
|
||||
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params: NotionRetrieveBlockParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
id: data.id,
|
||||
type: data.type ?? '',
|
||||
has_children: data.has_children ?? false,
|
||||
archived: data.archived ?? false,
|
||||
block: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: BLOCK_OUTPUT_PROPERTIES.id,
|
||||
type: BLOCK_OUTPUT_PROPERTIES.type,
|
||||
has_children: BLOCK_OUTPUT_PROPERTIES.has_children,
|
||||
archived: BLOCK_OUTPUT_PROPERTIES.archived,
|
||||
block: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The full Notion block object. Includes a type-specific field (e.g. paragraph, heading_1, image) whose shape varies by block type and is not enumerated below — read it directly off this object.',
|
||||
properties: BLOCK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const notionRetrieveBlockV2Tool: ToolConfig<
|
||||
NotionRetrieveBlockParams,
|
||||
NotionRetrieveBlockResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_block_v2',
|
||||
name: 'Notion Retrieve Block',
|
||||
description: 'Retrieve a single Notion block by its UUID, including its type-specific content',
|
||||
version: '2.0.0',
|
||||
oauth: notionRetrieveBlockTool.oauth,
|
||||
params: notionRetrieveBlockTool.params,
|
||||
request: notionRetrieveBlockTool.request,
|
||||
transformResponse: notionRetrieveBlockTool.transformResponse,
|
||||
outputs: notionRetrieveBlockTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { NotionRetrieveBlockChildrenParams } from '@/tools/notion/types'
|
||||
import { BLOCK_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import { clampNotionPageSize } from '@/tools/notion/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionRetrieveBlockChildrenResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const notionRetrieveBlockChildrenTool: ToolConfig<
|
||||
NotionRetrieveBlockChildrenParams,
|
||||
NotionRetrieveBlockChildrenResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_block_children',
|
||||
name: 'Notion Retrieve Block Children',
|
||||
description: 'Retrieve the block children (content) of a Notion page or block',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the page or block whose children to retrieve',
|
||||
},
|
||||
startCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned by a previous request',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (1-100, default 100)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionRetrieveBlockChildrenParams) => {
|
||||
const url = new URL(`https://api.notion.com/v1/blocks/${params.blockId.trim()}/children`)
|
||||
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
|
||||
const pageSize = clampNotionPageSize(params.pageSize)
|
||||
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: NotionRetrieveBlockChildrenParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
results: data.results ?? [],
|
||||
has_more: data.has_more ?? false,
|
||||
next_cursor: data.next_cursor ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: BLOCK_LIST_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
},
|
||||
}
|
||||
|
||||
export const notionRetrieveBlockChildrenV2Tool: ToolConfig<
|
||||
NotionRetrieveBlockChildrenParams,
|
||||
NotionRetrieveBlockChildrenResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_block_children_v2',
|
||||
name: 'Notion Retrieve Block Children',
|
||||
description: 'Retrieve the block children (content) of a Notion page or block',
|
||||
version: '2.0.0',
|
||||
oauth: notionRetrieveBlockChildrenTool.oauth,
|
||||
params: notionRetrieveBlockChildrenTool.params,
|
||||
request: notionRetrieveBlockChildrenTool.request,
|
||||
transformResponse: notionRetrieveBlockChildrenTool.transformResponse,
|
||||
outputs: notionRetrieveBlockChildrenTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { NotionRetrieveUserParams } from '@/tools/notion/types'
|
||||
import { USER_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionRetrieveUserResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
type: string | null
|
||||
name: string | null
|
||||
avatar_url: string | null
|
||||
email: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const notionRetrieveUserTool: ToolConfig<
|
||||
NotionRetrieveUserParams,
|
||||
NotionRetrieveUserResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_user',
|
||||
name: 'Notion Retrieve User',
|
||||
description: 'Retrieve a single Notion user by their UUID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the user to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionRetrieveUserParams) =>
|
||||
`https://api.notion.com/v1/users/${params.userId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params: NotionRetrieveUserParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
id: data.id,
|
||||
type: data.type ?? null,
|
||||
name: data.name ?? null,
|
||||
avatar_url: data.avatar_url ?? null,
|
||||
email: data.person?.email ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: USER_OUTPUT_PROPERTIES.id,
|
||||
type: USER_OUTPUT_PROPERTIES.type,
|
||||
name: USER_OUTPUT_PROPERTIES.name,
|
||||
avatar_url: USER_OUTPUT_PROPERTIES.avatar_url,
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'User email address (person users only)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const notionRetrieveUserV2Tool: ToolConfig<
|
||||
NotionRetrieveUserParams,
|
||||
NotionRetrieveUserResponse
|
||||
> = {
|
||||
id: 'notion_retrieve_user_v2',
|
||||
name: 'Notion Retrieve User',
|
||||
description: 'Retrieve a single Notion user by their UUID',
|
||||
version: '2.0.0',
|
||||
oauth: notionRetrieveUserTool.oauth,
|
||||
params: notionRetrieveUserTool.params,
|
||||
request: notionRetrieveUserTool.request,
|
||||
transformResponse: notionRetrieveUserTool.transformResponse,
|
||||
outputs: notionRetrieveUserTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { NotionResponse, NotionSearchParams } from '@/tools/notion/types'
|
||||
import { PAGINATION_OUTPUT_PROPERTIES, SEARCH_RESULTS_OUTPUT } from '@/tools/notion/types'
|
||||
import { extractTitleFromItem } from '@/tools/notion/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionSearchTool: ToolConfig<NotionSearchParams, NotionResponse> = {
|
||||
id: 'notion_search',
|
||||
name: 'Search Notion Workspace',
|
||||
description: 'Search across all pages and databases in Notion workspace',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search terms to find pages and databases (leave empty to get all pages)',
|
||||
},
|
||||
filterType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by object type: "page", "database", or leave empty for all',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default: 100, max: 100)',
|
||||
},
|
||||
startCursor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor returned by a previous request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.notion.com/v1/search',
|
||||
method: 'POST',
|
||||
headers: (params: NotionSearchParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionSearchParams) => {
|
||||
const body: any = {}
|
||||
|
||||
// Add query if provided
|
||||
if (params.query?.trim()) {
|
||||
body.query = params.query.trim()
|
||||
}
|
||||
|
||||
// Add filter if provided (skip 'all' as it means no filter)
|
||||
if (
|
||||
params.filterType &&
|
||||
params.filterType !== 'all' &&
|
||||
['page', 'database'].includes(params.filterType)
|
||||
) {
|
||||
body.filter = {
|
||||
value: params.filterType,
|
||||
property: 'object',
|
||||
}
|
||||
}
|
||||
|
||||
// Add page size if provided
|
||||
if (params.pageSize) {
|
||||
body.page_size = Math.min(Number(params.pageSize), 100)
|
||||
}
|
||||
|
||||
// Add pagination cursor if provided
|
||||
if (params.startCursor) {
|
||||
body.start_cursor = params.startCursor.trim()
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const results = data.results || []
|
||||
|
||||
// Format the results into readable content
|
||||
const content = results
|
||||
.map((item: any, index: number) => {
|
||||
const objectType = item.object === 'page' ? 'Page' : 'Database'
|
||||
const title = extractTitleFromItem(item)
|
||||
const url = item.url || ''
|
||||
const lastEdited = item.last_edited_time
|
||||
? new Date(item.last_edited_time).toLocaleDateString()
|
||||
: ''
|
||||
|
||||
return [
|
||||
`${index + 1}. ${objectType}: ${title}`,
|
||||
` URL: ${url}`,
|
||||
lastEdited ? ` Last edited: ${lastEdited}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: content || 'No results found',
|
||||
metadata: {
|
||||
totalResults: results.length,
|
||||
hasMore: data.has_more || false,
|
||||
nextCursor: data.next_cursor || null,
|
||||
results: results,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Formatted list of search results including pages and databases',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Search metadata including total results count, pagination info, and raw results array',
|
||||
properties: {
|
||||
totalResults: { type: 'number', description: 'Number of results returned' },
|
||||
hasMore: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
nextCursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
results: SEARCH_RESULTS_OUTPUT,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionSearchV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
results: any[]
|
||||
has_more: boolean
|
||||
next_cursor: string | null
|
||||
total_results: number
|
||||
}
|
||||
}
|
||||
|
||||
export const notionSearchV2Tool: ToolConfig<NotionSearchParams, NotionSearchV2Response> = {
|
||||
id: 'notion_search_v2',
|
||||
name: 'Search Notion Workspace',
|
||||
description: 'Search across all pages and databases in Notion workspace',
|
||||
version: '2.0.0',
|
||||
oauth: notionSearchTool.oauth,
|
||||
params: notionSearchTool.params,
|
||||
request: notionSearchTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const results = data.results || []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
has_more: data.has_more || false,
|
||||
next_cursor: data.next_cursor || null,
|
||||
total_results: results.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: SEARCH_RESULTS_OUTPUT,
|
||||
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
|
||||
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
|
||||
total_results: { type: 'number', description: 'Number of results returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Shared output property definitions for Notion API responses.
|
||||
* Based on official Notion API documentation at https://developers.notion.com/reference/
|
||||
*
|
||||
* @see https://developers.notion.com/reference/page
|
||||
* @see https://developers.notion.com/reference/database
|
||||
* @see https://developers.notion.com/reference/block
|
||||
* @see https://developers.notion.com/reference/user
|
||||
* @see https://developers.notion.com/reference/rich-text
|
||||
* @see https://developers.notion.com/reference/file-object
|
||||
* @see https://developers.notion.com/reference/parent-object
|
||||
*/
|
||||
|
||||
/**
|
||||
* Partial User object properties returned by Notion API.
|
||||
* Used for created_by and last_edited_by fields.
|
||||
* @see https://developers.notion.com/reference/user
|
||||
*/
|
||||
export const PARTIAL_USER_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "user"' },
|
||||
id: { type: 'string', description: 'User UUID' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete partial user output definition
|
||||
*/
|
||||
export const PARTIAL_USER_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Partial user object',
|
||||
properties: PARTIAL_USER_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Full User object properties returned by Notion API.
|
||||
* @see https://developers.notion.com/reference/user
|
||||
*/
|
||||
export const USER_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "user"' },
|
||||
id: { type: 'string', description: 'User UUID' },
|
||||
type: { type: 'string', description: 'User type: "person" or "bot"', optional: true },
|
||||
name: { type: 'string', description: 'User display name', optional: true },
|
||||
avatar_url: { type: 'string', description: 'URL to user avatar image', optional: true },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete user output definition
|
||||
*/
|
||||
export const USER_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'User object',
|
||||
properties: USER_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent object properties for pages and databases.
|
||||
* @see https://developers.notion.com/reference/parent-object
|
||||
*/
|
||||
export const PARENT_OUTPUT_PROPERTIES = {
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Parent type: "database_id", "data_source_id", "page_id", "workspace", or "block_id"',
|
||||
},
|
||||
database_id: {
|
||||
type: 'string',
|
||||
description: 'Parent database UUID (if type is database_id)',
|
||||
optional: true,
|
||||
},
|
||||
data_source_id: {
|
||||
type: 'string',
|
||||
description: 'Parent data source UUID (if type is data_source_id)',
|
||||
optional: true,
|
||||
},
|
||||
page_id: { type: 'string', description: 'Parent page UUID (if type is page_id)', optional: true },
|
||||
workspace: {
|
||||
type: 'boolean',
|
||||
description: 'True if parent is workspace (if type is workspace)',
|
||||
optional: true,
|
||||
},
|
||||
block_id: {
|
||||
type: 'string',
|
||||
description: 'Parent block UUID (if type is block_id)',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete parent output definition
|
||||
*/
|
||||
export const PARENT_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Parent object specifying hierarchical relationship',
|
||||
properties: PARENT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich text annotation properties.
|
||||
* @see https://developers.notion.com/reference/rich-text
|
||||
*/
|
||||
export const ANNOTATIONS_OUTPUT_PROPERTIES = {
|
||||
bold: { type: 'boolean', description: 'Bold styling' },
|
||||
italic: { type: 'boolean', description: 'Italic styling' },
|
||||
strikethrough: { type: 'boolean', description: 'Strikethrough styling' },
|
||||
underline: { type: 'boolean', description: 'Underline styling' },
|
||||
code: { type: 'boolean', description: 'Code styling' },
|
||||
color: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Text color (default, blue, green, red, purple, orange, pink, gray, brown, yellow, or _background variants)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Rich text object properties.
|
||||
* @see https://developers.notion.com/reference/rich-text
|
||||
*/
|
||||
export const RICH_TEXT_OUTPUT_PROPERTIES = {
|
||||
type: { type: 'string', description: 'Rich text type: "text", "mention", or "equation"' },
|
||||
plain_text: { type: 'string', description: 'Plain text content without annotations' },
|
||||
href: { type: 'string', description: 'URL for links or Notion mentions', optional: true },
|
||||
annotations: {
|
||||
type: 'object',
|
||||
description: 'Text styling annotations',
|
||||
properties: ANNOTATIONS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete rich text array output definition
|
||||
*/
|
||||
export const RICH_TEXT_ARRAY_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of rich text objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: RICH_TEXT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Notion-hosted file object properties (type: "file").
|
||||
* @see https://developers.notion.com/reference/file-object
|
||||
*/
|
||||
export const NOTION_FILE_OUTPUT_PROPERTIES = {
|
||||
url: { type: 'string', description: 'Authenticated URL valid for one hour' },
|
||||
expiry_time: { type: 'string', description: 'ISO 8601 timestamp when URL expires' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* API-uploaded file object properties (type: "file_upload").
|
||||
* @see https://developers.notion.com/reference/file-object
|
||||
*/
|
||||
export const FILE_UPLOAD_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'File upload UUID' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* External file object properties (type: "external").
|
||||
* @see https://developers.notion.com/reference/file-object
|
||||
*/
|
||||
export const EXTERNAL_FILE_OUTPUT_PROPERTIES = {
|
||||
url: { type: 'string', description: 'External file URL (never expires)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* File object properties.
|
||||
* @see https://developers.notion.com/reference/file-object
|
||||
*/
|
||||
export const FILE_OUTPUT_PROPERTIES = {
|
||||
type: { type: 'string', description: 'File type: "file", "file_upload", or "external"' },
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Notion-hosted file object (when type is "file")',
|
||||
optional: true,
|
||||
properties: NOTION_FILE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
file_upload: {
|
||||
type: 'object',
|
||||
description: 'API-uploaded file object (when type is "file_upload")',
|
||||
optional: true,
|
||||
properties: FILE_UPLOAD_OUTPUT_PROPERTIES,
|
||||
},
|
||||
external: {
|
||||
type: 'object',
|
||||
description: 'External file object (when type is "external")',
|
||||
optional: true,
|
||||
properties: EXTERNAL_FILE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Emoji object properties.
|
||||
* @see https://developers.notion.com/reference/emoji-object
|
||||
*/
|
||||
export const EMOJI_OUTPUT_PROPERTIES = {
|
||||
type: { type: 'string', description: 'Always "emoji" for standard emojis' },
|
||||
emoji: { type: 'string', description: 'The emoji character' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Custom emoji object properties.
|
||||
* @see https://developers.notion.com/reference/emoji-object
|
||||
*/
|
||||
export const CUSTOM_EMOJI_OUTPUT_PROPERTIES = {
|
||||
type: { type: 'string', description: 'Always "custom_emoji"' },
|
||||
custom_emoji: {
|
||||
type: 'object',
|
||||
description: 'Custom emoji details',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Custom emoji UUID' },
|
||||
name: { type: 'string', description: 'Custom emoji name', optional: true },
|
||||
url: { type: 'string', description: 'URL to custom emoji image', optional: true },
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Icon output (can be emoji, custom_emoji, or file)
|
||||
*/
|
||||
export const ICON_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Page/database icon (emoji, custom_emoji, or file)',
|
||||
optional: true,
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Icon type: "emoji", "custom_emoji", or "file"' },
|
||||
emoji: { type: 'string', description: 'Emoji character (if type is emoji)', optional: true },
|
||||
custom_emoji: {
|
||||
type: 'object',
|
||||
description: 'Custom emoji object (if type is custom_emoji)',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Custom emoji UUID' },
|
||||
name: { type: 'string', description: 'Custom emoji name', optional: true },
|
||||
url: { type: 'string', description: 'URL to custom emoji image', optional: true },
|
||||
},
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Notion-hosted file (if type is file)',
|
||||
optional: true,
|
||||
properties: NOTION_FILE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
external: {
|
||||
type: 'object',
|
||||
description: 'External file (if type is external)',
|
||||
optional: true,
|
||||
properties: EXTERNAL_FILE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Cover output (file object for page/database covers)
|
||||
*/
|
||||
export const COVER_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Page/database cover image',
|
||||
optional: true,
|
||||
properties: FILE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common page object properties from Notion API.
|
||||
* @see https://developers.notion.com/reference/page
|
||||
*/
|
||||
export const PAGE_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "page"' },
|
||||
id: { type: 'string', description: 'Page UUID' },
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
created_by: PARTIAL_USER_OUTPUT,
|
||||
last_edited_by: PARTIAL_USER_OUTPUT,
|
||||
archived: { type: 'boolean', description: 'Whether the page is archived' },
|
||||
in_trash: { type: 'boolean', description: 'Whether the page is in trash' },
|
||||
url: { type: 'string', description: 'Notion page URL' },
|
||||
public_url: {
|
||||
type: 'string',
|
||||
description: 'Public web URL if shared, null otherwise',
|
||||
optional: true,
|
||||
},
|
||||
parent: PARENT_OUTPUT,
|
||||
icon: ICON_OUTPUT,
|
||||
cover: COVER_OUTPUT,
|
||||
properties: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Page property values (structure depends on parent type - database properties or title only)',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete page output definition for array items
|
||||
*/
|
||||
export const PAGE_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Notion page object',
|
||||
properties: PAGE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common database object properties from Notion API.
|
||||
* @see https://developers.notion.com/reference/database
|
||||
*/
|
||||
export const DATABASE_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "database"' },
|
||||
id: { type: 'string', description: 'Database UUID' },
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
created_by: PARTIAL_USER_OUTPUT,
|
||||
last_edited_by: PARTIAL_USER_OUTPUT,
|
||||
title: RICH_TEXT_ARRAY_OUTPUT,
|
||||
description: RICH_TEXT_ARRAY_OUTPUT,
|
||||
icon: ICON_OUTPUT,
|
||||
cover: COVER_OUTPUT,
|
||||
parent: PARENT_OUTPUT,
|
||||
url: { type: 'string', description: 'Notion database URL' },
|
||||
public_url: {
|
||||
type: 'string',
|
||||
description: 'Public web URL if shared, null otherwise',
|
||||
optional: true,
|
||||
},
|
||||
archived: { type: 'boolean', description: 'Whether the database is archived' },
|
||||
in_trash: { type: 'boolean', description: 'Whether the database is in trash' },
|
||||
is_inline: { type: 'boolean', description: 'Whether displayed as inline block or child page' },
|
||||
properties: { type: 'object', description: 'Database properties schema' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete database output definition for array items
|
||||
*/
|
||||
export const DATABASE_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Notion database object',
|
||||
properties: DATABASE_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Block object properties from Notion API.
|
||||
* @see https://developers.notion.com/reference/block
|
||||
*/
|
||||
export const BLOCK_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "block"' },
|
||||
id: { type: 'string', description: 'Block UUID' },
|
||||
parent: PARENT_OUTPUT,
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Block type (paragraph, heading_1, heading_2, heading_3, image, etc.)',
|
||||
},
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
created_by: PARTIAL_USER_OUTPUT,
|
||||
last_edited_by: PARTIAL_USER_OUTPUT,
|
||||
archived: { type: 'boolean', description: 'Whether the block is archived' },
|
||||
in_trash: { type: 'boolean', description: 'Whether the block is in trash' },
|
||||
has_children: { type: 'boolean', description: 'Whether the block has nested blocks' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete block output definition for array items
|
||||
*/
|
||||
export const BLOCK_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Notion block object',
|
||||
properties: BLOCK_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination output properties for list responses.
|
||||
* @see https://developers.notion.com/reference/intro (JSON conventions - Pagination section)
|
||||
*/
|
||||
export const PAGINATION_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "list"' },
|
||||
has_more: { type: 'boolean', description: 'Whether more results are available' },
|
||||
next_cursor: { type: 'string', description: 'Cursor for next page of results', optional: true },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Type of items in results (e.g., "page_or_database", "page_or_data_source")',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Search/query results array output with page items
|
||||
*/
|
||||
export const SEARCH_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of search results (pages and/or databases)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
object: { type: 'string', description: 'Object type: "page" or "database"' },
|
||||
id: { type: 'string', description: 'Object UUID' },
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
created_by: PARTIAL_USER_OUTPUT,
|
||||
last_edited_by: PARTIAL_USER_OUTPUT,
|
||||
archived: { type: 'boolean', description: 'Whether the object is archived' },
|
||||
in_trash: { type: 'boolean', description: 'Whether the object is in trash' },
|
||||
url: { type: 'string', description: 'Object URL' },
|
||||
public_url: { type: 'string', description: 'Public web URL if shared', optional: true },
|
||||
parent: PARENT_OUTPUT,
|
||||
properties: { type: 'object', description: 'Object properties' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Database query results array output with page items
|
||||
*/
|
||||
export const DATABASE_QUERY_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of page objects from the database',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: PAGE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment object properties from Notion API.
|
||||
* @see https://developers.notion.com/reference/comment-object
|
||||
*/
|
||||
export const COMMENT_OUTPUT_PROPERTIES = {
|
||||
object: { type: 'string', description: 'Always "comment"' },
|
||||
id: { type: 'string', description: 'Comment UUID' },
|
||||
parent: PARENT_OUTPUT,
|
||||
discussion_id: { type: 'string', description: 'UUID of the discussion thread' },
|
||||
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
|
||||
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
|
||||
created_by: PARTIAL_USER_OUTPUT,
|
||||
rich_text: RICH_TEXT_ARRAY_OUTPUT,
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Array of block objects (used by append/retrieve block children).
|
||||
*/
|
||||
export const BLOCK_LIST_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of Notion block objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: BLOCK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of comment objects (used by list comments).
|
||||
*/
|
||||
export const COMMENT_LIST_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of Notion comment objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: COMMENT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of user objects (used by list users).
|
||||
*/
|
||||
export const USER_LIST_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'Array of Notion user objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: USER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
export interface NotionReadParams {
|
||||
pageId: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata?: {
|
||||
title?: string
|
||||
lastEditedTime?: string
|
||||
createdTime?: string
|
||||
url?: string
|
||||
// Additional metadata for query/search operations
|
||||
totalResults?: number
|
||||
hasMore?: boolean
|
||||
nextCursor?: string | null
|
||||
results?: any[]
|
||||
// Additional metadata for create operations
|
||||
id?: string
|
||||
properties?: Record<string, any>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface NotionWriteParams {
|
||||
pageId: string
|
||||
content: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionCreatePageParams {
|
||||
parentId: string
|
||||
title?: string
|
||||
content?: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionUpdatePageParams {
|
||||
pageId: string
|
||||
properties: Record<string, any>
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionQueryDatabaseParams {
|
||||
databaseId: string
|
||||
filter?: string
|
||||
sorts?: string
|
||||
pageSize?: number
|
||||
startCursor?: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionSearchParams {
|
||||
query?: string
|
||||
filterType?: string
|
||||
pageSize?: number
|
||||
startCursor?: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionCreateDatabaseParams {
|
||||
parentId: string
|
||||
title: string
|
||||
properties?: Record<string, any>
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionAddDatabaseRowParams {
|
||||
databaseId: string
|
||||
properties: Record<string, any>
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionAppendBlocksParams {
|
||||
blockId: string
|
||||
children: any[] | string
|
||||
after?: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionRetrieveBlockChildrenParams {
|
||||
blockId: string
|
||||
startCursor?: string
|
||||
pageSize?: number
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionUpdateBlockParams {
|
||||
blockId: string
|
||||
block: Record<string, any> | string
|
||||
archived?: boolean
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionDeleteBlockParams {
|
||||
blockId: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionCreateCommentParams {
|
||||
pageId?: string
|
||||
discussionId?: string
|
||||
content: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionListCommentsParams {
|
||||
blockId: string
|
||||
startCursor?: string
|
||||
pageSize?: number
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionListUsersParams {
|
||||
startCursor?: string
|
||||
pageSize?: number
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export interface NotionRetrieveUserParams {
|
||||
userId: string
|
||||
accessToken: string
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { isRecordLike } from '@sim/utils/object'
|
||||
import type { NotionUpdateBlockParams } from '@/tools/notion/types'
|
||||
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface NotionUpdateBlockResponse {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
type: string
|
||||
archived: boolean
|
||||
block: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce the block param into an object, accepting either a JSON string
|
||||
* (when called directly by an agent) or an already-parsed object.
|
||||
*/
|
||||
function parseBlock(block: Record<string, any> | string): Record<string, any> {
|
||||
if (typeof block === 'string') {
|
||||
const parsed = JSON.parse(block)
|
||||
if (!isRecordLike(parsed)) {
|
||||
throw new Error('block must be a JSON object describing the block-type fields to update')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
if (isRecordLike(block)) return block
|
||||
throw new Error('block must be a JSON object describing the block-type fields to update')
|
||||
}
|
||||
|
||||
export const notionUpdateBlockTool: ToolConfig<NotionUpdateBlockParams, NotionUpdateBlockResponse> =
|
||||
{
|
||||
id: 'notion_update_block',
|
||||
name: 'Notion Update Block',
|
||||
description: 'Update the content or archived state of a single Notion block',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
blockId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the block to update',
|
||||
},
|
||||
block: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Block-type object with the fields to update, e.g. {"paragraph": {"rich_text": [...]}}',
|
||||
},
|
||||
archived: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Set to true to archive (delete) the block, or false to restore it',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionUpdateBlockParams) =>
|
||||
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params: NotionUpdateBlockParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionUpdateBlockParams) => {
|
||||
const body: Record<string, any> = parseBlock(params.block)
|
||||
if (params.archived != null) body.archived = params.archived
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
id: data.id,
|
||||
type: data.type ?? '',
|
||||
archived: data.archived ?? false,
|
||||
block: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: BLOCK_OUTPUT_PROPERTIES.id,
|
||||
type: BLOCK_OUTPUT_PROPERTIES.type,
|
||||
archived: BLOCK_OUTPUT_PROPERTIES.archived,
|
||||
block: {
|
||||
type: 'object',
|
||||
description: 'The full updated Notion block object',
|
||||
properties: BLOCK_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const notionUpdateBlockV2Tool: ToolConfig<
|
||||
NotionUpdateBlockParams,
|
||||
NotionUpdateBlockResponse
|
||||
> = {
|
||||
id: 'notion_update_block_v2',
|
||||
name: 'Notion Update Block',
|
||||
description: 'Update the content or archived state of a single Notion block',
|
||||
version: '2.0.0',
|
||||
oauth: notionUpdateBlockTool.oauth,
|
||||
params: notionUpdateBlockTool.params,
|
||||
request: notionUpdateBlockTool.request,
|
||||
transformResponse: notionUpdateBlockTool.transformResponse,
|
||||
outputs: notionUpdateBlockTool.outputs,
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type { NotionResponse, NotionUpdatePageParams } from '@/tools/notion/types'
|
||||
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionUpdatePageTool: ToolConfig<NotionUpdatePageParams, NotionResponse> = {
|
||||
id: 'notion_update_page',
|
||||
name: 'Notion Page Updater',
|
||||
description: 'Update properties of a Notion page',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the Notion page to update',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON object of properties to update',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionUpdatePageParams) => {
|
||||
return `https://api.notion.com/v1/pages/${params.pageId.trim()}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params: NotionUpdatePageParams) => {
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionUpdatePageParams) => ({
|
||||
properties: params.properties,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
// Try to extract the title from properties
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: 'Successfully updated page properties',
|
||||
metadata: {
|
||||
title: pageTitle,
|
||||
pageId: data.id,
|
||||
url: data.url,
|
||||
lastEditedTime: data.last_edited_time,
|
||||
updatedTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Success message confirming page properties update',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Page metadata including title, page ID, URL, and update timestamps',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
pageId: PAGE_OUTPUT_PROPERTIES.id,
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
lastEditedTime: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
updatedTime: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when update was performed',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionUpdatePageV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
last_edited_time: string
|
||||
}
|
||||
}
|
||||
|
||||
export const notionUpdatePageV2Tool: ToolConfig<
|
||||
NotionUpdatePageParams,
|
||||
NotionUpdatePageV2Response
|
||||
> = {
|
||||
id: 'notion_update_page_v2',
|
||||
name: 'Notion Page Updater',
|
||||
description: 'Update properties of a Notion page',
|
||||
version: '2.0.0',
|
||||
oauth: notionUpdatePageTool.oauth,
|
||||
params: notionUpdatePageTool.params,
|
||||
request: notionUpdatePageTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
let pageTitle = 'Untitled'
|
||||
|
||||
if (data.properties?.title) {
|
||||
const titleProperty = data.properties.title
|
||||
if (
|
||||
titleProperty.title &&
|
||||
Array.isArray(titleProperty.title) &&
|
||||
titleProperty.title.length > 0
|
||||
) {
|
||||
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
title: pageTitle,
|
||||
url: data.url,
|
||||
last_edited_time: data.last_edited_time,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: PAGE_OUTPUT_PROPERTIES.id,
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
url: PAGE_OUTPUT_PROPERTIES.url,
|
||||
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Clamp a Notion `page_size` value to the API's supported 1-100 range. Returns
|
||||
* undefined when the input isn't a finite number so callers can omit the param.
|
||||
*/
|
||||
export function clampNotionPageSize(value: unknown): number | undefined {
|
||||
if (value == null || value === '') return undefined
|
||||
const parsed = Number(value)
|
||||
if (!Number.isFinite(parsed)) return undefined
|
||||
return Math.min(Math.max(Math.trunc(parsed), 1), 100)
|
||||
}
|
||||
|
||||
export function formatPropertyValue(property: any): string {
|
||||
switch (property.type) {
|
||||
case 'title':
|
||||
return property.title?.map((t: any) => t.plain_text || '').join('') || ''
|
||||
case 'rich_text':
|
||||
return property.rich_text?.map((t: any) => t.plain_text || '').join('') || ''
|
||||
case 'number':
|
||||
return String(property.number || '')
|
||||
case 'select':
|
||||
return property.select?.name || ''
|
||||
case 'multi_select':
|
||||
return property.multi_select?.map((s: any) => s.name).join(', ') || ''
|
||||
case 'date':
|
||||
return property.date?.start || ''
|
||||
case 'checkbox':
|
||||
return property.checkbox ? 'Yes' : 'No'
|
||||
case 'url':
|
||||
return property.url || ''
|
||||
case 'email':
|
||||
return property.email || ''
|
||||
case 'phone_number':
|
||||
return property.phone_number || ''
|
||||
default:
|
||||
return JSON.stringify(property)
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTitle(properties: Record<string, any>): string {
|
||||
for (const [, value] of Object.entries(properties)) {
|
||||
if (
|
||||
value.type === 'title' &&
|
||||
value.title &&
|
||||
Array.isArray(value.title) &&
|
||||
value.title.length > 0
|
||||
) {
|
||||
return value.title.map((t: any) => t.plain_text || '').join('')
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function extractTitleFromItem(item: any): string {
|
||||
if (item.object === 'page') {
|
||||
// For pages, check properties first
|
||||
if (item.properties?.title?.title && Array.isArray(item.properties.title.title)) {
|
||||
const title = item.properties.title.title.map((t: any) => t.plain_text || '').join('')
|
||||
if (title) return title
|
||||
}
|
||||
// Fallback to page title
|
||||
return item.title || 'Untitled Page'
|
||||
}
|
||||
if (item.object === 'database') {
|
||||
// For databases, get title from title array
|
||||
if (item.title && Array.isArray(item.title)) {
|
||||
return item.title.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
|
||||
}
|
||||
return 'Untitled Database'
|
||||
}
|
||||
return 'Untitled'
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { NotionResponse, NotionWriteParams } from '@/tools/notion/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const notionWriteTool: ToolConfig<NotionWriteParams, NotionResponse> = {
|
||||
id: 'notion_write',
|
||||
name: 'Notion Content Appender',
|
||||
description: 'Append content to a Notion page',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'notion',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Notion OAuth access token',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The UUID of the Notion page to append content to',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The content to append to the page',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: NotionWriteParams) => {
|
||||
return `https://api.notion.com/v1/blocks/${params.pageId.trim()}/children`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params: NotionWriteParams) => {
|
||||
// Validate access token
|
||||
if (!params.accessToken) {
|
||||
throw new Error('Access token is required')
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
body: (params: NotionWriteParams) => ({
|
||||
children: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
type: 'text',
|
||||
text: {
|
||||
content: params.content,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const _data = await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
content: 'Successfully appended content to Notion page',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Success message confirming content was appended to page',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// V2 Tool with API-aligned outputs
|
||||
interface NotionWriteV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
appended: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const notionWriteV2Tool: ToolConfig<NotionWriteParams, NotionWriteV2Response> = {
|
||||
id: 'notion_write_v2',
|
||||
name: 'Notion Content Appender',
|
||||
description: 'Append content to a Notion page',
|
||||
version: '2.0.0',
|
||||
oauth: notionWriteTool.oauth,
|
||||
params: notionWriteTool.params,
|
||||
request: notionWriteTool.request,
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
await response.json()
|
||||
return {
|
||||
success: response.ok,
|
||||
output: {
|
||||
appended: response.ok,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
appended: { type: 'boolean', description: 'Whether content was successfully appended' },
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user