@inkeep/agents-work-apps 0.53.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/env.d.ts CHANGED
@@ -14,11 +14,11 @@ declare const envSchema: z.ZodObject<{
14
14
  pentest: "pentest";
15
15
  }>>;
16
16
  LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
17
+ error: "error";
17
18
  trace: "trace";
18
19
  debug: "debug";
19
20
  info: "info";
20
21
  warn: "warn";
21
- error: "error";
22
22
  }>>;
23
23
  INKEEP_AGENTS_RUN_DATABASE_URL: z.ZodOptional<z.ZodString>;
24
24
  INKEEP_AGENTS_MANAGE_UI_URL: z.ZodOptional<z.ZodString>;
@@ -44,7 +44,7 @@ declare const envSchema: z.ZodObject<{
44
44
  declare const env: {
45
45
  NODE_ENV: "development" | "production" | "test";
46
46
  ENVIRONMENT: "development" | "production" | "test" | "pentest";
47
- LOG_LEVEL: "trace" | "debug" | "info" | "warn" | "error";
47
+ LOG_LEVEL: "error" | "trace" | "debug" | "info" | "warn";
48
48
  INKEEP_AGENTS_RUN_DATABASE_URL?: string | undefined;
49
49
  INKEEP_AGENTS_MANAGE_UI_URL?: string | undefined;
50
50
  GITHUB_APP_ID?: string | undefined;
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types0 from "hono/types";
2
+ import * as hono_types7 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_types0.BlankSchema, "/">;
9
+ }, hono_types7.BlankSchema, "/">;
10
10
  //#endregion
11
11
  export { app as default };
@@ -331,7 +331,8 @@ const getServer = async (toolId) => {
331
331
  repo: z.string().describe("Repository name"),
332
332
  from_branch: z.string().optional().describe("Branch to create from (defaults to default branch)")
333
333
  }, async ({ owner, repo, from_branch }) => {
334
- const branch_name = `docs-writer-ai-update-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
334
+ const suffix = Math.random().toString(36).slice(2, 8);
335
+ const branch_name = `docs-writer-ai-update-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-${suffix}`;
335
336
  try {
336
337
  const githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
337
338
  const repoInfo = await githubClient.rest.repos.get({
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types4 from "hono/types";
2
+ import * as hono_types5 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/setup.d.ts
5
- declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types5.BlankEnv, hono_types5.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,7 +1,7 @@
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/tokenExchange.d.ts
5
- declare const app: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types8.BlankEnv, hono_types8.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types8 from "hono/types";
2
+ import * as hono_types3 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_types8.BlankEnv, hono_types8.BlankSchema, "/">;
10
+ declare const app: Hono<hono_types3.BlankEnv, hono_types3.BlankSchema, "/">;
11
11
  //#endregion
12
12
  export { WebhookVerificationResult, app as default, verifyWebhookSignature };
@@ -62,6 +62,7 @@ async function dispatchSlackEvent(eventType, payload, options, span) {
62
62
  slackUserId: event.user,
63
63
  channel: event.channel,
64
64
  text: question,
65
+ attachments: event.attachments,
65
66
  threadTs: event.thread_ts || event.ts || "",
66
67
  messageTs: event.ts || "",
67
68
  teamId,
@@ -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,
@@ -8,6 +8,7 @@
8
8
  /** Configuration for a resolved agent */
9
9
  interface ResolvedAgentConfig {
10
10
  projectId: string;
11
+ projectName?: string;
11
12
  agentId: string;
12
13
  agentName?: string;
13
14
  source: 'channel' | 'workspace' | 'none';
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from "../../logger.js";
2
2
  import runDbClient_default from "../../db/runDbClient.js";
3
3
  import { getWorkspaceDefaultAgentFromNango } from "./nango.js";
4
- import { fetchAgentsForProject } from "./events/utils.js";
4
+ import { fetchAgentsForProject, fetchProjectsForTenant } from "./events/utils.js";
5
5
  import { findWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
6
6
 
7
7
  //#region src/slack/services/agent-resolution.ts
@@ -41,6 +41,35 @@ async function lookupAgentName(tenantId, projectId, agentId) {
41
41
  }
42
42
  return agents.find((a) => a.id === agentId)?.name || void 0;
43
43
  }
44
+ const PROJECT_NAME_CACHE_TTL_MS = 300 * 1e3;
45
+ const PROJECT_NAME_CACHE_MAX_SIZE = 200;
46
+ const projectNameCache = /* @__PURE__ */ new Map();
47
+ async function lookupProjectName(tenantId, projectId) {
48
+ const cacheKey = `${tenantId}:${projectId}`;
49
+ const cached = projectNameCache.get(cacheKey);
50
+ if (cached && cached.expiresAt > Date.now()) return cached.name || void 0;
51
+ const projects = await fetchProjectsForTenant(tenantId);
52
+ for (const project of projects) {
53
+ const key = `${tenantId}:${project.id}`;
54
+ projectNameCache.set(key, {
55
+ name: project.name || null,
56
+ expiresAt: Date.now() + PROJECT_NAME_CACHE_TTL_MS
57
+ });
58
+ }
59
+ if (projectNameCache.size > PROJECT_NAME_CACHE_MAX_SIZE) {
60
+ const now = Date.now();
61
+ for (const [key, entry] of projectNameCache) if (entry.expiresAt <= now) projectNameCache.delete(key);
62
+ if (projectNameCache.size > PROJECT_NAME_CACHE_MAX_SIZE) {
63
+ const excess = projectNameCache.size - PROJECT_NAME_CACHE_MAX_SIZE;
64
+ const keys = projectNameCache.keys();
65
+ for (let i = 0; i < excess; i++) {
66
+ const { value } = keys.next();
67
+ if (value) projectNameCache.delete(value);
68
+ }
69
+ }
70
+ }
71
+ return projects.find((p) => p.id === projectId)?.name || void 0;
72
+ }
44
73
  /**
45
74
  * Resolve the effective agent configuration.
46
75
  * Priority: Channel default > Workspace default
@@ -83,6 +112,7 @@ async function resolveEffectiveAgent(params) {
83
112
  }, "Resolved agent from workspace config");
84
113
  result = {
85
114
  projectId: workspaceConfig.projectId,
115
+ projectName: workspaceConfig.projectName,
86
116
  agentId: workspaceConfig.agentId,
87
117
  agentName: workspaceConfig.agentName,
88
118
  source: "workspace",
@@ -100,6 +130,10 @@ async function resolveEffectiveAgent(params) {
100
130
  }, "Enriched agent config with name from manage API");
101
131
  }
102
132
  }
133
+ if (result && !result.projectName) {
134
+ const projectName = await lookupProjectName(tenantId, result.projectId);
135
+ if (projectName) result.projectName = projectName;
136
+ }
103
137
  if (!result) logger.debug({
104
138
  tenantId,
105
139
  teamId,
@@ -130,6 +164,7 @@ async function getAgentConfigSources(params) {
130
164
  const wsConfig = await getWorkspaceDefaultAgentFromNango(teamId);
131
165
  if (wsConfig?.agentId && wsConfig.projectId) workspaceConfig = {
132
166
  projectId: wsConfig.projectId,
167
+ projectName: wsConfig.projectName,
133
168
  agentId: wsConfig.agentId,
134
169
  agentName: wsConfig.agentName,
135
170
  source: "workspace",
@@ -140,6 +175,10 @@ async function getAgentConfigSources(params) {
140
175
  const name = await lookupAgentName(tenantId, effective.projectId, effective.agentId);
141
176
  if (name) effective.agentName = name;
142
177
  }
178
+ if (effective && !effective.projectName) {
179
+ const projectName = await lookupProjectName(tenantId, effective.projectId);
180
+ if (projectName) effective.projectName = projectName;
181
+ }
143
182
  return {
144
183
  channelConfig,
145
184
  workspaceConfig,
@@ -53,18 +53,25 @@ interface AgentConfigSources {
53
53
  channelConfig: {
54
54
  agentName?: string;
55
55
  agentId: string;
56
+ projectId: string;
57
+ projectName?: string;
56
58
  } | null;
57
59
  workspaceConfig: {
58
60
  agentName?: string;
59
61
  agentId: string;
62
+ projectId: string;
63
+ projectName?: string;
60
64
  } | null;
61
65
  effective: {
62
66
  agentName?: string;
63
67
  agentId: string;
68
+ projectId: string;
69
+ projectName?: string;
64
70
  source: string;
65
71
  } | null;
66
72
  }
67
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>;
68
75
  interface ToolApprovalButtonValue {
69
76
  toolCallId: string;
70
77
  conversationId: string;
@@ -143,7 +150,6 @@ declare function buildCitationsBlock(citations: Array<{
143
150
  title?: string;
144
151
  url?: string;
145
152
  }>): any[];
146
- declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
147
153
  declare function createCreateInkeepAccountMessage(acceptUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
148
154
  //#endregion
149
- 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 };
@@ -67,10 +67,32 @@ function createNotLinkedMessage() {
67
67
  }
68
68
  function createStatusMessage(email, linkedAt, dashboardUrl, agentConfigs) {
69
69
  const { effective } = agentConfigs;
70
+ const baseUrl = dashboardUrl.replace(/\/work-apps\/slack$/, "");
70
71
  let agentLine;
71
- if (effective) agentLine = `${Md.bold("Agent:")} ${effective.agentName || effective.agentId}`;
72
- else agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
73
- 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();
72
+ let projectLine;
73
+ if (effective) {
74
+ const agentDisplayName = effective.agentName || effective.agentId;
75
+ const agentUrl = `${baseUrl}/projects/${effective.projectId}/agents/${effective.agentId}`;
76
+ agentLine = `${Md.bold("Agent:")} <${agentUrl}|${agentDisplayName}>`;
77
+ const projectDisplayName = effective.projectName || effective.projectId;
78
+ const projectUrl = `${baseUrl}/projects/${effective.projectId}/agents`;
79
+ projectLine = `${Md.bold("Project:")} <${projectUrl}|${projectDisplayName}>`;
80
+ } else {
81
+ agentLine = `${Md.bold("Agent:")} None configured\n${Md.italic("Ask your admin to set up an agent in the dashboard.")}`;
82
+ projectLine = "";
83
+ }
84
+ const lines = [
85
+ Md.bold("Connected to Inkeep"),
86
+ "",
87
+ `${Md.bold("Account:")} ${email}`,
88
+ `${Md.bold("Linked:")} ${new Date(linkedAt).toLocaleDateString()}`,
89
+ agentLine
90
+ ];
91
+ if (projectLine) lines.push(projectLine);
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
+ }
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();
74
96
  }
75
97
  const ToolApprovalButtonValueSchema = z.object({
76
98
  toolCallId: z.string(),
@@ -297,12 +319,9 @@ function buildCitationsBlock(citations) {
297
319
  }
298
320
  }];
299
321
  }
300
- function createJwtLinkMessage(linkUrl, expiresInMinutes) {
301
- 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();
302
- }
303
322
  function createCreateInkeepAccountMessage(acceptUrl, expiresInMinutes) {
304
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();
305
324
  }
306
325
 
307
326
  //#endregion
308
- 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
 
@@ -1,18 +1,7 @@
1
+ import { SlackAttachment } from "./utils.js";
2
+
1
3
  //#region src/slack/services/events/app-mention.d.ts
2
- /**
3
- * Handler for Slack @mention events
4
- *
5
- * Flow:
6
- * 1. Resolve workspace connection (single lookup, cached)
7
- * 2. Parallel: resolve agent config + check user link
8
- * 3. If no agent configured → prompt to set up in dashboard
9
- * 4. If not linked → prompt to link account
10
- * 5. Handle based on context:
11
- * - Channel + no query → Show usage hint
12
- * - Channel + query → Execute agent with streaming response
13
- * - Thread + no query → Auto-execute agent with thread context as query
14
- * - Thread + query → Execute agent with thread context included
15
- */
4
+
16
5
  /**
17
6
  * Metadata passed to the agent selector modal via button value
18
7
  */
@@ -32,6 +21,7 @@ declare function handleAppMention(params: {
32
21
  slackUserId: string;
33
22
  channel: string;
34
23
  text: string;
24
+ attachments?: SlackAttachment[];
35
25
  threadTs: string;
36
26
  messageTs: string;
37
27
  teamId: string;