opencode-gitlab-plugin 2.5.0 → 2.6.1

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.
@@ -28790,7 +28790,13 @@ var GitLabApiClient = class {
28790
28790
  }
28791
28791
  return projectId;
28792
28792
  }
28793
- async fetch(method, path2, body) {
28793
+ /**
28794
+ * Core HTTP request used by all REST helpers.
28795
+ *
28796
+ * Returns the raw `Response` after validating the status code so callers can
28797
+ * inspect headers (e.g. for pagination) before parsing the body.
28798
+ */
28799
+ async rawFetch(method, path2, body) {
28794
28800
  const url2 = `${this.instanceUrl}/api/v4${path2}`;
28795
28801
  const response = await fetch(url2, {
28796
28802
  method,
@@ -28801,12 +28807,53 @@ var GitLabApiClient = class {
28801
28807
  const errorText = await response.text();
28802
28808
  throw new Error(`GitLab API error ${response.status}: ${errorText}`);
28803
28809
  }
28810
+ return response;
28811
+ }
28812
+ async fetch(method, path2, body) {
28813
+ const response = await this.rawFetch(method, path2, body);
28804
28814
  const text = await response.text();
28805
28815
  if (!text) {
28806
28816
  return {};
28807
28817
  }
28808
28818
  return JSON.parse(text);
28809
28819
  }
28820
+ /**
28821
+ * Parse a numeric pagination header, guarding against empty strings.
28822
+ *
28823
+ * GitLab returns `""` (rather than omitting the header) for `X-Next-Page` and
28824
+ * `X-Prev-Page` when there is no next/prev page, and bare `parseInt` without
28825
+ * a radix on an empty string yields `NaN`. Treat empty/missing as `undefined`.
28826
+ */
28827
+ parsePaginationHeader(response, name) {
28828
+ const raw = response.headers.get(name);
28829
+ if (raw == null || raw === "") return void 0;
28830
+ const value = parseInt(raw, 10);
28831
+ return Number.isNaN(value) ? void 0 : value;
28832
+ }
28833
+ /**
28834
+ * Pagination info extracted from GitLab API response headers
28835
+ */
28836
+ extractPaginationInfo(response) {
28837
+ return {
28838
+ total: this.parsePaginationHeader(response, "X-Total"),
28839
+ totalPages: this.parsePaginationHeader(response, "X-Total-Pages"),
28840
+ page: this.parsePaginationHeader(response, "X-Page"),
28841
+ perPage: this.parsePaginationHeader(response, "X-Per-Page"),
28842
+ nextPage: this.parsePaginationHeader(response, "X-Next-Page"),
28843
+ prevPage: this.parsePaginationHeader(response, "X-Prev-Page")
28844
+ };
28845
+ }
28846
+ /**
28847
+ * Fetch with pagination info from response headers.
28848
+ * Use this for list endpoints where knowing if more data exists is important.
28849
+ */
28850
+ async fetchWithPagination(method, path2, body) {
28851
+ const response = await this.rawFetch(method, path2, body);
28852
+ const text = await response.text();
28853
+ const data = text ? JSON.parse(text) : [];
28854
+ const pagination = this.extractPaginationInfo(response);
28855
+ return { data, pagination };
28856
+ }
28810
28857
  async fetchText(method, path2) {
28811
28858
  const url2 = `${this.instanceUrl}/api/v4${path2}`;
28812
28859
  const response = await fetch(url2, {
@@ -29058,7 +29105,7 @@ var RESOLVE_DISCUSSION_MUTATION = `
29058
29105
  }
29059
29106
  }
29060
29107
  `;
29061
- var MergeRequestsClient = class extends GitLabApiClient {
29108
+ var MergeRequestsClient = class _MergeRequestsClient extends GitLabApiClient {
29062
29109
  async getMergeRequest(projectId, mrIid, includeChanges) {
29063
29110
  const encodedProject = this.encodeProjectId(projectId);
29064
29111
  let path2 = `/projects/${encodedProject}/merge_requests/${mrIid}`;
@@ -29070,6 +29117,7 @@ var MergeRequestsClient = class extends GitLabApiClient {
29070
29117
  async listMergeRequests(options) {
29071
29118
  const params = new URLSearchParams();
29072
29119
  params.set("per_page", String(options.limit || 20));
29120
+ if (options.page) params.set("page", String(options.page));
29073
29121
  if (options.state) params.set("state", options.state);
29074
29122
  if (options.scope) params.set("scope", options.scope);
29075
29123
  if (options.search) params.set("search", options.search);
@@ -29081,7 +29129,7 @@ var MergeRequestsClient = class extends GitLabApiClient {
29081
29129
  } else {
29082
29130
  path2 = `/merge_requests?${params}`;
29083
29131
  }
29084
- return this.fetch("GET", path2);
29132
+ return this.fetchWithPagination("GET", path2);
29085
29133
  }
29086
29134
  async getMrChanges(projectId, mrIid) {
29087
29135
  const encodedProject = this.encodeProjectId(projectId);
@@ -29279,20 +29327,48 @@ var MergeRequestsClient = class extends GitLabApiClient {
29279
29327
  * Set auto-merge (MWPS) on a merge request using GraphQL API
29280
29328
  * Uses the mergeRequestAccept mutation with a merge strategy
29281
29329
  */
29282
- async setAutoMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS") {
29283
- const result = await this.fetchGraphQL(SET_AUTO_MERGE_MUTATION, {
29284
- projectPath: projectId,
29285
- iid: String(mrIid),
29286
- sha,
29287
- strategy
29288
- });
29289
- if (result.mergeRequestAccept.errors.length > 0) {
29290
- throw new Error(`Failed to set auto-merge: ${result.mergeRequestAccept.errors.join(", ")}`);
29291
- }
29292
- if (!result.mergeRequestAccept.mergeRequest) {
29293
- throw new Error("Failed to set auto-merge: No merge request returned");
29330
+ async setAutoMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS", retries = 3) {
29331
+ let lastError = null;
29332
+ for (let attempt = 0; attempt <= retries; attempt++) {
29333
+ const result = await this.fetchGraphQL(SET_AUTO_MERGE_MUTATION, {
29334
+ projectPath: projectId,
29335
+ iid: String(mrIid),
29336
+ sha,
29337
+ strategy
29338
+ });
29339
+ if (result.mergeRequestAccept.errors.length > 0) {
29340
+ const message = result.mergeRequestAccept.errors.join(", ");
29341
+ lastError = new Error(`Failed to set auto-merge: ${message}`);
29342
+ if (attempt < retries && _MergeRequestsClient.isTransientMergeError(message)) {
29343
+ await _MergeRequestsClient.delay(750 * (attempt + 1));
29344
+ continue;
29345
+ }
29346
+ throw lastError;
29347
+ }
29348
+ if (!result.mergeRequestAccept.mergeRequest) {
29349
+ lastError = new Error("Failed to set auto-merge: No merge request returned");
29350
+ if (attempt < retries) {
29351
+ await _MergeRequestsClient.delay(750 * (attempt + 1));
29352
+ continue;
29353
+ }
29354
+ throw lastError;
29355
+ }
29356
+ return result.mergeRequestAccept.mergeRequest;
29294
29357
  }
29295
- return result.mergeRequestAccept.mergeRequest;
29358
+ throw lastError ?? new Error("Failed to set auto-merge: exhausted retries");
29359
+ }
29360
+ /**
29361
+ * True for GitLab merge errors that are transient because mergeability is
29362
+ * still being (re)computed. Such errors clear on their own within a second
29363
+ * or two, so callers should retry rather than treat them as real failures.
29364
+ */
29365
+ static isTransientMergeError(message) {
29366
+ const m = message.toLowerCase();
29367
+ return m.includes("being checked") || m.includes("being recomputed") || m.includes("cannot be merged") || m.includes("not mergeable") || m.includes("merge failed") || m.includes("try again");
29368
+ }
29369
+ /** Small awaitable delay helper (ms). */
29370
+ static delay(ms) {
29371
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
29296
29372
  }
29297
29373
  /**
29298
29374
  * Approve a merge request
@@ -29340,7 +29416,13 @@ var MergeRequestsClient = class extends GitLabApiClient {
29340
29416
  * Returns detailed context about what happened
29341
29417
  */
29342
29418
  async smartMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS") {
29343
- const mr = await this.getMergeRequest(projectId, mrIid);
29419
+ let mr = await this.getMergeRequest(projectId, mrIid);
29420
+ if (mr.detailed_merge_status === "checking") {
29421
+ for (let attempt = 0; attempt < 5 && mr.detailed_merge_status === "checking"; attempt++) {
29422
+ await _MergeRequestsClient.delay(1e3 * (attempt + 1));
29423
+ mr = await this.getMergeRequest(projectId, mrIid);
29424
+ }
29425
+ }
29344
29426
  const context = {
29345
29427
  state: mr.state,
29346
29428
  detailedMergeStatus: mr.detailed_merge_status,
@@ -29379,14 +29461,13 @@ var MergeRequestsClient = class extends GitLabApiClient {
29379
29461
  };
29380
29462
  }
29381
29463
  }
29382
- const autoMergeStatuses = ["ci_still_running", "not_approved", "checking"];
29464
+ const autoMergeStatuses = ["ci_still_running", "not_approved"];
29383
29465
  if (autoMergeStatuses.includes(mr.detailed_merge_status)) {
29384
29466
  try {
29385
29467
  const result = await this.setAutoMerge(projectId, mrIid, sha, strategy);
29386
29468
  const autoMergeMessages = {
29387
29469
  ci_still_running: "pipeline passes",
29388
- not_approved: "approved",
29389
- checking: "checks complete"
29470
+ not_approved: "approved"
29390
29471
  };
29391
29472
  return {
29392
29473
  action: "auto_merge_enabled",
@@ -29493,6 +29574,7 @@ var IssuesClient = class extends GitLabApiClient {
29493
29574
  async listIssues(options) {
29494
29575
  const params = new URLSearchParams();
29495
29576
  params.set("per_page", String(options.limit || 20));
29577
+ if (options.page) params.set("page", String(options.page));
29496
29578
  if (options.state) params.set("state", options.state);
29497
29579
  if (options.scope) params.set("scope", options.scope);
29498
29580
  if (options.search) params.set("search", options.search);
@@ -29505,7 +29587,7 @@ var IssuesClient = class extends GitLabApiClient {
29505
29587
  } else {
29506
29588
  path2 = `/issues?${params}`;
29507
29589
  }
29508
- return this.fetch("GET", path2);
29590
+ return this.fetchWithPagination("GET", path2);
29509
29591
  }
29510
29592
  /**
29511
29593
  * List notes on an issue using GraphQL API with pagination support
@@ -29744,22 +29826,8 @@ var GET_WORK_ITEM_NOTES_QUERY = `
29744
29826
  }
29745
29827
  `;
29746
29828
  var CREATE_WORK_ITEM_MUTATION = `
29747
- mutation createWorkItem(
29748
- $projectPath: ID!
29749
- $title: String!
29750
- $workItemTypeId: WorkItemsTypeID!
29751
- $description: String
29752
- $labelIds: [LabelID!]
29753
- $assigneeIds: [UserID!]
29754
- ) {
29755
- workItemCreate(input: {
29756
- projectPath: $projectPath
29757
- title: $title
29758
- workItemTypeId: $workItemTypeId
29759
- descriptionWidget: { description: $description }
29760
- labelsWidget: { labelIds: $labelIds }
29761
- assigneesWidget: { assigneeIds: $assigneeIds }
29762
- }) {
29829
+ mutation createWorkItem($input: WorkItemCreateInput!) {
29830
+ workItemCreate(input: $input) {
29763
29831
  workItem {
29764
29832
  ${WORK_ITEM_FIELDS}
29765
29833
  }
@@ -29768,22 +29836,8 @@ var CREATE_WORK_ITEM_MUTATION = `
29768
29836
  }
29769
29837
  `;
29770
29838
  var UPDATE_WORK_ITEM_MUTATION = `
29771
- mutation updateWorkItem(
29772
- $id: WorkItemID!
29773
- $title: String
29774
- $description: String
29775
- $stateEvent: WorkItemStateEvent
29776
- $labelIds: [LabelID!]
29777
- $assigneeIds: [UserID!]
29778
- ) {
29779
- workItemUpdate(input: {
29780
- id: $id
29781
- title: $title
29782
- descriptionWidget: { description: $description }
29783
- stateEvent: $stateEvent
29784
- labelsWidget: { labelIds: $labelIds }
29785
- assigneesWidget: { assigneeIds: $assigneeIds }
29786
- }) {
29839
+ mutation updateWorkItem($input: WorkItemUpdateInput!) {
29840
+ workItemUpdate(input: $input) {
29787
29841
  workItem {
29788
29842
  ${WORK_ITEM_FIELDS}
29789
29843
  }
@@ -29877,14 +29931,21 @@ var WorkItemsClient = class extends GitLabApiClient {
29877
29931
  async createWorkItem(projectId, options) {
29878
29932
  const labelIds = options.labels?.map((l) => toGid("Label", l));
29879
29933
  const assigneeIds = options.assignee_ids?.map((id) => toGid("User", id));
29880
- const result = await this.fetchGraphQL(CREATE_WORK_ITEM_MUTATION, {
29934
+ const input = {
29881
29935
  projectPath: projectId,
29882
29936
  title: options.title,
29883
- workItemTypeId: toGid("WorkItems::Type", options.work_item_type_id),
29884
- description: options.description,
29885
- labelIds: labelIds?.length ? labelIds : void 0,
29886
- assigneeIds: assigneeIds?.length ? assigneeIds : void 0
29887
- });
29937
+ workItemTypeId: toGid("WorkItems::Type", options.work_item_type_id)
29938
+ };
29939
+ if (options.description !== void 0) {
29940
+ input.descriptionWidget = { description: options.description };
29941
+ }
29942
+ if (labelIds?.length) {
29943
+ input.labelsWidget = { labelIds };
29944
+ }
29945
+ if (assigneeIds?.length) {
29946
+ input.assigneesWidget = { assigneeIds };
29947
+ }
29948
+ const result = await this.fetchGraphQL(CREATE_WORK_ITEM_MUTATION, { input });
29888
29949
  if (result.workItemCreate.errors.length > 0) {
29889
29950
  throw new Error(`Failed to create work item: ${result.workItemCreate.errors.join(", ")}`);
29890
29951
  }
@@ -29902,14 +29963,23 @@ var WorkItemsClient = class extends GitLabApiClient {
29902
29963
  const stateEvent = options.state_event ? options.state_event === "close" ? "CLOSE" : "REOPEN" : void 0;
29903
29964
  const labelIds = options.labels?.map((l) => toGid("Label", l));
29904
29965
  const assigneeIds = options.assignee_ids?.map((id) => toGid("User", id));
29905
- const result = await this.fetchGraphQL(UPDATE_WORK_ITEM_MUTATION, {
29906
- id: gid,
29907
- title: options.title,
29908
- description: options.description,
29909
- stateEvent,
29910
- labelIds: labelIds?.length ? labelIds : void 0,
29911
- assigneeIds: assigneeIds?.length ? assigneeIds : void 0
29912
- });
29966
+ const input = { id: gid };
29967
+ if (options.title !== void 0) {
29968
+ input.title = options.title;
29969
+ }
29970
+ if (options.description !== void 0) {
29971
+ input.descriptionWidget = { description: options.description };
29972
+ }
29973
+ if (stateEvent !== void 0) {
29974
+ input.stateEvent = stateEvent;
29975
+ }
29976
+ if (labelIds?.length) {
29977
+ input.labelsWidget = { addLabelIds: labelIds };
29978
+ }
29979
+ if (assigneeIds?.length) {
29980
+ input.assigneesWidget = { assigneeIds };
29981
+ }
29982
+ const result = await this.fetchGraphQL(UPDATE_WORK_ITEM_MUTATION, { input });
29913
29983
  if (result.workItemUpdate.errors.length > 0) {
29914
29984
  throw new Error(`Failed to update work item: ${result.workItemUpdate.errors.join(", ")}`);
29915
29985
  }
@@ -31417,26 +31487,31 @@ Returns: title, description, state, author, assignees, reviewers, labels, diff s
31417
31487
  }),
31418
31488
  gitlab_list_merge_requests: tool({
31419
31489
  description: `List merge requests for a project or search globally.
31420
- Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.`,
31490
+ Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.
31491
+
31492
+ IMPORTANT: Response includes pagination info. If nextPage exists, there are more results.
31493
+ Always paginate until you get an empty response or no nextPage to ensure you have all data.`,
31421
31494
  args: {
31422
31495
  project_id: z.string().optional().describe("The project ID or path. If not provided, searches globally."),
31423
31496
  state: z.enum(["opened", "closed", "merged", "all"]).optional().describe("Filter by MR state (default: opened)"),
31424
31497
  scope: z.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
31425
31498
  search: z.string().optional().describe("Search MRs by title or description"),
31426
31499
  labels: z.string().optional().describe("Comma-separated list of labels to filter by"),
31427
- limit: z.number().optional().describe("Maximum number of results (default: 20)")
31500
+ limit: z.number().optional().describe("Maximum number of results per page (default: 20)"),
31501
+ page: z.number().optional().describe("Page number for pagination (default: 1)")
31428
31502
  },
31429
31503
  execute: async (args, _ctx) => {
31430
31504
  const client = getGitLabClient();
31431
- const mrs = await client.listMergeRequests({
31505
+ const result = await client.listMergeRequests({
31432
31506
  projectId: args.project_id,
31433
31507
  state: args.state,
31434
31508
  scope: args.scope,
31435
31509
  search: args.search,
31436
31510
  labels: args.labels,
31437
- limit: args.limit
31511
+ limit: args.limit,
31512
+ page: args.page
31438
31513
  });
31439
- return JSON.stringify(mrs, null, 2);
31514
+ return JSON.stringify(result, null, 2);
31440
31515
  }
31441
31516
  }),
31442
31517
  gitlab_get_mr_changes: tool({
@@ -31767,7 +31842,10 @@ Returns: title, description, state, author, assignees, labels, milestone, weight
31767
31842
  }),
31768
31843
  gitlab_list_issues: tool({
31769
31844
  description: `List issues for a project or search globally.
31770
- Can filter by state, labels, assignee, milestone.`,
31845
+ Can filter by state, labels, assignee, milestone.
31846
+
31847
+ IMPORTANT: Response includes pagination info. If nextPage exists, there are more results.
31848
+ Always paginate until you get an empty response or no nextPage to ensure you have all data.`,
31771
31849
  args: {
31772
31850
  project_id: z2.string().optional().describe("The project ID or path. If not provided, searches globally."),
31773
31851
  state: z2.enum(["opened", "closed", "all"]).optional().describe("Filter by issue state (default: opened)"),
@@ -31775,20 +31853,22 @@ Can filter by state, labels, assignee, milestone.`,
31775
31853
  search: z2.string().optional().describe("Search issues by title or description"),
31776
31854
  labels: z2.string().optional().describe("Comma-separated list of labels to filter by"),
31777
31855
  milestone: z2.string().optional().describe("Filter by milestone title"),
31778
- limit: z2.number().optional().describe("Maximum number of results (default: 20)")
31856
+ limit: z2.number().optional().describe("Maximum number of results per page (default: 20)"),
31857
+ page: z2.number().optional().describe("Page number for pagination (default: 1)")
31779
31858
  },
31780
31859
  execute: async (args, _ctx) => {
31781
31860
  const client = getGitLabClient();
31782
- const issues = await client.listIssues({
31861
+ const result = await client.listIssues({
31783
31862
  projectId: args.project_id,
31784
31863
  state: args.state,
31785
31864
  scope: args.scope,
31786
31865
  search: args.search,
31787
31866
  labels: args.labels,
31788
31867
  milestone: args.milestone,
31789
- limit: args.limit
31868
+ limit: args.limit,
31869
+ page: args.page
31790
31870
  });
31791
- return JSON.stringify(issues, null, 2);
31871
+ return JSON.stringify(result, null, 2);
31792
31872
  }
31793
31873
  })
31794
31874
  };
@@ -34016,7 +34096,7 @@ async function main() {
34016
34096
  ...auditTools,
34017
34097
  ...awardEmojiTools
34018
34098
  };
34019
- const version2 = true ? "2.5.0" : "0.0.0";
34099
+ const version2 = true ? "2.6.1" : "0.0.0";
34020
34100
  const server = new McpServer({ name: "gitlab", version: version2 });
34021
34101
  adaptToolsToMcp(server, allTools);
34022
34102
  const transport = new StdioServerTransport();