@inkeep/agents-work-apps 0.47.5 → 0.48.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 (65) 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/auth.d.ts +2 -2
  5. package/dist/github/mcp/index.d.ts +2 -2
  6. package/dist/github/mcp/index.js +23 -34
  7. package/dist/github/mcp/schemas.d.ts +1 -1
  8. package/dist/github/routes/setup.d.ts +2 -2
  9. package/dist/github/routes/tokenExchange.d.ts +2 -2
  10. package/dist/github/routes/webhooks.d.ts +2 -2
  11. package/dist/slack/i18n/index.d.ts +2 -0
  12. package/dist/slack/i18n/index.js +3 -0
  13. package/dist/slack/i18n/strings.d.ts +73 -0
  14. package/dist/slack/i18n/strings.js +67 -0
  15. package/dist/slack/index.d.ts +18 -0
  16. package/dist/slack/index.js +28 -0
  17. package/dist/slack/middleware/permissions.d.ts +31 -0
  18. package/dist/slack/middleware/permissions.js +167 -0
  19. package/dist/slack/routes/events.d.ts +10 -0
  20. package/dist/slack/routes/events.js +551 -0
  21. package/dist/slack/routes/index.d.ts +10 -0
  22. package/dist/slack/routes/index.js +47 -0
  23. package/dist/slack/routes/oauth.d.ts +20 -0
  24. package/dist/slack/routes/oauth.js +344 -0
  25. package/dist/slack/routes/users.d.ts +10 -0
  26. package/dist/slack/routes/users.js +365 -0
  27. package/dist/slack/routes/workspaces.d.ts +10 -0
  28. package/dist/slack/routes/workspaces.js +909 -0
  29. package/dist/slack/services/agent-resolution.d.ts +41 -0
  30. package/dist/slack/services/agent-resolution.js +99 -0
  31. package/dist/slack/services/blocks/index.d.ts +73 -0
  32. package/dist/slack/services/blocks/index.js +103 -0
  33. package/dist/slack/services/client.d.ts +108 -0
  34. package/dist/slack/services/client.js +232 -0
  35. package/dist/slack/services/commands/index.d.ts +19 -0
  36. package/dist/slack/services/commands/index.js +553 -0
  37. package/dist/slack/services/events/app-mention.d.ts +40 -0
  38. package/dist/slack/services/events/app-mention.js +297 -0
  39. package/dist/slack/services/events/block-actions.d.ts +40 -0
  40. package/dist/slack/services/events/block-actions.js +265 -0
  41. package/dist/slack/services/events/index.d.ts +6 -0
  42. package/dist/slack/services/events/index.js +7 -0
  43. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  44. package/dist/slack/services/events/modal-submission.js +400 -0
  45. package/dist/slack/services/events/streaming.d.ts +26 -0
  46. package/dist/slack/services/events/streaming.js +255 -0
  47. package/dist/slack/services/events/utils.d.ts +146 -0
  48. package/dist/slack/services/events/utils.js +370 -0
  49. package/dist/slack/services/index.d.ts +16 -0
  50. package/dist/slack/services/index.js +16 -0
  51. package/dist/slack/services/modals.d.ts +86 -0
  52. package/dist/slack/services/modals.js +355 -0
  53. package/dist/slack/services/nango.d.ts +85 -0
  54. package/dist/slack/services/nango.js +476 -0
  55. package/dist/slack/services/security.d.ts +35 -0
  56. package/dist/slack/services/security.js +65 -0
  57. package/dist/slack/services/types.d.ts +26 -0
  58. package/dist/slack/services/types.js +1 -0
  59. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  60. package/dist/slack/services/workspace-tokens.js +27 -0
  61. package/dist/slack/tracer.d.ts +40 -0
  62. package/dist/slack/tracer.js +39 -0
  63. package/dist/slack/types.d.ts +10 -0
  64. package/dist/slack/types.js +1 -0
  65. package/package.json +11 -2
@@ -0,0 +1,146 @@
1
+ import { DefaultAgentConfig, SlackWorkspaceConnection } from "../nango.js";
2
+ import { AgentOption } from "../modals.js";
3
+
4
+ //#region src/slack/services/events/utils.d.ts
5
+
6
+ /**
7
+ * Find a user mapping with bounded in-memory caching (5 min TTL, max 5000 entries).
8
+ * Called on every @mention and /inkeep command — caching avoids redundant DB queries.
9
+ */
10
+ declare function findCachedUserMapping(tenantId: string, slackUserId: string, teamId: string, clientId?: string): Promise<{
11
+ createdAt: string;
12
+ updatedAt: string;
13
+ id: string;
14
+ tenantId: string;
15
+ clientId: string;
16
+ slackUserId: string;
17
+ slackTeamId: string;
18
+ slackEnterpriseId: string | null;
19
+ inkeepUserId: string;
20
+ slackUsername: string | null;
21
+ slackEmail: string | null;
22
+ linkedAt: string;
23
+ lastUsedAt: string | null;
24
+ } | null>;
25
+ /**
26
+ * Convert standard Markdown to Slack's mrkdwn format
27
+ *
28
+ * Key differences:
29
+ * - **bold** or __bold__ → *bold*
30
+ * - *italic* (when not bold) → _italic_
31
+ * - # Header → *Header* (Slack has no headers)
32
+ * - [text](url) → <url|text>
33
+ * - Keeps code blocks, inline code, and lists as-is
34
+ */
35
+ declare function markdownToMrkdwn(markdown: string): string;
36
+ /**
37
+ * Error types for user-friendly error messages
38
+ */
39
+ declare enum SlackErrorType {
40
+ TIMEOUT = "timeout",
41
+ RATE_LIMIT = "rate_limit",
42
+ API_ERROR = "api_error",
43
+ AUTH_ERROR = "auth_error",
44
+ UNKNOWN = "unknown",
45
+ }
46
+ /**
47
+ * Classify an error into a SlackErrorType for appropriate user messaging
48
+ */
49
+ declare function classifyError(error: unknown, httpStatus?: number): SlackErrorType;
50
+ /**
51
+ * Get a user-friendly error message based on error type
52
+ */
53
+ declare function getUserFriendlyErrorMessage(errorType: SlackErrorType, agentName?: string): string;
54
+ type ProjectOption = {
55
+ id: string;
56
+ name: string;
57
+ };
58
+ declare function fetchProjectsForTenant(tenantId: string): Promise<ProjectOption[]>;
59
+ declare function fetchAgentsForProject(tenantId: string, projectId: string): Promise<AgentOption[]>;
60
+ declare function getWorkspaceDefaultAgent(teamId: string): Promise<DefaultAgentConfig | null>;
61
+ declare function getChannelAgentConfig(teamId: string, channelId: string): Promise<DefaultAgentConfig | null>;
62
+ /**
63
+ * Resolve channel agent config using a pre-resolved workspace connection.
64
+ * Avoids redundant workspace lookups when the connection is already available.
65
+ */
66
+ declare function resolveChannelAgentConfig(teamId: string, channelId: string, workspace: SlackWorkspaceConnection | null): Promise<DefaultAgentConfig | null>;
67
+ declare function sendResponseUrlMessage(responseUrl: string, message: {
68
+ text: string;
69
+ response_type?: 'ephemeral' | 'in_channel';
70
+ replace_original?: boolean;
71
+ delete_original?: boolean;
72
+ blocks?: unknown[];
73
+ }): Promise<void>;
74
+ /**
75
+ * Generate a deterministic conversation ID for Slack threads/DMs.
76
+ * This ensures the same thread + agent combination gets the same conversation ID,
77
+ * allowing the agent to maintain conversation history.
78
+ *
79
+ * Including agentId ensures switching agents in the same thread starts a fresh
80
+ * conversation, avoiding sub-agent conflicts when the Run API tries to resume
81
+ * a conversation that was started by a different agent.
82
+ *
83
+ * Format: slack-thread-{teamId}-{identifier}[-{agentId}]
84
+ */
85
+ declare function generateSlackConversationId(params: {
86
+ teamId: string;
87
+ threadTs?: string;
88
+ channel: string;
89
+ isDM?: boolean;
90
+ agentId?: string;
91
+ }): string;
92
+ /**
93
+ * Check if a thread was initiated by the bot (i.e., the parent message is from the bot).
94
+ * This helps distinguish "bot threads" (where users are conversing with the bot)
95
+ * from "user threads" (where users are having their own conversation).
96
+ *
97
+ * Uses conversations.replies which returns thread messages with the parent as the first message.
98
+ */
99
+ declare function checkIfBotThread(slackClient: {
100
+ conversations: {
101
+ replies: (params: {
102
+ channel: string;
103
+ ts: string;
104
+ limit?: number;
105
+ }) => Promise<{
106
+ messages?: Array<{
107
+ bot_id?: string;
108
+ user?: string;
109
+ text?: string;
110
+ }>;
111
+ }>;
112
+ };
113
+ }, channel: string, threadTs: string): Promise<boolean>;
114
+ interface ThreadContextOptions {
115
+ includeLastMessage?: boolean;
116
+ resolveUserNames?: boolean;
117
+ }
118
+ declare function getThreadContext(slackClient: {
119
+ conversations: {
120
+ replies: (params: {
121
+ channel: string;
122
+ ts: string;
123
+ limit?: number;
124
+ }) => Promise<{
125
+ messages?: Array<{
126
+ bot_id?: string;
127
+ user?: string;
128
+ text?: string;
129
+ ts?: string;
130
+ }>;
131
+ }>;
132
+ };
133
+ users?: {
134
+ info: (params: {
135
+ user: string;
136
+ }) => Promise<{
137
+ user?: {
138
+ real_name?: string;
139
+ display_name?: string;
140
+ name?: string;
141
+ };
142
+ }>;
143
+ };
144
+ }, channel: string, threadTs: string, options?: ThreadContextOptions): Promise<string>;
145
+ //#endregion
146
+ export { ProjectOption, SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, resolveChannelAgentConfig, sendResponseUrlMessage };
@@ -0,0 +1,370 @@
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 { InternalServices, findWorkAppSlackChannelAgentConfig, findWorkAppSlackUserMapping, generateInternalServiceToken } from "@inkeep/agents-core";
6
+
7
+ //#region src/slack/services/events/utils.ts
8
+ /**
9
+ * Shared utilities for Slack event handlers
10
+ */
11
+ const logger = getLogger("slack-event-utils");
12
+ const USER_MAPPING_CACHE_TTL_MS = 300 * 1e3;
13
+ const USER_MAPPING_CACHE_MAX_SIZE = 5e3;
14
+ const userMappingCache = /* @__PURE__ */ new Map();
15
+ function evictExpiredEntries() {
16
+ if (userMappingCache.size <= USER_MAPPING_CACHE_MAX_SIZE) return;
17
+ const now = Date.now();
18
+ for (const [key, entry] of userMappingCache) if (entry.expiresAt <= now) userMappingCache.delete(key);
19
+ if (userMappingCache.size > USER_MAPPING_CACHE_MAX_SIZE) {
20
+ const excess = userMappingCache.size - USER_MAPPING_CACHE_MAX_SIZE;
21
+ const keys = userMappingCache.keys();
22
+ for (let i = 0; i < excess; i++) {
23
+ const { value } = keys.next();
24
+ if (value) userMappingCache.delete(value);
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Find a user mapping with bounded in-memory caching (5 min TTL, max 5000 entries).
30
+ * Called on every @mention and /inkeep command — caching avoids redundant DB queries.
31
+ */
32
+ async function findCachedUserMapping(tenantId, slackUserId, teamId, clientId = "work-apps-slack") {
33
+ const cacheKey = `${tenantId}:${slackUserId}:${teamId}:${clientId}`;
34
+ const cached = userMappingCache.get(cacheKey);
35
+ if (cached && cached.expiresAt > Date.now()) return cached.mapping;
36
+ const mapping = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, slackUserId, teamId, clientId);
37
+ if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
38
+ evictExpiredEntries();
39
+ if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
40
+ const oldestKey = userMappingCache.keys().next().value;
41
+ if (oldestKey) userMappingCache.delete(oldestKey);
42
+ }
43
+ }
44
+ userMappingCache.set(cacheKey, {
45
+ mapping,
46
+ expiresAt: Date.now() + USER_MAPPING_CACHE_TTL_MS
47
+ });
48
+ return mapping;
49
+ }
50
+ /**
51
+ * Convert standard Markdown to Slack's mrkdwn format
52
+ *
53
+ * Key differences:
54
+ * - **bold** or __bold__ → *bold*
55
+ * - *italic* (when not bold) → _italic_
56
+ * - # Header → *Header* (Slack has no headers)
57
+ * - [text](url) → <url|text>
58
+ * - Keeps code blocks, inline code, and lists as-is
59
+ */
60
+ function markdownToMrkdwn(markdown) {
61
+ if (!markdown) return markdown;
62
+ let result = markdown;
63
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
64
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
65
+ result = result.replace(/\*\*([^*]+)\*\*/g, "*$1*");
66
+ result = result.replace(/__([^_]+)__/g, "*$1*");
67
+ result = result.replace(/~~([^~]+)~~/g, "~$1~");
68
+ return result;
69
+ }
70
+ /**
71
+ * Error types for user-friendly error messages
72
+ */
73
+ let SlackErrorType = /* @__PURE__ */ function(SlackErrorType$1) {
74
+ SlackErrorType$1["TIMEOUT"] = "timeout";
75
+ SlackErrorType$1["RATE_LIMIT"] = "rate_limit";
76
+ SlackErrorType$1["API_ERROR"] = "api_error";
77
+ SlackErrorType$1["AUTH_ERROR"] = "auth_error";
78
+ SlackErrorType$1["UNKNOWN"] = "unknown";
79
+ return SlackErrorType$1;
80
+ }({});
81
+ /**
82
+ * Classify an error into a SlackErrorType for appropriate user messaging
83
+ */
84
+ function classifyError(error, httpStatus) {
85
+ if (httpStatus === 429) return SlackErrorType.RATE_LIMIT;
86
+ if (httpStatus === 401 || httpStatus === 403) return SlackErrorType.AUTH_ERROR;
87
+ if (httpStatus && httpStatus >= 400) return SlackErrorType.API_ERROR;
88
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
89
+ if (errorMessage.includes("timeout") || errorMessage.includes("timed out") || errorMessage.includes("aborted") || errorMessage.includes("econnreset")) return SlackErrorType.TIMEOUT;
90
+ if (errorMessage.includes("rate limit") || errorMessage.includes("too many requests")) return SlackErrorType.RATE_LIMIT;
91
+ if (errorMessage.includes("unauthorized") || errorMessage.includes("forbidden")) return SlackErrorType.AUTH_ERROR;
92
+ return SlackErrorType.UNKNOWN;
93
+ }
94
+ /**
95
+ * Get a user-friendly error message based on error type
96
+ */
97
+ function getUserFriendlyErrorMessage(errorType, agentName) {
98
+ const agent = agentName || "The agent";
99
+ switch (errorType) {
100
+ case SlackErrorType.TIMEOUT: return `⏱️ *Request timed out*\n\n${agent} took too long to respond. This can happen with complex queries.\n\n*Try:*\n• Simplifying your question\n• Breaking it into smaller parts\n• Trying again in a moment`;
101
+ case SlackErrorType.RATE_LIMIT: return `⚠️ *Too many requests*\n\nYou've hit the rate limit. Please wait a moment before trying again.\n\n*Tip:* Space out your requests to avoid this.`;
102
+ case SlackErrorType.AUTH_ERROR: return `🔐 *Authentication issue*\n\nThere was a problem with your account connection.\n\n*Try:*\n• Running \`/inkeep link\` to re-link your account\n• Contacting your workspace admin if the issue persists`;
103
+ case SlackErrorType.API_ERROR: return `❌ *Something went wrong*\n\n${agent} encountered an error processing your request.\n\n*Try:*\n• Rephrasing your question\n• Trying again in a moment\n• Using \`/inkeep help\` for more options`;
104
+ default: return `❌ *Unexpected error*\n\nSomething went wrong while processing your request.\n\n*Try:*\n• Trying again in a moment\n• Using \`/inkeep help\` for more options`;
105
+ }
106
+ }
107
+ const INTERNAL_FETCH_TIMEOUT_MS = 1e4;
108
+ async function fetchProjectsForTenant(tenantId) {
109
+ const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
110
+ const token = await generateInternalServiceToken({
111
+ serviceId: InternalServices.INKEEP_AGENTS_MANAGE_API,
112
+ tenantId
113
+ });
114
+ const controller = new AbortController();
115
+ const timeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
116
+ try {
117
+ const response = await fetch(`${apiUrl}/manage/tenants/${tenantId}/projects?limit=50`, {
118
+ method: "GET",
119
+ headers: {
120
+ Authorization: `Bearer ${token}`,
121
+ "Content-Type": "application/json"
122
+ },
123
+ signal: controller.signal
124
+ });
125
+ if (!response.ok) {
126
+ const errorBody = await response.text().catch(() => "");
127
+ logger.warn({
128
+ status: response.status,
129
+ tenantId,
130
+ errorBody
131
+ }, "Failed to fetch projects from API");
132
+ return [];
133
+ }
134
+ const result = await response.json();
135
+ logger.debug({
136
+ tenantId,
137
+ projectCount: result.data.length
138
+ }, "Fetched projects from API");
139
+ return result.data.map((p) => ({
140
+ id: p.id,
141
+ name: p.name || p.id
142
+ }));
143
+ } catch (error) {
144
+ logger.error({
145
+ error,
146
+ tenantId
147
+ }, "Error fetching projects from API");
148
+ return [];
149
+ } finally {
150
+ clearTimeout(timeout);
151
+ }
152
+ }
153
+ async function fetchAgentsForProject(tenantId, projectId) {
154
+ const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
155
+ const token = await generateInternalServiceToken({
156
+ serviceId: InternalServices.INKEEP_AGENTS_MANAGE_API,
157
+ tenantId,
158
+ projectId
159
+ });
160
+ const controller = new AbortController();
161
+ const timeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
162
+ try {
163
+ const response = await fetch(`${apiUrl}/manage/tenants/${tenantId}/projects/${projectId}/agents?limit=50`, {
164
+ method: "GET",
165
+ headers: {
166
+ Authorization: `Bearer ${token}`,
167
+ "Content-Type": "application/json",
168
+ "x-inkeep-project-id": projectId
169
+ },
170
+ signal: controller.signal
171
+ });
172
+ if (!response.ok) {
173
+ const errorBody = await response.text().catch(() => "");
174
+ logger.warn({
175
+ status: response.status,
176
+ tenantId,
177
+ projectId,
178
+ errorBody
179
+ }, "Failed to fetch agents from API");
180
+ return [];
181
+ }
182
+ const result = await response.json();
183
+ logger.debug({
184
+ tenantId,
185
+ projectId,
186
+ agentCount: result.data.length
187
+ }, "Fetched agents from API");
188
+ return result.data.map((a) => ({
189
+ id: a.id,
190
+ name: a.name || a.id,
191
+ projectId,
192
+ projectName: projectId
193
+ }));
194
+ } catch (error) {
195
+ logger.error({
196
+ error,
197
+ tenantId,
198
+ projectId
199
+ }, "Error fetching agents from API");
200
+ return [];
201
+ } finally {
202
+ clearTimeout(timeout);
203
+ }
204
+ }
205
+ async function getWorkspaceDefaultAgent(teamId) {
206
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
207
+ if (workspace?.defaultAgent) {
208
+ logger.debug({ teamId }, "Found workspace default agent");
209
+ return workspace.defaultAgent;
210
+ }
211
+ return null;
212
+ }
213
+ async function getChannelAgentConfig(teamId, channelId) {
214
+ return resolveChannelAgentConfig(teamId, channelId, await findWorkspaceConnectionByTeamId(teamId));
215
+ }
216
+ /**
217
+ * Resolve channel agent config using a pre-resolved workspace connection.
218
+ * Avoids redundant workspace lookups when the connection is already available.
219
+ */
220
+ async function resolveChannelAgentConfig(teamId, channelId, workspace) {
221
+ const tenantId = workspace?.tenantId;
222
+ if (!tenantId) return null;
223
+ const channelConfig = await findWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
224
+ if (channelConfig?.enabled) return {
225
+ projectId: channelConfig.projectId,
226
+ agentId: channelConfig.agentId,
227
+ agentName: channelConfig.agentName || channelConfig.agentId,
228
+ projectName: channelConfig.projectId
229
+ };
230
+ return workspace?.defaultAgent || null;
231
+ }
232
+ async function sendResponseUrlMessage(responseUrl, message) {
233
+ try {
234
+ const payload = { text: message.text };
235
+ if (message.replace_original) payload.replace_original = true;
236
+ else if (message.delete_original) payload.delete_original = true;
237
+ else if (message.response_type) payload.response_type = message.response_type;
238
+ if (message.blocks) payload.blocks = message.blocks;
239
+ logger.info({
240
+ hasBlocks: !!message.blocks,
241
+ blockCount: Array.isArray(message.blocks) ? message.blocks.length : 0,
242
+ replace: !!message.replace_original
243
+ }, "Sending response_url message");
244
+ const controller = new AbortController();
245
+ const responseTimeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
246
+ const response = await fetch(responseUrl, {
247
+ method: "POST",
248
+ headers: { "Content-Type": "application/json" },
249
+ body: JSON.stringify(payload),
250
+ signal: controller.signal
251
+ });
252
+ clearTimeout(responseTimeout);
253
+ const responseBody = await response.text().catch(() => "");
254
+ logger.info({
255
+ status: response.status,
256
+ responseBody: responseBody.slice(0, 300)
257
+ }, "response_url response received");
258
+ if (!response.ok) logger.error({
259
+ status: response.status,
260
+ errorBody: responseBody
261
+ }, "response_url request failed");
262
+ } catch (error) {
263
+ const errorMessage = error instanceof Error ? error.message : String(error);
264
+ logger.error({ errorMessage }, "Failed to send response_url message");
265
+ }
266
+ }
267
+ /**
268
+ * Generate a deterministic conversation ID for Slack threads/DMs.
269
+ * This ensures the same thread + agent combination gets the same conversation ID,
270
+ * allowing the agent to maintain conversation history.
271
+ *
272
+ * Including agentId ensures switching agents in the same thread starts a fresh
273
+ * conversation, avoiding sub-agent conflicts when the Run API tries to resume
274
+ * a conversation that was started by a different agent.
275
+ *
276
+ * Format: slack-thread-{teamId}-{identifier}[-{agentId}]
277
+ */
278
+ function generateSlackConversationId(params) {
279
+ const { teamId, threadTs, channel, isDM, agentId } = params;
280
+ const base = isDM ? `slack-dm-${teamId}-${channel}` : `slack-thread-${teamId}-${threadTs || channel}`;
281
+ return agentId ? `${base}-${agentId}` : base;
282
+ }
283
+ /**
284
+ * Check if a thread was initiated by the bot (i.e., the parent message is from the bot).
285
+ * This helps distinguish "bot threads" (where users are conversing with the bot)
286
+ * from "user threads" (where users are having their own conversation).
287
+ *
288
+ * Uses conversations.replies which returns thread messages with the parent as the first message.
289
+ */
290
+ async function checkIfBotThread(slackClient, channel, threadTs) {
291
+ try {
292
+ const parentMessage = (await slackClient.conversations.replies({
293
+ channel,
294
+ ts: threadTs,
295
+ limit: 1
296
+ })).messages?.[0];
297
+ if (!parentMessage) {
298
+ logger.debug({
299
+ channel,
300
+ threadTs
301
+ }, "No parent message found for thread");
302
+ return false;
303
+ }
304
+ const isBotThread = Boolean(parentMessage.bot_id);
305
+ logger.debug({
306
+ channel,
307
+ threadTs,
308
+ isBotThread,
309
+ botId: parentMessage.bot_id
310
+ }, "Checked if thread is bot-owned");
311
+ return isBotThread;
312
+ } catch (error) {
313
+ logger.warn({
314
+ error,
315
+ channel,
316
+ threadTs
317
+ }, "Failed to check if thread is bot-owned");
318
+ return false;
319
+ }
320
+ }
321
+ async function getThreadContext(slackClient, channel, threadTs, options = {}) {
322
+ const { includeLastMessage = false, resolveUserNames = true } = options;
323
+ try {
324
+ const threadMessages = await slackClient.conversations.replies({
325
+ channel,
326
+ ts: threadTs,
327
+ limit: 50
328
+ });
329
+ if (!threadMessages.messages || threadMessages.messages.length === 0) return "";
330
+ const messagesToProcess = includeLastMessage ? threadMessages.messages : threadMessages.messages.slice(0, -1);
331
+ if (messagesToProcess.length === 0) return "";
332
+ const userNameCache = /* @__PURE__ */ new Map();
333
+ if (resolveUserNames && slackClient.users) {
334
+ const uniqueUserIds = [...new Set(messagesToProcess.filter((m) => !!m.user).map((m) => m.user))];
335
+ await Promise.all(uniqueUserIds.map(async (userId) => {
336
+ try {
337
+ const userInfo = await slackClient.users?.info({ user: userId });
338
+ const name = userInfo?.user?.display_name || userInfo?.user?.real_name || userInfo?.user?.name || userId;
339
+ userNameCache.set(userId, name);
340
+ } catch {
341
+ userNameCache.set(userId, userId);
342
+ }
343
+ }));
344
+ }
345
+ return messagesToProcess.map((msg, index) => {
346
+ const isBot = !!msg.bot_id;
347
+ const isParent = index === 0;
348
+ let role;
349
+ if (isBot) role = "Inkeep Agent";
350
+ else if (msg.user) role = resolveUserNames ? userNameCache.get(msg.user) || msg.user : `<@${msg.user}>`;
351
+ else role = "Unknown";
352
+ const prefix = isParent ? "[Thread Start] " : "";
353
+ const messageText = msg.text?.replace(/<@U[A-Z0-9]+>/g, (match) => {
354
+ const userId = match.slice(2, -1);
355
+ return `@${userNameCache.get(userId) || userId}`;
356
+ }) || "";
357
+ return `${prefix}${role}: ${messageText}`;
358
+ }).join("\n\n");
359
+ } catch (threadError) {
360
+ logger.warn({
361
+ threadError,
362
+ channel,
363
+ threadTs
364
+ }, "Failed to fetch thread context");
365
+ }
366
+ return "";
367
+ }
368
+
369
+ //#endregion
370
+ export { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, resolveChannelAgentConfig, sendResponseUrlMessage };
@@ -0,0 +1,16 @@
1
+ import { AgentResolutionParams, ResolvedAgentConfig, getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
2
+ import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
3
+ import { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
4
+ import { DefaultAgentConfig, SlackWorkspaceConnection, WorkspaceInstallData, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
5
+ import { SlackCommandPayload, SlackCommandResponse } from "./types.js";
6
+ import { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
7
+ import { InlineSelectorMetadata, handleAppMention } from "./events/app-mention.js";
8
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
9
+ import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
10
+ import { AgentOption, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, FollowUpModalMetadata, ModalMetadata, buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
11
+ import { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
12
+ import { StreamResult, streamAgentResponse } from "./events/streaming.js";
13
+ import "./events/index.js";
14
+ import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
15
+ import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
16
+ export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, FollowUpButtonParams, FollowUpModalMetadata, InlineSelectorMetadata, ModalMetadata, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, WorkspaceInstallData, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAgentListMessage, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentListCommand, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -0,0 +1,16 @@
1
+ import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
2
+ import { getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
3
+ import { buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
4
+ import { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
5
+ import { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
6
+ import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
7
+ import { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
8
+ import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
9
+ import { streamAgentResponse } from "./events/streaming.js";
10
+ import { handleAppMention } from "./events/app-mention.js";
11
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
12
+ import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
13
+ import "./events/index.js";
14
+ import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
15
+
16
+ export { SlackErrorType, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAgentListMessage, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentListCommand, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -0,0 +1,86 @@
1
+ import { ModalView } from "@slack/web-api";
2
+
3
+ //#region src/slack/services/modals.d.ts
4
+
5
+ /** Agent option for dropdown selection */
6
+ interface AgentOption {
7
+ id: string;
8
+ name: string | null;
9
+ projectId: string;
10
+ projectName: string | null;
11
+ }
12
+ interface ModalMetadata {
13
+ channel: string;
14
+ threadTs?: string;
15
+ messageTs: string;
16
+ teamId: string;
17
+ slackUserId: string;
18
+ tenantId: string;
19
+ isInThread: boolean;
20
+ threadMessageCount?: number;
21
+ buttonResponseUrl?: string;
22
+ messageContext?: string;
23
+ }
24
+ interface FollowUpModalMetadata {
25
+ conversationId: string;
26
+ agentId: string;
27
+ projectId: string;
28
+ tenantId: string;
29
+ teamId: string;
30
+ slackUserId: string;
31
+ channel: string;
32
+ }
33
+ interface BuildAgentSelectorModalParams {
34
+ projects: Array<{
35
+ id: string;
36
+ name: string;
37
+ }>;
38
+ agents: AgentOption[];
39
+ metadata: ModalMetadata;
40
+ selectedProjectId?: string;
41
+ }
42
+ /**
43
+ * Build the agent selector modal.
44
+ *
45
+ * Shows:
46
+ * - Project dropdown
47
+ * - Agent dropdown (updates based on project selection)
48
+ * - Include thread context checkbox (if in thread)
49
+ * - Question/instructions input
50
+ * - Dashboard link
51
+ *
52
+ * All responses from this modal are private (ephemeral).
53
+ */
54
+ declare function buildAgentSelectorModal(params: BuildAgentSelectorModalParams): ModalView;
55
+ /**
56
+ * Build a follow-up modal for continuing a conversation.
57
+ *
58
+ * Shows only a prompt input. Agent and project are carried from the previous turn
59
+ * via metadata. The conversationId ensures the agent has full history.
60
+ */
61
+ declare function buildFollowUpModal(metadata: FollowUpModalMetadata): ModalView;
62
+ interface BuildMessageShortcutModalParams {
63
+ projects: Array<{
64
+ id: string;
65
+ name: string;
66
+ }>;
67
+ agents: AgentOption[];
68
+ metadata: ModalMetadata;
69
+ selectedProjectId?: string;
70
+ messageContext: string;
71
+ }
72
+ /**
73
+ * Build the modal for message shortcut (context menu on a message).
74
+ *
75
+ * Shows:
76
+ * - Message context (read-only display)
77
+ * - Project dropdown
78
+ * - Agent dropdown
79
+ * - Additional instructions input
80
+ * - Dashboard link
81
+ *
82
+ * All responses from this modal are private (ephemeral).
83
+ */
84
+ declare function buildMessageShortcutModal(params: BuildMessageShortcutModalParams): ModalView;
85
+ //#endregion
86
+ export { AgentOption, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, FollowUpModalMetadata, ModalMetadata, buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal };