diff --git a/.github/actions/update-linear-status/action.yml b/.github/actions/update-linear-status/action.yml new file mode 100644 index 00000000000..daf0d6350b5 --- /dev/null +++ b/.github/actions/update-linear-status/action.yml @@ -0,0 +1,26 @@ +name: Update Linear Status + +inputs: + linearApiKey: + required: true + teamName: + required: true + targetLabel: + required: true + +runs: + using: 'composite' + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + shell: bash + run: pnpm install tsx -w --save-dev + + - name: Update issues + shell: bash + run: npx tsx ./.github/scripts/update-linear.ts + env: + LINEAR_API_KEY: ${{inputs.linearApiKey}} + TEAM_NAME: ${{inputs.teamName}} + TARGET_LABEL: ${{inputs.targetLabel}} diff --git a/.github/scripts/update-linear.ts b/.github/scripts/update-linear.ts new file mode 100644 index 00000000000..6313db3c47f --- /dev/null +++ b/.github/scripts/update-linear.ts @@ -0,0 +1,93 @@ +import { LinearClient, Team, WorkflowState, IssueLabel } from '@linear/sdk' + +const LINEAR_API_KEY = process.env.LINEAR_API_KEY +const SOURCE_STATE_NAME = 'Staging' +const TARGET_STATE_NAME = 'Production (Done)' +const TEAM_NAME = process.env.TEAM_NAME +const TARGET_LABEL = process.env.TARGET_LABEL + +if (!LINEAR_API_KEY) { + throw new Error('No LINEAR_API_KEY environment variable') +} + +if (!TEAM_NAME) { + throw new Error('No TEAM_NAME environment variable') +} + +if (!TARGET_LABEL) { + throw new Error('No TARGET_LABEL environment variable') +} + +void updateLinearIssues() + +async function getTeam(): Promise { + const client = new LinearClient({ apiKey: LINEAR_API_KEY }) + const teams = await client.teams() + const targetTeam = teams.nodes.find((t) => t.name === TEAM_NAME) + + if (!targetTeam) throw new Error(`Could not find team with name "${TEAM_NAME}"`) + return targetTeam +} + +async function getStates(targetTeam: Team): Promise<{ sourceState: WorkflowState; targetState: WorkflowState }> { + const states = await targetTeam.states() + const sourceState = states.nodes.find((s) => s.name === SOURCE_STATE_NAME) + const targetState = states.nodes.find((s) => s.name === TARGET_STATE_NAME) + + if (!sourceState) throw new Error(`Could not find workflow state ${SOURCE_STATE_NAME}`) + if (!targetState) throw new Error(`Could not find workflow state ${TARGET_STATE_NAME}`) + + console.log(`Found source state: ${sourceState.name} (${sourceState.id})`) + console.log(`Found target state: ${targetState.name} (${targetState.id})`) + + return { sourceState, targetState } +} + +async function getTargetLabels(targetTeam: Team): Promise { + let labels = await targetTeam.labels() + const labelsArray = [...labels.nodes] + + while (labels.pageInfo.hasNextPage) { + labels = await labels.fetchNext() + labelsArray.push(...labels.nodes) + } + + const targetLabels = labelsArray.filter( + (label) => label.name === TARGET_LABEL || label.name.startsWith(TARGET_LABEL + '/') + ) + + if (targetLabels.length === 0) { + throw new Error(`Could not find any labels matching ${TARGET_LABEL}`) + } + + console.log(`Found ${targetLabels.length} matching label(s):`) + targetLabels.forEach((label) => { + console.log(` - ${label.name} (${label.id})`) + }) + + return targetLabels +} + +async function updateLinearIssues() { + const targetTeam = await getTeam() + const { sourceState, targetState } = await getStates(targetTeam) + const targetLabels = await getTargetLabels(targetTeam) + + const targetLabelIds = targetLabels.map((label) => label.id) + + const issues = await targetTeam.issues({ + filter: { + labels: { some: { id: { in: targetLabelIds } } }, + state: { id: { eq: sourceState.id } }, + }, + }) + + console.log(`Found ${issues.nodes.length} issue(s) in state "${SOURCE_STATE_NAME}"`) + + await Promise.all( + issues.nodes.map(async (issue) => { + console.log(`Updating issue ${issue.identifier} to ${TARGET_STATE_NAME}`) + await issue.update({ stateId: targetState.id }) + }) + ) +} diff --git a/.github/workflows/deploy-integrations-production.yml b/.github/workflows/deploy-integrations-production.yml index 5413793c15c..a87c6261eea 100644 --- a/.github/workflows/deploy-integrations-production.yml +++ b/.github/workflows/deploy-integrations-production.yml @@ -44,3 +44,11 @@ jobs: force: ${{ github.event.inputs.force == 'true' }} token_cloud_ops_account: ${{ secrets.PRODUCTION_TOKEN_CLOUD_OPS_ACCOUNT }} cloud_ops_workspace_id: ${{ secrets.PRODUCTION_CLOUD_OPS_WORKSPACE_ID }} + + - name: Update Linear Status + uses: ./.github/actions/update-linear-status + continue-on-error: true + with: + linearApiKey: ${{secrets.LINEAR_API_KEY}} + teamName: 'SHELL (Integration)' + targetLabel: 'area/integration' diff --git a/bots/bugbuster/bot.definition.ts b/bots/bugbuster/bot.definition.ts index fcf99af224a..a012f8271e6 100644 --- a/bots/bugbuster/bot.definition.ts +++ b/bots/bugbuster/bot.definition.ts @@ -43,9 +43,6 @@ export default new sdk.BotDefinition({ output: { schema: sdk.z.object({}) }, }, }, - __advanced: { - useLegacyZuiTransformer: true, - }, }) .addIntegration(github, { enabled: true, @@ -55,7 +52,6 @@ export default new sdk.BotDefinition({ githubWebhookSecret: genenv.BUGBUSTER_GITHUB_WEBHOOK_SECRET, }, }) - // TODO: replace Telegram with Slack when available .addIntegration(telegram, { enabled: true, configurationType: null, diff --git a/bots/bugbuster/src/bootstrap.ts b/bots/bugbuster/src/bootstrap.ts new file mode 100644 index 00000000000..798574925b9 --- /dev/null +++ b/bots/bugbuster/src/bootstrap.ts @@ -0,0 +1,24 @@ +import { IssueProcessor } from './services/issue-processor' +import { TeamsManager } from './services/teams-manager' +import * as types from './types' +import * as utils from './utils' + +export const bootstrap = async (props: types.CommonHandlerProps, conversationId?: string) => { + const { client, logger, ctx } = props + const botpress = utils.botpress.BotpressApi.create(props) + + const _handleError = (context: string) => (thrown: unknown) => + botpress.handleError({ context, conversationId }, thrown) + + // TODO: make this synchronous so it won't slow down bootstraping or throw + const linear = await utils.linear.LinearApi.create().catch(_handleError('trying to initialize Linear API')) + const teamsManager = new TeamsManager(linear, client, ctx.botId) + const issueProcessor = new IssueProcessor(logger, linear, teamsManager) + + return { + botpress, + linear, + teamsManager, + issueProcessor, + } +} diff --git a/bots/bugbuster/src/handlers/github-issue-opened.ts b/bots/bugbuster/src/handlers/github-issue-opened.ts index d2348b02cbd..a51d35bf839 100644 --- a/bots/bugbuster/src/handlers/github-issue-opened.ts +++ b/bots/bugbuster/src/handlers/github-issue-opened.ts @@ -1,4 +1,4 @@ -import * as utils from '../utils' +import * as boot from '../bootstrap' import * as bp from '.botpress' export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = async (props): Promise => { @@ -6,20 +6,30 @@ export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = a props.logger.info('Received GitHub issue', githubIssue) - const linear = await utils.linear.LinearApi.create() + const { linear, botpress } = await boot.bootstrap(props) + + const _handleError = + (context: string) => + (thrown: unknown): Promise => + botpress.handleError({ context, conversationId: undefined }, thrown) + + const githubLabel = await linear + .findLabel({ name: 'github', parentName: 'origin' }) + .catch(_handleError('trying to find the origin/github label in Linear')) - const githubLabel = await linear.findLabel({ name: 'github', parentName: 'origin' }) if (!githubLabel) { props.logger.error('Label origin/github not found in engineering team') } - const linearResponse = await linear.client.createIssue({ - teamId: linear.teams.ENG.id, - stateId: linear.states.ENG.TRIAGE.id, - title: githubIssue.issue.name, - description: githubIssue.issue.body, - labelIds: githubLabel ? [githubLabel.id] : [], - }) + const linearResponse = await linear.client + .createIssue({ + teamId: linear.teams.ENG.id, + stateId: linear.states.ENG.TRIAGE.id, + title: githubIssue.issue.name, + description: githubIssue.issue.body, + labelIds: githubLabel ? [githubLabel.id] : [], + }) + .catch(_handleError('trying to create a Linear issue from the GitHub issue')) const comment = [ 'This issue was created from GitHub by BugBuster Bot.', @@ -27,8 +37,10 @@ export const handleGithubIssueOpened: bp.EventHandlers['github:issueOpened'] = a `GitHub Issue: [${githubIssue.issue.name}](${githubIssue.issue.url})`, ].join('\n') - await linear.client.createComment({ - issueId: linearResponse.issueId, - body: comment, - }) + await linear.client + .createComment({ + issueId: linearResponse.issueId, + body: comment, + }) + .catch(_handleError('trying to create a comment on the Linear issue created from GitHub')) } diff --git a/bots/bugbuster/src/handlers/issue-processor.ts b/bots/bugbuster/src/handlers/issue-processor.ts deleted file mode 100644 index 12825116b6a..00000000000 --- a/bots/bugbuster/src/handlers/issue-processor.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { BotLogger } from '@botpress/sdk' -import { Issue, Pagination } from 'src/utils/graphql-queries' -import { LinearApi, StateKey } from 'src/utils/linear-utils' -import * as linlint from '../linear-lint-issue' -import { listTeams } from './teams-manager' -import { Client, WorkflowHandlerProps } from '.botpress' - -const IGNORED_STATUSES: StateKey[] = ['TRIAGE', 'PRODUCTION_DONE', 'CANCELED', 'STALE'] -const LINTIGNORE_LABEL_NAME = 'lintignore' - -export class IssueProcessor { - public constructor( - private _logger: BotLogger, - private _linear: LinearApi, - private _client: Client, - private _botId: string - ) {} - - /** - * @returns The corresponding issue, or `undefined` if the issue is not found or not valid. - */ - public async findIssue( - issueNumber: number, - teamKey: string | undefined, - eventName: string - ): Promise { - if (!issueNumber || !teamKey) { - this._logger.error('Missing issueNumber or teamKey in event payload') - return - } - - this._logger.info(`Linear issue ${eventName} event received`, `${teamKey}-${issueNumber}`) - - const teams = await listTeams(this._client, this._botId) - if (!this._linear.isTeam(teamKey) || !teams.result?.includes(teamKey)) { - this._logger.error(`Ignoring issue of team "${teamKey}"`) - return - } - - const issue = await this._linear.findIssue({ teamKey, issueNumber }) - if (!issue) { - this._logger.error(`Issue with number ${issueNumber} not found in team ${teamKey}`) - return - } - return issue - } - - public async listIssues(teams: string[], endCursor?: string): Promise { - const validatedTeams = teams.filter((value) => this._linear.isTeam(value)) - - const issues: Issue[] = [] - let pagination: Pagination | undefined - - do { - const { issues: newIssues, pagination: newPagination } = await this._linear.listIssues( - { - teamKeys: validatedTeams, - statusesToOmit: IGNORED_STATUSES, - }, - endCursor - ) - - issues.push(...newIssues) - pagination = newPagination - endCursor = pagination?.endCursor - } while (pagination?.hasNextPage) - - return issues - } - - public async runLint(issue: Issue) { - const status = this._linear.issueStatus(issue) - if (IGNORED_STATUSES.includes(status) || issue.labels.nodes.some((label) => label.name === LINTIGNORE_LABEL_NAME)) { - return - } - - const errors = await linlint.lintIssue(issue, status) - - if (errors.length === 0) { - this._logger.info(`Issue ${issue.identifier} passed all lint checks.`) - await this._linear.resolveComments(issue) - return - } - - this._logger.warn(`Issue ${issue.identifier} has ${errors.length} lint errors:`) - - await this._linear.client.createComment({ - issueId: issue.id, - body: [ - `BugBuster Bot found the following problems with ${issue.identifier}:`, - '', - ...errors.map((error: any) => `- ${error.message}`), - ].join('\n'), - }) - } - - public async runLints(issues: Issue[], workflow: WorkflowHandlerProps['lintAll']['workflow']) { - for (const issue of issues) { - await this.runLint(issue) - await workflow.acknowledgeStartOfProcessing() - await this._client.setState({ - id: workflow.id, - name: 'lastLintedId', - type: 'workflow', - payload: { id: issue.id }, - }) - } - } -} diff --git a/bots/bugbuster/src/handlers/linear-issue-created.ts b/bots/bugbuster/src/handlers/linear-issue-created.ts index 9dd4ef6ed47..1f3124fcd83 100644 --- a/bots/bugbuster/src/handlers/linear-issue-created.ts +++ b/bots/bugbuster/src/handlers/linear-issue-created.ts @@ -1,17 +1,22 @@ -import * as utils from '../utils' -import { IssueProcessor } from './issue-processor' +import * as boot from '../bootstrap' import * as bp from '.botpress' export const handleLinearIssueCreated: bp.EventHandlers['linear:issueCreated'] = async (props) => { - const { client, event, logger, ctx } = props + const { event } = props const { number: issueNumber, teamKey } = event.payload - const linear = await utils.linear.LinearApi.create() - const issueProcessor = new IssueProcessor(logger, linear, client, ctx.botId) - const issue = await issueProcessor.findIssue(issueNumber, teamKey, 'created') + + const { botpress, issueProcessor } = await boot.bootstrap(props) + + const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown) + + props.logger.info('Linear issue created event received', `${teamKey}-${issueNumber}`) + const issue = await issueProcessor + .findIssue(issueNumber, teamKey) + .catch(_handleError('trying to find the created Linear issue')) if (!issue) { return } - await issueProcessor.runLint(issue) + await issueProcessor.lintIssue(issue).catch(_handleError('trying to lint the created Linear issue')) } diff --git a/bots/bugbuster/src/handlers/linear-issue-updated.ts b/bots/bugbuster/src/handlers/linear-issue-updated.ts index fc8ba88ffaf..035e7452431 100644 --- a/bots/bugbuster/src/handlers/linear-issue-updated.ts +++ b/bots/bugbuster/src/handlers/linear-issue-updated.ts @@ -1,27 +1,32 @@ -import * as utils from '../utils' -import { IssueProcessor } from './issue-processor' +import * as boot from '../bootstrap' import * as bp from '.botpress' export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] = async (props) => { - const { client, ctx, event, logger } = props + const { event, logger } = props const { number: issueNumber, teamKey } = event.payload - const linear = await utils.linear.LinearApi.create() - const issueProcessor = new IssueProcessor(logger, linear, client, ctx.botId) - const issue = await issueProcessor.findIssue(issueNumber, teamKey, 'updated') + const { botpress, issueProcessor } = await boot.bootstrap(props) + + const _handleError = (context: string) => (thrown: unknown) => botpress.handleError({ context }, thrown) + + props.logger.info('Linear issue updated event received', `${teamKey}-${issueNumber}`) + const issue = await issueProcessor + .findIssue(issueNumber, teamKey) + .catch(_handleError('trying to find the updated Linear issue')) if (!issue) { return } - const botpress = await utils.botpress.BotpressApi.create(props) - const recentlyLinted = await botpress.getRecentlyLinted() + const recentlyLinted = await botpress.getRecentlyLinted().catch(_handleError('trying to get recently linted issues')) if (recentlyLinted.some(({ id: issueId }) => issue.id === issueId)) { logger.info(`Issue ${issue.identifier} has already been linted recently, skipping...`) return } - await issueProcessor.runLint(issue) - await botpress.setRecentlyLinted([...recentlyLinted, { id: issue.id, lintedAt: new Date().toISOString() }]) + await issueProcessor.lintIssue(issue).catch(_handleError('trying to lint the updated Linear issue')) + await botpress + .setRecentlyLinted([...recentlyLinted, { id: issue.id, lintedAt: new Date().toISOString() }]) + .catch(_handleError('trying to update recently linted issues')) } diff --git a/bots/bugbuster/src/handlers/lint-all.ts b/bots/bugbuster/src/handlers/lint-all.ts index 5c3cc38c46b..993b5165f38 100644 --- a/bots/bugbuster/src/handlers/lint-all.ts +++ b/bots/bugbuster/src/handlers/lint-all.ts @@ -1,40 +1,59 @@ -import { BotClient, BotContext, BotLogger } from '@botpress/sdk/dist/bot' -import { Result } from 'src/types' -import { BotpressApi } from 'src/utils/botpress-utils' -import { handleError } from 'src/utils/error-handler' -import { LinearApi } from 'src/utils/linear-utils' -import { IssueProcessor } from './issue-processor' -import { listTeams } from './teams-manager' -import { TBot, WorkflowHandlerProps } from '.botpress' - -export const lintAll = async ( - client: BotClient, - logger: BotLogger, - ctx: BotContext, - workflow: WorkflowHandlerProps['lintAll']['workflow'], - conversationId?: string -): Promise> => { - const _handleError = (context: string) => handleError({ context, logger, botpress, conversationId }) - const botpress = new BotpressApi(client, ctx.botId) - - const teamsResult = await listTeams(client, ctx.botId).catch(_handleError('trying to lint all issues')) - if (!teamsResult.success || !teamsResult.result) { - return { success: false, message: teamsResult.message } - } +import * as boot from '../bootstrap' +import * as utils from '../utils' +import * as bp from '.botpress' + +export const handleLintAll: bp.WorkflowHandlers['lintAll'] = async (props) => { + const { client, workflow, conversation } = props + + const conversationId = conversation?.id + + const { botpress, issueProcessor } = await boot.bootstrap(props, conversationId) - const lastLintedId = await client.getOrSetState({ - id: workflow.id, - name: 'lastLintedId', - type: 'workflow', - payload: {}, - }) + const _handleError = (context: string) => (thrown: unknown) => + botpress.handleError({ context, conversationId }, thrown) + + const { + state: { + payload: { id: lastLintedId }, + }, + } = await client + .getOrSetState({ + id: workflow.id, + name: 'lastLintedId', + type: 'workflow', + payload: {}, + }) + .catch(_handleError('trying to get last linted issue ID')) - const linear = await LinearApi.create().catch(_handleError('trying to lint all issues')) - const issueProcessor = new IssueProcessor(logger, linear, client, ctx.botId) const issues = await issueProcessor - .listIssues(teamsResult.result, lastLintedId.state.payload.id) + .listRelevantIssues(lastLintedId) // TODO: we should not list all issues at first, bug fetch next page and lint progressively .catch(_handleError('trying to list all issues')) - await issueProcessor.runLints(issues, workflow).catch(_handleError('trying to run lints on all issues')) - return { success: true, message: 'linted all issues' } + for (const issue of issues) { + await issueProcessor.lintIssue(issue).catch(_handleError(`trying to lint issue ${issue.identifier}`)) + await workflow.acknowledgeStartOfProcessing().catch(_handleError('trying to acknowledge start of processing')) + await client + .setState({ + id: workflow.id, + name: 'lastLintedId', + type: 'workflow', + payload: { id: issue.id }, + }) + .catch(_handleError('trying to update last linted issue ID')) + } + + if (conversationId) { + await botpress.respondText(conversationId, 'linted all issues').catch(() => {}) + } + + await workflow.setCompleted() +} + +export const handleLintAllTimeout: bp.WorkflowHandlers['lintAll'] = async (props) => { + const { conversation } = props + + const botpress = utils.botpress.BotpressApi.create(props) + if (conversation?.id) { + await botpress.respondText(conversation.id, "Error: the 'lintAll' operation timed out") + } } diff --git a/bots/bugbuster/src/handlers/message-created.ts b/bots/bugbuster/src/handlers/message-created.ts index 07bc57d06f2..c1e92ed7bc5 100644 --- a/bots/bugbuster/src/handlers/message-created.ts +++ b/bots/bugbuster/src/handlers/message-created.ts @@ -1,6 +1,4 @@ -import { handleError } from 'src/utils/error-handler' -import * as utils from '../utils' -import { addTeam, listTeams, removeTeam } from './teams-manager' +import * as boot from '../bootstrap' import * as bp from '.botpress' const MESSAGING_INTEGRATIONS = ['telegram', 'slack'] @@ -13,13 +11,13 @@ const COMMAND_LIST_MESSAGE = `Unknown command. Here's a list of possible command const ARGUMENT_REQUIRED_MESSAGE = 'Error: an argument is required with this command.' export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => { - const { conversation, message, client, ctx, logger } = props + const { conversation, message, client } = props if (!MESSAGING_INTEGRATIONS.includes(conversation.integration)) { props.logger.info(`Ignoring message from ${conversation.integration}`) return } - const botpress = await utils.botpress.BotpressApi.create(props) + const { botpress, teamsManager } = await boot.bootstrap(props, conversation.id) if (message.type !== 'text') { await botpress.respondText(conversation.id, COMMAND_LIST_MESSAGE) @@ -37,29 +35,22 @@ export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => { return } - const _handleError = (context: string) => handleError({ context, logger, botpress, conversationId: conversation.id }) + const _handleError = (context: string) => (thrown: unknown) => + botpress.handleError({ context, conversationId: conversation.id }, thrown) switch (command) { - case '#health': { - let isLinearHealthy = true - try { - await utils.linear.LinearApi.create() - } catch { - isLinearHealthy = false - } - - await botpress.respondText(conversation.id, `Linear: ${isLinearHealthy ? '' : 'un'}healthy`) - break - } case '#addTeam': { if (!teamKey) { await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE) return } - const linear = await utils.linear.LinearApi.create().catch(_handleError('trying to add a team')) - const result = await addTeam(client, ctx.botId, teamKey, linear).catch(_handleError('trying to add a team')) - await botpress.respondText(conversation.id, result.message) + await teamsManager.addWatchedTeam(teamKey).catch(_handleError('trying to add a team')) + + await botpress.respondText( + conversation.id, + `Success: the team with the key '${teamKey}' has been added to the watched team list.` + ) break } case '#removeTeam': { @@ -67,13 +58,17 @@ export const handleMessageCreated: bp.MessageHandlers['*'] = async (props) => { await botpress.respondText(conversation.id, ARGUMENT_REQUIRED_MESSAGE) return } - const result = await removeTeam(client, ctx.botId, teamKey).catch(_handleError('trying to remove a team')) - await botpress.respondText(conversation.id, result.message) + + await teamsManager.removeWatchedTeam(teamKey).catch(_handleError('trying to remove a team')) + await botpress.respondText( + conversation.id, + `Success: the team with the key '${teamKey}' has been removed from the watched team list.` + ) break } case '#listTeams': { - const result = await listTeams(client, ctx.botId).catch(_handleError('trying to list teams')) - await botpress.respondText(conversation.id, result.message) + const teams = await teamsManager.listWatchedTeams().catch(_handleError('trying to list teams')) + await botpress.respondText(conversation.id, teams.join(', ')) break } case '#lintAll': { diff --git a/bots/bugbuster/src/handlers/teams-manager.ts b/bots/bugbuster/src/handlers/teams-manager.ts deleted file mode 100644 index fd3000625b1..00000000000 --- a/bots/bugbuster/src/handlers/teams-manager.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Result } from 'src/types' -import { LinearApi } from 'src/utils/linear-utils' -import * as bp from '.botpress' - -const _getWatchedTeams = async (client: bp.Client, botId: string) => { - return ( - await client.getOrSetState({ - id: botId, - name: 'watchedTeams', - type: 'bot', - payload: { - teamKeys: [], - }, - }) - ).state.payload.teamKeys -} - -const _setWatchedTeams = async (client: bp.Client, botId: string, teamKeys: string[]) => { - await client.setState({ - id: botId, - name: 'watchedTeams', - type: 'bot', - payload: { - teamKeys, - }, - }) -} - -export async function addTeam(client: bp.Client, botId: string, key: string, linear: LinearApi): Promise> { - const teamKeys = await _getWatchedTeams(client, botId) - if (teamKeys.includes(key)) { - return { - success: false, - message: `Error: the team with the key '${key}' is already being watched.`, - } - } - if (!linear.isTeam(key)) { - return { - success: false, - message: `Error: the team with the key '${key}' does not exist.`, - } - } - - await _setWatchedTeams(client, botId, [...teamKeys, key]) - return { - success: true, - message: `Success: the team with the key '${key}' has been added to the watched team list.`, - } -} - -export async function removeTeam(client: bp.Client, botId: string, key: string): Promise> { - const teamKeys = await _getWatchedTeams(client, botId) - if (!teamKeys.includes(key)) { - return { - message: `Error: the team with the key '${key}' is not currently being watched.`, - success: false, - } - } - - await _setWatchedTeams( - client, - botId, - teamKeys.filter((k) => k !== key) - ) - return { - success: false, - message: `Success: the team with the key '${key}' has been removed from the watched team list.`, - } -} - -export async function listTeams(client: bp.Client, botId: string): Promise> { - const teamKeys = await _getWatchedTeams(client, botId) - if (teamKeys.length === 0) { - return { - success: false, - message: 'You have no watched teams.', - } - } - return { - success: true, - message: teamKeys.join(', '), - result: teamKeys, - } -} diff --git a/bots/bugbuster/src/index.ts b/bots/bugbuster/src/index.ts index 6ff82060b56..6e731892797 100644 --- a/bots/bugbuster/src/index.ts +++ b/bots/bugbuster/src/index.ts @@ -1,5 +1,4 @@ import * as handlers from './handlers' -import { BotpressApi } from './utils/botpress-utils' import * as bp from '.botpress' export const bot = new bp.Bot({ actions: {} }) @@ -9,52 +8,8 @@ bot.on.event('linear:issueUpdated', handlers.handleLinearIssueUpdated) bot.on.event('linear:issueCreated', handlers.handleLinearIssueCreated) bot.on.message('*', handlers.handleMessageCreated) -const LINT_ALL_ERROR_PREFIX = "Error during the 'lintAll' workflow: " - -bot.on.workflowStart('lintAll', async (props) => { - await handleLintAllWorkflow(props) -}) - -bot.on.workflowContinue('lintAll', async (props) => { - await handleLintAllWorkflow(props) -}) - -bot.on.workflowTimeout('lintAll', async (props) => { - const { - client, - ctx, - workflow: { - input: { conversationId }, - }, - } = props - - const botpress = new BotpressApi(client, ctx.botId) - if (conversationId) { - await botpress.respondText(conversationId, "Error: the 'lintAll' operation timed out") - } -}) - -const handleLintAllWorkflow = async (props: bp.WorkflowHandlerProps['lintAll']) => { - const { client, logger, ctx, workflow, conversation } = props - const conversationId = conversation?.id - const botpress = new BotpressApi(client, ctx.botId) - - try { - const result = await handlers.lintAll(client, logger, ctx, workflow, conversationId) - if (!result.success) { - if (conversationId) { - await botpress.respondText(conversationId, LINT_ALL_ERROR_PREFIX + result.message) - } - await workflow.setFailed({ failureReason: result.message }) - return - } - if (conversationId) { - await botpress.respondText(conversationId, result.message) - } - await workflow.setCompleted() - } catch { - return - } -} +bot.on.workflowStart('lintAll', handlers.handleLintAll) +bot.on.workflowContinue('lintAll', handlers.handleLintAll) +bot.on.workflowTimeout('lintAll', handlers.handleLintAllTimeout) export default bot diff --git a/bots/bugbuster/src/services/issue-processor/index.ts b/bots/bugbuster/src/services/issue-processor/index.ts new file mode 100644 index 00000000000..9f62936426c --- /dev/null +++ b/bots/bugbuster/src/services/issue-processor/index.ts @@ -0,0 +1,88 @@ +import * as sdk from '@botpress/sdk' +import * as lin from '../../utils/linear-utils' +import * as tm from '../teams-manager' +import { lintIssue } from './lint-issue' + +const IGNORED_STATUSES: lin.StateKey[] = ['TRIAGE', 'PRODUCTION_DONE', 'CANCELED', 'STALE'] +const LINTIGNORE_LABEL_NAME = 'lintignore' + +export class IssueProcessor { + public constructor( + private _logger: sdk.BotLogger, + private _linear: lin.LinearApi, + private _teamsManager: tm.TeamsManager + ) {} + + /** + * @returns The corresponding issue, or `undefined` if the issue is not found or not valid. + */ + public async findIssue(issueNumber: number, teamKey: string | undefined): Promise { + if (!issueNumber || !teamKey) { + this._logger.error('Missing issueNumber or teamKey in event payload') + return + } + + const watchedTeams = await this._teamsManager.listWatchedTeams() + if (!this._linear.isTeam(teamKey) || !watchedTeams.includes(teamKey)) { + this._logger.info(`Ignoring issue of team "${teamKey}"`) + return + } + + const issue = await this._linear.findIssue({ teamKey, issueNumber }) + if (!issue) { + this._logger.warn(`Issue with number ${issueNumber} not found in team ${teamKey}`) + return + } + + return issue + } + + public async listRelevantIssues(endCursor?: string): Promise { + const watchedTeams = await this._teamsManager.listWatchedTeams() + + const issues: lin.Issue[] = [] + let pagination: lin.Pagination | undefined + + do { + const { issues: newIssues, pagination: newPagination } = await this._linear.listIssues( + { + teamKeys: watchedTeams, + statusesToOmit: IGNORED_STATUSES, + }, + endCursor + ) + + issues.push(...newIssues) + pagination = newPagination + endCursor = pagination?.endCursor + } while (pagination?.hasNextPage) + + return issues + } + + public async lintIssue(issue: lin.Issue) { + const status = this._linear.issueStatus(issue) + if (IGNORED_STATUSES.includes(status) || issue.labels.nodes.some((label) => label.name === LINTIGNORE_LABEL_NAME)) { + return + } + + const errors = await lintIssue(issue, status) + + if (errors.length === 0) { + this._logger.info(`Issue ${issue.identifier} passed all lint checks.`) + await this._linear.resolveComments(issue) + return + } + + this._logger.warn(`Issue ${issue.identifier} has ${errors.length} lint errors:`) + + await this._linear.client.createComment({ + issueId: issue.id, + body: [ + `BugBuster Bot found the following problems with ${issue.identifier}:`, + '', + ...errors.map((error) => `- ${error.message}`), + ].join('\n'), + }) + } +} diff --git a/bots/bugbuster/src/issue-title-format-validator.test.ts b/bots/bugbuster/src/services/issue-processor/issue-title-format-validator.test.ts similarity index 100% rename from bots/bugbuster/src/issue-title-format-validator.test.ts rename to bots/bugbuster/src/services/issue-processor/issue-title-format-validator.test.ts diff --git a/bots/bugbuster/src/issue-title-format-validator.ts b/bots/bugbuster/src/services/issue-processor/issue-title-format-validator.ts similarity index 100% rename from bots/bugbuster/src/issue-title-format-validator.ts rename to bots/bugbuster/src/services/issue-processor/issue-title-format-validator.ts diff --git a/bots/bugbuster/src/linear-lint-issue.ts b/bots/bugbuster/src/services/issue-processor/lint-issue.ts similarity index 90% rename from bots/bugbuster/src/linear-lint-issue.ts rename to bots/bugbuster/src/services/issue-processor/lint-issue.ts index e65269fa61c..b46bd7c38e3 100644 --- a/bots/bugbuster/src/linear-lint-issue.ts +++ b/bots/bugbuster/src/services/issue-processor/lint-issue.ts @@ -1,12 +1,11 @@ +import * as lin from '../../utils/linear-utils' import { isIssueTitleFormatValid } from './issue-title-format-validator' -import { Issue } from './utils/graphql-queries' -import { StateKey } from './utils/linear-utils' export type IssueLint = { message: string } -export const lintIssue = async (issue: Issue, status: StateKey): Promise => { +export const lintIssue = async (issue: lin.Issue, status: lin.StateKey): Promise => { const lints: string[] = [] if (!_hasLabelOfCategory(issue, 'type')) { @@ -68,6 +67,6 @@ export const lintIssue = async (issue: Issue, status: StateKey): Promise ({ message })) } -const _hasLabelOfCategory = (issue: Issue, category: string) => { +const _hasLabelOfCategory = (issue: lin.Issue, category: string) => { return issue.labels.nodes.some((label) => label.parent?.name === category) } diff --git a/bots/bugbuster/src/services/teams-manager.ts b/bots/bugbuster/src/services/teams-manager.ts new file mode 100644 index 00000000000..4db529f18dc --- /dev/null +++ b/bots/bugbuster/src/services/teams-manager.ts @@ -0,0 +1,61 @@ +import * as lin from '../utils/linear-utils' +import * as bp from '.botpress' + +export class TeamsManager { + public constructor( + private _linear: lin.LinearApi, + private _client: bp.Client, + private _botId: string + ) {} + + public async addWatchedTeam(key: string): Promise { + const teamKeys = await this._getWatchedTeams() + if (teamKeys.includes(key)) { + throw new Error(`The team with the key '${key}' is already being watched.`) + } + if (!this._linear.isTeam(key)) { + throw new Error(`The team with the key '${key}' does not exist.`) + } + + await this._setWatchedTeams([...teamKeys, key]) + } + + public async removeWatchedTeam(key: string): Promise { + const teamKeys = await this._getWatchedTeams() + if (!teamKeys.includes(key)) { + throw new Error(`The team with the key '${key}' is not currently being watched.`) + } + } + + public async listWatchedTeams(): Promise { + const teamKeys = await this._getWatchedTeams() + if (teamKeys.length === 0) { + throw new Error('You have no watched teams.') + } + return teamKeys + } + + private _getWatchedTeams = async () => { + return ( + await this._client.getOrSetState({ + id: this._botId, + name: 'watchedTeams', + type: 'bot', + payload: { + teamKeys: [], + }, + }) + ).state.payload.teamKeys + } + + private _setWatchedTeams = async (teamKeys: string[]) => { + await this._client.setState({ + id: this._botId, + name: 'watchedTeams', + type: 'bot', + payload: { + teamKeys, + }, + }) + } +} diff --git a/bots/bugbuster/src/types.ts b/bots/bugbuster/src/types.ts index 93c80d5dd8d..a46d1b34d2b 100644 --- a/bots/bugbuster/src/types.ts +++ b/bots/bugbuster/src/types.ts @@ -1,5 +1,3 @@ -export type Result = { - success: boolean - message: string - result?: T -} +import * as bp from '.botpress' + +export type CommonHandlerProps = bp.WorkflowHandlerProps['lintAll'] | bp.EventHandlerProps | bp.MessageHandlerProps diff --git a/bots/bugbuster/src/utils/botpress-utils.ts b/bots/bugbuster/src/utils/botpress-utils.ts index e783575fc5e..00c4d127a42 100644 --- a/bots/bugbuster/src/utils/botpress-utils.ts +++ b/bots/bugbuster/src/utils/botpress-utils.ts @@ -1,20 +1,28 @@ +import * as sdk from '@botpress/sdk' +import * as types from '../types' import * as bp from '.botpress' -export type BotProps = bp.EventHandlerProps | bp.MessageHandlerProps export type BotMessage = Pick export type GithubIssue = bp.integrations.github.actions.findTarget.output.Output['targets'][number] export type IssueLintEntry = bp.states.recentlyLinted.RecentlyLinted['payload']['issues'][number] const RECENT_THRESHOLD: number = 1000 * 60 * 10 // 10 minutes +export type ErrorHandlerProps = { + context: string + conversationId?: string +} + +// TODO: most of this class is not really meant to be in utils, consider moving it in services like the teams-manager class export class BotpressApi { - public constructor( + private constructor( private _client: bp.Client, - private _botId: string + private _botId: string, + private _logger: sdk.BotLogger ) {} - public static async create(props: BotProps): Promise { - return new BotpressApi(props.client, props.ctx.botId) + public static create(props: types.CommonHandlerProps): BotpressApi { + return new BotpressApi(props.client, props.ctx.botId, props.logger) } public async respond(conversationId: string, msg: BotMessage): Promise { @@ -73,6 +81,16 @@ export class BotpressApi { }) } + public handleError = async (props: ErrorHandlerProps, thrown: unknown): Promise => { + const error = thrown instanceof Error ? thrown : new Error(String(thrown)) + const message = `An error occured while ${props.context}: ${error.message}` + this._logger.error(message) + if (props.conversationId) { + await this.respondText(props.conversationId, message).catch(() => {}) // if this fails, there's nothing we can do + } + throw new sdk.RuntimeError(error.message) + } + private _isRecentlyLinted = (issue: IssueLintEntry): boolean => { const lintedAt = new Date(issue.lintedAt).getTime() const now = new Date().getTime() diff --git a/bots/bugbuster/src/utils/error-handler.ts b/bots/bugbuster/src/utils/error-handler.ts deleted file mode 100644 index f543193d13c..00000000000 --- a/bots/bugbuster/src/utils/error-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BotLogger, RuntimeError } from '@botpress/sdk' -import { BotpressApi } from './botpress-utils' - -export type ErrorHandlerProps = { - context: string - logger: BotLogger - botpress: BotpressApi - conversationId?: string -} - -export const handleError = (props: ErrorHandlerProps) => async (thrown: unknown) => { - const error = thrown instanceof Error ? thrown : new Error(String(thrown)) - const message = `An error occured while ${props.context}: ${error.message}` - props.logger.error(message) - if (props.conversationId) { - await props.botpress.respondText(props.conversationId, message) - } - throw new RuntimeError(error.message) -} diff --git a/bots/bugbuster/src/utils/linear-utils.ts b/bots/bugbuster/src/utils/linear-utils/client.ts similarity index 87% rename from bots/bugbuster/src/utils/linear-utils.ts rename to bots/bugbuster/src/utils/linear-utils/client.ts index 0af9660ae4e..aa7fc3de4ab 100644 --- a/bots/bugbuster/src/utils/linear-utils.ts +++ b/bots/bugbuster/src/utils/linear-utils/client.ts @@ -1,7 +1,7 @@ import * as lin from '@linear/sdk' -import * as genenv from '../../.genenv' -import * as utils from '.' -import { Issue, GRAPHQL_QUERIES, QUERY_INPUT, QUERY_RESPONSE, Pagination } from './graphql-queries' +import * as utils from '..' +import * as genenv from '../../../.genenv' +import * as graphql from './graphql-queries' const TEAM_KEYS = ['SQD', 'FT', 'BE', 'ENG'] as const export type TeamKey = (typeof TEAM_KEYS)[number] @@ -55,7 +55,7 @@ export class LinearApi { return this._teams.some((team) => team.key === teamKey) } - public async findIssue(filter: { teamKey: string; issueNumber: number }): Promise { + public async findIssue(filter: { teamKey: string; issueNumber: number }): Promise { const { teamKey, issueNumber } = filter const { issues } = await this.listIssues({ @@ -77,7 +77,7 @@ export class LinearApi { statusesToOmit?: StateKey[] }, nextPage?: string - ): Promise<{ issues: Issue[]; pagination?: Pagination }> { + ): Promise<{ issues: graphql.Issue[]; pagination?: graphql.Pagination }> { const { teamKeys, issueNumber, statusesToOmit } = filter const teamsExist = teamKeys.every((key) => this._teams.some((team) => team.key === key)) @@ -93,7 +93,7 @@ export class LinearApi { return '' }) - const queryInput: GRAPHQL_QUERIES['listIssues'][QUERY_INPUT] = { + const queryInput: graphql.GRAPHQL_QUERIES['listIssues'][graphql.QUERY_INPUT] = { filter: { team: { key: { in: teamKeys } }, ...(issueNumber && { number: { eq: issueNumber } }), @@ -121,7 +121,7 @@ export class LinearApi { return label || undefined } - public issueStatus(issue: Issue): StateKey { + public issueStatus(issue: graphql.Issue): StateKey { const state = this._states.find((s) => s.state.id === issue.state.id) if (!state) { throw new Error(`State with ID "${issue.state.id}" not found.`) @@ -129,7 +129,7 @@ export class LinearApi { return state.key } - public async resolveComments(issue: Issue): Promise { + public async resolveComments(issue: graphql.Issue): Promise { const comments = issue.comments.nodes const promises: Promise[] = [] @@ -206,11 +206,11 @@ export class LinearApi { return stateObjects } - private async _executeGraphqlQuery( + private async _executeGraphqlQuery( queryName: K, - variables: GRAPHQL_QUERIES[K][QUERY_INPUT] - ): Promise { - return (await this._client.client.rawRequest(GRAPHQL_QUERIES[queryName].query, variables)) - .data as GRAPHQL_QUERIES[K][QUERY_RESPONSE] + variables: graphql.GRAPHQL_QUERIES[K][graphql.QUERY_INPUT] + ): Promise { + return (await this._client.client.rawRequest(graphql.GRAPHQL_QUERIES[queryName].query, variables)) + .data as graphql.GRAPHQL_QUERIES[K][graphql.QUERY_RESPONSE] } } diff --git a/bots/bugbuster/src/utils/graphql-queries.ts b/bots/bugbuster/src/utils/linear-utils/graphql-queries.ts similarity index 100% rename from bots/bugbuster/src/utils/graphql-queries.ts rename to bots/bugbuster/src/utils/linear-utils/graphql-queries.ts diff --git a/bots/bugbuster/src/utils/linear-utils/index.ts b/bots/bugbuster/src/utils/linear-utils/index.ts new file mode 100644 index 00000000000..d3cb888c590 --- /dev/null +++ b/bots/bugbuster/src/utils/linear-utils/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export { Issue, Pagination } from './graphql-queries' diff --git a/integrations/asana/integration.definition.ts b/integrations/asana/integration.definition.ts index 1071843fe87..42180a9815c 100644 --- a/integrations/asana/integration.definition.ts +++ b/integrations/asana/integration.definition.ts @@ -7,7 +7,7 @@ import { configuration, states, user, channels, actions } from './src/definition export default new IntegrationDefinition({ name: INTEGRATION_NAME, - version: '0.3.7', + version: '0.3.8', title: 'Asana', readme: 'hub.md', description: 'Connect your bot to your Asana inbox, create and update tasks, add comments, and locate users.', diff --git a/integrations/asana/src/definitions/actions.ts b/integrations/asana/src/definitions/actions.ts index 0075127e41c..6058a43a5a7 100644 --- a/integrations/asana/src/definitions/actions.ts +++ b/integrations/asana/src/definitions/actions.ts @@ -9,7 +9,6 @@ import { addCommentToTaskInputSchema, addCommentToTaskOutputSchema, } from '../misc/custom-schemas' -import { createTaskUi, updateTaskUi, findUserUi, addCommentToTaskUi } from '../misc/custom-uis' type SdkActions = NonNullable type SdkAction = SdkActions[string] @@ -19,7 +18,6 @@ const createTask = { description: 'Create Task', input: { schema: createTaskInputSchema, - ui: createTaskUi, }, output: { schema: createTaskOutputSchema, @@ -31,7 +29,6 @@ const updateTask = { description: 'Update Task by taskId', input: { schema: updateTaskInputSchema, - ui: updateTaskUi, }, output: { schema: updateTaskOutputSchema, @@ -43,7 +40,6 @@ const findUser = { description: 'Find User by userId', input: { schema: findUserInputSchema, - ui: findUserUi, }, output: { schema: findUserOutputSchema, @@ -55,7 +51,6 @@ const addCommentToTask = { description: 'Add Comment to Task, by task ID', input: { schema: addCommentToTaskInputSchema, - ui: addCommentToTaskUi, }, output: { schema: addCommentToTaskOutputSchema, diff --git a/integrations/asana/src/definitions/channels.ts b/integrations/asana/src/definitions/channels.ts index 5aba40950ba..3d46c035824 100644 --- a/integrations/asana/src/definitions/channels.ts +++ b/integrations/asana/src/definitions/channels.ts @@ -2,6 +2,8 @@ import { IntegrationDefinitionProps, messages } from '@botpress/sdk' export const channels = { channel: { + title: 'Channel', + description: 'The channel for Asana', messages: { ...messages.defaults, markdown: messages.markdown, diff --git a/integrations/asana/src/misc/custom-schemas.ts b/integrations/asana/src/misc/custom-schemas.ts index e9304e30015..4b5919d37c4 100644 --- a/integrations/asana/src/misc/custom-schemas.ts +++ b/integrations/asana/src/misc/custom-schemas.ts @@ -2,36 +2,47 @@ import { z } from '@botpress/sdk' import { photoSchema, workspaceSchema } from './sub-schemas' export const createTaskInputSchema = z.object({ - name: z.string().describe('The name of the task (e.g. "My Test Task")'), + name: z.string().describe('The name of the task (e.g. "My Test Task")').title('Name'), notes: z .string() .optional() - .describe('The description of the task (Optional) (e.g. "This is my other task created using the Asana API")'), + .describe('The description of the task (Optional) (e.g. "This is my other task created using the Asana API")') + .title('Notes'), assignee: z .string() .optional() .default('me') .describe( 'The ID of the user who will be assigned to the task or "me" to assign to the current user (Optional) (e.g. "1215207682932839") (Default: "me")' - ), + ) + .title('Assignee'), projects: z .string() .optional() .describe( 'The project IDs should be strings separated by commas (Optional) (e.g. "1205199808673678, 1215207282932839").' - ), - parent: z.string().optional().describe('The ID of the parent task (Optional) (e.g. "1205206556256028")'), + ) + .title('Projects'), + parent: z + .string() + .optional() + .describe('The ID of the parent task (Optional) (e.g. "1205206556256028")') + .title('Parent'), start_on: z .string() .optional() - .describe('The start date of the task in YYYY-MM-DD format (Optional) (e.g. "2023-08-13")'), + .describe('The start date of the task in YYYY-MM-DD format (Optional) (e.g. "2023-08-13")') + .title('Start On'), due_on: z .string() .optional() - .describe('The due date of the task without a specific time in YYYY-MM-DD format (Optional) (e.g. "2023-08-15")'), + .describe('The due date of the task without a specific time in YYYY-MM-DD format (Optional) (e.g. "2023-08-15")') + .title('Due On'), }) -export const taskOutputSchema = z.object({ permalink_url: z.string() }) +export const taskOutputSchema = z.object({ + permalink_url: z.string().describe('The permalink url').title('Permalink Url'), +}) export const createTaskOutputSchema = taskOutputSchema export const updateTaskInputSchema = createTaskInputSchema @@ -40,43 +51,45 @@ export const updateTaskInputSchema = createTaskInputSchema parent: true, }) .extend({ - taskId: z.string().describe('Task ID to update'), - name: z.string().optional().describe('The name of the task (Optional) (e.g. "My Test Task")'), + taskId: z.string().describe('Task ID to update').title('Task ID'), + name: z.string().optional().describe('The name of the task (Optional) (e.g. "My Test Task")').title('Name'), assignee: z .string() .optional() .describe( 'The ID of the user who will be assigned to the task or "me" to assign to the current user (Optional) (e.g. "1215207682932839")' - ), + ) + .title('Assignee'), completed: z .string() .optional() .describe( 'If the task is completed, enter "true" (without quotes), otherwise it will keep its previous status. (Optional)' - ), + ) + .title('Completed'), }) export const updateTaskOutputSchema = taskOutputSchema export const findUserInputSchema = z.object({ - userEmail: z.string().describe('User Email (e.g. "mrsomebody@example.com")'), + userEmail: z.string().describe('User Email (e.g. "mrsomebody@example.com")').title('User Email'), }) export const findUserOutputSchema = z .object({ - gid: z.string(), - name: z.string(), - email: z.string(), - photo: photoSchema, - resource_type: z.string(), - workspaces: z.array(workspaceSchema), + gid: z.string().describe('The GID of the User').title('GID'), + name: z.string().describe('The name of the user').title('Name'), + email: z.string().describe('The email of the user').title('Email'), + photo: photoSchema.describe('The photo of the user').title('Photo'), + resource_type: z.string().describe('The resource type of the user').title('Resource Type'), + workspaces: z.array(workspaceSchema).describe('List of the workspaces').title('Workspaces'), }) .partial() export const addCommentToTaskInputSchema = z.object({ - taskId: z.string().describe('Task ID to comment'), - comment: z.string().describe('Content of the comment to be added'), + taskId: z.string().describe('Task ID to comment').title('Task ID'), + comment: z.string().describe('Content of the comment to be added').title('Comment'), }) export const addCommentToTaskOutputSchema = z.object({ - text: z.string(), + text: z.string().describe('The text of the comment').title('Text'), }) diff --git a/integrations/asana/src/misc/custom-uis.ts b/integrations/asana/src/misc/custom-uis.ts deleted file mode 100644 index a1423b3cd05..00000000000 --- a/integrations/asana/src/misc/custom-uis.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const taskUi = { - name: { - title: 'The name of the task (e.g. "My Test Task")', - }, - notes: { - title: 'The description of the task (Optional) (e.g. "This is my other task created using the Asana API")', - }, - assignee: { - title: - 'The ID of the user who will be assigned to the task or "me" to assign to the current user (Optional) (e.g. "1215207682932839") (Default: "me")', - }, - projects: { - title: - 'The project IDs should be strings separated by commas (Optional) (e.g. "1205199808673678, 1215207282932839").', - }, - parent: { - title: 'The ID of the parent task (Optional) (e.g. "1205206556256028")', - }, - start_on: { - title: 'The start date of the task in YYYY-MM-DD format (Optional) (e.g. "2023-08-13")', - }, - due_on: { - title: 'The due date of the task without a specific time in YYYY-MM-DD format (Optional) (e.g. "2023-08-15")', - }, -} - -export const createTaskUi = taskUi - -export const updateTaskUi = { - ...taskUi, - projects: undefined, - parent: undefined, - taskId: { - title: 'Task ID to update', - }, - name: { - title: 'The name of the task (Optional) (e.g. "My Test Task")', - }, - assignee: { - title: - 'The ID of the user who will be assigned to the task or "me" to assign to the current user (Optional) (e.g. "1215207682932839")', - }, - completed: { - title: - 'If the task is completed, enter "true" (without quotes), otherwise it will keep its previous status. (Optional)', - }, -} - -export const findUserUi = { - userEmail: { - title: 'User Email (e.g. "mrsomebody@example.com")', - }, -} - -export const addCommentToTaskUi = { - taskId: { - title: 'Task ID to comment', - }, - comment: { - title: 'Content of the comment to be added', - }, -} diff --git a/integrations/asana/src/misc/sub-schemas/index.ts b/integrations/asana/src/misc/sub-schemas/index.ts index a9e24145e16..474ead4d5b8 100644 --- a/integrations/asana/src/misc/sub-schemas/index.ts +++ b/integrations/asana/src/misc/sub-schemas/index.ts @@ -1,18 +1,18 @@ import { z } from '@botpress/sdk' const workspaceSchema = z.object({ - gid: z.string(), - name: z.string(), - resource_type: z.string(), + gid: z.string().describe('The GID of the workspace').title('GID'), + name: z.string().describe('The name of the workspace').title('Name'), + resource_type: z.string().describe('The resource type of the workspace').title('Resource Type'), }) const photoSchema = z .object({ - image_21x21: z.string(), - image_27x27: z.string(), - image_36x36: z.string(), - image_60x60: z.string(), - image_128x128: z.string(), + image_21x21: z.string().describe('An Image 21 by 21').title('Image 21x21'), + image_27x27: z.string().describe('An Image 27 by 27').title('Image 27x27'), + image_36x36: z.string().describe('An Image 36 by 36').title('Image 36x36'), + image_60x60: z.string().describe('An Image 60 by 60').title('Image 60x60'), + image_128x128: z.string().describe('An Image 128 by 128').title('Image 128x128'), }) .nullable() diff --git a/integrations/charts/integration.definition.ts b/integrations/charts/integration.definition.ts index c3748e65923..e2610158708 100644 --- a/integrations/charts/integration.definition.ts +++ b/integrations/charts/integration.definition.ts @@ -5,12 +5,14 @@ import { actionDefinitions } from 'src/definitions/actions' export default new IntegrationDefinition({ name: 'charts', description: 'Easily generate a variety of charts, including line, bar, pie, and scatter plots, etc.', - version: '0.2.3', + version: '0.2.4', readme: 'hub.md', icon: 'icon.svg', actions: actionDefinitions, secrets: { - QUICKCHARTS_API_KEY: {}, + QUICKCHARTS_API_KEY: { + description: 'Quickcharts key', + }, }, __advanced: { useLegacyZuiTransformer: true, diff --git a/integrations/charts/src/definitions/actions.ts b/integrations/charts/src/definitions/actions.ts index 5cc8e570785..d9cd64b066c 100644 --- a/integrations/charts/src/definitions/actions.ts +++ b/integrations/charts/src/definitions/actions.ts @@ -2,112 +2,163 @@ import { z } from '@botpress/sdk' const generateLinePlot = { title: 'Line Plot', + description: 'Generate a line plot', input: { schema: z.object({ - xData: z.array(z.string().or(z.number())).catch(() => [1, 2, 3, 4, 5]), - yData: z.array(z.number()).catch(() => [1, 2, 3, 4, 5]), - title: z.string().optional(), - xAxisTitle: z.string().optional(), - yAxisTitle: z.string().optional(), + xData: z + .array(z.string().or(z.number())) + .catch(() => [1, 2, 3, 4, 5]) + .describe('The data for the x axis') + .title('X Data'), + yData: z + .array(z.number()) + .catch(() => [1, 2, 3, 4, 5]) + .describe('the data for the y axis') + .title('Y Data'), + title: z.string().optional().describe('The title of the plot').title('Line Plot Title'), + xAxisTitle: z.string().optional().describe('The title of the x axis').title('X Axis Title'), + yAxisTitle: z.string().optional().describe('The title of the y axis').title('Y Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generateBarChart = { title: 'Bar Chart', + description: 'Generate a Bar chart', input: { schema: z.object({ - xData: z.array(z.string().or(z.number())).catch(() => [1, 2, 3, 4, 5]), - yData: z.array(z.number()).catch(() => [1, 2, 3, 4, 5]), - title: z.string().optional(), - xAxisTitle: z.string().optional(), - yAxisTitle: z.string().optional(), + xData: z + .array(z.string().or(z.number())) + .catch(() => [1, 2, 3, 4, 5]) + .describe('The data for the x axis') + .title('X Data'), + yData: z + .array(z.number()) + .catch(() => [1, 2, 3, 4, 5]) + .describe('The data for the y axis') + .title('Y Data'), + title: z.string().optional().describe('The title of the Bar Chart').title('Bar Chart Title'), + xAxisTitle: z.string().optional().describe('The title of the x axis').title('X Axis Title'), + yAxisTitle: z.string().optional().describe('The title of the y axis').title('Y Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generatePieChart = { title: 'Pie Chart', + description: 'Generate a pie chart', input: { schema: z.object({ - labels: z.array(z.string()).catch(() => ['Label 1', 'Label 2', 'Label 3']), - data: z.array(z.number()).catch(() => [10, 20, 30]), - title: z.string().optional(), + labels: z + .array(z.string()) + .catch(() => ['Label 1', 'Label 2', 'Label 3']) + .describe('The labels for the data') + .title('Labels'), + data: z + .array(z.number()) + .catch(() => [10, 20, 30]) + .describe('The data to plot') + .title('Data'), + title: z.string().optional().describe('The title of the pie chart').title('Pie Chart Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } export const generateScatterPlot = { title: 'Scatter Plot', + description: 'Generate a scatter plot', input: { schema: z.object({ - data: z.array(z.object({ x: z.number(), y: z.number() })).catch(() => [ - { x: 1, y: 2 }, - { x: 2, y: 3 }, - { x: 3, y: 4 }, - ]), - title: z.string().optional(), - xAxisTitle: z.string().optional(), - yAxisTitle: z.string().optional(), + data: z + .array(z.object({ x: z.number(), y: z.number() })) + .catch(() => [ + { x: 1, y: 2 }, + { x: 2, y: 3 }, + { x: 3, y: 4 }, + ]) + .describe('The data to plot') + .title('Data'), + title: z.string().optional().describe('The title of the scatter plot').title('Scatter Plot Title'), + xAxisTitle: z.string().optional().describe('The title of the x axis').title('X Axis Title'), + yAxisTitle: z.string().optional().describe('The title of the y axis').title('Y Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generateDoughnutChart = { title: 'Doughnut Chart', + description: 'Generate a Doughnut Chart', input: { schema: z.object({ - labels: z.array(z.string()).catch(() => ['Label 1', 'Label 2', 'Label 3']), - data: z.array(z.number()).catch(() => [10, 20, 30]), - title: z.string().optional(), + labels: z + .array(z.string()) + .catch(() => ['Label 1', 'Label 2', 'Label 3']) + .describe('The labels for the data') + .title('Labels'), + data: z + .array(z.number()) + .catch(() => [10, 20, 30]) + .describe('The data to plot') + .title('Data'), + title: z.string().optional().describe('The title of the doughnut chart').title('Doughnut Chart Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generateRadarChart = { title: 'Radar Chart', + description: 'Generate a radar Chart', input: { schema: z.object({ - labels: z.array(z.string()).catch(() => ['Label 1', 'Label 2', 'Label 3']), - data: z.array(z.number()).catch(() => [10, 20, 30]), - title: z.string().optional(), - axisTitle: z.string().optional(), + labels: z + .array(z.string()) + .catch(() => ['Label 1', 'Label 2', 'Label 3']) + .describe('The labels for the data') + .title('Labels'), + data: z + .array(z.number()) + .catch(() => [10, 20, 30]) + .describe('The data to plot') + .title('Data'), + title: z.string().optional().describe('The title of the radar chart').title('Radar Chart Title'), + axisTitle: z.string().optional().describe('The title of the axis').title('Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generateBubbleChart = { title: 'Bubble Chart', + description: 'Generate a bubble Chart', input: { schema: z.object({ data: z @@ -122,33 +173,44 @@ const generateBubbleChart = { { x: 1, y: 2, r: 5 }, { x: 2, y: 3, r: 10 }, { x: 3, y: 4, r: 15 }, - ]), - title: z.string().optional(), - xAxisTitle: z.string().optional(), - yAxisTitle: z.string().optional(), + ]) + .describe('The data to plot') + .title('Data'), + title: z.string().optional().describe('The title of the bubble chart').title('Bubble Chart Title'), + xAxisTitle: z.string().optional().describe('The title of the x axis').title('X Axis Title'), + yAxisTitle: z.string().optional().describe('The title of the y axis').title('Y Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } const generateHorizontalBarChart = { title: 'Horizontal Bar Chart', + description: 'Generate a horizontal Chart', input: { schema: z.object({ - xData: z.array(z.string().or(z.number())).catch(() => [1, 2, 3, 4, 5]), - yData: z.array(z.number()).catch(() => [1, 2, 3, 4, 5]), - title: z.string().optional(), - xAxisTitle: z.string().optional(), - yAxisTitle: z.string().optional(), + xData: z + .array(z.string().or(z.number())) + .catch(() => [1, 2, 3, 4, 5]) + .describe('The data for the x axis') + .title('X Data'), + yData: z + .array(z.number()) + .catch(() => [1, 2, 3, 4, 5]) + .describe('The data for the y axis') + .title('Y Data'), + title: z.string().optional().describe('The title of the bar chart').title('Bar Chart Title'), + xAxisTitle: z.string().optional().describe('The title of the x axis').title('X Axis Title'), + yAxisTitle: z.string().optional().describe('The title of the y axis').title('Y Axis Title'), }), }, output: { schema: z.object({ - imageUrl: z.string(), + imageUrl: z.string().describe('The url of the generated image').title('Image Url'), }), }, } diff --git a/integrations/linear/integration.definition.ts b/integrations/linear/integration.definition.ts index 580c4f15a19..a18764ce74f 100644 --- a/integrations/linear/integration.definition.ts +++ b/integrations/linear/integration.definition.ts @@ -1,4 +1,3 @@ -/* bplint-disable */ import { IntegrationDefinition } from '@botpress/sdk' import { sentry as sentryHelpers } from '@botpress/sdk-addons' import deletable from './bp_modules/deletable' @@ -7,7 +6,7 @@ import { actions, channels, events, configuration, configurations, user, states, export default new IntegrationDefinition({ name: 'linear', - version: '1.1.3', + version: '1.1.4', title: 'Linear', description: 'Manage your projects autonomously. Have your bot participate in discussions, manage issues and teams, and track progress.', diff --git a/integrations/linear/src/handler.ts b/integrations/linear/src/handler.ts index 75975e1aca4..f4a08ed643b 100644 --- a/integrations/linear/src/handler.ts +++ b/integrations/linear/src/handler.ts @@ -5,7 +5,7 @@ import { fireIssueCreated } from './events/issueCreated' import { fireIssueDeleted } from './events/issueDeleted' import { fireIssueUpdated } from './events/issueUpdated' import { LinearEvent, handleOauth } from './misc/linear' -import { getUserAndConversation } from './misc/utils' +import { getLinearClient, getUserAndConversation } from './misc/utils' import * as bp from '.botpress' export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client, logger }) => { @@ -32,6 +32,12 @@ export const handler: bp.IntegrationProps['handler'] = async ({ req, ctx, client throw new Error('Webhook event is not properly authenticated: the signing secret is invalid.') } + const linearBotId = await _getLinearBotId({ client, ctx }) + if (linearEvent.data.userId === linearBotId || linearEvent.data.user?.id === linearBotId) { + logger.forBot().debug('Received a webhook event from the bot itself, skipping...') + return + } + // ============ EVENTS ============== if (linearEvent.type === 'issue' && (linearEvent.action === 'create' || linearEvent.action === 'restore')) { await fireIssueCreated({ linearEvent, client, ctx }) @@ -109,3 +115,9 @@ const _isWebhookProperlyAuthenticated = ({ const _getWebhookSigningSecret = ({ ctx }: { ctx: bp.Context }) => ctx.configurationType === 'apiKey' ? ctx.configuration.webhookSigningSecret : bp.secrets.WEBHOOK_SIGNING_SECRET + +const _getLinearBotId = async ({ client, ctx }: { client: bp.Client; ctx: bp.Context }) => { + const linearClient = await getLinearClient({ client, ctx }, ctx.integrationId) + const me = await linearClient.viewer + return me.id +} diff --git a/integrations/pdf-generator/integration.definition.ts b/integrations/pdf-generator/integration.definition.ts index 5bad7b72e49..b908842320b 100644 --- a/integrations/pdf-generator/integration.definition.ts +++ b/integrations/pdf-generator/integration.definition.ts @@ -3,7 +3,7 @@ import { IntegrationDefinition, z } from '@botpress/sdk' export default new IntegrationDefinition({ name: 'pdf-generator', - version: '0.0.2', + version: '0.0.3', readme: 'hub.md', icon: 'icon.svg', description: 'Converts markdown content to PDF using PDFShift', @@ -16,20 +16,21 @@ export default new IntegrationDefinition({ description: 'Converts a markdown content to a PDF file', input: { schema: z.object({ - markdown: z.string().min(1).describe('The markdown content to convert to PDF'), + markdown: z.string().min(1).describe('The markdown content to convert to PDF').title('Markdown'), filename: z .string() .min(1) .endsWith('.pdf') .describe('The filename of the PDF') + .title('Filename') .optional() .default('generated.pdf'), }), }, output: { schema: z.object({ - fileId: z.string().describe('The generated PDF file ID'), - fileUrl: z.string().describe('The public URL to download the PDF'), + fileId: z.string().describe('The generated PDF file ID').title('File ID'), + fileUrl: z.string().describe('The public URL to download the PDF').title('File URL'), }), }, }, @@ -38,20 +39,21 @@ export default new IntegrationDefinition({ description: 'Converts an HTML document to a PDF file', input: { schema: z.object({ - html: z.string().min(1).describe('The HTML content to convert to PDF'), + html: z.string().min(1).describe('The HTML content to convert to PDF').title('HTML'), filename: z .string() .min(1) .endsWith('.pdf') .describe('The filename of the PDF') + .title('Filename') .optional() .default('generated.pdf'), }), }, output: { schema: z.object({ - fileId: z.string().describe('The generated PDF file ID'), - fileUrl: z.string().describe('The public URL to download the PDF'), + fileId: z.string().describe('The generated PDF file ID').title('File ID'), + fileUrl: z.string().describe('The public URL to download the PDF').title('File URL'), }), }, }, diff --git a/integrations/stripe/integration.definition.ts b/integrations/stripe/integration.definition.ts index 3594e9b2b00..b972ef52c49 100644 --- a/integrations/stripe/integration.definition.ts +++ b/integrations/stripe/integration.definition.ts @@ -35,7 +35,7 @@ import { export default new IntegrationDefinition({ name: 'stripe', - version: '0.5.1', + version: '0.5.2', title: 'Stripe', readme: 'hub.md', icon: 'icon.svg', @@ -95,6 +95,7 @@ export default new IntegrationDefinition({ tags: { id: { title: 'Stripe customer ID', + description: 'The unique identifier for a Stripe customer.', }, }, }, @@ -103,13 +104,17 @@ export default new IntegrationDefinition({ stripeIntegrationInfo: { type: 'integration', schema: z.object({ - stripeWebhookId: z.string(), + stripeWebhookId: z + .string() + .title('Stripe Webhook ID') + .describe('The unique identifier for the Stripe webhook.'), }), }, }, actions: { createPaymentLink: { title: 'Create Payment Link', + description: 'Creates a Stripe payment link for a product.', input: { schema: createPaymentLinkInputSchema, }, @@ -119,6 +124,7 @@ export default new IntegrationDefinition({ }, listProductPrices: { title: 'List Product Prices', + description: 'Lists all Stripe product prices.', input: { schema: listProductPricesInputSchema, }, @@ -128,6 +134,7 @@ export default new IntegrationDefinition({ }, createSubsLink: { title: 'Create Subscription Payment Link', + description: 'Creates a Stripe payment link for a subscription product.', input: { schema: createSubsLinkInputSchema, }, @@ -137,6 +144,7 @@ export default new IntegrationDefinition({ }, listPaymentLinks: { title: 'List Payment Links', + description: 'Lists all active Stripe payment links.', input: { schema: listPaymentLinksInputSchema, }, @@ -146,6 +154,7 @@ export default new IntegrationDefinition({ }, findPaymentLink: { title: 'Find Payment Link', + description: 'Finds a Stripe payment link by URL.', input: { schema: findPaymentLinkInputSchema, }, @@ -155,6 +164,7 @@ export default new IntegrationDefinition({ }, deactivatePaymentLink: { title: 'Deactivate Payment Link', + description: 'Deactivates a Stripe payment link by ID.', input: { schema: deactivatePaymentLinkInputSchema, }, @@ -164,6 +174,7 @@ export default new IntegrationDefinition({ }, listCustomers: { title: 'List Customers By Email', + description: 'Lists Stripe customers, optionally filtered by email.', input: { schema: listCustomersInputSchema, }, @@ -173,6 +184,7 @@ export default new IntegrationDefinition({ }, searchCustomers: { title: 'Search Customers By Fields', + description: 'Searches Stripe customers by email, name, or phone.', input: { schema: searchCustomersInputSchema, }, @@ -182,6 +194,7 @@ export default new IntegrationDefinition({ }, createCustomer: { title: 'Create Customer', + description: 'Creates a new Stripe customer.', input: { schema: createCustomerInputSchema, }, @@ -191,6 +204,7 @@ export default new IntegrationDefinition({ }, createOrRetrieveCustomer: { title: 'Create Or Retrieve Customer', + description: 'Creates a new Stripe customer or retrieves an existing one by email.', input: { schema: createOrRetrieveCustomerInputSchema, }, @@ -200,6 +214,7 @@ export default new IntegrationDefinition({ }, retrieveCustomerById: { title: 'Retrieve Customer By ID', + description: 'Retrieves a Stripe customer by their ID.', input: { schema: retrieveCustomerByIdInputSchema, }, diff --git a/integrations/stripe/src/misc/custom-schemas.ts b/integrations/stripe/src/misc/custom-schemas.ts index 4fe671920e6..f05f0a48c3b 100644 --- a/integrations/stripe/src/misc/custom-schemas.ts +++ b/integrations/stripe/src/misc/custom-schemas.ts @@ -2,51 +2,65 @@ import { z } from '@botpress/sdk' const partialCustomer = z .object({ - id: z.string(), - email: z.string().nullable(), - name: z.string().nullable().optional(), - description: z.string().nullable(), - phone: z.string().nullable().optional(), - address: z.object({}).passthrough().nullable().optional(), - created: z.number(), - delinquent: z.boolean().nullable().optional(), + id: z.string().describe('The id of the customer').title('ID'), + email: z.string().nullable().describe('The email of the customer').title('Email'), + name: z.string().nullable().optional().describe('The name of the customer').title('Name'), + description: z.string().nullable().describe('The description of the customer').title('Description'), + phone: z.string().nullable().optional().describe('The phone of the customer').title('Phone'), + address: z.object({}).passthrough().nullable().optional().describe('The addess of the customer').title('Address'), + created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Created'), + delinquent: z.boolean().nullable().optional().describe('If the customer is delinquent').title('Delinquent'), }) .passthrough() export const createPaymentLinkInputSchema = z.object({ - productName: z.string().placeholder('ex: T-Shirt').describe('The name of the product to be sold'), + productName: z + .string() + .placeholder('ex: T-Shirt') + .title('Product Name') + .describe('The name of the product to be sold'), unit_amount: z .number() - .title('Unit Price') .min(50, 'Amount must be at least 50 cents ($0.50 USD)') .optional() .default(50) + .title('Unit Price') .describe('The unit price in cents (minimum 50 cents for USD)'), - currency: z.string().optional().default('usd').describe('The currency in which the price will be expressed'), - quantity: z.number().optional().default(1).describe('The quantity of the product being purchased'), - adjustableQuantity: z.boolean().optional().default(false).describe('Whether or not the quantity can be adjusted'), + currency: z + .string() + .optional() + .default('usd') + .title('Currency') + .describe('The currency in which the price will be expressed'), + quantity: z.number().title('Quantity').optional().default(1).describe('The quantity of the product being purchased'), + adjustableQuantity: z + .boolean() + .optional() + .default(false) + .title('Adjustable Quantity') + .describe('Whether or not the quantity can be adjusted'), adjustableQuantityMaximum: z .number() - .title('Max Quantity') .min(2) .max(999) .optional() .default(99) + .title('Max Quantity') .describe('The maximum quantity the customer can purchase, up to 999'), adjustableQuantityMinimum: z .number() - .title('Min Quantity') .min(1) .max(998) .optional() .default(1) + .title('Min Quantity') .describe('The minimum quantity the customer can purchase'), }) export const createPaymentLinkOutputSchema = z .object({ - id: z.string(), - url: z.string(), + id: z.string().describe('The ID of the created payment link').title('ID'), + url: z.string().describe('The URL of the created payment link').title('URL'), }) .partial() @@ -54,27 +68,54 @@ export const listProductPricesInputSchema = z.object({}) export const listProductPricesOutputSchema = z .object({ - products: z.record( - z.object({ - name: z.string(), - prices: z.array( - z.object({ - unit_amount: z.number().nullable(), - currency: z.string(), - recurring: z.object({}).passthrough().nullable().optional(), - }) - ), - }) - ), + products: z + .record( + z.object({ + name: z.string().describe('The name of the product').title('Name'), + prices: z + .array( + z.object({ + unit_amount: z.number().nullable().describe('The unit amount for the product').title('Unit Amount'), + currency: z.string().describe('The currency for the product').title('Currency'), + recurring: z + .object({}) + .passthrough() + .nullable() + .optional() + .describe('Recurring price details') + .title('Recurring'), + }) + ) + .describe('A list of prices for the product') + .title('Prices'), + }) + ) + .describe('A record of products and their prices') + .title('Products'), }) .partial() export const createSubsLinkInputSchema = z.object({ - productName: z.string().describe('The name of the subscription product'), - unit_amount: z.number().optional().default(0).describe('The unit price in cents'), - currency: z.string().optional().default('usd').describe('The currency in which the price is expressed'), - quantity: z.number().optional().default(1).describe('The quantity of the subscription being purchased'), - adjustableQuantity: z.boolean().optional().default(false).describe('Whether or not the quantity can be adjusted'), + productName: z.string().title('Product Name').describe('The name of the subscription product'), + unit_amount: z.number().title('Unit Amount').optional().default(0).describe('The unit price in cents'), + currency: z + .string() + .title('Currency') + .optional() + .default('usd') + .describe('The currency in which the price is expressed'), + quantity: z + .number() + .title('Quantity') + .optional() + .default(1) + .describe('The quantity of the subscription being purchased'), + adjustableQuantity: z + .boolean() + .title('Adjustable Quantity') + .optional() + .default(false) + .describe('Whether or not the quantity can be adjusted'), adjustableQuantityMaximum: z .number() .title('Max Quantity') @@ -97,14 +138,19 @@ export const createSubsLinkInputSchema = z.object({ .optional() .default('month') .describe('The interval at which the customer will be charged'), - trial_period_days: z.number().min(1).optional().describe('The number of free trial days for the subscription'), - description: z.string().optional().describe('A description of the subscription product'), + trial_period_days: z + .number() + .title('Trial Period Days') + .min(1) + .optional() + .describe('The number of free trial days for the subscription'), + description: z.string().title('Description').optional().describe('A description of the subscription product'), }) export const createSubsLinkOutputSchema = z .object({ - id: z.string(), - url: z.string(), + id: z.string().describe('The ID of the created subscription link').title('ID'), + url: z.string().describe('The URL of the created subscription link').title('URL'), }) .partial() @@ -112,12 +158,15 @@ export const listPaymentLinksInputSchema = z.object({}) export const listPaymentLinksOutputSchema = z .object({ - paymentLinks: z.array( - z.object({ - id: z.string(), - url: z.string(), - }) - ), + paymentLinks: z + .array( + z.object({ + id: z.string().describe('The ID of the payment link').title('ID'), + url: z.string().describe('The URL of the payment link').title('URL'), + }) + ) + .describe('A list of payment links') + .title('Payment Links'), }) .partial() @@ -131,64 +180,81 @@ export const findPaymentLinkInputSchema = z.object({ export const findPaymentLinkOutputSchema = z .object({ - id: z.string(), + id: z.string().describe('The ID of the found payment link').title('ID'), }) .partial() export const deactivatePaymentLinkInputSchema = z.object({ - id: z.string().describe('The payment link ID to deactivate').placeholder('ex: test_aEUdTEdRP95RdvaaEJ'), + id: z + .string() + .title('Payment Link ID') + .describe('The payment link ID to deactivate') + .placeholder('ex: test_aEUdTEdRP95RdvaaEJ'), }) export const deactivatePaymentLinkOutputSchema = z .object({ - id: z.string(), - url: z.string(), - active: z.boolean(), + id: z.string().describe('The ID of the deactivated payment link').title('ID'), + url: z.string().describe('The URL of the deactivated payment link').title('URL'), + active: z.boolean().describe('Whether the payment link is active').title('Active'), }) .partial() export const listCustomersInputSchema = z.object({ - email: z.string().email().max(512).optional().describe('The e-mail of the customer to search for'), + email: z.string().email().max(512).optional().describe('The e-mail of the customer to search for').title('Email'), }) export const listCustomersOutputSchema = z .object({ - customers: z.record(z.array(partialCustomer)), + customers: z.record(z.array(partialCustomer)).describe('A record of customers grouped by email').title('Customers'), }) .partial() export const searchCustomersInputSchema = z.object({ - email: z.string().max(512).optional().describe('Search by query on customer emails'), - name: z.string().optional().describe('Search by query on customer name'), - phone: z.string().optional().describe('Search by query on customer phone number'), + email: z.string().title('Email').max(512).optional().describe('Search by query on customer emails').title('Email'), + name: z.string().title('Name').optional().describe('Search by query on customer name').title('Name'), + phone: z.string().title('Phone').optional().describe('Search by query on customer phone number').title('Phone'), }) export const searchCustomersOutputSchema = z .object({ - customers: z.array(partialCustomer), + customers: z.array(partialCustomer).describe('A list of customers matching the search criteria').title('Customers'), }) .partial() export const createCustomerInputSchema = z.object({ - email: z.string().email().max(512).describe('The email of the customer').placeholder('johndoe@botpress.com'), - name: z.string().optional().describe('The name of the customer').placeholder('John Doe'), - phone: z.string().optional().describe('The phone number of the customer').placeholder('+1234567890'), - description: z.string().optional().describe('A description for the customer').placeholder('Customer Description'), + email: z + .string() + .email() + .max(512) + .title('Email') + .describe('The email of the customer') + .placeholder('johndoe@botpress.com'), + name: z.string().optional().describe('The name of the customer').placeholder('John Doe').title('Name'), + phone: z.string().optional().describe('The phone number of the customer').placeholder('+1234567890').title('Phone'), + description: z + .string() + .optional() + .title('Description') + .describe('A description for the customer') + .placeholder('Customer Description'), paymentMethodId: z .string() .optional() + .title('Payment Method ID') .describe('The ID of the payment method to attach to the customer') .placeholder('payment-method-id'), address: z .string() .optional() + .title('Address') .describe('The address of the customer') .placeholder('123 Bot Street, Bot City, Botland, 12345'), }) export const createCustomerOutputSchema = z .object({ - customer: partialCustomer, + customer: partialCustomer.describe('The created customer object').title('Customer'), }) .partial() @@ -196,98 +262,142 @@ export const createOrRetrieveCustomerInputSchema = createCustomerInputSchema export const createOrRetrieveCustomerOutputSchema = z .object({ - customer: partialCustomer.optional(), - customers: z.array(partialCustomer).optional(), + customer: partialCustomer.optional().describe('The created or retrieved customer object').title('Customer'), + customers: z + .array(partialCustomer) + .optional() + .describe('A list of customers matching the criteria') + .title('Customers'), }) .partial() export const retrieveCustomerByIdInputSchema = z.object({ - id: z.string().describe('The ID of the customer to retrieve').placeholder('cus_1234567890'), + id: z.string().describe('The ID of the customer to retrieve').title('Customer ID').placeholder('cus_1234567890'), }) export const retrieveCustomerByIdOutputSchema = z .object({ - id: z.string(), - email: z.string().nullable(), - description: z.string().nullable(), - created: z.number(), + id: z.string().describe('The ID of the retrieved customer').title('Customer ID'), + email: z.string().describe('The email of the retrieved customer').title('Customer Email').nullable(), + description: z + .string() + .describe('The description of the retrieved customer') + .title('Customer Description') + .nullable(), + created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Customer Created Timestamp'), }) .passthrough() .partial() const baseSchema = z.object({ - origin: z.literal('stripe').describe('The origin of the event trigger'), - userId: z.string().describe('Botpress User ID'), + origin: z.literal('stripe').describe('The origin of the event trigger').title('Origin'), + userId: z.string().describe('Botpress User ID').title('User ID'), }) export const chargeFailedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('charge.failed'), - object: z.object({}).passthrough().describe('The object of the failed charge'), + type: z.string().default('charge.failed').describe('The data type').title('Type'), + object: z.object({}).passthrough().describe('The object of the failed charge').title('Charge Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const subscriptionCreatedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('customer.subscription.created'), - object: z.object({}).passthrough().describe('The object of the created subscription'), + type: z.string().default('customer.subscription.created').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the created subscription') + .title('Subscription Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const subscriptionDeletedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('customer.subscription.deleted'), - object: z.object({}).passthrough().describe('The object of the deleted subscription'), + type: z.string().default('customer.subscription.deleted').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the deleted subscription') + .title('Subscription Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const subscriptionUpdatedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('customer.subscription.updated'), - object: z.object({}).passthrough().describe('The object of the updated subscription'), + type: z.string().default('customer.subscription.updated').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the updated subscription') + .title('Subscription Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const invoicePaymentFailedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('invoice.payment_failed'), - object: z.object({}).passthrough().describe('The object of the invoice whose payment failed'), + type: z.string().default('invoice.payment_failed').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the invoice whose payment failed') + .title('Invoice Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const paymentIntentFailedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('payment_intent.payment_failed'), - object: z.object({}).passthrough().describe('The object of the payment intent that failed'), + type: z.string().default('payment_intent.payment_failed').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the payment intent that failed') + .title('Payment Intent Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const subscriptionScheduleCreatedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('subscription_schedule.created'), - object: z.object({}).passthrough().describe('The object of the created subscription schedule'), + type: z.string().default('subscription_schedule.created').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the created subscription schedule') + .title('Subscription Schedule Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) export const subscriptionScheduleUpdatedSchema = baseSchema.extend({ data: z .object({ - type: z.string().default('subscription_schedule.updated'), - object: z.object({}).passthrough().describe('The object of the updated subscription schedule'), + type: z.string().default('subscription_schedule.updated').describe('The data type').title('Type'), + object: z + .object({}) + .passthrough() + .describe('The object of the updated subscription schedule') + .title('Subscription Schedule Object'), }) - .describe('The data to send with the event'), + .describe('The data to send with the event') + .title('Data'), }) diff --git a/package.json b/package.json index 94f117f8873..ec91fe50400 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@bpinternal/depsynky": "0.2.0", "@bpinternal/readiness": "^0.0.16", "@bpinternal/retry-cli": "^0.1.1", + "@linear/sdk": "^55.0.0", "@stylistic/eslint-plugin": "^5.3.1", "@types/node": "^22.16.4", "@typescript-eslint/eslint-plugin": "^8.42.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 3e4565299d1..f7496eb2e76 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "4.27.1", + "version": "4.27.2", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -27,7 +27,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.4", "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.1", + "@botpress/sdk": "4.20.2", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/src/code-generation/bot-implementation/bot-typings/index.ts b/packages/cli/src/code-generation/bot-implementation/bot-typings/index.ts index 2240cd8fb57..ae7f2b84edd 100644 --- a/packages/cli/src/code-generation/bot-implementation/bot-typings/index.ts +++ b/packages/cli/src/code-generation/bot-implementation/bot-typings/index.ts @@ -2,6 +2,7 @@ import * as sdk from '@botpress/sdk' import * as consts from '../../consts' import { IntegrationTypingsModule } from '../../integration-implementation/integration-typings' import { Module, ReExportTypeModule } from '../../module' +import * as strings from '../../strings' import { ActionsModule } from './actions-module' import { EventsModule } from './events-module' import { StatesModule } from './states-module' @@ -16,7 +17,7 @@ class BotIntegrationsModule extends ReExportTypeModule { for (const [alias, integration] of Object.entries(bot.integrations ?? {})) { const integrationModule = new IntegrationTypingsModule(integration.definition).setCustomTypeName(alias) - integrationModule.unshift(alias) + integrationModule.unshift(strings.dirName(alias)) this.pushDep(integrationModule) } } diff --git a/packages/cli/src/code-generation/module.ts b/packages/cli/src/code-generation/module.ts index 0878ef5c3b4..5d4111ff124 100644 --- a/packages/cli/src/code-generation/module.ts +++ b/packages/cli/src/code-generation/module.ts @@ -49,7 +49,7 @@ export abstract class Module { } public get importAlias(): string { - return this.typeName.split(pathlib.sep).map(strings.importAlias).join('__') + return this.typeName.split(/\\|\//).map(strings.importAlias).join('__') } protected constructor(private _def: ModuleProps) {} diff --git a/packages/cli/src/code-generation/plugin-implementation/plugin-typings/index.ts b/packages/cli/src/code-generation/plugin-implementation/plugin-typings/index.ts index 0b4ee8e0a65..78b3d8604f4 100644 --- a/packages/cli/src/code-generation/plugin-implementation/plugin-typings/index.ts +++ b/packages/cli/src/code-generation/plugin-implementation/plugin-typings/index.ts @@ -4,6 +4,7 @@ import * as consts from '../../consts' import { IntegrationTypingsModule } from '../../integration-implementation/integration-typings' import { InterfaceTypingsModule } from '../../interface-implementation' import { Module, ReExportTypeModule, SingleFileModule } from '../../module' +import * as strings from '../../strings' import { ActionsModule } from './actions-module' import { DefaultConfigurationModule } from './configuration-module' import { EventsModule } from './events-module' @@ -19,7 +20,7 @@ class PluginIntegrationsModule extends ReExportTypeModule { for (const [alias, integration] of Object.entries(plugin.integrations ?? {})) { const integrationModule = new IntegrationTypingsModule(integration.definition) - integrationModule.unshift(alias) + integrationModule.unshift(strings.dirName(alias)) this.pushDep(integrationModule) } } @@ -33,7 +34,7 @@ class PluginInterfacesModule extends ReExportTypeModule { for (const [alias, intrface] of Object.entries(plugin.interfaces ?? {})) { const interfaceModule = new InterfaceTypingsModule(intrface.definition) - interfaceModule.unshift(alias) + interfaceModule.unshift(strings.dirName(alias)) this.pushDep(interfaceModule) } } diff --git a/packages/cli/src/code-generation/strings.ts b/packages/cli/src/code-generation/strings.ts index b297f25fbfc..9f2d207272e 100644 --- a/packages/cli/src/code-generation/strings.ts +++ b/packages/cli/src/code-generation/strings.ts @@ -81,3 +81,4 @@ export const typeName = (name: string) => apply(name, utils.casing.to.pascalCase export const importAlias = (name: string) => apply(name, utils.casing.to.camelCase, escapeTypescriptReserved) export const varName = (name: string) => apply(name, utils.casing.to.camelCase, escapeTypescriptReserved) export const fileName = (name: string) => apply(name, escapeFileNameSpecialChars) +export const dirName = fileName diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index ff047fdad1d..530ed23adf7 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.1" + "@botpress/sdk": "4.20.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index 9264fa5fd9f..b51fa09fa11 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.1" + "@botpress/sdk": "4.20.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index 71919fba8ca..837f82d82fc 100644 --- a/packages/cli/templates/empty-plugin/package.json +++ b/packages/cli/templates/empty-plugin/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/sdk": "4.20.1" + "@botpress/sdk": "4.20.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 7222d15c25d..67ae35691e7 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.1" + "@botpress/sdk": "4.20.2" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index 242a0d6efc0..403362aa1f3 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.27.2", - "@botpress/sdk": "4.20.1", + "@botpress/sdk": "4.20.2", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/llmz/package.json b/packages/llmz/package.json index 0062e2feaf4..f6a6d9d25a3 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.32", + "version": "0.0.33", "types": "./dist/index.d.ts", "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/packages/llmz/src/llmz.ts b/packages/llmz/src/llmz.ts index b7506bb5387..ba1c5f14db3 100644 --- a/packages/llmz/src/llmz.ts +++ b/packages/llmz/src/llmz.ts @@ -513,7 +513,7 @@ const executeIteration = async ({ internalValues[name] = value const initialValue = value - const schema = type ?? z.any() + const schema = (type ?? z.any()) as z.ZodType Object.defineProperty(instance, name, { enumerable: true, diff --git a/packages/llmz/src/objects.ts b/packages/llmz/src/objects.ts index 6f76a074305..aece181bff4 100644 --- a/packages/llmz/src/objects.ts +++ b/packages/llmz/src/objects.ts @@ -3,7 +3,7 @@ import { z } from '@bpinternal/zui' import { formatTypings } from './formatting.js' import { hoistTypings } from './hoist.js' import { Tool } from './tool.js' -import { Serializable } from './types.js' +import { Serializable, ZuiType } from './types.js' import { getTypings } from './typings.js' import { escapeString, getMultilineComment, isValidIdentifier } from './utils.js' @@ -34,7 +34,7 @@ export type ObjectProperty = { /** The current value of the property */ value: any /** Optional Zod schema for validation when the property is modified */ - type?: z.Schema + type?: ZuiType /** Optional human-readable description of the property */ description?: string /** Whether the LLM can modify this property (default: false) */ @@ -486,7 +486,7 @@ function getObjectTypings(obj: ObjectInstance) { let type = 'unknown' if (prop.type) { - type = await getTypings(prop.type, {}) + type = await getTypings(prop.type as z.ZodType, {}) } else if (prop.value !== undefined) { type = typeof prop.value } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d98baee2186..7cb84748d17 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "4.20.1", + "version": "4.20.2", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/bot/definition.ts b/packages/sdk/src/bot/definition.ts index d96809a6d4e..4c95c775c7a 100644 --- a/packages/sdk/src/bot/definition.ts +++ b/packages/sdk/src/bot/definition.ts @@ -217,7 +217,7 @@ export class BotDefinition< self.integrations = {} } - const integrationAlias = config?.alias ?? integrationPkg.name.replace('/', '-') + const integrationAlias = config?.alias ?? integrationPkg.name if (self.integrations[integrationAlias]) { throw new Error(`Another integration with alias "${integrationAlias}" is already installed in the bot`) @@ -240,7 +240,7 @@ export class BotDefinition< self.plugins = {} } - const pluginAlias = config.alias ?? pluginPkg.name.replace('/', '-') + const pluginAlias = config.alias ?? pluginPkg.name if (self.plugins[pluginAlias]) { throw new Error(`Another plugin with alias "${pluginAlias}" is already installed in the bot`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad491670854..d62f0114578 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: '@bpinternal/retry-cli': specifier: ^0.1.1 version: 0.1.1 + '@linear/sdk': + specifier: ^55.0.0 + version: 55.2.1 '@stylistic/eslint-plugin': specifier: ^5.3.1 version: 5.3.1(eslint@9.34.0(jiti@2.4.2)) @@ -2306,7 +2309,7 @@ importers: specifier: 1.27.2 version: link:../client '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2424,7 +2427,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2440,7 +2443,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2453,7 +2456,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2469,7 +2472,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../../../sdk devDependencies: '@types/node': @@ -2485,7 +2488,7 @@ importers: specifier: 1.27.2 version: link:../../../client '@botpress/sdk': - specifier: 4.20.1 + specifier: 4.20.2 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -4705,6 +4708,10 @@ packages: resolution: {integrity: sha512-cqOTIkayqCDTBITCieuZ3acMiJmWZB0bR9FhfrfMs2B5PotntDgcfNCBW+21LcMwcBrq0A84kwK2AQzdu+Atsg==} engines: {node: '>=12.x', yarn: 1.x} + '@linear/sdk@55.2.1': + resolution: {integrity: sha512-0+xjyphwdMMeGIV5O1Q7/AhS/tyTv+J0azE/WPbVXJHCDkBdgoHERvBgNXM0nRjcVt2RKUl8V0o+Fly2CJRlOA==} + engines: {node: '>=12.x', yarn: 1.x} + '@mailchimp/mailchimp_marketing@3.0.80': resolution: {integrity: sha512-Cgz0xPb+1DUjmrl5whAsmqfAChBko+Wf4/PLQE4RvwfPlcq2agfHr1QFiXEhZ8e+GQwQ3hZQn9iLGXwIXwxUCg==} engines: {node: '>=10.0.0'} @@ -13627,6 +13634,14 @@ snapshots: transitivePeerDependencies: - encoding + '@linear/sdk@55.2.1': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) + graphql: 15.8.0 + isomorphic-unfetch: 3.1.0 + transitivePeerDependencies: + - encoding + '@mailchimp/mailchimp_marketing@3.0.80': dependencies: dotenv: 8.6.0