paperclip-github-plugin 0.3.4 → 0.3.5

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
@@ -15,6 +15,10 @@ var repositoryProperty = {
15
15
  type: "string",
16
16
  description: "GitHub repository as owner/repo or https://github.com/owner/repo. Omit when the current Paperclip project has exactly one mapped repository."
17
17
  };
18
+ var organizationProperty = {
19
+ type: "string",
20
+ description: "GitHub organization login that owns the Projects."
21
+ };
18
22
  var paperclipIssueIdProperty = {
19
23
  type: "string",
20
24
  description: "Paperclip issue id used to infer the linked GitHub issue and repository when available."
@@ -29,6 +33,15 @@ var pullRequestNumberProperty = {
29
33
  minimum: 1,
30
34
  description: "GitHub pull request number."
31
35
  };
36
+ var projectIdProperty = {
37
+ type: "string",
38
+ description: "GitHub ProjectV2 node id. You can use the id returned by list_organization_projects."
39
+ };
40
+ var projectNumberProperty = {
41
+ type: "integer",
42
+ minimum: 1,
43
+ description: "GitHub organization project number. Requires organization when projectId is not provided."
44
+ };
32
45
  var llmModelProperty = {
33
46
  type: "string",
34
47
  description: "Exact LLM name used to draft the comment. Required so the plugin can append the mandatory AI-authorship footer."
@@ -53,6 +66,16 @@ var pullRequestTargetSchema = {
53
66
  }
54
67
  ]
55
68
  };
69
+ var organizationProjectTargetSchema = {
70
+ anyOf: [
71
+ {
72
+ required: ["projectId"]
73
+ },
74
+ {
75
+ required: ["organization", "projectNumber"]
76
+ }
77
+ ]
78
+ };
56
79
  var GITHUB_AGENT_TOOLS = [
57
80
  {
58
81
  name: "search_repository_items",
@@ -436,6 +459,51 @@ var GITHUB_AGENT_TOOLS = [
436
459
  }
437
460
  ]
438
461
  }
462
+ },
463
+ {
464
+ name: "list_organization_projects",
465
+ displayName: "List Organization Projects",
466
+ description: "List GitHub organization-level Projects so an agent can choose where to associate pull requests.",
467
+ parametersSchema: {
468
+ type: "object",
469
+ additionalProperties: false,
470
+ required: ["organization"],
471
+ properties: {
472
+ organization: organizationProperty,
473
+ includeClosed: {
474
+ type: "boolean",
475
+ description: "Include closed Projects in the results. Defaults to false."
476
+ },
477
+ query: {
478
+ type: "string",
479
+ description: "Optional free-text filter matched against project titles and descriptions after loading the organization projects."
480
+ },
481
+ limit: {
482
+ type: "integer",
483
+ minimum: 1,
484
+ maximum: 100,
485
+ description: "Maximum number of projects to return."
486
+ }
487
+ }
488
+ }
489
+ },
490
+ {
491
+ name: "add_pull_request_to_project",
492
+ displayName: "Add Pull Request To Project",
493
+ description: "Associate a GitHub pull request with an organization-level GitHub Project.",
494
+ parametersSchema: {
495
+ type: "object",
496
+ additionalProperties: false,
497
+ allOf: [pullRequestTargetSchema, organizationProjectTargetSchema],
498
+ properties: {
499
+ repository: repositoryProperty,
500
+ pullRequestNumber: pullRequestNumberProperty,
501
+ paperclipIssueId: paperclipIssueIdProperty,
502
+ projectId: projectIdProperty,
503
+ organization: organizationProperty,
504
+ projectNumber: projectNumberProperty
505
+ }
506
+ }
439
507
  }
440
508
  ];
