@inkeep/agents-work-apps 0.47.5 → 0.48.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 (61) hide show
  1. package/dist/env.d.ts +24 -2
  2. package/dist/env.js +13 -2
  3. package/dist/github/mcp/index.d.ts +2 -2
  4. package/dist/github/mcp/index.js +23 -34
  5. package/dist/github/routes/setup.d.ts +2 -2
  6. package/dist/github/routes/webhooks.d.ts +2 -2
  7. package/dist/slack/i18n/index.d.ts +2 -0
  8. package/dist/slack/i18n/index.js +3 -0
  9. package/dist/slack/i18n/strings.d.ts +73 -0
  10. package/dist/slack/i18n/strings.js +67 -0
  11. package/dist/slack/index.d.ts +18 -0
  12. package/dist/slack/index.js +28 -0
  13. package/dist/slack/middleware/permissions.d.ts +31 -0
  14. package/dist/slack/middleware/permissions.js +167 -0
  15. package/dist/slack/routes/events.d.ts +10 -0
  16. package/dist/slack/routes/events.js +551 -0
  17. package/dist/slack/routes/index.d.ts +10 -0
  18. package/dist/slack/routes/index.js +47 -0
  19. package/dist/slack/routes/oauth.d.ts +20 -0
  20. package/dist/slack/routes/oauth.js +344 -0
  21. package/dist/slack/routes/users.d.ts +10 -0
  22. package/dist/slack/routes/users.js +365 -0
  23. package/dist/slack/routes/workspaces.d.ts +10 -0
  24. package/dist/slack/routes/workspaces.js +909 -0
  25. package/dist/slack/services/agent-resolution.d.ts +41 -0
  26. package/dist/slack/services/agent-resolution.js +99 -0
  27. package/dist/slack/services/blocks/index.d.ts +73 -0
  28. package/dist/slack/services/blocks/index.js +103 -0
  29. package/dist/slack/services/client.d.ts +108 -0
  30. package/dist/slack/services/client.js +232 -0
  31. package/dist/slack/services/commands/index.d.ts +19 -0
  32. package/dist/slack/services/commands/index.js +553 -0
  33. package/dist/slack/services/events/app-mention.d.ts +40 -0
  34. package/dist/slack/services/events/app-mention.js +304 -0
  35. package/dist/slack/services/events/block-actions.d.ts +40 -0
  36. package/dist/slack/services/events/block-actions.js +265 -0
  37. package/dist/slack/services/events/index.d.ts +6 -0
  38. package/dist/slack/services/events/index.js +7 -0
  39. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  40. package/dist/slack/services/events/modal-submission.js +400 -0
  41. package/dist/slack/services/events/streaming.d.ts +26 -0
  42. package/dist/slack/services/events/streaming.js +272 -0
  43. package/dist/slack/services/events/utils.d.ts +146 -0
  44. package/dist/slack/services/events/utils.js +370 -0
  45. package/dist/slack/services/index.d.ts +16 -0
  46. package/dist/slack/services/index.js +16 -0
  47. package/dist/slack/services/modals.d.ts +86 -0
  48. package/dist/slack/services/modals.js +355 -0
  49. package/dist/slack/services/nango.d.ts +85 -0
  50. package/dist/slack/services/nango.js +476 -0
  51. package/dist/slack/services/security.d.ts +35 -0
  52. package/dist/slack/services/security.js +65 -0
  53. package/dist/slack/services/types.d.ts +26 -0
  54. package/dist/slack/services/types.js +1 -0
  55. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  56. package/dist/slack/services/workspace-tokens.js +27 -0
  57. package/dist/slack/tracer.d.ts +40 -0
  58. package/dist/slack/tracer.js +39 -0
  59. package/dist/slack/types.d.ts +10 -0
  60. package/dist/slack/types.js +1 -0
  61. package/package.json +11 -2
