@inkeep/agents-work-apps 0.47.5 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/env.d.ts +24 -2
  2. package/dist/env.js +13 -2
  3. package/dist/github/index.d.ts +3 -3
  4. package/dist/github/mcp/auth.d.ts +2 -2
  5. package/dist/github/mcp/index.d.ts +2 -2
  6. package/dist/github/mcp/index.js +23 -34
  7. package/dist/github/mcp/schemas.d.ts +1 -1
  8. package/dist/github/routes/setup.d.ts +2 -2
  9. package/dist/github/routes/tokenExchange.d.ts +2 -2
  10. package/dist/github/routes/webhooks.d.ts +2 -2
  11. package/dist/slack/i18n/index.d.ts +2 -0
  12. package/dist/slack/i18n/index.js +3 -0
  13. package/dist/slack/i18n/strings.d.ts +73 -0
  14. package/dist/slack/i18n/strings.js +67 -0
  15. package/dist/slack/index.d.ts +18 -0
  16. package/dist/slack/index.js +28 -0
  17. package/dist/slack/middleware/permissions.d.ts +31 -0
  18. package/dist/slack/middleware/permissions.js +167 -0
  19. package/dist/slack/routes/events.d.ts +10 -0
  20. package/dist/slack/routes/events.js +551 -0
  21. package/dist/slack/routes/index.d.ts +10 -0
  22. package/dist/slack/routes/index.js +47 -0
  23. package/dist/slack/routes/oauth.d.ts +20 -0
  24. package/dist/slack/routes/oauth.js +344 -0
  25. package/dist/slack/routes/users.d.ts +10 -0
  26. package/dist/slack/routes/users.js +365 -0
  27. package/dist/slack/routes/workspaces.d.ts +10 -0
  28. package/dist/slack/routes/workspaces.js +909 -0
  29. package/dist/slack/services/agent-resolution.d.ts +41 -0
  30. package/dist/slack/services/agent-resolution.js +99 -0
  31. package/dist/slack/services/blocks/index.d.ts +73 -0
  32. package/dist/slack/services/blocks/index.js +103 -0
  33. package/dist/slack/services/client.d.ts +108 -0
  34. package/dist/slack/services/client.js +232 -0
  35. package/dist/slack/services/commands/index.d.ts +19 -0
  36. package/dist/slack/services/commands/index.js +553 -0
  37. package/dist/slack/services/events/app-mention.d.ts +40 -0
  38. package/dist/slack/services/events/app-mention.js +297 -0
  39. package/dist/slack/services/events/block-actions.d.ts +40 -0
  40. package/dist/slack/services/events/block-actions.js +265 -0
  41. package/dist/slack/services/events/index.d.ts +6 -0
  42. package/dist/slack/services/events/index.js +7 -0
  43. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  44. package/dist/slack/services/events/modal-submission.js +400 -0
  45. package/dist/slack/services/events/streaming.d.ts +26 -0
  46. package/dist/slack/services/events/streaming.js +255 -0
  47. package/dist/slack/services/events/utils.d.ts +146 -0
  48. package/dist/slack/services/events/utils.js +370 -0
  49. package/dist/slack/services/index.d.ts +16 -0
  50. package/dist/slack/services/index.js +16 -0
  51. package/dist/slack/services/modals.d.ts +86 -0
  52. package/dist/slack/services/modals.js +355 -0
  53. package/dist/slack/services/nango.d.ts +85 -0
  54. package/dist/slack/services/nango.js +476 -0
  55. package/dist/slack/services/security.d.ts +35 -0
  56. package/dist/slack/services/security.js +65 -0
  57. package/dist/slack/services/types.d.ts +26 -0
  58. package/dist/slack/services/types.js +1 -0
  59. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  60. package/dist/slack/services/workspace-tokens.js +27 -0
  61. package/dist/slack/tracer.d.ts +40 -0
  62. package/dist/slack/tracer.js +39 -0
  63. package/dist/slack/types.d.ts +10 -0
  64. package/dist/slack/types.js +1 -0
  65. package/package.json +11 -2