441
509
  function getGitHubAgentToolDeclaration(name) {
@@ -640,6 +708,7 @@ var FAILED_CHECK_RUN_CONCLUSIONS = /* @__PURE__ */ new Set([
640
708
  var SUCCESSFUL_STATUS_CONTEXT_STATES = /* @__PURE__ */ new Set(["SUCCESS"]);
641
709
  var FAILED_STATUS_CONTEXT_STATES = /* @__PURE__ */ new Set(["ERROR", "FAILURE"]);
642
710
  var PENDING_STATUS_CONTEXT_STATES = /* @__PURE__ */ new Set(["EXPECTED", "PENDING"]);
711
+ var GITHUB_REPOSITORY_MAINTAINER_WARMUP_CONCURRENCY = 4;
643
712
  var GITHUB_REPOSITORY_MAINTAINER_ROLE_NAMES = /* @__PURE__ */ new Set(["admin", "maintain"]);
644
713
  var GITHUB_ISSUE_STATUS_SNAPSHOT_QUERY = `
645
714
  query GitHubIssueStatusSnapshot($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
@@ -1039,6 +1108,114 @@ var GITHUB_PULL_REQUEST_REVIEW_THREADS_DETAILED_QUERY = `
1039
1108
  }
1040
1109
  }
1041
1110
  `;
1111
+ var GITHUB_ORGANIZATION_PROJECTS_QUERY = `
1112
+ query GitHubOrganizationProjects($organization: String!, $after: String, $first: Int!) {
1113
+ organization(login: $organization) {
1114
+ projectsV2(first: $first, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
1115
+ pageInfo {
1116
+ hasNextPage
1117
+ endCursor
1118
+ }
1119
+ nodes {
1120
+ id
1121
+ number
1122
+ title
1123
+ shortDescription
1124
+ url
1125
+ closed
1126
+ updatedAt
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ `;
1132
+ var GITHUB_ORGANIZATION_PROJECT_BY_NUMBER_QUERY = `
1133
+ query GitHubOrganizationProjectByNumber($organization: String!, $projectNumber: Int!) {
1134
+ organization(login: $organization) {
1135
+ projectV2(number: $projectNumber) {
1136
+ id
1137
+ number
1138
+ title
1139
+ url
1140
+ closed
1141
+ owner {
1142
+ __typename
1143
+ ... on Organization {
1144
+ login
1145
+ }
1146
+ ... on User {
1147
+ login
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ `;
1154
+ var GITHUB_PULL_REQUEST_PROJECT_ITEMS_QUERY = `
1155
+ query GitHubPullRequestProjectItems($owner: String!, $repo: String!, $pullRequestNumber: Int!, $after: String) {
1156
+ repository(owner: $owner, name: $repo) {
1157
+ pullRequest(number: $pullRequestNumber) {
1158
+ id
1159
+ number
1160
+ title
1161
+ url
1162
+ projectItems(first: 100, after: $after) {
1163
+ pageInfo {
1164
+ hasNextPage
1165
+ endCursor
1166
+ }
1167
+ nodes {
1168
+ id
1169
+ project {
1170
+ id
1171
+ number
1172
+ title
1173
+ url
1174
+ closed
1175
+ owner {
1176
+ __typename
1177
+ ... on Organization {
1178
+ login
1179
+ }
1180
+ ... on User {
1181
+ login
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+ `;
1191
+ var GITHUB_ADD_PULL_REQUEST_TO_PROJECT_MUTATION = `
1192
+ mutation GitHubAddPullRequestToProject($projectId: ID!, $contentId: ID!) {
1193
+ addProjectV2ItemById(input: {
1194
+ projectId: $projectId
1195
+ contentId: $contentId
1196
+ }) {
1197
+ item {
1198
+ id
1199
+ project {
1200
+ id
1201
+ number
1202
+ title
1203
+ url
1204
+ closed
1205
+ owner {
1206
+ __typename
1207
+ ... on Organization {
1208
+ login
1209
+ }
1210
+ ... on User {
1211
+ login
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ }
1218
+ `;
1042
1219
  var GITHUB_ADD_PULL_REQUEST_REVIEW_THREAD_REPLY_MUTATION = `
1043
1220
  mutation GitHubAddPullRequestReviewThreadReply($pullRequestReviewThreadId: ID!, $body: String!) {
1044
1221
  addPullRequestReviewThreadReply(input: {
@@ -3615,7 +3792,7 @@ function normalizePaperclipIssueStatus(value) {
3615
3792
  return PAPERCLIP_ISSUE_STATUSES.includes(value) ? value : void 0;
3616
3793
  }
3617
3794
  function describeGitHubStatusTransitionReason(params) {
3618
- const { snapshot, previousCommentCount, hasTrustedNewComment } = params;
3795
+ const { snapshot, previousCommentCount, hasTrustedNewComment, maintainerAuthoredImportedIssue } = params;
3619
3796
  if (snapshot.state === "closed") {
3620
3797
  switch (snapshot.stateReason) {
3621
3798
  case "duplicate":
@@ -3631,6 +3808,9 @@ function describeGitHubStatusTransitionReason(params) {
3631
3808
  return "a new GitHub comment from the issue author or a repository maintainer was added";
3632
3809
  }
3633
3810
  if (snapshot.linkedPullRequests.length === 0) {
3811
+ if (maintainerAuthoredImportedIssue) {
3812
+ return "the GitHub issue is open with no linked pull requests and was created by a repository maintainer";
3813
+ }
3634
3814
  return "the GitHub issue is open with no linked pull requests";
3635
3815
  }
3636
3816
  const linkedPullRequestSubject = snapshot.linkedPullRequests.length === 1 ? "the linked pull request" : "linked pull requests";
@@ -3669,11 +3849,20 @@ function buildStatusTransitionCommentAnnotation(params) {
3669
3849
  };
3670
3850
  }
3671
3851
  function buildPaperclipIssueStatusTransitionComment(params) {
3672
- const { previousStatus, nextStatus, repository, snapshot, previousCommentCount, hasTrustedNewComment } = params;
3852
+ const {
3853
+ previousStatus,
3854
+ nextStatus,
3855
+ repository,
3856
+ snapshot,
3857
+ previousCommentCount,
3858
+ hasTrustedNewComment,
3859
+ maintainerAuthoredImportedIssue
3860
+ } = params;
3673
3861
  const reason = describeGitHubStatusTransitionReason({
3674
3862
  snapshot,
3675
3863
  previousCommentCount,
3676
- hasTrustedNewComment
3864
+ hasTrustedNewComment,
3865
+ maintainerAuthoredImportedIssue
3677
3866
  });
3678
3867
  return {
3679
3868
  body: `GitHub Sync updated the status from \`${formatPaperclipIssueStatus(previousStatus)}\` to \`${formatPaperclipIssueStatus(nextStatus)}\` because ${reason}.`,
@@ -3693,7 +3882,8 @@ function resolvePaperclipIssueStatus(params) {
3693
3882
  previousCommentCount,
3694
3883
  hasTrustedNewComment,
3695
3884
  wasImportedThisRun,
3696
- defaultImportedStatus
3885
+ defaultImportedStatus,
3886
+ maintainerAuthoredImportedIssue
3697
3887
  } = params;
3698
3888
  if (snapshot.state === "closed") {
3699
3889
  return snapshot.stateReason === "duplicate" || snapshot.stateReason === "not_planned" ? "cancelled" : "done";
@@ -3709,7 +3899,7 @@ function resolvePaperclipIssueStatus(params) {
3709
3899
  return resolvePaperclipStatusFromLinkedPullRequests(snapshot.linkedPullRequests);
3710
3900
  }
3711
3901
  if (wasImportedThisRun) {
3712
- return defaultImportedStatus;
3902
+ return maintainerAuthoredImportedIssue ? "todo" : defaultImportedStatus;
3713
3903
  }
3714
3904
  if (currentStatus === "done" || currentStatus === "cancelled") {
3715
3905
  return "todo";
@@ -4116,6 +4306,40 @@ async function hasTrustedNewGitHubIssueComment(params) {
4116
4306
  }
4117
4307
  return false;
4118
4308
  }
4309
+ async function isMaintainerAuthoredGitHubIssue(params) {
4310
+ const authorLogin = normalizeGitHubUserLogin(params.githubIssue.authorLogin);
4311
+ if (!authorLogin) {
4312
+ return false;
4313
+ }
4314
+ return isGitHubUserRepositoryMaintainer(
4315
+ params.octokit,
4316
+ params.repository,
4317
+ authorLogin,
4318
+ params.maintainerCache
4319
+ );
4320
+ }
4321
+ async function warmGitHubRepositoryMaintainerCache(params) {
4322
+ const uniqueAuthorLogins = [...new Set(
4323
+ params.githubIssues.map((issue) => normalizeGitHubUserLogin(issue.authorLogin)).filter((authorLogin) => Boolean(authorLogin))
4324
+ )].filter((authorLogin) => !params.maintainerCache.has(buildGitHubRepositoryActorCacheKey(params.repository, authorLogin)));
4325
+ if (uniqueAuthorLogins.length === 0) {
4326
+ return;
4327
+ }
4328
+ await mapWithConcurrency(uniqueAuthorLogins, GITHUB_REPOSITORY_MAINTAINER_WARMUP_CONCURRENCY, async (authorLogin) => {
4329
+ try {
4330
+ await isGitHubUserRepositoryMaintainer(
4331
+ params.octokit,
4332
+ params.repository,
4333
+ authorLogin,
4334
+ params.maintainerCache
4335
+ );
4336
+ } catch (error) {
4337
+ if (isGitHubRateLimitError(error)) {
4338
+ throw error;
4339
+ }
4340
+ }
4341
+ });
4342
+ }
4119
4343
  function parseGitHubIssueHtmlUrl(value) {
4120
4344
  try {
4121
4345
  const url = new URL(value.trim());
@@ -5910,13 +6134,21 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
5910
6134
  currentCommentCount: snapshot.commentCount,
5911
6135
  maintainerCache: repositoryMaintainerCache
5912
6136
  });
6137
+ const wasImportedThisRun = createdIssueIds.has(importedIssue.githubIssueId);
6138
+ const maintainerAuthoredImportedIssue = wasImportedThisRun && advancedSettings.defaultStatus !== "todo" && snapshot.state === "open" && snapshot.linkedPullRequests.length === 0 ? await isMaintainerAuthoredGitHubIssue({
6139
+ octokit,
6140
+ repository,
6141
+ githubIssue,
6142
+ maintainerCache: repositoryMaintainerCache
6143
+ }) : false;
5913
6144
  const nextStatus = resolvePaperclipIssueStatus({
5914
6145
  currentStatus: paperclipIssue.status,
5915
6146
  snapshot,
5916
6147
  previousCommentCount,
5917
6148
  hasTrustedNewComment,
5918
- wasImportedThisRun: createdIssueIds.has(importedIssue.githubIssueId),
5919
- defaultImportedStatus: advancedSettings.defaultStatus
6149
+ wasImportedThisRun,
6150
+ defaultImportedStatus: advancedSettings.defaultStatus,
6151
+ maintainerAuthoredImportedIssue
5920
6152
  });
5921
6153
  importedIssue.githubIssueNumber = githubIssue.number;
5922
6154
  importedIssue.lastSeenCommentCount = snapshot.commentCount;
@@ -5929,7 +6161,8 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
5929
6161
  repository,
5930
6162
  snapshot,
5931
6163
  previousCommentCount,
5932
- hasTrustedNewComment
6164
+ hasTrustedNewComment,
6165
+ maintainerAuthoredImportedIssue
5933
6166
  });
5934
6167
  updateSyncFailureContext(syncFailureContext, {
5935
6168
  phase: "updating_paperclip_status",
@@ -6496,6 +6729,170 @@ async function resolveGitHubPullRequestToolTarget(ctx, runCtx, input) {
6496
6729
  pullRequestNumber
6497
6730
  };
6498
6731
  }
6732
+ function normalizeGitHubProjectRecord(project, fallbackOwnerLogin) {
6733
+ const id = normalizeOptionalString2(project?.id);
6734
+ const title = normalizeOptionalString2(project?.title);
6735
+ const url = normalizeOptionalString2(project?.url);
6736
+ const number = typeof project?.number === "number" && Number.isInteger(project.number) && project.number > 0 ? Math.floor(project.number) : null;
6737
+ if (!id || !title || !url || number === null) {
6738
+ return null;
6739
+ }
6740
+ const shortDescription = normalizeOptionalString2(project?.shortDescription);
6741
+ const updatedAt = normalizeOptionalString2(project?.updatedAt);
6742
+ const ownerLogin = normalizeOptionalString2(project?.owner?.login) ?? fallbackOwnerLogin;
6743
+ return {
6744
+ id,
6745
+ number,
6746
+ title,
6747
+ url,
6748
+ closed: project?.closed === true,
6749
+ ...shortDescription ? { shortDescription } : {},
6750
+ ...updatedAt ? { updatedAt } : {},
6751
+ ...ownerLogin ? { ownerLogin } : {}
6752
+ };
6753
+ }
6754
+ function buildGitHubProjectToolData(project, options) {
6755
+ return {
6756
+ id: project.id,
6757
+ number: project.number,
6758
+ title: project.title,
6759
+ ...project.shortDescription ? { shortDescription: project.shortDescription } : {},
6760
+ url: project.url,
6761
+ closed: project.closed,
6762
+ ...project.updatedAt ? { updatedAt: project.updatedAt } : {},
6763
+ ...options?.includeOwnerLogin && project.ownerLogin ? { ownerLogin: project.ownerLogin } : {}
6764
+ };
6765
+ }
6766
+ function matchesGitHubProjectFilter(project, query) {
6767
+ const normalizedQuery = normalizeOptionalString2(query)?.toLowerCase();
6768
+ if (!normalizedQuery) {
6769
+ return true;
6770
+ }
6771
+ return [project.title, project.shortDescription].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes(normalizedQuery));
6772
+ }
6773
+ async function listGitHubOrganizationProjects(octokit, organization, options) {
6774
+ const normalizedOrganization = normalizeOptionalString2(organization);
6775
+ if (!normalizedOrganization) {
6776
+ throw new Error("organization is required.");
6777
+ }
6778
+ const includeClosed = options?.includeClosed === true;
6779
+ const limit = Math.min(normalizeToolPositiveInteger(options?.limit) ?? 20, 100);
6780
+ const projects = [];
6781
+ let after;
6782
+ do {
6783
+ const response = await octokit.graphql(
6784
+ GITHUB_ORGANIZATION_PROJECTS_QUERY,
6785
+ {
6786
+ organization: normalizedOrganization,
6787
+ first: Math.min(50, Math.max(1, limit)),
6788
+ after
6789
+ }
6790
+ );
6791
+ if (!response.organization) {
6792
+ throw new Error(`GitHub organization ${normalizedOrganization} was not found or is not visible to the configured token.`);
6793
+ }
6794
+ const connection = response.organization.projectsV2;
6795
+ for (const node of connection?.nodes ?? []) {
6796
+ const project = normalizeGitHubProjectRecord(node, normalizedOrganization);
6797
+ if (!project) {
6798
+ continue;
6799
+ }
6800
+ if (!includeClosed && project.closed) {
6801
+ continue;
6802
+ }
6803
+ if (!matchesGitHubProjectFilter(project, options?.query)) {
6804
+ continue;
6805
+ }
6806
+ projects.push(project);
6807
+ if (projects.length >= limit) {
6808
+ return projects;
6809
+ }
6810
+ }
6811
+ after = getPageCursor(connection?.pageInfo);
6812
+ } while (after);
6813
+ return projects;
6814
+ }
6815
+ async function resolveGitHubProjectToolTarget(octokit, input) {
6816
+ const explicitProjectId = normalizeOptionalString2(input.projectId);
6817
+ if (explicitProjectId) {
6818
+ return {
6819
+ projectId: explicitProjectId
6820
+ };
6821
+ }
6822
+ const organization = normalizeOptionalToolString(input.organization);
6823
+ const projectNumber = normalizeToolPositiveInteger(input.projectNumber);
6824
+ if (!organization || projectNumber === void 0) {
6825
+ throw new Error("Provide either projectId or both organization and projectNumber.");
6826
+ }
6827
+ const response = await octokit.graphql(
6828
+ GITHUB_ORGANIZATION_PROJECT_BY_NUMBER_QUERY,
6829
+ {
6830
+ organization,
6831
+ projectNumber
6832
+ }
6833
+ );
6834
+ const project = normalizeGitHubProjectRecord(response.organization?.projectV2, organization);
6835
+ if (!project) {
6836
+ throw new Error(`GitHub organization project #${projectNumber} was not found in ${organization}.`);
6837
+ }
6838
+ return {
6839
+ projectId: project.id,
6840
+ project
6841
+ };
6842
+ }
6843
+ async function getGitHubPullRequestProjectItems(octokit, repository, pullRequestNumber) {
6844
+ const projectItems = [];
6845
+ let after;
6846
+ let pullRequestId;
6847
+ let pullRequestTitle;
6848
+ let pullRequestUrl;
6849
+ do {
6850
+ const response = await octokit.graphql(
6851
+ GITHUB_PULL_REQUEST_PROJECT_ITEMS_QUERY,
6852
+ {
6853
+ owner: repository.owner,
6854
+ repo: repository.repo,
6855
+ pullRequestNumber,
6856
+ after
6857
+ }
6858
+ );
6859
+ const pullRequest = response.repository?.pullRequest;
6860
+ const currentPullRequestId = normalizeOptionalString2(pullRequest?.id);
6861
+ const currentPullRequestTitle = normalizeOptionalString2(pullRequest?.title);
6862
+ const currentPullRequestUrl = normalizeOptionalString2(pullRequest?.url);
6863
+ if (!currentPullRequestId || !currentPullRequestTitle || !currentPullRequestUrl) {
6864
+ throw new Error(`GitHub pull request #${pullRequestNumber} was not found in ${formatRepositoryLabel(repository)}.`);
6865
+ }
6866
+ pullRequestId ??= currentPullRequestId;
6867
+ pullRequestTitle ??= currentPullRequestTitle;
6868
+ pullRequestUrl ??= currentPullRequestUrl;
6869
+ const connection = response.repository?.pullRequest?.projectItems;
6870
+ for (const node of connection?.nodes ?? []) {
6871
+ const itemId = normalizeOptionalString2(node?.id);
6872
+ const project = normalizeGitHubProjectRecord(node?.project);
6873
+ if (!itemId || !project) {
6874
+ continue;
6875
+ }
6876
+ projectItems.push({
6877
+ id: itemId,
6878
+ project
6879
+ });
6880
+ }
6881
+ after = getPageCursor(connection?.pageInfo);
6882
+ } while (after);
6883
+ if (!pullRequestId || !pullRequestTitle || !pullRequestUrl) {
6884
+ throw new Error(`GitHub pull request #${pullRequestNumber} was not found in ${formatRepositoryLabel(repository)}.`);
6885
+ }
6886
+ return {
6887
+ pullRequestId,
6888
+ pullRequest: {
6889
+ number: pullRequestNumber,
6890
+ title: pullRequestTitle,
6891
+ url: pullRequestUrl
6892
+ },
6893
+ projectItems
6894
+ };
6895
+ }
6499
6896
  function formatAiAuthorshipFooter(llmModel) {
6500
6897
  return `
6501
6898
 
@@ -9144,6 +9541,22 @@ async function performSync(ctx, trigger, options = {}) {
9144
9541
  const importedIssuesForSynchronization = [...importRegistryByIssueId.values()].filter(
9145
9542
  (importedIssue) => allIssuesById.has(importedIssue.githubIssueId) && doesImportedIssueMatchTarget(importedIssue, options.target)
9146
9543
  );
9544
+ const newlyImportedIssuesForMaintainerWarmup = advancedSettings.defaultStatus === "todo" ? [] : importedIssuesForSynchronization.filter((importedIssue) => createdIssueIds.has(importedIssue.githubIssueId)).map((importedIssue) => allIssuesById.get(importedIssue.githubIssueId)).filter(
9545
+ (githubIssue) => githubIssue !== void 0 && githubIssue.state === "open"
9546
+ ).filter(
9547
+ (githubIssue) => !(linkedPullRequestsByIssueNumber.get(githubIssue.number) ?? []).some(
9548
+ (pullRequest) => pullRequest.state === "OPEN"
9549
+ )
9550
+ );
9551
+ if (newlyImportedIssuesForMaintainerWarmup.length > 0) {
9552
+ await warmGitHubRepositoryMaintainerCache({
9553
+ octokit,
9554
+ repository,
9555
+ githubIssues: newlyImportedIssuesForMaintainerWarmup,
9556
+ maintainerCache: repositoryMaintainerCache
9557
+ });
9558
+ await throwIfSyncCancelled();
9559
+ }
9147
9560
  currentProgress = {
9148
9561
  phase: "syncing",
9149
9562
  totalRepositoryCount: mappings.length,
@@ -10043,6 +10456,91 @@ function registerGitHubAgentTools(ctx) {
10043
10456
  );
10044
10457
  })
10045
10458
  );
10459
+ ctx.tools.register(
10460
+ "list_organization_projects",
10461
+ getGitHubAgentToolDeclaration("list_organization_projects"),
10462
+ async (params) => executeGitHubTool(async () => {
10463
+ const input = getToolInputRecord(params);
10464
+ const organization = normalizeOptionalToolString(input.organization);
10465
+ if (!organization) {
10466
+ throw new Error("organization is required.");
10467
+ }
10468
+ const octokit = await createGitHubToolOctokit(ctx);
10469
+ const projects = await listGitHubOrganizationProjects(octokit, organization, {
10470
+ includeClosed: input.includeClosed === true,
10471
+ query: normalizeOptionalToolString(input.query),
10472
+ limit: normalizeToolPositiveInteger(input.limit)
10473
+ });
10474
+ return buildToolSuccessResult(
10475
+ `Loaded ${projects.length} GitHub organization ${projects.length === 1 ? "project" : "projects"} from ${organization}.`,
10476
+ {
10477
+ organization,
10478
+ projects: projects.map((project) => buildGitHubProjectToolData(project))
10479
+ }
10480
+ );
10481
+ })
10482
+ );
10483
+ ctx.tools.register(
10484
+ "add_pull_request_to_project",
10485
+ getGitHubAgentToolDeclaration("add_pull_request_to_project"),
10486
+ async (params, runCtx) => executeGitHubTool(async () => {
10487
+ const input = getToolInputRecord(params);
10488
+ const target = await resolveGitHubPullRequestToolTarget(ctx, runCtx, input);
10489
+ const octokit = await createGitHubToolOctokit(ctx);
10490
+ const projectTarget = await resolveGitHubProjectToolTarget(octokit, input);
10491
+ const pullRequest = await getGitHubPullRequestProjectItems(
10492
+ octokit,
10493
+ target.repository,
10494
+ target.pullRequestNumber
10495
+ );
10496
+ const existingProjectItem = pullRequest.projectItems.find((item) => item.project.id === projectTarget.projectId);
10497
+ if (existingProjectItem) {
10498
+ return buildToolSuccessResult(
10499
+ `Pull request #${target.pullRequestNumber} is already associated with GitHub project #${existingProjectItem.project.number}.`,
10500
+ {
10501
+ repository: target.repository.url,
10502
+ pullRequest: pullRequest.pullRequest,
10503
+ project: buildGitHubProjectToolData(existingProjectItem.project, {
10504
+ includeOwnerLogin: true
10505
+ }),
10506
+ projectItem: {
10507
+ id: existingProjectItem.id
10508
+ },
10509
+ alreadyAssociated: true
10510
+ }
10511
+ );
10512
+ }
10513
+ const response = await octokit.graphql(
10514
+ GITHUB_ADD_PULL_REQUEST_TO_PROJECT_MUTATION,
10515
+ {
10516
+ projectId: projectTarget.projectId,
10517
+ contentId: pullRequest.pullRequestId
10518
+ }
10519
+ );
10520
+ const projectItemId = normalizeOptionalString2(response.addProjectV2ItemById?.item?.id);
10521
+ const project = normalizeGitHubProjectRecord(
10522
+ response.addProjectV2ItemById?.item?.project,
10523
+ projectTarget.project?.ownerLogin
10524
+ ) ?? projectTarget.project;
10525
+ if (!projectItemId || !project) {
10526
+ throw new Error("GitHub did not return the created project item.");
10527
+ }
10528
+ return buildToolSuccessResult(
10529
+ `Added pull request #${target.pullRequestNumber} to GitHub project #${project.number}.`,
10530
+ {
10531
+ repository: target.repository.url,
10532
+ pullRequest: pullRequest.pullRequest,
10533
+ project: buildGitHubProjectToolData(project, {
10534
+ includeOwnerLogin: true
10535
+ }),
10536
+ projectItem: {
10537
+ id: projectItemId
10538
+ },
10539
+ alreadyAssociated: false
10540
+ }
10541
+ );
10542
+ })
10543
+ );
10046
10544
  }
10047
10545
  function shouldStartWorkerHost(moduleUrl, entry = process.argv[1]) {
10048
10546
  if (typeof entry !== "string" || !entry.trim()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "dev": "node ./scripts/build.mjs --watch",
35
35
  "dev:ui": "paperclip-plugin-dev-server --root . --ui-dir dist/ui --port 4177",
36
36
  "prepack": "pnpm build",
37
- "test": "tsx --test tests/plugin.spec.ts",
37
+ "test": "node --test tests/build-script.spec.mjs && tsx --test tests/plugin.spec.ts",
38
38
  "test:e2e": "pnpm build && pnpm exec playwright install chromium && node ./scripts/e2e/run-paperclip-smoke.mjs",
39
39
  "typecheck": "tsc --noEmit",
40
40
  "verify:manual": "pnpm build && pnpm exec playwright install chromium && node ./scripts/e2e/manual-paperclip-verify.mjs"