paperclip-github-plugin 0.7.4 → 0.8.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.
package/dist/worker.js CHANGED
@@ -223,6 +223,21 @@ var GITHUB_AGENT_TOOLS = [
223
223
  }
224
224
  }
225
225
  },
226
+ {
227
+ name: "assign_to_current_user",
228
+ displayName: "Assign To Current User",
229
+ description: "Assign a GitHub issue to the GitHub user authenticated by the saved token, preserving any existing assignees.",
230
+ parametersSchema: {
231
+ type: "object",
232
+ additionalProperties: false,
233
+ ...issueTargetSchema,
234
+ properties: {
235
+ repository: repositoryProperty,
236
+ issueNumber: issueNumberProperty,
237
+ paperclipIssueId: paperclipIssueIdProperty
238
+ }
239
+ }
240
+ },
226
241
  {
227
242
  name: "add_issue_comment",
228
243
  displayName: "Add Issue Comment",
@@ -2243,6 +2258,40 @@ function doesImportedIssueMatchTarget(issue, target) {
2243
2258
  }
2244
2259
  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;
2245
2260
  }
2261
+ function resolvePaperclipIssueOriginGitHubIssueLink(issue, companyId) {
2262
+ if (issue?.originKind !== GITHUB_ISSUE_ORIGIN_KIND || typeof issue.originId !== "string") {
2263
+ return null;
2264
+ }
2265
+ const reference = parseGitHubIssueHtmlUrl(issue.originId);
2266
+ if (!reference) {
2267
+ return null;
2268
+ }
2269
+ return {
2270
+ source: "issue_origin",
2271
+ companyId,
2272
+ paperclipProjectId: issue.projectId ?? void 0,
2273
+ repositoryUrl: reference.repositoryUrl,
2274
+ githubIssueNumber: reference.issueNumber,
2275
+ githubIssueUrl: reference.issueUrl,
2276
+ linkedPullRequestNumbers: [],
2277
+ linkedPullRequests: []
2278
+ };
2279
+ }
2280
+ function resolvePaperclipIssueOriginGitHubPullRequestLink(issue) {
2281
+ if (issue?.originKind !== GITHUB_PULL_REQUEST_ORIGIN_KIND || typeof issue.originId !== "string") {
2282
+ return null;
2283
+ }
2284
+ const reference = parseGitHubPullRequestHtmlUrl(issue.originId);
2285
+ if (!reference) {
2286
+ return null;
2287
+ }
2288
+ return {
2289
+ source: "issue_origin",
2290
+ repositoryUrl: reference.repositoryUrl,
2291
+ githubPullRequestNumber: reference.pullRequestNumber,
2292
+ githubPullRequestUrl: reference.pullRequestUrl
2293
+ };
2294
+ }
2246
2295
  async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options = {}) {
2247
2296
  const linkRecords = options.linkRecords ?? await listGitHubIssueLinkRecords(ctx, {
2248
2297
  paperclipIssueId: issueId
@@ -2287,6 +2336,10 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2287
2336
  }
2288
2337
  }
2289
2338
  const issue = options.paperclipIssue ?? await ctx.issues.get(issueId, companyId);
2339
+ const issueOriginLink = resolvePaperclipIssueOriginGitHubIssueLink(issue, companyId);
2340
+ if (issueOriginLink) {
2341
+ return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, issueOriginLink) ?? issueOriginLink;
2342
+ }
2290
2343
  const githubIssueUrl = extractImportedGitHubIssueUrlFromDescription(issue?.description);
2291
2344
  const githubIssueReference = githubIssueUrl ? parseGitHubIssueHtmlUrl(githubIssueUrl) : null;
2292
2345
  if (!githubIssueReference) {
@@ -2304,6 +2357,18 @@ async function resolvePaperclipIssueGitHubLink(ctx, issueId, companyId, options
2304
2357
  };
2305
2358
  return await hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) ?? fallbackLink;
2306
2359
  }
