@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.
- package/dist/env.d.ts +24 -2
- package/dist/env.js +13 -2
- package/dist/github/mcp/index.d.ts +2 -2
- package/dist/github/mcp/index.js +23 -34
- package/dist/github/routes/setup.d.ts +2 -2
- package/dist/github/routes/webhooks.d.ts +2 -2
- package/dist/slack/i18n/index.d.ts +2 -0
- package/dist/slack/i18n/index.js +3 -0
- package/dist/slack/i18n/strings.d.ts +73 -0
- package/dist/slack/i18n/strings.js +67 -0
- package/dist/slack/index.d.ts +18 -0
- package/dist/slack/index.js +28 -0
- package/dist/slack/middleware/permissions.d.ts +31 -0
- package/dist/slack/middleware/permissions.js +167 -0
- package/dist/slack/routes/events.d.ts +10 -0
- package/dist/slack/routes/events.js +551 -0
- package/dist/slack/routes/index.d.ts +10 -0
- package/dist/slack/routes/index.js +47 -0
- package/dist/slack/routes/oauth.d.ts +20 -0
- package/dist/slack/routes/oauth.js +344 -0
- package/dist/slack/routes/users.d.ts +10 -0
- package/dist/slack/routes/users.js +365 -0
- package/dist/slack/routes/workspaces.d.ts +10 -0
- package/dist/slack/routes/workspaces.js +909 -0
- package/dist/slack/services/agent-resolution.d.ts +41 -0
- package/dist/slack/services/agent-resolution.js +99 -0
- package/dist/slack/services/blocks/index.d.ts +73 -0
- package/dist/slack/services/blocks/index.js +103 -0
- package/dist/slack/services/client.d.ts +108 -0
- package/dist/slack/services/client.js +232 -0
- package/dist/slack/services/commands/index.d.ts +19 -0
- package/dist/slack/services/commands/index.js +553 -0
- package/dist/slack/services/events/app-mention.d.ts +40 -0
- package/dist/slack/services/events/app-mention.js +304 -0
- package/dist/slack/services/events/block-actions.d.ts +40 -0
- package/dist/slack/services/events/block-actions.js +265 -0
- package/dist/slack/services/events/index.d.ts +6 -0
- package/dist/slack/services/events/index.js +7 -0
- package/dist/slack/services/events/modal-submission.d.ts +30 -0
- package/dist/slack/services/events/modal-submission.js +400 -0
- package/dist/slack/services/events/streaming.d.ts +26 -0
- package/dist/slack/services/events/streaming.js +272 -0
- package/dist/slack/services/events/utils.d.ts +146 -0
- package/dist/slack/services/events/utils.js +370 -0
- package/dist/slack/services/index.d.ts +16 -0
- package/dist/slack/services/index.js +16 -0
- package/dist/slack/services/modals.d.ts +86 -0
- package/dist/slack/services/modals.js +355 -0
- package/dist/slack/services/nango.d.ts +85 -0
- package/dist/slack/services/nango.js +476 -0
- package/dist/slack/services/security.d.ts +35 -0
- package/dist/slack/services/security.js +65 -0
- package/dist/slack/services/types.d.ts +26 -0
- package/dist/slack/services/types.js +1 -0
- package/dist/slack/services/workspace-tokens.d.ts +25 -0
- package/dist/slack/services/workspace-tokens.js +27 -0
- package/dist/slack/tracer.d.ts +40 -0
- package/dist/slack/tracer.js +39 -0
- package/dist/slack/types.d.ts +10 -0
- package/dist/slack/types.js +1 -0
- 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 };
|