@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 +2 -2
- package/dist/github/mcp/index.d.ts +2 -2
- package/dist/github/mcp/index.js +2 -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 +1 -0
- package/dist/slack/routes/users.js +29 -3
- package/dist/slack/services/agent-resolution.d.ts +1 -0
- package/dist/slack/services/agent-resolution.js +40 -1
- package/dist/slack/services/blocks/index.d.ts +8 -2
- package/dist/slack/services/blocks/index.js +26 -7
- package/dist/slack/services/commands/index.d.ts +1 -1
- package/dist/slack/services/commands/index.js +32 -122
- package/dist/slack/services/events/app-mention.d.ts +4 -14
- package/dist/slack/services/events/app-mention.js +40 -19
- package/dist/slack/services/events/index.d.ts +1 -1
- package/dist/slack/services/events/utils.d.ts +21 -2
- package/dist/slack/services/events/utils.js +35 -10
- package/dist/slack/services/index.d.ts +4 -4
- package/dist/slack/services/index.js +2 -2
- package/dist/slack/services/link-prompt.d.ts +27 -0
- package/dist/slack/services/link-prompt.js +142 -0
- package/dist/slack/services/resume-intent.d.ts +15 -0
- package/dist/slack/services/resume-intent.js +338 -0
- package/package.json +2 -2
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: "
|
|
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
|
|
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
|
-
},
|
|
9
|
+
}, hono_types7.BlankSchema, "/">;
|
|
10
10
|
//#endregion
|
|
11
11
|
export { app as default };
|
package/dist/github/mcp/index.js
CHANGED
|
@@ -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
|
|
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
|
|
2
|
+
import * as hono_types5 from "hono/types";
|
|
3
3
|
|
|
4
4
|
//#region src/github/routes/setup.d.ts
|
|
5
|
-
declare const app: Hono<
|
|
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
|
|
2
|
+
import * as hono_types8 from "hono/types";
|
|
3
3
|
|
|
4
4
|
//#region src/github/routes/tokenExchange.d.ts
|
|
5
|
-
declare const app: Hono<
|
|
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
|
|
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<
|
|
10
|
+
declare const app: Hono<hono_types3.BlankEnv, hono_types3.BlankSchema, "/">;
|
|
11
11
|
//#endregion
|
|
12
12
|
export { WebhookVerificationResult, app as default, verifyWebhookSignature };
|
package/dist/slack/dispatcher.js
CHANGED
|
@@ -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
|
-
|
|
109
|
-
|
|
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,
|
|
@@ -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,
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const linkToken = await signSlackLinkToken({
|
|
23
|
+
try {
|
|
24
|
+
return {
|
|
25
|
+
response_type: "ephemeral",
|
|
26
|
+
...buildLinkPromptMessage(await resolveUnlinkedUserAction({
|
|
99
27
|
tenantId,
|
|
100
|
-
|
|
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
|
|
184
|
+
const linkResult = await resolveUnlinkedUserAction({
|
|
284
185
|
tenantId,
|
|
285
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
...
|
|
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;
|