2360
+ async function resolvePaperclipIssueGitHubPullRequestLink(ctx, issueId, companyId) {
2361
+ const links = await listGitHubPullRequestLinkRecords(ctx, {
2362
+ paperclipIssueId: issueId
2363
+ });
2364
+ return links.filter((record) => !record.data.companyId || record.data.companyId === companyId).sort((left, right) => {
2365
+ const rightTimestamp = Date.parse(right.updatedAt ?? right.createdAt ?? "");
2366
+ const leftTimestamp = Date.parse(left.updatedAt ?? left.createdAt ?? "");
2367
+ const safeRightTimestamp = Number.isFinite(rightTimestamp) ? rightTimestamp : 0;
2368
+ const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
2369
+ return safeRightTimestamp - safeLeftTimestamp;
2370
+ })[0] ?? null;
2371
+ }
2307
2372
  async function hydrateRecoveredPaperclipIssueGitHubLink(ctx, issueId, fallbackLink) {
2308
2373
  const repository = parseRepositoryReference(fallbackLink.repositoryUrl);
2309
2374
  if (!repository) {
@@ -2379,21 +2444,33 @@ async function resolveManualSyncTarget(ctx, settings, input) {
2379
2444
  }
2380
2445
  const link = await resolvePaperclipIssueGitHubLink(ctx, input.issueId, companyId2);
2381
2446
  if (!link) {
2382
- throw new Error("This Paperclip issue is not linked to a GitHub issue yet. Run a broader sync first.");
2383
- }
2384
- const candidateMappings = getSyncableMappingsForTarget(settings.mappings, {
2385
- kind: "issue",
2386
- companyId: companyId2,
2387
- projectId: link.paperclipProjectId,
2388
- repositoryUrl: link.repositoryUrl,
2389
- issueId: input.issueId,
2390
- githubIssueId: link.githubIssueId,
2391
- githubIssueNumber: link.githubIssueNumber,
2392
- githubIssueUrl: link.githubIssueUrl,
2393
- displayLabel: `issue #${link.githubIssueNumber}`
2394
- });
2395
- if (candidateMappings.length === 0) {
2396
- throw new Error("No saved GitHub repository mapping matches this Paperclip issue.");
2447
+ const pullRequestLink = await resolvePaperclipIssueGitHubPullRequestLink(ctx, input.issueId, companyId2);
2448
+ if (!pullRequestLink) {
2449
+ throw new Error("This Paperclip issue is not linked to a GitHub issue or pull request yet. Run a broader sync first.");
2450
+ }
2451
+ const candidateMappings = getSyncableMappingsForTarget(settings.mappings, {
2452
+ kind: "issue",
2453
+ companyId: companyId2,
2454
+ projectId: pullRequestLink.data.paperclipProjectId,
2455
+ repositoryUrl: pullRequestLink.data.repositoryUrl,
2456
+ issueId: input.issueId,
2457
+ githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2458
+ githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2459
+ displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
2460
+ });
2461
+ if (candidateMappings.length === 0) {
2462
+ throw new Error("No saved GitHub repository mapping matches this Paperclip issue.");
2463
+ }
2464
+ return {
2465
+ kind: "issue",
2466
+ companyId: companyId2,
2467
+ projectId: pullRequestLink.data.paperclipProjectId,
2468
+ issueId: input.issueId,
2469
+ repositoryUrl: pullRequestLink.data.repositoryUrl,
2470
+ githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2471
+ githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2472
+ displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
2473
+ };
2397
2474
  }
2398
2475
  return {
2399
2476
  kind: "issue",
@@ -2517,23 +2594,46 @@ async function buildToolbarSyncState(ctx, input) {
2517
2594
  }
2518
2595
  if (entityType === "issue" && entityId && companyId) {
2519
2596
  const link = await resolvePaperclipIssueGitHubLink(ctx, entityId, companyId);
2520
- const mappings = link ? getSyncableMappingsForTarget(settings.mappings, {
2597
+ if (link) {
2598
+ const mappings2 = getSyncableMappingsForTarget(settings.mappings, {
2599
+ kind: "issue",
2600
+ companyId,
2601
+ projectId: link.paperclipProjectId,
2602
+ issueId: entityId,
2603
+ repositoryUrl: link.repositoryUrl,
2604
+ githubIssueId: link.githubIssueId,
2605
+ githubIssueNumber: link.githubIssueNumber,
2606
+ githubIssueUrl: link.githubIssueUrl,
2607
+ displayLabel: `issue #${link.githubIssueNumber}`
2608
+ });
2609
+ return {
2610
+ kind: "issue",
2611
+ visible: false,
2612
+ canRun: githubTokenConfigured && mappings2.length > 0,
2613
+ label: `Sync #${link.githubIssueNumber}`,
2614
+ message: `Sync ${link.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} issue #${link.githubIssueNumber}.`,
2615
+ syncState: settings.syncState,
2616
+ githubTokenConfigured,
2617
+ savedMappingCount
2618
+ };
2619
+ }
2620
+ const pullRequestLink = await resolvePaperclipIssueGitHubPullRequestLink(ctx, entityId, companyId);
2621
+ const mappings = pullRequestLink ? getSyncableMappingsForTarget(settings.mappings, {
2521
2622
  kind: "issue",
2522
2623
  companyId,
2523
- projectId: link.paperclipProjectId,
2624
+ projectId: pullRequestLink.data.paperclipProjectId,
2524
2625
  issueId: entityId,
2525
- repositoryUrl: link.repositoryUrl,
2526
- githubIssueId: link.githubIssueId,
2527
- githubIssueNumber: link.githubIssueNumber,
2528
- githubIssueUrl: link.githubIssueUrl,
2529
- displayLabel: `issue #${link.githubIssueNumber}`
2626
+ repositoryUrl: pullRequestLink.data.repositoryUrl,
2627
+ githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2628
+ githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2629
+ displayLabel: `pull request #${pullRequestLink.data.githubPullRequestNumber}`
2530
2630
  }) : [];
2531
2631
  return {
2532
2632
  kind: "issue",
2533
2633
  visible: false,
2534
2634
  canRun: githubTokenConfigured && mappings.length > 0,
2535
- label: link?.githubIssueNumber ? `Sync #${link.githubIssueNumber}` : "Sync issue",
2536
- message: link ? `Sync ${link.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} issue #${link.githubIssueNumber}.` : "This Paperclip issue is not linked to GitHub yet.",
2635
+ label: pullRequestLink?.data.githubPullRequestNumber ? `Sync PR #${pullRequestLink.data.githubPullRequestNumber}` : "Sync issue",
2636
+ message: pullRequestLink ? `Sync ${pullRequestLink.data.repositoryUrl.replace(/^https:\/\/github\.com\//, "")} pull request #${pullRequestLink.data.githubPullRequestNumber}.` : "This Paperclip issue is not linked to GitHub yet.",
2537
2637
  syncState: settings.syncState,
2538
2638
  githubTokenConfigured,
2539
2639
  savedMappingCount
@@ -2747,26 +2847,45 @@ async function buildIssueGitHubDetails(ctx, input) {
2747
2847
  const safeLeftTimestamp = Number.isFinite(leftTimestamp) ? leftTimestamp : 0;
2748
2848
  return safeRightTimestamp - safeLeftTimestamp;
2749
2849
  })[0];
2750
- if (!pullRequestLink) {
2850
+ if (pullRequestLink) {
2851
+ return {
2852
+ paperclipIssueId: issueId,
2853
+ kind: "pull_request",
2854
+ source: "pull_request_entity",
2855
+ repositoryUrl: pullRequestLink.data.repositoryUrl,
2856
+ githubPullRequestNumber: pullRequestLink.data.githubPullRequestNumber,
2857
+ githubPullRequestUrl: pullRequestLink.data.githubPullRequestUrl,
2858
+ githubPullRequestState: pullRequestLink.data.githubPullRequestState,
2859
+ title: pullRequestLink.data.title,
2860
+ linkedPullRequestNumbers: [pullRequestLink.data.githubPullRequestNumber],
2861
+ linkedPullRequests: [
2862
+ {
2863
+ number: pullRequestLink.data.githubPullRequestNumber,
2864
+ repositoryUrl: pullRequestLink.data.repositoryUrl
2865
+ }
2866
+ ],
2867
+ syncedAt: pullRequestLink.data.syncedAt
2868
+ };
2869
+ }
2870
+ const paperclipIssue = await ctx.issues.get(issueId, companyId);
2871
+ const pullRequestOriginLink = resolvePaperclipIssueOriginGitHubPullRequestLink(paperclipIssue);
2872
+ if (!pullRequestOriginLink) {
2751
2873
  return null;
2752
2874
  }
2753
2875
  return {
2754
2876
  paperclipIssueId: issueId,
2755
2877
  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],
2878
+ source: pullRequestOriginLink.source,
2879
+ repositoryUrl: pullRequestOriginLink.repositoryUrl,
2880
+ githubPullRequestNumber: pullRequestOriginLink.githubPullRequestNumber,
2881
+ githubPullRequestUrl: pullRequestOriginLink.githubPullRequestUrl,
2882
+ linkedPullRequestNumbers: [pullRequestOriginLink.githubPullRequestNumber],
2763
2883
  linkedPullRequests: [
2764
2884
  {
2765
- number: pullRequestLink.data.githubPullRequestNumber,
2766
- repositoryUrl: pullRequestLink.data.repositoryUrl
2885
+ number: pullRequestOriginLink.githubPullRequestNumber,
2886
+ repositoryUrl: pullRequestOriginLink.repositoryUrl
2767
2887
  }
2768
- ],
2769
- syncedAt: pullRequestLink.data.syncedAt
2888
+ ]
2770
2889
  };
2771
2890
  }
2772
2891
  async function resolveIssueByIdentifier(ctx, input) {
@@ -8040,6 +8159,40 @@ async function listRepositoryIssues(octokit, repository, state = "open", options
8040
8159
  }
8041
8160
  return normalizedIssues;
8042
8161
  }
8162
+ async function listRepositoryIssuesForSyncTarget(octokit, repository, state, target, options = {}) {
8163
+ if (target?.kind !== "issue") {
8164
+ return listRepositoryIssues(octokit, repository, state, options);
8165
+ }
8166
+ if (target.githubIssueNumber === void 0) {
8167
+ if (options.onProgress) {
8168
+ await options.onProgress({
8169
+ loadedIssueCount: 0
8170
+ });
8171
+ }
8172
+ return [];
8173
+ }
8174
+ const response = await octokit.rest.issues.get({
8175
+ owner: repository.owner,
8176
+ repo: repository.repo,
8177
+ issue_number: target.githubIssueNumber,
8178
+ headers: {
8179
+ accept: "application/vnd.github+json",
8180
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
8181
+ }
8182
+ });
8183
+ const issue = response.data;
8184
+ if ("pull_request" in issue) {
8185
+ return [];
8186
+ }
8187
+ const normalizedIssue = normalizeGitHubIssueRecord(issue);
8188
+ const issues = state === "open" && normalizedIssue.state !== "open" ? [] : [normalizedIssue];
8189
+ if (options.onProgress) {
8190
+ await options.onProgress({
8191
+ loadedIssueCount: issues.length
8192
+ });
8193
+ }
8194
+ return issues;
8195
+ }
8043
8196
  async function listRepositoryIssuesForImport(allIssues) {
8044
8197
  return sortIssuesForImport(allIssues.filter((issue) => issue.state === "open"));
8045
8198
  }
@@ -12301,10 +12454,11 @@ async function performSync(ctx, trigger, options = {}) {
12301
12454
  updateSyncFailureContext(failureContext, {
12302
12455
  phase: "listing_github_issues"
12303
12456
  });
12304
- const allIssues = await listRepositoryIssues(
12457
+ const allIssues = await listRepositoryIssuesForSyncTarget(
12305
12458
  octokit,
12306
12459
  repository,
12307
12460
  shouldLoadClosedIssues ? "all" : "open",
12461
+ options.target,
12308
12462
  {
12309
12463
  onProgress: async (progress) => {
12310
12464
  currentProgress = {
@@ -12427,7 +12581,7 @@ async function performSync(ctx, trigger, options = {}) {
12427
12581
  };
12428
12582
  await persistRunningProgress(true);
12429
12583
  try {
12430
- const warmedLinkedPullRequests = await loadLinkedPullRequestsForOpenIssues(octokit, repository);
12584
+ const warmedLinkedPullRequests = options.target?.kind === "issue" ? /* @__PURE__ */ new Map() : await loadLinkedPullRequestsForOpenIssues(octokit, repository);
12431
12585
  for (const [issueNumber, linkedPullRequests] of warmedLinkedPullRequests.entries()) {
12432
12586
  linkedPullRequestsByIssueNumber.set(issueNumber, linkedPullRequests);
12433
12587
  }
@@ -13024,6 +13178,66 @@ function registerGitHubAgentTools(ctx) {
13024
13178
  );
13025
13179
  })
13026
13180
  );
13181
+ ctx.tools.register(
13182
+ "assign_to_current_user",
13183
+ getGitHubAgentToolDeclaration("assign_to_current_user"),
13184
+ async (params, runCtx) => executeGitHubTool(async () => {
13185
+ const input = getToolInputRecord(params);
13186
+ const target = await resolveGitHubIssueToolTarget(ctx, runCtx, input);
13187
+ const octokit = await createGitHubToolOctokit(ctx, runCtx.companyId);
13188
+ const [currentResponse, authenticatedUserResponse] = await Promise.all([
13189
+ octokit.rest.issues.get({
13190
+ owner: target.repository.owner,
13191
+ repo: target.repository.repo,
13192
+ issue_number: target.issueNumber,
13193
+ headers: {
13194
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
13195
+ }
13196
+ }),
13197
+ octokit.rest.users.getAuthenticated({
13198
+ headers: {
13199
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
13200
+ }
13201
+ })
13202
+ ]);
13203
+ const assignedLogin = normalizeOptionalToolString(authenticatedUserResponse.data.login);
13204
+ if (!assignedLogin) {
13205
+ throw new Error("GitHub did not return the login for the saved token.");
13206
+ }
13207
+ const currentAssignees = (currentResponse.data.assignees ?? []).map((assignee) => assignee?.login ?? "").filter(Boolean);
13208
+ const nextAssignees = mergeNamedValues(currentAssignees, {
13209
+ setValues: [],
13210
+ addValues: [assignedLogin],
13211
+ removeValues: []
13212
+ });
13213
+ const alreadyAssigned = nextAssignees.length === currentAssignees.length && nextAssignees.every((assignee, index) => assignee.toLowerCase() === currentAssignees[index]?.toLowerCase());
13214
+ const updatedResponse = alreadyAssigned ? currentResponse : await octokit.rest.issues.update({
13215
+ owner: target.repository.owner,
13216
+ repo: target.repository.repo,
13217
+ issue_number: target.issueNumber,
13218
+ assignees: nextAssignees,
13219
+ headers: {
13220
+ "X-GitHub-Api-Version": GITHUB_API_VERSION
13221
+ }
13222
+ });
13223
+ const updatedIssue = normalizeGitHubIssueRecord(updatedResponse.data);
13224
+ return buildToolSuccessResult(
13225
+ alreadyAssigned ? `GitHub issue #${updatedIssue.number} in ${formatRepositoryLabel(target.repository)} was already assigned to ${assignedLogin}.` : `Assigned GitHub issue #${updatedIssue.number} in ${formatRepositoryLabel(target.repository)} to ${assignedLogin}.`,
13226
+ {
13227
+ repository: target.repository.url,
13228
+ assignedLogin,
13229
+ issue: {
13230
+ number: updatedIssue.number,
13231
+ title: updatedIssue.title,
13232
+ url: updatedIssue.htmlUrl,
13233
+ state: updatedIssue.state,
13234
+ labels: normalizeGitHubIssueLabels(updatedResponse.data.labels),
13235
+ assignees: (updatedResponse.data.assignees ?? []).map((assignee) => assignee?.login ?? "").filter(Boolean)
13236
+ }
13237
+ }
13238
+ );
13239
+ })
13240
+ );
13027
13241
  ctx.tools.register(
13028
13242
  "add_issue_comment",
13029
13243
  getGitHubAgentToolDeclaration("add_issue_comment"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
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.427.0",
44
+ "@paperclipai/plugin-sdk": "^2026.428.0",
45
45
  "react": "^19.2.5",
46
46
  "react-markdown": "^10.1.0",
47
47
  "rehype-raw": "^7.0.0",