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 +3 -0
- package/dist/manifest.js +1 -1
- package/dist/worker.js +142 -27
- package/package.json +2 -2
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
2258
|
-
githubIssueNumber:
|
|
2259
|
-
githubIssueUrl:
|
|
2260
|
-
repositoryUrl:
|
|
2261
|
-
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 "
|
|
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
|
-
|
|
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
|
-
|
|
4575
|
+
function buildGitHubIssueLinkRecord(target, issueId, githubIssue, linkedPullRequestNumbers) {
|
|
4471
4576
|
const githubIssueUrl = normalizeGitHubIssueHtmlUrl(githubIssue.htmlUrl) ?? githubIssue.htmlUrl;
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
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
|
-
...
|
|
4481
|
-
...
|
|
4482
|
-
repositoryUrl
|
|
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
|
+
"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.
|
|
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",
|