@inkeep/agents-work-apps 0.0.0-dev-20260224193013 → 0.0.0-dev-20260224195557

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.
@@ -4,10 +4,10 @@ import "./routes/setup.js";
4
4
  import "./routes/tokenExchange.js";
5
5
  import { WebhookVerificationResult, verifyWebhookSignature } from "./routes/webhooks.js";
6
6
  import { Hono } from "hono";
7
- import * as hono_types2 from "hono/types";
7
+ import * as hono_types0 from "hono/types";
8
8
 
9
9
  //#region src/github/index.d.ts
10
- declare function createGithubRoutes(): Hono<hono_types2.BlankEnv, hono_types2.BlankSchema, "/">;
11
- declare const githubRoutes: Hono<hono_types2.BlankEnv, hono_types2.BlankSchema, "/">;
10
+ declare function createGithubRoutes(): Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
11
+ declare const githubRoutes: Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
12
12
  //#endregion
13
13
  export { GenerateInstallationAccessTokenResult, GenerateTokenError, GenerateTokenResult, GitHubAppConfig, InstallationAccessToken, InstallationInfo, LookupInstallationError, LookupInstallationForRepoResult, LookupInstallationResult, WebhookVerificationResult, clearConfigCache, createAppJwt, createGithubRoutes, determineStatus, fetchInstallationDetails, fetchInstallationRepositories, generateInstallationAccessToken, getGitHubAppConfig, getGitHubAppName, getStateSigningSecret, getWebhookSecret, githubRoutes, isGitHubAppConfigured, isGitHubAppNameConfigured, isStateSigningConfigured, isWebhookConfigured, lookupInstallationForRepo, validateGitHubAppConfigOnStartup, validateGitHubInstallFlowConfigOnStartup, validateGitHubWebhookConfigOnStartup, verifyWebhookSignature };
@@ -1,7 +1,7 @@
1
- import * as hono0 from "hono";
1
+ import * as hono1 from "hono";
2
2
 
3
3
  //#region src/github/mcp/auth.d.ts
