diff --git a/apps/server/src/api/v2/slack/handler.ts b/apps/server/src/api/v2/slack/handler.ts index dae50c5a5..584cdb16a 100644 --- a/apps/server/src/api/v2/slack/handler.ts +++ b/apps/server/src/api/v2/slack/handler.ts @@ -1,9 +1,14 @@ import { getUserOrganizationId } from '@buster/database'; import { + type GetChannelsResponse, + type GetIntegrationResponse, + type InitiateOAuthResponse, InitiateOAuthSchema, OAuthCallbackSchema, + type RemoveIntegrationResponse, SlackError, - SlackErrorCodes, + type SlackErrorResponse, + type UpdateIntegrationResponse, UpdateIntegrationSchema, } from '@buster/server-shared/slack'; import { SlackChannelService } from '@buster/slack'; @@ -34,14 +39,14 @@ export class SlackHandler { * POST /api/v2/slack/auth/init * Initiate OAuth flow */ - async initiateOAuth(c: Context) { + async initiateOAuth(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); // Check if service is available if (!slackOAuthService) { - return c.json( + return c.json( { error: 'Slack integration is not configured', code: 'INTEGRATION_NOT_CONFIGURED', @@ -52,7 +57,7 @@ export class SlackHandler { // Check if integration is enabled if (!slackOAuthService.isEnabled()) { - return c.json( + return c.json( { error: 'Slack integration is not enabled', code: 'INTEGRATION_DISABLED', @@ -75,10 +80,10 @@ export class SlackHandler { } // Parse request body - const body = await c.req.json().catch(() => ({})); + const body: unknown = await c.req.json().catch(() => ({})); const parsed = InitiateOAuthSchema.safeParse(body); - const metadata = parsed.success ? parsed.data.metadata : undefined; + const metadata = parsed.success ? parsed.data?.metadata : undefined; // Add IP address to metadata const enrichedMetadata = { @@ -93,7 +98,10 @@ export class SlackHandler { metadata: enrichedMetadata, }); - return c.json(result); + return c.json({ + auth_url: result.authUrl, + state: result.state, + }); } catch (error) { console.error('Failed to initiate OAuth:', error); @@ -121,7 +129,7 @@ export class SlackHandler { * GET /api/v2/slack/auth/callback * Handle OAuth callback from Slack */ - async handleOAuthCallback(c: Context) { + async handleOAuthCallback(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); @@ -132,7 +140,7 @@ export class SlackHandler { } // Parse query parameters - const query = c.req.query(); + const query = c.req.query() as Record; console.info('OAuth callback received', { hasCode: !!query.code, hasState: !!query.state, @@ -149,7 +157,7 @@ export class SlackHandler { } console.error('Invalid OAuth callback parameters:', { - errors: parsed.error.errors, + errors: parsed.error.issues, providedKeys: Object.keys(query), expectedKeys: ['code', 'state'], }); @@ -198,14 +206,14 @@ export class SlackHandler { * GET /api/v2/slack/integration * Get current integration status */ - async getIntegration(c: Context) { + async getIntegration(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); // Check if service is available if (!slackOAuthService) { - return c.json( + return c.json( { error: 'Slack integration is not configured', code: 'INTEGRATION_NOT_CONFIGURED', @@ -228,7 +236,19 @@ export class SlackHandler { const status = await slackOAuthService.getIntegrationStatus(organizationGrant.organizationId); - return c.json(status); + return c.json({ + connected: status.connected, + integration: status.integration + ? { + id: status.integration.id, + team_name: status.integration.teamName, + installed_at: status.integration.installedAt, + team_domain: status.integration.teamDomain, + last_used_at: status.integration.lastUsedAt, + default_channel: status.integration.defaultChannel, + } + : undefined, + }); } catch (error) { console.error('Failed to get integration status:', error); @@ -248,14 +268,14 @@ export class SlackHandler { * DELETE /api/v2/slack/integration * Remove Slack integration */ - async removeIntegration(c: Context) { + async removeIntegration(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); // Check if service is available if (!slackOAuthService) { - return c.json( + return c.json( { error: 'Slack integration is not configured', code: 'INTEGRATION_NOT_CONFIGURED', @@ -289,7 +309,7 @@ export class SlackHandler { ); } - return c.json({ + return c.json({ message: 'Slack integration removed successfully', }); } catch (error) { @@ -311,14 +331,14 @@ export class SlackHandler { * PUT /api/v2/slack/integration * Update Slack integration settings */ - async updateIntegration(c: Context) { + async updateIntegration(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); // Check if service is available if (!slackOAuthService) { - return c.json( + return c.json( { error: 'Slack integration is not configured', code: 'INTEGRATION_NOT_CONFIGURED', @@ -340,12 +360,12 @@ export class SlackHandler { } // Parse request body - const body = await c.req.json().catch(() => ({})); + const body: unknown = await c.req.json().catch(() => ({})); const parsed = UpdateIntegrationSchema.safeParse(body); if (!parsed.success) { throw new SlackError( - `Invalid request body: ${parsed.error.errors.map((e) => e.message).join(', ')}`, + `Invalid request body: ${parsed.error.issues.map((e) => e.message).join(', ')}`, 400, 'INVALID_REQUEST_BODY' ); @@ -363,7 +383,7 @@ export class SlackHandler { await updateDefaultChannel(integration.id, parsed.data.default_channel); } - return c.json({ + return c.json({ message: 'Integration updated successfully', ...parsed.data, }); @@ -386,14 +406,14 @@ export class SlackHandler { * GET /api/v2/slack/channels * Get public channels for the current integration */ - async getChannels(c: Context) { + async getChannels(c: Context): Promise { try { // Get service instance (lazy initialization) const slackOAuthService = this.getSlackOAuthService(); // Check if service is available if (!slackOAuthService) { - return c.json( + return c.json( { error: 'Slack integration is not configured', code: 'INTEGRATION_NOT_CONFIGURED', @@ -418,7 +438,7 @@ export class SlackHandler { const integration = await slackHelpers.getActiveIntegration(organizationGrant.organizationId); if (!integration) { - return c.json( + return c.json( { error: 'No active Slack integration found', code: 'INTEGRATION_NOT_FOUND', @@ -446,7 +466,7 @@ export class SlackHandler { await slackHelpers.updateLastUsedAt(integration.id); // Return only id and name as requested - return c.json({ + return c.json({ channels: channels.map((channel) => ({ id: channel.id, name: channel.name, diff --git a/apps/server/src/api/v2/slack/services/slack-oauth-service.ts b/apps/server/src/api/v2/slack/services/slack-oauth-service.ts index 4baa1731e..3983fe8a6 100644 --- a/apps/server/src/api/v2/slack/services/slack-oauth-service.ts +++ b/apps/server/src/api/v2/slack/services/slack-oauth-service.ts @@ -40,7 +40,13 @@ export class SlackOAuthService { clientId: this.env.SLACK_CLIENT_ID, clientSecret: this.env.SLACK_CLIENT_SECRET, redirectUri: this.env.SLACK_REDIRECT_URI, - scopes: ['channels:history', 'chat:write', 'chat:write.public', 'commands'], + scopes: [ + 'channels:history', + 'channels:read', + 'chat:write', + 'chat:write.public', + 'commands', + ], }, tokenStorage, oauthStateStorage diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts index a78d06dc8..e84de4843 100644 --- a/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts +++ b/apps/trigger/src/tasks/message-post-processing/helpers/data-transformers.test.ts @@ -320,7 +320,13 @@ describe('data-transformers', () => { }); it('should handle empty conversation history', () => { - const result = buildWorkflowInput(baseMessageContext, [], basePreviousResults, baseDatasets, false); + const result = buildWorkflowInput( + baseMessageContext, + [], + basePreviousResults, + baseDatasets, + false + ); expect(result.conversationHistory).toBeUndefined(); }); }); diff --git a/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts b/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts index a2387e2b7..bf2daca4d 100644 --- a/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts +++ b/apps/trigger/src/tasks/message-post-processing/helpers/slack-notifier.ts @@ -127,7 +127,7 @@ export async function getExistingSlackMessageForChat(chatId: string): Promise<{ * Send a Slack notification based on post-processing results */ export async function sendSlackNotification( - params: SlackNotificationParams, + params: SlackNotificationParams ): Promise { try { // Step 1: Check if organization has active Slack integration @@ -139,8 +139,8 @@ export async function sendSlackNotification( and( eq(slackIntegrations.organizationId, params.organizationId), eq(slackIntegrations.status, 'active'), - isNull(slackIntegrations.deletedAt), - ), + isNull(slackIntegrations.deletedAt) + ) ) .limit(1); @@ -189,7 +189,7 @@ export async function sendSlackNotification( const result = await sendSlackMessage( tokenSecret.secret, integration.defaultChannel.id, - slackMessage, + slackMessage ); if (result.success) { @@ -229,7 +229,7 @@ export async function sendSlackNotification( * Send a Slack reply notification to an existing thread */ export async function sendSlackReplyNotification( - params: SlackReplyNotificationParams, + params: SlackReplyNotificationParams ): Promise { try { // Step 1: Check if we should send a notification @@ -257,7 +257,7 @@ export async function sendSlackReplyNotification( tokenSecret.secret, params.channelId, slackMessage, - params.threadTs, + params.threadTs ); if (result.success) { @@ -372,7 +372,7 @@ function formatSlackReplyMessage(params: SlackReplyNotificationParams): SlackMes throw new Error( 'Invalid reply notification parameters: Missing required fields. ' + - 'Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message', + 'Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message' ); } @@ -456,7 +456,7 @@ function formatSlackMessage(params: SlackNotificationParams): SlackMessage { throw new Error( `Invalid notification parameters: Missing required fields. Requires either formattedMessage, summaryTitle with summaryMessage, or toolCalled="flagChat" with message. Received: formattedMessage=${!!params.formattedMessage}, summaryTitle=${!!params.summaryTitle}, summaryMessage=${!!params.summaryMessage}, toolCalled="${ params.toolCalled - }", message=${!!params.message}`, + }", message=${!!params.message}` ); } @@ -467,7 +467,7 @@ async function sendSlackMessage( accessToken: string, channelId: string, message: SlackMessage, - threadTs?: string, + threadTs?: string ): Promise<{ success: boolean; messageTs?: string; error?: string }> { try { const response = await fetch('https://slack.com/api/chat.postMessage', { @@ -540,8 +540,8 @@ export async function trackSlackNotification(params: { content: params.slackBlocks ? JSON.stringify({ blocks: params.slackBlocks }) : params.summaryTitle && params.summaryMessage - ? `${params.summaryTitle}\n\n${params.summaryMessage}` - : 'Notification sent', + ? `${params.summaryTitle}\n\n${params.summaryMessage}` + : 'Notification sent', senderInfo: { sentBy: 'buster-post-processing', userName: params.userName, diff --git a/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts b/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts index 497b16f89..32b2fd247 100644 --- a/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts +++ b/apps/trigger/src/tasks/message-post-processing/message-post-processing.test.ts @@ -24,14 +24,16 @@ vi.mock('@buster/database', () => ({ getDb: vi.fn(), eq: vi.fn((a, b) => ({ type: 'eq', a, b })), messages: { id: 'messages.id' }, - getBraintrustMetadata: vi.fn(() => Promise.resolve({ - userName: 'John Doe', - userId: 'user-123', - organizationName: 'Test Org', - organizationId: 'org-123', - messageId: 'msg-12345', - chatId: 'chat-123', - })), + getBraintrustMetadata: vi.fn(() => + Promise.resolve({ + userName: 'John Doe', + userId: 'user-123', + organizationName: 'Test Org', + organizationId: 'org-123', + messageId: 'msg-12345', + chatId: 'chat-123', + }) + ), })); vi.mock('@buster/ai/workflows/post-processing-workflow', () => ({ diff --git a/packages/ai/src/steps/post-processing/combine-parallel-results-step.ts b/packages/ai/src/steps/post-processing/combine-parallel-results-step.ts index d66f95687..d47222b42 100644 --- a/packages/ai/src/steps/post-processing/combine-parallel-results-step.ts +++ b/packages/ai/src/steps/post-processing/combine-parallel-results-step.ts @@ -19,7 +19,9 @@ export const combineParallelResultsOutputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of previous messages for context'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), diff --git a/packages/ai/src/steps/post-processing/flag-chat-step.ts b/packages/ai/src/steps/post-processing/flag-chat-step.ts index 4af76b893..fdcd927ba 100644 --- a/packages/ai/src/steps/post-processing/flag-chat-step.ts +++ b/packages/ai/src/steps/post-processing/flag-chat-step.ts @@ -15,7 +15,9 @@ const inputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of previous messages for context'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), }); @@ -28,7 +30,9 @@ export const flagChatOutputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of previous messages for context'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), diff --git a/packages/ai/src/steps/post-processing/identify-assumptions-step.ts b/packages/ai/src/steps/post-processing/identify-assumptions-step.ts index a9b7ff15f..7d2ec8f70 100644 --- a/packages/ai/src/steps/post-processing/identify-assumptions-step.ts +++ b/packages/ai/src/steps/post-processing/identify-assumptions-step.ts @@ -18,7 +18,9 @@ const inputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of previous messages for context'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), }); @@ -31,7 +33,9 @@ export const identifyAssumptionsOutputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of previous messages for context'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), diff --git a/packages/ai/src/steps/post-processing/schemas.ts b/packages/ai/src/steps/post-processing/schemas.ts index 143bf907f..c5d341fc7 100644 --- a/packages/ai/src/steps/post-processing/schemas.ts +++ b/packages/ai/src/steps/post-processing/schemas.ts @@ -9,7 +9,9 @@ export const postProcessingWorkflowInputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of the previous post-processing messages'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), }); @@ -23,7 +25,9 @@ export const postProcessingWorkflowOutputSchema = z.object({ userId: z.string().describe('User ID for the current operation'), chatId: z.string().describe('Chat ID for the current operation'), isFollowUp: z.boolean().describe('Whether this is a follow-up message'), - isSlackFollowUp: z.boolean().describe('Whether this is a follow-up message for an existing Slack thread'), + isSlackFollowUp: z + .boolean() + .describe('Whether this is a follow-up message for an existing Slack thread'), previousMessages: z.array(z.string()).describe('Array of the previous post-processing messages'), datasets: z.string().describe('Assembled YAML content of all available datasets for context'), diff --git a/packages/ai/src/tools/visualization-tools/bar-line-axis-validator.ts b/packages/ai/src/tools/visualization-tools/bar-line-axis-validator.ts index 8bf318e1e..2adbc7bdd 100644 --- a/packages/ai/src/tools/visualization-tools/bar-line-axis-validator.ts +++ b/packages/ai/src/tools/visualization-tools/bar-line-axis-validator.ts @@ -30,7 +30,8 @@ export function validateAndAdjustBarLineAxes(metricYml: MetricYml): AxisValidati return { isValid: false, shouldSwapAxes: false, - error: 'Bar and line charts require at least one column for each axis. Please specify both X and Y axis columns.', + error: + 'Bar and line charts require at least one column for each axis. Please specify both X and Y axis columns.', }; } const xColumns = barAndLineAxis.x;