paperclip-github-plugin 0.3.3 → 0.3.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/README.md CHANGED
@@ -118,6 +118,8 @@ When a token is saved, the settings page audits the mapped repositories for the
118
118
 
119
119
  Imported issues keep the original GitHub title and use the normalized GitHub body as the Paperclip description. The worker also normalizes GitHub HTML that Paperclip descriptions do not render cleanly, including elements such as `<br>`, `<hr>`, `<details>`, `<summary>`, and inline images.
120
120
 
121
+ To keep imported issues recognizable without cluttering the visible description, the plugin appends a hidden HTML comment footer with the source GitHub issue URL. Agents and repair flows use that marker when the plugin-owned link entity or import registry is missing.
122
+
121
123
  Repeated syncs keep existing imports current instead of creating duplicates again. If the plugin's import registry is stale, the worker can repair deduplication by reusing existing Paperclip issues when durable GitHub link metadata is already present.
122
124
 
123
125
  When the local Paperclip API is available, the plugin also syncs labels by name, prefers exact color matches when multiple Paperclip labels share the same name, and creates missing Paperclip labels when needed.
@@ -136,6 +138,7 @@ When the local Paperclip API is available, the plugin also syncs labels by name,
136
138
  Additional behavior:
137
139
 
138
140
  - Open imported issues that are already in `backlog` stay in `backlog` until someone changes them in Paperclip.
141
+ - If an imported issue is `done` or `cancelled` and GitHub shows it open again with no linked pull request, sync moves it to `todo` so agents can pick it up again.
139
142
  - Trusted new GitHub comments from the original issue author or a verified maintainer/admin can move an open imported issue back to `todo`.
140
143
  - When the sync changes a Paperclip issue status, it adds a Paperclip comment explaining what changed and why.
141
144
 
package/dist/manifest.js CHANGED
@@ -435,7 +435,7 @@ var require2 = createRequire(import.meta.url);
435
435
  var packageJson = require2("../package.json");
436
436
  var DASHBOARD_WIDGET_CAPABILITY = "ui.dashboardWidget.register";
437
437
  var SCHEDULE_TICK_CRON = "* * * * *";
438
- var MANIFEST_VERSION = "0.3.3"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
438
+ var MANIFEST_VERSION = "0.3.4"?.trim() || typeof packageJson.version === "string" && packageJson.version.trim() || process.env.npm_package_version?.trim() || "0.0.0-dev";
439
439
  var manifest = {
440
440
  id: "paperclip-github-plugin",
441
441
  apiVersion: 1,
package/dist/worker.js CHANGED
@@ -560,6 +560,8 @@ var ISSUE_LINK_ENTITY_TYPE = "paperclip-github-plugin.issue-link";
560
560
  var PULL_REQUEST_LINK_ENTITY_TYPE = "paperclip-github-plugin.pull-request-link";
561
561
  var COMMENT_ANNOTATION_ENTITY_TYPE = "paperclip-github-plugin.comment-annotation";
562
562
  var AI_AUTHORED_COMMENT_FOOTER_PREFIX = "Created by a Paperclip AI agent using ";
563
+ var HIDDEN_GITHUB_IMPORT_MARKER_PREFIX = "<!-- paperclip-github-plugin-imported-from: ";
564
+ var HIDDEN_GITHUB_IMPORT_MARKER_SUFFIX = " -->";
563
565
  function normalizeCompanyId(value) {
564
566
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
565
567
  }
@@ -1990,8 +1992,8 @@ function doesImportedIssueMatchTarget(issue, target) {
1990
1992
  }
1991
1993
  return target.issueId !== void 0 && issue.paperclipIssueId === target.issueId || target.githubIssueId !== void 0 && issue.githubIssueId === target.githubIssueId || target.githubIssueNumber !== void 0 && issue.githubIssueNumber === target.githubIssueNumber;
1992
1994
  }
1993
- async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
1994
- const linkRecords = await listGitHubIssueLinkRecords(ctx, {
1995
+ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options = {}) {
1996
+ const linkRecords = options.linkRecords ?? await listGitHubIssueLinkRecords(ctx, {
1995
1997
  paperclipIssueId: issueId
1996
1998
  });
1997
1999
  const entityMatch = linkRecords.find((record) => !record.data.companyId || record.data.companyId === companyId);
@@ -2004,7 +2006,8 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
2004
2006
  githubIssueId: entityMatch.data.githubIssueId,
2005
2007
  githubIssueNumber: entityMatch.data.githubIssueNumber,
2006
2008
  githubIssueUrl: entityMatch.data.githubIssueUrl,
2007
- linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers
2009
+ linkedPullRequestNumbers: entityMatch.data.linkedPullRequestNumbers,
2010
+ entityRecord: entityMatch
2008
2011
  };
2009
2012
  }
2010
2013
  const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
@@ -2017,7 +2020,7 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
2017
2020
  registryMatch.githubIssueNumber
2018
2021
  );
2019
2022
  if (githubIssueUrl2) {
2020
- return {
2023
+ const fallbackLink2 = {
2021
2024
  source: "import_registry",
2022
2025
  companyId: registryMatch.companyId,
2023
2026
  paperclipProjectId: registryMatch.paperclipProjectId,
@@ -2027,15 +2030,16 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
2027
2030
  githubIssueUrl: githubIssueUrl2,
2028
2031
  linkedPullRequestNumbers: []
2029
2032
  };
2033
+ return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink2) ?? fallbackLink2;
2030
2034
  }
2031
2035
  }
2032
- const issue = await ctx.issues.get(issueId, companyId);
2036
+ const issue = options.paperclipIssue ?? await ctx.issues.get(issueId, companyId);
2033
2037
  const githubIssueUrl = extractImportedGitHubIssueUrlFromDescription(issue?.description);
2034
2038
  const githubIssueReference = githubIssueUrl ? parseGitHubIssueHtmlUrl(githubIssueUrl) : null;
2035
2039
  if (!githubIssueReference) {
2036
2040
  return null;
2037
2041
  }
2038
- return {
2042
+ const fallbackLink = {
2039
2043
  source: "description",
2040
2044
  companyId,
2041
2045
  paperclipProjectId: issue?.projectId ?? void 0,
@@ -2044,6 +2048,73 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId) {
2044
2048
  githubIssueUrl: githubIssueReference.issueUrl,
2045
2049
  linkedPullRequestNumbers: []
2046
2050
  };
2051
+ return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) ?? fallbackLink;
2052
+ }
2053
+ async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) {
2054
+ const repository = parseRepositoryReference(fallbackLink.repositoryUrl);
2055
+ if (!repository) {
2056
+ return null;
2057
+ }
2058
+ let octokit;
2059
+ try {
2060
+ octokit = await createGitHubToolOctokit(ctx);
2061
+ } catch {
2062
+ return null;
2063
+ }
2064
+ try {
2065
+ const response = await octokit.rest.issues.get({
2066
+ owner: repository.owner,
2067
+ repo: repository.repo,
2068
+ issue_number: fallbackLink.githubIssueNumber,
2069
+ headers: {
2070
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
2071
+ }
2072
+ });
2073
+ const githubIssue = normalizeGitHubIssueRecord(response.data);
2074
+ const linkedPullRequests = await listLinkedPullRequestsForIssue(octokit, repository, githubIssue.number);
2075
+ const linkedPullRequestNumbers = linkedPullRequests.map((pullRequest) => pullRequest.number);
2076
+ const entityRecord = buildGitHubIssueLinkRecord(
2077
+ {
2078
+ companyId: fallbackLink.companyId,
2079
+ paperclipProjectId: fallbackLink.paperclipProjectId,
2080
+ repositoryUrl: fallbackLink.repositoryUrl
2081
+ },
2082
+ issueId,
2083
+ githubIssue,
2084
+ linkedPullRequestNumbers
2085
+ );
2086
+ await upsertGitHubIssueLinkRecord(
2087
+ ctx,
2088
+ {
2089
+ companyId: fallbackLink.companyId,
2090
+ paperclipProjectId: fallbackLink.paperclipProjectId,
2091
+ repositoryUrl: fallbackLink.repositoryUrl
2092
+ },
2093
+ issueId,
2094
+ githubIssue,
2095
+ linkedPullRequestNumbers
2096
+ );
2097
+ return {
2098
+ source: "entity",
2099
+ companyId: fallbackLink.companyId,
2100
+ paperclipProjectId: fallbackLink.paperclipProjectId,
2101
+ repositoryUrl: fallbackLink.repositoryUrl,
2102
+ githubIssueId: githubIssue.id,
2103
+ githubIssueNumber: githubIssue.number,
2104
+ githubIssueUrl: normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl,
2105
+ linkedPullRequestNumbers,
2106
+ entityRecord
2107
+ };
2108
+ } catch (error) {
2109
+ ctx.logger.warn("Unable to hydrate recovered GitHub issue metadata for a Paperclip issue fallback link.", {
2110
+ issueId,
2111
+ companyId: fallbackLink.companyId,
2112
+ repositoryUrl: fallbackLink.repositoryUrl,
2113
+ githubIssueNumber: fallbackLink.githubIssueNumber,
2114
+ error: getErrorMessage(error)
2115
+ });
2116
+ return null;
2117
+ }
2047
2118
  }
2048
2119
  async function resolveManualSyncTarget(ctx, settings, input) {
2049
2120
  if (input.issueId) {
@@ -2232,7 +2303,13 @@ async function buildIssueGitHubDetails(ctx, input) {
2232
2303
  const linkRecords = await listGitHubIssueLinkRecords(ctx, {
2233
2304
  paperclipIssueId: issueId
2234
2305
  });
2235
- const entityMatch = linkRecords.find((record) => !record.data.companyId || record.data.companyId === companyId);
2306
+ const link = await resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, {
2307
+ linkRecords
2308
+ });
2309
+ if (!link) {
2310
+ return null;
2311
+ }
2312
+ const entityMatch = link.entityRecord;
2236
2313
  if (entityMatch) {
2237
2314
  return {
2238
2315
  paperclipIssueId: issueId,
@@ -2248,17 +2325,13 @@ async function buildIssueGitHubDetails(ctx, input) {
2248
2325
  syncedAt: entityMatch.data.syncedAt
2249
2326
  };
2250
2327
  }
2251
- const fallbackLink = await resolvePaperclipIssueGitHubLink(ctx, issueId, companyId);
2252
- if (!fallbackLink) {
2253
- return null;
2254
- }
2255
2328
  return {
2256
2329
  paperclipIssueId: issueId,
2257
- source: fallbackLink.source,
2258
- githubIssueNumber: fallbackLink.githubIssueNumber,
2259
- githubIssueUrl: fallbackLink.githubIssueUrl,
2260
- repositoryUrl: fallbackLink.repositoryUrl,
2261
- linkedPullRequestNumbers: fallbackLink.linkedPullRequestNumbers
2330
+ source: link.source,
2331
+ githubIssueNumber: link.githubIssueNumber,
2332
+ githubIssueUrl: link.githubIssueUrl,
2333
+ repositoryUrl: link.repositoryUrl,
2334
+ linkedPullRequestNumbers: link.linkedPullRequestNumbers
2262
2335
  };
2263
2336
  }
2264
2337
  async function resolveIssueByIdentifier(ctx, input) {
@@ -3639,7 +3712,7 @@ function resolvePaperclipIssueStatus(params) {
3639
3712
  return defaultImportedStatus;
3640
3713
  }
3641
3714
  if (currentStatus === "done" || currentStatus === "cancelled") {
3642
- return "backlog";
3715
+ return "todo";
3643
3716
  }
3644
3717
  return currentStatus;
3645
3718
  }
@@ -4068,6 +4141,15 @@ function parseGitHubIssueHtmlUrl(value) {
4068
4141
  function normalizeGitHubIssueHtmlUrl(value) {
4069
4142
  return parseGitHubIssueHtmlUrl(value)?.issueUrl;
4070
4143
  }
4144
+ function escapeRegExp(value) {
4145
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4146
+ }
4147
+ function getHiddenGitHubImportMarkerPattern() {
4148
+ return new RegExp(
4149
+ `${escapeRegExp(HIDDEN_GITHUB_IMPORT_MARKER_PREFIX)}(\\S+?)${escapeRegExp(HIDDEN_GITHUB_IMPORT_MARKER_SUFFIX)}`,
4150
+ "i"
4151
+ );
4152
+ }
4071
4153
  function normalizeLinkedPullRequestNumbers(values) {
4072
4154
  return [...new Set(
4073
4155
  values.filter((pullRequestNumber) => Number.isInteger(pullRequestNumber) && pullRequestNumber > 0)
@@ -4077,6 +4159,10 @@ function extractImportedGitHubIssueUrlFromDescription(description) {
4077
4159
  if (typeof description !== "string") {
4078
4160
  return void 0;
4079
4161
  }
4162
+ const hiddenMarkerMatch = description.match(getHiddenGitHubImportMarkerPattern());
4163
+ if (hiddenMarkerMatch) {
4164
+ return normalizeGitHubIssueHtmlUrl(hiddenMarkerMatch[1]);
4165
+ }
4080
4166
  const markdownMetadataMatch = description.match(/^\*\s+GitHub issue:\s+\[[^\]]+\]\(([^)]+)\)/m);
4081
4167
  if (markdownMetadataMatch) {
4082
4168
  return normalizeGitHubIssueHtmlUrl(markdownMetadataMatch[1]);
@@ -4219,8 +4305,27 @@ ${markdownImage}
4219
4305
  }
4220
4306
  function buildPaperclipIssueDescription(issue, linkedPullRequestNumbers = []) {
4221
4307
  const normalizedBody = normalizeGitHubIssueBodyForPaperclip(issue.body);
4308
+ const hiddenImportMarker = buildHiddenGitHubImportMarker(issue.htmlUrl);
4222
4309
  void linkedPullRequestNumbers;
4223
- return normalizedBody ?? "";
4310
+ if (!hiddenImportMarker) {
4311
+ return normalizedBody ?? "";
4312
+ }
4313
+ if (!normalizedBody) {
4314
+ return hiddenImportMarker;
4315
+ }
4316
+ return `${normalizedBody}
4317
+
4318
+ ${hiddenImportMarker}`;
4319
+ }
4320
+ function buildHiddenGitHubImportMarker(githubIssueUrl) {
4321
+ if (typeof githubIssueUrl !== "string") {
4322
+ return void 0;
4323
+ }
4324
+ const normalizedIssueUrl = normalizeGitHubIssueHtmlUrl(githubIssueUrl);
4325
+ if (!normalizedIssueUrl) {
4326
+ return void 0;
4327
+ }
4328
+ return `${HIDDEN_GITHUB_IMPORT_MARKER_PREFIX}${normalizedIssueUrl}${HIDDEN_GITHUB_IMPORT_MARKER_SUFFIX}`;
4224
4329
  }
4225
4330
  function normalizeIssueDescriptionValue(value) {
4226
4331
  return typeof value === "string" ? value : "";
@@ -4467,19 +4572,17 @@ async function findStoredStatusTransitionCommentAnnotation(ctx, params) {
4467
4572
  }
4468
4573
  return null;
4469
4574
  }
4470
- async function upsertGitHubIssueLinkRecord(ctx, mapping, issueId, githubIssue, linkedPullRequestNumbers) {
4575
+ function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers) {
4471
4576
  const githubIssueUrl = normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl;
4472
- await ctx.entities.upsert({
4473
- entityType: ISSUE_LINK_ENTITY_TYPE,
4474
- scopeKind: "issue",
4475
- scopeId: issueId,
4476
- externalId: githubIssueUrl,
4577
+ const repositoryUrl = parseRepositoryReference(target.repositoryUrl)?.url ?? target.repositoryUrl.trim();
4578
+ return {
4579
+ paperclipIssueId: issueId,
4477
4580
  title: `GitHub issue #${githubIssue.number}`,
4478
4581
  status: githubIssue.state,
4479
4582
  data: {
4480
- ...mapping.companyId ? { companyId: mapping.companyId } : {},
4481
- ...mapping.paperclipProjectId ? { paperclipProjectId: mapping.paperclipProjectId } : {},
4482
- repositoryUrl: getNormalizedMappingRepositoryUrl(mapping),
4583
+ ...target.companyId ? { companyId: target.companyId } : {},
4584
+ ...target.paperclipProjectId ? { paperclipProjectId: target.paperclipProjectId } : {},
4585
+ repositoryUrl,
4483
4586
  githubIssueId: githubIssue.id,
4484
4587
  githubIssueNumber: githubIssue.number,
4485
4588
  githubIssueUrl,
@@ -4490,6 +4593,18 @@ async function upsertGitHubIssueLinkRecord(ctx, mapping, issueId, githubIssue, l
4490
4593
  labels: githubIssue.labels,
4491
4594
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
4492
4595
  }
4596
+ };
4597
+ }
4598
+ async function upsertGitHubIssueLinkRecord(ctx, target, issueId, githubIssue, linkedPullRequestNumbers) {
4599
+ const record = buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers);
4600
+ await ctx.entities.upsert({
4601
+ entityType: ISSUE_LINK_ENTITY_TYPE,
4602
+ scopeKind: "issue",
4603
+ scopeId: issueId,
4604
+ externalId: record.data.githubIssueUrl,
4605
+ ...record.title ? { title: record.title } : {},
4606
+ ...record.status ? { status: record.status } : {},
4607
+ data: record.data
4493
4608
  });
4494
4609
  }
4495
4610
  async function upsertGitHubPullRequestLinkRecord(ctx, params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@octokit/rest": "^22.0.1",
44
- "@paperclipai/plugin-sdk": "^2026.403.0",
44
+ "@paperclipai/plugin-sdk": "^2026.416.0",
45
45
  "react": "^19.2.5",
46
46
  "react-markdown": "^10.1.0",
47
47
  "rehype-raw": "^7.0.0",