@inkeep/agents-work-apps 0.50.6 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/github/mcp/auth.d.ts +2 -2
  2. package/dist/github/mcp/index.d.ts +2 -2
  3. package/dist/github/mcp/index.js +60 -1
  4. package/dist/github/mcp/schemas.d.ts +1 -1
  5. package/dist/github/mcp/utils.d.ts +10 -1
  6. package/dist/github/mcp/utils.js +87 -1
  7. package/dist/github/routes/setup.d.ts +2 -2
  8. package/dist/github/routes/tokenExchange.d.ts +2 -2
  9. package/dist/github/routes/webhooks.d.ts +2 -2
  10. package/dist/slack/dispatcher.js +24 -1
  11. package/dist/slack/i18n/strings.d.ts +1 -0
  12. package/dist/slack/i18n/strings.js +1 -0
  13. package/dist/slack/routes/events.js +2 -2
  14. package/dist/slack/routes/oauth.js +3 -4
  15. package/dist/slack/routes/users.js +13 -11
  16. package/dist/slack/routes/workspaces.js +85 -1
  17. package/dist/slack/services/blocks/index.d.ts +81 -1
  18. package/dist/slack/services/blocks/index.js +238 -19
  19. package/dist/slack/services/commands/index.d.ts +1 -1
  20. package/dist/slack/services/commands/index.js +98 -4
  21. package/dist/slack/services/events/app-mention.js +2 -2
  22. package/dist/slack/services/events/block-actions.d.ts +12 -1
  23. package/dist/slack/services/events/block-actions.js +126 -2
  24. package/dist/slack/services/events/index.d.ts +2 -2
  25. package/dist/slack/services/events/index.js +2 -2
  26. package/dist/slack/services/events/streaming.d.ts +1 -1
  27. package/dist/slack/services/events/streaming.js +203 -7
  28. package/dist/slack/services/events/utils.d.ts +2 -2
  29. package/dist/slack/services/events/utils.js +5 -2
  30. package/dist/slack/services/index.d.ts +3 -3
  31. package/dist/slack/services/index.js +3 -3
  32. package/dist/slack/services/nango.js +1 -23
  33. package/dist/slack/tracer.d.ts +1 -0
  34. package/dist/slack/tracer.js +2 -1
  35. package/package.json +2 -2
@@ -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_types3 from "hono/types";
2
+ import * as hono_types9 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_types3.BlankSchema, "/">;
9
+ }, hono_types9.BlankSchema, "/">;
10
10
  //#endregion
11
11
  export { app as default };
@@ -1,7 +1,7 @@
1
1
  import runDbClient_default from "../../db/runDbClient.js";
2
2
  import { githubMcpAuth } from "./auth.js";
3
3
  import { ReactionContentSchema } from "./schemas.js";
4
- import { commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchComments, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, visualizeUpdateOperations } from "./utils.js";
4
+ import { commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchBranchChangedFiles, fetchComments, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, visualizeUpdateOperations } from "./utils.js";
5
5
  import { z } from "@hono/zod-openapi";
6
6
  import { getMcpToolRepositoryAccessWithDetails } from "@inkeep/agents-core";
7
7
  import { Hono } from "hono";
@@ -267,6 +267,65 @@ const getServer = async (toolId) => {
267
267
  };
268
268
  }
269
269
  });
