paperclip-github-plugin 0.8.8 → 0.8.10
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 +38 -3
- package/dist/manifest.js +63 -1
- package/dist/ui/index.js +73 -12
- package/dist/ui/index.js.map +3 -3
- package/dist/worker.js +390 -26
- package/package.json +2 -2
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/worker.ts
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
2
3
|
import { realpathSync } from "node:fs";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
@@ -532,6 +533,58 @@ var GITHUB_AGENT_TOOLS = [
|
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
535
|
},
|
|
536
|
+
{
|
|
537
|
+
name: "upload_pull_request_asset",
|
|
538
|
+
displayName: "Upload Pull Request Asset",
|
|
539
|
+
description: "Upload a PR-visible asset such as an image, PDF, log, archive, or report to a non-merge artifact branch and return durable markdown that can be embedded in the PR body.",
|
|
540
|
+
parametersSchema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
additionalProperties: false,
|
|
543
|
+
required: ["fileName"],
|
|
544
|
+
allOf: [pullRequestTargetSchema],
|
|
545
|
+
anyOf: [
|
|
546
|
+
{ required: ["contentBase64"] },
|
|
547
|
+
{ required: ["dataUrl"] }
|
|
548
|
+
],
|
|
549
|
+
properties: {
|
|
550
|
+
repository: repositoryProperty,
|
|
551
|
+
pullRequestNumber: pullRequestNumberProperty,
|
|
552
|
+
paperclipIssueId: paperclipIssueIdProperty,
|
|
553
|
+
fileName: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Asset filename. The plugin sanitizes it and preserves a safe extension."
|
|
556
|
+
},
|
|
557
|
+
label: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Human-readable link text for the returned Markdown. Defaults to the sanitized filename."
|
|
560
|
+
},
|
|
561
|
+
alt: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Backward-compatible alias for label, useful as image alt text."
|
|
564
|
+
},
|
|
565
|
+
caption: {
|
|
566
|
+
type: "string",
|
|
567
|
+
description: "Optional human-facing caption returned with the uploaded asset metadata."
|
|
568
|
+
},
|
|
569
|
+
contentBase64: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "Base64-encoded asset bytes. Assets are limited to 10 MiB."
|
|
572
|
+
},
|
|
573
|
+
dataUrl: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description: "Alternative base64 data URL input such as data:application/pdf;base64,... or data:image/png;base64,... ."
|
|
576
|
+
},
|
|
577
|
+
mimeType: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Optional MIME type such as application/pdf or image/png. If omitted, the plugin infers common types from fileName and otherwise uses application/octet-stream."
|
|
580
|
+
},
|
|
581
|
+
artifactBranch: {
|
|
582
|
+
type: "string",
|
|
583
|
+
description: "Optional artifact branch name. Defaults to paperclip-artifacts-pr-<pullRequestNumber>."
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
535
588
|
{
|
|
536
589
|
name: "link_github_item",
|
|
537
590
|
displayName: "Link GitHub Item",
|
|
@@ -637,6 +690,9 @@ var COMPANY_METRIC_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/a
|
|
|
637
690
|
var ISSUE_LINK_API_ROUTE_KEY = "link-github-item";
|
|
638
691
|
var ISSUE_LINK_API_ROUTE_PATH = "/issue-link";
|
|
639
692
|
var ISSUE_LINK_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${ISSUE_LINK_API_ROUTE_PATH}`;
|
|
693
|
+
var PULL_REQUEST_ASSET_API_ROUTE_KEY = "upload-pull-request-asset";
|
|
694
|
+
var PULL_REQUEST_ASSET_API_ROUTE_PATH = "/pull-request-assets";
|
|
695
|
+
var PULL_REQUEST_ASSET_API_ROUTE_URL_PATH = `/api/plugins/${GITHUB_SYNC_PLUGIN_ID}/api${PULL_REQUEST_ASSET_API_ROUTE_PATH}`;
|
|
640
696
|
|
|
641
697
|
// src/paperclip-health.ts
|
|
642
698
|
function normalizeOptionalString(value) {
|
|
@@ -739,6 +795,40 @@ var AI_AUTHORED_MARKDOWN_FOOTER_PATTERN = /\n\n---\n###### ✨ This (?:comment|i
|
|
|
739
795
|
var HIDDEN_GITHUB_IMPORT_MARKER_PREFIX = "<!-- paperclip-github-plugin-imported-from: ";
|
|
740
796
|
var HIDDEN_GITHUB_IMPORT_MARKER_SUFFIX = " -->";
|
|
741
797
|
var EMPTY_GITHUB_ISSUE_DESCRIPTION_PLACEHOLDER = "_No description provided on GitHub._";
|
|
798
|
+
var MAX_PULL_REQUEST_ASSET_BYTES = 10 * 1024 * 1024;
|
|
799
|
+
var DEFAULT_PULL_REQUEST_ASSET_MIME_TYPE = "application/octet-stream";
|
|
800
|
+
var PULL_REQUEST_ASSET_MIME_TYPE_BY_EXTENSION = {
|
|
801
|
+
png: "image/png",
|
|
802
|
+
jpg: "image/jpeg",
|
|
803
|
+
jpeg: "image/jpeg",
|
|
804
|
+
webp: "image/webp",
|
|
805
|
+
gif: "image/gif",
|
|
806
|
+
pdf: "application/pdf",
|
|
807
|
+
txt: "text/plain",
|
|
808
|
+
md: "text/markdown",
|
|
809
|
+
markdown: "text/markdown",
|
|
810
|
+
json: "application/json",
|
|
811
|
+
csv: "text/csv",
|
|
812
|
+
xml: "application/xml",
|
|
813
|
+
zip: "application/zip",
|
|
814
|
+
gz: "application/gzip",
|
|
815
|
+
tgz: "application/gzip"
|
|
816
|
+
};
|
|
817
|
+
var PULL_REQUEST_ASSET_EXTENSION_BY_MIME_TYPE = {
|
|
818
|
+
"image/png": "png",
|
|
819
|
+
"image/jpeg": "jpg",
|
|
820
|
+
"image/webp": "webp",
|
|
821
|
+
"image/gif": "gif",
|
|
822
|
+
"application/pdf": "pdf",
|
|
823
|
+
"text/plain": "txt",
|
|
824
|
+
"text/markdown": "md",
|
|
825
|
+
"application/json": "json",
|
|
826
|
+
"text/csv": "csv",
|
|
827
|
+
"application/xml": "xml",
|
|
828
|
+
"application/zip": "zip",
|
|
829
|
+
"application/gzip": "gz",
|
|
830
|
+
"application/octet-stream": "bin"
|
|
831
|
+
};
|
|
742
832
|
var pluginRuntimeContext = null;
|
|
743
833
|
function normalizeCompanyId(value) {
|
|
744
834
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -782,15 +872,15 @@ var PaperclipLabelSyncError = class extends Error {
|
|
|
782
872
|
const location = params.paperclipApiBaseUrl ? ` at ${params.paperclipApiBaseUrl}` : "";
|
|
783
873
|
let message;
|
|
784
874
|
if (failure?.requiresAuthentication) {
|
|
785
|
-
message = `Could not map ${labelSubject} because the worker reached an authenticated Paperclip API response${location} instead of JSON. Connect Paperclip board access in plugin settings, set
|
|
875
|
+
message = `Could not map ${labelSubject} because the worker reached an authenticated Paperclip API response${location} instead of JSON. Connect Paperclip board access in plugin settings, set Worker Paperclip API URL to a worker-accessible Paperclip API origin, or expose the local Paperclip API to the worker without browser-session auth.`;
|
|
786
876
|
} else if (failure?.status === 404 || failure?.status === 405) {
|
|
787
|
-
message = `Could not map ${labelSubject} because the Paperclip label API${location} is not available to the worker. Set
|
|
877
|
+
message = `Could not map ${labelSubject} because the Paperclip label API${location} is not available to the worker. Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.`;
|
|
788
878
|
} else if (failure?.errorMessage) {
|
|
789
879
|
message = `Could not map ${labelSubject} because the Paperclip label API${location} failed: ${failure.errorMessage}`;
|
|
790
880
|
} else if (params.paperclipApiBaseUrl) {
|
|
791
881
|
message = `Could not map ${labelSubject} because the Paperclip label API at ${params.paperclipApiBaseUrl} is unavailable to the worker.`;
|
|
792
882
|
} else {
|
|
793
|
-
message = `Could not map ${labelSubject} because no worker-accessible Paperclip label API is configured. Set
|
|
883
|
+
message = `Could not map ${labelSubject} because no worker-accessible Paperclip label API is configured. Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.`;
|
|
794
884
|
}
|
|
795
885
|
super(message);
|
|
796
886
|
this.status = failure?.status;
|
|
@@ -1514,6 +1604,39 @@ function getErrorMessage(error) {
|
|
|
1514
1604
|
}
|
|
1515
1605
|
return String(error);
|
|
1516
1606
|
}
|
|
1607
|
+
function getErrorCause(error) {
|
|
1608
|
+
if (!error || typeof error !== "object" || !("cause" in error)) {
|
|
1609
|
+
return void 0;
|
|
1610
|
+
}
|
|
1611
|
+
return error.cause;
|
|
1612
|
+
}
|
|
1613
|
+
function getErrorCode(error) {
|
|
1614
|
+
if (!error || typeof error !== "object" || !("code" in error)) {
|
|
1615
|
+
return void 0;
|
|
1616
|
+
}
|
|
1617
|
+
const code = error.code;
|
|
1618
|
+
return typeof code === "string" && code.trim() ? code.trim() : void 0;
|
|
1619
|
+
}
|
|
1620
|
+
function getErrorDiagnosticMessage(error) {
|
|
1621
|
+
const primaryMessage = getErrorMessage(error).trim();
|
|
1622
|
+
const cause = getErrorCause(error);
|
|
1623
|
+
const causeMessage = cause ? getErrorMessage(cause).trim() : "";
|
|
1624
|
+
const errorCode = getErrorCode(error);
|
|
1625
|
+
const causeCode = cause ? getErrorCode(cause) : void 0;
|
|
1626
|
+
const code = errorCode ?? causeCode;
|
|
1627
|
+
const parts = [primaryMessage || String(error)];
|
|
1628
|
+
if (causeMessage && causeMessage !== primaryMessage) {
|
|
1629
|
+
parts.push(`cause: ${causeMessage}`);
|
|
1630
|
+
}
|
|
1631
|
+
if (code) {
|
|
1632
|
+
parts.push(`code: ${code}`);
|
|
1633
|
+
}
|
|
1634
|
+
return parts.join(" | ");
|
|
1635
|
+
}
|
|
1636
|
+
function formatPaperclipApiFetchErrorMessage(error, url, init) {
|
|
1637
|
+
const method = typeof init?.method === "string" && init.method.trim() ? init.method.trim().toUpperCase() : "GET";
|
|
1638
|
+
return `Paperclip API fetch failed (${method} ${url}): ${getErrorDiagnosticMessage(error)}`;
|
|
1639
|
+
}
|
|
1517
1640
|
function isPaperclipLabelSyncError(error) {
|
|
1518
1641
|
return error instanceof PaperclipLabelSyncError;
|
|
1519
1642
|
}
|
|
@@ -2135,12 +2258,12 @@ function getSyncFailureSuggestedAction(error, context) {
|
|
|
2135
2258
|
}
|
|
2136
2259
|
if (isPaperclipLabelSyncError(error)) {
|
|
2137
2260
|
if (error.requiresAuthentication || error.status === 401 || error.status === 403) {
|
|
2138
|
-
return "The worker could not reuse the board login session for the Paperclip label API. Connect Paperclip board access in settings, or set
|
|
2261
|
+
return "The worker could not reuse the board login session for the Paperclip label API. Connect Paperclip board access in settings, or set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.";
|
|
2139
2262
|
}
|
|
2140
2263
|
if (error.paperclipApiBaseUrl) {
|
|
2141
2264
|
return `Confirm that the Paperclip label API at ${error.paperclipApiBaseUrl} is reachable from the plugin worker and returns JSON, then retry sync.`;
|
|
2142
2265
|
}
|
|
2143
|
-
return "Set
|
|
2266
|
+
return "Set Worker Paperclip API URL to a worker-accessible Paperclip API origin, then retry sync.";
|
|
2144
2267
|
}
|
|
2145
2268
|
const rawMessage = getErrorMessage(error).trim().toLowerCase();
|
|
2146
2269
|
if (rawMessage.includes("could not resolve to a pullrequest")) {
|
|
@@ -3955,17 +4078,7 @@ function normalizePaperclipApiBaseUrlByCompanyId(value) {
|
|
|
3955
4078
|
}).filter((entry) => entry !== null);
|
|
3956
4079
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3957
4080
|
}
|
|
3958
|
-
function getRuntimePaperclipApiBaseUrl() {
|
|
3959
|
-
if (typeof process === "undefined" || !process?.env) {
|
|
3960
|
-
return void 0;
|
|
3961
|
-
}
|
|
3962
|
-
return normalizePaperclipApiBaseUrl(process.env.PAPERCLIP_API_URL);
|
|
3963
|
-
}
|
|
3964
4081
|
function resolvePaperclipApiBaseUrl(...values) {
|
|
3965
|
-
const runtimePaperclipApiBaseUrl = getRuntimePaperclipApiBaseUrl();
|
|
3966
|
-
if (runtimePaperclipApiBaseUrl) {
|
|
3967
|
-
return runtimePaperclipApiBaseUrl;
|
|
3968
|
-
}
|
|
3969
4082
|
for (const value of values) {
|
|
3970
4083
|
const normalizedValue = normalizePaperclipApiBaseUrl(value);
|
|
3971
4084
|
if (normalizedValue) {
|
|
@@ -3983,10 +4096,6 @@ function getConfiguredPaperclipApiBaseUrl(settings, config, companyId) {
|
|
|
3983
4096
|
) : resolvePaperclipApiBaseUrl(config?.paperclipApiBaseUrl, settings?.paperclipApiBaseUrl);
|
|
3984
4097
|
}
|
|
3985
4098
|
function resolveTrustedPaperclipApiBaseUrlInput(value, settings, config, companyId) {
|
|
3986
|
-
const runtimePaperclipApiBaseUrl = getRuntimePaperclipApiBaseUrl();
|
|
3987
|
-
if (runtimePaperclipApiBaseUrl) {
|
|
3988
|
-
return runtimePaperclipApiBaseUrl;
|
|
3989
|
-
}
|
|
3990
4099
|
const requestedPaperclipApiBaseUrl = normalizePaperclipApiBaseUrl(value);
|
|
3991
4100
|
const configuredPaperclipApiBaseUrl = normalizePaperclipApiBaseUrl(config?.paperclipApiBaseUrl);
|
|
3992
4101
|
const normalizedCompanyId = normalizeCompanyId(companyId);
|
|
@@ -5637,9 +5746,6 @@ function resolvePaperclipIssueStatus(params) {
|
|
|
5637
5746
|
}
|
|
5638
5747
|
function resolvePaperclipPullRequestIssueStatus(params) {
|
|
5639
5748
|
const { currentStatus, pullRequest, hasExecutorHandoffTarget } = params;
|
|
5640
|
-
if (currentStatus === "done" || currentStatus === "cancelled") {
|
|
5641
|
-
return currentStatus;
|
|
5642
|
-
}
|
|
5643
5749
|
if (shouldPreserveBlockedExternalPullRequestWait({
|
|
5644
5750
|
currentStatus,
|
|
5645
5751
|
linkedPullRequests: [pullRequest]
|
|
@@ -5648,7 +5754,7 @@ function resolvePaperclipPullRequestIssueStatus(params) {
|
|
|
5648
5754
|
}
|
|
5649
5755
|
return resolvePaperclipStatusFromLinkedPullRequests([pullRequest], {
|
|
5650
5756
|
preferInProgress: hasExecutorHandoffTarget,
|
|
5651
|
-
preserveTransientUnknownMergeabilityWait: currentStatus === "in_review"
|
|
5757
|
+
preserveTransientUnknownMergeabilityWait: currentStatus === "done" || currentStatus === "in_review"
|
|
5652
5758
|
});
|
|
5653
5759
|
}
|
|
5654
5760
|
async function listLinkedPullRequestsForIssue(octokit, repository, issueNumber) {
|
|
@@ -7828,7 +7934,14 @@ function applyPaperclipApiAuthentication(init, companyId) {
|
|
|
7828
7934
|
};
|
|
7829
7935
|
}
|
|
7830
7936
|
async function fetchPaperclipApi(url, init, options) {
|
|
7831
|
-
|
|
7937
|
+
const authenticatedInit = applyPaperclipApiAuthentication(init, options?.companyId);
|
|
7938
|
+
try {
|
|
7939
|
+
return await fetch(url, authenticatedInit);
|
|
7940
|
+
} catch (error) {
|
|
7941
|
+
throw new Error(formatPaperclipApiFetchErrorMessage(error, url, authenticatedInit), {
|
|
7942
|
+
cause: error
|
|
7943
|
+
});
|
|
7944
|
+
}
|
|
7832
7945
|
}
|
|
7833
7946
|
async function detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl) {
|
|
7834
7947
|
if (!paperclipApiBaseUrl) {
|
|
@@ -9980,6 +10093,187 @@ function buildToolSuccessResult(content, data) {
|
|
|
9980
10093
|
data
|
|
9981
10094
|
};
|
|
9982
10095
|
}
|
|
10096
|
+
function normalizePullRequestAssetMimeType(value) {
|
|
10097
|
+
if (typeof value !== "string") {
|
|
10098
|
+
return void 0;
|
|
10099
|
+
}
|
|
10100
|
+
const normalized = value.trim().toLowerCase();
|
|
10101
|
+
if (!/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*(?:;\s*[a-z0-9!#$&^_.+-]+=[a-z0-9!#$&^_.+-]+)*$/.test(normalized)) {
|
|
10102
|
+
return void 0;
|
|
10103
|
+
}
|
|
10104
|
+
return normalized.split(";", 1)[0];
|
|
10105
|
+
}
|
|
10106
|
+
function getPullRequestAssetMimeTypeFromFileName(fileName) {
|
|
10107
|
+
const extensionMatch = fileName.trim().toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
10108
|
+
if (!extensionMatch) {
|
|
10109
|
+
return void 0;
|
|
10110
|
+
}
|
|
10111
|
+
return PULL_REQUEST_ASSET_MIME_TYPE_BY_EXTENSION[extensionMatch[1]];
|
|
10112
|
+
}
|
|
10113
|
+
function getPullRequestAssetExtension(fileName) {
|
|
10114
|
+
const extensionMatch = fileName.trim().toLowerCase().match(/\.([a-z0-9]+)$/);
|
|
10115
|
+
return extensionMatch?.[1];
|
|
10116
|
+
}
|
|
10117
|
+
function sanitizePullRequestAssetFileName(fileNameInput, mimeType) {
|
|
10118
|
+
const inferredExtension = PULL_REQUEST_ASSET_EXTENSION_BY_MIME_TYPE[mimeType] ?? "bin";
|
|
10119
|
+
const fallbackBaseName = `asset.${inferredExtension}`;
|
|
10120
|
+
const rawFileName = normalizeOptionalString2(fileNameInput) ?? fallbackBaseName;
|
|
10121
|
+
const lastSegment = rawFileName.split(/[\\/]+/).filter(Boolean).at(-1) ?? fallbackBaseName;
|
|
10122
|
+
const extension = getPullRequestAssetExtension(lastSegment) ?? inferredExtension;
|
|
10123
|
+
const withoutExtension = lastSegment.replace(/\.[A-Za-z0-9]+$/, "");
|
|
10124
|
+
const sanitizedBaseName = withoutExtension.normalize("NFKD").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/[-_.]+$/g, "").replace(/^[-_.]+/g, "").slice(0, 80) || "asset";
|
|
10125
|
+
return `${sanitizedBaseName}.${extension}`;
|
|
10126
|
+
}
|
|
10127
|
+
function sanitizePullRequestAssetLabel(value, fallbackFileName) {
|
|
10128
|
+
const normalized = normalizeOptionalString2(value);
|
|
10129
|
+
return (normalized ?? fallbackFileName.replace(/[-_.]+/g, " ")).slice(0, 200);
|
|
10130
|
+
}
|
|
10131
|
+
function sanitizePullRequestAssetArtifactBranch(value, pullRequestNumber) {
|
|
10132
|
+
const normalized = normalizeOptionalString2(value);
|
|
10133
|
+
const candidate = normalized ?? `paperclip-artifacts-pr-${pullRequestNumber}`;
|
|
10134
|
+
const sanitized = candidate.replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\.\.+/g, ".").replace(/\/{2,}/g, "/").replace(/^[-/.]+|[-/.]+$/g, "");
|
|
10135
|
+
if (!sanitized || sanitized.includes("..") || sanitized.startsWith("/") || sanitized.endsWith(".lock")) {
|
|
10136
|
+
throw new Error("artifactBranch must be a safe Git branch name.");
|
|
10137
|
+
}
|
|
10138
|
+
return sanitized;
|
|
10139
|
+
}
|
|
10140
|
+
function decodePullRequestAssetContent(payload) {
|
|
10141
|
+
const dataUrl = normalizeOptionalString2(payload.dataUrl);
|
|
10142
|
+
let contentBase64 = normalizeOptionalString2(payload.contentBase64);
|
|
10143
|
+
let mimeType = normalizePullRequestAssetMimeType(payload.mimeType);
|
|
10144
|
+
if (dataUrl) {
|
|
10145
|
+
const match = dataUrl.match(/^data:([^;,]+(?:;[^,]+)?);base64,(.+)$/is);
|
|
10146
|
+
if (!match) {
|
|
10147
|
+
throw new Error("dataUrl must be a base64 data URL.");
|
|
10148
|
+
}
|
|
10149
|
+
const dataUrlMimeType = normalizePullRequestAssetMimeType(match[1]);
|
|
10150
|
+
if (!dataUrlMimeType) {
|
|
10151
|
+
throw new Error("dataUrl MIME type must be a valid MIME type such as image/png or application/pdf.");
|
|
10152
|
+
}
|
|
10153
|
+
mimeType = dataUrlMimeType;
|
|
10154
|
+
contentBase64 = match[2].replace(/\s+/g, "");
|
|
10155
|
+
}
|
|
10156
|
+
if (!contentBase64) {
|
|
10157
|
+
throw new Error("contentBase64 or dataUrl is required.");
|
|
10158
|
+
}
|
|
10159
|
+
mimeType = mimeType ?? getPullRequestAssetMimeTypeFromFileName(normalizeOptionalString2(payload.fileName) ?? "") ?? DEFAULT_PULL_REQUEST_ASSET_MIME_TYPE;
|
|
10160
|
+
const normalizedBase64 = contentBase64.replace(/\s+/g, "");
|
|
10161
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalizedBase64) || normalizedBase64.length % 4 === 1) {
|
|
10162
|
+
throw new Error("Asset content must be valid base64.");
|
|
10163
|
+
}
|
|
10164
|
+
const bytes = Buffer.from(normalizedBase64, "base64");
|
|
10165
|
+
if (bytes.length === 0) {
|
|
10166
|
+
throw new Error("Asset content must not be empty.");
|
|
10167
|
+
}
|
|
10168
|
+
if (bytes.length > MAX_PULL_REQUEST_ASSET_BYTES) {
|
|
10169
|
+
throw new Error(`Asset content exceeds the ${MAX_PULL_REQUEST_ASSET_BYTES} byte limit.`);
|
|
10170
|
+
}
|
|
10171
|
+
return {
|
|
10172
|
+
bytes,
|
|
10173
|
+
mimeType
|
|
10174
|
+
};
|
|
10175
|
+
}
|
|
10176
|
+
function buildPullRequestAssetMarkdown(label, rawUrl, mimeType) {
|
|
10177
|
+
const normalizedLabel = label.replace(/[\]\n\r]/g, " ").trim();
|
|
10178
|
+
if (mimeType.startsWith("image/")) {
|
|
10179
|
+
return ``;
|
|
10180
|
+
}
|
|
10181
|
+
return `[${normalizedLabel}](${rawUrl})`;
|
|
10182
|
+
}
|
|
10183
|
+
async function uploadPullRequestAssetArtifact(params) {
|
|
10184
|
+
const { bytes, mimeType } = decodePullRequestAssetContent(params.payload);
|
|
10185
|
+
const fileName = sanitizePullRequestAssetFileName(params.payload.fileName, mimeType);
|
|
10186
|
+
const label = sanitizePullRequestAssetLabel(params.payload.label ?? params.payload.alt, fileName);
|
|
10187
|
+
const caption = normalizeOptionalString2(params.payload.caption);
|
|
10188
|
+
const artifactBranch = sanitizePullRequestAssetArtifactBranch(params.payload.artifactBranch, params.pullRequestNumber);
|
|
10189
|
+
const pullRequestResponse = await params.octokit.rest.pulls.get({
|
|
10190
|
+
owner: params.repository.owner,
|
|
10191
|
+
repo: params.repository.repo,
|
|
10192
|
+
pull_number: params.pullRequestNumber,
|
|
10193
|
+
headers: {
|
|
10194
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
10195
|
+
}
|
|
10196
|
+
});
|
|
10197
|
+
const headSha = pullRequestResponse.data.head.sha;
|
|
10198
|
+
const shortHeadSha = headSha.slice(0, 12);
|
|
10199
|
+
const contentPath = `assets/pr-${params.pullRequestNumber}/${shortHeadSha}/${fileName}`;
|
|
10200
|
+
let branchSha;
|
|
10201
|
+
try {
|
|
10202
|
+
const branchRefResponse = await params.octokit.rest.git.getRef({
|
|
10203
|
+
owner: params.repository.owner,
|
|
10204
|
+
repo: params.repository.repo,
|
|
10205
|
+
ref: `heads/${artifactBranch}`,
|
|
10206
|
+
headers: {
|
|
10207
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
10208
|
+
}
|
|
10209
|
+
});
|
|
10210
|
+
branchSha = branchRefResponse.data.object.sha;
|
|
10211
|
+
} catch (error) {
|
|
10212
|
+
if (getErrorStatus(error) !== 404) {
|
|
10213
|
+
throw error;
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
if (!branchSha) {
|
|
10217
|
+
await params.octokit.rest.git.createRef({
|
|
10218
|
+
owner: params.repository.owner,
|
|
10219
|
+
repo: params.repository.repo,
|
|
10220
|
+
ref: `refs/heads/${artifactBranch}`,
|
|
10221
|
+
sha: pullRequestResponse.data.base.sha,
|
|
10222
|
+
headers: {
|
|
10223
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
10224
|
+
}
|
|
10225
|
+
});
|
|
10226
|
+
}
|
|
10227
|
+
let existingFileSha;
|
|
10228
|
+
try {
|
|
10229
|
+
const existingContentResponse = await params.octokit.rest.repos.getContent({
|
|
10230
|
+
owner: params.repository.owner,
|
|
10231
|
+
repo: params.repository.repo,
|
|
10232
|
+
path: contentPath,
|
|
10233
|
+
ref: artifactBranch,
|
|
10234
|
+
headers: {
|
|
10235
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
10236
|
+
}
|
|
10237
|
+
});
|
|
10238
|
+
if (!Array.isArray(existingContentResponse.data) && existingContentResponse.data.type === "file") {
|
|
10239
|
+
existingFileSha = existingContentResponse.data.sha;
|
|
10240
|
+
}
|
|
10241
|
+
} catch (error) {
|
|
10242
|
+
if (getErrorStatus(error) !== 404) {
|
|
10243
|
+
throw error;
|
|
10244
|
+
}
|
|
10245
|
+
}
|
|
10246
|
+
const updateResponse = await params.octokit.rest.repos.createOrUpdateFileContents({
|
|
10247
|
+
owner: params.repository.owner,
|
|
10248
|
+
repo: params.repository.repo,
|
|
10249
|
+
path: contentPath,
|
|
10250
|
+
message: `Add asset for PR #${params.pullRequestNumber}`,
|
|
10251
|
+
content: bytes.toString("base64"),
|
|
10252
|
+
branch: artifactBranch,
|
|
10253
|
+
...existingFileSha ? { sha: existingFileSha } : {},
|
|
10254
|
+
headers: {
|
|
10255
|
+
"X-GitHub-Api-Version": GITHUB_API_VERSION
|
|
10256
|
+
}
|
|
10257
|
+
});
|
|
10258
|
+
const commitSha = updateResponse.data.commit.sha;
|
|
10259
|
+
const rawUrl = `https://raw.githubusercontent.com/${params.repository.owner}/${params.repository.repo}/${commitSha}/${contentPath}`;
|
|
10260
|
+
const markdown = buildPullRequestAssetMarkdown(label, rawUrl, mimeType);
|
|
10261
|
+
return {
|
|
10262
|
+
repository: params.repository.url,
|
|
10263
|
+
pullRequestNumber: params.pullRequestNumber,
|
|
10264
|
+
artifactBranch,
|
|
10265
|
+
path: contentPath,
|
|
10266
|
+
fileName,
|
|
10267
|
+
mimeType,
|
|
10268
|
+
sizeBytes: bytes.length,
|
|
10269
|
+
commitSha,
|
|
10270
|
+
rawUrl,
|
|
10271
|
+
markdown,
|
|
10272
|
+
label,
|
|
10273
|
+
...mimeType.startsWith("image/") ? { alt: label } : {},
|
|
10274
|
+
...caption ? { caption } : {}
|
|
10275
|
+
};
|
|
10276
|
+
}
|
|
9983
10277
|
function buildToolErrorResult(error) {
|
|
9984
10278
|
const rateLimitPause = getGitHubRateLimitPauseDetails(error);
|
|
9985
10279
|
if (rateLimitPause) {
|
|
@@ -10160,12 +10454,15 @@ async function handleCompanyMetricApiRoute(ctx, input) {
|
|
|
10160
10454
|
}
|
|
10161
10455
|
};
|
|
10162
10456
|
}
|
|
10163
|
-
function
|
|
10457
|
+
function parsePluginApiRouteJsonObjectBody(input, routeLabel) {
|
|
10164
10458
|
if (!input.body || typeof input.body !== "object" || Array.isArray(input.body)) {
|
|
10165
|
-
throw new Error(
|
|
10459
|
+
throw new Error(`${routeLabel} body must be a JSON object.`);
|
|
10166
10460
|
}
|
|
10167
10461
|
return input.body;
|
|
10168
10462
|
}
|
|
10463
|
+
function parseIssueLinkApiRouteBody(input) {
|
|
10464
|
+
return parsePluginApiRouteJsonObjectBody(input, "Issue link route");
|
|
10465
|
+
}
|
|
10169
10466
|
function normalizeIssueLinkApiRouteKind(payload) {
|
|
10170
10467
|
const explicitKind = normalizeIssueGitHubLinkKind(payload.kind);
|
|
10171
10468
|
if (explicitKind) {
|
|
@@ -10180,6 +10477,47 @@ function normalizeIssueLinkApiRouteKind(payload) {
|
|
|
10180
10477
|
}
|
|
10181
10478
|
return null;
|
|
10182
10479
|
}
|
|
10480
|
+
async function handlePullRequestAssetApiRoute(ctx, input) {
|
|
10481
|
+
if (input.actor.actorType !== "agent") {
|
|
10482
|
+
throw new Error("Pull request assets must be uploaded by an authenticated Paperclip agent.");
|
|
10483
|
+
}
|
|
10484
|
+
const payload = parsePluginApiRouteJsonObjectBody(input, "Pull request asset route");
|
|
10485
|
+
const rawPullRequestUrl = normalizeOptionalString2(payload.pullRequestUrl);
|
|
10486
|
+
const pullRequestUrl = normalizeGitHubPullRequestHtmlUrl(rawPullRequestUrl);
|
|
10487
|
+
if (rawPullRequestUrl && !pullRequestUrl) {
|
|
10488
|
+
throw new Error("pullRequestUrl must be a valid GitHub pull request URL.");
|
|
10489
|
+
}
|
|
10490
|
+
const parsedPullRequestUrl = pullRequestUrl ? parseGitHubPullRequestHtmlUrl(pullRequestUrl) : void 0;
|
|
10491
|
+
const repositoryInput = normalizeOptionalString2(payload.repository) ?? parsedPullRequestUrl?.repositoryUrl;
|
|
10492
|
+
if (!repositoryInput) {
|
|
10493
|
+
throw new Error("repository is required unless pullRequestUrl is provided.");
|
|
10494
|
+
}
|
|
10495
|
+
const repository = requireRepositoryReference(repositoryInput);
|
|
10496
|
+
const pullRequestNumber = normalizeToolPositiveInteger(payload.pullRequestNumber) ?? parsedPullRequestUrl?.pullRequestNumber;
|
|
10497
|
+
if (!pullRequestNumber) {
|
|
10498
|
+
throw new Error("pullRequestNumber is required unless pullRequestUrl is provided.");
|
|
10499
|
+
}
|
|
10500
|
+
if (parsedPullRequestUrl && !areRepositoriesEqual(repository, requireRepositoryReference(parsedPullRequestUrl.repositoryUrl))) {
|
|
10501
|
+
throw new Error("repository must match pullRequestUrl.");
|
|
10502
|
+
}
|
|
10503
|
+
const octokit = await createGitHubToolOctokit(ctx, input.companyId, {
|
|
10504
|
+
toolName: PULL_REQUEST_ASSET_API_ROUTE_KEY,
|
|
10505
|
+
repositoryUrl: repository.url
|
|
10506
|
+
});
|
|
10507
|
+
const asset = await uploadPullRequestAssetArtifact({
|
|
10508
|
+
octokit,
|
|
10509
|
+
repository,
|
|
10510
|
+
pullRequestNumber,
|
|
10511
|
+
payload
|
|
10512
|
+
});
|
|
10513
|
+
return {
|
|
10514
|
+
status: 201,
|
|
10515
|
+
body: {
|
|
10516
|
+
status: "uploaded",
|
|
10517
|
+
asset
|
|
10518
|
+
}
|
|
10519
|
+
};
|
|
10520
|
+
}
|
|
10183
10521
|
async function handleIssueLinkApiRoute(ctx, input) {
|
|
10184
10522
|
if (input.actor.actorType !== "agent") {
|
|
10185
10523
|
throw new Error("GitHub issue links must be recorded by an authenticated Paperclip agent.");
|
|
@@ -15062,6 +15400,27 @@ function registerGitHubAgentTools(ctx) {
|
|
|
15062
15400
|
);
|
|
15063
15401
|
})
|
|
15064
15402
|
);
|
|
15403
|
+
ctx.tools.register(
|
|
15404
|
+
"upload_pull_request_asset",
|
|
15405
|
+
getGitHubAgentToolDeclaration("upload_pull_request_asset"),
|
|
15406
|
+
async (params, runCtx) => executeGitHubTool(async () => {
|
|
15407
|
+
const input = getToolInputRecord(params);
|
|
15408
|
+
const target = await resolveGitHubPullRequestToolTarget(ctx, runCtx, input);
|
|
15409
|
+
const octokit = await createAgentToolOctokit(runCtx, "upload_pull_request_asset", target.repository);
|
|
15410
|
+
const asset = await uploadPullRequestAssetArtifact({
|
|
15411
|
+
octokit,
|
|
15412
|
+
repository: target.repository,
|
|
15413
|
+
pullRequestNumber: target.pullRequestNumber,
|
|
15414
|
+
payload: input
|
|
15415
|
+
});
|
|
15416
|
+
return buildToolSuccessResult(
|
|
15417
|
+
`Uploaded asset ${asset.fileName} for pull request #${target.pullRequestNumber}.`,
|
|
15418
|
+
{
|
|
15419
|
+
asset
|
|
15420
|
+
}
|
|
15421
|
+
);
|
|
15422
|
+
})
|
|
15423
|
+
);
|
|
15065
15424
|
ctx.tools.register(
|
|
15066
15425
|
"link_github_item",
|
|
15067
15426
|
getGitHubAgentToolDeclaration("link_github_item"),
|
|
@@ -15113,6 +15472,7 @@ function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
|
|
|
15113
15472
|
var __testing = {
|
|
15114
15473
|
buildSyncFallbackExecutionStatePatch,
|
|
15115
15474
|
createGitHubToolOctokit,
|
|
15475
|
+
formatPaperclipApiFetchErrorMessage,
|
|
15116
15476
|
hasUnresolvedPaperclipIssueBlocker,
|
|
15117
15477
|
isHealthyMaintainerWaitTransition,
|
|
15118
15478
|
resolveSyncTransitionAssignee
|
|
@@ -15148,6 +15508,7 @@ var plugin = definePlugin({
|
|
|
15148
15508
|
...getPublicSettingsForScope(settingsForResponse, requestedCompanyId),
|
|
15149
15509
|
...includeAssignees ? { availableAssignees } : {},
|
|
15150
15510
|
totalSyncedIssuesCount: countImportedIssuesForMappings(importRegistry, scopedMappings),
|
|
15511
|
+
paperclipApiBaseUrlConfigured: Boolean(normalizePaperclipApiBaseUrl(config.paperclipApiBaseUrl)),
|
|
15151
15512
|
githubTokenConfigured,
|
|
15152
15513
|
paperclipBoardAccessConfigured: requestedCompanyId ? hasConfiguredPaperclipBoardAccess(settingsForResponse, config, requestedCompanyId) : hasConfiguredPaperclipBoardAccessForMappings(settingsForResponse, config, scopedMappings),
|
|
15153
15514
|
...savedBoardTokenRef ? { paperclipBoardAccessConfigSyncRef: savedBoardTokenRef } : {},
|
|
@@ -15534,6 +15895,9 @@ var plugin = definePlugin({
|
|
|
15534
15895
|
if (input.routeKey === ISSUE_LINK_API_ROUTE_KEY) {
|
|
15535
15896
|
return handleIssueLinkApiRoute(pluginRuntimeContext, input);
|
|
15536
15897
|
}
|
|
15898
|
+
if (input.routeKey === PULL_REQUEST_ASSET_API_ROUTE_KEY) {
|
|
15899
|
+
return handlePullRequestAssetApiRoute(pluginRuntimeContext, input);
|
|
15900
|
+
}
|
|
15537
15901
|
return {
|
|
15538
15902
|
status: 404,
|
|
15539
15903
|
body: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-github-plugin",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
4
4
|
"description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@octokit/rest": "^22.0.1",
|
|
44
44
|
"@paperclipai/plugin-sdk": "^2026.428.0",
|
|
45
|
-
"react": "^19.2.
|
|
45
|
+
"react": "^19.2.6",
|
|
46
46
|
"react-markdown": "^10.1.0",
|
|
47
47
|
"rehype-raw": "^7.0.0",
|
|
48
48
|
"rehype-sanitize": "^6.0.0",
|