@@ -0,0 +1,553 @@
1
+ import { env } from "../../../env.js";
2
+ import { getLogger } from "../../../logger.js";
3
+ import runDbClient_default from "../../../db/runDbClient.js";
4
+ import { findWorkspaceConnectionByTeamId } from "../nango.js";
5
+ import { resolveEffectiveAgent } from "../agent-resolution.js";
6
+ import { SlackStrings } from "../../i18n/strings.js";
7
+ import { createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
+ import { getSlackClient } from "../client.js";
9
+ import { fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
10
+ import { buildAgentSelectorModal } from "../modals.js";
11
+ import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
12
+
13
+ //#region src/slack/services/commands/index.ts
14
+ /**
15
+ * Fetch all agents from the manage API.
16
+ * This uses the proper ref-middleware and Dolt branch resolution.
17
+ * Requires an auth token to access the manage API.
18
+ */
19
+ const INTERNAL_FETCH_TIMEOUT_MS = 1e4;
20
+ async function fetchAgentsFromManageApi(tenantId, authToken) {
21
+ const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
22
+ const controller = new AbortController();
23
+ const timeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
24
+ try {
25
+ const projectsResponse = await fetch(`${apiBaseUrl}/manage/tenants/${tenantId}/projects`, {
26
+ method: "GET",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ Authorization: `Bearer ${authToken}`
30
+ },
31
+ signal: controller.signal
32
+ });
33
+ if (!projectsResponse.ok) {
34
+ logger.error({
35
+ status: projectsResponse.status,
36
+ tenantId
37
+ }, "Failed to fetch projects from manage API");
38
+ return [];
39
+ }
40
+ const projectsData = await projectsResponse.json();
41
+ const projects = projectsData.data || projectsData || [];
42
+ logger.info({
43
+ projectCount: projects.length,
44
+ tenantId
45
+ }, "Fetched projects from manage API");
46
+ return (await Promise.all(projects.map(async (project) => {
47
+ try {
48
+ const agentsResponse = await fetch(`${apiBaseUrl}/manage/tenants/${tenantId}/projects/${project.id}/agents`, {
49
+ method: "GET",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ Authorization: `Bearer ${authToken}`
53
+ },
54
+ signal: controller.signal
55
+ });
56
+ if (agentsResponse.ok) {
57
+ const agentsData = await agentsResponse.json();
58
+ return (agentsData.data || agentsData || []).map((agent) => ({
59
+ id: agent.id,
60
+ name: agent.name,
61
+ projectId: project.id,
62
+ projectName: project.name
63
+ }));
64
+ }
65
+ logger.warn({
66
+ status: agentsResponse.status,
67
+ projectId: project.id
68
+ }, "Failed to fetch agents for project");
69
+ return [];
70
+ } catch (error) {
71
+ logger.error({
72
+ error,
73
+ projectId: project.id
74
+ }, "Failed to fetch agents for project");
75
+ return [];
76
+ }
77
+ }))).flat();
78
+ } catch (error) {
79
+ logger.error({
80
+ error,
81
+ tenantId
82
+ }, "Failed to fetch agents from manage API");
83
+ return [];
84
+ } finally {
85
+ clearTimeout(timeout);
86
+ }
87
+ }
88
+ /**
89
+ * Find an agent by name or ID from the manage API.
90
+ */
91
+ async function findAgentByIdentifier(tenantId, identifier, authToken) {
92
+ return (await fetchAgentsFromManageApi(tenantId, authToken)).find((a) => a.id === identifier || a.name?.toLowerCase() === identifier.toLowerCase()) || null;
93
+ }
94
+ const DEFAULT_CLIENT_ID = "work-apps-slack";
95
+ const LINK_CODE_TTL_MINUTES = 10;
96
+ const logger = getLogger("slack-commands");
97
+ /**
98
+ * Parse agent name and question from command text.
99
+ * Agent name must be in quotes: "agent name" question
100
+ */
101
+ function parseAgentAndQuestion(text) {
102
+ if (!text.trim()) return {
103
+ agentName: null,
104
+ question: null
105
+ };
106
+ const quotedMatch = text.match(/^["']([^"']+)["']\s+(.+)$/);
107
+ if (quotedMatch) return {
108
+ agentName: quotedMatch[1].trim(),
109
+ question: quotedMatch[2].trim()
110
+ };
111
+ return {
112
+ agentName: null,
113
+ question: null
114
+ };
115
+ }
116
+ async function handleLinkCommand(payload, dashboardUrl, tenantId) {
117
+ const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
118
+ if (existingLink) return {
119
+ response_type: "ephemeral",
120
+ ...createAlreadyLinkedMessage(existingLink.slackEmail || existingLink.slackUsername || "Unknown", existingLink.linkedAt, dashboardUrl)
121
+ };
122
+ try {
123
+ const linkToken = await signSlackLinkToken({
124
+ tenantId,
125
+ slackTeamId: payload.teamId,
126
+ slackUserId: payload.userId,
127
+ slackEnterpriseId: payload.enterpriseId,
128
+ slackUsername: payload.userName
129
+ });
130
+ const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
131
+ logger.info({
132
+ slackUserId: payload.userId,
133
+ tenantId
134
+ }, "Generated JWT link token");
135
+ return {
136
+ response_type: "ephemeral",
137
+ ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
138
+ };
139
+ } catch (error) {
140
+ logger.error({
141
+ error,
142
+ slackUserId: payload.userId,
143
+ tenantId
144
+ }, "Failed to generate link token");
145
+ return {
146
+ response_type: "ephemeral",
147
+ ...createErrorMessage("Failed to generate link. Please try again.")
148
+ };
149
+ }
150
+ }
151
+ async function handleUnlinkCommand(payload, tenantId) {
152
+ if (!await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID)) return {
153
+ response_type: "ephemeral",
154
+ ...createNotLinkedMessage()
155
+ };
156
+ try {
157
+ if (await deleteWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID)) {
158
+ logger.info({
159
+ slackUserId: payload.userId,
160
+ tenantId
161
+ }, "User unlinked Slack account");
162
+ return {
163
+ response_type: "ephemeral",
164
+ ...createUnlinkSuccessMessage()
165
+ };
166
+ }
167
+ return {
168
+ response_type: "ephemeral",
169
+ ...createErrorMessage("Failed to unlink account. Please try again.")
170
+ };
171
+ } catch (error) {
172
+ logger.error({
173
+ error,
174
+ slackUserId: payload.userId,
175
+ tenantId
176
+ }, "Failed to unlink account");
177
+ return {
178
+ response_type: "ephemeral",
179
+ ...createErrorMessage("Failed to unlink account. Please try again.")
180
+ };
181
+ }
182
+ }
183
+ async function handleStatusCommand(payload, dashboardUrl, tenantId) {
184
+ const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
185
+ if (existingLink) {
186
+ const { getAgentConfigSources } = await import("../agent-resolution.js");
187
+ const agentConfigs = await getAgentConfigSources({
188
+ tenantId,
189
+ teamId: payload.teamId,
190
+ channelId: payload.channelId,
191
+ userId: payload.userId
192
+ });
193
+ return {
194
+ response_type: "ephemeral",
195
+ ...createStatusMessage(existingLink.slackEmail || existingLink.slackUsername || payload.userName, existingLink.linkedAt, dashboardUrl, agentConfigs)
196
+ };
197
+ }
198
+ return {
199
+ response_type: "ephemeral",
200
+ ...createNotLinkedMessage()
201
+ };
202
+ }
203
+ async function handleHelpCommand() {
204
+ return {
205
+ response_type: "ephemeral",
206
+ ...createUpdatedHelpMessage()
207
+ };
208
+ }
209
+ /**
210
+ * Handle `/inkeep` with no arguments - opens the agent picker modal
211
+ * Similar to @mention behavior in channels
212
+ */
213
+ async function handleAgentPickerCommand(payload, tenantId, workspaceConnection) {
214
+ const { triggerId, teamId, channelId, userId, responseUrl } = payload;
215
+ try {
216
+ const connection = workspaceConnection ?? await findWorkspaceConnectionByTeamId(teamId);
217
+ if (!connection?.botToken) {
218
+ logger.error({ teamId }, "No bot token for agent picker modal");
219
+ return {
220
+ response_type: "ephemeral",
221
+ ...createErrorMessage(SlackStrings.errors.generic)
222
+ };
223
+ }
224
+ const slackClient = getSlackClient(connection.botToken);
225
+ const [projectListResult, defaultAgent] = await Promise.all([fetchProjectsForTenant(tenantId), getChannelAgentConfig(teamId, channelId)]);
226
+ let projectList = projectListResult;
227
+ if (projectList.length === 0 && defaultAgent) projectList = [{
228
+ id: defaultAgent.projectId,
229
+ name: defaultAgent.projectName || defaultAgent.projectId
230
+ }];
231
+ if (projectList.length === 0) return {
232
+ response_type: "ephemeral",
233
+ ...createErrorMessage(SlackStrings.status.noProjectsConfigured)
234
+ };
235
+ const firstProject = projectList[0];
236
+ let agentList = await fetchAgentsForProject(tenantId, firstProject.id);
237
+ if (agentList.length === 0 && defaultAgent && defaultAgent.projectId === firstProject.id) agentList = [{
238
+ id: defaultAgent.agentId,
239
+ name: defaultAgent.agentName || defaultAgent.agentId,
240
+ projectId: defaultAgent.projectId,
241
+ projectName: defaultAgent.projectName || defaultAgent.projectId
242
+ }];
243
+ const now = Date.now();
244
+ const modalMetadata = {
245
+ channel: channelId,
246
+ messageTs: `${Math.floor(now / 1e3)}.${String(now % 1e3).padStart(3, "0")}000`,
247
+ teamId,
248
+ slackUserId: userId,
249
+ tenantId,
250
+ isInThread: false,
251
+ buttonResponseUrl: responseUrl
252
+ };
253
+ const modal = buildAgentSelectorModal({
254
+ projects: projectList,
255
+ agents: agentList.map((a) => ({
256
+ id: a.id,
257
+ name: a.name,
258
+ projectId: a.projectId,
259
+ projectName: a.projectName || a.projectId
260
+ })),
261
+ metadata: modalMetadata,
262
+ selectedProjectId: firstProject.id
263
+ });
264
+ await slackClient.views.open({
265
+ trigger_id: triggerId,
266
+ view: modal
267
+ });
268
+ logger.info({
269
+ teamId,
270
+ channelId,
271
+ projectCount: projectList.length,
272
+ agentCount: agentList.length
273
+ }, "Opened agent picker modal from slash command");
274
+ return {};
275
+ } catch (error) {
276
+ logger.error({
277
+ error,
278
+ teamId
279
+ }, "Failed to open agent picker modal from slash command");
280
+ return {
281
+ response_type: "ephemeral",
282
+ ...createErrorMessage(SlackStrings.errors.failedToOpenSelector)
283
+ };
284
+ }
285
+ }
286
+ async function generateLinkCodeWithIntent(payload, tenantId) {
287
+ try {
288
+ const linkToken = await signSlackLinkToken({
289
+ tenantId,
290
+ slackTeamId: payload.teamId,
291
+ slackUserId: payload.userId,
292
+ slackEnterpriseId: payload.enterpriseId,
293
+ slackUsername: payload.userName
294
+ });
295
+ const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
296
+ logger.info({
297
+ slackUserId: payload.userId,
298
+ tenantId
299
+ }, "Generated JWT link token with intent");
300
+ return {
301
+ response_type: "ephemeral",
302
+ ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
303
+ };
304
+ } catch (error) {
305
+ logger.error({
306
+ error,
307
+ slackUserId: payload.userId,
308
+ tenantId
309
+ }, "Failed to generate link token");
310
+ return {
311
+ response_type: "ephemeral",
312
+ ...createErrorMessage("Failed to generate link. Please try again.")
313
+ };
314
+ }
315
+ }
316
+ async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId) {
317
+ const existingLink = await findWorkAppSlackUserMappingBySlackUser(runDbClient_default)(payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
318
+ if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId);
319
+ const userTenantId = existingLink.tenantId;
320
+ const resolvedAgent = await resolveEffectiveAgent({
321
+ tenantId: userTenantId,
322
+ teamId: payload.teamId,
323
+ channelId: payload.channelId,
324
+ userId: payload.userId
325
+ });
326
+ if (!resolvedAgent) return {
327
+ response_type: "ephemeral",
328
+ ...createErrorMessage("No default agent configured. Ask your admin to set a workspace default in the dashboard.\n\nUse `/inkeep list` to see available agents.")
329
+ };
330
+ executeAgentInBackground(payload, existingLink, {
331
+ id: resolvedAgent.agentId,
332
+ name: resolvedAgent.agentName || null,
333
+ projectId: resolvedAgent.projectId
334
+ }, question, userTenantId).catch((error) => {
335
+ logger.error({ error }, "Background execution promise rejected");
336
+ });
337
+ return {};
338
+ }
339
+ async function executeAgentInBackground(payload, existingLink, targetAgent, question, tenantId) {
340
+ try {
341
+ const slackUserToken = await signSlackUserToken({
342
+ inkeepUserId: existingLink.inkeepUserId,
343
+ tenantId,
344
+ slackTeamId: payload.teamId,
345
+ slackUserId: payload.userId,
346
+ slackEnterpriseId: payload.enterpriseId
347
+ });
348
+ const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
349
+ const controller = new AbortController();
350
+ const timeout = setTimeout(() => controller.abort(), 3e4);
351
+ let response;
352
+ try {
353
+ response = await fetch(`${apiBaseUrl}/run/api/chat`, {
354
+ method: "POST",
355
+ headers: {
356
+ "Content-Type": "application/json",
357
+ Authorization: `Bearer ${slackUserToken}`,
358
+ "x-inkeep-project-id": targetAgent.projectId,
359
+ "x-inkeep-agent-id": targetAgent.id
360
+ },
361
+ body: JSON.stringify({
362
+ messages: [{
363
+ role: "user",
364
+ content: question
365
+ }],
366
+ stream: false
367
+ }),
368
+ signal: controller.signal
369
+ });
370
+ } catch (error) {
371
+ clearTimeout(timeout);
372
+ if (error.name === "AbortError") {
373
+ logger.warn({
374
+ teamId: payload.teamId,
375
+ timeoutMs: 3e4
376
+ }, "Background agent execution timed out");
377
+ await sendResponseUrlMessage(payload.responseUrl, {
378
+ response_type: "ephemeral",
379
+ text: "Request timed out. Please try again."
380
+ });
381
+ return;
382
+ }
383
+ throw error;
384
+ } finally {
385
+ clearTimeout(timeout);
386
+ }
387
+ if (!response.ok) {
388
+ const errorText = await response.text();
389
+ logger.error({
390
+ status: response.status,
391
+ error: errorText,
392
+ agentId: targetAgent.id,
393
+ projectId: targetAgent.projectId
394
+ }, "Run API call failed");
395
+ await sendResponseUrlMessage(payload.responseUrl, {
396
+ response_type: "ephemeral",
397
+ text: `Failed to run agent: ${response.status} ${response.statusText}`
398
+ });
399
+ } else {
400
+ const result = await response.json();
401
+ const assistantMessage = result.choices?.[0]?.message?.content || result.message?.content || "No response received";
402
+ logger.info({
403
+ slackUserId: payload.userId,
404
+ agentId: targetAgent.id,
405
+ projectId: targetAgent.projectId,
406
+ tenantId
407
+ }, "Agent execution completed via Slack");
408
+ const contextBlock = createContextBlock({ agentName: targetAgent.name || targetAgent.id });
409
+ await sendResponseUrlMessage(payload.responseUrl, {
410
+ response_type: "ephemeral",
411
+ text: assistantMessage,
412
+ blocks: [{
413
+ type: "section",
414
+ text: {
415
+ type: "mrkdwn",
416
+ text: assistantMessage
417
+ }
418
+ }, contextBlock]
419
+ });
420
+ }
421
+ } catch (error) {
422
+ logger.error({
423
+ error,
424
+ slackUserId: payload.userId
425
+ }, "Background agent execution failed");
426
+ await sendResponseUrlMessage(payload.responseUrl, {
427
+ response_type: "ephemeral",
428
+ text: "An error occurred while running the agent. Please try again."
429
+ });
430
+ }
431
+ }
432
+ async function handleRunCommand(payload, agentIdentifier, question, _dashboardUrl, tenantId) {
433
+ const existingLink = await findWorkAppSlackUserMappingBySlackUser(runDbClient_default)(payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
434
+ if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId);
435
+ const userTenantId = existingLink.tenantId;
436
+ try {
437
+ const targetAgent = await findAgentByIdentifier(userTenantId, agentIdentifier, await signSlackUserToken({
438
+ inkeepUserId: existingLink.inkeepUserId,
439
+ tenantId: userTenantId,
440
+ slackTeamId: payload.teamId,
441
+ slackUserId: payload.userId,
442
+ slackEnterpriseId: payload.enterpriseId
443
+ }));
444
+ if (!targetAgent) return {
445
+ response_type: "ephemeral",
446
+ ...createErrorMessage(`Agent "${agentIdentifier}" not found. Use \`/inkeep list\` to see available agents.`)
447
+ };
448
+ executeAgentInBackground(payload, existingLink, targetAgent, question, userTenantId).catch((error) => {
449
+ logger.error({ error }, "Background execution promise rejected");
450
+ });
451
+ return {};
452
+ } catch (error) {
453
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
454
+ logger.error({
455
+ error: errorMessage,
456
+ tenantId: userTenantId
457
+ }, "Failed to run agent");
458
+ return {
459
+ response_type: "ephemeral",
460
+ ...createErrorMessage("Failed to run agent. Please try again or visit the dashboard.")
461
+ };
462
+ }
463
+ }
464
+ async function handleAgentListCommand(payload, dashboardUrl, _tenantId) {
465
+ const existingLink = await findWorkAppSlackUserMappingBySlackUser(runDbClient_default)(payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
466
+ if (!existingLink) return {
467
+ response_type: "ephemeral",
468
+ ...createNotLinkedMessage()
469
+ };
470
+ const userTenantId = existingLink.tenantId;
471
+ logger.info({
472
+ slackUserId: payload.userId,
473
+ existingLinkTenantId: existingLink.tenantId,
474
+ existingLinkInkeepUserId: existingLink.inkeepUserId
475
+ }, "Found user mapping for list command");
476
+ try {
477
+ const allAgents = await fetchAgentsFromManageApi(userTenantId, await signSlackUserToken({
478
+ inkeepUserId: existingLink.inkeepUserId,
479
+ tenantId: userTenantId,
480
+ slackTeamId: payload.teamId,
481
+ slackUserId: payload.userId,
482
+ slackEnterpriseId: payload.enterpriseId
483
+ }));
484
+ logger.info({
485
+ slackUserId: payload.userId,
486
+ tenantId: userTenantId,
487
+ agentCount: allAgents.length
488
+ }, "Listed agents for linked Slack user");
489
+ if (allAgents.length === 0) return {
490
+ response_type: "ephemeral",
491
+ ...createErrorMessage("No agents found. Create an agent in the Inkeep dashboard first.")
492
+ };
493
+ return {
494
+ response_type: "ephemeral",
495
+ ...createAgentListMessage(allAgents, dashboardUrl.replace("/work-apps/slack", ""))
496
+ };
497
+ } catch (error) {
498
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
499
+ logger.error({
500
+ error: errorMessage,
501
+ tenantId: userTenantId
502
+ }, "Failed to list agents");
503
+ return {
504
+ response_type: "ephemeral",
505
+ ...createErrorMessage("Failed to list agents. Please try again or visit the dashboard.")
506
+ };
507
+ }
508
+ }
509
+ async function handleCommand(payload) {
510
+ const text = payload.text.trim();
511
+ const subcommand = text.split(/\s+/)[0]?.toLowerCase() || "";
512
+ const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
513
+ const workspaceConnection = await findWorkspaceConnectionByTeamId(payload.teamId);
514
+ if (!workspaceConnection?.tenantId) {
515
+ logger.error({ teamId: payload.teamId }, "No workspace connection or missing tenantId");
516
+ return {
517
+ response_type: "ephemeral",
518
+ text: "This workspace is not properly configured. Please reinstall the Slack app from the Inkeep dashboard."
519
+ };
520
+ }
521
+ const tenantId = workspaceConnection.tenantId;
522
+ const dashboardUrl = `${manageUiUrl}/${tenantId}/work-apps/slack`;
523
+ logger.info({
524
+ command: payload.command,
525
+ subcommand,
526
+ slackUserId: payload.userId,
527
+ teamId: payload.teamId,
528
+ tenantId
529
+ }, "Slack command received");
530
+ switch (subcommand) {
531
+ case "link":
532
+ case "connect": return handleLinkCommand(payload, dashboardUrl, tenantId);
533
+ case "status": return handleStatusCommand(payload, dashboardUrl, tenantId);
534
+ case "unlink":
535
+ case "logout":
536
+ case "disconnect": return handleUnlinkCommand(payload, tenantId);
537
+ case "list": return handleAgentListCommand(payload, dashboardUrl, tenantId);
538
+ case "run": {
539
+ const parsed = parseAgentAndQuestion(text.slice(4).trim());
540
+ if (!parsed.agentName || !parsed.question) return {
541
+ response_type: "ephemeral",
542
+ ...createErrorMessage("Usage: `/inkeep run \"agent name\" [question]`\n\nExample: `/inkeep run \"my agent\" What is the weather?`\n\nAgent name must be in quotes.")
543
+ };
544
+ return handleRunCommand(payload, parsed.agentName, parsed.question, dashboardUrl, tenantId);
545
+ }
546
+ case "help": return handleHelpCommand();
547
+ case "": return handleAgentPickerCommand(payload, tenantId, workspaceConnection);
548
+ default: return handleQuestionCommand(payload, text, dashboardUrl, tenantId);
549
+ }
550
+ }
551
+
552
+ //#endregion
553
+ export { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand };
@@ -0,0 +1,40 @@
1
+ //#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
+ */
16
+ /**
17
+ * Metadata passed to the agent selector modal via button value
18
+ */
19
+ interface InlineSelectorMetadata {
20
+ channel: string;
21
+ threadTs?: string;
22
+ messageTs: string;
23
+ teamId: string;
24
+ slackUserId: string;
25
+ tenantId: string;
26
+ threadMessageCount?: number;
27
+ }
28
+ /**
29
+ * Main handler for @mention events in Slack
30
+ */
31
+ declare function handleAppMention(params: {
32
+ slackUserId: string;
33
+ channel: string;
34
+ text: string;
35
+ threadTs: string;
36
+ messageTs: string;
37
+ teamId: string;
38
+ }): Promise<void>;
39
+ //#endregion
40
+ export { InlineSelectorMetadata, handleAppMention };