@inkeep/agents-work-apps 0.0.0-dev-20260211191741 → 0.0.0-dev-20260211220939

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