@@ -0,0 +1,41 @@
1
+ //#region src/slack/services/agent-resolution.d.ts
2
+ /**
3
+ * Slack Agent Resolution Service
4
+ *
5
+ * Determines which agent to use for a given Slack interaction.
6
+ * Priority: Channel default > Workspace default (all admin-controlled)
7
+ */
8
+ /** Configuration for a resolved agent */
9
+ interface ResolvedAgentConfig {
10
+ projectId: string;
11
+ agentId: string;
12
+ agentName?: string;
13
+ source: 'channel' | 'workspace' | 'none';
14
+ }
15
+ interface AgentResolutionParams {
16
+ tenantId: string;
17
+ teamId: string;
18
+ channelId?: string;
19
+ userId?: string;
20
+ }
21
+ /**
22
+ * Resolve the effective agent configuration.
23
+ * Priority: Channel default > Workspace default
24
+ *
25
+ * @param params - Resolution parameters including tenant, team, and channel IDs
26
+ * @returns The resolved agent configuration, or null if no agent is configured
27
+ */
28
+ declare function resolveEffectiveAgent(params: AgentResolutionParams): Promise<ResolvedAgentConfig | null>;
29
+ /**
30
+ * Get all agent configuration sources for display purposes.
31
+ *
32
+ * @param params - Resolution parameters
33
+ * @returns Object containing channel, workspace configs, and the effective choice
34
+ */
35
+ declare function getAgentConfigSources(params: AgentResolutionParams): Promise<{
36
+ channelConfig: ResolvedAgentConfig | null;
37
+ workspaceConfig: ResolvedAgentConfig | null;
38
+ effective: ResolvedAgentConfig | null;
39
+ }>;
40
+ //#endregion
41
+ export { AgentResolutionParams, ResolvedAgentConfig, getAgentConfigSources, resolveEffectiveAgent };
@@ -0,0 +1,99 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import runDbClient_default from "../../db/runDbClient.js";
3
+ import { getWorkspaceDefaultAgentFromNango } from "./nango.js";
4
+ import { findWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
5
+
6
+ //#region src/slack/services/agent-resolution.ts
7
+ /**
8
+ * Slack Agent Resolution Service
9
+ *
10
+ * Determines which agent to use for a given Slack interaction.
11
+ * Priority: Channel default > Workspace default (all admin-controlled)
12
+ */
13
+ const logger = getLogger("slack-agent-resolution");
14
+ /**
15
+ * Resolve the effective agent configuration.
16
+ * Priority: Channel default > Workspace default
17
+ *
18
+ * @param params - Resolution parameters including tenant, team, and channel IDs
19
+ * @returns The resolved agent configuration, or null if no agent is configured
20
+ */
21
+ async function resolveEffectiveAgent(params) {
22
+ const { tenantId, teamId, channelId } = params;
23
+ logger.debug({
24
+ tenantId,
25
+ teamId,
26
+ channelId
27
+ }, "Resolving effective agent");
28
+ if (channelId) {
29
+ const channelConfig = await findWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
30
+ if (channelConfig?.enabled) {
31
+ logger.info({
32
+ channelId,
33
+ agentId: channelConfig.agentId,
34
+ source: "channel"
35
+ }, "Resolved agent from channel config");
36
+ return {
37
+ projectId: channelConfig.projectId,
38
+ agentId: channelConfig.agentId,
39
+ agentName: channelConfig.agentName || void 0,
40
+ source: "channel"
41
+ };
42
+ }
43
+ }
44
+ const workspaceConfig = await getWorkspaceDefaultAgentFromNango(teamId);
45
+ if (workspaceConfig?.agentId && workspaceConfig.projectId) {
46
+ logger.info({
47
+ teamId,
48
+ agentId: workspaceConfig.agentId,
49
+ source: "workspace"
50
+ }, "Resolved agent from workspace config");
51
+ return {
52
+ projectId: workspaceConfig.projectId,
53
+ agentId: workspaceConfig.agentId,
54
+ agentName: workspaceConfig.agentName,
55
+ source: "workspace"
56
+ };
57
+ }
58
+ logger.debug({
59
+ tenantId,
60
+ teamId,
61
+ channelId
62
+ }, "No agent configuration found");
63
+ return null;
64
+ }
65
+ /**
66
+ * Get all agent configuration sources for display purposes.
67
+ *
68
+ * @param params - Resolution parameters
69
+ * @returns Object containing channel, workspace configs, and the effective choice
70
+ */
71
+ async function getAgentConfigSources(params) {
72
+ const { tenantId, teamId, channelId } = params;
73
+ let channelConfig = null;
74
+ let workspaceConfig = null;
75
+ if (channelId) {
76
+ const config = await findWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
77
+ if (config?.enabled) channelConfig = {
78
+ projectId: config.projectId,
79
+ agentId: config.agentId,
80
+ agentName: config.agentName || void 0,
81
+ source: "channel"
82
+ };
83
+ }
84
+ const wsConfig = await getWorkspaceDefaultAgentFromNango(teamId);
85
+ if (wsConfig?.agentId && wsConfig.projectId) workspaceConfig = {
86
+ projectId: wsConfig.projectId,
87
+ agentId: wsConfig.agentId,
88
+ agentName: wsConfig.agentName,
89
+ source: "workspace"
90
+ };
91
+ return {
92
+ channelConfig,
93
+ workspaceConfig,
94
+ effective: channelConfig || workspaceConfig
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { getAgentConfigSources, resolveEffectiveAgent };
@@ -0,0 +1,73 @@
1
+ import * as slack_block_builder0 from "slack-block-builder";
2
+
3
+ //#region src/slack/services/blocks/index.d.ts
4
+ declare function createErrorMessage(message: string): Readonly<slack_block_builder0.SlackMessageDto>;
5
+ interface ContextBlockParams {
6
+ agentName: string;
7
+ isPrivate?: boolean;
8
+ }
9
+ declare function createContextBlock(params: ContextBlockParams): {
10
+ type: "context";
11
+ elements: {
12
+ type: "mrkdwn";
13
+ text: string;
14
+ }[];
15
+ };
16
+ interface FollowUpButtonParams {
17
+ conversationId: string;
18
+ agentId: string;
19
+ projectId: string;
20
+ tenantId: string;
21
+ teamId: string;
22
+ slackUserId: string;
23
+ channel: string;
24
+ }
25
+ declare function buildFollowUpButton(params: FollowUpButtonParams): {
26
+ type: "button";
27
+ text: {
28
+ type: "plain_text";
29
+ text: "Follow Up";
30
+ emoji: boolean;
31
+ };
32
+ action_id: string;
33
+ value: string;
34
+ }[];
35
+ /**
36
+ * Build Block Kit blocks for a private conversational response.
37
+ * Shows the user's message, a divider, the agent response, context, and a Follow Up button.
38
+ */
39
+ declare function buildConversationResponseBlocks(params: {
40
+ userMessage: string;
41
+ responseText: string;
42
+ agentName: string;
43
+ isError: boolean;
44
+ followUpParams: FollowUpButtonParams;
45
+ }): any[];
46
+ declare function createAgentListMessage(agents: Array<{
47
+ id: string;
48
+ name: string | null;
49
+ projectName: string | null;
50
+ }>, dashboardUrl: string): Readonly<slack_block_builder0.SlackMessageDto>;
51
+ declare function createUpdatedHelpMessage(): Readonly<slack_block_builder0.SlackMessageDto>;
52
+ declare function createAlreadyLinkedMessage(email: string, linkedAt: string, dashboardUrl: string): Readonly<slack_block_builder0.SlackMessageDto>;
53
+ declare function createUnlinkSuccessMessage(): Readonly<slack_block_builder0.SlackMessageDto>;
54
+ declare function createNotLinkedMessage(): Readonly<slack_block_builder0.SlackMessageDto>;
55
+ interface AgentConfigSources {
56
+ channelConfig: {
57
+ agentName?: string;
58
+ agentId: string;
59
+ } | null;
60
+ workspaceConfig: {
61
+ agentName?: string;
62
+ agentId: string;
63
+ } | null;
64
+ effective: {
65
+ agentName?: string;
66
+ agentId: string;
67
+ source: string;
68
+ } | null;
69
+ }
70
+ declare function createStatusMessage(email: string, linkedAt: string, dashboardUrl: string, agentConfigs: AgentConfigSources): Readonly<slack_block_builder0.SlackMessageDto>;
71
+ declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
72
+ //#endregion
73
+ export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -0,0 +1,103 @@
1
+ import { SlackStrings } from "../../i18n/strings.js";
2
+ import { Blocks, Elements, Md, Message } from "slack-block-builder";
3
+
4
+ //#region src/slack/services/blocks/index.ts
5
+ function createErrorMessage(message) {
6
+ return Message().blocks(Blocks.Section().text(`❌ ${message}`)).buildToObject();
7
+ }
8
+ function createContextBlock(params) {
9
+ const { agentName, isPrivate = false } = params;
10
+ let text = SlackStrings.context.poweredBy(agentName);
11
+ if (isPrivate) text = `${SlackStrings.context.privateResponse} • ${text}`;
12
+ return {
13
+ type: "context",
14
+ elements: [{
15
+ type: "mrkdwn",
16
+ text
17
+ }]
18
+ };
19
+ }
20
+ function buildFollowUpButton(params) {
21
+ return [{
22
+ type: "button",
23
+ text: {
24
+ type: "plain_text",
25
+ text: SlackStrings.buttons.followUp,
26
+ emoji: true
27
+ },
28
+ action_id: "open_follow_up_modal",
29
+ value: JSON.stringify(params)
30
+ }];
31
+ }
32
+ /**
33
+ * Build Block Kit blocks for a private conversational response.
34
+ * Shows the user's message, a divider, the agent response, context, and a Follow Up button.
35
+ */
36
+ function buildConversationResponseBlocks(params) {
37
+ const { userMessage, responseText, agentName, isError, followUpParams } = params;
38
+ const blocks = [
39
+ {
40
+ type: "context",
41
+ elements: [{
42
+ type: "mrkdwn",
43
+ text: `💬 *You:* ${userMessage.length > 200 ? `${userMessage.slice(0, 200)}...` : userMessage}`
44
+ }]
45
+ },
46
+ { type: "divider" },
47
+ {
48
+ type: "section",
49
+ text: {
50
+ type: "mrkdwn",
51
+ text: responseText
52
+ }
53
+ }
54
+ ];
55
+ if (!isError) {
56
+ const contextBlock = createContextBlock({
57
+ agentName,
58
+ isPrivate: true
59
+ });
60
+ blocks.push(contextBlock);
61
+ blocks.push({
62
+ type: "actions",
63
+ elements: buildFollowUpButton(followUpParams)
64
+ });
65
+ }
66
+ return blocks;
67
+ }
68
+ function createAgentListMessage(agents, dashboardUrl) {
69
+ const agentList = agents.slice(0, 15).map((a) => `• ${Md.bold(a.name || a.id)} ${a.projectName ? `(${Md.italic(a.projectName)})` : ""}`).join("\n");
70
+ const moreText = agents.length > 15 ? `\n\n${SlackStrings.agentList.andMore(agents.length - 15)}` : "";
71
+ return Message().blocks(Blocks.Section().text(`${Md.bold(SlackStrings.agentList.title)}\n\n` + agentList + moreText + `
72
+
73
+ ${Md.bold(SlackStrings.agentList.usage)}\n• ${SlackStrings.agentList.runUsage}`), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("view_agents"))).buildToObject();
74
+ }
75
+ function createUpdatedHelpMessage() {
76
+ return Message().blocks(Blocks.Section().text(`${Md.bold(SlackStrings.help.title)}`), Blocks.Section().text(SlackStrings.help.publicSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.privateSection), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.otherCommands)).buildToObject();
77
+ }
78
+ function createAlreadyLinkedMessage(email, linkedAt, dashboardUrl) {
79
+ return Message().blocks(Blocks.Section().text(Md.bold("✅ Already Linked!") + "\n\nYour Slack account is already connected to Inkeep.\n\n" + Md.bold("Inkeep Account:") + ` ${email}\n` + Md.bold("Linked:") + ` ${new Date(linkedAt).toLocaleDateString()}\n\nTo switch accounts, first run \`/inkeep unlink\``), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
80
+ }
81
+ function createUnlinkSuccessMessage() {
82
+ return Message().blocks(Blocks.Section().text(Md.bold("✅ Account Unlinked") + "\n\nYour Slack account has been disconnected from Inkeep.\n\nTo use Inkeep agents again, run `/inkeep link` to connect a new account.")).buildToObject();
83
+ }
84
+ function createNotLinkedMessage() {
85
+ return Message().blocks(Blocks.Section().text(Md.bold("❌ Not Linked") + "\n\nYour Slack account is not connected to Inkeep.\n\nRun `/inkeep link` to connect your account.")).buildToObject();
86
+ }
87
+ function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
88
+ const { effective } = agentConfigs;
89
+ let agentLine;
90
+ if (effective) agentLine = `${Md.bold("Agent:")} ${effective.agentName || effective.agentId}`;
91
+ else agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
92
+ return Message().blocks(Blocks.Section().text(Md.bold("✅ Connected to Inkeep") + `\n\n${Md.bold("Account:")} ${email}\n${Md.bold("Linked:")} ${new Date(linkedAt).toLocaleDateString()}\n` + agentLine), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
93
+ }
94
+ function createJwtLinkMessage(linkUrl, expiresInMinutes) {
95
+ return Message().blocks(Blocks.Section().text(`${Md.bold("🔗 Link your Inkeep account")}\n\nConnect your Slack and Inkeep accounts to unlock AI-powered assistance:`), Blocks.Section().text(`${Md.bold("What you can do after linking:")}\n• Ask questions with \`/inkeep [question]\` or \`@Inkeep\`
96
+ • Get personalized responses from AI agents
97
+ • Set your own default agent preferences`), Blocks.Section().text(`${Md.bold("How to link:")}\n1. Click the button below
98
+ 2. Sign in to Inkeep (or create an account)
99
+ 3. Done! Come back here and start asking questions`), Blocks.Actions().elements(Elements.Button().text("🔗 Link Account").url(linkUrl).actionId("link_account").primary()), Blocks.Context().elements(`${Md.emoji("clock")} This link expires in ${expiresInMinutes} minutes`)).buildToObject();
100
+ }
101
+
102
+ //#endregion
103
+ export { buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -0,0 +1,108 @@
1
+ import * as _slack_web_api0 from "@slack/web-api";
2
+ import { WebClient } from "@slack/web-api";
3
+
4
+ //#region src/slack/services/client.d.ts
5
+
6
+ /**
7
+ * Create a Slack WebClient with the provided bot token.
8
+ *
9
+ * Built-in retry behavior:
10
+ * - **Connection errors**: 5 retries in 5 minutes (exponential backoff + jitter).
11
+ *
12
+ * @param token - Bot OAuth token from Nango connection
13
+ * @returns Configured Slack WebClient instance
14
+ */
15
+ declare function getSlackClient(token: string): WebClient;
16
+ /**
17
+ * Fetch user profile information from Slack.
18
+ *
19
+ * @param client - Authenticated Slack WebClient
20
+ * @param userId - Slack user ID (e.g., U0ABC123)
21
+ * @returns User profile object, or null if not found
22
+ */
23
+ declare function getSlackUserInfo(client: WebClient, userId: string): Promise<{
24
+ id: string | undefined;
25
+ name: string | undefined;
26
+ realName: string | undefined;
27
+ displayName: string | undefined;
28
+ email: string | undefined;
29
+ isAdmin: boolean | undefined;
30
+ isOwner: boolean | undefined;
31
+ avatar: string | undefined;
32
+ } | null>;
33
+ /**
34
+ * Fetch workspace (team) information from Slack.
35
+ *
36
+ * @param client - Authenticated Slack WebClient
37
+ * @returns Team info object, or null if not available
38
+ */
39
+ declare function getSlackTeamInfo(client: WebClient): Promise<{
40
+ id: string | undefined;
41
+ name: string | undefined;
42
+ domain: string | undefined;
43
+ icon: string | undefined;
44
+ url: string | undefined;
45
+ } | null>;
46
+ /**
47
+ * List channels in the workspace (public, private, and shared).
48
+ *
49
+ * Note: The bot must be a member of private channels to see them.
50
+ * Users can invite the bot with `/invite @BotName` in the private channel.
51
+ *
52
+ * @param client - Authenticated Slack WebClient
53
+ * @param limit - Maximum number of channels to return. Fetches in pages of up to 200 until the limit is reached or all channels are returned.
54
+ * @returns Array of channel objects with id, name, member count, and privacy status
55
+ */
56
+ declare function getSlackChannels(client: WebClient, limit?: number): Promise<{
57
+ id: string | undefined;
58
+ name: string | undefined;
59
+ memberCount: number | undefined;
60
+ isBotMember: boolean | undefined;
61
+ isPrivate: boolean;
62
+ isShared: boolean;
63
+ }[]>;
64
+ /**
65
+ * Post a message to a Slack channel.
66
+ *
67
+ * @param client - Authenticated Slack WebClient
68
+ * @param channel - Channel ID to post to
69
+ * @param text - Fallback text for notifications
70
+ * @param blocks - Optional Block Kit blocks for rich formatting
71
+ * @returns Slack API response with message timestamp
72
+ */
73
+ declare function postMessage(client: WebClient, channel: string, text: string, blocks?: unknown[]): Promise<_slack_web_api0.ChatPostMessageResponse>;
74
+ /**
75
+ * Post a message as a reply in a thread.
76
+ *
77
+ * @param client - Authenticated Slack WebClient
78
+ * @param channel - Channel ID containing the thread
79
+ * @param threadTs - Thread parent message timestamp
80
+ * @param text - Fallback text for notifications
81
+ * @param blocks - Optional Block Kit blocks for rich formatting
82
+ * @returns Slack API response with message timestamp
83
+ */
84
+ declare function postMessageInThread(client: WebClient, channel: string, threadTs: string, text: string, blocks?: unknown[]): Promise<_slack_web_api0.ChatPostMessageResponse>;
85
+ /**
86
+ * Check if a user is a member of a Slack channel.
87
+ *
88
+ * Uses conversations.members to verify membership. Handles pagination
89
+ * for channels with many members.
90
+ *
91
+ * @param client - Authenticated Slack WebClient
92
+ * @param channelId - Channel ID to check membership for
93
+ * @param userId - Slack user ID to check
94
+ * @returns true if user is a member, false otherwise
95
+ */
96
+ declare function checkUserIsChannelMember(client: WebClient, channelId: string, userId: string): Promise<boolean>;
97
+ /**
98
+ * Revoke a Slack bot token.
99
+ *
100
+ * This should be called when uninstalling a workspace to ensure
101
+ * the token can no longer be used to make API calls.
102
+ *
103
+ * @param token - Bot OAuth token to revoke
104
+ * @returns true if revocation succeeded or token was already invalid
105
+ */
106
+ declare function revokeSlackToken(token: string): Promise<boolean>;
107
+ //#endregion
108
+ export { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
@@ -0,0 +1,232 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import { WebClient, retryPolicies } from "@slack/web-api";
3
+
4
+ //#region src/slack/services/client.ts
5
+ /**
6
+ * Slack Web API Client Utilities
7
+ *
8
+ * Wrapper functions for common Slack Web API operations.
9
+ * Tokens are fetched from Nango at runtime and passed to these functions.
10
+ */
11
+ const logger = getLogger("slack-client");
12
+ async function paginateSlack({ fetchPage, extractItems, getNextCursor, limit }) {
13
+ const items = [];
14
+ let cursor;
15
+ do {
16
+ const response = await fetchPage(cursor);
17
+ items.push(...extractItems(response));
18
+ cursor = getNextCursor(response);
19
+ } while (cursor && (limit === void 0 || items.length < limit));
20
+ return limit !== void 0 ? items.slice(0, limit) : items;
21
+ }
22
+ /**
23
+ * Create a Slack WebClient with the provided bot token.
24
+ *
25
+ * Built-in retry behavior:
26
+ * - **Connection errors**: 5 retries in 5 minutes (exponential backoff + jitter).
27
+ *
28
+ * @param token - Bot OAuth token from Nango connection
29
+ * @returns Configured Slack WebClient instance
30
+ */
31
+ function getSlackClient(token) {
32
+ return new WebClient(token, { retryConfig: retryPolicies.fiveRetriesInFiveMinutes });
33
+ }
34
+ /**
35
+ * Fetch user profile information from Slack.
36
+ *
37
+ * @param client - Authenticated Slack WebClient
38
+ * @param userId - Slack user ID (e.g., U0ABC123)
39
+ * @returns User profile object, or null if not found
40
+ */
41
+ async function getSlackUserInfo(client, userId) {
42
+ try {
43
+ const result = await client.users.info({ user: userId });
44
+ if (result.ok && result.user) return {
45
+ id: result.user.id,
46
+ name: result.user.name,
47
+ realName: result.user.real_name,
48
+ displayName: result.user.profile?.display_name,
49
+ email: result.user.profile?.email,
50
+ isAdmin: result.user.is_admin,
51
+ isOwner: result.user.is_owner,
52
+ avatar: result.user.profile?.image_72
53
+ };
54
+ return null;
55
+ } catch (error) {
56
+ logger.error({
57
+ error,
58
+ userId
59
+ }, "Failed to fetch Slack user info");
60
+ return null;
61
+ }
62
+ }
63
+ /**
64
+ * Fetch workspace (team) information from Slack.
65
+ *
66
+ * @param client - Authenticated Slack WebClient
67
+ * @returns Team info object, or null if not available
68
+ */
69
+ async function getSlackTeamInfo(client) {
70
+ try {
71
+ const result = await client.team.info();
72
+ if (result.ok && result.team) return {
73
+ id: result.team.id,
74
+ name: result.team.name,
75
+ domain: result.team.domain,
76
+ icon: result.team.icon?.image_68,
77
+ url: result.team.url
78
+ };
79
+ return null;
80
+ } catch (error) {
81
+ logger.error({ error }, "Failed to fetch Slack team info");
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * List channels in the workspace (public, private, and shared).
87
+ *
88
+ * Note: The bot must be a member of private channels to see them.
89
+ * Users can invite the bot with `/invite @BotName` in the private channel.
90
+ *
91
+ * @param client - Authenticated Slack WebClient
92
+ * @param limit - Maximum number of channels to return. Fetches in pages of up to 200 until the limit is reached or all channels are returned.
93
+ * @returns Array of channel objects with id, name, member count, and privacy status
94
+ */
95
+ async function getSlackChannels(client, limit = 200) {
96
+ return paginateSlack({
97
+ fetchPage: (cursor) => client.conversations.list({
98
+ types: "public_channel,private_channel",
99
+ exclude_archived: true,
100
+ limit: Math.min(limit, 200),
101
+ cursor
102
+ }),
103
+ extractItems: (result) => {
104
+ if (!result.ok) {
105
+ logger.warn({ error: result.error }, "Slack API returned ok: false during channel pagination");
106
+ return [];
107
+ }
108
+ return result.channels ? result.channels.map((ch) => ({
109
+ id: ch.id,
110
+ name: ch.name,
111
+ memberCount: ch.num_members,
112
+ isBotMember: ch.is_member,
113
+ isPrivate: ch.is_private ?? false,
114
+ isShared: ch.is_shared ?? ch.is_ext_shared ?? false
115
+ })) : [];
116
+ },
117
+ getNextCursor: (result) => result.response_metadata?.next_cursor || void 0,
118
+ limit
119
+ });
120
+ }
121
+ /**
122
+ * Post a message to a Slack channel.
123
+ *
124
+ * @param client - Authenticated Slack WebClient
125
+ * @param channel - Channel ID to post to
126
+ * @param text - Fallback text for notifications
127
+ * @param blocks - Optional Block Kit blocks for rich formatting
128
+ * @returns Slack API response with message timestamp
129
+ */
130
+ async function postMessage(client, channel, text, blocks) {
131
+ try {
132
+ const args = {
133
+ channel,
134
+ text
135
+ };
136
+ if (blocks) args.blocks = blocks;
137
+ return await client.chat.postMessage(args);
138
+ } catch (error) {
139
+ logger.error({
140
+ error,
141
+ channel
142
+ }, "Failed to post Slack message");
143
+ throw error;
144
+ }
145
+ }
146
+ /**
147
+ * Post a message as a reply in a thread.
148
+ *
149
+ * @param client - Authenticated Slack WebClient
150
+ * @param channel - Channel ID containing the thread
151
+ * @param threadTs - Thread parent message timestamp
152
+ * @param text - Fallback text for notifications
153
+ * @param blocks - Optional Block Kit blocks for rich formatting
154
+ * @returns Slack API response with message timestamp
155
+ */
156
+ async function postMessageInThread(client, channel, threadTs, text, blocks) {
157
+ try {
158
+ const args = {
159
+ channel,
160
+ text,
161
+ thread_ts: threadTs
162
+ };
163
+ if (blocks) args.blocks = blocks;
164
+ return await client.chat.postMessage(args);
165
+ } catch (error) {
166
+ logger.error({
167
+ error,
168
+ channel,
169
+ threadTs
170
+ }, "Failed to post Slack message in thread");
171
+ throw error;
172
+ }
173
+ }
174
+ /**
175
+ * Check if a user is a member of a Slack channel.
176
+ *
177
+ * Uses conversations.members to verify membership. Handles pagination
178
+ * for channels with many members.
179
+ *
180
+ * @param client - Authenticated Slack WebClient
181
+ * @param channelId - Channel ID to check membership for
182
+ * @param userId - Slack user ID to check
183
+ * @returns true if user is a member, false otherwise
184
+ */
185
+ async function checkUserIsChannelMember(client, channelId, userId) {
186
+ return (await paginateSlack({
187
+ fetchPage: (cursor) => client.conversations.members({
188
+ channel: channelId,
189
+ limit: 200,
190
+ cursor
191
+ }),
192
+ extractItems: (result) => {
193
+ if (!result.ok) {
194
+ logger.warn({ error: result.error }, "Slack API returned ok: false during members pagination");
195
+ return [];
196
+ }
197
+ return result.members ?? [];
198
+ },
199
+ getNextCursor: (result) => result.response_metadata?.next_cursor || void 0
200
+ })).includes(userId);
201
+ }
202
+ /**
203
+ * Revoke a Slack bot token.
204
+ *
205
+ * This should be called when uninstalling a workspace to ensure
206
+ * the token can no longer be used to make API calls.
207
+ *
208
+ * @param token - Bot OAuth token to revoke
209
+ * @returns true if revocation succeeded or token was already invalid
210
+ */
211
+ async function revokeSlackToken(token) {
212
+ try {
213
+ const result = await new WebClient(token).auth.revoke();
214
+ if (result.ok) {
215
+ logger.info({}, "Successfully revoked Slack token");
216
+ return true;
217
+ }
218
+ logger.warn({ error: result.error }, "Token revocation returned non-ok status");
219
+ return false;
220
+ } catch (error) {
221
+ const errorMessage = error instanceof Error ? error.message : String(error);
222
+ if (errorMessage.includes("token_revoked") || errorMessage.includes("invalid_auth")) {
223
+ logger.info({}, "Token already revoked or invalid");
224
+ return true;
225
+ }
226
+ logger.error({ error }, "Failed to revoke Slack token");
227
+ return false;
228
+ }
229
+ }
230
+
231
+ //#endregion
232
+ export { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
@@ -0,0 +1,19 @@
1
+ import { SlackWorkspaceConnection } from "../nango.js";
2
+ import { SlackCommandPayload, SlackCommandResponse } from "../types.js";
3
+
4
+ //#region src/slack/services/commands/index.d.ts
5
+ declare function handleLinkCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
6
+ declare function handleUnlinkCommand(payload: SlackCommandPayload, tenantId: string): Promise<SlackCommandResponse>;
7
+ declare function handleStatusCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
8
+ declare function handleHelpCommand(): Promise<SlackCommandResponse>;
9
+ /**
10
+ * Handle `/inkeep` with no arguments - opens the agent picker modal
11
+ * Similar to @mention behavior in channels
12
+ */
13
+ declare function handleAgentPickerCommand(payload: SlackCommandPayload, tenantId: string, workspaceConnection?: SlackWorkspaceConnection | null): Promise<SlackCommandResponse>;
14
+ declare function handleQuestionCommand(payload: SlackCommandPayload, question: string, _dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
15
+ declare function handleRunCommand(payload: SlackCommandPayload, agentIdentifier: string, question: string, _dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
16
+ declare function handleAgentListCommand(payload: SlackCommandPayload, dashboardUrl: string, _tenantId: string): Promise<SlackCommandResponse>;
17
+ declare function handleCommand(payload: SlackCommandPayload): Promise<SlackCommandResponse>;
18
+ //#endregion
19
+ export { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand };