@inkeep/agents-work-apps 0.53.2 → 0.53.4

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 (40) hide show
  1. package/dist/github/mcp/index.js +61 -1
  2. package/dist/github/mcp/utils.d.ts +18 -1
  3. package/dist/github/mcp/utils.js +52 -16
  4. package/dist/github/routes/setup.d.ts +2 -2
  5. package/dist/github/routes/tokenExchange.d.ts +2 -2
  6. package/dist/github/routes/webhooks.d.ts +2 -2
  7. package/dist/slack/dispatcher.js +54 -40
  8. package/dist/slack/i18n/strings.d.ts +6 -5
  9. package/dist/slack/i18n/strings.js +7 -10
  10. package/dist/slack/routes/events.js +1 -1
  11. package/dist/slack/routes/workspaces.js +3 -3
  12. package/dist/slack/services/blocks/index.d.ts +3 -35
  13. package/dist/slack/services/blocks/index.js +5 -42
  14. package/dist/slack/services/client.d.ts +21 -1
  15. package/dist/slack/services/client.js +43 -1
  16. package/dist/slack/services/commands/index.js +42 -104
  17. package/dist/slack/services/events/app-mention.js +8 -31
  18. package/dist/slack/services/events/block-actions.d.ts +1 -11
  19. package/dist/slack/services/events/block-actions.js +6 -49
  20. package/dist/slack/services/events/direct-message.d.ts +11 -0
  21. package/dist/slack/services/events/direct-message.js +148 -0
  22. package/dist/slack/services/events/execution.d.ts +20 -0
  23. package/dist/slack/services/events/execution.js +46 -0
  24. package/dist/slack/services/events/index.d.ts +5 -3
  25. package/dist/slack/services/events/index.js +5 -3
  26. package/dist/slack/services/events/modal-submission.d.ts +1 -21
  27. package/dist/slack/services/events/modal-submission.js +14 -294
  28. package/dist/slack/services/events/streaming.d.ts +1 -1
  29. package/dist/slack/services/events/streaming.js +69 -70
  30. package/dist/slack/services/events/utils.d.ts +2 -14
  31. package/dist/slack/services/events/utils.js +2 -13
  32. package/dist/slack/services/index.d.ts +8 -6
  33. package/dist/slack/services/index.js +9 -7
  34. package/dist/slack/services/modals.d.ts +1 -18
  35. package/dist/slack/services/modals.js +1 -48
  36. package/dist/slack/services/resume-intent.js +43 -3
  37. package/dist/slack/socket-mode.js +1 -1
  38. package/dist/slack/tracer.d.ts +2 -4
  39. package/dist/slack/tracer.js +1 -3
  40. package/package.json +2 -2
@@ -77,6 +77,26 @@ declare function getSlackChannels(client: WebClient, limit?: number): Promise<{
77
77
  isPrivate: boolean;
78
78
  isShared: boolean;
79
79
  }[]>;
80
+ /**
81
+ * Fetch only channels where the bot is a member using the `users.conversations` API.
82
+ *
83
+ * Compared to `getSlackChannels()` (which uses `conversations.list` and returns ALL visible channels),
84
+ * this function returns only channels the bot has been added to. It uses Tier 3 rate limits (50+ req/min)
85
+ * and supports up to 999 items per page, making it significantly more efficient for large workspaces.
86
+ *
87
+ * Use this for the Channel Defaults UI. Keep `getSlackChannels()` for other purposes (e.g., health checks).
88
+ *
89
+ * @param client - Authenticated Slack WebClient
90
+ * @param limit - Maximum number of channels to return. Fetches in pages of up to 999 until the limit is reached or all channels are returned.
91
+ * @returns Array of channel objects with id, name, member count, and privacy status
92
+ */
93
+ declare function getBotMemberChannels(client: WebClient, limit?: number): Promise<{
94
+ id: string | undefined;
95
+ name: string | undefined;
96
+ memberCount: number | undefined;
97
+ isPrivate: boolean;
98
+ isShared: boolean;
99
+ }[]>;
80
100
  /**
81
101
  * Post a message to a Slack channel.
82
102
  *
@@ -121,4 +141,4 @@ declare function checkUserIsChannelMember(client: WebClient, channelId: string,
121
141
  */
122
142
  declare function revokeSlackToken(token: string): Promise<boolean>;
123
143
  //#endregion
124
- export { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
144
+ export { checkUserIsChannelMember, getBotMemberChannels, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
@@ -146,6 +146,48 @@ async function getSlackChannels(client, limit = 200) {
146
146
  limit
147
147
  });
148
148
  }
149
+ function safeNumMembers(ch) {
150
+ const record = ch;
151
+ return typeof record.num_members === "number" ? record.num_members : void 0;
152
+ }
153
+ /**
154
+ * Fetch only channels where the bot is a member using the `users.conversations` API.
155
+ *
156
+ * Compared to `getSlackChannels()` (which uses `conversations.list` and returns ALL visible channels),
157
+ * this function returns only channels the bot has been added to. It uses Tier 3 rate limits (50+ req/min)
158
+ * and supports up to 999 items per page, making it significantly more efficient for large workspaces.
159
+ *
160
+ * Use this for the Channel Defaults UI. Keep `getSlackChannels()` for other purposes (e.g., health checks).
161
+ *
162
+ * @param client - Authenticated Slack WebClient
163
+ * @param limit - Maximum number of channels to return. Fetches in pages of up to 999 until the limit is reached or all channels are returned.
164
+ * @returns Array of channel objects with id, name, member count, and privacy status
165
+ */
166
+ async function getBotMemberChannels(client, limit = 999) {
167
+ return paginateSlack({
168
+ fetchPage: (cursor) => client.users.conversations({
169
+ types: "public_channel,private_channel",
170
+ exclude_archived: true,
171
+ limit: Math.min(limit, 999),
172
+ cursor
173
+ }),
174
+ extractItems: (result) => {
175
+ if (!result.ok) {
176
+ logger.warn({ error: result.error }, "Slack API returned ok: false during bot member channel pagination");
177
+ return [];
178
+ }
179
+ return result.channels ? result.channels.map((ch) => ({
180
+ id: ch.id,
181
+ name: ch.name,
182
+ memberCount: safeNumMembers(ch),
183
+ isPrivate: ch.is_private ?? false,
184
+ isShared: ch.is_shared ?? ch.is_ext_shared ?? false
185
+ })) : [];
186
+ },
187
+ getNextCursor: (result) => result.response_metadata?.next_cursor || void 0,
188
+ limit
189
+ });
190
+ }
149
191
  /**
150
192
  * Post a message to a Slack channel.
151
193
  *
@@ -257,4 +299,4 @@ async function revokeSlackToken(token) {
257
299
  }
258
300
 
259
301
  //#endregion
260
- export { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
302
+ export { checkUserIsChannelMember, getBotMemberChannels, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken };
@@ -2,14 +2,15 @@ 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
+ import { fetchAgentsForProject, fetchProjectsForTenant, generateSlackConversationId, getChannelAgentConfig } from "../events/utils.js";
6
6
  import { resolveEffectiveAgent } from "../agent-resolution.js";
7
7
  import { SlackStrings } from "../../i18n/strings.js";
8
- import { createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
+ import { createAlreadyLinkedMessage, createErrorMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
9
9
  import { getSlackClient } from "../client.js";
10
+ import { executeAgentPublicly } from "../events/execution.js";
10
11
  import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
11
12
  import { buildAgentSelectorModal } from "../modals.js";
12
- import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getInProcessFetch, getWaitUntil, signSlackUserToken } from "@inkeep/agents-core";
13
+ import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getWaitUntil, signSlackUserToken } from "@inkeep/agents-core";
13
14
 
14
15
  //#region src/slack/services/commands/index.ts
15
16
  const DEFAULT_CLIENT_ID = "work-apps-slack";
@@ -232,117 +233,54 @@ async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId,
232
233
  response_type: "ephemeral",
233
234
  ...createErrorMessage("No default agent configured. Ask your admin to set a workspace default in the dashboard.")
234
235
  };
235
- const questionWork = executeAgentInBackground(payload, existingLink, {
236
- id: resolvedAgent.agentId,
237
- name: resolvedAgent.agentName || null,
238
- projectId: resolvedAgent.projectId
239
- }, question, userTenantId, {
236
+ const slackClient = getSlackClient(botToken);
237
+ const slackUserToken = await signSlackUserToken({
238
+ inkeepUserId: existingLink.inkeepUserId,
239
+ tenantId: userTenantId,
240
+ slackTeamId: payload.teamId,
241
+ slackUserId: payload.userId,
242
+ slackEnterpriseId: payload.enterpriseId,
240
243
  slackAuthorized: resolvedAgent.grantAccessToMembers,
241
244
  slackAuthSource: resolvedAgent.source === "none" ? void 0 : resolvedAgent.source,
242
245
  slackChannelId: payload.channelId,
243
246
  slackAuthorizedProjectId: resolvedAgent.projectId
244
- }).catch((error) => {
247
+ });
248
+ const now = Date.now();
249
+ const messageTs = `${Math.floor(now / 1e3)}.${String(now % 1e3).padStart(3, "0")}000`;
250
+ const conversationId = generateSlackConversationId({
251
+ teamId: payload.teamId,
252
+ messageTs,
253
+ agentId: resolvedAgent.agentId
254
+ });
255
+ const questionWork = executeAgentPublicly({
256
+ slackClient,
257
+ channel: payload.channelId,
258
+ slackUserId: payload.userId,
259
+ teamId: payload.teamId,
260
+ jwtToken: slackUserToken,
261
+ projectId: resolvedAgent.projectId,
262
+ agentId: resolvedAgent.agentId,
263
+ agentName: resolvedAgent.agentName || resolvedAgent.agentId,
264
+ question,
265
+ conversationId
266
+ }).catch(async (error) => {
245
267
  logger.error({ error }, "Background execution promise rejected");
246
- }).finally(() => flushTraces());
247
- const waitUntil = await getWaitUntil();
248
- if (waitUntil) waitUntil(questionWork);
249
- return {};
250
- }
251
- async function executeAgentInBackground(payload, existingLink, targetAgent, question, tenantId, channelAuth) {
252
- try {
253
- const slackUserToken = await signSlackUserToken({
254
- inkeepUserId: existingLink.inkeepUserId,
255
- tenantId,
256
- slackTeamId: payload.teamId,
257
- slackUserId: payload.userId,
258
- slackEnterpriseId: payload.enterpriseId,
259
- ...channelAuth
260
- });
261
- const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
262
- const controller = new AbortController();
263
- const timeout = setTimeout(() => controller.abort(), 3e4);
264
- let response;
265
- try {
266
- response = await getInProcessFetch()(`${apiBaseUrl}/run/api/chat`, {
268
+ if (payload.responseUrl) try {
269
+ await fetch(payload.responseUrl, {
267
270
  method: "POST",
268
- headers: {
269
- "Content-Type": "application/json",
270
- Authorization: `Bearer ${slackUserToken}`,
271
- "x-inkeep-project-id": targetAgent.projectId,
272
- "x-inkeep-agent-id": targetAgent.id
273
- },
271
+ headers: { "Content-Type": "application/json" },
274
272
  body: JSON.stringify({
275
- messages: [{
276
- role: "user",
277
- content: question
278
- }],
279
- stream: false
280
- }),
281
- signal: controller.signal
282
- });
283
- } catch (error) {
284
- clearTimeout(timeout);
285
- if (error.name === "AbortError") {
286
- logger.warn({
287
- teamId: payload.teamId,
288
- timeoutMs: 3e4
289
- }, "Background agent execution timed out");
290
- await sendResponseUrlMessage(payload.responseUrl, {
291
273
  response_type: "ephemeral",
292
- text: "Request timed out. Please try again."
293
- });
294
- return;
295
- }
296
- throw error;
297
- } finally {
298
- clearTimeout(timeout);
299
- }
300
- if (!response.ok) {
301
- const errorText = await response.text();
302
- logger.error({
303
- status: response.status,
304
- error: errorText,
305
- agentId: targetAgent.id,
306
- projectId: targetAgent.projectId
307
- }, "Run API call failed");
308
- const apiMessage = extractApiErrorMessage(errorText);
309
- const errorMessage = apiMessage ? `*Error.* ${apiMessage}` : `Failed to run agent: ${response.status} ${response.statusText}`;
310
- await sendResponseUrlMessage(payload.responseUrl, {
311
- response_type: "ephemeral",
312
- text: errorMessage
313
- });
314
- } else {
315
- const result = await response.json();
316
- const assistantMessage = result.choices?.[0]?.message?.content || result.message?.content || "No response received";
317
- logger.info({
318
- slackUserId: payload.userId,
319
- agentId: targetAgent.id,
320
- projectId: targetAgent.projectId,
321
- tenantId
322
- }, "Agent execution completed via Slack");
323
- const contextBlock = createContextBlock({ agentName: targetAgent.name || targetAgent.id });
324
- await sendResponseUrlMessage(payload.responseUrl, {
325
- response_type: "in_channel",
326
- text: assistantMessage,
327
- blocks: [{
328
- type: "section",
329
- text: {
330
- type: "mrkdwn",
331
- text: assistantMessage
332
- }
333
- }, contextBlock]
274
+ text: SlackStrings.errors.generic
275
+ })
334
276
  });
277
+ } catch (e) {
278
+ logger.warn({ e }, "Failed to send error via response_url");
335
279
  }
336
- } catch (error) {
337
- logger.error({
338
- error,
339
- slackUserId: payload.userId
340
- }, "Background agent execution failed");
341
- await sendResponseUrlMessage(payload.responseUrl, {
342
- response_type: "ephemeral",
343
- text: "An error occurred while running the agent. Please try again."
344
- });
345
- }
280
+ }).finally(() => flushTraces());
281
+ const waitUntil = await getWaitUntil();
282
+ if (waitUntil) waitUntil(questionWork);
283
+ return {};
346
284
  }
347
285
  async function handleCommand(payload) {
348
286
  const text = payload.text.trim();
@@ -5,9 +5,9 @@ import { checkIfBotThread, classifyError, findCachedUserMapping, formatAttachmen
5
5
  import { resolveEffectiveAgent } from "../agent-resolution.js";
6
6
  import { SlackStrings } from "../../i18n/strings.js";
7
7
  import { getSlackChannelInfo, getSlackClient, getSlackUserInfo, postMessageInThread } from "../client.js";
8
- import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
9
8
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
10
- import { streamAgentResponse } from "./streaming.js";
9
+ import { executeAgentPublicly } from "./execution.js";
10
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
11
11
  import { signSlackUserToken } from "@inkeep/agents-core";
12
12
 
13
13
  //#region src/slack/services/events/app-mention.ts
@@ -72,7 +72,6 @@ async function handleAppMention(params) {
72
72
  const replyThreadTs = threadTs || messageTs;
73
73
  const isInThread = Boolean(threadTs && threadTs !== messageTs);
74
74
  const hasQuery = Boolean(text && text.trim().length > 0);
75
- let thinkingMessageTs;
76
75
  try {
77
76
  const { result: [agentConfig, existingLink] } = await timedOp(Promise.all([resolveEffectiveAgent({
78
77
  tenantId,
@@ -205,16 +204,9 @@ async function handleAppMention(params) {
205
204
  slackChannelId: channel,
206
205
  slackAuthorizedProjectId: agentConfig?.projectId
207
206
  });
208
- thinkingMessageTs = (await slackClient.chat.postMessage({
209
- channel,
210
- thread_ts: threadTs,
211
- text: SlackStrings.status.readingThread(agentDisplayName)
212
- })).ts || void 0;
213
207
  const conversationId$1 = generateSlackConversationId({
214
208
  teamId,
215
- threadTs,
216
- channel,
217
- isDM: false,
209
+ messageTs,
218
210
  agentId: agentConfig.agentId
219
211
  });
220
212
  span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId$1);
@@ -235,18 +227,17 @@ Respond naturally as if you're joining the conversation to help.`;
235
227
  agentId: agentConfig.agentId,
236
228
  conversationId: conversationId$1
237
229
  }, "Auto-executing agent with thread context");
238
- await streamAgentResponse({
230
+ await executeAgentPublicly({
239
231
  slackClient,
240
232
  channel,
241
233
  threadTs,
242
- thinkingMessageTs: thinkingMessageTs || "",
243
234
  slackUserId,
244
235
  teamId,
245
236
  jwtToken: slackUserToken$1,
246
237
  projectId: agentConfig.projectId,
247
238
  agentId: agentConfig.agentId,
248
- question: threadQuery,
249
239
  agentName: agentDisplayName,
240
+ question: threadQuery,
250
241
  conversationId: conversationId$1
251
242
  });
252
243
  span.end();
@@ -292,16 +283,9 @@ Respond naturally as if you're joining the conversation to help.`;
292
283
  slackChannelId: channel,
293
284
  slackAuthorizedProjectId: agentConfig?.projectId
294
285
  });
295
- thinkingMessageTs = (await slackClient.chat.postMessage({
296
- channel,
297
- thread_ts: replyThreadTs,
298
- text: SlackStrings.status.thinking(agentDisplayName)
299
- })).ts || void 0;
300
286
  const conversationId = generateSlackConversationId({
301
287
  teamId,
302
- threadTs: replyThreadTs,
303
- channel,
304
- isDM: false,
288
+ messageTs,
305
289
  agentId: agentConfig.agentId
306
290
  });
307
291
  span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
@@ -313,18 +297,17 @@ Respond naturally as if you're joining the conversation to help.`;
313
297
  totalPreExecMs,
314
298
  dispatchDelayMs
315
299
  }, "Executing agent");
316
- await streamAgentResponse({
300
+ await executeAgentPublicly({
317
301
  slackClient,
318
302
  channel,
319
303
  threadTs: replyThreadTs,
320
- thinkingMessageTs: thinkingMessageTs || "",
321
304
  slackUserId,
322
305
  teamId,
323
306
  jwtToken: slackUserToken,
324
307
  projectId: agentConfig.projectId,
325
308
  agentId: agentConfig.agentId,
326
- question: queryText,
327
309
  agentName: agentDisplayName,
310
+ question: queryText,
328
311
  conversationId
329
312
  });
330
313
  span.end();
@@ -336,12 +319,6 @@ Respond naturally as if you're joining the conversation to help.`;
336
319
  teamId
337
320
  }, "Failed in app mention handler");
338
321
  if (error instanceof Error) setSpanWithError(span, error);
339
- if (thinkingMessageTs) try {
340
- await slackClient.chat.delete({
341
- channel,
342
- ts: thinkingMessageTs
343
- });
344
- } catch {}
345
322
  const userMessage = getUserFriendlyErrorMessage(classifyError(error));
346
323
  try {
347
324
  await slackClient.chat.postEphemeral({
@@ -23,16 +23,6 @@ declare function handleOpenAgentSelectorModal(params: {
23
23
  teamId: string;
24
24
  responseUrl: string;
25
25
  }): Promise<void>;
26
- /**
27
- * Handle "Follow Up" button click.
28
- * Opens a prompt-only modal that carries the conversationId for multi-turn context.
29
- */
30
- declare function handleOpenFollowUpModal(params: {
31
- triggerId: string;
32
- actionValue: string;
33
- teamId: string;
34
- responseUrl?: string;
35
- }): Promise<void>;
36
26
  /**
37
27
  * Handle message shortcut (context menu action on a message)
38
28
  * Opens a modal with the message content pre-filled as context
@@ -48,4 +38,4 @@ declare function handleMessageShortcut(params: {
48
38
  responseUrl?: string;
49
39
  }): Promise<void>;
50
40
  //#endregion
51
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
41
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleToolApproval };
@@ -5,8 +5,8 @@ import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, g
5
5
  import { SlackStrings } from "../../i18n/strings.js";
6
6
  import { ToolApprovalButtonValueSchema, buildToolApprovalDoneBlocks } from "../blocks/index.js";
7
7
  import { getSlackClient } from "../client.js";
8
- import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "../modals.js";
9
8
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
9
+ import { buildAgentSelectorModal, buildMessageShortcutModal } from "../modals.js";
10
10
  import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
11
11
 
12
12
  //#region src/slack/services/events/block-actions.ts
@@ -36,11 +36,12 @@ async function handleToolApproval(params) {
36
36
  }
37
37
  const tenantId = workspaceConnection.tenantId;
38
38
  const slackClient = getSlackClient(workspaceConnection.botToken);
39
+ const approvalThreadParam = buttonValue.threadTs ? { thread_ts: buttonValue.threadTs } : {};
39
40
  if (slackUserId !== buttonValue.slackUserId) {
40
41
  await slackClient.chat.postEphemeral({
41
42
  channel: buttonValue.channel,
42
43
  user: slackUserId,
43
- thread_ts: buttonValue.threadTs,
44
+ ...approvalThreadParam,
44
45
  text: "Only the user who started this conversation can approve or deny this action."
45
46
  }).catch((e) => logger.warn({ error: e }, "Failed to send ownership error notification"));
46
47
  span.end();
@@ -51,7 +52,7 @@ async function handleToolApproval(params) {
51
52
  await slackClient.chat.postEphemeral({
52
53
  channel: buttonValue.channel,
53
54
  user: slackUserId,
54
- thread_ts: buttonValue.threadTs,
55
+ ...approvalThreadParam,
55
56
  text: "You need to link your Inkeep account first. Use `/inkeep link`."
56
57
  }).catch((e) => logger.warn({ error: e }, "Failed to send not-linked notification"));
57
58
  span.end();
@@ -99,7 +100,7 @@ async function handleToolApproval(params) {
99
100
  await slackClient.chat.postEphemeral({
100
101
  channel: buttonValue.channel,
101
102
  user: slackUserId,
102
- thread_ts: buttonValue.threadTs,
103
+ ...approvalThreadParam,
103
104
  text: `Failed to ${approved ? "approve" : "deny"} \`${toolName}\`. Please try again.`
104
105
  }).catch((e) => logger.warn({ error: e }, "Failed to send approval error notification"));
105
106
  span.end();
@@ -238,50 +239,6 @@ async function handleOpenAgentSelectorModal(params) {
238
239
  });
239
240
  }
240
241
  /**
241
- * Handle "Follow Up" button click.
242
- * Opens a prompt-only modal that carries the conversationId for multi-turn context.
243
- */
244
- async function handleOpenFollowUpModal(params) {
245
- return tracer.startActiveSpan(SLACK_SPAN_NAMES.OPEN_FOLLOW_UP_MODAL, async (span) => {
246
- const { triggerId, actionValue, teamId, responseUrl } = params;
247
- span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
248
- try {
249
- const metadata = JSON.parse(actionValue);
250
- span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, metadata.conversationId || "");
251
- span.setAttribute(SLACK_SPAN_KEYS.AGENT_ID, metadata.agentId || "");
252
- const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
253
- if (!workspaceConnection?.botToken) {
254
- logger.error({ teamId }, "No bot token for follow-up modal");
255
- span.end();
256
- return;
257
- }
258
- const slackClient = getSlackClient(workspaceConnection.botToken);
259
- const modal = buildFollowUpModal(metadata);
260
- await slackClient.views.open({
261
- trigger_id: triggerId,
262
- view: modal
263
- });
264
- logger.info({
265
- teamId,
266
- conversationId: metadata.conversationId,
267
- agentId: metadata.agentId
268
- }, "Opened follow-up modal");
269
- span.end();
270
- } catch (error) {
271
- if (error instanceof Error) setSpanWithError(span, error);
272
- logger.error({
273
- error,
274
- teamId
275
- }, "Failed to open follow-up modal");
276
- if (responseUrl) await sendResponseUrlMessage(responseUrl, {
277
- text: "Failed to open follow-up dialog. Please try again.",
278
- response_type: "ephemeral"
279
- }).catch((e) => logger.warn({ error: e }, "Failed to send follow-up error notification"));
280
- span.end();
281
- }
282
- });
283
- }
284
- /**
285
242
  * Handle message shortcut (context menu action on a message)
286
243
  * Opens a modal with the message content pre-filled as context
287
244
  */
@@ -386,4 +343,4 @@ async function handleMessageShortcut(params) {
386
343
  }
387
344
 
388
345
  //#endregion
389
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
346
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleToolApproval };
@@ -0,0 +1,11 @@
1
+ //#region src/slack/services/events/direct-message.d.ts
2
+ declare function handleDirectMessage(params: {
3
+ slackUserId: string;
4
+ channel: string;
5
+ text: string;
6
+ threadTs?: string;
7
+ messageTs: string;
8
+ teamId: string;
9
+ }): Promise<void>;
10
+ //#endregion
11
+ export { handleDirectMessage };
@@ -0,0 +1,148 @@
1
+ import { getLogger } from "../../../logger.js";
2
+ import { findWorkspaceConnectionByTeamId } from "../nango.js";
3
+ import { classifyError, findCachedUserMapping, generateSlackConversationId, getThreadContext, getUserFriendlyErrorMessage } from "./utils.js";
4
+ import { SlackStrings } from "../../i18n/strings.js";
5
+ import { getSlackClient } from "../client.js";
6
+ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
7
+ import { executeAgentPublicly } from "./execution.js";
8
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
9
+ import { signSlackUserToken } from "@inkeep/agents-core";
10
+
11
+ //#region src/slack/services/events/direct-message.ts
12
+ const logger = getLogger("slack-direct-message");
13
+ async function handleDirectMessage(params) {
14
+ return tracer.startActiveSpan(SLACK_SPAN_NAMES.DIRECT_MESSAGE, async (span) => {
15
+ const { slackUserId, channel, text, threadTs, messageTs, teamId } = params;
16
+ span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
17
+ span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channel);
18
+ span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
19
+ span.setAttribute(SLACK_SPAN_KEYS.MESSAGE_TS, messageTs);
20
+ if (threadTs) span.setAttribute(SLACK_SPAN_KEYS.THREAD_TS, threadTs);
21
+ logger.info({
22
+ slackUserId,
23
+ channel,
24
+ teamId
25
+ }, "Handling direct message");
26
+ const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
27
+ if (!workspaceConnection?.botToken) {
28
+ logger.error({ teamId }, "No bot token available — cannot respond to DM");
29
+ span.end();
30
+ return;
31
+ }
32
+ const { botToken, tenantId } = workspaceConnection;
33
+ if (!tenantId) {
34
+ logger.error({ teamId }, "Workspace connection has no tenantId");
35
+ span.end();
36
+ return;
37
+ }
38
+ span.setAttribute(SLACK_SPAN_KEYS.TENANT_ID, tenantId);
39
+ const slackClient = getSlackClient(botToken);
40
+ const replyThreadTs = threadTs || messageTs;
41
+ const isInThread = Boolean(threadTs && threadTs !== messageTs);
42
+ try {
43
+ const defaultAgent = workspaceConnection.defaultAgent;
44
+ if (!defaultAgent?.agentId || !defaultAgent?.projectId) {
45
+ logger.info({ teamId }, "No default agent configured — sending hint in DM");
46
+ await slackClient.chat.postMessage({
47
+ channel,
48
+ thread_ts: replyThreadTs,
49
+ text: SlackStrings.errors.noAgentConfigured
50
+ });
51
+ span.end();
52
+ return;
53
+ }
54
+ span.setAttribute(SLACK_SPAN_KEYS.AGENT_ID, defaultAgent.agentId);
55
+ span.setAttribute(SLACK_SPAN_KEYS.PROJECT_ID, defaultAgent.projectId);
56
+ const agentDisplayName = defaultAgent.agentName || defaultAgent.agentId;
57
+ const existingLink = await findCachedUserMapping(tenantId, slackUserId, teamId);
58
+ if (!existingLink) {
59
+ logger.info({
60
+ slackUserId,
61
+ teamId
62
+ }, "User not linked — sending link prompt in DM");
63
+ const message = buildLinkPromptMessage(await resolveUnlinkedUserAction({
64
+ tenantId,
65
+ teamId,
66
+ slackUserId,
67
+ botToken,
68
+ intent: {
69
+ entryPoint: "dm",
70
+ question: text.slice(0, 2e3),
71
+ channelId: channel,
72
+ messageTs,
73
+ agentId: defaultAgent.agentId,
74
+ projectId: defaultAgent.projectId
75
+ }
76
+ }));
77
+ await slackClient.chat.postMessage({
78
+ channel,
79
+ thread_ts: replyThreadTs,
80
+ text: SlackStrings.linkPrompt.intro,
81
+ blocks: message.blocks
82
+ });
83
+ span.end();
84
+ return;
85
+ }
86
+ let queryText = text;
87
+ if (isInThread && threadTs) {
88
+ const contextMessages = await getThreadContext(slackClient, channel, threadTs);
89
+ if (contextMessages) queryText = text ? `The following is thread context from a DM conversation:\n\n<slack_thread_context>\n${contextMessages}\n</slack_thread_context>\n\nUser message: ${text}` : contextMessages;
90
+ }
91
+ const slackUserToken = await signSlackUserToken({
92
+ inkeepUserId: existingLink.inkeepUserId,
93
+ tenantId,
94
+ slackTeamId: teamId,
95
+ slackUserId,
96
+ slackAuthorized: false
97
+ });
98
+ const conversationId = generateSlackConversationId({
99
+ teamId,
100
+ messageTs,
101
+ agentId: defaultAgent.agentId,
102
+ isDM: true
103
+ });
104
+ span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
105
+ logger.info({
106
+ agentId: defaultAgent.agentId,
107
+ projectId: defaultAgent.projectId,
108
+ conversationId
109
+ }, "Executing agent for DM");
110
+ await executeAgentPublicly({
111
+ slackClient,
112
+ channel,
113
+ threadTs: replyThreadTs,
114
+ slackUserId,
115
+ teamId,
116
+ jwtToken: slackUserToken,
117
+ projectId: defaultAgent.projectId,
118
+ agentId: defaultAgent.agentId,
119
+ agentName: agentDisplayName,
120
+ question: queryText,
121
+ conversationId
122
+ });
123
+ span.end();
124
+ } catch (error) {
125
+ const errorMsg = error instanceof Error ? error.message : String(error);
126
+ logger.error({
127
+ errorMessage: errorMsg,
128
+ channel,
129
+ teamId
130
+ }, "Failed in DM handler");
131
+ if (error instanceof Error) setSpanWithError(span, error);
132
+ const userMessage = getUserFriendlyErrorMessage(classifyError(error));
133
+ try {
134
+ await slackClient.chat.postMessage({
135
+ channel,
136
+ thread_ts: replyThreadTs,
137
+ text: userMessage
138
+ });
139
+ } catch (postError) {
140
+ logger.error({ error: postError }, "Failed to post DM error message");
141
+ }
142
+ span.end();
143
+ }
144
+ });
145
+ }
146
+
147
+ //#endregion
148
+ export { handleDirectMessage };
@@ -0,0 +1,20 @@
1
+ import { getSlackClient } from "../client.js";
2
+ import { StreamResult } from "./streaming.js";
3
+
4
+ //#region src/slack/services/events/execution.d.ts
5
+ interface PublicExecutionParams {
6
+ slackClient: ReturnType<typeof getSlackClient>;
7
+ channel: string;
8
+ threadTs?: string;
9
+ slackUserId: string;
10
+ teamId: string;
11
+ jwtToken: string;
12
+ projectId: string;
13
+ agentId: string;
14
+ agentName: string;
15
+ question: string;
16
+ conversationId: string;
17
+ }
18
+ declare function executeAgentPublicly(params: PublicExecutionParams): Promise<StreamResult>;
19
+ //#endregion
20
+ export { PublicExecutionParams, executeAgentPublicly };