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