@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.
- package/dist/env.d.ts +24 -2
- package/dist/env.js +13 -2
- package/dist/github/index.d.ts +3 -3
- package/dist/github/mcp/auth.d.ts +2 -2
- package/dist/github/mcp/index.d.ts +2 -2
- package/dist/github/mcp/index.js +23 -34
- package/dist/github/mcp/schemas.d.ts +1 -1
- package/dist/github/routes/setup.d.ts +2 -2
- package/dist/github/routes/tokenExchange.d.ts +2 -2
- package/dist/github/routes/webhooks.d.ts +2 -2
- package/dist/slack/i18n/index.d.ts +2 -0
- package/dist/slack/i18n/index.js +3 -0
- package/dist/slack/i18n/strings.d.ts +73 -0
- package/dist/slack/i18n/strings.js +67 -0
- package/dist/slack/index.d.ts +18 -0
- package/dist/slack/index.js +28 -0
- package/dist/slack/middleware/permissions.d.ts +31 -0
- package/dist/slack/middleware/permissions.js +167 -0
- package/dist/slack/routes/events.d.ts +10 -0
- package/dist/slack/routes/events.js +551 -0
- package/dist/slack/routes/index.d.ts +10 -0
- package/dist/slack/routes/index.js +47 -0
- package/dist/slack/routes/oauth.d.ts +20 -0
- package/dist/slack/routes/oauth.js +344 -0
- package/dist/slack/routes/users.d.ts +10 -0
- package/dist/slack/routes/users.js +365 -0
- package/dist/slack/routes/workspaces.d.ts +10 -0
- package/dist/slack/routes/workspaces.js +909 -0
- package/dist/slack/services/agent-resolution.d.ts +41 -0
- package/dist/slack/services/agent-resolution.js +99 -0
- package/dist/slack/services/blocks/index.d.ts +73 -0
- package/dist/slack/services/blocks/index.js +103 -0
- package/dist/slack/services/client.d.ts +108 -0
- package/dist/slack/services/client.js +232 -0
- package/dist/slack/services/commands/index.d.ts +19 -0
- package/dist/slack/services/commands/index.js +553 -0
- package/dist/slack/services/events/app-mention.d.ts +40 -0
- package/dist/slack/services/events/app-mention.js +297 -0
- package/dist/slack/services/events/block-actions.d.ts +40 -0
- package/dist/slack/services/events/block-actions.js +265 -0
- package/dist/slack/services/events/index.d.ts +6 -0
- package/dist/slack/services/events/index.js +7 -0
- package/dist/slack/services/events/modal-submission.d.ts +30 -0
- package/dist/slack/services/events/modal-submission.js +400 -0
- package/dist/slack/services/events/streaming.d.ts +26 -0
- package/dist/slack/services/events/streaming.js +255 -0
- package/dist/slack/services/events/utils.d.ts +146 -0
- package/dist/slack/services/events/utils.js +370 -0
- package/dist/slack/services/index.d.ts +16 -0
- package/dist/slack/services/index.js +16 -0
- package/dist/slack/services/modals.d.ts +86 -0
- package/dist/slack/services/modals.js +355 -0
- package/dist/slack/services/nango.d.ts +85 -0
- package/dist/slack/services/nango.js +476 -0
- package/dist/slack/services/security.d.ts +35 -0
- package/dist/slack/services/security.js +65 -0
- package/dist/slack/services/types.d.ts +26 -0
- package/dist/slack/services/types.js +1 -0
- package/dist/slack/services/workspace-tokens.d.ts +25 -0
- package/dist/slack/services/workspace-tokens.js +27 -0
- package/dist/slack/tracer.d.ts +40 -0
- package/dist/slack/tracer.js +39 -0
- package/dist/slack/types.d.ts +10 -0
- package/dist/slack/types.js +1 -0
- 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 };
|