4
- declare const githubMcpAuth: () => hono0.MiddlewareHandler<{
4
+ declare const githubMcpAuth: () => hono1.MiddlewareHandler<{
5
5
  Variables: {
6
6
  toolId: string;
7
7
  };
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types1 from "hono/types";
2
+ import * as hono_types0 from "hono/types";
3
3
 
4
4
  //#region src/github/mcp/index.d.ts
5
5
  declare const app: Hono<{
6
6
  Variables: {
7
7
  toolId: string;
8
8
  };
9
- }, hono_types1.BlankSchema, "/">;
9
+ }, hono_types0.BlankSchema, "/">;
10
10
  //#endregion
11
11
  export { app as default };
@@ -76,8 +76,8 @@ declare const ChangedFileSchema: z.ZodObject<{
76
76
  path: z.ZodString;
77
77
  status: z.ZodEnum<{
78
78
  added: "added";
79
- removed: "removed";
80
79
  modified: "modified";
80
+ removed: "removed";
81
81
  renamed: "renamed";
82
82
  copied: "copied";
83
83
  changed: "changed";
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types0 from "hono/types";
2
+ import * as hono_types6 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/setup.d.ts
5
- declare const app: Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types8 from "hono/types";
2
+ import * as hono_types4 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/tokenExchange.d.ts
5
- declare const app: Hono<hono_types8.BlankEnv, hono_types8.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types6 from "hono/types";
2
+ import * as hono_types8 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/webhooks.d.ts
5
5
  interface WebhookVerificationResult {
@@ -7,6 +7,6 @@ interface WebhookVerificationResult {
7
7
  error?: string;
8
8
  }
9
9
  declare function verifyWebhookSignature(payload: string, signature: string | undefined, secret: string): WebhookVerificationResult;
10
- declare const app: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
10
+ declare const app: Hono<hono_types8.BlankEnv, hono_types8.BlankSchema, "/">;
11
11
  //#endregion
12
12
  export { WebhookVerificationResult, app as default, verifyWebhookSignature };
@@ -2,8 +2,9 @@ import { getLogger } from "../../logger.js";
2
2
  import runDbClient_default from "../../db/runDbClient.js";
3
3
  import { createConnectSession } from "../services/nango.js";
4
4
  import "../services/index.js";
5
+ import { resumeSmartLinkIntent } from "../services/resume-intent.js";
5
6
  import { OpenAPIHono, z } from "@hono/zod-openapi";
6
- import { createWorkAppSlackUserMapping, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingByInkeepUserId, isUniqueConstraintError, verifySlackLinkToken } from "@inkeep/agents-core";
7
+ import { createWorkAppSlackUserMapping, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingByInkeepUserId, flushTraces, getWaitUntil, isUniqueConstraintError, verifySlackLinkToken } from "@inkeep/agents-core";
7
8
  import { createProtectedRoute, inheritedWorkAppsAuth } from "@inkeep/agents-core/middleware";
8
9
 
9
10
  //#region src/slack/routes/users.ts
@@ -105,8 +106,13 @@ app.openapi(createProtectedRoute({
105
106
  try {
106
107
  const verifyResult = await verifySlackLinkToken(body.token);
107
108
  if (!verifyResult.valid || !verifyResult.payload) {
108
- logger.warn({ error: verifyResult.error }, "Invalid link token");
109
- return c.json({ error: verifyResult.error || "Invalid or expired link token. Please run /inkeep link in Slack to get a new one." }, 400);
109
+ const isExpired = verifyResult.error?.includes("\"exp\" claim timestamp check failed");
110
+ const errorMessage = isExpired ? "Token expired. Please run /inkeep link in Slack to get a new one." : verifyResult.error || "Invalid or expired link token. Please run /inkeep link in Slack to get a new one.";
111
+ logger.warn({
112
+ error: verifyResult.error,
113
+ isExpired
114
+ }, "Invalid link token");
115
+ return c.json({ error: errorMessage }, 400);
110
116
  }
111
117
  const { tenantId, slack } = verifyResult.payload;
112
118
  const { teamId, userId: slackUserId, enterpriseId, username } = slack;
@@ -157,6 +163,26 @@ app.openapi(createProtectedRoute({
157
163
  inkeepUserId: body.userId,
158
164
  linkId: slackUserMapping.id
159
165
  }, "Successfully linked Slack user to Inkeep account via JWT token");
166
+ const { intent } = verifyResult.payload;
167
+ if (intent) {
168
+ logger.info({
169
+ event: "smart_link_intent_resume_triggered",
170
+ entryPoint: intent.entryPoint,
171
+ questionLength: intent.question.length
172
+ }, "Smart link intent detected in verify-token");
173
+ const resumeWork = resumeSmartLinkIntent({
174
+ intent,
175
+ teamId,
176
+ slackUserId,
177
+ inkeepUserId,
178
+ tenantId,
179
+ slackEnterpriseId: enterpriseId,
180
+ slackUsername: username
181
+ }).catch((error) => logger.error({ error }, "Resume smart link intent failed")).finally(() => flushTraces());
182
+ const waitUntil = await getWaitUntil();
183
+ if (waitUntil) waitUntil(resumeWork);
184
+ else logger.warn({ entryPoint: intent.entryPoint }, "waitUntil not available, resume work may not complete");
185
+ }
160
186
  return c.json({
161
187
  success: true,
162
188
  linkId: slackUserMapping.id,
@@ -71,6 +71,7 @@ interface AgentConfigSources {
71
71
  } | null;
72
72
  }
73
73
  declare function createStatusMessage(email: string, linkedAt: string, dashboardUrl: string, agentConfigs: AgentConfigSources): Readonly<slack_block_builder0.SlackMessageDto>;
74
+ declare function createSmartLinkMessage(linkUrl: string): Readonly<slack_block_builder0.SlackMessageDto>;
74
75
  interface ToolApprovalButtonValue {
75
76
  toolCallId: string;
76
77
  conversationId: string;
@@ -149,7 +150,6 @@ declare function buildCitationsBlock(citations: Array<{
149
150
  title?: string;
150
151
  url?: string;
151
152
  }>): any[];
152
- declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
153
153
  declare function createCreateInkeepAccountMessage(acceptUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
154
154
  //#endregion
155
- export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
155
+ export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -91,6 +91,9 @@ function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
91
91
  if (projectLine) lines.push(projectLine);
92
92
  return Message().blocks(Blocks.Section().text(lines.join("\n")), Blocks.Actions().elements(Elements.Button().text(SlackStrings.buttons.openDashboard).url(dashboardUrl).actionId("open_dashboard"))).buildToObject();
93
93
  }
94
+ function createSmartLinkMessage(linkUrl) {
95
+ return Message().blocks(Blocks.Section().text("To get started, let's connect your Inkeep account with Slack."), Blocks.Actions().elements(Elements.Button().text("Link Account").url(linkUrl).actionId("smart_link_account").primary()), Blocks.Context().elements("🕐 This only needs to happen once.")).buildToObject();
96
+ }
94
97
  const ToolApprovalButtonValueSchema = z.object({
95
98
  toolCallId: z.string(),
96
99
  conversationId: z.string(),
@@ -316,12 +319,9 @@ function buildCitationsBlock(citations) {
316
319
  }
317
320
  }];
318
321
  }
319
- function createJwtLinkMessage(linkUrl, expiresInMinutes) {
320
- 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();
321
- }
322
322
  function createCreateInkeepAccountMessage(acceptUrl, expiresInMinutes) {
323
323
  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();
324
324
  }
325
325
 
326
326
  //#endregion
327
- export { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
327
+ export { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
@@ -11,7 +11,7 @@ declare function handleHelpCommand(): Promise<SlackCommandResponse>;
11
11
  * Similar to @mention behavior in channels
12
12
  */
13
13
  declare function handleAgentPickerCommand(payload: SlackCommandPayload, tenantId: string, workspaceConnection?: SlackWorkspaceConnection | null): Promise<SlackCommandResponse>;
14
- declare function handleQuestionCommand(payload: SlackCommandPayload, question: string, _dashboardUrl: string, tenantId: string): Promise<SlackCommandResponse>;
14
+ declare function handleQuestionCommand(payload: SlackCommandPayload, question: string, _dashboardUrl: string, tenantId: string, botToken: string): Promise<SlackCommandResponse>;
15
15
  declare function handleCommand(payload: SlackCommandPayload): Promise<SlackCommandResponse>;
16
16
  //#endregion
17
17
  export { handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand };
@@ -5,131 +5,32 @@ import { findWorkspaceConnectionByTeamId } from "../nango.js";
5
5
  import { extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, getChannelAgentConfig, sendResponseUrlMessage } from "../events/utils.js";
6
6
  import { resolveEffectiveAgent } from "../agent-resolution.js";
7
7
  import { SlackStrings } from "../../i18n/strings.js";
8
- import { createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
8
+ import { createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "../blocks/index.js";
9
9
  import { getSlackClient } from "../client.js";
10
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
10
11
  import { buildAgentSelectorModal } from "../modals.js";
11
- import { createInvitationInDb, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, findWorkAppSlackWorkspaceByTeamId, flushTraces, getInProcessFetch, getOrganizationMemberByEmail, getPendingInvitationsByEmail, getWaitUntil, signSlackLinkToken, signSlackUserToken } from "@inkeep/agents-core";
12
+ import { deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingBySlackUser, flushTraces, getInProcessFetch, getWaitUntil, signSlackUserToken } from "@inkeep/agents-core";
12
13
 
13
14
  //#region src/slack/services/commands/index.ts
14
15
  const DEFAULT_CLIENT_ID = "work-apps-slack";
15
- const LINK_CODE_TTL_MINUTES = 10;
16
16
  const logger = getLogger("slack-commands");
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
17
  async function handleLinkCommand(payload, dashboardUrl, tenantId, botToken) {
89
18
  const existingLink = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
90
19
  if (existingLink) return {
91
20
  response_type: "ephemeral",
92
21
  ...createAlreadyLinkedMessage(existingLink.slackEmail || existingLink.slackUsername || "Unknown", existingLink.linkedAt, dashboardUrl)
93
22
  };
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({
23
+ try {
24
+ return {
25
+ response_type: "ephemeral",
26
+ ...buildLinkPromptMessage(await resolveUnlinkedUserAction({
99
27
  tenantId,
100
- slackTeamId: payload.teamId,
28
+ teamId: payload.teamId,
101
29
  slackUserId: payload.userId,
30
+ botToken: botToken || "",
102
31
  slackEnterpriseId: payload.enterpriseId,
103
32
  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
- }
117
- try {
118
- const linkToken = await signSlackLinkToken({
119
- tenantId,
120
- slackTeamId: payload.teamId,
121
- slackUserId: payload.userId,
122
- slackEnterpriseId: payload.enterpriseId,
123
- slackUsername: payload.userName
124
- });
125
- const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
126
- logger.info({
127
- slackUserId: payload.userId,
128
- tenantId
129
- }, "Generated JWT link token");
130
- return {
131
- response_type: "ephemeral",
132
- ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
33
+ }))
133
34
  };
134
35
  } catch (error) {
135
36
  logger.error({
@@ -278,23 +179,27 @@ async function handleAgentPickerCommand(payload, tenantId, workspaceConnection)
278
179
  };
279
180
  }
280
181
  }
281
- async function generateLinkCodeWithIntent(payload, tenantId) {
182
+ async function generateLinkCodeWithIntent(payload, tenantId, botToken, intent) {
282
183
  try {
283
- const linkToken = await signSlackLinkToken({
184
+ const linkResult = await resolveUnlinkedUserAction({
284
185
  tenantId,
285
- slackTeamId: payload.teamId,
186
+ teamId: payload.teamId,
286
187
  slackUserId: payload.userId,
188
+ botToken,
287
189
  slackEnterpriseId: payload.enterpriseId,
288
- slackUsername: payload.userName
190
+ slackUsername: payload.userName,
191
+ intent
289
192
  });
290
- const linkUrl = `${env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000"}/link?token=${encodeURIComponent(linkToken)}`;
291
- logger.info({
292
- slackUserId: payload.userId,
293
- tenantId
294
- }, "Generated JWT link token with intent");
193
+ if (!!intent) logger.info({
194
+ event: "smart_link_intent_captured",
195
+ entryPoint: intent.entryPoint,
196
+ linkType: linkResult.type,
197
+ questionLength: intent.question.length,
198
+ channelId: payload.channelId
199
+ }, "Smart link intent captured");
295
200
  return {
296
201
  response_type: "ephemeral",
297
- ...createJwtLinkMessage(linkUrl, LINK_CODE_TTL_MINUTES)
202
+ ...buildLinkPromptMessage(linkResult)
298
203
  };
299
204
  } catch (error) {
300
205
  logger.error({
@@ -308,9 +213,14 @@ async function generateLinkCodeWithIntent(payload, tenantId) {
308
213
  };
309
214
  }
310
215
  }
311
- async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId) {
216
+ async function handleQuestionCommand(payload, question, _dashboardUrl, tenantId, botToken) {
312
217
  const existingLink = await findWorkAppSlackUserMappingBySlackUser(runDbClient_default)(payload.userId, payload.teamId, DEFAULT_CLIENT_ID);
313
- if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId);
218
+ if (!existingLink) return generateLinkCodeWithIntent(payload, tenantId, botToken, {
219
+ entryPoint: "question_command",
220
+ question: question.slice(0, 2e3),
221
+ channelId: payload.channelId,
222
+ responseUrl: payload.responseUrl
223
+ });
314
224
  const userTenantId = existingLink.tenantId;
315
225
  const resolvedAgent = await resolveEffectiveAgent({
316
226
  tenantId: userTenantId,
@@ -464,7 +374,7 @@ async function handleCommand(payload) {
464
374
  case "disconnect": return handleUnlinkCommand(payload, tenantId);
465
375
  case "help": return handleHelpCommand();
466
376
  case "": return handleAgentPickerCommand(payload, tenantId, workspaceConnection);
467
- default: return handleQuestionCommand(payload, text, dashboardUrl, tenantId);
377
+ default: return handleQuestionCommand(payload, text, dashboardUrl, tenantId, workspaceConnection.botToken);
468
378
  }
469
379
  }
470
380
 
@@ -5,25 +5,12 @@ import { checkIfBotThread, classifyError, findCachedUserMapping, formatChannelCo
5
5
  import { resolveEffectiveAgent } from "../agent-resolution.js";
6
6
  import { SlackStrings } from "../../i18n/strings.js";
7
7
  import { getSlackChannelInfo, getSlackClient, getSlackUserInfo, postMessageInThread } from "../client.js";
8
+ import { buildLinkPromptMessage, resolveUnlinkedUserAction } from "../link-prompt.js";
8
9
  import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, setSpanWithError, tracer } from "../../tracer.js";
9
10
  import { streamAgentResponse } from "./streaming.js";
10
11
  import { signSlackUserToken } from "@inkeep/agents-core";
11
12
 
12
13
  //#region src/slack/services/events/app-mention.ts
13
- /**
14
- * Handler for Slack @mention events
15
- *
16
- * Flow:
17
- * 1. Resolve workspace connection (single lookup, cached)
18
- * 2. Parallel: resolve agent config + check user link
19
- * 3. If no agent configured → prompt to set up in dashboard
20
- * 4. If not linked → prompt to link account
21
- * 5. Handle based on context:
22
- * - Channel + no query → Show usage hint
23
- * - Channel + query → Execute agent with streaming response
24
- * - Thread + no query → Auto-execute agent with thread context as query
25
- * - Thread + query → Execute agent with thread context included
26
- */
27
14
  const logger = getLogger("slack-app-mention");
28
15
  /**
29
16
  * Main handler for @mention events in Slack
@@ -123,11 +110,36 @@ async function handleAppMention(params) {
123
110
  teamId,
124
111
  channel
125
112
  }, "User not linked — prompting account link");
113
+ const intent = {
114
+ entryPoint: "mention",
115
+ question: text.slice(0, 2e3),
116
+ channelId: channel,
117
+ threadTs: isInThread ? threadTs : void 0,
118
+ messageTs,
119
+ agentId: agentConfig.agentId,
120
+ projectId: agentConfig.projectId
121
+ };
122
+ const linkResult = await resolveUnlinkedUserAction({
123
+ tenantId,
124
+ teamId,
125
+ slackUserId,
126
+ botToken,
127
+ intent
128
+ });
129
+ const message = buildLinkPromptMessage(linkResult);
130
+ logger.info({
131
+ event: "smart_link_intent_captured",
132
+ entryPoint: "mention",
133
+ linkType: linkResult.type,
134
+ questionLength: intent.question.length,
135
+ channelId: channel
136
+ }, "Smart link intent captured");
126
137
  await slackClient.chat.postEphemeral({
127
138
  channel,
128
139
  user: slackUserId,
129
140
  thread_ts: isInThread ? threadTs : void 0,
130
- text: "*Link your account to use @Inkeep*\n\nRun `/inkeep link` to connect your Slack and Inkeep accounts."
141
+ text: "To get started, let's connect your Inkeep account with Slack.",
142
+ blocks: message.blocks
131
143
  });
132
144
  span.end();
133
145
  return;
@@ -34,17 +34,19 @@ async function findCachedUserMapping(tenantId, slackUserId, teamId, clientId = "
34
34
  const cached = userMappingCache.get(cacheKey);
35
35
  if (cached && cached.expiresAt > Date.now()) return cached.mapping;
36
36
  const mapping = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, slackUserId, teamId, clientId);
37
- if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
38
- evictExpiredEntries();
37
+ if (mapping) {
39
38
  if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
40
- const oldestKey = userMappingCache.keys().next().value;
41
- if (oldestKey) userMappingCache.delete(oldestKey);
39
+ evictExpiredEntries();
40
+ if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
41
+ const oldestKey = userMappingCache.keys().next().value;
42
+ if (oldestKey) userMappingCache.delete(oldestKey);
43
+ }
42
44
  }
45
+ userMappingCache.set(cacheKey, {
46
+ mapping,
47
+ expiresAt: Date.now() + USER_MAPPING_CACHE_TTL_MS
48
+ });
43
49
  }
44
- userMappingCache.set(cacheKey, {
45
- mapping,
46
- expiresAt: Date.now() + USER_MAPPING_CACHE_TTL_MS
47
- });
48
50
  return mapping;
49
51
  }
50
52
  /**
@@ -1,5 +1,5 @@
1
1
  import { AgentResolutionParams, ResolvedAgentConfig, getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
2
- import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
2
+ import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
3
3
  import { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
4
4
  import { DefaultAgentConfig, SlackWorkspaceConnection, WorkspaceInstallData, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
5
5
  import { SlackCommandPayload, SlackCommandResponse } from "./types.js";
@@ -13,4 +13,4 @@ import { StreamResult, streamAgentResponse } from "./events/streaming.js";
13
13
  import "./events/index.js";
14
14
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
15
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, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, WorkspaceInstallData, buildAgentSelectorModal, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
16
+ export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, FollowUpButtonParams, FollowUpModalMetadata, InlineSelectorMetadata, ModalMetadata, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, WorkspaceInstallData, buildAgentSelectorModal, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -1,7 +1,7 @@
1
1
  import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
2
2
  import { SlackErrorType, checkIfBotThread, classifyError, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
3
3
  import { getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
4
- import { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
4
+ import { ToolApprovalButtonValueSchema, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, createAlreadyLinkedMessage, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
5
5
  import { checkUserIsChannelMember, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
6
6
  import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
7
7
  import { handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
@@ -13,4 +13,4 @@ import "./events/index.js";
13
13
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
14
14
  import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
15
15
 
16
- export { SlackErrorType, ToolApprovalButtonValueSchema, buildAgentSelectorModal, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
16
+ export { SlackErrorType, ToolApprovalButtonValueSchema, buildAgentSelectorModal, buildCitationsBlock, buildConversationResponseBlocks, buildDataArtifactBlocks, buildDataComponentBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
@@ -0,0 +1,27 @@
1
+ import { SlackLinkIntent } from "@inkeep/agents-core";
2
+ import * as slack_block_builder0 from "slack-block-builder";
3
+
4
+ //#region src/slack/services/link-prompt.d.ts
5
+ type LinkPromptResult = {
6
+ type: 'auto_invite';
7
+ url: string;
8
+ email: string;
9
+ expiresInMinutes: number;
10
+ } | {
11
+ type: 'jwt_link';
12
+ url: string;
13
+ expiresInMinutes: number;
14
+ };
15
+ interface ResolveLinkActionParams {
16
+ tenantId: string;
17
+ teamId: string;
18
+ slackUserId: string;
19
+ botToken: string;
20
+ slackEnterpriseId?: string;
21
+ slackUsername?: string;
22
+ intent?: SlackLinkIntent;
23
+ }
24
+ declare function resolveUnlinkedUserAction(params: ResolveLinkActionParams): Promise<LinkPromptResult>;
25
+ declare function buildLinkPromptMessage(result: LinkPromptResult): Readonly<slack_block_builder0.SlackMessageDto>;
26
+ //#endregion
27
+ export { LinkPromptResult, ResolveLinkActionParams, buildLinkPromptMessage, resolveUnlinkedUserAction };
@@ -0,0 +1,142 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import runDbClient_default from "../../db/runDbClient.js";
4
+ import { createCreateInkeepAccountMessage, createSmartLinkMessage } from "./blocks/index.js";
5
+ import { getSlackClient } from "./client.js";
6
+ import { createInvitationInDb, findWorkAppSlackWorkspaceByTeamId, getOrganizationMemberByEmail, getPendingInvitationsByEmail, signSlackLinkToken } from "@inkeep/agents-core";
7
+
8
+ //#region src/slack/services/link-prompt.ts
9
+ const logger = getLogger("slack-link-prompt");
10
+ const LINK_CODE_TTL_MINUTES = 10;
11
+ async function resolveUnlinkedUserAction(params) {
12
+ const { tenantId, teamId, slackUserId, botToken, slackEnterpriseId, slackUsername, intent } = params;
13
+ const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
14
+ const autoInvite = await tryAutoInvite({
15
+ tenantId,
16
+ teamId,
17
+ slackUserId,
18
+ botToken
19
+ });
20
+ if (autoInvite) {
21
+ const linkToken$1 = await signSlackLinkToken({
22
+ tenantId,
23
+ slackTeamId: teamId,
24
+ slackUserId,
25
+ slackEnterpriseId,
26
+ slackUsername,
27
+ intent
28
+ });
29
+ const authMethod = autoInvite.authMethod;
30
+ const linkReturnUrl = `/link?token=${encodeURIComponent(linkToken$1)}`;
31
+ const acceptUrl = authMethod === "email-password" ? `${manageUiUrl}/accept-invitation/${autoInvite.invitationId}?email=${encodeURIComponent(autoInvite.email)}&returnUrl=${encodeURIComponent(linkReturnUrl)}` : `${manageUiUrl}/login?invitation=${encodeURIComponent(autoInvite.invitationId)}&returnUrl=${encodeURIComponent(linkReturnUrl)}&email=${encodeURIComponent(autoInvite.email)}&authMethod=${encodeURIComponent(authMethod)}`;
32
+ logger.info({
33
+ invitationId: autoInvite.invitationId,
34
+ email: autoInvite.email,
35
+ hasIntent: !!intent
36
+ }, "Directing unlinked user to accept-invitation page");
37
+ return {
38
+ type: "auto_invite",
39
+ url: acceptUrl,
40
+ email: autoInvite.email,
41
+ expiresInMinutes: LINK_CODE_TTL_MINUTES
42
+ };
43
+ }
44
+ const linkToken = await signSlackLinkToken({
45
+ tenantId,
46
+ slackTeamId: teamId,
47
+ slackUserId,
48
+ slackEnterpriseId,
49
+ slackUsername,
50
+ intent
51
+ });
52
+ const linkUrl = `${manageUiUrl}/link?token=${encodeURIComponent(linkToken)}`;
53
+ logger.info({
54
+ slackUserId,
55
+ tenantId,
56
+ hasIntent: !!intent
57
+ }, "Generated JWT link token for unlinked user");
58
+ return {
59
+ type: "jwt_link",
60
+ url: linkUrl,
61
+ expiresInMinutes: LINK_CODE_TTL_MINUTES
62
+ };
63
+ }
64
+ function buildLinkPromptMessage(result) {
65
+ if (result.type === "auto_invite") return createCreateInkeepAccountMessage(result.url, result.expiresInMinutes);
66
+ return createSmartLinkMessage(result.url);
67
+ }
68
+ async function tryAutoInvite(params) {
69
+ const { tenantId, teamId, slackUserId, botToken } = params;
70
+ if (!botToken) return null;
71
+ try {
72
+ if (!(await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(tenantId, teamId))?.shouldAllowJoinFromWorkspace) {
73
+ logger.warn({
74
+ userId: slackUserId,
75
+ tenantId,
76
+ teamId
77
+ }, "Workspace should not allow join from workspace");
78
+ return null;
79
+ }
80
+ const slackClient = getSlackClient(botToken);
81
+ let userEmail;
82
+ try {
83
+ userEmail = (await slackClient.users.info({ user: slackUserId })).user?.profile?.email;
84
+ } catch (error) {
85
+ logger.warn({
86
+ error,
87
+ userId: slackUserId
88
+ }, "Failed to get user info from Slack");
89
+ return null;
90
+ }
91
+ if (!userEmail) {
92
+ logger.warn({ userId: slackUserId }, "No email found in Slack user profile");
93
+ return null;
94
+ }
95
+ if (await getOrganizationMemberByEmail(runDbClient_default)(tenantId, userEmail)) {
96
+ logger.debug({
97
+ userId: slackUserId,
98
+ email: userEmail
99
+ }, "User already has Inkeep account, skipping auto-invite");
100
+ return null;
101
+ }
102
+ const existingInvitation = (await getPendingInvitationsByEmail(runDbClient_default)(userEmail)).find((inv) => inv.organizationId === tenantId);
103
+ if (existingInvitation) {
104
+ logger.info({
105
+ userId: slackUserId,
106
+ tenantId,
107
+ invitationId: existingInvitation.id,
108
+ email: userEmail
109
+ }, "Reusing existing pending invitation for Slack user");
110
+ return {
111
+ invitationId: existingInvitation.id,
112
+ email: userEmail,
113
+ authMethod: existingInvitation.authMethod ?? "email-password"
114
+ };
115
+ }
116
+ const invitation = await createInvitationInDb(runDbClient_default)({
117
+ organizationId: tenantId,
118
+ email: userEmail
119
+ });
120
+ logger.info({
121
+ userId: slackUserId,
122
+ tenantId,
123
+ invitationId: invitation.id,
124
+ email: userEmail
125
+ }, "Invitation created for Slack user without Inkeep account");
126
+ return {
127
+ invitationId: invitation.id,
128
+ email: userEmail,
129
+ authMethod: invitation.authMethod
130
+ };
131
+ } catch (error) {
132
+ logger.warn({
133
+ error,
134
+ userId: slackUserId,
135
+ tenantId
136
+ }, "Auto-invite attempt failed");
137
+ return null;
138
+ }
139
+ }
140
+
141
+ //#endregion
142
+ export { buildLinkPromptMessage, resolveUnlinkedUserAction };
@@ -0,0 +1,15 @@
1
+ import { SlackLinkIntent } from "@inkeep/agents-core";
2
+
3
+ //#region src/slack/services/resume-intent.d.ts
4
+ interface ResumeSmartLinkIntentParams {
5
+ intent: SlackLinkIntent;
6
+ teamId: string;
7
+ slackUserId: string;
8
+ inkeepUserId: string;
9
+ tenantId: string;
10
+ slackEnterpriseId?: string;
11
+ slackUsername?: string;
12
+ }
13
+ declare function resumeSmartLinkIntent(params: ResumeSmartLinkIntentParams): Promise<void>;
14
+ //#endregion
15
+ export { ResumeSmartLinkIntentParams, resumeSmartLinkIntent };
@@ -0,0 +1,338 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import { findWorkspaceConnectionByTeamId } from "./nango.js";
4
+ import { generateSlackConversationId, sendResponseUrlMessage } from "./events/utils.js";
5
+ import { resolveEffectiveAgent } from "./agent-resolution.js";
6
+ import { createContextBlock } from "./blocks/index.js";
7
+ import { getSlackClient } from "./client.js";
8
+ import { streamAgentResponse } from "./events/streaming.js";
9
+ import { signSlackUserToken } from "@inkeep/agents-core";
10
+
11
+ //#region src/slack/services/resume-intent.ts
12
+ const logger = getLogger("slack-resume-intent");
13
+ function getChannelAuthClaims(agentConfig, channelId) {
14
+ return {
15
+ slackAuthorized: agentConfig?.grantAccessToMembers ?? false,
16
+ slackAuthSource: agentConfig?.source && agentConfig.source !== "none" ? agentConfig.source : void 0,
17
+ slackChannelId: channelId,
18
+ slackAuthorizedProjectId: agentConfig?.projectId
19
+ };
20
+ }
21
+ async function resumeSmartLinkIntent(params) {
22
+ const { intent, teamId, slackUserId, inkeepUserId, tenantId, slackEnterpriseId } = params;
23
+ const startTime = Date.now();
24
+ try {
25
+ const botToken = (await findWorkspaceConnectionByTeamId(teamId))?.botToken;
26
+ if (!botToken) {
27
+ logger.error({
28
+ teamId,
29
+ entryPoint: intent.entryPoint
30
+ }, "No bot token available for resume");
31
+ return;
32
+ }
33
+ const slackClient = getSlackClient(botToken);
34
+ const tokenCtx = {
35
+ inkeepUserId,
36
+ tenantId,
37
+ slackTeamId: teamId,
38
+ slackUserId,
39
+ slackEnterpriseId
40
+ };
41
+ let resolvedAgentId;
42
+ let deliveryMethod;
43
+ switch (intent.entryPoint) {
44
+ case "mention":
45
+ resolvedAgentId = intent.agentId;
46
+ deliveryMethod = "streaming";
47
+ await resumeMention(intent, slackClient, tokenCtx, teamId, tenantId);
48
+ break;
49
+ case "question_command":
50
+ deliveryMethod = intent.responseUrl ? "response_url" : "bot_token";
51
+ await resumeCommand(intent, slackClient, tokenCtx, teamId, tenantId);
52
+ break;
53
+ case "run_command":
54
+ deliveryMethod = intent.responseUrl ? "response_url" : "bot_token";
55
+ await resumeRunCommand(intent, slackClient, tokenCtx, teamId, tenantId);
56
+ break;
57
+ }
58
+ const durationMs = Date.now() - startTime;
59
+ logger.info({
60
+ event: "smart_link_intent_resumed",
61
+ entryPoint: intent.entryPoint,
62
+ channelId: intent.channelId,
63
+ agentId: resolvedAgentId || intent.agentId,
64
+ deliveryMethod,
65
+ durationMs
66
+ }, "Smart link intent resumed");
67
+ } catch (error) {
68
+ logger.error({
69
+ event: "smart_link_intent_failed",
70
+ entryPoint: intent.entryPoint,
71
+ error: error instanceof Error ? error.message : String(error)
72
+ }, "Smart link intent resume failed");
73
+ }
74
+ }
75
+ async function resumeMention(intent, slackClient, tokenCtx, teamId, tenantId) {
76
+ const { slackUserId } = tokenCtx;
77
+ if (!intent.agentId || !intent.projectId) {
78
+ logger.error({
79
+ entryPoint: intent.entryPoint,
80
+ channelId: intent.channelId,
81
+ hasAgentId: !!intent.agentId,
82
+ hasProjectId: !!intent.projectId
83
+ }, "Mention intent missing agentId or projectId");
84
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId, intent.threadTs);
85
+ return;
86
+ }
87
+ const replyThreadTs = intent.threadTs || intent.messageTs;
88
+ if (!replyThreadTs) {
89
+ logger.error({
90
+ entryPoint: intent.entryPoint,
91
+ channelId: intent.channelId
92
+ }, "Mention intent missing threadTs and messageTs");
93
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId, void 0, "We couldn't resume your question due to a technical issue. Please try mentioning @Inkeep again.");
94
+ return;
95
+ }
96
+ const agentConfig = await resolveEffectiveAgent({
97
+ tenantId,
98
+ teamId,
99
+ channelId: intent.channelId
100
+ });
101
+ const slackUserToken = await signSlackUserToken({
102
+ ...tokenCtx,
103
+ ...getChannelAuthClaims(agentConfig, intent.channelId)
104
+ });
105
+ const ackMessage = await slackClient.chat.postMessage({
106
+ channel: intent.channelId,
107
+ thread_ts: replyThreadTs,
108
+ text: "_Answering your question..._"
109
+ });
110
+ const conversationId = generateSlackConversationId({
111
+ teamId,
112
+ threadTs: replyThreadTs,
113
+ channel: intent.channelId,
114
+ isDM: false,
115
+ agentId: intent.agentId
116
+ });
117
+ await streamAgentResponse({
118
+ slackClient,
119
+ channel: intent.channelId,
120
+ threadTs: replyThreadTs,
121
+ thinkingMessageTs: ackMessage.ts || "",
122
+ slackUserId,
123
+ teamId,
124
+ jwtToken: slackUserToken,
125
+ projectId: intent.projectId,
126
+ agentId: intent.agentId,
127
+ question: intent.question,
128
+ agentName: intent.agentId,
129
+ conversationId
130
+ });
131
+ }
132
+ async function resumeCommand(intent, slackClient, tokenCtx, teamId, tenantId) {
133
+ const { slackUserId } = tokenCtx;
134
+ const resolvedAgent = await resolveEffectiveAgent({
135
+ tenantId,
136
+ teamId,
137
+ channelId: intent.channelId,
138
+ userId: slackUserId
139
+ });
140
+ if (!resolvedAgent) {
141
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId, void 0, "The agent couldn't be found. Try asking your question again.");
142
+ return;
143
+ }
144
+ await executeAndDeliver({
145
+ intent,
146
+ slackClient,
147
+ slackUserToken: await signSlackUserToken({
148
+ ...tokenCtx,
149
+ ...getChannelAuthClaims(resolvedAgent, intent.channelId)
150
+ }),
151
+ slackUserId,
152
+ teamId,
153
+ agentId: resolvedAgent.agentId,
154
+ agentName: resolvedAgent.agentName || resolvedAgent.agentId,
155
+ projectId: resolvedAgent.projectId
156
+ });
157
+ }
158
+ async function resumeRunCommand(intent, slackClient, tokenCtx, teamId, tenantId) {
159
+ const { slackUserId } = tokenCtx;
160
+ if (!intent.agentIdentifier) {
161
+ logger.error({
162
+ entryPoint: intent.entryPoint,
163
+ channelId: intent.channelId
164
+ }, "Run command intent missing agentIdentifier");
165
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId);
166
+ return;
167
+ }
168
+ const agentConfig = await resolveEffectiveAgent({
169
+ tenantId,
170
+ teamId,
171
+ channelId: intent.channelId
172
+ });
173
+ const slackUserToken = await signSlackUserToken({
174
+ ...tokenCtx,
175
+ ...getChannelAuthClaims(agentConfig, intent.channelId)
176
+ });
177
+ const agentInfo = await findAgentByIdentifierViaApi(tenantId, intent.agentIdentifier, slackUserToken);
178
+ if (!agentInfo) {
179
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId, void 0, `Agent "${intent.agentIdentifier}" couldn't be found. Try asking your question again.`);
180
+ return;
181
+ }
182
+ await executeAndDeliver({
183
+ intent,
184
+ slackClient,
185
+ slackUserToken,
186
+ slackUserId,
187
+ teamId,
188
+ agentId: agentInfo.id,
189
+ agentName: agentInfo.name || agentInfo.id,
190
+ projectId: agentInfo.projectId
191
+ });
192
+ }
193
+ async function findAgentByIdentifierViaApi(tenantId, identifier, authToken) {
194
+ const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
195
+ const controller = new AbortController();
196
+ const timeout = setTimeout(() => controller.abort(), 1e4);
197
+ try {
198
+ const projectsResponse = await fetch(`${apiBaseUrl}/manage/tenants/${tenantId}/projects`, {
199
+ method: "GET",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ Authorization: `Bearer ${authToken}`
203
+ },
204
+ signal: controller.signal
205
+ });
206
+ if (!projectsResponse.ok) return null;
207
+ const projectsData = await projectsResponse.json();
208
+ const projects = projectsData.data || projectsData || [];
209
+ return (await Promise.all(projects.map(async (project) => {
210
+ try {
211
+ const agentsResponse = await fetch(`${apiBaseUrl}/manage/tenants/${tenantId}/projects/${project.id}/agents`, {
212
+ method: "GET",
213
+ headers: {
214
+ "Content-Type": "application/json",
215
+ Authorization: `Bearer ${authToken}`
216
+ },
217
+ signal: controller.signal
218
+ });
219
+ if (!agentsResponse.ok) return [];
220
+ const agentsData = await agentsResponse.json();
221
+ return (agentsData.data || agentsData || []).map((agent) => ({
222
+ id: agent.id,
223
+ name: agent.name,
224
+ projectId: project.id
225
+ }));
226
+ } catch (error) {
227
+ logger.warn({
228
+ error: error instanceof Error ? error.message : String(error),
229
+ projectId: project.id
230
+ }, "Failed to fetch agents for project during identifier lookup");
231
+ return [];
232
+ }
233
+ }))).flat().find((a) => a.id === identifier || a.name?.toLowerCase() === identifier.toLowerCase()) || null;
234
+ } catch (error) {
235
+ const isTimeout = error instanceof Error && error.name === "AbortError";
236
+ logger.warn({
237
+ error: error instanceof Error ? error.message : String(error),
238
+ tenantId,
239
+ identifier,
240
+ isTimeout
241
+ }, "Failed to find agent by identifier");
242
+ return null;
243
+ } finally {
244
+ clearTimeout(timeout);
245
+ }
246
+ }
247
+ async function executeAndDeliver(params) {
248
+ const { intent, slackClient, slackUserToken, slackUserId, teamId, agentId, agentName, projectId } = params;
249
+ const apiBaseUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
250
+ const controller = new AbortController();
251
+ const timeout = setTimeout(() => controller.abort(), 3e4);
252
+ let response;
253
+ try {
254
+ response = await fetch(`${apiBaseUrl}/run/api/chat`, {
255
+ method: "POST",
256
+ headers: {
257
+ "Content-Type": "application/json",
258
+ Authorization: `Bearer ${slackUserToken}`,
259
+ "x-inkeep-project-id": projectId,
260
+ "x-inkeep-agent-id": agentId
261
+ },
262
+ body: JSON.stringify({
263
+ messages: [{
264
+ role: "user",
265
+ content: intent.question
266
+ }],
267
+ stream: false
268
+ }),
269
+ signal: controller.signal
270
+ });
271
+ } catch (error) {
272
+ clearTimeout(timeout);
273
+ if (error.name === "AbortError") logger.warn({
274
+ teamId,
275
+ timeoutMs: 3e4
276
+ }, "Resume agent execution timed out");
277
+ throw error;
278
+ } finally {
279
+ clearTimeout(timeout);
280
+ }
281
+ if (!response.ok) {
282
+ logger.error({
283
+ status: response.status,
284
+ agentId,
285
+ projectId
286
+ }, "Resume run API call failed");
287
+ await postErrorToChannel(slackClient, intent.channelId, slackUserId, void 0, "Something went wrong while answering your question. Please try again.");
288
+ return;
289
+ }
290
+ const result = await response.json();
291
+ const assistantMessage = result.choices?.[0]?.message?.content || result.message?.content || "No response received";
292
+ const contextBlock = createContextBlock({ agentName });
293
+ if (intent.responseUrl) try {
294
+ await sendResponseUrlMessage(intent.responseUrl, {
295
+ response_type: "ephemeral",
296
+ text: assistantMessage,
297
+ blocks: [{
298
+ type: "section",
299
+ text: {
300
+ type: "mrkdwn",
301
+ text: assistantMessage
302
+ }
303
+ }, contextBlock]
304
+ });
305
+ return;
306
+ } catch {
307
+ logger.warn({ channelId: intent.channelId }, "response_url delivery failed, falling back to bot channel post");
308
+ }
309
+ await slackClient.chat.postMessage({
310
+ channel: intent.channelId,
311
+ text: assistantMessage,
312
+ blocks: [{
313
+ type: "section",
314
+ text: {
315
+ type: "mrkdwn",
316
+ text: assistantMessage
317
+ }
318
+ }, contextBlock]
319
+ });
320
+ }
321
+ async function postErrorToChannel(slackClient, channelId, slackUserId, threadTs, message = "The agent couldn't be found. Try asking your question again.") {
322
+ try {
323
+ await slackClient.chat.postEphemeral({
324
+ channel: channelId,
325
+ user: slackUserId,
326
+ thread_ts: threadTs,
327
+ text: message
328
+ });
329
+ } catch (error) {
330
+ logger.warn({
331
+ error,
332
+ channelId
333
+ }, "Failed to post error message to Slack");
334
+ }
335
+ }
336
+
337
+ //#endregion
338
+ export { resumeSmartLinkIntent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.0.0-dev-20260224193013",
3
+ "version": "0.0.0-dev-20260224195557",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -33,7 +33,7 @@
33
33
  "jose": "^6.1.0",
34
34
  "minimatch": "^10.1.1",
35
35
  "slack-block-builder": "^2.8.0",
36
- "@inkeep/agents-core": "0.0.0-dev-20260224193013"
36
+ "@inkeep/agents-core": "0.0.0-dev-20260224195557"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@hono/zod-openapi": "^1.1.5",