@inkeep/agents-work-apps 0.51.0 → 0.53.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.
@@ -1,11 +1,11 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types3 from "hono/types";
2
+ import * as hono_types0 from "hono/types";
3
3
 
4
4
  //#region src/github/mcp/index.d.ts
5
5
  declare const app: Hono<{
6
6
  Variables: {
7
7
  toolId: string;
8
8
  };
9
- }, hono_types3.BlankSchema, "/">;
9
+ }, hono_types0.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, listIssueReactions, 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"),
@@ -607,13 +666,17 @@ const getServer = async (toolId) => {
607
666
  };
608
667
  }
609
668
  });
610
- server.tool("add-comment-reaction", `Add a reaction to a comment on a pull request. Supports general pull request comments and inline PR review comments. ${getAvailableRepositoryString(repositoryAccess)}`, {
669
+ server.tool("add-reaction", `Add a reaction to a pull request body, a general PR comment, or an inline PR review comment. ${getAvailableRepositoryString(repositoryAccess)}`, {
611
670
  owner: z.string().describe("Repository owner name"),
612
671
  repo: z.string().describe("Repository name"),
613
- comment_id: z.number().describe("The ID of the comment to react to"),
614
- comment_type: z.enum(["issue_comment", "review_comment"]).describe("The type of comment: \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments"),
672
+ target_id: z.number().describe("The target to react to: a comment ID for issue_comment/review_comment, or the pull request number for pull_request"),
673
+ target_type: z.enum([
674
+ "pull_request",
675
+ "issue_comment",
676
+ "review_comment"
677
+ ]).describe("The type of target: \"pull_request\" for the PR body itself, \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments"),
615
678
  reaction: ReactionContentSchema.describe("The reaction emoji to add: +1, -1, laugh, hooray, confused, heart, rocket, or eyes")
616
- }, async ({ owner, repo, comment_id, comment_type, reaction }) => {
679
+ }, async ({ owner, repo, target_id, target_type, reaction }) => {
617
680
  try {
618
681
  let githubClient;
619
682
  try {
@@ -627,9 +690,23 @@ const getServer = async (toolId) => {
627
690
  isError: true
628
691
  };
629
692
  }
693
+ let result;
694
+ if (target_type === "pull_request") {
695
+ const { data } = await githubClient.rest.reactions.createForIssue({
696
+ owner,
697
+ repo,
698
+ issue_number: target_id,
699
+ content: reaction
700
+ });
701
+ result = {
702
+ id: data.id,
703
+ content: data.content
704
+ };
705
+ } else if (target_type === "issue_comment") result = await createIssueCommentReaction(githubClient, owner, repo, target_id, reaction);
706
+ else result = await createPullRequestReviewCommentReaction(githubClient, owner, repo, target_id, reaction);
630
707
  return { content: [{
631
708
  type: "text",
632
- text: `Successfully added ${reaction} reaction to ${comment_type} comment ${comment_id} in ${owner}/${repo}\n\nReaction ID: ${(comment_type === "issue_comment" ? await createIssueCommentReaction(githubClient, owner, repo, comment_id, reaction) : await createPullRequestReviewCommentReaction(githubClient, owner, repo, comment_id, reaction)).id}`
709
+ text: `Successfully added ${reaction} reaction to ${target_type === "pull_request" ? `PR #${target_id}` : `${target_type} ${target_id}`} in ${owner}/${repo}\n\nReaction ID: ${result.id}`
633
710
  }] };
634
711
  } catch (error) {
635
712
  if (error instanceof Error && "status" in error) {
@@ -637,21 +714,21 @@ const getServer = async (toolId) => {
637
714
  if (apiError.status === 404) return {
638
715
  content: [{
639
716
  type: "text",
640
- text: `Comment ${comment_id} not found in ${owner}/${repo}.`
717
+ text: `Target ${target_id} (${target_type}) not found in ${owner}/${repo}.`
641
718
  }],
642
719
  isError: true
643
720
  };
644
721
  if (apiError.status === 422) return {
645
722
  content: [{
646
723
  type: "text",
647
- text: `Invalid reaction. Ensure the reaction type is valid and the comment exists.`
724
+ text: `Invalid reaction. Ensure the reaction type is valid and the target exists.`
648
725
  }],
649
726
  isError: true
650
727
  };
651
728
  if (apiError.status === 403) return {
652
729
  content: [{
653
730
  type: "text",
654
- text: `Access denied when adding reaction to comment ${comment_id} in ${owner}/${repo}. Your GitHub App may not have sufficient permissions to create reactions.`
731
+ text: `Access denied when adding reaction to ${target_type} ${target_id} in ${owner}/${repo}. Your GitHub App may not have sufficient permissions to create reactions.`
655
732
  }],
656
733
  isError: true
657
734
  };
@@ -665,13 +742,17 @@ const getServer = async (toolId) => {
665
742
  };
666
743
  }
667
744
  });
668
- server.tool("remove-comment-reaction", `Remove a reaction from a comment on a pull request. Requires the reaction ID (returned when adding a reaction or available from comment data). ${getAvailableRepositoryString(repositoryAccess)}`, {
745
+ server.tool("remove-reaction", `Remove a reaction from a pull request body, a general PR comment, or an inline PR review comment. Requires the reaction ID (returned when adding a reaction or from list-reactions). ${getAvailableRepositoryString(repositoryAccess)}`, {
669
746
  owner: z.string().describe("Repository owner name"),
670
747
  repo: z.string().describe("Repository name"),
671
- comment_id: z.number().describe("The ID of the comment the reaction belongs to"),
672
- comment_type: z.enum(["issue_comment", "review_comment"]).describe("The type of comment: \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments"),
748
+ target_id: z.number().describe("The target the reaction belongs to: a comment ID for issue_comment/review_comment, or the pull request number for pull_request"),
749
+ target_type: z.enum([
750
+ "pull_request",
751
+ "issue_comment",
752
+ "review_comment"
753
+ ]).describe("The type of target: \"pull_request\" for the PR body itself, \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments"),
673
754
  reaction_id: z.number().describe("The ID of the reaction to remove")
674
- }, async ({ owner, repo, comment_id, comment_type, reaction_id }) => {
755
+ }, async ({ owner, repo, target_id, target_type, reaction_id }) => {
675
756
  try {
676
757
  let githubClient;
677
758
  try {
@@ -685,18 +766,24 @@ const getServer = async (toolId) => {
685
766
  isError: true
686
767
  };
687
768
  }
688
- if (comment_type === "issue_comment") await deleteIssueCommentReaction(githubClient, owner, repo, comment_id, reaction_id);
689
- else await deletePullRequestReviewCommentReaction(githubClient, owner, repo, comment_id, reaction_id);
769
+ if (target_type === "pull_request") await githubClient.rest.reactions.deleteForIssue({
770
+ owner,
771
+ repo,
772
+ issue_number: target_id,
773
+ reaction_id
774
+ });
775
+ else if (target_type === "issue_comment") await deleteIssueCommentReaction(githubClient, owner, repo, target_id, reaction_id);
776
+ else await deletePullRequestReviewCommentReaction(githubClient, owner, repo, target_id, reaction_id);
690
777
  return { content: [{
691
778
  type: "text",
692
- text: `Successfully removed reaction ${reaction_id} from ${comment_type} comment ${comment_id} in ${owner}/${repo}`
779
+ text: `Successfully removed reaction ${reaction_id} from ${target_type === "pull_request" ? `PR #${target_id}` : `${target_type} ${target_id}`} in ${owner}/${repo}`
693
780
  }] };
694
781
  } catch (error) {
695
782
  if (error instanceof Error && "status" in error) {
696
783
  if (error.status === 404) return {
697
784
  content: [{
698
785
  type: "text",
699
- text: `Comment ${comment_id} or reaction ${reaction_id} not found in ${owner}/${repo}.`
786
+ text: `Target ${target_id} (${target_type}) or reaction ${reaction_id} not found in ${owner}/${repo}.`
700
787
  }],
701
788
  isError: true
702
789
  };
@@ -710,12 +797,16 @@ const getServer = async (toolId) => {
710
797
  };
711
798
  }
712
799
  });
713
- server.tool("list-comment-reactions", `List all reactions on a comment, including each reaction's ID (needed for removal). Supports both general issue/PR comments and inline PR review comments. ${getAvailableRepositoryString(repositoryAccess)}`, {
800
+ server.tool("list-reactions", `List all reactions on a pull request body, a general PR comment, or an inline PR review comment. Returns each reaction's ID (needed for removal). ${getAvailableRepositoryString(repositoryAccess)}`, {
714
801
  owner: z.string().describe("Repository owner name"),
715
802
  repo: z.string().describe("Repository name"),
716
- comment_id: z.number().describe("The ID of the comment to list reactions for"),
717
- comment_type: z.enum(["issue_comment", "review_comment"]).describe("The type of comment: \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments")
718
- }, async ({ owner, repo, comment_id, comment_type }) => {
803
+ target_id: z.number().describe("The target to list reactions for: a comment ID for issue_comment/review_comment, or the pull request number for pull_request"),
804
+ target_type: z.enum([
805
+ "pull_request",
806
+ "issue_comment",
807
+ "review_comment"
808
+ ]).describe("The type of target: \"pull_request\" for the PR body itself, \"issue_comment\" for general pull request comments, \"review_comment\" for inline PR review comments")
809
+ }, async ({ owner, repo, target_id, target_type }) => {
719
810
  try {
720
811
  let githubClient;
721
812
  try {
@@ -729,22 +820,26 @@ const getServer = async (toolId) => {
729
820
  isError: true
730
821
  };
731
822
  }
732
- const reactions = comment_type === "issue_comment" ? await listIssueCommentReactions(githubClient, owner, repo, comment_id) : await listPullRequestReviewCommentReactions(githubClient, owner, repo, comment_id);
823
+ let reactions = [];
824
+ if (target_type === "pull_request") reactions = await listIssueReactions(githubClient, owner, repo, target_id);
825
+ else if (target_type === "issue_comment") reactions = await listIssueCommentReactions(githubClient, owner, repo, target_id);
826
+ else reactions = await listPullRequestReviewCommentReactions(githubClient, owner, repo, target_id);
827
+ const label = target_type === "pull_request" ? `PR #${target_id}` : `${target_type} ${target_id}`;
733
828
  if (reactions.length === 0) return { content: [{
734
829
  type: "text",
735
- text: `No reactions found on ${comment_type} comment ${comment_id} in ${owner}/${repo}.`
830
+ text: `No reactions found on ${label} in ${owner}/${repo}.`
736
831
  }] };
737
832
  const formatted = reactions.map((r) => `• ${r.content} by @${r.user} (reaction_id: ${r.id})`).join("\n");
738
833
  return { content: [{
739
834
  type: "text",
740
- text: `Found ${reactions.length} reaction(s) on ${comment_type} comment ${comment_id} in ${owner}/${repo}:\n\n${formatted}`
835
+ text: `Found ${reactions.length} reaction(s) on ${label} in ${owner}/${repo}:\n\n${formatted}`
741
836
  }] };
742
837
  } catch (error) {
743
838
  if (error instanceof Error && "status" in error) {
744
839
  if (error.status === 404) return {
745
840
  content: [{
746
841
  type: "text",
747
- text: `Comment ${comment_id} not found in ${owner}/${repo}.`
842
+ text: `Target ${target_id} (${target_type}) not found in ${owner}/${repo}.`
748
843
  }],
749
844
  isError: true
750
845
  };
@@ -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
  */
@@ -245,6 +254,7 @@ interface ReactionDetail {
245
254
  }
246
255
  declare function listIssueCommentReactions(octokit: Octokit, owner: string, repo: string, commentId: number): Promise<ReactionDetail[]>;
247
256
  declare function listPullRequestReviewCommentReactions(octokit: Octokit, owner: string, repo: string, commentId: number): Promise<ReactionDetail[]>;
257
+ declare function listIssueReactions(octokit: Octokit, owner: string, repo: string, issueNumber: number): Promise<ReactionDetail[]>;
248
258
  declare function formatFileDiff(pullRequestNumber: number, files: ChangedFile[], includeContents?: boolean): Promise<string>;
249
259
  //#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 };
260
+ 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, listIssueReactions, 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) {
@@ -672,6 +758,21 @@ async function listPullRequestReviewCommentReactions(octokit, owner, repo, comme
672
758
  });
673
759
  return reactions;
674
760
  }
761
+ async function listIssueReactions(octokit, owner, repo, issueNumber) {
762
+ const reactions = [];
763
+ for await (const response of octokit.paginate.iterator(octokit.rest.reactions.listForIssue, {
764
+ owner,
765
+ repo,
766
+ issue_number: issueNumber,
767
+ per_page: 100
768
+ })) for (const r of response.data) reactions.push({
769
+ id: r.id,
770
+ content: r.content,
771
+ user: r.user?.login ?? "unknown",
772
+ createdAt: r.created_at
773
+ });
774
+ return reactions;
775
+ }
675
776
  async function formatFileDiff(pullRequestNumber, files, includeContents = false) {
676
777
  let output = `## File Patches for PR #${pullRequestNumber}\n\n`;
677
778
  output += `Found ${files.length} file(s) matching the requested paths.\n\n`;
@@ -694,4 +795,4 @@ async function formatFileDiff(pullRequestNumber, files, includeContents = false)
694
795
  }
695
796
 
696
797
  //#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 };
798
+ export { applyOperation, applyOperations, commitFileChanges, commitNewFile, createIssueCommentReaction, createPullRequestReviewCommentReaction, deleteIssueCommentReaction, deletePullRequestReviewCommentReaction, fetchBranchChangedFiles, fetchComments, fetchCommitDetails, fetchPrCommits, fetchPrFileDiffs, fetchPrFiles, fetchPrInfo, formatFileDiff, generatePrMarkdown, getFilePathsInRepo, getGitHubClientFromInstallationId, getGitHubClientFromRepo, listIssueCommentReactions, listIssueReactions, listPullRequestReviewCommentReactions, validateLineNumbers, visualizeUpdateOperations };
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from "../logger.js";
2
2
  import { findWorkspaceConnectionByTeamId } from "./services/nango.js";
3
- import { getSlackClient } from "./services/client.js";
4
3
  import { sendResponseUrlMessage } from "./services/events/utils.js";
4
+ import { getSlackClient } from "./services/client.js";
5
5
  import { SLACK_SPAN_KEYS } from "./tracer.js";
6
6
  import { handleAppMention } from "./services/events/app-mention.js";
7
7
  import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleToolApproval } from "./services/events/block-actions.js";
@@ -35,6 +35,15 @@ async function dispatchSlackEvent(eventType, payload, options, span) {
35
35
  }, "Ignoring bot message");
36
36
  return { outcome };
37
37
  }
38
+ if (event?.edited) {
39
+ outcome = "ignored_edited_message";
40
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
41
+ logger.info({
42
+ teamId,
43
+ innerEventType
44
+ }, "Ignoring edited message");
45
+ return { outcome };
46
+ }
38
47
  if (event?.type === "app_mention" && event.channel && event.user && teamId) {
39
48
  outcome = "handled";
40
49
  span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
@@ -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, listWorkAppSlackWorkspacesByTenant } 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",
@@ -212,6 +213,26 @@ app.openapi(createProtectedRoute({
212
213
  appId: tokenData.app_id,
213
214
  installedAt: (/* @__PURE__ */ new Date()).toISOString()
214
215
  };
216
+ if (tenantId && workspaceData.teamId) {
217
+ let existingWorkspaces;
218
+ try {
219
+ existingWorkspaces = await listWorkAppSlackWorkspacesByTenant(runDbClient_default)(tenantId);
220
+ } catch (err) {
221
+ logger.error({
222
+ err,
223
+ tenantId
224
+ }, "Failed to check existing workspaces");
225
+ return c.redirect(`${dashboardUrl}?error=workspace_check_failed`);
226
+ }
227
+ if (existingWorkspaces.some((w) => w.slackTeamId !== workspaceData.teamId)) {
228
+ logger.warn({
229
+ tenantId,
230
+ newTeamId: workspaceData.teamId,
231
+ existingTeamIds: existingWorkspaces.map((w) => w.slackTeamId)
232
+ }, "Tenant already has a different Slack workspace, rejecting installation");
233
+ return c.redirect(`${dashboardUrl}?error=workspace_limit_reached`);
234
+ }
235
+ }
215
236
  if (workspaceData.teamId && workspaceData.botToken) {
216
237
  clearWorkspaceConnectionCache(workspaceData.teamId);
217
238
  const nangoResult = await storeWorkspaceInstallation({
@@ -252,8 +273,7 @@ app.openapi(createProtectedRoute({
252
273
  tenantId
253
274
  }, "Persisted workspace installation to database");
254
275
  } catch (dbError) {
255
- const dbErrorMessage = dbError instanceof Error ? dbError.message : String(dbError);
256
- if (dbErrorMessage.includes("duplicate key") || dbErrorMessage.includes("unique constraint")) logger.info({
276
+ if (isUniqueConstraintError(dbError)) logger.info({
257
277
  teamId: workspaceData.teamId,
258
278
  tenantId
259
279
  }, "Workspace already exists in database");
@@ -261,7 +281,6 @@ app.openapi(createProtectedRoute({
261
281
  const pgCode = dbError && typeof dbError === "object" && "code" in dbError ? dbError.code : void 0;
262
282
  logger.error({
263
283
  err: dbError,
264
- dbErrorMessage,
265
284
  pgCode,
266
285
  teamId: workspaceData.teamId,
267
286
  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
@@ -165,7 +165,7 @@ app.openapi(createProtectedRoute({
165
165
  tenantId
166
166
  });
167
167
  } catch (error) {
168
- if (error instanceof Error && (error.message.includes("duplicate key") || error.message.includes("unique constraint")) || typeof error === "object" && error !== null && "cause" in error && typeof error.cause === "object" && error.cause?.code === "23505") {
168
+ if (isUniqueConstraintError(error)) {
169
169
  logger.info({ userId: body.userId }, "Concurrent link resolved — mapping already exists");
170
170
  return c.json({ success: true });
171
171
  }