paperclip-github-plugin 0.7.3 → 0.7.4

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/worker.js CHANGED
@@ -255,6 +255,10 @@ var GITHUB_AGENT_TOOLS = [
255
255
  required: ["head", "base", "title"],
256
256
  properties: {
257
257
  repository: repositoryProperty,
258
+ paperclipIssueId: {
259
+ type: "string",
260
+ description: "Optional Paperclip issue id to link with the created pull request so GitHub Sync can monitor PR status for that issue."
261
+ },
258
262
  head: {
259
263
  type: "string",
260
264
  description: "Head branch name or owner:branch."
@@ -2696,13 +2700,11 @@ async function buildIssueGitHubDetails(ctx, input) {
2696
2700
  const link = await resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, {
2697
2701
  linkRecords
2698
2702
  });
2699
- if (!link) {
2700
- return null;
2701
- }
2702
- const entityMatch = link.entityRecord;
2703
- if (entityMatch) {
2703
+ if (link?.entityRecord) {
2704
+ const entityMatch = link.entityRecord;
2704
2705
  return {
2705
2706
  paperclipIssueId: issueId,
2707
+ kind: "issue",
2706
2708
  source: "entity",
2707
2709
  githubIssueNumber: entityMatch.data.githubIssueNumber,
2708
2710
  githubIssueUrl: entityMatch.data.githubIssueUrl,
@@ -2723,14 +2725,48 @@ async function buildIssueGitHubDetails(ctx, input) {
2723
2725
  syncedAt: entityMatch.data.syncedAt
2724
2726
  };
2725
2727
  }
2728
+ if (link) {
2729
+ return {
2730
+ paperclipIssueId: issueId,
2731
+ kind: "issue",
2732
+ source: link.source,
2733
+ githubIssueNumber: link.githubIssueNumber,
2734
+ githubIssueUrl: link.githubIssueUrl,
2735
+ repositoryUrl: link.repositoryUrl,
2736
+ linkedPullRequestNumbers: link.linkedPullRequestNumbers,
2737
+ linkedPullRequests: link.linkedPullRequests
2738
+ };
2739
+ }
2740
+ const pullRequestLinks = await listGitHubPullRequestLinkRecords(ctx, {
2741
+ paperclipIssueId: issueId
2742
+ });
2743
+ const pullRequestLink = pullRequestLinks.filter((record) => !record.data.companyId || record.data.companyId === companyId).sort((left, right) => {
2744
+ const rightTimestamp = Date.parse(right.updatedAt ?? right.createdAt ?? "");
2745
+ const leftTimestamp = Date.parse(left.updatedAt ?? left.createdAt ?? "");
2746
+ const safeRightTimestamp = Number.isFinite(rightTimestamp) ? rightTimestamp : 0;
2747
+ const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
2748
+ return safeRightTimestamp - safeLeftTimestamp;
2749
+ })[0];
2750
+ if (!pullRequestLink) {
2751
+ return null;
2752
+ }
2726
2753
  return {
2727
2754
  paperclipIssueId: issueId,
2728
- source: link.source,
2729
- githubIssueNumber: link.githubIssueNumber,
2730
- githubIssueUrl: link.githubIssueUrl,
2731
- repositoryUrl: link.repositoryUrl,
2732
- linkedPullRequestNumbers: link.linkedPullRequestNumbers,
2733
- linkedPullRequests: link.linkedPullRequests
2755
+ kind: "pull_request",
2756
+ source: "pull_request_entity",
2757
+ repositoryUrl: pullRequestLink.data.repositoryUrl,
2758
+ githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2759
+ githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2760
+ githubPullRequestState: pullRequestLink.data.githubPullRequestState,
2761
+ title: pullRequestLink.data.title,
2762
+ linkedPullRequestNumbers: [pullRequestLink.data.githubPullRequestNumber],
2763
+ linkedPullRequests: [
2764
+ {
2765
+ number: pullRequestLink.data.githubPullRequestNumber,
2766
+ repositoryUrl: pullRequestLink.data.repositoryUrl
2767
+ }
2768
+ ],
2769
+ syncedAt: pullRequestLink.data.syncedAt
2734
2770
  };
2735
2771
  }
2736
2772
  async function resolveIssueByIdentifier(ctx, input) {
@@ -6014,24 +6050,34 @@ function parseGitHubIssueHtmlUrl(value) {
6014
6050
  function normalizeGitHubIssueHtmlUrl(value) {
6015
6051
  return parseGitHubIssueHtmlUrl(value)?.issueUrl;
6016
6052
  }
6017
- function normalizeGitHubPullRequestHtmlUrl(value) {
6018
- if (typeof value !== "string" || !value.trim()) {
6019
- return void 0;
6020
- }
6053
+ function parseGitHubPullRequestHtmlUrl(value) {
6021
6054
  try {
6022
- const parsed = new URL(value);
6023
- if (parsed.protocol !== "https:" || parsed.hostname !== "github.com") {
6055
+ const url = new URL(value.trim());
6056
+ const hostname = url.hostname.trim().toLowerCase();
6057
+ if (hostname !== "github.com" && hostname !== "www.github.com") {
6024
6058
  return void 0;
6025
6059
  }
6026
- const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i);
6060
+ const match = url.pathname.match(/^\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)\/?$/);
6027
6061
  if (!match) {
6028
6062
  return void 0;
6029
6063
  }
6030
- return `https://github.com/${match[1]}/${match[2]}/pull/${match[3]}`;
6064
+ return {
6065
+ owner: match[1],
6066
+ repo: match[2],
6067
+ repositoryUrl: `https://github.com/${match[1]}/${match[2]}`,
6068
+ pullRequestNumber: Number(match[3]),
6069
+ pullRequestUrl: `https://github.com/${match[1]}/${match[2]}/pull/${match[3]}`
6070
+ };
6031
6071
  } catch {
6032
6072
  return void 0;
6033
6073
  }
6034
6074
  }
6075
+ function normalizeGitHubPullRequestHtmlUrl(value) {
6076
+ if (typeof value !== "string" || !value.trim()) {
6077
+ return void 0;
6078
+ }
6079
+ return parseGitHubPullRequestHtmlUrl(value)?.pullRequestUrl;
6080
+ }
6035
6081
  function escapeRegExp(value) {
6036
6082
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6037
6083
  }
@@ -6705,6 +6751,257 @@ async function upsertGitHubPullRequestLinkRecord(ctx, params) {
6705
6751
  }
6706
6752
  });
6707
6753
  }
6754
+ function normalizeIssueGitHubLinkKind(value) {
6755
+ if (value === "issue" || value === "github_issue") {
6756
+ return "issue";
6757
+ }
6758
+ if (value === "pull_request" || value === "pr" || value === "github_pull_request") {
6759
+ return "pull_request";
6760
+ }
6761
+ return null;
6762
+ }
6763
+ function getGitHubPullRequestStateForLink(value) {
6764
+ return getPullRequestApiState(value) === "open" ? "open" : "closed";
6765
+ }
6766
+ async function assertPaperclipIssueHasNoManualGitHubLink(ctx, params) {
6767
+ const existingIssueLink = await resolvePaperclipIssueGitHubLink(ctx, params.issueId, params.companyId);
6768
+ if (existingIssueLink) {
6769
+ throw new Error("This Paperclip issue is already linked to a GitHub issue.");
6770
+ }
6771
+ const existingPullRequestLinks = await listGitHubPullRequestLinkRecords(ctx, {
6772
+ paperclipIssueId: params.issueId
6773
+ });
6774
+ const matchingPullRequestLink = existingPullRequestLinks.find(
6775
+ (record) => !record.data.companyId || record.data.companyId === params.companyId
6776
+ );
6777
+ if (matchingPullRequestLink) {
6778
+ throw new Error("This Paperclip issue is already linked to a GitHub pull request.");
6779
+ }
6780
+ }
6781
+ async function resolveIssueGitHubLinkMapping(ctx, params) {
6782
+ const issue = await ctx.issues.get(params.issueId, params.companyId);
6783
+ if (!issue) {
6784
+ throw new Error("Paperclip issue was not found.");
6785
+ }
6786
+ const projectId = typeof issue.projectId === "string" && issue.projectId.trim() ? issue.projectId.trim() : void 0;
6787
+ if (!projectId) {
6788
+ throw new Error("This Paperclip issue is not in a project that can be mapped to a GitHub repository.");
6789
+ }
6790
+ const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
6791
+ const mappings = await resolveProjectScopedMappings(ctx, settings.mappings, {
6792
+ companyId: params.companyId,
6793
+ projectId
6794
+ });
6795
+ if (mappings.length === 0) {
6796
+ throw new Error("This Paperclip issue project is not mapped to a GitHub repository.");
6797
+ }
6798
+ const requestedRepository = params.repositoryUrl ? parseRepositoryReference(params.repositoryUrl) : null;
6799
+ if (params.repositoryUrl && !requestedRepository) {
6800
+ throw new Error(`Invalid GitHub repository: ${params.repositoryUrl}. Use owner/repo or https://github.com/owner/repo.`);
6801
+ }
6802
+ const matchingMappings = requestedRepository ? mappings.filter(
6803
+ (mapping2) => areRepositoriesEqual(requireRepositoryReference(mapping2.repositoryUrl), requestedRepository)
6804
+ ) : mappings;
6805
+ if (matchingMappings.length === 0 && requestedRepository) {
6806
+ throw new Error("The current Paperclip issue project is not mapped to the selected GitHub repository.");
6807
+ }
6808
+ if (matchingMappings.length > 1 && !requestedRepository) {
6809
+ throw new Error("This Paperclip issue project has multiple GitHub repositories. Enter the full GitHub URL.");
6810
+ }
6811
+ const mapping = matchingMappings[0];
6812
+ if (!mapping) {
6813
+ throw new Error("This Paperclip issue project is not mapped to a GitHub repository.");
6814
+ }
6815
+ return {
6816
+ issue,
6817
+ projectId,
6818
+ mapping,
6819
+ repository: requestedRepository ?? requireRepositoryReference(mapping.repositoryUrl)
6820
+ };
6821
+ }
6822
+ function resolveGitHubIssueLinkReference(input) {
6823
+ const reference = normalizeOptionalString2(input.reference);
6824
+ if (reference) {
6825
+ const parsedIssueUrl = parseGitHubIssueHtmlUrl(reference);
6826
+ if (parsedIssueUrl) {
6827
+ return {
6828
+ repositoryUrl: parsedIssueUrl.repositoryUrl,
6829
+ issueNumber: parsedIssueUrl.issueNumber,
6830
+ issueUrl: parsedIssueUrl.issueUrl
6831
+ };
6832
+ }
6833
+ if (parseGitHubPullRequestHtmlUrl(reference)) {
6834
+ throw new Error("That reference is a GitHub pull request. Choose pull request instead.");
6835
+ }
6836
+ const referenceNumber = normalizePositiveIntegerReference(reference);
6837
+ if (referenceNumber) {
6838
+ return {
6839
+ repositoryUrl: input.repositoryUrl,
6840
+ issueNumber: referenceNumber
6841
+ };
6842
+ }
6843
+ }
6844
+ const explicitIssueNumber = normalizePositiveIntegerReference(input.issueNumber);
6845
+ if (explicitIssueNumber) {
6846
+ return {
6847
+ repositoryUrl: input.repositoryUrl,
6848
+ issueNumber: explicitIssueNumber
6849
+ };
6850
+ }
6851
+ throw new Error("Enter a GitHub issue number or full GitHub issue URL.");
6852
+ }
6853
+ function resolveGitHubPullRequestLinkReference(input) {
6854
+ const explicitPullRequestUrl = normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(input.pullRequestUrl));
6855
+ const parsedExplicitPullRequestUrl = explicitPullRequestUrl ? parseGitHubPullRequestHtmlUrl(explicitPullRequestUrl) : void 0;
6856
+ const reference = normalizeOptionalString2(input.reference);
6857
+ const parsedReferenceUrl = reference ? parseGitHubPullRequestHtmlUrl(reference) : void 0;
6858
+ if (reference && parseGitHubIssueHtmlUrl(reference)) {
6859
+ throw new Error("That reference is a GitHub issue. Choose issue instead.");
6860
+ }
6861
+ const parsedUrl = parsedReferenceUrl ?? parsedExplicitPullRequestUrl;
6862
+ const explicitPullRequestNumber = normalizePositiveIntegerReference(input.pullRequestNumber);
6863
+ const referenceNumber = reference && !parsedReferenceUrl ? normalizePositiveIntegerReference(reference) : void 0;
6864
+ const pullRequestNumber = parsedUrl?.pullRequestNumber ?? explicitPullRequestNumber ?? referenceNumber;
6865
+ const repositoryUrl = parsedUrl?.repositoryUrl ?? input.repositoryUrl;
6866
+ if (!pullRequestNumber) {
6867
+ throw new Error("Enter a GitHub pull request number or full GitHub pull request URL.");
6868
+ }
6869
+ if (parsedUrl && explicitPullRequestNumber && explicitPullRequestNumber !== parsedUrl.pullRequestNumber) {
6870
+ throw new Error("pullRequestNumber must match the supplied GitHub pull request URL.");
6871
+ }
6872
+ if (parsedUrl && input.repositoryUrl) {
6873
+ const requestedRepository = parseRepositoryReference(input.repositoryUrl);
6874
+ const urlRepository = parseRepositoryReference(parsedUrl.repositoryUrl);
6875
+ if (requestedRepository && urlRepository && !areRepositoriesEqual(requestedRepository, urlRepository)) {
6876
+ throw new Error("repository must match the supplied GitHub pull request URL.");
6877
+ }
6878
+ }
6879
+ return {
6880
+ repositoryUrl,
6881
+ pullRequestNumber,
6882
+ ...parsedUrl?.pullRequestUrl ? { pullRequestUrl: parsedUrl.pullRequestUrl } : {}
6883
+ };
6884
+ }
6885
+ async function linkPaperclipIssueToGitHubIssue(ctx, params) {
6886
+ const companyId = normalizeCompanyId(params.companyId);
6887
+ const issueId = normalizeOptionalString2(params.issueId);
6888
+ if (!companyId || !issueId) {
6889
+ throw new Error("companyId and issueId are required.");
6890
+ }
6891
+ if (params.requireUnlinked) {
6892
+ await assertPaperclipIssueHasNoManualGitHubLink(ctx, {
6893
+ companyId,
6894
+ issueId
6895
+ });
6896
+ }
6897
+ const reference = resolveGitHubIssueLinkReference({
6898
+ kind: "issue",
6899
+ reference: params.reference,
6900
+ repositoryUrl: params.repositoryUrl,
6901
+ issueNumber: params.issueNumber
6902
+ });
6903
+ const scope = await resolveIssueGitHubLinkMapping(ctx, {
6904
+ companyId,
6905
+ issueId,
6906
+ repositoryUrl: reference.repositoryUrl
6907
+ });
6908
+ const octokit = await createGitHubToolOctokit(ctx, companyId);
6909
+ const response = await octokit.rest.issues.get({
6910
+ owner: scope.repository.owner,
6911
+ repo: scope.repository.repo,
6912
+ issue_number: reference.issueNumber,
6913
+ headers: {
6914
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6915
+ }
6916
+ });
6917
+ const rawIssue = response.data;
6918
+ if (rawIssue.pull_request) {
6919
+ throw new Error("That GitHub number is a pull request. Choose pull request instead.");
6920
+ }
6921
+ const githubIssue = normalizeGitHubIssueRecord(rawIssue);
6922
+ const linkedPullRequests = await listLinkedPullRequestsForIssue(octokit, scope.repository, githubIssue.number);
6923
+ await upsertGitHubIssueLinkRecord(ctx, scope.mapping, issueId, githubIssue, linkedPullRequests);
6924
+ const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
6925
+ upsertImportedIssueRecord(
6926
+ importRegistry,
6927
+ buildImportedIssueRecord(scope.mapping, githubIssue, issueId, (/* @__PURE__ */ new Date()).toISOString())
6928
+ );
6929
+ await ctx.state.set(IMPORT_REGISTRY_SCOPE, importRegistry);
6930
+ invalidateProjectPullRequestCaches({
6931
+ companyId,
6932
+ projectId: scope.mapping.paperclipProjectId ?? scope.projectId,
6933
+ repository: scope.repository
6934
+ });
6935
+ return {
6936
+ kind: "issue",
6937
+ paperclipIssueId: issueId,
6938
+ repositoryUrl: scope.repository.url,
6939
+ githubIssueNumber: githubIssue.number,
6940
+ githubIssueUrl: normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl,
6941
+ linkedPullRequestNumbers: linkedPullRequests.map((pullRequest) => pullRequest.number)
6942
+ };
6943
+ }
6944
+ async function linkPaperclipIssueToGitHubPullRequest(ctx, params) {
6945
+ const companyId = normalizeCompanyId(params.companyId);
6946
+ const issueId = normalizeOptionalString2(params.issueId);
6947
+ if (!companyId || !issueId) {
6948
+ throw new Error("companyId and issueId are required.");
6949
+ }
6950
+ if (params.requireUnlinked) {
6951
+ await assertPaperclipIssueHasNoManualGitHubLink(ctx, {
6952
+ companyId,
6953
+ issueId
6954
+ });
6955
+ }
6956
+ const reference = resolveGitHubPullRequestLinkReference({
6957
+ reference: params.reference,
6958
+ repositoryUrl: params.repositoryUrl,
6959
+ pullRequestNumber: params.pullRequestNumber,
6960
+ pullRequestUrl: params.pullRequestUrl
6961
+ });
6962
+ const scope = await resolveIssueGitHubLinkMapping(ctx, {
6963
+ companyId,
6964
+ issueId,
6965
+ repositoryUrl: reference.repositoryUrl
6966
+ });
6967
+ const octokit = await createGitHubToolOctokit(ctx, companyId);
6968
+ const response = await octokit.rest.pulls.get({
6969
+ owner: scope.repository.owner,
6970
+ repo: scope.repository.repo,
6971
+ pull_number: reference.pullRequestNumber,
6972
+ headers: {
6973
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
6974
+ }
6975
+ });
6976
+ const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(response.data.html_url ?? reference.pullRequestUrl) ?? reference.pullRequestUrl ?? `${scope.repository.url}/pull/${reference.pullRequestNumber}`;
6977
+ const pullRequestState = getGitHubPullRequestStateForLink({
6978
+ state: response.data.state,
6979
+ merged: response.data.merged
6980
+ });
6981
+ await upsertGitHubPullRequestLinkRecord(ctx, {
6982
+ companyId,
6983
+ projectId: scope.mapping.paperclipProjectId ?? scope.projectId,
6984
+ issueId,
6985
+ repositoryUrl: scope.repository.url,
6986
+ pullRequestNumber: reference.pullRequestNumber,
6987
+ pullRequestUrl,
6988
+ pullRequestTitle: response.data.title || `Pull request #${reference.pullRequestNumber}`,
6989
+ pullRequestState
6990
+ });
6991
+ invalidateProjectPullRequestCaches({
6992
+ companyId,
6993
+ projectId: scope.mapping.paperclipProjectId ?? scope.projectId,
6994
+ repository: scope.repository
6995
+ });
6996
+ return {
6997
+ kind: "pull_request",
6998
+ paperclipIssueId: issueId,
6999
+ repositoryUrl: scope.repository.url,
7000
+ githubPullRequestNumber: reference.pullRequestNumber,
7001
+ githubPullRequestUrl: pullRequestUrl,
7002
+ githubPullRequestState: pullRequestState
7003
+ };
7004
+ }
6708
7005
  async function upsertStatusTransitionCommentAnnotation(ctx, params) {
6709
7006
  const { issueId, commentId, annotation } = params;
6710
7007
  await ctx.entities.upsert({
@@ -8565,6 +8862,20 @@ function normalizeOptionalToolString(value) {
8565
8862
  function normalizeToolPositiveInteger(value) {
8566
8863
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
8567
8864
  }
8865
+ function normalizePositiveIntegerReference(value) {
8866
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
8867
+ return value;
8868
+ }
8869
+ if (typeof value !== "string") {
8870
+ return void 0;
8871
+ }
8872
+ const match = value.trim().match(/^#?(\d+)$/);
8873
+ if (!match) {
8874
+ return void 0;
8875
+ }
8876
+ const parsed = Number(match[1]);
8877
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
8878
+ }
8568
8879
  function normalizeToolStringArray(value) {
8569
8880
  if (!Array.isArray(value)) {
8570
8881
  return [];
@@ -8722,12 +9033,34 @@ async function handleCompanyMetricApiRoute(ctx, input) {
8722
9033
  const pullRequestNumber = normalizeToolPositiveInteger(payload.pullRequestNumber);
8723
9034
  const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(payload.pullRequestUrl));
8724
9035
  const eventKey = normalizeOptionalString2(payload.eventKey);
9036
+ const paperclipIssueId = normalizeOptionalString2(payload.paperclipIssueId);
9037
+ let linkedPaperclipIssueId;
9038
+ let linkedRepository = null;
9039
+ let linkedPullRequestNumber;
9040
+ let linkedPullRequestUrl;
9041
+ if (paperclipIssueId) {
9042
+ const linkResult = await linkPaperclipIssueToGitHubPullRequest(ctx, {
9043
+ companyId,
9044
+ issueId: paperclipIssueId,
9045
+ repositoryUrl: repository?.url,
9046
+ pullRequestNumber,
9047
+ pullRequestUrl
9048
+ });
9049
+ linkedPaperclipIssueId = typeof linkResult.paperclipIssueId === "string" ? linkResult.paperclipIssueId : paperclipIssueId;
9050
+ const resultRepositoryUrl = normalizeOptionalString2(linkResult.repositoryUrl);
9051
+ linkedRepository = resultRepositoryUrl ? parseRepositoryReference(resultRepositoryUrl) : null;
9052
+ linkedPullRequestNumber = normalizeToolPositiveInteger(linkResult.githubPullRequestNumber);
9053
+ linkedPullRequestUrl = normalizeGitHubPullRequestHtmlUrl(normalizeOptionalString2(linkResult.githubPullRequestUrl));
9054
+ }
9055
+ const metricRepositoryUrl = repository?.url ?? linkedRepository?.url;
9056
+ const metricPullRequestNumber = pullRequestNumber ?? linkedPullRequestNumber;
9057
+ const metricPullRequestUrl = pullRequestUrl ?? linkedPullRequestUrl;
8725
9058
  const dedupeKey = buildCompanyMetricEventKey({
8726
9059
  metric,
8727
9060
  eventKey,
8728
- repositoryUrl: repository?.url,
8729
- pullRequestNumber,
8730
- pullRequestUrl
9061
+ repositoryUrl: metricRepositoryUrl,
9062
+ pullRequestNumber: metricPullRequestNumber,
9063
+ pullRequestUrl: metricPullRequestUrl
8731
9064
  });
8732
9065
  if (!dedupeKey) {
8733
9066
  throw new Error(
@@ -8742,9 +9075,9 @@ async function handleCompanyMetricApiRoute(ctx, input) {
8742
9075
  count: normalizeToolPositiveInteger(payload.count),
8743
9076
  occurredAt: normalizeOptionalString2(payload.occurredAt),
8744
9077
  eventKey,
8745
- repositoryUrl: repository?.url,
8746
- pullRequestNumber,
8747
- pullRequestUrl
9078
+ repositoryUrl: metricRepositoryUrl,
9079
+ pullRequestNumber: metricPullRequestNumber,
9080
+ pullRequestUrl: metricPullRequestUrl
8748
9081
  },
8749
9082
  {
8750
9083
  throwOnPersistFailure: true
@@ -8756,9 +9089,10 @@ async function handleCompanyMetricApiRoute(ctx, input) {
8756
9089
  routeKey: input.routeKey,
8757
9090
  companyId,
8758
9091
  metric,
8759
- repositoryUrl: repository?.url,
8760
- pullRequestNumber,
8761
- pullRequestUrl,
9092
+ repositoryUrl: metricRepositoryUrl,
9093
+ pullRequestNumber: metricPullRequestNumber,
9094
+ pullRequestUrl: metricPullRequestUrl,
9095
+ linkedPaperclipIssueId: linkedPaperclipIssueId ?? null,
8762
9096
  agentId: input.actor.agentId ?? null,
8763
9097
  runId: input.actor.runId ?? null
8764
9098
  }
@@ -8769,7 +9103,8 @@ async function handleCompanyMetricApiRoute(ctx, input) {
8769
9103
  status: recordedMetric.recorded ? "recorded" : "duplicate",
8770
9104
  recorded: recordedMetric.recorded,
8771
9105
  companyId,
8772
- metric: "pull_request_created"
9106
+ metric: "pull_request_created",
9107
+ ...linkedPaperclipIssueId ? { paperclipIssueId: linkedPaperclipIssueId } : {}
8773
9108
  }
8774
9109
  };
8775
9110
  }
@@ -12727,7 +13062,13 @@ function registerGitHubAgentTools(ctx) {
12727
13062
  getGitHubAgentToolDeclaration("create_pull_request"),
12728
13063
  async (params, runCtx) => executeGitHubTool(async () => {
12729
13064
  const input = getToolInputRecord(params);
12730
- const repository = await resolveGitHubToolRepository(ctx, runCtx, input);
13065
+ const paperclipIssueId = normalizeOptionalToolString(input.paperclipIssueId);
13066
+ const explicitRepository = normalizeOptionalToolString(input.repository);
13067
+ const issueLinkScope = paperclipIssueId && !explicitRepository ? await resolveIssueGitHubLinkMapping(ctx, {
13068
+ companyId: runCtx.companyId,
13069
+ issueId: paperclipIssueId
13070
+ }) : null;
13071
+ const repository = issueLinkScope?.repository ?? await resolveGitHubToolRepository(ctx, runCtx, input);
12731
13072
  const head = normalizeOptionalToolString(input.head);
12732
13073
  const base = normalizeOptionalToolString(input.base);
12733
13074
  const title = normalizeOptionalToolString(input.title);
@@ -12752,6 +13093,32 @@ function registerGitHubAgentTools(ctx) {
12752
13093
  "X-GitHub-Api-Version": GITHUB_API_VERSION
12753
13094
  }
12754
13095
  });
13096
+ if (paperclipIssueId) {
13097
+ const linkScope = issueLinkScope ?? await resolveIssueGitHubLinkMapping(ctx, {
13098
+ companyId: runCtx.companyId,
13099
+ issueId: paperclipIssueId,
13100
+ repositoryUrl: repository.url
13101
+ });
13102
+ const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(response.data.html_url) ?? `${repository.url}/pull/${response.data.number}`;
13103
+ await upsertGitHubPullRequestLinkRecord(ctx, {
13104
+ companyId: runCtx.companyId,
13105
+ projectId: linkScope.mapping.paperclipProjectId ?? linkScope.projectId,
13106
+ issueId: paperclipIssueId,
13107
+ repositoryUrl: repository.url,
13108
+ pullRequestNumber: response.data.number,
13109
+ pullRequestUrl,
13110
+ pullRequestTitle: response.data.title || title,
13111
+ pullRequestState: getGitHubPullRequestStateForLink({
13112
+ state: response.data.state,
13113
+ merged: false
13114
+ })
13115
+ });
13116
+ invalidateProjectPullRequestCaches({
13117
+ companyId: runCtx.companyId,
13118
+ projectId: linkScope.mapping.paperclipProjectId ?? linkScope.projectId,
13119
+ repository
13120
+ });
13121
+ }
12755
13122
  await persistCompanyActivityMetricEvent(
12756
13123
  ctx,
12757
13124
  {
@@ -13346,6 +13713,34 @@ var plugin = definePlugin({
13346
13713
  const record = input && typeof input === "object" ? input : {};
13347
13714
  return buildCommentAnnotationData(ctx, record);
13348
13715
  });
13716
+ ctx.actions.register("issue.linkGitHubItem", async (input) => {
13717
+ const record = input && typeof input === "object" ? input : {};
13718
+ const kind = normalizeIssueGitHubLinkKind(record.kind);
13719
+ if (!kind) {
13720
+ throw new Error('kind must be "issue" or "pull_request".');
13721
+ }
13722
+ const companyId = normalizeCompanyId(record.companyId);
13723
+ const issueId = normalizeOptionalString2(record.issueId);
13724
+ if (!companyId || !issueId) {
13725
+ throw new Error("companyId and issueId are required.");
13726
+ }
13727
+ return kind === "issue" ? linkPaperclipIssueToGitHubIssue(ctx, {
13728
+ companyId,
13729
+ issueId,
13730
+ reference: record.reference,
13731
+ repositoryUrl: normalizeOptionalString2(record.repository),
13732
+ issueNumber: record.issueNumber,
13733
+ requireUnlinked: true
13734
+ }) : linkPaperclipIssueToGitHubPullRequest(ctx, {
13735
+ companyId,
13736
+ issueId,
13737
+ reference: record.reference,
13738
+ repositoryUrl: normalizeOptionalString2(record.repository),
13739
+ pullRequestNumber: record.pullRequestNumber,
13740
+ pullRequestUrl: record.pullRequestUrl,
13741
+ requireUnlinked: true
13742
+ });
13743
+ });
13349
13744
  ctx.actions.register("settings.saveRegistration", async (input) => {
13350
13745
  const previous = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
13351
13746
  const config = await getResolvedConfig(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",