270
+ server.tool("get-changed-files-for-branch", `Get the list of files changed on a branch compared to a base ref, without requiring a pull request. Returns file paths, status, additions/deletions, and optionally patches and full file contents. ${getAvailableRepositoryString(repositoryAccess)}`, {
271
+ owner: z.string().describe("Repository owner name"),
272
+ repo: z.string().describe("Repository name"),
273
+ head: z.string().describe("The branch or ref to check for changes (e.g. \"feat/my-feature\")"),
274
+ base: z.string().optional().describe("The base branch or ref to compare against (defaults to the repository default branch)"),
275
+ file_paths: z.array(z.string()).optional().describe("Optional list of file path glob patterns to filter results"),
276
+ include_contents: z.boolean().default(false).describe("Whether to include full file contents for each changed file"),
277
+ include_patch: z.boolean().default(false).describe("Whether to include the patch/diff text for each changed file")
278
+ }, async ({ owner, repo, head, base, file_paths, include_contents, include_patch }) => {
279
+ try {
280
+ let githubClient;
281
+ try {
282
+ githubClient = getGitHubClientFromRepo(owner, repo, installationIdMap);
283
+ } catch (error) {
284
+ return {
285
+ content: [{
286
+ type: "text",
287
+ text: `Error accessing GitHub: ${error instanceof Error ? error.message : "Unknown error"}`
288
+ }],
289
+ isError: true
290
+ };
291
+ }
292
+ const baseRef = base ?? (await githubClient.rest.repos.get({
293
+ owner,
294
+ repo
295
+ })).data.default_branch;
296
+ const files = await fetchBranchChangedFiles(githubClient, owner, repo, baseRef, head, {
297
+ pathFilters: file_paths ?? [],
298
+ includeContents: include_contents,
299
+ includePatch: include_patch
300
+ });
301
+ if (files.length === 0) return { content: [{
302
+ type: "text",
303
+ text: `No changed files found between ${baseRef} and ${head} in ${owner}/${repo}.${file_paths?.length ? `\n\nFilters applied: ${file_paths.join(", ")}` : ""}`
304
+ }] };
305
+ const output = await formatFileDiff(0, files, include_contents);
306
+ return { content: [{
307
+ type: "text",
308
+ text: `## Changed files: ${baseRef}...${head} in ${owner}/${repo}\n\nFound ${files.length} changed file(s).\n\n` + output
309
+ }] };
310
+ } catch (error) {
311
+ if (error instanceof Error && "status" in error) {
312
+ if (error.status === 404) return {
313
+ content: [{
314
+ type: "text",
315
+ text: `Repository ${owner}/${repo} not found, or one of the refs ("${base ?? "default"}", "${head}") does not exist.`
316
+ }],
317
+ isError: true
318
+ };
319
+ }
320
+ return {
321
+ content: [{
322
+ type: "text",
323
+ text: `Error fetching changed files: ${error instanceof Error ? error.message : "Unknown error"}`
324
+ }],
325
+ isError: true
326
+ };
327
+ }
328
+ });
270
329
  server.tool("create-branch", `Create a new branch in a repository. ${getAvailableRepositoryString(repositoryAccess)}`, {
271
330
  owner: z.string().describe("Repository owner name"),
272
331
  repo: z.string().describe("Repository name"),
@@ -76,8 +76,8 @@ declare const ChangedFileSchema: z.ZodObject<{
76
76
  path: z.ZodString;
77
77
  status: z.ZodEnum<{
78
78
  added: "added";
79
- modified: "modified";
80
79
  removed: "removed";
80
+ modified: "modified";
81
81
  renamed: "renamed";
82
82
  copied: "copied";
83
83
  changed: "changed";
@@ -152,6 +152,15 @@ declare function fetchPrFiles(octokit: Octokit, owner: string, repo: string, prN
152
152
  * Get file-based diffs with all commit messages that impacted each file.
153
153
  */
154
154
  declare function fetchPrFileDiffs(octokit: Octokit, owner: string, repo: string, prNumber: number): Promise<ChangedFile[]>;
155
+ /**
156
+ * Fetch files changed on a branch compared to a base ref, without requiring a PR.
157
+ * Uses the GitHub Compare API (`repos.compareCommitsWithBasehead`).
158
+ */
159
+ declare function fetchBranchChangedFiles(octokit: Octokit, owner: string, repo: string, base: string, head: string, options?: {
160
+ pathFilters?: string[];
161
+ includeContents?: boolean;
162
+ includePatch?: boolean;
163
+ }): Promise<ChangedFile[]>;
155
164
  /**
156
165
  * Fetch all PR comments (both issue comments and review comments)
157
166
  */
@@ -247,4 +256,4 @@ declare function listIssueCommentReactions(octokit: Octokit, owner: string, repo
247
256
  declare function listPullRequestReviewCommentReactions(octokit: Octokit, owner: string, repo: string, commentId: number): Promise<ReactionDetail[]>;
248
257
  declare function formatFileDiff(pullRequestNumber: number, files: ChangedFile[], includeContents?: boolean): Promise<string>;
249
258
  //#endregion
250
- export { CommitData, LLMUpdateOperation, PullCommit, ReactionDetail, applyOperation, applyOperations, commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchComments, fetchCommitDetails, fetchPrCommits, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getFilePathsInRepo, getGitHubClientFromInstallationId, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, validateLineNumbers, visualizeUpdateOperations };
259
+ export { CommitData, LLMUpdateOperation, PullCommit, ReactionDetail, applyOperation, applyOperations, commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchBranchChangedFiles, fetchComments, fetchCommitDetails, fetchPrCommits, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getFilePathsInRepo, getGitHubClientFromInstallationId, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, validateLineNumbers, visualizeUpdateOperations };
@@ -265,6 +265,92 @@ async function fetchPrFileDiffs(octokit, owner, repo, prNumber) {
265
265
  }
266
266
  }
267
267
  /**
268
+ * Fetch files changed on a branch compared to a base ref, without requiring a PR.
269
+ * Uses the GitHub Compare API (`repos.compareCommitsWithBasehead`).
270
+ */
271
+ async function fetchBranchChangedFiles(octokit, owner, repo, base, head, options = {}) {
272
+ const { pathFilters = [], includeContents = false, includePatch = false } = options;
273
+ logger.info({
274
+ owner,
275
+ repo,
276
+ base,
277
+ head,
278
+ pathFilters,
279
+ includeContents,
280
+ includePatch
281
+ }, `Fetching changed files between ${base}...${head}`);
282
+ const totalCommits = (await octokit.rest.repos.compareCommitsWithBasehead({
283
+ owner,
284
+ repo,
285
+ basehead: `${base}...${head}`,
286
+ per_page: 1
287
+ })).data.total_commits;
288
+ const collectedFiles = [];
289
+ let page = 1;
290
+ const perPage = 100;
291
+ while (true) {
292
+ const files = (await octokit.rest.repos.compareCommitsWithBasehead({
293
+ owner,
294
+ repo,
295
+ basehead: `${base}...${head}`,
296
+ per_page: perPage,
297
+ page
298
+ })).data.files ?? [];
299
+ if (files.length === 0) break;
300
+ for (const file of files) {
301
+ if (pathFilters.length > 0 && !pathFilters.some((filter) => minimatch(file.filename, filter))) continue;
302
+ collectedFiles.push({
303
+ commit_messages: [],
304
+ path: file.filename,
305
+ status: file.status,
306
+ additions: file.additions,
307
+ deletions: file.deletions,
308
+ patch: includePatch ? file.patch : void 0,
309
+ previousPath: file.previous_filename
310
+ });
311
+ }
312
+ if (files.length < perPage) break;
313
+ page++;
314
+ }
315
+ if (includeContents) {
316
+ const BATCH_SIZE = 10;
317
+ const filesToFetch = collectedFiles.filter((f) => f.status !== "removed");
318
+ for (let i = 0; i < filesToFetch.length; i += BATCH_SIZE) {
319
+ const batch = filesToFetch.slice(i, i + BATCH_SIZE);
320
+ await Promise.all(batch.map(async (changedFile) => {
321
+ try {
322
+ const { data: content } = await octokit.rest.repos.getContent({
323
+ owner,
324
+ repo,
325
+ path: changedFile.path,
326
+ ref: head
327
+ });
328
+ if ("content" in content && content.encoding === "base64") changedFile.contents = Buffer.from(content.content, "base64").toString("utf-8");
329
+ } catch (error) {
330
+ logger.warn({
331
+ owner,
332
+ repo,
333
+ base,
334
+ head,
335
+ file: changedFile.path
336
+ }, `Failed to fetch contents for ${changedFile.path}: ${error}`);
337
+ }
338
+ }));
339
+ }
340
+ }
341
+ logger.info({
342
+ owner,
343
+ repo,
344
+ base,
345
+ head,
346
+ totalCommits,
347
+ pathFilters,
348
+ includeContents,
349
+ fileCount: collectedFiles.length
350
+ }, `Found ${collectedFiles.length} changed files between ${base}...${head} (${totalCommits} commits)`);
351
+ return collectedFiles;
352
+ }
353
+ /**
268
354
  * Fetch all PR comments (both issue comments and review comments)
269
355
  */
270
356
  async function fetchComments(octokit, owner, repo, prNumber) {
@@ -694,4 +780,4 @@ async function formatFileDiff(pullRequestNumber, files, includeContents = false)
694
780
  }
695
781
 
696
782
  //#endregion
697
- export { applyOperation, applyOperations, commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchComments, fetchCommitDetails, fetchPrCommits, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getFilePathsInRepo, getGitHubClientFromInstallationId, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, validateLineNumbers, visualizeUpdateOperations };
783
+ export { applyOperation, applyOperations, commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchBranchChangedFiles, fetchComments, fetchCommitDetails, fetchPrCommits, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getFilePathsInRepo, getGitHubClientFromInstallationId, getGitHubClientFromRepo, listIssueCommentReactions, listPullRequestReviewCommentReactions, validateLineNumbers, visualizeUpdateOperations };
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types4 from "hono/types";
2
+ import * as hono_types3 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_types3.BlankEnv, hono_types3.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_types5 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_types5.BlankEnv, hono_types5.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_types7 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_types7.BlankEnv, hono_types7.BlankSchema, "/">;
11
11
  //#endregion
12
12
  export { WebhookVerificationResult, app as default, verifyWebhookSignature };
@@ -4,7 +4,7 @@ import { getSlackClient } from "./services/client.js";
4
4
  import { sendResponseUrlMessage } from "./services/events/utils.js";
5
5
  import { SLACK_SPAN_KEYS } from "./tracer.js";
6
6
  import { handleAppMention } from "./services/events/app-mention.js";
7
- import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./services/events/block-actions.js";
7
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./services/events/block-actions.js";
8
8
  import { handleFollowUpSubmission, handleModalSubmission } from "./services/events/modal-submission.js";
9
9
  import "./services/events/index.js";
10
10
  import "./services/index.js";
@@ -176,6 +176,29 @@ async function dispatchSlackEvent(eventType, payload, options, span) {
176
176
  }
177
177
  })());
178
178
  }
179
+ if ((action.action_id === "tool_approval_approve" || action.action_id === "tool_approval_deny") && action.value) {
180
+ anyHandled = true;
181
+ const approved = action.action_id === "tool_approval_approve";
182
+ const slackUserId = payload.user?.id || "";
183
+ logger.info({
184
+ teamId,
185
+ actionId: action.action_id,
186
+ approved
187
+ }, `Handling block_action: ${action.action_id}`);
188
+ registerBackgroundWork(handleToolApproval({
189
+ actionValue: action.value,
190
+ approved,
191
+ teamId,
192
+ slackUserId,
193
+ responseUrl
194
+ }).catch((err) => {
195
+ const errorMessage = err instanceof Error ? err.message : String(err);
196
+ logger.error({
197
+ errorMessage,
198
+ actionId: action.action_id
199
+ }, "Failed to handle tool approval");
200
+ }).finally(() => flushTraces()));
201
+ }
179
202
  if (action.action_id === "open_follow_up_modal" && action.value && triggerId) {
180
203
  anyHandled = true;
181
204
  logger.info({
@@ -45,6 +45,7 @@ declare const SlackStrings: {
45
45
  };
46
46
  readonly status: {
47
47
  readonly thinking: (agentName: string) => string;
48
+ readonly readingThread: (agentName: string) => string;
48
49
  readonly noAgentsAvailable: "No agents available";
49
50
  readonly noProjectsConfigured: "No projects configured. Set up projects in the dashboard.";
50
51
  };
@@ -41,6 +41,7 @@ const SlackStrings = {
41
41
  usage: { mentionEmpty: "*Include a message to use your Inkeep agent:*\n\n• `@Inkeep <message>` — Message the default agent (reply appears in a thread)\n• `@Inkeep <message>` in a thread — Includes thread as context\n• `@Inkeep` in a thread — Uses the full thread as context\n\nUse `/inkeep help` for all available commands." },
42
42
  status: {
43
43
  thinking: (agentName) => `_${agentName} is thinking..._`,
44
+ readingThread: (agentName) => `_${agentName} is reading this thread..._`,
44
45
  noAgentsAvailable: "No agents available",
45
46
  noProjectsConfigured: "No projects configured. Set up projects in the dashboard."
46
47
  },
@@ -70,7 +70,7 @@ app.post("/events", async (c) => {
70
70
  retryReason
71
71
  }, "Acknowledging Slack retry without re-processing");
72
72
  span.end();
73
- return c.json({ ok: true });
73
+ return c.body(null, 200);
74
74
  });
75
75
  const waitUntil = await getWaitUntil();
76
76
  return tracer.startActiveSpan(SLACK_SPAN_NAMES.WEBHOOK, async (span) => {
@@ -128,7 +128,7 @@ app.post("/events", async (c) => {
128
128
  return c.json(result.response);
129
129
  }
130
130
  span.end();
131
- return c.json({ ok: true });
131
+ return c.body(null, 200);
132
132
  } catch (error) {
133
133
  outcome = "error";
134
134
  span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
@@ -6,7 +6,7 @@ import { getSlackClient, getSlackTeamInfo, getSlackUserInfo } from "../services/
6
6
  import { getBotTokenForTeam, setBotTokenForTeam } from "../services/workspace-tokens.js";
7
7
  import "../services/index.js";
8
8
  import { OpenAPIHono, z } from "@hono/zod-openapi";
9
- import { createWorkAppSlackWorkspace } from "@inkeep/agents-core";
9
+ import { createWorkAppSlackWorkspace, isUniqueConstraintError } from "@inkeep/agents-core";
10
10
  import * as crypto$1 from "node:crypto";
11
11
  import { createProtectedRoute, noAuth } from "@inkeep/agents-core/middleware";
12
12
 
@@ -96,6 +96,7 @@ app.openapi(createProtectedRoute({
96
96
  "chat:write",
97
97
  "chat:write.public",
98
98
  "commands",
99
+ "files:write",
99
100
  "groups:history",
100
101
  "groups:read",
101
102
  "im:history",
@@ -252,8 +253,7 @@ app.openapi(createProtectedRoute({
252
253
  tenantId
253
254
  }, "Persisted workspace installation to database");
254
255
  } catch (dbError) {
255
- const dbErrorMessage = dbError instanceof Error ? dbError.message : String(dbError);
256
- if (dbErrorMessage.includes("duplicate key") || dbErrorMessage.includes("unique constraint")) logger.info({
256
+ if (isUniqueConstraintError(dbError)) logger.info({
257
257
  teamId: workspaceData.teamId,
258
258
  tenantId
259
259
  }, "Workspace already exists in database");
@@ -261,7 +261,6 @@ app.openapi(createProtectedRoute({
261
261
  const pgCode = dbError && typeof dbError === "object" && "code" in dbError ? dbError.code : void 0;
262
262
  logger.error({
263
263
  err: dbError,
264
- dbErrorMessage,
265
264
  pgCode,
266
265
  teamId: workspaceData.teamId,
267
266
  tenantId,
@@ -3,7 +3,7 @@ import runDbClient_default from "../../db/runDbClient.js";
3
3
  import { createConnectSession } from "../services/nango.js";
4
4
  import "../services/index.js";
5
5
  import { OpenAPIHono, z } from "@hono/zod-openapi";
6
- import { createWorkAppSlackUserMapping, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingByInkeepUserId, verifySlackLinkToken } from "@inkeep/agents-core";
6
+ import { createWorkAppSlackUserMapping, deleteWorkAppSlackUserMapping, findWorkAppSlackUserMapping, findWorkAppSlackUserMappingByInkeepUserId, isUniqueConstraintError, verifySlackLinkToken } from "@inkeep/agents-core";
7
7
  import { createProtectedRoute, inheritedWorkAppsAuth } from "@inkeep/agents-core/middleware";
8
8
 
9
9
  //#region src/slack/routes/users.ts
@@ -131,12 +131,15 @@ app.openapi(createProtectedRoute({
131
131
  tenantId
132
132
  });
133
133
  }
134
- if (existingLink) logger.info({
135
- slackUserId,
136
- existingUserId: existingLink.inkeepUserId,
137
- newUserId: inkeepUserId,
138
- tenantId
139
- }, "Slack user already linked, updating to new user");
134
+ if (existingLink) {
135
+ logger.info({
136
+ slackUserId,
137
+ existingUserId: existingLink.inkeepUserId,
138
+ newUserId: inkeepUserId,
139
+ tenantId
140
+ }, "Slack user already linked, updating to new user");
141
+ await deleteWorkAppSlackUserMapping(runDbClient_default)(tenantId, slackUserId, teamId, "work-apps-slack");
142
+ }
140
143
  const slackUserMapping = await createWorkAppSlackUserMapping(runDbClient_default)({
141
144
  tenantId,
142
145
  clientId: "work-apps-slack",
@@ -162,10 +165,9 @@ app.openapi(createProtectedRoute({
162
165
  tenantId
163
166
  });
164
167
  } catch (error) {
165
- const errorMessage = error instanceof Error ? error.message : String(error);
166
- if (errorMessage.includes("duplicate key") || errorMessage.includes("unique constraint")) {
167
- logger.warn({ userId: body.userId }, "Slack user already linked");
168
- return c.json({ error: "This Slack account is already linked to an Inkeep account." }, 409);
168
+ if (isUniqueConstraintError(error)) {
169
+ logger.info({ userId: body.userId }, "Concurrent link resolved — mapping already exists");
170
+ return c.json({ success: true });
169
171
  }
170
172
  logger.error({
171
173
  error,
@@ -5,7 +5,7 @@ import { getSlackChannels, getSlackClient, revokeSlackToken } from "../services/
5
5
  import "../services/index.js";
6
6
  import { requireChannelMemberOrAdmin, requireWorkspaceAdmin } from "../middleware/permissions.js";
7
7
  import { OpenAPIHono, z } from "@hono/zod-openapi";
8
- import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
8
+ import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, findWorkAppSlackWorkspaceByTeamId, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, updateWorkAppSlackWorkspace, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
9
9
  import { createProtectedRoute, inheritedWorkAppsAuth } from "@inkeep/agents-core/middleware";
10
10
 
11
11
  //#region src/slack/routes/workspaces.ts
@@ -48,6 +48,7 @@ const ChannelAgentConfigSchema = z.object({
48
48
  grantAccessToMembers: z.boolean().optional()
49
49
  });
50
50
  const WorkspaceSettingsSchema = z.object({ defaultAgent: ChannelAgentConfigSchema.optional() });
51
+ const JoinFromWorkspaceSettingsSchema = z.object({ shouldAllowJoinFromWorkspace: z.boolean() });
51
52
  app.openapi(createProtectedRoute({
52
53
  method: "get",
53
54
  path: "/",
@@ -222,6 +223,89 @@ app.openapi(createProtectedRoute({
222
223
  }
223
224
  return c.json({ success: true });
224
225
  });
226
+ app.openapi(createProtectedRoute({
227
+ method: "get",
228
+ path: "/{teamId}/join-from-workspace",
229
+ summary: "Get Join From Workspace Setting",
230
+ description: "Get the join from workspace setting for the workspace",
231
+ operationId: "slack-get-join-from-workspace",
232
+ tags: [
233
+ "Work Apps",
234
+ "Slack",
235
+ "Workspaces"
236
+ ],
237
+ permission: inheritedWorkAppsAuth(),
238
+ request: { params: z.object({ teamId: z.string() }) },
239
+ responses: {
240
+ 200: {
241
+ description: "Join from workspace setting",
242
+ content: { "application/json": { schema: JoinFromWorkspaceSettingsSchema } }
243
+ },
244
+ 404: { description: "Workspace not found" }
245
+ }
246
+ }), async (c) => {
247
+ const { teamId } = c.req.valid("param");
248
+ const sessionTenantId = c.get("tenantId");
249
+ if (!sessionTenantId) return c.json({ error: "Unauthorized" }, 401);
250
+ const workspace = await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(sessionTenantId, teamId);
251
+ if (!workspace) return c.json({ shouldAllowJoinFromWorkspace: false });
252
+ return c.json({ shouldAllowJoinFromWorkspace: workspace.shouldAllowJoinFromWorkspace ?? false });
253
+ });
254
+ app.openapi(createProtectedRoute({
255
+ method: "put",
256
+ path: "/{teamId}/join-from-workspace",
257
+ summary: "Update Join From Workspace Setting",
258
+ description: "Enable or disable join from workspace for the workspace",
259
+ operationId: "slack-update-join-from-workspace",
260
+ tags: [
261
+ "Work Apps",
262
+ "Slack",
263
+ "Workspaces"
264
+ ],
265
+ permission: requireWorkspaceAdmin(),
266
+ request: {
267
+ params: z.object({ teamId: z.string() }),
268
+ body: { content: { "application/json": { schema: JoinFromWorkspaceSettingsSchema } } }
269
+ },
270
+ responses: {
271
+ 200: {
272
+ description: "Join from workspace setting updated",
273
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
274
+ },
275
+ 401: { description: "Unauthorized" },
276
+ 404: { description: "Workspace not found" },
277
+ 500: { description: "Failed to update setting" }
278
+ }
279
+ }), async (c) => {
280
+ const { teamId } = c.req.valid("param");
281
+ const { shouldAllowJoinFromWorkspace } = c.req.valid("json");
282
+ const sessionTenantId = c.get("tenantId");
283
+ if (!sessionTenantId) return c.json({ error: "Unauthorized" }, 401);
284
+ const workspace = await findWorkAppSlackWorkspaceByTeamId(runDbClient_default)(sessionTenantId, teamId);
285
+ if (!workspace) return c.json({ error: "Workspace not found" }, 404);
286
+ try {
287
+ if (!await updateWorkAppSlackWorkspace(runDbClient_default)(workspace.id, { shouldAllowJoinFromWorkspace })) {
288
+ logger.error({
289
+ teamId,
290
+ shouldAllowJoinFromWorkspace
291
+ }, "Failed to update join from workspace setting");
292
+ return c.json({ error: "Failed to update setting" }, 500);
293
+ }
294
+ logger.info({
295
+ teamId,
296
+ shouldAllowJoinFromWorkspace,
297
+ workspaceId: workspace.id
298
+ }, "Updated workspace join from workspace settings");
299
+ return c.json({ success: true });
300
+ } catch (error) {
301
+ logger.error({
302
+ teamId,
303
+ shouldAllowJoinFromWorkspace,
304
+ error
305
+ }, "Failed to update join from workspace setting");
306
+ return c.json({ error: "Failed to update setting" }, 500);
307
+ }
308
+ });
225
309
  app.openapi(createProtectedRoute({
226
310
  method: "delete",
227
311
  path: "/{teamId}",
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import * as slack_block_builder0 from "slack-block-builder";
2
3
 
3
4
  //#region src/slack/services/blocks/index.d.ts
@@ -63,6 +64,85 @@ interface AgentConfigSources {
63
64
  } | null;
64
65
  }
65
66
  declare function createStatusMessage(email: string, linkedAt: string, dashboardUrl: string, agentConfigs: AgentConfigSources): Readonly<slack_block_builder0.SlackMessageDto>;
67
+ interface ToolApprovalButtonValue {
68
+ toolCallId: string;
69
+ conversationId: string;
70
+ projectId: string;
71
+ agentId: string;
72
+ slackUserId: string;
73
+ channel: string;
74
+ threadTs: string;
75
+ toolName: string;
76
+ }
77
+ declare const ToolApprovalButtonValueSchema: z.ZodObject<{
78
+ toolCallId: z.ZodString;
79
+ conversationId: z.ZodString;
80
+ projectId: z.ZodString;
81
+ agentId: z.ZodString;
82
+ slackUserId: z.ZodString;
83
+ channel: z.ZodString;
84
+ threadTs: z.ZodString;
85
+ toolName: z.ZodString;
86
+ }, z.core.$strip>;
87
+ declare function buildToolApprovalBlocks(params: {
88
+ toolName: string;
89
+ input?: Record<string, unknown>;
90
+ buttonValue: string;
91
+ }): any[];
92
+ declare function buildToolApprovalDoneBlocks(params: {
93
+ toolName: string;
94
+ approved: boolean;
95
+ actorUserId: string;
96
+ }): {
97
+ type: string;
98
+ elements: {
99
+ type: string;
100
+ text: string;
101
+ }[];
102
+ }[];
103
+ declare function buildToolApprovalExpiredBlocks(params: {
104
+ toolName: string;
105
+ }): {
106
+ type: string;
107
+ elements: {
108
+ type: string;
109
+ text: string;
110
+ }[];
111
+ }[];
112
+ declare function buildToolOutputErrorBlock(toolName: string, errorText: string): {
113
+ type: "context";
114
+ elements: {
115
+ type: "mrkdwn";
116
+ text: string;
117
+ }[];
118
+ };
119
+ declare function buildSummaryBreadcrumbBlock(labels: string[]): {
120
+ type: "context";
121
+ elements: {
122
+ type: "mrkdwn";
123
+ text: string;
124
+ }[];
125
+ };
126
+ declare function buildDataComponentBlocks(component: {
127
+ id: string;
128
+ data: Record<string, unknown>;
129
+ }): {
130
+ blocks: any[];
131
+ overflowJson?: string;
132
+ componentType?: string;
133
+ };
134
+ declare function buildDataArtifactBlocks(artifact: {
135
+ data: Record<string, unknown>;
136
+ }): {
137
+ blocks: any[];
138
+ overflowContent?: string;
139
+ artifactName?: string;
140
+ };
141
+ declare function buildCitationsBlock(citations: Array<{
142
+ title?: string;
143
+ url?: string;
144
+ }>): any[];
66
145
  declare function createJwtLinkMessage(linkUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
146
+ declare function createCreateInkeepAccountMessage(acceptUrl: string, expiresInMinutes: number): Readonly<slack_block_builder0.SlackMessageDto>;
67
147
  //#endregion
68
- export { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage };
148
+ 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 };