@inkeep/agents-work-apps 0.52.0 → 0.53.1

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 (37) hide show
  1. package/dist/env.d.ts +2 -2
  2. package/dist/github/mcp/auth.d.ts +2 -2
  3. package/dist/github/mcp/index.d.ts +2 -2
  4. package/dist/github/mcp/index.js +63 -26
  5. package/dist/github/mcp/schemas.d.ts +1 -1
  6. package/dist/github/mcp/utils.d.ts +2 -1
  7. package/dist/github/mcp/utils.js +16 -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/dispatcher.js +11 -1
  12. package/dist/slack/routes/oauth.js +21 -1
  13. package/dist/slack/routes/users.js +29 -3
  14. package/dist/slack/services/agent-resolution.d.ts +1 -0
  15. package/dist/slack/services/agent-resolution.js +105 -18
  16. package/dist/slack/services/blocks/index.d.ts +9 -2
  17. package/dist/slack/services/blocks/index.js +27 -11
  18. package/dist/slack/services/commands/index.d.ts +1 -1
  19. package/dist/slack/services/commands/index.js +34 -124
  20. package/dist/slack/services/events/app-mention.d.ts +4 -14
  21. package/dist/slack/services/events/app-mention.js +40 -19
  22. package/dist/slack/services/events/block-actions.js +1 -1
  23. package/dist/slack/services/events/index.d.ts +1 -1
  24. package/dist/slack/services/events/modal-submission.js +14 -7
  25. package/dist/slack/services/events/streaming.js +9 -12
  26. package/dist/slack/services/events/utils.d.ts +26 -5
  27. package/dist/slack/services/events/utils.js +40 -15
  28. package/dist/slack/services/index.d.ts +4 -4
  29. package/dist/slack/services/index.js +3 -3
  30. package/dist/slack/services/link-prompt.d.ts +27 -0
  31. package/dist/slack/services/link-prompt.js +142 -0
  32. package/dist/slack/services/modals.d.ts +1 -0
  33. package/dist/slack/services/modals.js +6 -4
  34. package/dist/slack/services/resume-intent.d.ts +15 -0
  35. package/dist/slack/services/resume-intent.js +338 -0
  36. package/dist/slack/tracer.d.ts +1 -1
  37. package/package.json +2 -2
