@inkeep/agents-work-apps 0.50.6 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/github/mcp/auth.d.ts +2 -2
  2. package/dist/github/mcp/index.d.ts +2 -2
  3. package/dist/github/mcp/index.js +60 -1
  4. package/dist/github/mcp/schemas.d.ts +1 -1
  5. package/dist/github/mcp/utils.d.ts +10 -1
  6. package/dist/github/mcp/utils.js +87 -1
  7. package/dist/github/routes/setup.d.ts +2 -2
  8. package/dist/github/routes/tokenExchange.d.ts +2 -2
  9. package/dist/github/routes/webhooks.d.ts +2 -2
  10. package/dist/slack/dispatcher.js +24 -1
  11. package/dist/slack/i18n/strings.d.ts +1 -0
  12. package/dist/slack/i18n/strings.js +1 -0
  13. package/dist/slack/routes/events.js +2 -2
  14. package/dist/slack/routes/oauth.js +3 -4
  15. package/dist/slack/routes/users.js +13 -11
  16. package/dist/slack/routes/workspaces.js +85 -1
  17. package/dist/slack/services/blocks/index.d.ts +81 -1
  18. package/dist/slack/services/blocks/index.js +238 -19
  19. package/dist/slack/services/commands/index.d.ts +1 -1
  20. package/dist/slack/services/commands/index.js +98 -4
  21. package/dist/slack/services/events/app-mention.js +2 -2
  22. package/dist/slack/services/events/block-actions.d.ts +12 -1
  23. package/dist/slack/services/events/block-actions.js +126 -2
  24. package/dist/slack/services/events/index.d.ts +2 -2
  25. package/dist/slack/services/events/index.js +2 -2
  26. package/dist/slack/services/events/streaming.d.ts +1 -1
  27. package/dist/slack/services/events/streaming.js +203 -7
  28. package/dist/slack/services/events/utils.d.ts +2 -2
  29. package/dist/slack/services/events/utils.js +5 -2
  30. package/dist/slack/services/index.d.ts +3 -3
  31. package/dist/slack/services/index.js +3 -3
  32. package/dist/slack/services/nango.js +1 -23
  33. package/dist/slack/tracer.d.ts +1 -0
  34. package/dist/slack/tracer.js +2 -1
  35. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { SlackStrings } from "../../i18n/strings.js";
2
+ import { z } from "zod";
2
3
  import { Blocks, Elements, Md, Message } from "slack-block-builder";
3
4
 
4
5
  //#region src/slack/services/blocks/index.ts
@@ -34,24 +35,14 @@ function buildFollowUpButton(params) {
34
35
  * Shows the user's message, a divider, the agent response, context, and a Follow Up button.
35
36
  */
36
37
  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
- }
38
+ const { responseText, agentName, isError, followUpParams } = params;
39
+ const blocks = [{
40
+ type: "section",
41
+ text: {
42
+ type: "mrkdwn",
43
+ text: responseText
53
44
  }
54
- ];
45
+ }];
55
46
  if (!isError) {
56
47
  const contextBlock = createContextBlock({
57
48
  agentName,
@@ -66,7 +57,7 @@ function buildConversationResponseBlocks(params) {
66
57
  return blocks;
67
58
  }
68
59
  function createUpdatedHelpMessage() {
69
- 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), Blocks.Divider(), Blocks.Section().text(SlackStrings.help.docsLink)).buildToObject();
60
+ return Message().blocks(Blocks.Header().text(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), Blocks.Divider(), Blocks.Context().elements(SlackStrings.help.docsLink)).buildToObject();
70
61
  }
71
62
  function createAlreadyLinkedMessage(email, linkedAt, dashboardUrl) {
72
63
  return Message().blocks(Blocks.Section().text(Md.bold("Already linked") + "\n\nYour Slack account is connected to Inkeep.\n\n" + Md.bold("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();
@@ -84,9 +75,237 @@ function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
84
75
  else agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
85
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();
86
77
  }
78
+ const ToolApprovalButtonValueSchema = z.object({
79
+ toolCallId: z.string(),
80
+ conversationId: z.string(),
81
+ projectId: z.string(),
82
+ agentId: z.string(),
83
+ slackUserId: z.string(),
84
+ channel: z.string(),
85
+ threadTs: z.string(),
86
+ toolName: z.string()
87
+ });
88
+ function buildToolApprovalBlocks(params) {
89
+ const { toolName, input, buttonValue } = params;
90
+ const blocks = [{
91
+ type: "section",
92
+ text: {
93
+ type: "mrkdwn",
94
+ text: `*Approval required - \`${toolName}\`*`
95
+ }
96
+ }];
97
+ if (input && Object.keys(input).length > 0) {
98
+ const fields = Object.entries(input).slice(0, 10).map(([k, v]) => {
99
+ const val = typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
100
+ return {
101
+ type: "mrkdwn",
102
+ text: `*${k}:*\n${val.length > 80 ? `${val.slice(0, 80)}…` : val}`
103
+ };
104
+ });
105
+ blocks.push({
106
+ type: "section",
107
+ fields
108
+ });
109
+ }
110
+ blocks.push({
111
+ type: "actions",
112
+ elements: [{
113
+ type: "button",
114
+ text: {
115
+ type: "plain_text",
116
+ text: "Approve",
117
+ emoji: true
118
+ },
119
+ style: "primary",
120
+ action_id: "tool_approval_approve",
121
+ value: buttonValue
122
+ }, {
123
+ type: "button",
124
+ text: {
125
+ type: "plain_text",
126
+ text: "Deny",
127
+ emoji: true
128
+ },
129
+ style: "danger",
130
+ action_id: "tool_approval_deny",
131
+ value: buttonValue
132
+ }]
133
+ });
134
+ return blocks;
135
+ }
136
+ function buildToolApprovalDoneBlocks(params) {
137
+ const { toolName, approved, actorUserId } = params;
138
+ return [{
139
+ type: "context",
140
+ elements: [{
141
+ type: "mrkdwn",
142
+ text: approved ? `✅ Approved \`${toolName}\` · <@${actorUserId}>` : `❌ Denied \`${toolName}\` · <@${actorUserId}>`
143
+ }]
144
+ }];
145
+ }
146
+ function buildToolApprovalExpiredBlocks(params) {
147
+ return [{
148
+ type: "context",
149
+ elements: [{
150
+ type: "mrkdwn",
151
+ text: `⏱️ Expired · \`${params.toolName}\``
152
+ }]
153
+ }];
154
+ }
155
+ function buildToolOutputErrorBlock(toolName, errorText) {
156
+ return {
157
+ type: "context",
158
+ elements: [{
159
+ type: "mrkdwn",
160
+ text: `⚠️ *${toolName}* · failed: ${errorText.length > 100 ? `${errorText.slice(0, 100)}…` : errorText}`
161
+ }]
162
+ };
163
+ }
164
+ function buildSummaryBreadcrumbBlock(labels) {
165
+ return {
166
+ type: "context",
167
+ elements: [{
168
+ type: "mrkdwn",
169
+ text: labels.join(" → ")
170
+ }]
171
+ };
172
+ }
173
+ function isFlatRecord(obj) {
174
+ return Object.values(obj).every((v) => v === null || [
175
+ "string",
176
+ "number",
177
+ "boolean"
178
+ ].includes(typeof v));
179
+ }
180
+ function findSourcesArray(data) {
181
+ for (const value of Object.values(data)) if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && value[0] !== null && ("url" in value[0] || "href" in value[0])) return value;
182
+ return null;
183
+ }
184
+ function buildDataComponentBlocks(component) {
185
+ const { data } = component;
186
+ const componentType = typeof data.type === "string" ? data.type : void 0;
187
+ const blocks = [{
188
+ type: "header",
189
+ text: {
190
+ type: "plain_text",
191
+ text: `📊 ${componentType || "Data Component"}`,
192
+ emoji: true
193
+ }
194
+ }];
195
+ const payload = Object.fromEntries(Object.entries(data).filter(([k]) => k !== "type"));
196
+ let overflowJson;
197
+ if (Object.keys(payload).length > 0) if (isFlatRecord(payload)) {
198
+ const fields = Object.entries(payload).slice(0, 10).map(([k, v]) => {
199
+ const val = String(v ?? "");
200
+ return {
201
+ type: "mrkdwn",
202
+ text: `*${k}*\n${val.length > 80 ? `${val.slice(0, 80)}…` : val}`
203
+ };
204
+ });
205
+ blocks.push({
206
+ type: "section",
207
+ fields
208
+ });
209
+ } else {
210
+ const jsonStr = JSON.stringify(payload, null, 2);
211
+ if (jsonStr.length > 2900) overflowJson = jsonStr;
212
+ else blocks.push({
213
+ type: "section",
214
+ text: {
215
+ type: "mrkdwn",
216
+ text: `\`\`\`json\n${jsonStr}\n\`\`\``
217
+ }
218
+ });
219
+ }
220
+ if (componentType) blocks.push({
221
+ type: "context",
222
+ elements: [{
223
+ type: "mrkdwn",
224
+ text: `data component · type: ${componentType}`
225
+ }]
226
+ });
227
+ return {
228
+ blocks,
229
+ overflowJson,
230
+ componentType
231
+ };
232
+ }
233
+ function buildDataArtifactBlocks(artifact) {
234
+ const { data } = artifact;
235
+ const sourcesArray = findSourcesArray(data);
236
+ if (sourcesArray && sourcesArray.length > 0) {
237
+ const MAX_SOURCES = 10;
238
+ const lines = sourcesArray.slice(0, MAX_SOURCES).map((s) => {
239
+ const url = s.url || s.href;
240
+ const title = s.title || s.name || url;
241
+ return url ? `• <${url}|${title}>` : null;
242
+ }).filter((l) => l !== null);
243
+ if (lines.length > 0) {
244
+ const suffix = sourcesArray.length > MAX_SOURCES ? `\n_and ${sourcesArray.length - MAX_SOURCES} more_` : "";
245
+ return { blocks: [{
246
+ type: "section",
247
+ text: {
248
+ type: "mrkdwn",
249
+ text: `📚 *Sources*\n${lines.join("\n")}${suffix}`
250
+ }
251
+ }] };
252
+ }
253
+ }
254
+ const artifactType = typeof data.type === "string" ? data.type : void 0;
255
+ const name = typeof data.name === "string" && data.name ? data.name : artifactType || "Artifact";
256
+ const blocks = [{
257
+ type: "header",
258
+ text: {
259
+ type: "plain_text",
260
+ text: `📄 ${name}`,
261
+ emoji: true
262
+ }
263
+ }];
264
+ if (artifactType) blocks.push({
265
+ type: "context",
266
+ elements: [{
267
+ type: "mrkdwn",
268
+ text: `type: ${artifactType}`
269
+ }]
270
+ });
271
+ let overflowContent;
272
+ if (typeof data.description === "string" && data.description) if (data.description.length > 2900) overflowContent = data.description;
273
+ else blocks.push({
274
+ type: "section",
275
+ text: {
276
+ type: "mrkdwn",
277
+ text: data.description
278
+ }
279
+ });
280
+ return {
281
+ blocks,
282
+ overflowContent,
283
+ artifactName: name
284
+ };
285
+ }
286
+ function buildCitationsBlock(citations) {
287
+ const MAX_CITATIONS = 10;
288
+ const lines = citations.slice(0, MAX_CITATIONS).map((c) => {
289
+ const url = c.url;
290
+ const title = c.title || url;
291
+ return url ? `• <${url}|${title}>` : null;
292
+ }).filter((l) => l !== null);
293
+ if (lines.length === 0) return [];
294
+ const suffix = citations.length > MAX_CITATIONS ? `\n_and ${citations.length - MAX_CITATIONS} more_` : "";
295
+ return [{
296
+ type: "section",
297
+ text: {
298
+ type: "mrkdwn",
299
+ text: `📚 *Sources*\n${lines.join("\n")}${suffix}`
300
+ }
301
+ }];
302
+ }
87
303
  function createJwtLinkMessage(linkUrl, expiresInMinutes) {
88
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();
89
305
  }
306
+ function createCreateInkeepAccountMessage(acceptUrl, expiresInMinutes) {
307
+ 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
+ }
90
309
 
91
310
  //#endregion
92
- export { buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
311
+ export { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -2,7 +2,7 @@ import { SlackWorkspaceConnection } from "../nango.js";
2
2
  import { SlackCommandPayload, SlackCommandResponse } from "../types.js";
3
3
 
4
4
  //#region src/slack/services/commands/index.d.ts
5
- declare function handleLinkCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
5
+ declare function handleLinkCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string, botToken?: string): Promise<SlackCommandResponse>;
6
6
  declare function handleUnlinkCommand(payload: SlackCommandPayload, tenantId: string): Promise<SlackCommandResponse>;
7
7
  declare function handleStatusCommand(payload: SlackCommandPayload, dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
8
8
  declare function handleHelpCommand(): Promise<SlackCommandResponse>;
@@ -4,22 +4,116 @@ import runDbClient_default from "../../../db/runDbClient.js";
4
4
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
5
5
  import { resolveEffectiveAgent } from "../agent-resolution.js";
6
6
  import { SlackStrings } from "../../i18n/strings.js";
7
- import { createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
7
+ import { createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
8
  import { getSlackClient } from "../client.js";
9
9
  import { extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
10
10
  import { buildAgentSelectorModal } from "../modals.js";
11
- import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getInProcessFetch, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
11
+ import { createInvitationInDb, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, findWorkAppSlackWorkspaceByTeamId, flushTraces, getInProcessFetch, getOrganizationMemberByEmail, getPendingInvitationsByEmail, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
12
12
 
13
13
  //#region src/slack/services/commands/index.ts
14
14
  const DEFAULT_CLIENT_ID = "work-apps-slack";
15
15
  const LINK_CODE_TTL_MINUTES = 10;
16
16
  const logger = getLogger("slack-commands");
17
- async function handleLinkCommand(payload, dashboardUrl, tenantId) {
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
+ async function handleLinkCommand(payload, dashboardUrl, tenantId, botToken) {
18
89
  const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
19
90
  if (existingLink) return {
20
91
  response_type: "ephemeral",
21
92
  ...createAlreadyLinkedMessage(existingLink.slackEmail || existingLink.slackUsername || "Unknown", existingLink.linkedAt, dashboardUrl)
22
93
  };
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({
99
+ tenantId,
100
+ slackTeamId: payload.teamId,
101
+ slackUserId: payload.userId,
102
+ slackEnterpriseId: payload.enterpriseId,
103
+ 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
+ }
23
117
  try {
24
118
  const linkToken = await signSlackLinkToken({
25
119
  tenantId,
@@ -363,7 +457,7 @@ async function handleCommand(payload) {
363
457
  }, "Slack command received");
364
458
  switch (subcommand) {
365
459
  case "link":
366
- case "connect": return handleLinkCommand(payload, dashboardUrl, tenantId);
460
+ case "connect": return handleLinkCommand(payload, dashboardUrl, tenantId, workspaceConnection.botToken);
367
461
  case "status": return handleStatusCommand(payload, dashboardUrl, tenantId);
368
462
  case "unlink":
369
463
  case "logout":
@@ -196,7 +196,7 @@ async function handleAppMention(params) {
196
196
  thinkingMessageTs = (await slackClient.chat.postMessage({
197
197
  channel,
198
198
  thread_ts: threadTs,
199
- text: `_${agentDisplayName} is reading this thread..._`
199
+ text: SlackStrings.status.readingThread(agentDisplayName)
200
200
  })).ts || void 0;
201
201
  const conversationId$1 = generateSlackConversationId({
202
202
  teamId,
@@ -274,7 +274,7 @@ Respond naturally as if you're joining the conversation to help.`;
274
274
  thinkingMessageTs = (await slackClient.chat.postMessage({
275
275
  channel,
276
276
  thread_ts: replyThreadTs,
277
- text: `_${agentDisplayName} is preparing a response..._`
277
+ text: SlackStrings.status.thinking(agentDisplayName)
278
278
  })).ts || void 0;
279
279
  const conversationId = generateSlackConversationId({
280
280
  teamId,
@@ -3,6 +3,17 @@
3
3
  * Handlers for Slack block action events (button clicks, selections, etc.)
4
4
  * and message shortcuts
5
5
  */
6
+ /**
7
+ * Handle tool approval/denial button clicks.
8
+ * Called when a user clicks "Approve" or "Deny" on a tool approval message.
9
+ */
10
+ declare function handleToolApproval(params: {
11
+ actionValue: string;
12
+ approved: boolean;
13
+ teamId: string;
14
+ slackUserId: string;
15
+ responseUrl?: string;
16
+ }): Promise<void>;
6
17
  /**
7
18
  * Handle opening the agent selector modal when user clicks "Select Agent" button
8
19
  */
@@ -37,4 +48,4 @@ declare function handleMessageShortcut(params: {
37
48
  responseUrl?: string;
38
49
  }): Promise<void>;
39
50
  //#endregion
40
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal };
51
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
@@ -1,10 +1,13 @@
1
+ import { env } from "../../../env.js";
1
2
  import { getLogger } from "../../../logger.js";
2
3
  import { findWorkspaceConnectionByTeamId } from "../nango.js";
3
4
  import { SlackStrings } from "../../i18n/strings.js";
5
+ import { ToolApprovalButtonValueSchema, buildToolApprovalDoneBlocks } from "../blocks/index.js";
4
6
  import { getSlackClient } from "../client.js";
5
- import { fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
7
+ import { fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, getChannelAgentConfig, sendResponseUrlMessage } from "./utils.js";
6
8
  import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "../modals.js";
7
9
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
10
+ import { getInProcessFetch, signSlackUserToken } from "@inkeep/agents-core";
8
11
 
9
12
  //#region src/slack/services/events/block-actions.ts
10
13
  /**
@@ -13,6 +16,127 @@ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../
13
16
  */
14
17
  const logger = getLogger("slack-block-actions");
15
18
  /**
19
+ * Handle tool approval/denial button clicks.
20
+ * Called when a user clicks "Approve" or "Deny" on a tool approval message.
21
+ */
22
+ async function handleToolApproval(params) {
23
+ return tracer.startActiveSpan(SLACK_SPAN_NAMES.TOOL_APPROVAL, async (span) => {
24
+ const { actionValue, approved, teamId, slackUserId, responseUrl } = params;
25
+ span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
26
+ span.setAttribute(SLACK_SPAN_KEYS.USER_ID, slackUserId);
27
+ try {
28
+ const buttonValue = ToolApprovalButtonValueSchema.parse(JSON.parse(actionValue));
29
+ const { toolCallId, conversationId, projectId, agentId, toolName } = buttonValue;
30
+ span.setAttribute(SLACK_SPAN_KEYS.CONVERSATION_ID, conversationId);
31
+ const workspaceConnection = await findWorkspaceConnectionByTeamId(teamId);
32
+ if (!workspaceConnection?.botToken) {
33
+ logger.error({ teamId }, "No bot token for tool approval");
34
+ span.end();
35
+ return;
36
+ }
37
+ const tenantId = workspaceConnection.tenantId;
38
+ const slackClient = getSlackClient(workspaceConnection.botToken);
39
+ if (slackUserId !== buttonValue.slackUserId) {
40
+ await slackClient.chat.postEphemeral({
41
+ channel: buttonValue.channel,
42
+ user: slackUserId,
43
+ thread_ts: buttonValue.threadTs,
44
+ text: "Only the user who started this conversation can approve or deny this action."
45
+ }).catch((e) => logger.warn({ error: e }, "Failed to send ownership error notification"));
46
+ span.end();
47
+ return;
48
+ }
49
+ const existingLink = await findCachedUserMapping(tenantId, slackUserId, teamId);
50
+ if (!existingLink) {
51
+ await slackClient.chat.postEphemeral({
52
+ channel: buttonValue.channel,
53
+ user: slackUserId,
54
+ thread_ts: buttonValue.threadTs,
55
+ text: "You need to link your Inkeep account first. Use `/inkeep link`."
56
+ }).catch((e) => logger.warn({ error: e }, "Failed to send not-linked notification"));
57
+ span.end();
58
+ return;
59
+ }
60
+ const slackUserToken = await signSlackUserToken({
61
+ inkeepUserId: existingLink.inkeepUserId,
62
+ tenantId,
63
+ slackTeamId: teamId,
64
+ slackUserId
65
+ });
66
+ const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
67
+ const approvalResponse = await getInProcessFetch()(`${apiUrl}/run/api/chat`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: `Bearer ${slackUserToken}`,
72
+ "x-inkeep-project-id": projectId,
73
+ "x-inkeep-agent-id": agentId
74
+ },
75
+ body: JSON.stringify({
76
+ conversationId,
77
+ messages: [{
78
+ role: "tool",
79
+ parts: [{
80
+ type: "tool-call",
81
+ toolCallId,
82
+ state: "approval-responded",
83
+ approval: {
84
+ id: toolCallId,
85
+ approved
86
+ }
87
+ }]
88
+ }]
89
+ })
90
+ });
91
+ if (!approvalResponse.ok) {
92
+ const errorBody = await approvalResponse.text().catch(() => "");
93
+ logger.error({
94
+ status: approvalResponse.status,
95
+ errorBody,
96
+ toolCallId,
97
+ conversationId
98
+ }, "Tool approval API call failed");
99
+ await slackClient.chat.postEphemeral({
100
+ channel: buttonValue.channel,
101
+ user: slackUserId,
102
+ thread_ts: buttonValue.threadTs,
103
+ text: `Failed to ${approved ? "approve" : "deny"} \`${toolName}\`. Please try again.`
104
+ }).catch((e) => logger.warn({ error: e }, "Failed to send approval error notification"));
105
+ span.end();
106
+ return;
107
+ }
108
+ if (responseUrl) await sendResponseUrlMessage(responseUrl, {
109
+ text: approved ? `✅ Approved \`${toolName}\`` : `❌ Denied \`${toolName}\``,
110
+ replace_original: true,
111
+ blocks: buildToolApprovalDoneBlocks({
112
+ toolName,
113
+ approved,
114
+ actorUserId: slackUserId
115
+ })
116
+ }).catch((e) => logger.warn({ error: e }, "Failed to update approval message"));
117
+ logger.info({
118
+ toolCallId,
119
+ conversationId,
120
+ approved,
121
+ slackUserId
122
+ }, "Tool approval processed");
123
+ span.end();
124
+ } catch (error) {
125
+ if (error instanceof Error) setSpanWithError(span, error);
126
+ logger.error({
127
+ error,
128
+ teamId,
129
+ slackUserId
130
+ }, "Failed to handle tool approval");
131
+ if (responseUrl) await sendResponseUrlMessage(responseUrl, {
132
+ text: "Something went wrong processing your request. Please try again.",
133
+ response_type: "ephemeral"
134
+ }).catch((e) => logger.warn({ error: e }, "Failed to send error notification"));
135
+ span.end();
136
+ }
137
+ });
138
+ }
139
+ /**
16
140
  * Handle opening the agent selector modal when user clicks "Select Agent" button
17
141
  */
18
142
  async function handleOpenAgentSelectorModal(params) {
@@ -262,4 +386,4 @@ async function handleMessageShortcut(params) {
262
386
  }
263
387
 
264
388
  //#endregion
265
- export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal };
389
+ export { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval };
@@ -1,6 +1,6 @@
1
1
  import { InlineSelectorMetadata, handleAppMention } from "./app-mention.js";
2
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./block-actions.js";
2
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./block-actions.js";
3
3
  import { handleFollowUpSubmission, handleModalSubmission } from "./modal-submission.js";
4
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
- export { type InlineSelectorMetadata, SlackErrorType, type StreamResult, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
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,7 +1,7 @@
1
1
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./utils.js";
2
2
  import { streamAgentResponse } from "./streaming.js";
3
3
  import { handleAppMention } from "./app-mention.js";
4
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./block-actions.js";
4
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./block-actions.js";
5
5
  import { handleFollowUpSubmission, handleModalSubmission } from "./modal-submission.js";
6
6
 
7
- export { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
7
+ export { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, handleAppMention, handleFollowUpSubmission, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval, markdownToMrkdwn, sendResponseUrlMessage, streamAgentResponse };
@@ -20,7 +20,7 @@ declare function streamAgentResponse(params: {
20
20
  agentId: string;
21
21
  question: string;
22
22
  agentName: string;
23
- conversationId?: string;
23
+ conversationId: string;
24
24
  }): Promise<StreamResult>;
25
25
  //#endregion
26
26
  export { StreamResult, streamAgentResponse };