diff --git a/integrations/gsheets/definitions/actions.ts b/integrations/gsheets/definitions/actions.ts index 4759594695d..ddad905b3a5 100644 --- a/integrations/gsheets/definitions/actions.ts +++ b/integrations/gsheets/definitions/actions.ts @@ -438,21 +438,251 @@ const createNamedRangeInSheet = { }, } as const satisfies ActionDef +const _rowSchema = z + .object({ + rowIndex: z.number().title('Row Index').describe('The 1-based index of the row in the sheet.'), + values: z.array(z.string().title('Cell Value')).title('Values').describe('The cell values in the row.'), + }) + .title('Row') + .describe('A row with its index and values.') + +const findRows = { + title: 'Find Rows', + description: + 'Search for rows where a specific column matches a value. Returns all matching rows with their indexes. Handles empty sheets and no matches gracefully by returning an empty array.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + searchColumn: z.string().title('Search Column').describe('The column letter to search in (e.g. "A", "B", "AA").'), + searchValue: z.string().title('Search Value').describe('The value to search for in the specified column.'), + dataRange: z + .string() + .title('Data Range') + .optional() + .describe( + 'Optional A1 notation range to limit the search (e.g. "A1:F100"). If not provided, searches the entire sheet.' + ), + }), + }, + output: { + schema: z.object({ + rows: z.array(_rowSchema).title('Matching Rows').describe('The rows that match the search criteria.'), + totalMatches: z.number().title('Total Matches').describe('The total number of matching rows found.'), + }), + }, +} as const satisfies ActionDef + +const findRow = { + title: 'Find Row (First Match)', + description: + 'Search for the first row where a specific column matches a value. Returns the row data and its index, or null if no match is found.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + searchColumn: z.string().title('Search Column').describe('The column letter to search in (e.g. "A", "B", "AA").'), + searchValue: z.string().title('Search Value').describe('The value to search for in the specified column.'), + dataRange: z + .string() + .title('Data Range') + .optional() + .describe( + 'Optional A1 notation range to limit the search (e.g. "A1:F100"). If not provided, searches the entire sheet.' + ), + }), + }, + output: { + schema: z.object({ + found: z.boolean().title('Found').describe('Whether a matching row was found.'), + row: _rowSchema.nullable().describe('The first matching row, or null if not found.'), + }), + }, +} as const satisfies ActionDef + +const getRow = { + title: 'Get Row', + description: 'Fetch a specific row by its 1-based index. Provides direct row access without A1 notation math.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + rowIndex: z + .number() + .title('Row Index') + .describe('The 1-based row index to retrieve (e.g. 1 for the first row, 2 for the second row).'), + startColumn: z + .string() + .title('Start Column') + .optional() + .default('A') + .describe('The starting column letter (e.g. "A"). Defaults to "A".'), + endColumn: z + .string() + .title('End Column') + .optional() + .describe('The ending column letter (e.g. "Z"). If not provided, returns all columns with data.'), + }), + }, + output: { + schema: z.object({ + found: z.boolean().title('Found').describe('Whether the row exists and has data.'), + row: _rowSchema.nullable().describe('The row data, or null if the row is empty or does not exist.'), + }), + }, +} as const satisfies ActionDef + +const updateRow = { + title: 'Update Row', + description: + 'Update a specific row by its 1-based index with a partial or complete set of values. Only the provided values are updated.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + rowIndex: z.number().title('Row Index').describe('The 1-based row index to update.'), + values: z + .array(z.string().title('Cell Value')) + .title('Values') + .describe('The values to write to the row, starting from the start column.'), + startColumn: z + .string() + .title('Start Column') + .optional() + .default('A') + .describe('The starting column letter for the update (e.g. "A"). Defaults to "A".'), + }), + }, + output: { + schema: z.object({ + updatedRange: z.string().title('Updated Range').describe('The range (in A1 notation) that was updated.'), + updatedCells: z.number().title('Updated Cells').describe('The number of cells updated.'), + }), + }, +} as const satisfies ActionDef + +const insertRowAtIndex = { + title: 'Insert Row at Index', + description: 'Insert a new row at a specific 1-based index. Existing rows at and below the index are shifted down.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + rowIndex: z.number().title('Row Index').describe('The 1-based index where the new row should be inserted.'), + values: z + .array(z.string().title('Cell Value')) + .title('Values') + .optional() + .describe('Optional values to populate the new row.'), + startColumn: z + .string() + .title('Start Column') + .optional() + .default('A') + .describe('The starting column letter for the values (e.g. "A"). Defaults to "A".'), + }), + }, + output: { + schema: z.object({ + insertedRowIndex: z.number().title('Inserted Row Index').describe('The 1-based index of the newly inserted row.'), + }), + }, +} as const satisfies ActionDef + +const deleteRows = { + title: 'Delete Rows', + description: + 'Delete one or more rows by their 1-based indexes. Rows are deleted in reverse order to preserve indexes during deletion.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + rowIndexes: z + .array(z.number().title('Row Index')) + .title('Row Indexes') + .describe('The 1-based row indexes to delete.'), + }), + }, + output: { + schema: z.object({ + deletedCount: z.number().title('Deleted Count').describe('The number of rows deleted.'), + }), + }, +} as const satisfies ActionDef + +const upsertRow = { + title: 'Upsert Row', + description: + 'Update a row if it exists (based on a key column match), or append a new row if no match is found. Useful for maintaining unique records.', + input: { + schema: z.object({ + sheetName: z + .string() + .title('Sheet Name') + .optional() + .describe('The name of the sheet (e.g. "Sheet1"). If not provided, the first visible sheet is used.'), + keyColumn: z + .string() + .title('Key Column') + .describe('The column letter to use for matching (e.g. "A" for ID column).'), + keyValue: z.string().title('Key Value').describe('The value to match in the key column.'), + values: z.array(z.string().title('Cell Value')).title('Values').describe('The values to write to the row.'), + startColumn: z + .string() + .title('Start Column') + .optional() + .default('A') + .describe('The starting column letter for the values (e.g. "A"). Defaults to "A".'), + }), + }, + output: { + schema: z.object({ + action: z.enum(['updated', 'inserted']).title('Action').describe('Whether the row was updated or inserted.'), + rowIndex: z.number().title('Row Index').describe('The 1-based index of the affected row.'), + }), + }, +} as const satisfies ActionDef + export const actions = { addSheet, appendValues, clearValues, createNamedRangeInSheet, + deleteRows, deleteSheet, + findRow, + findRows, getAllSheetsInSpreadsheet, getInfoSpreadsheet, getNamedRanges, getProtectedRanges, + getRow, getValues, + insertRowAtIndex, moveSheetHorizontally, protectNamedRange, renameSheet, setSheetVisibility, unprotectRange, + updateRow, + upsertRow, setValues, } as const satisfies ActionDefinitions diff --git a/integrations/gsheets/integration.definition.ts b/integrations/gsheets/integration.definition.ts index cce63159223..6809bb968fc 100644 --- a/integrations/gsheets/integration.definition.ts +++ b/integrations/gsheets/integration.definition.ts @@ -13,7 +13,7 @@ import { } from './definitions' export const INTEGRATION_NAME = 'gsheets' -export const INTEGRATION_VERSION = '2.0.1' +export const INTEGRATION_VERSION = '2.1.0' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, diff --git a/integrations/gsheets/src/actions/implementations/delete-rows.ts b/integrations/gsheets/src/actions/implementations/delete-rows.ts new file mode 100644 index 00000000000..5f5a6919f5d --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/delete-rows.ts @@ -0,0 +1,21 @@ +import { wrapAction } from '../action-wrapper' + +export const deleteRows = wrapAction( + { actionName: 'deleteRows', errorMessageWhenFailed: 'Failed to delete rows' }, + async ({ googleClient }, { sheetName, rowIndexes }) => { + if (rowIndexes.length === 0) { + return { deletedCount: 0 } + } + + const { sheetId } = await googleClient.getSheetIdByName(sheetName) + + await googleClient.deleteRowsFromSheet({ + sheetId, + rowIndexes, + }) + + return { + deletedCount: rowIndexes.length, + } + } +) diff --git a/integrations/gsheets/src/actions/implementations/find-row.ts b/integrations/gsheets/src/actions/implementations/find-row.ts new file mode 100644 index 00000000000..afbb070b03c --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/find-row.ts @@ -0,0 +1,43 @@ +import { wrapAction } from '../action-wrapper' +import { buildSheetPrefix, columnLetterToIndex } from './row-utils' + +export const findRow = wrapAction( + { actionName: 'findRow', errorMessageWhenFailed: 'Failed to find row' }, + async ({ googleClient }, { sheetName, searchColumn, searchValue, dataRange }) => { + const { sheetTitle } = await googleClient.getSheetIdByName(sheetName) + const prefix = buildSheetPrefix(sheetTitle) + + const rangeA1 = dataRange ? `${prefix}${dataRange.split('!').pop()}` : `${prefix}A:ZZ` + + let values: string[][] = [] + try { + const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' }) + values = result.values ?? [] + } catch { + return { found: false, row: null } + } + + if (values.length === 0) { + return { found: false, row: null } + } + + const searchColumnIndex = columnLetterToIndex(searchColumn) + + for (const [i, row] of values.entries()) { + const rowValues = row ?? [] + const cellValue = rowValues[searchColumnIndex] ?? '' + + if (cellValue === searchValue) { + return { + found: true, + row: { + rowIndex: i + 1, + values: rowValues.map(String), + }, + } + } + } + + return { found: false, row: null } + } +) diff --git a/integrations/gsheets/src/actions/implementations/find-rows.ts b/integrations/gsheets/src/actions/implementations/find-rows.ts new file mode 100644 index 00000000000..aa8df1d1931 --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/find-rows.ts @@ -0,0 +1,44 @@ +import { wrapAction } from '../action-wrapper' +import { buildSheetPrefix, columnLetterToIndex } from './row-utils' + +export const findRows = wrapAction( + { actionName: 'findRows', errorMessageWhenFailed: 'Failed to find rows' }, + async ({ googleClient }, { sheetName, searchColumn, searchValue, dataRange }) => { + const { sheetTitle } = await googleClient.getSheetIdByName(sheetName) + const prefix = buildSheetPrefix(sheetTitle) + + const rangeA1 = dataRange ? `${prefix}${dataRange.split('!').pop()}` : `${prefix}A:ZZ` + + let values: string[][] = [] + try { + const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' }) + values = result.values ?? [] + } catch { + return { rows: [], totalMatches: 0 } + } + + if (values.length === 0) { + return { rows: [], totalMatches: 0 } + } + + const searchColumnIndex = columnLetterToIndex(searchColumn) + const rows: Array<{ rowIndex: number; values: string[] }> = [] + + for (const [i, row] of values.entries()) { + const rowValues = row ?? [] + const cellValue = rowValues[searchColumnIndex] ?? '' + + if (cellValue === searchValue) { + rows.push({ + rowIndex: i + 1, + values: rowValues.map(String), + }) + } + } + + return { + rows, + totalMatches: rows.length, + } + } +) diff --git a/integrations/gsheets/src/actions/implementations/get-row.ts b/integrations/gsheets/src/actions/implementations/get-row.ts new file mode 100644 index 00000000000..2fb369f60ac --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/get-row.ts @@ -0,0 +1,36 @@ +import { wrapAction } from '../action-wrapper' +import { buildRowRange } from './row-utils' + +export const getRow = wrapAction( + { actionName: 'getRow', errorMessageWhenFailed: 'Failed to get row' }, + async ({ googleClient }, { sheetName, rowIndex, startColumn, endColumn }) => { + const { sheetTitle } = await googleClient.getSheetIdByName(sheetName) + + const rangeA1 = buildRowRange({ + sheetTitle, + rowIndex, + startColumn: startColumn ?? 'A', + endColumn, + }) + + let values: string[][] = [] + try { + const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' }) + values = result.values ?? [] + } catch { + return { found: false, row: null } + } + + if (values.length === 0 || !values[0] || values[0].length === 0) { + return { found: false, row: null } + } + + return { + found: true, + row: { + rowIndex, + values: values[0].map(String), + }, + } + } +) diff --git a/integrations/gsheets/src/actions/implementations/insert-row-at-index.ts b/integrations/gsheets/src/actions/implementations/insert-row-at-index.ts new file mode 100644 index 00000000000..576f2b389cd --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/insert-row-at-index.ts @@ -0,0 +1,39 @@ +import { wrapAction } from '../action-wrapper' +import { buildRowRange, indexToColumnLetter, columnLetterToIndex } from './row-utils' + +export const insertRowAtIndex = wrapAction( + { actionName: 'insertRowAtIndex', errorMessageWhenFailed: 'Failed to insert row at index' }, + async ({ googleClient }, { sheetName, rowIndex, values, startColumn }) => { + const { sheetId, sheetTitle } = await googleClient.getSheetIdByName(sheetName) + + await googleClient.insertRows({ + sheetId, + startIndex: rowIndex - 1, + numberOfRows: 1, + }) + + if (values && values.length > 0) { + const start = startColumn ?? 'A' + const startColIndex = columnLetterToIndex(start) + const endColIndex = startColIndex + values.length - 1 + const endColumn = indexToColumnLetter(endColIndex) + + const rangeA1 = buildRowRange({ + sheetTitle, + rowIndex, + startColumn: start, + endColumn, + }) + + await googleClient.updateValuesInSpreadsheetRange({ + rangeA1, + values: [values], + majorDimension: 'ROWS', + }) + } + + return { + insertedRowIndex: rowIndex, + } + } +) diff --git a/integrations/gsheets/src/actions/implementations/row-utils.test.ts b/integrations/gsheets/src/actions/implementations/row-utils.test.ts new file mode 100644 index 00000000000..8b4b0510efc --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/row-utils.test.ts @@ -0,0 +1,129 @@ +import { describe, it, expect } from 'vitest' +import { + columnLetterToIndex, + indexToColumnLetter, + buildSheetPrefix, + buildRowRange, + buildColumnRange, +} from './row-utils' + +describe.concurrent('columnLetterToIndex', () => { + it('converts single letters A-Z to indices 0-25', () => { + expect(columnLetterToIndex('A')).toBe(0) + expect(columnLetterToIndex('B')).toBe(1) + expect(columnLetterToIndex('Z')).toBe(25) + }) + + it('converts double letters starting with AA', () => { + expect(columnLetterToIndex('AA')).toBe(26) + expect(columnLetterToIndex('AB')).toBe(27) + expect(columnLetterToIndex('AZ')).toBe(51) + }) + + it('converts BA and beyond', () => { + expect(columnLetterToIndex('BA')).toBe(52) + expect(columnLetterToIndex('ZZ')).toBe(701) + }) + + it('is case-insensitive', () => { + expect(columnLetterToIndex('a')).toBe(0) + expect(columnLetterToIndex('aa')).toBe(26) + expect(columnLetterToIndex('Aa')).toBe(26) + }) +}) + +describe.concurrent('indexToColumnLetter', () => { + it('converts indices 0-25 to single letters A-Z', () => { + expect(indexToColumnLetter(0)).toBe('A') + expect(indexToColumnLetter(1)).toBe('B') + expect(indexToColumnLetter(25)).toBe('Z') + }) + + it('converts index 26+ to double letters', () => { + expect(indexToColumnLetter(26)).toBe('AA') + expect(indexToColumnLetter(27)).toBe('AB') + expect(indexToColumnLetter(51)).toBe('AZ') + }) + + it('converts higher indices correctly', () => { + expect(indexToColumnLetter(52)).toBe('BA') + expect(indexToColumnLetter(701)).toBe('ZZ') + }) +}) + +describe.concurrent('columnLetterToIndex and indexToColumnLetter roundtrip', () => { + it('indexToColumnLetter inverts columnLetterToIndex', () => { + const letters = ['A', 'Z', 'AA', 'AZ', 'BA', 'ZZ'] + for (const letter of letters) { + expect(indexToColumnLetter(columnLetterToIndex(letter))).toBe(letter) + } + }) + + it('columnLetterToIndex inverts indexToColumnLetter', () => { + const indices = [0, 25, 26, 51, 52, 701] + for (const index of indices) { + expect(columnLetterToIndex(indexToColumnLetter(index))).toBe(index) + } + }) +}) + +describe.concurrent('buildSheetPrefix', () => { + it('returns empty string when no sheet title', () => { + expect(buildSheetPrefix()).toBe('') + expect(buildSheetPrefix(undefined)).toBe('') + }) + + it('returns simple prefix for alphanumeric titles', () => { + expect(buildSheetPrefix('Sheet1')).toBe('Sheet1!') + expect(buildSheetPrefix('Data')).toBe('Data!') + }) + + it('quotes titles containing spaces', () => { + expect(buildSheetPrefix('My Sheet')).toBe("'My Sheet'!") + }) + + it('quotes and escapes titles containing apostrophes', () => { + expect(buildSheetPrefix("It's")).toBe("'It''s'!") + expect(buildSheetPrefix("Don't stop")).toBe("'Don''t stop'!") + }) +}) + +describe.concurrent('buildRowRange', () => { + it('builds simple row range with defaults', () => { + expect(buildRowRange({ rowIndex: 1 })).toBe('A1:1') + }) + + it('builds row range with explicit start and end columns', () => { + expect(buildRowRange({ rowIndex: 5, startColumn: 'B', endColumn: 'D' })).toBe('B5:D5') + }) + + it('includes sheet prefix when provided', () => { + expect(buildRowRange({ sheetTitle: 'Data', rowIndex: 3, startColumn: 'A', endColumn: 'C' })).toBe('Data!A3:C3') + }) + + it('handles sheet titles requiring quotes', () => { + expect(buildRowRange({ sheetTitle: 'My Data', rowIndex: 1, endColumn: 'B' })).toBe("'My Data'!A1:B1") + }) +}) + +describe.concurrent('buildColumnRange', () => { + it('builds single column range with defaults', () => { + expect(buildColumnRange({})).toBe('A1:A100000') + }) + + it('builds range for specified columns', () => { + expect(buildColumnRange({ startColumn: 'B', endColumn: 'D' })).toBe('B1:D100000') + }) + + it('uses startColumn as endColumn when endColumn not specified', () => { + expect(buildColumnRange({ startColumn: 'C' })).toBe('C1:C100000') + }) + + it('respects custom maxRow', () => { + expect(buildColumnRange({ startColumn: 'A', endColumn: 'B', maxRow: 500 })).toBe('A1:B500') + }) + + it('includes sheet prefix when provided', () => { + expect(buildColumnRange({ sheetTitle: 'Sales', startColumn: 'A', endColumn: 'C' })).toBe('Sales!A1:C100000') + }) +}) diff --git a/integrations/gsheets/src/actions/implementations/row-utils.ts b/integrations/gsheets/src/actions/implementations/row-utils.ts new file mode 100644 index 00000000000..10663f27dae --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/row-utils.ts @@ -0,0 +1,58 @@ +const ASCII_UPPERCASE_A = 65 +const BASE_26 = 26 + +export const columnLetterToIndex = (col: string): number => + col + .toUpperCase() + .split('') + .reduce((acc, char) => acc * BASE_26 + char.charCodeAt(0) - ASCII_UPPERCASE_A + 1, 0) - 1 + +export const indexToColumnLetter = (index: number): string => { + let letter = '' + let i = index + while (i >= 0) { + letter = String.fromCharCode((i % BASE_26) + ASCII_UPPERCASE_A) + letter + i = Math.floor(i / BASE_26) - 1 + } + return letter +} + +export const buildSheetPrefix = (sheetTitle?: string): string => { + if (!sheetTitle) { + return '' + } + const needsQuotes = sheetTitle.includes(' ') || sheetTitle.includes("'") + return needsQuotes ? `'${sheetTitle.replace(/'/g, "''")}'!` : `${sheetTitle}!` +} + +export const buildRowRange = ({ + sheetTitle, + rowIndex, + startColumn = 'A', + endColumn, +}: { + sheetTitle?: string + rowIndex: number + startColumn?: string + endColumn?: string +}): string => { + const prefix = buildSheetPrefix(sheetTitle) + const end = endColumn ? `${endColumn}${rowIndex}` : `${rowIndex}` + return `${prefix}${startColumn}${rowIndex}:${end}` +} + +export const buildColumnRange = ({ + sheetTitle, + startColumn = 'A', + endColumn, + maxRow = 100000, +}: { + sheetTitle?: string + startColumn?: string + endColumn?: string + maxRow?: number +}): string => { + const prefix = buildSheetPrefix(sheetTitle) + const end = endColumn ?? startColumn + return `${prefix}${startColumn}1:${end}${maxRow}` +} diff --git a/integrations/gsheets/src/actions/implementations/update-row.ts b/integrations/gsheets/src/actions/implementations/update-row.ts new file mode 100644 index 00000000000..59bb0352cbe --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/update-row.ts @@ -0,0 +1,32 @@ +import { wrapAction } from '../action-wrapper' +import { buildRowRange, indexToColumnLetter, columnLetterToIndex } from './row-utils' + +export const updateRow = wrapAction( + { actionName: 'updateRow', errorMessageWhenFailed: 'Failed to update row' }, + async ({ googleClient }, { sheetName, rowIndex, values, startColumn }) => { + const { sheetTitle } = await googleClient.getSheetIdByName(sheetName) + + const start = startColumn ?? 'A' + const startColIndex = columnLetterToIndex(start) + const endColIndex = startColIndex + values.length - 1 + const endColumn = indexToColumnLetter(endColIndex) + + const rangeA1 = buildRowRange({ + sheetTitle, + rowIndex, + startColumn: start, + endColumn, + }) + + const result = await googleClient.updateValuesInSpreadsheetRange({ + rangeA1, + values: [values], + majorDimension: 'ROWS', + }) + + return { + updatedRange: result.updatedRange, + updatedCells: result.updatedCells, + } + } +) diff --git a/integrations/gsheets/src/actions/implementations/upsert-row.ts b/integrations/gsheets/src/actions/implementations/upsert-row.ts new file mode 100644 index 00000000000..d603e9f0255 --- /dev/null +++ b/integrations/gsheets/src/actions/implementations/upsert-row.ts @@ -0,0 +1,75 @@ +import { wrapAction } from '../action-wrapper' +import { buildSheetPrefix, buildRowRange, columnLetterToIndex, indexToColumnLetter } from './row-utils' + +export const upsertRow = wrapAction( + { actionName: 'upsertRow', errorMessageWhenFailed: 'Failed to upsert row' }, + async ({ googleClient }, { sheetName, keyColumn, keyValue, values, startColumn }) => { + const { sheetTitle } = await googleClient.getSheetIdByName(sheetName) + const prefix = buildSheetPrefix(sheetTitle) + + const rangeA1 = `${prefix}A:ZZ` + + let existingValues: string[][] = [] + try { + const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' }) + existingValues = result.values ?? [] + } catch { + existingValues = [] + } + + const keyColumnIndex = columnLetterToIndex(keyColumn) + let matchingRowIndex: number | null = null + + for (const [i, row] of existingValues.entries()) { + const rowValues = row ?? [] + const cellValue = rowValues[keyColumnIndex] ?? '' + + if (cellValue === keyValue) { + matchingRowIndex = i + 1 + break + } + } + + const start = startColumn ?? 'A' + const startColIndex = columnLetterToIndex(start) + const endColIndex = startColIndex + values.length - 1 + const endColumn = indexToColumnLetter(endColIndex) + + if (matchingRowIndex !== null) { + const updateRangeA1 = buildRowRange({ + sheetTitle, + rowIndex: matchingRowIndex, + startColumn: start, + endColumn, + }) + + await googleClient.updateValuesInSpreadsheetRange({ + rangeA1: updateRangeA1, + values: [values], + majorDimension: 'ROWS', + }) + + return { + action: 'updated' as const, + rowIndex: matchingRowIndex, + } + } + + const appendRangeA1 = `${prefix}${start}:${endColumn}` + + const appendResult = await googleClient.appendValuesToSpreadsheetRange({ + rangeA1: appendRangeA1, + values: [values], + majorDimension: 'ROWS', + }) + + const updatedRange = appendResult.updates.updatedRange + const rowMatch = updatedRange.match(/:?[A-Z]+(\d+)$/) + const insertedRowIndex = rowMatch?.[1] ? parseInt(rowMatch[1], 10) : existingValues.length + 1 + + return { + action: 'inserted' as const, + rowIndex: insertedRowIndex, + } + } +) diff --git a/integrations/gsheets/src/actions/index.ts b/integrations/gsheets/src/actions/index.ts index e5f260909bb..b547f5fead4 100644 --- a/integrations/gsheets/src/actions/index.ts +++ b/integrations/gsheets/src/actions/index.ts @@ -2,18 +2,25 @@ import { addSheet } from './implementations/add-sheet' import { appendValues } from './implementations/append-values' import { clearValues } from './implementations/clear-values' import { createNamedRangeInSheet } from './implementations/create-named-range-in-sheet' +import { deleteRows } from './implementations/delete-rows' import { deleteSheet } from './implementations/delete-sheet' +import { findRow } from './implementations/find-row' +import { findRows } from './implementations/find-rows' import { getAllSheetsInSpreadsheet } from './implementations/get-all-sheets-in-spreadsheet' import { getInfoSpreadsheet } from './implementations/get-info-spread-sheet' import { getNamedRanges } from './implementations/get-named-ranges' import { getProtectedRanges } from './implementations/get-protected-ranges' +import { getRow } from './implementations/get-row' import { getValues } from './implementations/get-values' +import { insertRowAtIndex } from './implementations/insert-row-at-index' import { moveSheetHorizontally } from './implementations/move-sheet-horizontally' import { protectNamedRange } from './implementations/protect-named-range' import { renameSheet } from './implementations/rename-sheet' import { setSheetVisibility } from './implementations/set-sheet-visibility' import { setValues } from './implementations/set-values' import { unprotectRange } from './implementations/unprotect-range' +import { updateRow } from './implementations/update-row' +import { upsertRow } from './implementations/upsert-row' import * as bp from '.botpress' export default { @@ -21,16 +28,23 @@ export default { appendValues, clearValues, createNamedRangeInSheet, + deleteRows, deleteSheet, + findRow, + findRows, getAllSheetsInSpreadsheet, getInfoSpreadsheet, getNamedRanges, getProtectedRanges, + getRow, getValues, + insertRowAtIndex, moveSheetHorizontally, protectNamedRange, renameSheet, setSheetVisibility, - unprotectRange, setValues, + unprotectRange, + updateRow, + upsertRow, } as const satisfies bp.IntegrationProps['actions'] diff --git a/integrations/gsheets/src/google-api/google-client.ts b/integrations/gsheets/src/google-api/google-client.ts index 12de544ca4b..82d5755cf77 100644 --- a/integrations/gsheets/src/google-api/google-client.ts +++ b/integrations/gsheets/src/google-api/google-client.ts @@ -305,4 +305,81 @@ export class GoogleClient { return `spreadsheet "${title}"` + (sheetsTitles.length ? ` with sheets "${sheetsTitles.join('", "')}"` : '') } + + @handleErrors('Failed to get sheet ID by name') + public async getSheetIdByName(sheetName?: string): Promise<{ sheetId: number; sheetTitle: string }> { + const meta = await this.getSpreadsheetMetadata({ fields: 'sheets.properties' }) + const sheets = meta.sheets ?? [] + + if (!sheetName) { + const firstVisibleSheet = sheets.find((s) => !s.properties?.hidden) + if (!firstVisibleSheet?.properties) { + throw new Error('No visible sheets found in spreadsheet') + } + return { + sheetId: firstVisibleSheet.properties.sheetId ?? 0, + sheetTitle: firstVisibleSheet.properties.title ?? '', + } + } + + const sheet = sheets.find((s) => s.properties?.title === sheetName) + if (!sheet?.properties) { + throw new Error(`Sheet "${sheetName}" not found`) + } + return { + sheetId: sheet.properties.sheetId ?? 0, + sheetTitle: sheet.properties.title ?? '', + } + } + + @handleErrors('Failed to insert rows') + public async insertRows({ + sheetId, + startIndex, + numberOfRows = 1, + }: { + sheetId: number + startIndex: number + numberOfRows?: number + }) { + await this._sheetsClient.spreadsheets.batchUpdate({ + spreadsheetId: this._spreadsheetId, + requestBody: { + requests: [ + { + insertDimension: { + range: { + sheetId, + dimension: 'ROWS', + startIndex, + endIndex: startIndex + numberOfRows, + }, + inheritFromBefore: startIndex > 0, + }, + }, + ], + }, + }) + } + + @handleErrors('Failed to delete rows') + public async deleteRowsFromSheet({ sheetId, rowIndexes }: { sheetId: number; rowIndexes: number[] }) { + const sortedIndexes = [...rowIndexes].sort((a, b) => b - a) + + const requests = sortedIndexes.map((rowIndex) => ({ + deleteDimension: { + range: { + sheetId, + dimension: 'ROWS' as const, + startIndex: rowIndex - 1, + endIndex: rowIndex, + }, + }, + })) + + await this._sheetsClient.spreadsheets.batchUpdate({ + spreadsheetId: this._spreadsheetId, + requestBody: { requests }, + }) + } } diff --git a/integrations/notion/definitions/actions.ts b/integrations/notion/definitions/actions.ts index eaf0db1a248..3ad76997959 100644 --- a/integrations/notion/definitions/actions.ts +++ b/integrations/notion/definitions/actions.ts @@ -1,41 +1,81 @@ import * as sdk from '@botpress/sdk' export const actions = { - addPageToDb: { - title: 'Create Page in Database', - description: 'Add a new page to a database in Notion', + createPage: { + title: 'Create Page', + description: 'Create a new page in Notion', input: { schema: sdk.z.object({ - databaseId: sdk.z + parentType: sdk.z.enum(['data source', 'page']).title('Parent Type').describe('The type of the parent'), + parentId: sdk.z .string() .min(1) - .title('Database ID') - .describe('The ID of the database to add the page to. Can be found in the URL of the database'), - pageProperties: sdk.z - .record(sdk.z.string(), sdk.z.object({}).passthrough()) - .title('Page Properties') - .describe("The values of the page's properties. Must match the parent database's properties"), + .title('Parent ID') + .describe('The ID of the parent to add the page to. Can be found in the URL of the parent'), + title: sdk.z.string().title('Page Title').describe('The title of the page'), + dataSourceTitleName: sdk.z + .string() + .title('Data Source Title Name') + .describe('The name of the title property in the data source. If not provided, the default is "Name".') + .optional() + .default('Name'), }), }, output: { - schema: sdk.z.object({}), + schema: sdk.z.object({ + pageId: sdk.z.string().title('Page ID').describe('The ID of the page that was created'), + }), }, }, - addCommentToPage: { - title: 'Add Comment to Page', - description: 'Add a comment to a page in Notion', + updatePageProperties: { + title: 'Update Page Properties', + description: 'Update one or more properties on a Notion page using raw Notion properties JSON', input: { schema: sdk.z.object({ pageId: sdk.z .string() .min(1) .title('Page ID') - .describe('The ID of the page to add the comment to. Can be found in the URL of the page'), + .describe('The ID of the page to update. Can be found in the page URL'), + propertiesJson: sdk.z + .string() + .min(2) + .title('Properties (JSON)') + .describe( + 'Stringified JSON object for the Notion properties payload (same format as Notion pages.update API endpoint but without the "properties" key). Check the Notion API documentation for the correct format. https://developers.notion.com/reference/patch-page' + ) + .placeholder('{"In stock": { "checkbox": true }}'), + }), + }, + output: { + schema: sdk.z.object({ + pageId: sdk.z.string().title('Page ID').describe('The updated page ID'), + }), + }, + }, + addComment: { + title: 'Add Comment', + description: 'Add a comment to a page, block, or discussion in Notion', + input: { + schema: sdk.z.object({ + parentType: sdk.z.enum(['page', 'block', 'discussion']).title('Parent Type').describe('The type of the parent'), + parentId: sdk.z + .string() + .min(1) + .title('Parent ID') + .describe('The ID of the parent to add the comment to. Can be found in the URL of the parent'), commentBody: sdk.z.string().min(1).title('Comment Body').describe('Must be plain text'), }), }, output: { - schema: sdk.z.object({}), + schema: sdk.z.object({ + commentId: sdk.z.string().title('Comment ID').describe('The ID of the comment that was created'), + discussionId: sdk.z + .string() + .optional() + .title('Discussion ID') + .describe('The ID of the discussion that was created'), + }), }, }, deleteBlock: { @@ -51,7 +91,9 @@ export const actions = { }), }, output: { - schema: sdk.z.object({}), + schema: sdk.z.object({ + blockId: sdk.z.string().title('Block ID').describe('The ID of the block that was deleted'), + }), }, }, getDb: { @@ -68,33 +110,170 @@ export const actions = { }, output: { schema: sdk.z.object({ - object: sdk.z.string().title('Database Object').describe('A stringified representation of the database'), + object: sdk.z.string().optional().title('Database Object').describe('The type of object returned'), + dataSources: sdk.z + .array( + sdk.z.object({ + id: sdk.z.string().title('Data Source ID').describe('The ID of the data source'), + name: sdk.z.string().title('Data Source Name').describe('The name of the data source'), + }) + ) + .title('Data Sources') + .describe('List of data sources in the database'), + }), + }, + }, + getDataSource: { + title: 'Get Data Source', + description: 'Get a data source from Notion', + input: { + schema: sdk.z.object({ + dataSourceId: sdk.z + .string() + .min(1) + .title('Data Source ID') + .describe('The ID of the data source to fetch. Can be found in the URL of the data source'), + }), + }, + output: { + schema: sdk.z.object({ + object: sdk.z.string().title('Data Source Object').describe('A stringified representation of the data source'), properties: sdk.z .record(sdk.z.string(), sdk.z.object({}).passthrough()) - .title('Database Properties') - .describe('Schema of properties for the database as they appear in Notion'), - structure: sdk.z + .title('Data Source Properties') + .describe('Schema of properties for the data source as they appear in Notion'), + pages: sdk.z + .array( + sdk.z.object({ + id: sdk.z.string().title('Page ID').describe('The ID of the page'), + title: sdk.z.string().title('Page Title').describe('The title of the page'), + pageProperties: sdk.z + .record(sdk.z.string(), sdk.z.object({}).passthrough()) + .title('Page Properties') + .describe('Schema of properties for the page as they appear in Notion'), + }) + ) + .title('Pages') + .describe('List of pages in the data source'), + }), + }, + }, + getPage: { + title: 'Get Page', + description: 'Get a page from Notion', + input: { + schema: sdk.z.object({ + pageId: sdk.z .string() - .title('Database Structure') - .describe('A stringified representation of the database structure'), + .min(1) + .title('Page ID') + .describe('The ID of the page to fetch. Can be found in the URL of the page'), + }), + }, + output: { + schema: sdk.z.object({ + object: sdk.z.string().optional().title('Result Object').describe('The type of object returned'), + id: sdk.z.string().optional().title('Result ID').describe('The ID of the object returned'), + created_time: sdk.z.string().optional().title('Created Time').describe('The time the object was created'), + parent: sdk.z.object({}).passthrough().optional().title('Parent').describe('The parent of the object'), + created_by: sdk.z + .object({ + object: sdk.z.string().optional().title('Created By Object').describe('The type of object returned'), + id: sdk.z.string().optional().title('Created By ID').describe('The ID of the object returned'), + }) + .optional() + .title('Created By') + .describe('The user who created the object'), + archived: sdk.z.boolean().optional().title('Archived').describe('Whether the object is archived'), + properties: sdk.z + .record(sdk.z.string(), sdk.z.object({}).passthrough()) + .optional() + .title('Page Properties') + .describe('Schema of properties for the page as they appear in Notion'), }), }, }, - addCommentToDiscussion: { - title: 'Add Comment to Discussion', - description: 'Add a comment to a discussion in Notion', + appendBlocksToPage: { + title: 'Append Blocks to Page', + description: 'Append a markdown text to a page in Notion. The markdown text will be converted to notion blocks.', input: { schema: sdk.z.object({ - discussionId: sdk.z + pageId: sdk.z .string() .min(1) - .title('Discussion ID') - .describe('The ID of the discussion to add the comment to. Can be found in the URL of the discussion'), - commentBody: sdk.z.string().min(1).title('Comment Body').describe('Must be plain text'), + .title('Page ID') + .describe('The ID of the page to append the blocks to. Can be found in the URL of the page'), + markdownText: sdk.z.string().title('Markdown Text').describe('The markdown text to append to the page'), + }), + }, + output: { + schema: sdk.z.object({ + pageId: sdk.z.string().title('Page ID').describe('The ID of the page where blocks were appended'), + blockIds: sdk.z.array(sdk.z.string()).title('Block IDs').describe('The IDs of the blocks that were created'), + }), + }, + }, + searchByTitle: { + title: 'Search by Title', + description: + 'Search for pages and databases in Notion. Optionally filter by title. Only returns items that have been shared with the integration.', + input: { + schema: sdk.z.object({ + title: sdk.z + .string() + .optional() + .title('Title') + .describe( + 'Optional search query to match against page and database titles. If not provided, returns all accessible pages and databases.' + ), + }), + }, + output: { + schema: sdk.z.object({ + results: sdk.z + .array( + sdk.z.object({ + id: sdk.z.string().title('ID').describe('The ID of the page or database'), + title: sdk.z.string().title('Title').describe('The title of the page or database'), + type: sdk.z.string().title('Type').describe('The type of the result (page or database)'), + url: sdk.z.string().title('URL').describe('The URL to the page or database'), + }) + ) + .title('Results') + .describe('Array of pages and databases matching the search query'), + }), + }, + }, + getPageContent: { + title: 'Get Page Content', + description: 'Get the content blocks of a page or block in Notion', + input: { + schema: sdk.z.object({ + pageId: sdk.z + .string() + .min(1) + .title('Page or Block ID') + .describe('The ID of the page or block to fetch content from. Can be found in the URL'), }), }, output: { - schema: sdk.z.object({}), + schema: sdk.z.object({ + blocks: sdk.z + .array( + sdk.z.object({ + blockId: sdk.z.string().title('Block ID').describe('The unique ID of the block'), + parentId: sdk.z.string().optional().title('Parent ID').describe('The ID of the parent page or block'), + type: sdk.z.string().title('Type').describe('The type of the block (paragraph, heading_1, etc.)'), + hasChildren: sdk.z.boolean().title('Has Children').describe('Whether the block has nested child blocks'), + richText: sdk.z + .array(sdk.z.object({}).passthrough()) + .title('Rich Text') + .describe('The rich text content of the block with formatting information'), + }) + ) + .title('Blocks') + .describe('Array of content blocks from the page'), + }), }, }, } as const satisfies sdk.IntegrationDefinitionProps['actions'] diff --git a/integrations/notion/definitions/configuration.ts b/integrations/notion/definitions/configuration.ts index 9a840a53a6c..34419bd80c7 100644 --- a/integrations/notion/definitions/configuration.ts +++ b/integrations/notion/definitions/configuration.ts @@ -18,11 +18,19 @@ export const configurations = { title: 'Manual configuration with a custom Notion integration', description: 'Configure the integration using a Notion integration token.', schema: sdk.z.object({ - authToken: sdk.z + internalIntegrationSecret: sdk.z .string() .min(1) - .title('Notion Integration Token') - .describe('Can be found on Notion in your integration settings.'), + .title('Internal Integration Secret') + .describe('Can be found on Notion in your integration settings under the Configuration tab.'), + webhookVerificationSecret: sdk.z + .string() + .min(1) + .optional() + .title('Webhook Verification Secret') + .describe( + 'Note: Requires saving the integration in order to generate a new secret. Once the integration has been saved, configure the notion webhook, verify the webhook url, and copy the secret from the bot logs.' + ), }), }, } as const satisfies sdk.IntegrationDefinitionProps['configurations'] diff --git a/integrations/notion/definitions/events.ts b/integrations/notion/definitions/events.ts new file mode 100644 index 00000000000..c37230aa399 --- /dev/null +++ b/integrations/notion/definitions/events.ts @@ -0,0 +1,35 @@ +import * as sdk from '@botpress/sdk' + +export const BASE_EVENT_PAYLOAD = sdk.z.object({ + workspace_id: sdk.z.string().min(1), + type: sdk.z.string().min(1), + entity: sdk.z.object({ + type: sdk.z.enum(['page', 'block', 'database', 'comment']), + id: sdk.z.string().min(1), + }), + data: sdk.z.object({}).passthrough(), +}) + +export const events = { + commentCreated: { + title: 'Comment Created', + description: 'A comment was created in Notion', + schema: BASE_EVENT_PAYLOAD.extend({ + type: sdk.z.literal('comment.created').title('Type').describe('The type of event'), + entity: BASE_EVENT_PAYLOAD.shape.entity + .extend({ + type: sdk.z.literal('comment'), + }) + .title('Entity') + .describe('The entity that the event is related to'), + workspace_id: sdk.z.string().min(1).title('Workspace ID').describe('The ID of the Notion workspace'), + workspace_name: sdk.z.string().min(1).title('Workspace Name').describe('The name of the Notion workspace'), + data: sdk.z + .object({ + page_id: sdk.z.string().min(1).title('Page ID').describe('The ID of the page the comment was created on'), + }) + .title('Data') + .describe('Additional data about the event'), + }), + }, +} as const satisfies sdk.IntegrationDefinitionProps['events'] diff --git a/integrations/notion/definitions/index.ts b/integrations/notion/definitions/index.ts index 12b5f0198d7..0623fb6c7b4 100644 --- a/integrations/notion/definitions/index.ts +++ b/integrations/notion/definitions/index.ts @@ -1,5 +1,6 @@ export { actions } from './actions' export { configuration, configurations, identifier } from './configuration' +export { events } from './events' export { secrets } from './secrets' export { states } from './states' export { user } from './user-tags' diff --git a/integrations/notion/definitions/notion-schemas.ts b/integrations/notion/definitions/notion-schemas.ts new file mode 100644 index 00000000000..d112129e865 --- /dev/null +++ b/integrations/notion/definitions/notion-schemas.ts @@ -0,0 +1,64 @@ +import { z } from '@botpress/sdk' + +const richText = z + .object({ + text: z.object({ content: z.string() }), + type: z.literal('text').optional(), + }) + .passthrough() + +const titleProp = z.object({ type: z.literal('title'), title: z.array(richText) }) +const richTextProp = z.object({ rich_text: z.array(richText) }) +const numberProp = z.object({ number: z.number().nullable() }) +const checkboxProp = z.object({ checkbox: z.boolean() }) +const selectProp = z.object({ select: z.object({ name: z.string() }).nullable() }) +const statusProp = z.object({ status: z.object({ name: z.string() }).nullable() }) +const multiSelectProp = z.object({ multi_select: z.array(z.object({ name: z.string() })) }) +const dateProp = z.object({ + date: z + .object({ + start: z.string(), + end: z.string().optional().nullable(), + }) + .nullable(), +}) +const urlProp = z.object({ url: z.string().nullable() }) +const emailProp = z.object({ email: z.string().nullable() }) +const phoneProp = z.object({ phone_number: z.string().nullable() }) +const peopleProp = z.object({ people: z.array(z.object({ id: z.string() })) }) +const relationProp = z.object({ relation: z.array(z.object({ id: z.string() })) }) +const filesProp = z.object({ + files: z.array( + z.union([ + z.object({ + external: z.object({ url: z.string() }), + name: z.string(), + type: z.literal('external').optional(), + }), + z.object({ + file: z.object({ url: z.string(), expiry_time: z.string().optional() }), + name: z.string(), + type: z.literal('file').optional(), + }), + ]) + ), +}) + +export const updatePagePropertiesSchema = z.record( + z.union([ + titleProp, + richTextProp, + numberProp, + checkboxProp, + selectProp, + statusProp, + multiSelectProp, + dateProp, + urlProp, + emailProp, + phoneProp, + peopleProp, + relationProp, + filesProp, + ]) +) diff --git a/integrations/notion/hub.md b/integrations/notion/hub.md index 3f83009e057..0f8ffb259ec 100644 --- a/integrations/notion/hub.md +++ b/integrations/notion/hub.md @@ -1,5 +1,20 @@ The Notion Integration for Botpress Studio allows you to do the following things: +## Migrating from version `2.x` to `3.x` + +Version `3.x` of the Notion integration brings a lot of features to the table. Here is a summary of the changes coming to Notion: + +- Upgraded to Notion API version **2025-09-03** +- Page interactions: Get Page, Get Page Content, Append Blocks to Page, Update Page Properties +- Search by Title +- Comment created Event +- Consolidate comment actions into one action - `Add Comment` + +Another change that the update brings is new manual configuration. It now asks for: + +- **Internal Integration Secret (required)**: Same as API Token but changed the name to match what is found in Notion's integration's page. +- **Webhook Verification Secret**: This is used to verify webhook events. Can be found in the bot logs when configuring the webhooks. + ## Migrating from version `0.x` or `1.x` to `2.x` Version `2.0` of the Notion integration adds OAuth support, which is now the default configuration option. @@ -16,6 +31,8 @@ This is the simplest way to set up the integration. To set up the Notion integra When using this configuration mode, a Botpress-managed Notion application will be used to connect to your Notion account. Actions taken by the bot will be attributed to this application, not your personal Notion account. +**Note:** Ensure that you have chosen the correct workspace which can be found on the top right during OAuth. + ### Manual configuration with a custom Notion integration #### Step 1 - Create Integration @@ -28,38 +45,12 @@ Give your integration access to all the pages and databases that you want to use #### Step 3 - Configure your Bot -Give your integration access to all the pages and databases that you want to use with Botpress. [Share a database with your integration - Notion Developers](https://developers.notion.com/docs/create-a-notion-integration#step-2-share-a-database-with-your-integration) - You need a token to get your newly created Notion Integration _(not the same as Botpress Studio's Notion Integration)_ connected with Botpress Studio: -- `Auth Token` - You'll find this by going to your integration under `https://www.notion.so/my-integrations`. Once you click on your integration, go to the "Secrets" section and find the "Internal Integration Secret" field. Click "Show" then "Copy". Paste the copied token under `Auth Token` field for Notion integration under the "Integrations" tab for your bot. +- `Internal Integration Secret` - You'll find this by going to your integration under `https://www.notion.so/my-integrations`. Once you click on your integration, under the "Configuration" tab, find the "Internal Integration Secret" field. Click "Show" then "Copy". Paste the copied token under `Internal Integration Secret` field for Notion integration under the "Integrations" tab for your bot. With that you just need to enable your integration and you can start expanding your Bot's capabilities with Notion. -## Usage - -The following actions require you to know the Ids of the Notion entities your bot will work with. All notion entities (pages, databases, etc) have and id that can be found in the URL when you visit those in your Notion account in a Browser,or by getting the link by clicking on the "Copy Link" item in the (...) menu. See [Get a Database Id - Notion Developers](https://developers.notion.com/docs/create-a-notion-integration#step-3-save-the-database-id) for more information - -### Add Comment to a Discussion - -This action allows you to add a comment to an existing discussion. Use this for replying to a comment. - -### Add Comment to a Page - -You can add page level comments with this action. - -### Get a Database - -This allows you to get the details of a Database. This is ideally used with the `Add Page to a Database` action. In addition to the response from the Notion API ([Retreive a Database - Notion Developers](https://developers.notion.com/reference/retrieve-a-database)), this action also returns a optimized `structure` property (technically a type decleration) that can be used as an input for an AI task to instruct it to generate a payload for adding or updating a page in a Notion Database based on a user input. - -### Add Page to a Database - -This action should ideally be used in tandem with `Get a Database` that returns the structure of the Database that you can use to instruct an [AI task](https://botpress.com/docs/cloud/generative-ai/ai-task-card/) to generate a payload. See [Working with Databases - Notion Developers](https://developers.notion.com/docs/working-with-databases) for more info. - -### Delete a block - -You can delete the following entities: +#### Step 4 - Setup Webhooks (optional) -- a page in a database -- a page -- a block +After saving Step 3 configuration, copy the Botpress integration webhook URL. In your Notion integration's Webhooks tab, paste it in `Webhook URL` and click `verify`. Copy the secret from your Bot logs and paste it back in the verification field. Then add this secret to the `Webhook Verification Secret` field in your Botpress Notion integration configuration to validate webhook events. diff --git a/integrations/notion/integration.definition.ts b/integrations/notion/integration.definition.ts index 8a66307bd08..e6a688d84c7 100644 --- a/integrations/notion/integration.definition.ts +++ b/integrations/notion/integration.definition.ts @@ -1,9 +1,9 @@ import * as sdk from '@botpress/sdk' import filesReadonly from './bp_modules/files-readonly' -import { actions, configuration, configurations, identifier, secrets, states, user } from './definitions' +import { actions, configuration, configurations, events, identifier, secrets, states, user } from './definitions' export const INTEGRATION_NAME = 'notion' -export const INTEGRATION_VERSION = '2.2.3' +export const INTEGRATION_VERSION = '3.0.0' export default new sdk.IntegrationDefinition({ name: INTEGRATION_NAME, @@ -16,6 +16,7 @@ export default new sdk.IntegrationDefinition({ configuration, configurations, identifier, + events, secrets, states, user, diff --git a/integrations/notion/package.json b/integrations/notion/package.json index 99cbcbf0897..3445b363aa0 100644 --- a/integrations/notion/package.json +++ b/integrations/notion/package.json @@ -11,7 +11,8 @@ "@botpress/common": "workspace:*", "@botpress/sdk": "workspace:*", "@botpress/sdk-addons": "workspace:*", - "@notionhq/client": "^2.3.0", + "@notionhq/client": "^5.6.0", + "@tryfabric/martian": "^1.2.4", "notion-to-md": "^4.0.0-alpha.4" }, "devDependencies": { diff --git a/integrations/notion/src/actions/add-comment-to-discussion.ts b/integrations/notion/src/actions/add-comment-to-discussion.ts deleted file mode 100644 index 55370f78cd8..00000000000 --- a/integrations/notion/src/actions/add-comment-to-discussion.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { wrapAction } from '../action-wrapper' - -export const addCommentToDiscussion = wrapAction( - { actionName: 'addCommentToDiscussion', errorMessage: 'Failed to add comment to discussion' }, - async ({ notionClient }, { commentBody, discussionId }) => { - await notionClient.addCommentToDiscussion({ commentBody, discussionId }) - } -) diff --git a/integrations/notion/src/actions/add-comment-to-page.ts b/integrations/notion/src/actions/add-comment-to-page.ts deleted file mode 100644 index 31fda96a948..00000000000 --- a/integrations/notion/src/actions/add-comment-to-page.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { wrapAction } from '../action-wrapper' - -export const addCommentToPage = wrapAction( - { actionName: 'addCommentToPage', errorMessage: 'Failed to add comment to page' }, - async ({ notionClient }, { commentBody, pageId }) => { - await notionClient.addCommentToPage({ commentBody, pageId }) - } -) diff --git a/integrations/notion/src/actions/add-comment.ts b/integrations/notion/src/actions/add-comment.ts new file mode 100644 index 00000000000..1a860db915a --- /dev/null +++ b/integrations/notion/src/actions/add-comment.ts @@ -0,0 +1,11 @@ +import { wrapAction } from '../action-wrapper' + +export const addComment = wrapAction( + { actionName: 'addComment', errorMessage: 'Failed to add comment' }, + async ( + { notionClient }, + { parentType, parentId, commentBody }: { parentType: string; parentId: string; commentBody: string } + ) => { + return await notionClient.addComment({ parentType, parentId, commentBody }) + } +) diff --git a/integrations/notion/src/actions/add-page-to-db.ts b/integrations/notion/src/actions/add-page-to-db.ts deleted file mode 100644 index 3e87ab8ff6b..00000000000 --- a/integrations/notion/src/actions/add-page-to-db.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { wrapAction } from '../action-wrapper' - -export const addPageToDb = wrapAction( - { actionName: 'addPageToDb', errorMessage: 'Failed to add page to database' }, - async ({ notionClient }, { databaseId, pageProperties }) => { - await notionClient.addPageToDb({ databaseId, properties: pageProperties as any }) // TODO: fix type and bump major - } -) diff --git a/integrations/notion/src/actions/append-blocks-to-page.ts b/integrations/notion/src/actions/append-blocks-to-page.ts new file mode 100644 index 00000000000..7916f187a15 --- /dev/null +++ b/integrations/notion/src/actions/append-blocks-to-page.ts @@ -0,0 +1,12 @@ +import { markdownToBlocks } from '@tryfabric/martian' +import { wrapAction } from '../action-wrapper' + +export const appendBlocksToPage = wrapAction( + { actionName: 'appendBlocksToPage', errorMessage: 'Failed to append blocks to page' }, + async ({ notionClient }, input) => { + const { pageId, markdownText } = input + const blocks = markdownToBlocks(markdownText) + // @ts-expect-error - @tryfabric/martian uses notion@1.0.4 types which needs to be updated to notion@5.6.0 but it works. To find a better solution + return await notionClient.appendBlocksToPage({ pageId, blocks }) + } +) diff --git a/integrations/notion/src/actions/create-page.ts b/integrations/notion/src/actions/create-page.ts new file mode 100644 index 00000000000..ca03b304326 --- /dev/null +++ b/integrations/notion/src/actions/create-page.ts @@ -0,0 +1,11 @@ +import { wrapAction } from '../action-wrapper' + +export const createPage = wrapAction( + { actionName: 'createPage', errorMessage: 'Failed to create page' }, + async ({ notionClient }, { parentType, parentId, title, dataSourceTitleName }) => { + if (!dataSourceTitleName) { + dataSourceTitleName = 'Name' // default title property name for data sources + } + return await notionClient.createPage({ parentType, parentId, title, dataSourceTitleName }) + } +) diff --git a/integrations/notion/src/actions/delete-block.ts b/integrations/notion/src/actions/delete-block.ts index 39340255feb..ded5b12deb3 100644 --- a/integrations/notion/src/actions/delete-block.ts +++ b/integrations/notion/src/actions/delete-block.ts @@ -3,6 +3,6 @@ import { wrapAction } from '../action-wrapper' export const deleteBlock = wrapAction( { actionName: 'deleteBlock', errorMessage: 'Failed to delete block' }, async ({ notionClient }, { blockId }) => { - await notionClient.deleteBlock({ blockId }) + return await notionClient.deleteBlock({ blockId }) } ) diff --git a/integrations/notion/src/actions/files-readonly/list-items-in-folder.ts b/integrations/notion/src/actions/files-readonly/list-items-in-folder.ts index 4f2c83afe95..72b10511f3b 100644 --- a/integrations/notion/src/actions/files-readonly/list-items-in-folder.ts +++ b/integrations/notion/src/actions/files-readonly/list-items-in-folder.ts @@ -50,8 +50,8 @@ const _enumeratePageAndChildItems: EnumerateItemsFn = async ({ folderId, prevTok const _enumerateDbChildItems: EnumerateItemsFn = async ({ folderId, prevToken, notionClient }) => { const dbId = folderId!.slice(mapping.PREFIXES.DB_FOLDER.length) - const { nextToken, results } = await notionClient.enumerateDatabaseChildren({ - databaseId: dbId, + const { nextToken, results } = await notionClient.enumerateDataSourceChildren({ + dataSourceId: dbId, nextToken: prevToken, }) diff --git a/integrations/notion/src/actions/get-data-source.ts b/integrations/notion/src/actions/get-data-source.ts new file mode 100644 index 00000000000..07196adccdd --- /dev/null +++ b/integrations/notion/src/actions/get-data-source.ts @@ -0,0 +1,43 @@ +import { RuntimeError } from '@botpress/client' +import { RichTextItemResponse } from '@notionhq/client/build/src/api-endpoints' +import { wrapAction } from '../action-wrapper' + +export const getDataSource = wrapAction( + { actionName: 'getDataSource', errorMessage: 'Failed to fetch data source' }, + async ({ notionClient }, { dataSourceId }) => { + const [dataSource, pagesResult] = await Promise.all([ + notionClient.getDataSource({ dataSourceId }), + notionClient.enumerateDataSourceChildren({ dataSourceId }), + ]) + + if (!dataSource) { + throw new RuntimeError(`Data source with ID ${dataSourceId} not found`) + } + + const pages = pagesResult.results.map((page) => { + let title = '' + + if (page.object === 'page' && 'properties' in page) { + const titleProp = Object.values(page.properties).find( + (prop): prop is { type: 'title'; title: RichTextItemResponse[]; id: string } => + prop !== null && 'type' in prop && prop.type === 'title' + ) + if (titleProp && 'title' in titleProp) { + title = titleProp.title.map((t) => t.plain_text).join('') + } + } + + return { + id: page.id, + title, + pageProperties: page.properties, + } + }) + + return { + object: dataSource.object, + properties: dataSource.properties, + pages, + } + } +) diff --git a/integrations/notion/src/actions/get-db.ts b/integrations/notion/src/actions/get-db.ts index fd5a991efb2..63944cdd1c4 100644 --- a/integrations/notion/src/actions/get-db.ts +++ b/integrations/notion/src/actions/get-db.ts @@ -1,8 +1,13 @@ +import { RuntimeError } from '@botpress/sdk' import { wrapAction } from '../action-wrapper' export const getDb = wrapAction( { actionName: 'getDb', errorMessage: 'Failed to fetch database' }, async ({ notionClient }, { databaseId }) => { - return await notionClient.getDbWithStructure({ databaseId }) + const database = await notionClient.getDatabase({ databaseId }) + if (!database) { + throw new RuntimeError(`Database with ID ${databaseId} not found`) + } + return database } ) diff --git a/integrations/notion/src/actions/get-page-content.ts b/integrations/notion/src/actions/get-page-content.ts new file mode 100644 index 00000000000..6877c3b273d --- /dev/null +++ b/integrations/notion/src/actions/get-page-content.ts @@ -0,0 +1,8 @@ +import { wrapAction } from '../action-wrapper' + +export const getPageContent = wrapAction( + { actionName: 'getPageContent', errorMessage: 'Failed to fetch page content' }, + async ({ notionClient }, { pageId }) => { + return await notionClient.getPageContent({ pageId }) + } +) diff --git a/integrations/notion/src/actions/get-page.ts b/integrations/notion/src/actions/get-page.ts new file mode 100644 index 00000000000..68e80097d42 --- /dev/null +++ b/integrations/notion/src/actions/get-page.ts @@ -0,0 +1,9 @@ +import { wrapAction } from '../action-wrapper' + +export const getPage = wrapAction( + { actionName: 'getPage', errorMessage: 'Failed to fetch page' }, + async ({ notionClient }, { pageId }) => { + const page = await notionClient.getPage({ pageId }) + return page ?? {} + } +) diff --git a/integrations/notion/src/actions/index.ts b/integrations/notion/src/actions/index.ts index 3763e92e46e..843784472c1 100644 --- a/integrations/notion/src/actions/index.ts +++ b/integrations/notion/src/actions/index.ts @@ -1,18 +1,28 @@ -import { addCommentToDiscussion } from './add-comment-to-discussion' -import { addCommentToPage } from './add-comment-to-page' -import { addPageToDb } from './add-page-to-db' +import { addComment } from './add-comment' +import { appendBlocksToPage } from './append-blocks-to-page' +import { createPage } from './create-page' import { deleteBlock } from './delete-block' import { filesReadonlyListItemsInFolder } from './files-readonly/list-items-in-folder' import { filesReadonlyTransferFileToBotpress } from './files-readonly/transfer-file-to-botpress' +import { getDataSource } from './get-data-source' import { getDb } from './get-db' +import { getPage } from './get-page' +import { getPageContent } from './get-page-content' +import { searchByTitle } from './search-by-title' +import { updatePageProperties } from './update-page-properties' import * as bp from '.botpress' export const actions = { - addCommentToDiscussion, - addCommentToPage, - addPageToDb, + addComment, + createPage, + appendBlocksToPage, deleteBlock, filesReadonlyListItemsInFolder, filesReadonlyTransferFileToBotpress, getDb, + getDataSource, + getPage, + getPageContent, + searchByTitle, + updatePageProperties, } as const satisfies bp.IntegrationProps['actions'] diff --git a/integrations/notion/src/actions/search-by-title.ts b/integrations/notion/src/actions/search-by-title.ts new file mode 100644 index 00000000000..031c9f6ea43 --- /dev/null +++ b/integrations/notion/src/actions/search-by-title.ts @@ -0,0 +1,8 @@ +import { wrapAction } from '../action-wrapper' + +export const searchByTitle = wrapAction( + { actionName: 'searchByTitle', errorMessage: 'Failed to search by title' }, + async ({ notionClient }, { title }) => { + return await notionClient.searchByTitle({ title }) + } +) diff --git a/integrations/notion/src/actions/update-page-properties.ts b/integrations/notion/src/actions/update-page-properties.ts new file mode 100644 index 00000000000..44ff47563e1 --- /dev/null +++ b/integrations/notion/src/actions/update-page-properties.ts @@ -0,0 +1,39 @@ +import { RuntimeError } from '@botpress/client' +import { updatePagePropertiesSchema } from '../../definitions/notion-schemas' +import { wrapAction } from '../action-wrapper' + +export const updatePageProperties = wrapAction( + { actionName: 'updatePageProperties', errorMessage: 'Failed to update page properties' }, + async ({ notionClient }, { pageId, propertiesJson }) => { + let parsed: unknown + + if (!propertiesJson) { + throw new RuntimeError('propertiesJson is required') + } + + try { + parsed = JSON.parse(propertiesJson) + } catch (thrown) { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError(`propertiesJson must be valid JSON: ${error.message}`) + } + + const updatePagePropertiesResult = updatePagePropertiesSchema.safeParse(parsed) + if (!updatePagePropertiesResult.success) { + throw new RuntimeError( + `propertiesJson must be a valid Notion properties object. Check the Notion API documentation for the correct format. ${updatePagePropertiesResult.error.message}` + ) + } + + const updatedPage = await notionClient + .updatePageProperties({ pageId, properties: updatePagePropertiesResult.data }) + .catch((thrown) => { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + throw new RuntimeError(`Failed to update page properties: ${error.message}`) + }) + + return { + pageId: updatedPage.id, + } + } +) diff --git a/integrations/notion/src/files-readonly/mapping.ts b/integrations/notion/src/files-readonly/mapping.ts index 71bc7e633c8..fba03218407 100644 --- a/integrations/notion/src/files-readonly/mapping.ts +++ b/integrations/notion/src/files-readonly/mapping.ts @@ -7,15 +7,15 @@ type FilesReadonlyFolder = FilesReadonlyEntity & { type: 'folder' } /* This files handles the mapping of Notion entities to Botpress files-readonly -entities. Since we're mapping Pages and Databases to Files and Folders, we need +entities. Since we're mapping Pages and Data sources to Files and Folders, we need to be careful about the parent-child relationships, since Files cannot contain other Files or Folders. From the Notion API documentation: General parenting rules: - - Pages can be parented by other pages, databases, blocks, or by the whole workspace. - - Blocks can be parented by pages, databases, or blocks. - - Databases can be parented by pages, blocks, or by the whole workspace. + - Pages can be parented by other pages, data sources, blocks, or by the whole workspace. + - Blocks can be parented by pages, data sources, or blocks. + - Data sources can be parented by pages, blocks, or by the whole workspace. --- @@ -46,8 +46,17 @@ export const PAGE_FILE_NAME = 'page.mdx' export const mapEntities = (entities: types.NotionItem[]): FilesReadonlyEntity[] => entities.map(_mapEntity) -const _mapEntity = (entity: types.NotionItem): FilesReadonlyEntity => - entity.object === 'page' || 'child_page' in entity ? mapPageToFolder(entity) : mapDatabaseToFolder(entity) +const _mapEntity = (entity: types.NotionItem): FilesReadonlyEntity => { + if (entity.object === 'page' || (entity.object === 'block' && entity.type === 'child_page')) { + return mapPageToFolder(entity) + } else if (entity.object === 'data_source') { + return mapDataSourceToFolder(entity) + } else if (entity.object === 'block' && entity.type === 'child_database') { + return mapDatabaseToFolder(entity) + } + // Default to data source folder for any other cases + return mapDataSourceToFolder(entity as types.NotionDataSource) +} export const mapPageToFolder = (page: types.NotionPage | types.NotionChildPage): FilesReadonlyFolder => ({ type: 'folder', @@ -64,7 +73,14 @@ export const mapPageToFile = (page: types.NotionPage | types.NotionChildPage): F lastModifiedDate: page.last_edited_time, }) -export const mapDatabaseToFolder = (db: types.NotionDatabase | types.NotionChildDatabase): FilesReadonlyFolder => ({ +export const mapDataSourceToFolder = (ds: types.NotionDataSource): FilesReadonlyFolder => ({ + type: 'folder', + id: PREFIXES.DB_FOLDER + ds.id, + name: getDataSourceTitle(ds), + parentId: _getParentId(ds), +}) + +export const mapDatabaseToFolder = (db: types.NotionChildDatabase): FilesReadonlyFolder => ({ type: 'folder', id: PREFIXES.DB_FOLDER + db.id, name: getDatabaseTitle(db), @@ -82,10 +98,12 @@ export const getPageTitle = (page: types.NotionPage | types.NotionChildPage): st return titleProperty?.title[0]?.plain_text ?? `Untitled Page (${_uuidToShortId(page.id)})` } -export const getDatabaseTitle = (db: types.NotionDatabase | types.NotionChildDatabase): string => { - return db.object === 'block' - ? db.child_database.title - : (db.title[0]?.plain_text ?? `Untitled Database (${_uuidToShortId(db.id)})`) +export const getDataSourceTitle = (ds: types.NotionDataSource): string => { + return ds.title[0]?.plain_text ?? `Untitled Data Source (${_uuidToShortId(ds.id)})` +} + +export const getDatabaseTitle = (db: types.NotionChildDatabase): string => { + return db.child_database.title } const _uuidToShortId = (uuid: string): string => uuid.replaceAll('-', '') @@ -96,6 +114,8 @@ const _getParentId = ({ parent }: types.NotionItem): string | undefined => { return PREFIXES.PAGE_FOLDER + parent.page_id case 'database_id': return PREFIXES.DB_FOLDER + parent.database_id + case 'data_source_id': + return PREFIXES.DB_FOLDER + parent.data_source_id case 'block_id': break case 'workspace': diff --git a/integrations/notion/src/files-readonly/path-utils.ts b/integrations/notion/src/files-readonly/path-utils.ts index 6586b4c047f..de141cd7f42 100644 --- a/integrations/notion/src/files-readonly/path-utils.ts +++ b/integrations/notion/src/files-readonly/path-utils.ts @@ -9,16 +9,22 @@ export const retrieveParentPath = async ( const parentPathFragments: string[] = [] let currentParent = parentObject - while (currentParent.type === 'database_id' || currentParent.type === 'page_id') { - if (currentParent.type === 'database_id') { - const db = await notionClient.getDatabase({ databaseId: currentParent.database_id }) - - if (!db) { + while ( + currentParent.type === 'database_id' || + currentParent.type === 'data_source_id' || + currentParent.type === 'page_id' + ) { + if (currentParent.type === 'database_id' || currentParent.type === 'data_source_id') { + const dataSourceId = + currentParent.type === 'database_id' ? currentParent.database_id : currentParent.data_source_id + const ds = await notionClient.getDataSource({ dataSourceId }) + + if (!ds) { return '/' } - parentPathFragments.unshift(mapping.getDatabaseTitle(db)) - currentParent = db.parent + parentPathFragments.unshift(mapping.getDataSourceTitle(ds)) + currentParent = ds.parent continue } diff --git a/integrations/notion/src/notion-api/db-structure/consts.ts b/integrations/notion/src/notion-api/db-structure/consts.ts index 0a1b677e58d..84da0d23323 100644 --- a/integrations/notion/src/notion-api/db-structure/consts.ts +++ b/integrations/notion/src/notion-api/db-structure/consts.ts @@ -27,4 +27,5 @@ export const NOTION_PROPERTY_STRINGIFIED_TYPE_MAP: Record { _stringifiedTypes += `${key}:{type:"${value.type}";"${value.type}":${ diff --git a/integrations/notion/src/notion-api/db-structure/fixtures/mock-responses.ts b/integrations/notion/src/notion-api/db-structure/fixtures/mock-responses.ts index 26acbebf0a8..7c23e2e67c3 100644 --- a/integrations/notion/src/notion-api/db-structure/fixtures/mock-responses.ts +++ b/integrations/notion/src/notion-api/db-structure/fixtures/mock-responses.ts @@ -1,8 +1,8 @@ -import { GetDatabaseResponse } from '@notionhq/client/build/src/api-endpoints' +import { GetDataSourceResponse } from '@notionhq/client/build/src/api-endpoints' import { NOTION_PROPERTY_STRINGIFIED_TYPE_MAP } from '../consts' -export const MOCK_RESPONSE_1: GetDatabaseResponse = { - object: 'database', +export const MOCK_RESPONSE_1: GetDataSourceResponse = { + object: 'data_source', id: 'e819c5b1-77f8-4a7d-953c-3dc9e9c46037', cover: { type: 'external', @@ -158,7 +158,9 @@ export const MOCK_RESPONSE_1: GetDatabaseResponse = { Author: { id: 'qNw_', name: 'Author', type: 'rich_text', rich_text: {}, description: '' }, Name: { id: 'title', name: 'Name', type: 'title', title: {}, description: '' }, }, - parent: { type: 'workspace', workspace: true }, + parent: { type: 'data_source_id', data_source_id: 'parent-ds-id', database_id: 'parent-db-id' }, + database_parent: { type: 'workspace', workspace: true }, + in_trash: false, url: 'https://www.notion.so/e819c5b177f84a7d953c3dc9e9c46037', public_url: null, archived: false, diff --git a/integrations/notion/src/notion-api/notion-client.ts b/integrations/notion/src/notion-api/notion-client.ts index c1a8dcbe2fe..3d2e9d50158 100644 --- a/integrations/notion/src/notion-api/notion-client.ts +++ b/integrations/notion/src/notion-api/notion-client.ts @@ -1,4 +1,15 @@ import * as notionhq from '@notionhq/client' +import { + BlockObjectRequest, + BlockObjectResponse, + DataSourceObjectResponse, + PartialDataSourceObjectResponse, + PartialPageObjectResponse, + RichTextItemResponse, + UpdatePageParameters, + CreatePageParameters, + CreateCommentParameters, +} from '@notionhq/client/build/src/api-endpoints' import { getDbStructure } from './db-structure' import { handleErrorsDecorator as handleErrors } from './error-handling' import { NotionOAuthClient } from './notion-oauth-client' @@ -19,7 +30,6 @@ export class NotionClient { public static async create({ ctx, client }: { client: bp.Client; ctx: bp.Context }): Promise { const accessToken = await NotionClient._getAccessToken({ ctx, client }) - return new NotionClient({ accessToken, }) @@ -35,7 +45,7 @@ export class NotionClient { private static async _getAccessToken({ ctx, client }: { client: bp.Client; ctx: bp.Context }): Promise { if (ctx.configurationType === 'customApp') { - return ctx.configuration.authToken + return ctx.configuration.internalIntegrationSecret } const oauthClient = new NotionOAuthClient({ ctx, client }) @@ -55,57 +65,137 @@ export class NotionClient { }: { databaseId: string properties: Record - }): Promise { - void (await this._notion.pages.create({ + }): Promise<{ pageId: string }> { + const response = await this._notion.pages.create({ parent: { database_id: databaseId }, properties, - })) - } - - @handleErrors('Failed to add comment to page') - public async addCommentToPage({ pageId, commentBody }: { pageId: string; commentBody: string }): Promise { - void (await this._notion.comments.create({ - parent: { page_id: pageId }, - rich_text: [ - { - type: 'text', - text: { - content: commentBody, - }, - }, - ], - })) - } - - @handleErrors('Failed to add comment to discussion') - public async addCommentToDiscussion({ - discussionId, + }) + return { pageId: response.id } + } + + @handleErrors('Failed to create page') + public async createPage({ + parentType, + parentId, + title, + dataSourceTitleName, + }: { + parentType: string + parentId: string + title: string + dataSourceTitleName: string + }): Promise<{ pageId: string }> { + let parent: CreatePageParameters['parent'] + let properties: CreatePageParameters['properties'] + + if (parentType === 'data source') { + const dataSource = await this._notion.dataSources.retrieve({ data_source_id: parentId }) + if (!dataSource.properties[dataSourceTitleName]) { + throw new Error(`Title property "${dataSourceTitleName}" not found in data source properties`) + } + parent = { data_source_id: parentId, type: 'data_source_id' } + properties = { [dataSourceTitleName]: { title: [{ text: { content: title } }] } } + } else { + parent = { page_id: parentId, type: 'page_id' } + properties = { title: { title: [{ text: { content: title } }] } } + } + + const response = await this._notion.pages.create({ parent, properties }) + return { pageId: response.id } + } + + @handleErrors('Failed to add comment') + public async addComment({ + parentType, + parentId, commentBody, }: { - discussionId: string + parentType: string + parentId: string commentBody: string - }): Promise { - void (await this._notion.comments.create({ - discussion_id: discussionId, - rich_text: [ - { - type: 'text', - text: { - content: commentBody, - }, - }, - ], - })) + }): Promise<{ commentId: string; discussionId?: string }> { + let body: CreateCommentParameters + if (parentType === 'page') { + body = { + parent: { page_id: parentId, type: 'page_id' }, + rich_text: [{ type: 'text', text: { content: commentBody } }], + } + } else if (parentType === 'block') { + body = { + parent: { block_id: parentId, type: 'block_id' }, + rich_text: [{ type: 'text', text: { content: commentBody } }], + } + } else if (parentType === 'discussion') { + body = { discussion_id: parentId, rich_text: [{ type: 'text', text: { content: commentBody } }] } + } else { + throw new Error(`Invalid parent type: ${parentType}`) + } + const response = await this._notion.comments.create(body) + + return { commentId: response.id, discussionId: 'discussion_id' in response ? response.discussion_id : undefined } + } + + @handleErrors('Failed to update page properties') + public async updatePageProperties({ + pageId, + properties, + }: { + pageId: string + properties: UpdatePageParameters['properties'] + }) { + return await this._notion.pages.update({ + page_id: pageId, + properties, + }) } @handleErrors('Failed to delete block') - public async deleteBlock({ blockId }: { blockId: string }): Promise { - void (await this._notion.blocks.delete({ block_id: blockId })) + public async deleteBlock({ blockId }: { blockId: string }): Promise<{ blockId: string }> { + const response = await this._notion.blocks.delete({ block_id: blockId }) + return { blockId: response.id } + } + + @handleErrors('Failed to append block to page') + public async appendBlocksToPage({ + pageId, + blocks, + }: { + pageId: string + blocks: BlockObjectRequest[] + }): Promise<{ pageId: string; blockIds: string[] }> { + const response = await this._notion.blocks.children.append({ + block_id: pageId, + children: blocks, + }) + return { + pageId, + blockIds: response.results.map((block) => block.id), + } + } + + @handleErrors('Failed to search by title') + public async searchByTitle({ title }: { title?: string }) { + const [response, dataSourceResponse] = await Promise.all([ + this._notion.search({ + query: title, + filter: { property: 'object', value: 'page' }, + }), + this._notion.search({ + query: title, + filter: { property: 'object', value: 'data_source' }, + }), + ]) + + const allResults = [...response.results, ...dataSourceResponse.results] + + const formattedResults = this._formatSearchResults(allResults) + + return { results: formattedResults } } @handleErrors('Failed to get database') public async getDbWithStructure({ databaseId }: { databaseId: string }) { - const response = await this._notion.databases.retrieve({ database_id: databaseId }) + const response = await this._notion.dataSources.retrieve({ data_source_id: databaseId }) // TODO: do not return the raw response; perform mapping @@ -144,29 +234,84 @@ export class NotionClient { } } + @handleErrors('Failed to get data source') + public async getDataSource({ dataSourceId }: { dataSourceId: string }) { + const ds = await this._notion.dataSources.retrieve({ data_source_id: dataSourceId }) + return 'parent' in ds && 'created_time' in ds ? ds : undefined + } + @handleErrors('Failed to retrieve page') public async getPage({ pageId }: { pageId: string }) { const page = await this._notion.pages.retrieve({ page_id: pageId }) + return 'parent' in page && 'created_time' in page ? page : undefined + } + + @handleErrors('Failed to get page content') + public async getPageContent({ pageId }: { pageId: string }) { + const blocks: types.BlockContent[] = [] + let nextCursor: string | undefined + + do { + const response = await this._notion.blocks.children.list({ + block_id: pageId, + start_cursor: nextCursor, + }) + + for (const block of response.results) { + if (!this._isBlockObjectResponse(block)) { + continue + } + + const blockType = block.type + const richText = this._extractRichTextFromBlockSwitch(block) + + let parentId: string | undefined + if (block.parent.type === 'page_id') { + parentId = block.parent.page_id + } else if (block.parent.type === 'block_id') { + parentId = block.parent.block_id + } else { + parentId = undefined + } - return 'parent' in page ? page : undefined + blocks.push({ + blockId: block.id, + parentId, + type: blockType, + hasChildren: block.has_children, + richText, + }) + } + + nextCursor = response.next_cursor ?? undefined + } while (nextCursor) + + return { blocks } } @handleErrors('Failed to get database') public async getDatabase({ databaseId }: { databaseId: string }) { const db = await this._notion.databases.retrieve({ database_id: databaseId }) - return 'parent' in db ? db : undefined + return 'parent' in db && 'created_time' in db + ? { + object: db.object, + dataSources: db.data_sources, + } + : undefined } - @handleErrors('Failed to enumerate database children') - public async enumerateDatabaseChildren({ databaseId, nextToken }: { databaseId: string; nextToken?: string }) { - const { next_cursor, results } = await this._notion.databases.query({ - database_id: databaseId, + @handleErrors('Failed to enumerate data source children') + public async enumerateDataSourceChildren({ dataSourceId, nextToken }: { dataSourceId: string; nextToken?: string }) { + const { next_cursor, results } = await this._notion.dataSources.query({ + data_source_id: dataSourceId, in_trash: false, start_cursor: nextToken, }) - const filteredResults = results.filter((res) => 'parent' in res && !res.in_trash) as types.NotionDatabaseChild[] + const filteredResults = results.filter( + (res): res is types.NotionDataSourceChild => 'parent' in res && !res.in_trash + ) return { results: filteredResults, @@ -180,4 +325,75 @@ export class NotionClient { return { markdown } } + + private _formatSearchResults( + results: (PartialPageObjectResponse | PartialDataSourceObjectResponse | DataSourceObjectResponse)[] + ) { + return results + .filter( + (result): result is types.NotionTopLevelItem => 'parent' in result && !('archived' in result && result.archived) + ) + .map((result) => { + let resultTitle = '' + let resultUrl = '' + + if (result.object === 'page' && 'properties' in result) { + const titleProp = Object.values(result.properties).find( + (prop): prop is { type: 'title'; title: RichTextItemResponse[]; id: string } => + typeof prop === 'object' && prop !== null && 'type' in prop && prop.type === 'title' + ) + if (titleProp) { + resultTitle = titleProp.title.map((t) => t.plain_text).join('') + } + if ('url' in result) { + resultUrl = result.url + } + } else if (result.object === 'data_source' && 'title' in result && Array.isArray(result.title)) { + resultTitle = result.title.map((t) => t.plain_text).join('') + if ('url' in result) { + resultUrl = result.url + } + } + + return { + id: result.id, + title: resultTitle, + type: result.object, + url: resultUrl, + } + }) + } + + private _isBlockObjectResponse(block: unknown): block is BlockObjectResponse { + return typeof block === 'object' && block !== null && 'type' in block && typeof block.type === 'string' + } + + private _extractRichTextFromBlockSwitch(block: BlockObjectResponse): RichTextItemResponse[] { + switch (block.type) { + case 'paragraph': + return block[block.type].rich_text + case 'heading_1': + return block[block.type].rich_text + case 'heading_2': + return block[block.type].rich_text + case 'heading_3': + return block[block.type].rich_text + case 'bulleted_list_item': + return block[block.type].rich_text + case 'numbered_list_item': + return block[block.type].rich_text + case 'to_do': + return block[block.type].rich_text + case 'toggle': + return block[block.type].rich_text + case 'quote': + return block[block.type].rich_text + case 'callout': + return block[block.type].rich_text + case 'code': + return block[block.type].rich_text + default: + return [] + } + } } diff --git a/integrations/notion/src/notion-api/types.ts b/integrations/notion/src/notion-api/types.ts index a8fa90c0ea5..068f7eeb848 100644 --- a/integrations/notion/src/notion-api/types.ts +++ b/integrations/notion/src/notion-api/types.ts @@ -1,8 +1,16 @@ import type * as notionhq from '@notionhq/client' -import type { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints' +import type { PageObjectResponse, RichTextItemResponse } from '@notionhq/client/build/src/api-endpoints' export type NotionPagePropertyTypes = Valueof['type'] +export type BlockContent = { + blockId: string + parentId: string | undefined + type: string + hasChildren: boolean + richText: RichTextItemResponse[] +} + export type NotionTopLevelItem = Extract< Awaited>['results'][number], { parent: any } @@ -13,14 +21,14 @@ export type NotionPageChild = Extract< { parent: any; type: 'child_page' | 'child_database' } > -export type NotionDatabaseChild = Extract< - Awaited>['results'][number], +export type NotionDataSourceChild = Extract< + Awaited>['results'][number], { parent: any } > -export type NotionItem = NotionTopLevelItem | NotionPageChild | NotionDatabaseChild +export type NotionItem = NotionTopLevelItem | NotionPageChild | NotionDataSourceChild export type NotionPage = Extract -export type NotionDatabase = Extract +export type NotionDataSource = Extract export type NotionChildPage = Extract export type NotionChildDatabase = Extract diff --git a/integrations/notion/src/webhook-events/base-payload.ts b/integrations/notion/src/webhook-events/base-payload.ts deleted file mode 100644 index 1c218d4b649..00000000000 --- a/integrations/notion/src/webhook-events/base-payload.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as sdk from '@botpress/sdk' - -export const BASE_EVENT_PAYLOAD = sdk.z.object({ - workspace_id: sdk.z.string().min(1), - type: sdk.z.string().min(1), - entity: sdk.z.object({ - type: sdk.z.enum(['page', 'block', 'database', 'comment']), - id: sdk.z.string().min(1), - }), - data: sdk.z.object({}).passthrough(), -}) diff --git a/integrations/notion/src/webhook-events/handler-dispatcher.ts b/integrations/notion/src/webhook-events/handler-dispatcher.ts index 512d182d628..4d8aa82d5ea 100644 --- a/integrations/notion/src/webhook-events/handler-dispatcher.ts +++ b/integrations/notion/src/webhook-events/handler-dispatcher.ts @@ -4,6 +4,8 @@ import * as handlers from './handlers' import * as bp from '.botpress' export const handler: bp.IntegrationProps['handler'] = async (props) => { + props.logger.forBot().debug('Received webhook event: ' + props.req.body) + if (handlers.isWebhookVerificationRequest(props)) { return await handlers.handleWebhookVerificationRequest(props) } else if (handlers.isOAuthCallback(props)) { @@ -21,6 +23,8 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { return await handlers.handlePageDeletedEvent(props) } else if (handlers.isPageMovedEvent(props)) { return await handlers.handlePageMovedEvent(props) + } else if (handlers.isCommentCreatedEvent(props)) { + return await handlers.handleCommentCreatedEvent(props) } } catch (thrown) { const error = thrown instanceof Error ? thrown : new Error(String(thrown)) @@ -33,27 +37,39 @@ export const handler: bp.IntegrationProps['handler'] = async (props) => { } const _validatePayloadSignature = (props: bp.HandlerProps) => { - const bodySignatureFromNotion = props.req.headers['X-Notion-Signature'] ?? props.req.headers['x-notion-signature'] + const rawSignatureHeader = props.req.headers['X-Notion-Signature'] ?? props.req.headers['x-notion-signature'] - if (!bodySignatureFromNotion) { + if (!rawSignatureHeader) { throw new sdk.RuntimeError('Missing Notion signature in request headers') } - if (props.ctx.configurationType === null) { - // We currently don't support webhook verification for custom Notion integrations - return - } + // Notion signature may be prefixed with "v1=" - extract just the hash part + const bodySignatureFromNotion = rawSignatureHeader.includes('=') + ? rawSignatureHeader.split('=')[1] + : rawSignatureHeader - const bodySignatureFromBotpress = - 'sha256=' + - crypto + let bodySignatureFromBotpress: string + if (props.ctx.configurationType === 'customApp') { + if (!props.ctx.configuration.webhookVerificationSecret) { + throw new sdk.RuntimeError('Webhook verification secret is not set in the integration configuration') + } + bodySignatureFromBotpress = crypto + .createHmac('sha256', props.ctx.configuration.webhookVerificationSecret) + .update(props.req.body ?? '') + .digest('hex') + } else { + bodySignatureFromBotpress = crypto .createHmac('sha256', bp.secrets.WEBHOOK_VERIFICATION_SECRET) .update(props.req.body ?? '') .digest('hex') + } + + const notionSignatureBuffer = Buffer.from(bodySignatureFromNotion ?? '') + const expectedSignatureBuffer = Buffer.from(bodySignatureFromBotpress) const payloadSignatureMatchesExpectedSignature = crypto.timingSafeEqual( - Buffer.from(bodySignatureFromNotion), - Buffer.from(bodySignatureFromBotpress) + notionSignatureBuffer, + expectedSignatureBuffer ) if (!payloadSignatureMatchesExpectedSignature) { diff --git a/integrations/notion/src/webhook-events/handlers/comment-created.ts b/integrations/notion/src/webhook-events/handlers/comment-created.ts new file mode 100644 index 00000000000..c0e80d8856d --- /dev/null +++ b/integrations/notion/src/webhook-events/handlers/comment-created.ts @@ -0,0 +1,29 @@ +import { events } from 'definitions/events' +import * as bp from '.botpress' + +const commentCreatedPayloadSchema = events.commentCreated.schema + +export const isCommentCreatedEvent = (props: bp.HandlerProps): boolean => + Boolean( + props.req.method.toUpperCase() === 'POST' && + props.req.body?.length && + commentCreatedPayloadSchema.safeParse(JSON.parse(props.req.body)).success + ) + +export const handleCommentCreatedEvent: bp.IntegrationProps['handler'] = async (props) => { + const { logger, client, req } = props + + const payload = commentCreatedPayloadSchema.parse(JSON.parse(req.body!)) + logger.forBot().debug('Creating comment created event: ' + JSON.stringify(payload)) + + try { + await client.createEvent({ + type: 'commentCreated', + payload, + }) + logger.forBot().debug('Successfully created comment created event') + } catch (error) { + logger.forBot().error('Failed to create comment event: ' + (error instanceof Error ? error.message : String(error))) + throw error + } +} diff --git a/integrations/notion/src/webhook-events/handlers/database-deleted.ts b/integrations/notion/src/webhook-events/handlers/database-deleted.ts index a3117b638e5..d7751931da0 100644 --- a/integrations/notion/src/webhook-events/handlers/database-deleted.ts +++ b/integrations/notion/src/webhook-events/handlers/database-deleted.ts @@ -1,7 +1,7 @@ import * as sdk from '@botpress/sdk' +import { BASE_EVENT_PAYLOAD } from 'definitions/events' import * as filesReadonly from '../../files-readonly' import { NotionClient } from '../../notion-api' -import { BASE_EVENT_PAYLOAD } from '../base-payload' import * as bp from '.botpress' const NOTIFICATION_PAYLOAD = BASE_EVENT_PAYLOAD.extend({ @@ -22,22 +22,23 @@ export const handleDatabaseDeletedEvent: bp.IntegrationProps['handler'] = async const payload: sdk.z.infer = JSON.parse(props.req.body!) const notionClient = await NotionClient.create(props) - const deletedDatabase = await notionClient.getDatabase({ databaseId: payload.entity.id }) + // Try to get as data source first (new API) + const deletedDataSource = await notionClient.getDataSource({ dataSourceId: payload.entity.id }) - if (!deletedDatabase) { - console.debug(`Notion database ${payload.entity.id} not found. Ignoring database.deleted event.`) + if (!deletedDataSource) { + console.debug(`Notion data source ${payload.entity.id} not found. Ignoring database.deleted event.`) return } - const databaseName = filesReadonly.getDatabaseTitle(deletedDatabase) - const parentPath = await filesReadonly.retrieveParentPath(deletedDatabase.parent, notionClient) - const absolutePath = `/${parentPath}/${databaseName}` + const dataSourceName = filesReadonly.getDataSourceTitle(deletedDataSource) + const parentPath = await filesReadonly.retrieveParentPath(deletedDataSource.parent, notionClient) + const absolutePath = `/${parentPath}/${dataSourceName}` await props.client.createEvent({ type: 'folderDeletedRecursive', payload: { folder: { - ...filesReadonly.mapDatabaseToFolder(deletedDatabase), + ...filesReadonly.mapDataSourceToFolder(deletedDataSource), absolutePath, }, }, diff --git a/integrations/notion/src/webhook-events/handlers/index.ts b/integrations/notion/src/webhook-events/handlers/index.ts index 9b731cececb..d4094e6eb40 100644 --- a/integrations/notion/src/webhook-events/handlers/index.ts +++ b/integrations/notion/src/webhook-events/handlers/index.ts @@ -4,3 +4,4 @@ export * from './page-created' export * from './page-deleted' export * from './page-moved' export * from './webhook-verification' +export * from './comment-created' diff --git a/integrations/notion/src/webhook-events/handlers/page-created.ts b/integrations/notion/src/webhook-events/handlers/page-created.ts index 7e3c54036ea..7d892ae9551 100644 --- a/integrations/notion/src/webhook-events/handlers/page-created.ts +++ b/integrations/notion/src/webhook-events/handlers/page-created.ts @@ -1,7 +1,7 @@ import * as sdk from '@botpress/sdk' +import { BASE_EVENT_PAYLOAD } from 'definitions/events' import * as filesReadonly from '../../files-readonly' import { NotionClient } from '../../notion-api' -import { BASE_EVENT_PAYLOAD } from '../base-payload' import * as bp from '.botpress' const NOTIFICATION_PAYLOAD = BASE_EVENT_PAYLOAD.extend({ diff --git a/integrations/notion/src/webhook-events/handlers/page-deleted.ts b/integrations/notion/src/webhook-events/handlers/page-deleted.ts index 022475f19fd..0d2bbf7df11 100644 --- a/integrations/notion/src/webhook-events/handlers/page-deleted.ts +++ b/integrations/notion/src/webhook-events/handlers/page-deleted.ts @@ -1,7 +1,7 @@ import * as sdk from '@botpress/sdk' +import { BASE_EVENT_PAYLOAD } from 'definitions/events' import * as filesReadonly from '../../files-readonly' import { NotionClient } from '../../notion-api' -import { BASE_EVENT_PAYLOAD } from '../base-payload' import * as bp from '.botpress' const NOTIFICATION_PAYLOAD = BASE_EVENT_PAYLOAD.extend({ diff --git a/integrations/notion/src/webhook-events/handlers/page-moved.ts b/integrations/notion/src/webhook-events/handlers/page-moved.ts index 72fed14ff44..4ef3b8ed229 100644 --- a/integrations/notion/src/webhook-events/handlers/page-moved.ts +++ b/integrations/notion/src/webhook-events/handlers/page-moved.ts @@ -1,7 +1,7 @@ import * as sdk from '@botpress/sdk' +import { BASE_EVENT_PAYLOAD } from 'definitions/events' import * as filesReadonly from '../../files-readonly' import { NotionClient } from '../../notion-api' -import { BASE_EVENT_PAYLOAD } from '../base-payload' import * as bp from '.botpress' const NOTIFICATION_PAYLOAD = BASE_EVENT_PAYLOAD.extend({ diff --git a/integrations/notion/tsconfig.json b/integrations/notion/tsconfig.json index 758f7d7ec50..c2216822479 100644 --- a/integrations/notion/tsconfig.json +++ b/integrations/notion/tsconfig.json @@ -4,7 +4,8 @@ "baseUrl": ".", "outDir": "dist", "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "noErrorTruncation": true }, "include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"] } diff --git a/integrations/zendesk/integration.definition.ts b/integrations/zendesk/integration.definition.ts index d5a0a1fa0cc..29c002a9ba5 100644 --- a/integrations/zendesk/integration.definition.ts +++ b/integrations/zendesk/integration.definition.ts @@ -6,7 +6,7 @@ import { actions, events, configuration, channels, states, user } from './src/de export default new sdk.IntegrationDefinition({ name: 'zendesk', title: 'Zendesk', - version: '3.0.3', + version: '3.0.4', icon: 'icon.svg', description: 'Optimize your support workflow. Trigger workflows from ticket updates as well as manage tickets, access conversations, and engage with customers.', diff --git a/integrations/zendesk/src/oauth/wizard.ts b/integrations/zendesk/src/oauth/wizard.ts index 2578bb9af5c..386af2e983d 100644 --- a/integrations/zendesk/src/oauth/wizard.ts +++ b/integrations/zendesk/src/oauth/wizard.ts @@ -146,7 +146,7 @@ const _oauthCallbackHandler: WizardHandler = async (props) => { const newCredentials = { ...credentials, accessToken } await _patchCredentialsState(client, ctx, newCredentials) - await client.configureIntegration({ identifier: subdomain }) + await client.configureIntegration({ identifier: ctx.webhookId }) return responses.redirectToStep('end') } diff --git a/packages/cognitive/package.json b/packages/cognitive/package.json index 165b90ba256..261b9ff9e1c 100644 --- a/packages/cognitive/package.json +++ b/packages/cognitive/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cognitive", - "version": "0.3.5", + "version": "0.3.6", "description": "Wrapper around the Botpress Client to call LLMs", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/cognitive/src/client.ts b/packages/cognitive/src/client.ts index 1d332690375..663871ff94d 100644 --- a/packages/cognitive/src/client.ts +++ b/packages/cognitive/src/client.ts @@ -174,7 +174,7 @@ export class Cognitive { } public async generateContent(input: InputProps): Promise { - if (!this._useBeta || !getCognitiveV2Model(input.model!)) { + if (!this._useBeta || !input.model || !getCognitiveV2Model(input.model)) { return this._generateContent(input) } diff --git a/packages/llmz/package.json b/packages/llmz/package.json index df7f6281aac..add9498253e 100644 --- a/packages/llmz/package.json +++ b/packages/llmz/package.json @@ -2,7 +2,7 @@ "name": "llmz", "type": "module", "description": "LLMz - An LLM-native Typescript VM built on top of Zui", - "version": "0.0.41", + "version": "0.0.42", "types": "./dist/index.d.ts", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -71,7 +71,7 @@ }, "peerDependencies": { "@botpress/client": "1.29.0", - "@botpress/cognitive": "0.3.5", + "@botpress/cognitive": "0.3.6", "@bpinternal/thicktoken": "^1.0.5", "@bpinternal/zui": "^1.3.2" }, diff --git a/packages/zai/package.json b/packages/zai/package.json index 015b0d02595..fc77664663e 100644 --- a/packages/zai/package.json +++ b/packages/zai/package.json @@ -1,7 +1,7 @@ { "name": "@botpress/zai", "description": "Zui AI (zai) – An LLM utility library written on top of Zui and the Botpress API", - "version": "2.5.8", + "version": "2.5.9", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -32,7 +32,7 @@ "author": "", "license": "ISC", "dependencies": { - "@botpress/cognitive": "0.3.5", + "@botpress/cognitive": "0.3.6", "json5": "^2.2.3", "jsonrepair": "^3.10.0", "lodash-es": "^4.17.21", diff --git a/plugins/conversation-insights/package.json b/plugins/conversation-insights/package.json index faeb3941fee..0895270af1b 100644 --- a/plugins/conversation-insights/package.json +++ b/plugins/conversation-insights/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/cognitive": "0.3.5", + "@botpress/cognitive": "0.3.6", "@botpress/sdk": "workspace:*", "browser-or-node": "^2.1.1", "jsonrepair": "^3.10.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 249ec6b7faf..c323d83df78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1541,11 +1541,14 @@ importers: specifier: workspace:* version: link:../../packages/sdk-addons '@notionhq/client': - specifier: ^2.3.0 - version: 2.3.0 + specifier: ^5.6.0 + version: 5.6.0 + '@tryfabric/martian': + specifier: ^1.2.4 + version: 1.2.4 notion-to-md: specifier: ^4.0.0-alpha.4 - version: 4.0.0-alpha.4(@notionhq/client@2.3.0) + version: 4.0.0-alpha.4(@notionhq/client@5.6.0) devDependencies: '@botpress/cli': specifier: workspace:* @@ -2827,7 +2830,7 @@ importers: specifier: 1.29.0 version: link:../client '@botpress/cognitive': - specifier: 0.3.5 + specifier: 0.3.6 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^1.0.5 @@ -3013,7 +3016,7 @@ importers: packages/zai: dependencies: '@botpress/cognitive': - specifier: 0.3.5 + specifier: 0.3.6 version: link:../cognitive '@bpinternal/thicktoken': specifier: ^1.0.0 @@ -3132,7 +3135,7 @@ importers: plugins/conversation-insights: dependencies: '@botpress/cognitive': - specifier: 0.3.5 + specifier: 0.3.6 version: link:../../packages/cognitive '@botpress/sdk': specifier: workspace:* @@ -5024,10 +5027,14 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@notionhq/client@2.3.0': - resolution: {integrity: sha512-l7WqTCpQqC+HibkB9chghONQTYcxNQT0/rOJemBfmuKQRTu2vuV8B3yA395iKaUdDo7HI+0KvQaz9687Xskzkw==} + '@notionhq/client@1.0.4': + resolution: {integrity: sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==} engines: {node: '>=12'} + '@notionhq/client@5.6.0': + resolution: {integrity: sha512-eA3dO87vQJhFmR59utXH8r0nnulW7C7oTcxfp3bpiiTiv59luCkOkbbALCIa8TzBDdELoRD/zJEIfKcynyFR6Q==} + engines: {node: '>=18'} + '@octokit/app@13.1.5': resolution: {integrity: sha512-6qTa24S+gdQUU66SCVfqTkyt2jAr9/ZeyPqJhnNI9PZ8Wum4lQy3bPS+voGlxABNOlzRKnxbSdYKoraMr3MqBA==} engines: {node: '>= 14'} @@ -6016,6 +6023,10 @@ packages: '@telegraf/types@7.1.0': resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} + '@tryfabric/martian@1.2.4': + resolution: {integrity: sha512-g7SP7beaxrjxLnW//vskra07a1jsJowqp07KMouxh4gCwaF+ItHbRZN8O+1dhJivBi3VdasT71BPyk+8wzEreQ==} + engines: {node: '>=15'} + '@tsconfig/node10@1.0.9': resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -6173,6 +6184,9 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -6266,6 +6280,9 @@ packages: '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -6760,6 +6777,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + bail@1.0.5: + resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -6981,6 +7001,9 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + ccount@1.1.0: + resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -7003,12 +7026,21 @@ packages: character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + chart.js@3.9.1: resolution: {integrity: sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==} @@ -8478,6 +8510,12 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -8504,6 +8542,10 @@ packages: resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} engines: {node: '>= 0.4'} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -8528,6 +8570,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8568,6 +8613,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -8595,6 +8643,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -9033,6 +9085,10 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + katex@0.12.0: + resolution: {integrity: sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==} + hasBin: true + keyv@3.1.0: resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} @@ -9158,6 +9214,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + longest-streak@2.0.4: + resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -9238,6 +9297,9 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -9260,39 +9322,69 @@ packages: peerDependencies: react: 18.x + mdast-util-find-and-replace@1.1.1: + resolution: {integrity: sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + mdast-util-gfm-autolink-literal@0.1.3: + resolution: {integrity: sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==} + mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} mdast-util-gfm-footnote@2.1.0: resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + mdast-util-gfm-strikethrough@0.2.3: + resolution: {integrity: sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==} + mdast-util-gfm-strikethrough@2.0.0: resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + mdast-util-gfm-table@0.1.6: + resolution: {integrity: sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==} + mdast-util-gfm-table@2.0.0: resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + mdast-util-gfm-task-list-item@0.1.6: + resolution: {integrity: sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==} + mdast-util-gfm-task-list-item@2.0.0: resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + mdast-util-gfm@0.1.2: + resolution: {integrity: sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==} + mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@0.1.2: + resolution: {integrity: sha512-fogAitds+wH+QRas78Yr1TwmQGN4cW/G2WRw5ePuNoJbBSPJCxIOCE8MTzHgWHVSpgkRaPQTgfzXRE1CrwWSlg==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-markdown@0.6.5: + resolution: {integrity: sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} @@ -9331,27 +9423,48 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-gfm-autolink-literal@0.5.7: + resolution: {integrity: sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==} + micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} micromark-extension-gfm-footnote@2.1.0: resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + micromark-extension-gfm-strikethrough@0.6.5: + resolution: {integrity: sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==} + micromark-extension-gfm-strikethrough@2.1.0: resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + micromark-extension-gfm-table@0.4.3: + resolution: {integrity: sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==} + micromark-extension-gfm-table@2.1.1: resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + micromark-extension-gfm-tagfilter@0.3.0: + resolution: {integrity: sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==} + micromark-extension-gfm-tagfilter@2.0.0: resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + micromark-extension-gfm-task-list-item@0.3.3: + resolution: {integrity: sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==} + micromark-extension-gfm-task-list-item@2.1.0: resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + micromark-extension-gfm@0.3.3: + resolution: {integrity: sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==} + micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@0.1.2: + resolution: {integrity: sha512-ZJXsT2eVPM8VTmcw0CPSDeyonOn9SziGK3Z+nkf9Vb6xMPeU+4JMEnO6vzDL10562Favw8Vste74f54rxJ/i6Q==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -9409,6 +9522,9 @@ packages: micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} @@ -9829,6 +9945,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} @@ -10263,12 +10382,21 @@ packages: rehype-stringify@10.0.1: resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + remark-gfm@1.0.0: + resolution: {integrity: sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==} + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-math@4.0.0: + resolution: {integrity: sha512-lH7SoQenXtQrvL0bm+mjZbvOk//YWNuyR+MxV18Qyv8rgFmMEGNuB0TSCQDkoDaiJ40FCnG8lxErc/zhcedYbw==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + remark-parse@9.0.0: + resolution: {integrity: sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==} + remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} @@ -10278,6 +10406,10 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + request@2.88.2: resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} engines: {node: '>= 6'} @@ -10927,6 +11059,9 @@ packages: triple-beam@1.3.0: resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} + trough@1.0.5: + resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -11187,15 +11322,27 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unified@9.2.2: + resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + unist-util-visit-parents@6.0.1: resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} @@ -11315,9 +11462,15 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -11659,6 +11812,9 @@ packages: zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + zwitch@1.0.5: + resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -13994,13 +14150,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - '@notionhq/client@2.3.0': + '@notionhq/client@1.0.4': dependencies: '@types/node-fetch': 2.6.12 node-fetch: 2.7.0 transitivePeerDependencies: - encoding + '@notionhq/client@5.6.0': {} + '@octokit/app@13.1.5': dependencies: '@octokit/auth-app': 4.0.13 @@ -15314,6 +15472,17 @@ snapshots: '@telegraf/types@7.1.0': {} + '@tryfabric/martian@1.2.4': + dependencies: + '@notionhq/client': 1.0.4 + remark-gfm: 1.0.0 + remark-math: 4.0.0 + remark-parse: 9.0.0 + unified: 9.2.2 + transitivePeerDependencies: + - encoding + - supports-color + '@tsconfig/node10@1.0.9': {} '@tsconfig/node12@1.0.11': {} @@ -15497,6 +15666,10 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -15586,6 +15759,8 @@ snapshots: '@types/turndown@5.0.6': {} + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@types/urijs@1.19.25': {} @@ -16293,6 +16468,8 @@ snapshots: babel-plugin-jest-hoist: 29.5.0 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.26.9) + bail@1.0.5: {} + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -16601,6 +16778,8 @@ snapshots: caseless@0.12.0: {} + ccount@1.1.0: {} + ccount@2.0.1: {} chai@5.1.2: @@ -16622,10 +16801,16 @@ snapshots: character-entities-html4@2.1.0: {} + character-entities-legacy@1.1.4: {} + character-entities-legacy@3.0.0: {} + character-entities@1.2.4: {} + character-entities@2.0.2: {} + character-reference-invalid@1.1.4: {} + chart.js@3.9.1: {} check-error@2.1.1: {} @@ -18467,6 +18652,13 @@ snapshots: ipaddr.js@1.9.1: {} + is-alphabetical@1.0.4: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -18497,6 +18689,8 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-buffer@2.0.5: {} + is-callable@1.2.7: {} is-core-module@2.15.1: @@ -18522,6 +18716,8 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-decimal@1.0.4: {} + is-docker@3.0.0: {} is-electron@2.2.0: {} @@ -18553,6 +18749,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@1.0.4: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -18572,6 +18770,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} is-plain-object@5.0.0: {} @@ -19242,6 +19442,10 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 + katex@0.12.0: + dependencies: + commander: 2.20.3 + keyv@3.1.0: dependencies: json-buffer: 3.0.0 @@ -19364,6 +19568,8 @@ snapshots: safe-stable-stringify: 2.4.3 triple-beam: 1.3.0 + longest-streak@2.0.4: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -19444,6 +19650,10 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + markdown-table@3.0.4: {} marked@15.0.1: {} @@ -19457,6 +19667,12 @@ snapshots: marked: 7.0.4 react: 18.3.1 + mdast-util-find-and-replace@1.1.1: + dependencies: + escape-string-regexp: 4.0.0 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -19464,6 +19680,16 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + mdast-util-from-markdown@0.8.5: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -19481,6 +19707,14 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@0.1.3: + dependencies: + ccount: 1.1.0 + mdast-util-find-and-replace: 1.1.1 + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -19499,6 +19733,10 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-strikethrough@0.2.3: + dependencies: + mdast-util-to-markdown: 0.6.5 + mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -19507,6 +19745,11 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-table@0.1.6: + dependencies: + markdown-table: 2.0.0 + mdast-util-to-markdown: 0.6.5 + mdast-util-gfm-table@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -19517,6 +19760,10 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-task-list-item@0.1.6: + dependencies: + mdast-util-to-markdown: 0.6.5 + mdast-util-gfm-task-list-item@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -19526,6 +19773,16 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm@0.1.2: + dependencies: + mdast-util-gfm-autolink-literal: 0.1.3 + mdast-util-gfm-strikethrough: 0.2.3 + mdast-util-gfm-table: 0.1.6 + mdast-util-gfm-task-list-item: 0.1.6 + mdast-util-to-markdown: 0.6.5 + transitivePeerDependencies: + - supports-color + mdast-util-gfm@3.1.0: dependencies: mdast-util-from-markdown: 2.0.2 @@ -19538,6 +19795,12 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@0.1.2: + dependencies: + longest-streak: 2.0.4 + mdast-util-to-markdown: 0.6.5 + repeat-string: 1.6.1 + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -19555,6 +19818,15 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + mdast-util-to-markdown@0.6.5: + dependencies: + '@types/unist': 2.0.11 + longest-streak: 2.0.4 + mdast-util-to-string: 2.0.0 + parse-entities: 2.0.0 + repeat-string: 1.6.1 + zwitch: 1.0.5 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -19567,6 +19839,8 @@ snapshots: unist-util-visit: 5.0.0 zwitch: 2.0.4 + mdast-util-to-string@2.0.0: {} + mdast-util-to-string@4.0.0: dependencies: '@types/mdast': 4.0.4 @@ -19646,6 +19920,12 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@0.5.7: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.1 @@ -19664,6 +19944,12 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-strikethrough@0.6.5: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + micromark-extension-gfm-strikethrough@2.1.0: dependencies: devlop: 1.1.0 @@ -19673,6 +19959,12 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-table@0.4.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + micromark-extension-gfm-table@2.1.1: dependencies: devlop: 1.1.0 @@ -19681,10 +19973,18 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-tagfilter@0.3.0: {} + micromark-extension-gfm-tagfilter@2.0.0: dependencies: micromark-util-types: 2.0.2 + micromark-extension-gfm-task-list-item@0.3.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + micromark-extension-gfm-task-list-item@2.1.0: dependencies: devlop: 1.1.0 @@ -19693,6 +19993,17 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm@0.3.3: + dependencies: + micromark: 2.11.4 + micromark-extension-gfm-autolink-literal: 0.5.7 + micromark-extension-gfm-strikethrough: 0.6.5 + micromark-extension-gfm-table: 0.4.3 + micromark-extension-gfm-tagfilter: 0.3.0 + micromark-extension-gfm-task-list-item: 0.3.3 + transitivePeerDependencies: + - supports-color + micromark-extension-gfm@3.0.0: dependencies: micromark-extension-gfm-autolink-literal: 2.1.0 @@ -19704,6 +20015,13 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@0.1.2: + dependencies: + katex: 0.12.0 + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -19796,6 +20114,13 @@ snapshots: micromark-util-types@2.0.2: {} + micromark@2.11.4: + dependencies: + debug: 4.4.1 + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + micromark@4.0.2: dependencies: '@types/debug': 4.1.12 @@ -20021,9 +20346,9 @@ snapshots: normalize-url@4.5.1: {} - notion-to-md@4.0.0-alpha.4(@notionhq/client@2.3.0): + notion-to-md@4.0.0-alpha.4(@notionhq/client@5.6.0): dependencies: - '@notionhq/client': 2.3.0 + '@notionhq/client': 5.6.0 mime: 3.0.0 node-fetch: 2.7.0 transitivePeerDependencies: @@ -20238,6 +20563,15 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 @@ -20639,6 +20973,13 @@ snapshots: hast-util-to-html: 9.0.5 unified: 11.0.5 + remark-gfm@1.0.0: + dependencies: + mdast-util-gfm: 0.1.2 + micromark-extension-gfm: 0.3.3 + transitivePeerDependencies: + - supports-color + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -20650,6 +20991,13 @@ snapshots: transitivePeerDependencies: - supports-color + remark-math@4.0.0: + dependencies: + mdast-util-math: 0.1.2 + micromark-extension-math: 0.1.2 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -20659,6 +21007,12 @@ snapshots: transitivePeerDependencies: - supports-color + remark-parse@9.0.0: + dependencies: + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 @@ -20682,6 +21036,8 @@ snapshots: transitivePeerDependencies: - supports-color + repeat-string@1.6.1: {} + request@2.88.2: dependencies: aws-sign2: 0.7.0 @@ -21440,6 +21796,8 @@ snapshots: triple-beam@1.3.0: {} + trough@1.0.5: {} + trough@2.2.0: {} ts-api-utils@2.1.0(typescript@5.6.3): @@ -21730,6 +22088,18 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unified@9.2.2: + dependencies: + '@types/unist': 2.0.11 + bail: 1.0.5 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 2.1.0 + trough: 1.0.5 + vfile: 4.2.1 + + unist-util-is@4.1.0: {} + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -21738,10 +22108,19 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -21846,11 +22225,23 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.4.1 + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + vfile@6.0.3: dependencies: '@types/unist': 3.0.3 @@ -22210,4 +22601,6 @@ snapshots: zod@3.24.2: {} + zwitch@1.0.5: {} + zwitch@2.0.4: {}