@@ -17,6 +17,7 @@ declare function createContextBlock(params: ContextBlockParams): {
17
17
  interface FollowUpButtonParams {
18
18
  conversationId: string;
19
19
  agentId: string;
20
+ agentName?: string;
20
21
  projectId: string;
21
22
  tenantId: string;
22
23
  teamId: string;
@@ -52,18 +53,25 @@ interface AgentConfigSources {
52
53
  channelConfig: {
53
54
  agentName?: string;
54
55
  agentId: string;
56
+ projectId: string;
57
+ projectName?: string;
55
58
  } | null;
56
59
  workspaceConfig: {
57
60
  agentName?: string;
58
61
  agentId: string;
62
+ projectId: string;
63
+ projectName?: string;
59
64
  } | null;
60
65
  effective: {
61
66
  agentName?: string;
62
67
  agentId: string;
68
+ projectId: string;
69
+ projectName?: string;
63
70
  source: string;
64
71
  } | null;
65
72
  }
66
73
  declare function createStatusMessage(email: string, linkedAt: string, dashboardUrl: string, agentConfigs: AgentConfigSources): Readonly<slack_block_builder0.SlackMessageDto>;
74
+ declare function createSmartLinkMessage(linkUrl: string): Readonly<slack_block_builder0.SlackMessageDto>;
67
75
  interface ToolApprovalButtonValue {
68
76
  toolCallId: string;
69
77
  conversationId: string;
@@ -142,7 +150,6 @@ declare function buildCitationsBlock(citations: Array<{
142
150
  title?: string;
143
151
  url?: string;
144
152
  }>): any[];
145
- declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
146
153
  declare function createCreateInkeepAccountMessage(acceptUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
147
154
  //#endregion
148
- export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
155
+ export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -44,10 +44,7 @@ function buildConversationResponseBlocks(params) {
44
44
  }
45
45
  }];
46
46
  if (!isError) {
47
- const contextBlock = createContextBlock({
48
- agentName,
49
- isPrivate: true
50
- });
47
+ const contextBlock = createContextBlock({ agentName });
51
48
  blocks.push(contextBlock);
52
49
  blocks.push({
53
50
  type: "actions",
@@ -70,10 +67,32 @@ function createNotLinkedMessage() {
70
67
  }
71
68
  function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
72
69
  const { effective } = agentConfigs;
70
+ const baseUrl = dashboardUrl.replace(/\/work-apps\/slack$/, "");
73
71
  let agentLine;
74
- if (effective) agentLine = `${Md.bold("Agent:")} ${effective.agentName || effective.agentId}`;
75
- else agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
76
- 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();
72
+ let projectLine;
73
+ if (effective) {
74
+ const agentDisplayName = effective.agentName || effective.agentId;
75
+ const agentUrl = `${baseUrl}/projects/${effective.projectId}/agents/${effective.agentId}`;
76
+ agentLine = `${Md.bold("Agent:")} <${agentUrl}|${agentDisplayName}>`;
77
+ const projectDisplayName = effective.projectName || effective.projectId;
78
+ const projectUrl = `${baseUrl}/projects/${effective.projectId}/agents`;
79
+ projectLine = `${Md.bold("Project:")} <${projectUrl}|${projectDisplayName}>`;
80
+ } else {
81
+ agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
82
+ projectLine = "";
83
+ }
84
+ const lines = [
85
+ Md.bold("Connected to Inkeep"),
86
+ "",
87
+ `${Md.bold("Account:")} ${email}`,
88
+ `${Md.bold("Linked:")} ${new Date(linkedAt).toLocaleDateString()}`,
89
+ agentLine
90
+ ];
91
+ if (projectLine) lines.push(projectLine);
92
+ return Message().blocks(Blocks.Section().text(lines.join("\n")), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
93
+ }
94
+ function createSmartLinkMessage(linkUrl) {
95
+ return Message().blocks(Blocks.Section().text("To get started, let's connect your Inkeep account with Slack."), Blocks.Actions().elements(Elements.Button().text("Link Account").url(linkUrl).actionId("smart_link_account").primary()), Blocks.Context().elements("🕐 This only needs to happen once.")).buildToObject();
77
96
  }
78
97
  const ToolApprovalButtonValueSchema = z.object({
79
98
  toolCallId: z.string(),
@@ -300,12 +319,9 @@ function buildCitationsBlock(citations) {
300
319
  }
301
320
  }];
302
321
  }
303
- function createJwtLinkMessage(linkUrl, expiresInMinutes) {
304
- return Message().blocks(Blocks.Section().text(`${Md.bold("Link your Inkeep account")}\n\nConnect your Slack and Inkeep accounts to use Inkeep agents.`), Blocks.Actions().elements(Elements.Button().text("Link Account").url(linkUrl).actionId("link_account").primary()), Blocks.Context().elements(`This link expires in ${expiresInMinutes} minutes.`)).buildToObject();
305
- }
306
322
  function createCreateInkeepAccountMessage(acceptUrl, expiresInMinutes) {
307
323
  return Message().blocks(Blocks.Section().text(`${Md.bold("Create your Inkeep account")}\n\nYou've been invited to join Inkeep. Create an account to start using Inkeep agents in Slack.`), Blocks.Actions().elements(Elements.Button().text("Create Account").url(acceptUrl).actionId("create_account").primary()), Blocks.Context().elements(`This link expires in ${expiresInMinutes} minutes.`)).buildToObject();
308
324
  }
309
325
 
310
326
  //#endregion
311
- export { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
327
+ export { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -11,7 +11,7 @@ declare function handleHelpCommand(): Promise<SlackCommandResponse>;
11
11
  * Similar to @mention behavior in channels
12
12
  */
13
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>;
14
+ declare function handleQuestionCommand(payload: SlackCommandPayload, question: string, _dashboardUrl: string, tenantId: string, botToken: string): Promise<SlackCommandResponse>;
15
15
  declare function handleCommand(payload: SlackCommandPayload): Promise<SlackCommandResponse>;
16
16
  //#endregion
17
17
  export { handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand };
@@ -2,134 +2,35 @@ import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
3
  import runDbClient_default from "../../../db/runDbClient.js";
4
4
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
5
+ import { extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
5
6
  import { resolveEffectiveAgent } from "../agent-resolution.js";
6
7
  import { SlackStrings } from "../../i18n/strings.js";
7
- import { createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
+ import { createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
9
  import { getSlackClient } from "../client.js";
9
- import { extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
10
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
10
11
  import { buildAgentSelectorModal } from "../modals.js";
11
- import { createInvitationInDb, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, findWorkAppSlackWorkspaceByTeamId, flushTraces, getInProcessFetch, getOrganizationMemberByEmail, getPendingInvitationsByEmail, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
12
+ import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getInProcessFetch, getWaitUntil, signSlackUserToken } from "@inkeep/agents-core";
12
13
 
13
14
  //#region src/slack/services/commands/index.ts
14
15
  const DEFAULT_CLIENT_ID = "work-apps-slack";
15
- const LINK_CODE_TTL_MINUTES = 10;
16
16
  const logger = getLogger("slack-commands");
17
- /**
18
- * Create an invitation for a Slack user who doesn't have an Inkeep account yet.
19
- * Returns the invitation ID and email so the caller can direct the user
20
- * to the accept-invitation page.
21
- *
22
- * Returns null if:
23
- * - Workspace doesn't have shouldAllowJoinFromWorkspace enabled
24
- * - User already has an Inkeep account (JWT link flow is sufficient)
25
- * - Service account is not configured
26
- */
27
- async function tryAutoInvite(payload, tenantId, botToken) {
28
- try {
29
- if (!(await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(tenantId, payload.teamId))?.shouldAllowJoinFromWorkspace) return null;
30
- const slackClient = getSlackClient(botToken);
31
- let userEmail;
32
- try {
33
- userEmail = (await slackClient.users.info({ user: payload.userId })).user?.profile?.email;
34
- } catch (error) {
35
- logger.warn({
36
- error,
37
- userId: payload.userId
38
- }, "Failed to get user info from Slack");
39
- return null;
40
- }
41
- if (!userEmail) {
42
- logger.warn({ userId: payload.userId }, "No email found in Slack user profile");
43
- return null;
44
- }
45
- if (await getOrganizationMemberByEmail(runDbClient_default)(tenantId, userEmail)) {
46
- logger.debug({
47
- userId: payload.userId,
48
- email: userEmail
49
- }, "User already has Inkeep account, skipping auto-invite");
50
- return null;
51
- }
52
- const existingInvitation = (await getPendingInvitationsByEmail(runDbClient_default)(userEmail)).find((inv) => inv.organizationId === tenantId);
53
- if (existingInvitation) {
54
- logger.info({
55
- userId: payload.userId,
56
- tenantId,
57
- invitationId: existingInvitation.id,
58
- email: userEmail
59
- }, "Reusing existing pending invitation for Slack user");
60
- return {
61
- invitationId: existingInvitation.id,
62
- email: userEmail
63
- };
64
- }
65
- const invitation = await createInvitationInDb(runDbClient_default)({
66
- organizationId: tenantId,
67
- email: userEmail
68
- });
69
- logger.info({
70
- userId: payload.userId,
71
- tenantId,
72
- invitationId: invitation.id,
73
- email: userEmail
74
- }, "Invitation created for Slack user without Inkeep account");
75
- return {
76
- invitationId: invitation.id,
77
- email: userEmail
78
- };
79
- } catch (error) {
80
- logger.warn({
81
- error,
82
- userId: payload.userId,
83
- tenantId
84
- }, "Auto-invite attempt failed");
85
- return null;
86
- }
87
- }
88
17
  async function handleLinkCommand(payload, dashboardUrl, tenantId, botToken) {
89
18
  const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
90
19
  if (existingLink) return {
91
20
  response_type: "ephemeral",
92
21
  ...createAlreadyLinkedMessage(existingLink.slackEmail || existingLink.slackUsername || "Unknown", existingLink.linkedAt, dashboardUrl)
93
22
  };
94
- if (botToken) {
95
- const autoInvite = await tryAutoInvite(payload, tenantId, botToken);
96
- if (autoInvite) {
97
- const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
98
- const linkToken = await signSlackLinkToken({
23
+ try {
24
+ return {
25
+ response_type: "ephemeral",
26
+ ...buildLinkPromptMessage(await resolveUnlinkedUserAction({
99
27
  tenantId,
100
- slackTeamId: payload.teamId,
28
+ teamId: payload.teamId,
101
29
  slackUserId: payload.userId,
30
+ botToken: botToken || "",
102
31
  slackEnterpriseId: payload.enterpriseId,
103
32
  slackUsername: payload.userName
104
- });
105
- const linkReturnUrl = `/link?token=${encodeURIComponent(linkToken)}`;
106
- const acceptUrl = `${manageUiUrl}/accept-invitation/${autoInvite.invitationId}?email=${encodeURIComponent(autoInvite.email)}&returnUrl=${encodeURIComponent(linkReturnUrl)}`;
107
- logger.info({
108
- invitationId: autoInvite.invitationId,
109
- email: autoInvite.email
110
- }, "Directing new user to accept-invitation page with link returnUrl");
111
- return {
112
- response_type: "ephemeral",
113
- ...createCreateInkeepAccountMessage(acceptUrl, LINK_CODE_TTL_MINUTES)
114
- };
115
- }
116
- }
117
- try {
118
- const linkToken = await signSlackLinkToken({
119
- tenantId,
120
- slackTeamId: payload.teamId,
121
- slackUserId: payload.userId,
122
- slackEnterpriseId: payload.enterpriseId,
123
- slackUsername: payload.userName
124
- });
125
- const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
126
- logger.info({
127
- slackUserId: payload.userId,
128
- tenantId
129
- }, "Generated JWT link token");
130
- return {
131
- response_type: "ephemeral",
132
- ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
33
+ }))
133
34
  };
134
35
  } catch (error) {
135
36
  logger.error({
@@ -278,23 +179,27 @@ async function handleAgentPickerCommand(payload, tenantId, workspaceConnection)
278
179
  };
279
180
  }
280
181
  }
281
- async function generateLinkCodeWithIntent(payload, tenantId) {
182
+ async function generateLinkCodeWithIntent(payload, tenantId, botToken, intent) {
282
183
  try {
283
- const linkToken = await signSlackLinkToken({
184
+ const linkResult = await resolveUnlinkedUserAction({
284
185
  tenantId,
285
- slackTeamId: payload.teamId,
186
+ teamId: payload.teamId,
286
187
  slackUserId: payload.userId,
188
+ botToken,
287
189
  slackEnterpriseId: payload.enterpriseId,
288
- slackUsername: payload.userName
190
+ slackUsername: payload.userName,
191
+ intent
289
192
  });
290
- const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
291
- logger.info({
292
- slackUserId: payload.userId,
293
- tenantId
294
- }, "Generated JWT link token with intent");
193
+ if (!!intent) logger.info({
194
+ event: "smart_link_intent_captured",
195
+ entryPoint: intent.entryPoint,
196
+ linkType: linkResult.type,
197
+ questionLength: intent.question.length,
198
+ channelId: payload.channelId
199
+ }, "Smart link intent captured");
295
200
  return {
296
201
  response_type: "ephemeral",
297
- ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
202
+ ...buildLinkPromptMessage(linkResult)
298
203
  };
299
204
  } catch (error) {
300
205
  logger.error({
@@ -308,9 +213,14 @@ async function generateLinkCodeWithIntent(payload, tenantId) {
308
213
  };
309
214
  }
310
215
  }
311
- async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId) {
216
+ async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId, botToken) {
312
217
  const existingLink = await findWorkAppSlackUserMappingBySlackUser(runDbClient_default)(payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
313
- if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId);
218
+ if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId, botToken, {
219
+ entryPoint: "question_command",
220
+ question: question.slice(0, 2e3),
221
+ channelId: payload.channelId,
222
+ responseUrl: payload.responseUrl
223
+ });
314
224
  const userTenantId = existingLink.tenantId;
315
225
  const resolvedAgent = await resolveEffectiveAgent({
316
226
  tenantId: userTenantId,
@@ -412,7 +322,7 @@ async function executeAgentInBackground(payload, existingLink, targetAgent, ques
412
322
  }, "Agent execution completed via Slack");
413
323
  const contextBlock = createContextBlock({ agentName: targetAgent.name || targetAgent.id });
414
324
  await sendResponseUrlMessage(payload.responseUrl, {
415
- response_type: "ephemeral",
325
+ response_type: "in_channel",
416
326
  text: assistantMessage,
417
327
  blocks: [{
418
328
  type: "section",
@@ -464,7 +374,7 @@ async function handleCommand(payload) {
464
374
  case "disconnect": return handleUnlinkCommand(payload, tenantId);
465
375
  case "help": return handleHelpCommand();
466
376
  case "": return handleAgentPickerCommand(payload, tenantId, workspaceConnection);
467
- default: return handleQuestionCommand(payload, text, dashboardUrl, tenantId);
377
+ default: return handleQuestionCommand(payload, text, dashboardUrl, tenantId, workspaceConnection.botToken);
468
378
  }
469
379
  }
470
380
 
@@ -1,18 +1,7 @@
1
+ import { SlackAttachment } from "./utils.js";
2
+
1
3
  //#region src/slack/services/events/app-mention.d.ts
2
- /**
3
- * Handler for Slack @mention events
4
- *
5
- * Flow:
6
- * 1. Resolve workspace connection (single lookup, cached)
7
- * 2. Parallel: resolve agent config + check user link
8
- * 3. If no agent configured → prompt to set up in dashboard
9
- * 4. If not linked → prompt to link account
10
- * 5. Handle based on context:
11
- * - Channel + no query → Show usage hint
12
- * - Channel + query → Execute agent with streaming response
13
- * - Thread + no query → Auto-execute agent with thread context as query
14
- * - Thread + query → Execute agent with thread context included
15
- */
4
+
16
5
  /**
17
6
  * Metadata passed to the agent selector modal via button value
18
7
  */
@@ -32,6 +21,7 @@ declare function handleAppMention(params: {
32
21
  slackUserId: string;
33
22
  channel: string;
34
23
  text: string;
24
+ attachments?: SlackAttachment[];
35
25
  threadTs: string;
36
26
  messageTs: string;
37
27
  teamId: string;
@@ -1,36 +1,23 @@
1
1
  import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
3
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
4
+ import { checkIfBotThread, classifyError, findCachedUserMapping, formatAttachments, formatChannelContext, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage, timedOp } from "./utils.js";
4
5
  import { resolveEffectiveAgent } from "../agent-resolution.js";
5
6
  import { SlackStrings } from "../../i18n/strings.js";
6
7
  import { getSlackChannelInfo, getSlackClient, getSlackUserInfo, postMessageInThread } from "../client.js";
7
- import { checkIfBotThread, classifyError, findCachedUserMapping, formatChannelContext, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage, timedOp } from "./utils.js";
8
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
8
9
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
9
10
  import { streamAgentResponse } from "./streaming.js";
10
11
  import { signSlackUserToken } from "@inkeep/agents-core";
11
12
 
12
13
  //#region src/slack/services/events/app-mention.ts
13
- /**
14
- * Handler for Slack @mention events
15
- *
16
- * Flow:
17
- * 1. Resolve workspace connection (single lookup, cached)
18
- * 2. Parallel: resolve agent config + check user link
19
- * 3. If no agent configured → prompt to set up in dashboard
20
- * 4. If not linked → prompt to link account
21
- * 5. Handle based on context:
22
- * - Channel + no query → Show usage hint
23
- * - Channel + query → Execute agent with streaming response
24
- * - Thread + no query → Auto-execute agent with thread context as query
25
- * - Thread + query → Execute agent with thread context included
26
- */
27
14
  const logger = getLogger("slack-app-mention");
28
15
  /**
29
16
  * Main handler for @mention events in Slack
30
17
  */
31
18
  async function handleAppMention(params) {
32
19
  return tracer.startActiveSpan(SLACK_SPAN_NAMES.APP_MENTION, async (span) => {
33
- const { slackUserId, channel, text, threadTs, messageTs, teamId, dispatchedAt } = params;
20
+ const { slackUserId, channel, text, attachments, threadTs, messageTs, teamId, dispatchedAt } = params;
34
21
  const handlerStartedAt = Date.now();
35
22
  const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
36
23
  const dispatchDelayMs = dispatchedAt ? handlerStartedAt - dispatchedAt : void 0;
@@ -123,11 +110,36 @@ async function handleAppMention(params) {
123
110
  teamId,
124
111
  channel
125
112
  }, "User not linked — prompting account link");
113
+ const intent = {
114
+ entryPoint: "mention",
115
+ question: text.slice(0, 2e3),
116
+ channelId: channel,
117
+ threadTs: isInThread ? threadTs : void 0,
118
+ messageTs,
119
+ agentId: agentConfig.agentId,
120
+ projectId: agentConfig.projectId
121
+ };
122
+ const linkResult = await resolveUnlinkedUserAction({
123
+ tenantId,
124
+ teamId,
125
+ slackUserId,
126
+ botToken,
127
+ intent
128
+ });
129
+ const message = buildLinkPromptMessage(linkResult);
130
+ logger.info({
131
+ event: "smart_link_intent_captured",
132
+ entryPoint: "mention",
133
+ linkType: linkResult.type,
134
+ questionLength: intent.question.length,
135
+ channelId: channel
136
+ }, "Smart link intent captured");
126
137
  await slackClient.chat.postEphemeral({
127
138
  channel,
128
139
  user: slackUserId,
129
140
  thread_ts: isInThread ? threadTs : void 0,
130
- text: "*Link your account to use @Inkeep*\n\nRun `/inkeep link` to connect your Slack and Inkeep accounts."
141
+ text: "To get started, let's connect your Inkeep account with Slack.",
142
+ blocks: message.blocks
131
143
  });
132
144
  span.end();
133
145
  return;
@@ -241,6 +253,7 @@ Respond naturally as if you're joining the conversation to help.`;
241
253
  return;
242
254
  }
243
255
  let queryText = text;
256
+ const attachmentContext = formatAttachments(attachments);
244
257
  if (isInThread && threadTs) {
245
258
  const { result: [contextMessages, channelInfo] } = await timedOp(Promise.all([getThreadContext(slackClient, channel, threadTs), getSlackChannelInfo(slackClient, channel)]), {
246
259
  label: "thread context fetch",
@@ -250,7 +263,12 @@ Respond naturally as if you're joining the conversation to help.`;
250
263
  threadTs
251
264
  }
252
265
  });
253
- if (contextMessages) queryText = `The following is thread context from ${formatChannelContext(channelInfo)}:\n\n<slack_thread_context>\n${contextMessages}\n</slack_thread_context>\n\nMessage from ${slackUserId}: ${text}`;
266
+ if (contextMessages) {
267
+ const channelContext = formatChannelContext(channelInfo);
268
+ let messageContent = text;
269
+ if (attachmentContext) messageContent = `${text}\n\n<attached_content>\n${attachmentContext}\n</attached_content>`;
270
+ queryText = `The following is thread context from ${channelContext}:\n\n<slack_thread_context>\n${contextMessages}\n</slack_thread_context>\n\nMessage from ${slackUserId}: ${messageContent}`;
271
+ }
254
272
  } else {
255
273
  const { result: [channelInfo, userInfo] } = await timedOp(Promise.all([getSlackChannelInfo(slackClient, channel), getSlackUserInfo(slackClient, slackUserId)]), {
256
274
  label: "channel/user info fetch",
@@ -259,7 +277,10 @@ Respond naturally as if you're joining the conversation to help.`;
259
277
  channel
260
278
  }
261
279
  });
262
- queryText = `The following is a message from ${formatChannelContext(channelInfo)} from ${userInfo?.displayName || "User"}: """${text}"""`;
280
+ const channelContext = formatChannelContext(channelInfo);
281
+ const userName = userInfo?.displayName || "User";
282
+ if (attachmentContext) queryText = `The following is a message from ${channelContext} from ${userName}: """${text}"""\n\nThe message also includes the following shared/forwarded content:\n\n<attached_content>\n${attachmentContext}\n</attached_content>`;
283
+ else queryText = `The following is a message from ${channelContext} from ${userName}: """${text}"""`;
263
284
  }
264
285
  const slackUserToken = await signSlackUserToken({
265
286
  inkeepUserId: existingLink.inkeepUserId,
@@ -1,10 +1,10 @@
1
1
  import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
3
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
4
+ import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
4
5
  import { SlackStrings } from "../../i18n/strings.js";
5
6
  import { ToolApprovalButtonValueSchema, buildToolApprovalDoneBlocks } from "../blocks/index.js";
6
7
  import { getSlackClient } from "../client.js";
7
- import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
8
8
  import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "../modals.js";
9
9
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
10
10
  import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
@@ -1,6 +1,6 @@
1
+ import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
1
2
  import { InlineSelectorMetadata, handleAppMention } from "./app-mention.js";
2
3
  import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./block-actions.js";
3
4
  import { handleFollowUpSubmission, handleModalSubmission } from "./modal-submission.js";
4
- import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
5
5
  import { StreamResult, streamAgentResponse } from "./streaming.js";
6
6
  export { type InlineSelectorMetadata, SlackErrorType, type StreamResult, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
@@ -1,10 +1,10 @@
1
1
  import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
3
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
4
+ import { classifyError, extractApiErrorMessage, findCachedUserMapping, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
4
5
  import { SlackStrings } from "../../i18n/strings.js";
5
6
  import { buildConversationResponseBlocks } from "../blocks/index.js";
6
7
  import { getSlackClient } from "../client.js";
7
- import { classifyError, extractApiErrorMessage, findCachedUserMapping, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
8
8
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
9
9
  import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
10
10
 
@@ -36,13 +36,16 @@ async function handleModalSubmission(view) {
36
36
  const includeContext = includeContextValue?.selected_options?.some((o) => o.value === "include_context") ?? true;
37
37
  let agentId = metadata.selectedAgentId;
38
38
  let projectId = metadata.selectedProjectId;
39
+ let agentName = null;
39
40
  if (agentSelectValue?.selected_option?.value) try {
40
41
  const parsed = JSON.parse(agentSelectValue.selected_option.value);
41
42
  agentId = parsed.agentId;
42
43
  projectId = parsed.projectId;
44
+ agentName = parsed.agentName || null;
43
45
  } catch {
44
46
  logger.warn({ value: agentSelectValue.selected_option.value }, "Failed to parse agent select value");
45
47
  }
48
+ const agentDisplayName = agentName || agentId || "Agent";
46
49
  if (!agentId || !projectId) {
47
50
  logger.error({ metadata }, "Missing agent or project ID in modal submission");
48
51
  if (metadata.buttonResponseUrl) await sendResponseUrlMessage(metadata.buttonResponseUrl, {
@@ -112,7 +115,7 @@ async function handleModalSubmission(view) {
112
115
  });
113
116
  span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
114
117
  const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
115
- const thinkingText = SlackStrings.status.thinking(agentId);
118
+ const thinkingText = SlackStrings.status.thinking(agentDisplayName);
116
119
  if (metadata.buttonResponseUrl) await sendResponseUrlMessage(metadata.buttonResponseUrl, {
117
120
  text: thinkingText,
118
121
  response_type: "ephemeral",
@@ -139,6 +142,7 @@ async function handleModalSubmission(view) {
139
142
  slackClient,
140
143
  metadata,
141
144
  agentId,
145
+ agentDisplayName,
142
146
  projectId,
143
147
  tenantId,
144
148
  conversationId,
@@ -194,7 +198,8 @@ async function handleFollowUpSubmission(view) {
194
198
  span.end();
195
199
  return;
196
200
  }
197
- const { conversationId, agentId, projectId, tenantId, teamId, slackUserId, channel } = metadata;
201
+ const { conversationId, agentId, agentName, projectId, tenantId, teamId, slackUserId, channel } = metadata;
202
+ const agentDisplayName = agentName || agentId || "Agent";
198
203
  span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
199
204
  span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channel);
200
205
  span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
@@ -234,7 +239,7 @@ async function handleFollowUpSubmission(view) {
234
239
  await slackClient.chat.postEphemeral({
235
240
  channel,
236
241
  user: slackUserId,
237
- text: SlackStrings.status.thinking(agentId)
242
+ text: SlackStrings.status.thinking(agentDisplayName)
238
243
  });
239
244
  const responseText = await callAgentApi({
240
245
  apiBaseUrl,
@@ -247,11 +252,12 @@ async function handleFollowUpSubmission(view) {
247
252
  const responseBlocks = buildConversationResponseBlocks({
248
253
  userMessage: question,
249
254
  responseText: responseText.text,
250
- agentName: agentId,
255
+ agentName: agentDisplayName,
251
256
  isError: responseText.isError,
252
257
  followUpParams: {
253
258
  conversationId,
254
259
  agentId,
260
+ agentName: agentDisplayName,
255
261
  projectId,
256
262
  tenantId,
257
263
  teamId,
@@ -370,15 +376,16 @@ async function callAgentApi(params) {
370
376
  });
371
377
  }
372
378
  async function postPrivateResponse(params) {
373
- const { slackClient, metadata, agentId, projectId, tenantId, conversationId, userMessage, responseText, isError } = params;
379
+ const { slackClient, metadata, agentId, agentDisplayName, projectId, tenantId, conversationId, userMessage, responseText, isError } = params;
374
380
  const responseBlocks = buildConversationResponseBlocks({
375
381
  userMessage,
376
382
  responseText,
377
- agentName: agentId,
383
+ agentName: agentDisplayName,
378
384
  isError,
379
385
  followUpParams: {
380
386
  conversationId,
381
387
  agentId,
388
+ agentName: agentDisplayName,
382
389
  projectId,
383
390
  tenantId,
384
391
  teamId: metadata.teamId,
@@ -1,7 +1,7 @@
1
1
  import { env } from "../../../env.js";
2
2
  import { getLogger } from "../../../logger.js";
3
- import { buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createContextBlock } from "../blocks/index.js";
4
3
  import { SlackErrorType, classifyError, extractApiErrorMessage, getUserFriendlyErrorMessage } from "./utils.js";
4
+ import { buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createContextBlock } from "../blocks/index.js";
5
5
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
6
6
  import { getInProcessFetch, retryWithBackoff } from "@inkeep/agents-core";
7
7
 
@@ -193,6 +193,7 @@ async function streamAgentResponse(params) {
193
193
  });
194
194
  const pendingApprovalMessages = [];
195
195
  const toolCallIdToName = /* @__PURE__ */ new Map();
196
+ const toolCallIdToInput = /* @__PURE__ */ new Map();
196
197
  const toolErrors = [];
197
198
  const citations = [];
198
199
  const summaryLabels = [];
@@ -221,9 +222,9 @@ async function streamAgentResponse(params) {
221
222
  continue;
222
223
  }
223
224
  if (data.type === "tool-approval-request" && conversationId) {
224
- const toolName = data.toolName || "Tool";
225
225
  const toolCallId = data.toolCallId;
226
- const input = data.input;
226
+ const toolName = toolCallIdToName.get(toolCallId) || "Tool";
227
+ const input = toolCallIdToInput.get(toolCallId);
227
228
  const buttonValue = {
228
229
  toolCallId,
229
230
  conversationId,
@@ -260,6 +261,7 @@ async function streamAgentResponse(params) {
260
261
  }
261
262
  if (data.type === "tool-input-available" && data.toolCallId && data.toolName) {
262
263
  toolCallIdToName.set(String(data.toolCallId), String(data.toolName));
264
+ if (data.input && typeof data.input === "object") toolCallIdToInput.set(String(data.toolCallId), data.input);
263
265
  continue;
264
266
  }
265
267
  if (data.type === "tool-output-denied" && data.toolCallId) {
@@ -383,6 +385,10 @@ async function streamAgentResponse(params) {
383
385
  const stopBlocks = [];
384
386
  for (const { toolName, errorText } of toolErrors) stopBlocks.push(buildToolOutputErrorBlock(toolName, errorText));
385
387
  if (summaryLabels.length > 0) stopBlocks.push(buildSummaryBreadcrumbBlock(summaryLabels));
388
+ if (citations.length > 0) {
389
+ const citationBlocks = buildCitationsBlock(citations);
390
+ stopBlocks.push(...citationBlocks);
391
+ }
386
392
  stopBlocks.push(createContextBlock({ agentName }));
387
393
  try {
388
394
  await withTimeout(streamer.stop({ blocks: stopBlocks.slice(0, 50) }), CHATSTREAM_OP_TIMEOUT_MS, "streamer.stop");
@@ -395,15 +401,6 @@ async function streamAgentResponse(params) {
395
401
  responseLength: fullText.length
396
402
  }, "Failed to finalize chatStream — content was already delivered");
397
403
  }
398
- if (citations.length > 0) {
399
- const citationBlocks = buildCitationsBlock(citations);
400
- if (citationBlocks.length > 0) await slackClient.chat.postMessage({
401
- channel,
402
- thread_ts: threadTs,
403
- text: "📚 Sources",
404
- blocks: citationBlocks
405
- }).catch((e) => logger.warn({ error: e }, "Failed to post citations"));
406
- }
407
404
  if (thinkingMessageTs) try {
408
405
  await slackClient.chat.delete({
409
406
  channel,