opencode-gitlab-plugin 2.4.0 → 2.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## [2.6.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.5.0...v2.6.0) (2026-05-20)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * **pagination:** expose pagination info in list MR and issue responses ([1c1722e](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/1c1722e62e0e41f00ab5409057bea1e76ec17a8d))
11
+
12
+
13
+ ### ♻️ Code Refactoring
14
+
15
+ * **client:** address review feedback on !23 ([2e94e46](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/2e94e46706807bc8797f10884f71fc56ecf967b9)), closes [23#note_3135645102](https://gitlab.com/vglafirov/23/issues/note_3135645102) [23#note_3135645423](https://gitlab.com/vglafirov/23/issues/note_3135645423) [23#note_3135644923](https://gitlab.com/vglafirov/23/issues/note_3135644923)
16
+
17
+ ## [2.5.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.4.0...v2.5.0) (2026-05-20)
18
+
19
+
20
+ ### ✨ Features
21
+
22
+ * **notes:** add gitlab_update_note tool for editing comments ([54a87e7](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/54a87e70d4d276c077ab25619e1d910d6d709575))
23
+
24
+
25
+ ### 🐛 Bug Fixes
26
+
27
+ * handle ToolResult union type in mcp-adapter and tests ([a91d5dd](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/a91d5ddc5574d1cf9b7dab2d9624cdddeefd158f))
28
+ * **tests:** update orbit.test.ts for ToolResult type ([29fecec](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/29fecec6a8ba436b3d95b720cf67f162c8c62104))
29
+
30
+
31
+ ### ♻️ Code Refactoring
32
+
33
+ * **notes:** address review feedback on !28 ([11e13ba](https://gitlab.com/vglafirov/opencode-gitlab-plugin/commit/11e13ba5a6dafeb77dfb256bde3e9b7b1f5ddd32))
34
+
5
35
  ## [2.4.0](https://gitlab.com/vglafirov/opencode-gitlab-plugin/compare/v2.3.0...v2.4.0) (2026-05-19)
6
36
 
7
37
 
package/dist/index.js CHANGED
@@ -26,7 +26,13 @@ var GitLabApiClient = class {
26
26
  }
27
27
  return projectId;
28
28
  }
29
- async fetch(method, path2, body) {
29
+ /**
30
+ * Core HTTP request used by all REST helpers.
31
+ *
32
+ * Returns the raw `Response` after validating the status code so callers can
33
+ * inspect headers (e.g. for pagination) before parsing the body.
34
+ */
35
+ async rawFetch(method, path2, body) {
30
36
  const url = `${this.instanceUrl}/api/v4${path2}`;
31
37
  const response = await fetch(url, {
32
38
  method,
@@ -37,12 +43,53 @@ var GitLabApiClient = class {
37
43
  const errorText = await response.text();
38
44
  throw new Error(`GitLab API error ${response.status}: ${errorText}`);
39
45
  }
46
+ return response;
47
+ }
48
+ async fetch(method, path2, body) {
49
+ const response = await this.rawFetch(method, path2, body);
40
50
  const text = await response.text();
41
51
  if (!text) {
42
52
  return {};
43
53
  }
44
54
  return JSON.parse(text);
45
55
  }
56
+ /**
57
+ * Parse a numeric pagination header, guarding against empty strings.
58
+ *
59
+ * GitLab returns `""` (rather than omitting the header) for `X-Next-Page` and
60
+ * `X-Prev-Page` when there is no next/prev page, and bare `parseInt` without
61
+ * a radix on an empty string yields `NaN`. Treat empty/missing as `undefined`.
62
+ */
63
+ parsePaginationHeader(response, name) {
64
+ const raw = response.headers.get(name);
65
+ if (raw == null || raw === "") return void 0;
66
+ const value = parseInt(raw, 10);
67
+ return Number.isNaN(value) ? void 0 : value;
68
+ }
69
+ /**
70
+ * Pagination info extracted from GitLab API response headers
71
+ */
72
+ extractPaginationInfo(response) {
73
+ return {
74
+ total: this.parsePaginationHeader(response, "X-Total"),
75
+ totalPages: this.parsePaginationHeader(response, "X-Total-Pages"),
76
+ page: this.parsePaginationHeader(response, "X-Page"),
77
+ perPage: this.parsePaginationHeader(response, "X-Per-Page"),
78
+ nextPage: this.parsePaginationHeader(response, "X-Next-Page"),
79
+ prevPage: this.parsePaginationHeader(response, "X-Prev-Page")
80
+ };
81
+ }
82
+ /**
83
+ * Fetch with pagination info from response headers.
84
+ * Use this for list endpoints where knowing if more data exists is important.
85
+ */
86
+ async fetchWithPagination(method, path2, body) {
87
+ const response = await this.rawFetch(method, path2, body);
88
+ const text = await response.text();
89
+ const data = text ? JSON.parse(text) : [];
90
+ const pagination = this.extractPaginationInfo(response);
91
+ return { data, pagination };
92
+ }
46
93
  async fetchText(method, path2) {
47
94
  const url = `${this.instanceUrl}/api/v4${path2}`;
48
95
  const response = await fetch(url, {
@@ -306,6 +353,7 @@ var MergeRequestsClient = class extends GitLabApiClient {
306
353
  async listMergeRequests(options) {
307
354
  const params = new URLSearchParams();
308
355
  params.set("per_page", String(options.limit || 20));
356
+ if (options.page) params.set("page", String(options.page));
309
357
  if (options.state) params.set("state", options.state);
310
358
  if (options.scope) params.set("scope", options.scope);
311
359
  if (options.search) params.set("search", options.search);
@@ -317,7 +365,7 @@ var MergeRequestsClient = class extends GitLabApiClient {
317
365
  } else {
318
366
  path2 = `/merge_requests?${params}`;
319
367
  }
320
- return this.fetch("GET", path2);
368
+ return this.fetchWithPagination("GET", path2);
321
369
  }
322
370
  async getMrChanges(projectId, mrIid) {
323
371
  const encodedProject = this.encodeProjectId(projectId);
@@ -438,6 +486,14 @@ var MergeRequestsClient = class extends GitLabApiClient {
438
486
  { body }
439
487
  );
440
488
  }
489
+ async updateMrNote(projectId, mrIid, noteId, body) {
490
+ const encodedProject = this.encodeProjectId(projectId);
491
+ return this.fetch(
492
+ "PUT",
493
+ `/projects/${encodedProject}/merge_requests/${mrIid}/notes/${noteId}`,
494
+ { body }
495
+ );
496
+ }
441
497
  async createMergeRequest(projectId, options) {
442
498
  const encodedProject = this.encodeProjectId(projectId);
443
499
  return this.fetch(
@@ -721,6 +777,7 @@ var IssuesClient = class extends GitLabApiClient {
721
777
  async listIssues(options) {
722
778
  const params = new URLSearchParams();
723
779
  params.set("per_page", String(options.limit || 20));
780
+ if (options.page) params.set("page", String(options.page));
724
781
  if (options.state) params.set("state", options.state);
725
782
  if (options.scope) params.set("scope", options.scope);
726
783
  if (options.search) params.set("search", options.search);
@@ -733,7 +790,7 @@ var IssuesClient = class extends GitLabApiClient {
733
790
  } else {
734
791
  path2 = `/issues?${params}`;
735
792
  }
736
- return this.fetch("GET", path2);
793
+ return this.fetchWithPagination("GET", path2);
737
794
  }
738
795
  /**
739
796
  * List notes on an issue using GraphQL API with pagination support
@@ -816,6 +873,14 @@ var IssuesClient = class extends GitLabApiClient {
816
873
  `/projects/${encodedProject}/issues/${issueIid}/notes/${noteId}`
817
874
  );
818
875
  }
876
+ async updateIssueNote(projectId, issueIid, noteId, body) {
877
+ const encodedProject = this.encodeProjectId(projectId);
878
+ return this.fetch(
879
+ "PUT",
880
+ `/projects/${encodedProject}/issues/${issueIid}/notes/${noteId}`,
881
+ { body }
882
+ );
883
+ }
819
884
  };
820
885
 
821
886
  // src/client/work-items.ts
@@ -2150,6 +2215,14 @@ var EpicsClient = class extends GitLabApiClient {
2150
2215
  `/groups/${encodedGroup}/epics/${epicIid}/notes/${noteId}`
2151
2216
  );
2152
2217
  }
2218
+ async updateEpicNote(groupId, epicIid, noteId, body) {
2219
+ const encodedGroup = encodeURIComponent(groupId);
2220
+ return this.fetch(
2221
+ "PUT",
2222
+ `/groups/${encodedGroup}/epics/${epicIid}/notes/${noteId}`,
2223
+ { body }
2224
+ );
2225
+ }
2153
2226
  };
2154
2227
 
2155
2228
  // src/client/snippets.ts
@@ -2278,6 +2351,14 @@ var SnippetsClient = class extends GitLabApiClient {
2278
2351
  { body }
2279
2352
  );
2280
2353
  }
2354
+ async updateSnippetNote(projectId, snippetId, noteId, body) {
2355
+ const encodedProject = this.encodeProjectId(projectId);
2356
+ return this.fetch(
2357
+ "PUT",
2358
+ `/projects/${encodedProject}/snippets/${snippetId}/notes/${noteId}`,
2359
+ { body }
2360
+ );
2361
+ }
2281
2362
  };
2282
2363
 
2283
2364
  // src/client/discussions.ts
@@ -2615,26 +2696,31 @@ Returns: title, description, state, author, assignees, reviewers, labels, diff s
2615
2696
  }),
2616
2697
  gitlab_list_merge_requests: tool({
2617
2698
  description: `List merge requests for a project or search globally.
2618
- Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.`,
2699
+ Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.
2700
+
2701
+ IMPORTANT: Response includes pagination info. If nextPage exists, there are more results.
2702
+ Always paginate until you get an empty response or no nextPage to ensure you have all data.`,
2619
2703
  args: {
2620
2704
  project_id: z.string().optional().describe("The project ID or path. If not provided, searches globally."),
2621
2705
  state: z.enum(["opened", "closed", "merged", "all"]).optional().describe("Filter by MR state (default: opened)"),
2622
2706
  scope: z.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
2623
2707
  search: z.string().optional().describe("Search MRs by title or description"),
2624
2708
  labels: z.string().optional().describe("Comma-separated list of labels to filter by"),
2625
- limit: z.number().optional().describe("Maximum number of results (default: 20)")
2709
+ limit: z.number().optional().describe("Maximum number of results per page (default: 20)"),
2710
+ page: z.number().optional().describe("Page number for pagination (default: 1)")
2626
2711
  },
2627
2712
  execute: async (args, _ctx) => {
2628
2713
  const client = getGitLabClient();
2629
- const mrs = await client.listMergeRequests({
2714
+ const result = await client.listMergeRequests({
2630
2715
  projectId: args.project_id,
2631
2716
  state: args.state,
2632
2717
  scope: args.scope,
2633
2718
  search: args.search,
2634
2719
  labels: args.labels,
2635
- limit: args.limit
2720
+ limit: args.limit,
2721
+ page: args.page
2636
2722
  });
2637
- return JSON.stringify(mrs, null, 2);
2723
+ return JSON.stringify(result, null, 2);
2638
2724
  }
2639
2725
  }),
2640
2726
  gitlab_get_mr_changes: tool({
@@ -2966,7 +3052,10 @@ Returns: title, description, state, author, assignees, labels, milestone, weight
2966
3052
  }),
2967
3053
  gitlab_list_issues: tool2({
2968
3054
  description: `List issues for a project or search globally.
2969
- Can filter by state, labels, assignee, milestone.`,
3055
+ Can filter by state, labels, assignee, milestone.
3056
+
3057
+ IMPORTANT: Response includes pagination info. If nextPage exists, there are more results.
3058
+ Always paginate until you get an empty response or no nextPage to ensure you have all data.`,
2970
3059
  args: {
2971
3060
  project_id: z2.string().optional().describe("The project ID or path. If not provided, searches globally."),
2972
3061
  state: z2.enum(["opened", "closed", "all"]).optional().describe("Filter by issue state (default: opened)"),
@@ -2974,20 +3063,22 @@ Can filter by state, labels, assignee, milestone.`,
2974
3063
  search: z2.string().optional().describe("Search issues by title or description"),
2975
3064
  labels: z2.string().optional().describe("Comma-separated list of labels to filter by"),
2976
3065
  milestone: z2.string().optional().describe("Filter by milestone title"),
2977
- limit: z2.number().optional().describe("Maximum number of results (default: 20)")
3066
+ limit: z2.number().optional().describe("Maximum number of results per page (default: 20)"),
3067
+ page: z2.number().optional().describe("Page number for pagination (default: 1)")
2978
3068
  },
2979
3069
  execute: async (args, _ctx) => {
2980
3070
  const client = getGitLabClient();
2981
- const issues = await client.listIssues({
3071
+ const result = await client.listIssues({
2982
3072
  projectId: args.project_id,
2983
3073
  state: args.state,
2984
3074
  scope: args.scope,
2985
3075
  search: args.search,
2986
3076
  labels: args.labels,
2987
3077
  milestone: args.milestone,
2988
- limit: args.limit
3078
+ limit: args.limit,
3079
+ page: args.page
2989
3080
  });
2990
- return JSON.stringify(issues, null, 2);
3081
+ return JSON.stringify(result, null, 2);
2991
3082
  }
2992
3083
  })
2993
3084
  };
@@ -4489,19 +4580,22 @@ function filterNotesByResolved(result, resolved) {
4489
4580
  }
4490
4581
  };
4491
4582
  }
4492
- var VALID_LIST_CREATE_TYPES = ["merge_request", "issue", "epic", "snippet"];
4493
- var VALID_GET_NOTE_TYPES = ["issue", "epic"];
4583
+ var NOTE_RESOURCE_TYPES = ["merge_request", "issue", "epic", "snippet"];
4584
+ var GET_NOTE_RESOURCE_TYPES = ["issue", "epic"];
4494
4585
  function validationError3(param, resourceType) {
4495
4586
  return new Error(
4496
4587
  `Missing required parameter: '${param}' is required for resource_type '${resourceType}'`
4497
4588
  );
4498
4589
  }
4499
- function validateListCreateParams(resourceType, args) {
4500
- if (!VALID_LIST_CREATE_TYPES.includes(resourceType)) {
4590
+ function validateNoteParams(resourceType, args, opts) {
4591
+ if (!opts.allowedTypes.includes(resourceType)) {
4501
4592
  throw new Error(
4502
- `Invalid resource_type '${resourceType}'. Must be one of: ${VALID_LIST_CREATE_TYPES.join(", ")}`
4593
+ `Invalid resource_type '${resourceType}'. Must be one of: ${opts.allowedTypes.join(", ")}`
4503
4594
  );
4504
4595
  }
4596
+ if (opts.requireNoteId && args.note_id == null) {
4597
+ throw validationError3("note_id", resourceType);
4598
+ }
4505
4599
  switch (resourceType) {
4506
4600
  case "merge_request":
4507
4601
  case "issue":
@@ -4518,24 +4612,6 @@ function validateListCreateParams(resourceType, args) {
4518
4612
  break;
4519
4613
  }
4520
4614
  }
4521
- function validateGetNoteParams(resourceType, args) {
4522
- if (!VALID_GET_NOTE_TYPES.includes(resourceType)) {
4523
- throw new Error(
4524
- `Invalid resource_type '${resourceType}'. Must be one of: ${VALID_GET_NOTE_TYPES.join(", ")}`
4525
- );
4526
- }
4527
- if (args.note_id == null) throw validationError3("note_id", resourceType);
4528
- switch (resourceType) {
4529
- case "issue":
4530
- if (!args.project_id) throw validationError3("project_id", resourceType);
4531
- if (args.iid == null) throw validationError3("iid", resourceType);
4532
- break;
4533
- case "epic":
4534
- if (!args.group_id) throw validationError3("group_id", resourceType);
4535
- if (args.iid == null) throw validationError3("iid", resourceType);
4536
- break;
4537
- }
4538
- }
4539
4615
  var notesUnifiedTools = {
4540
4616
  /**
4541
4617
  * List notes/comments for any GitLab resource type
@@ -4571,7 +4647,7 @@ Examples:
4571
4647
  before: z14.string().optional().describe("Cursor for backward pagination - use startCursor from previous response")
4572
4648
  },
4573
4649
  execute: async (args, _ctx) => {
4574
- validateListCreateParams(args.resource_type, args);
4650
+ validateNoteParams(args.resource_type, args, { allowedTypes: NOTE_RESOURCE_TYPES });
4575
4651
  const client = getGitLabClient();
4576
4652
  const paginationOptions = {
4577
4653
  first: args.first,
@@ -4625,7 +4701,10 @@ Examples:
4625
4701
  iid: z14.number().describe("Internal ID of the issue or epic")
4626
4702
  },
4627
4703
  execute: async (args, _ctx) => {
4628
- validateGetNoteParams(args.resource_type, args);
4704
+ validateNoteParams(args.resource_type, args, {
4705
+ allowedTypes: GET_NOTE_RESOURCE_TYPES,
4706
+ requireNoteId: true
4707
+ });
4629
4708
  const client = getGitLabClient();
4630
4709
  switch (args.resource_type) {
4631
4710
  case "issue":
@@ -4669,7 +4748,7 @@ Examples:
4669
4748
  snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
4670
4749
  },
4671
4750
  execute: async (args, _ctx) => {
4672
- validateListCreateParams(args.resource_type, args);
4751
+ validateNoteParams(args.resource_type, args, { allowedTypes: NOTE_RESOURCE_TYPES });
4673
4752
  const client = getGitLabClient();
4674
4753
  switch (args.resource_type) {
4675
4754
  case "merge_request":
@@ -4700,6 +4779,68 @@ Examples:
4700
4779
  throw new Error(`Unsupported resource type: ${args.resource_type}`);
4701
4780
  }
4702
4781
  }
4782
+ }),
4783
+ /**
4784
+ * Update an existing note/comment on any GitLab resource
4785
+ */
4786
+ gitlab_update_note: tool14({
4787
+ description: `Update an existing note/comment on any GitLab resource.
4788
+ Modifies the body content of an existing note. You can only update notes you authored.
4789
+
4790
+ Examples:
4791
+ - MR note: resource_type="merge_request", project_id="group/project", iid=123, note_id=456, body="Updated comment"
4792
+ - Issue note: resource_type="issue", project_id="group/project", iid=456, note_id=789, body="Updated comment"
4793
+ - Epic note: resource_type="epic", group_id="my-group", iid=1, note_id=456, body="Updated comment"
4794
+ - Snippet note: resource_type="snippet", project_id="group/project", snippet_id=789, note_id=123, body="Updated comment"`,
4795
+ args: {
4796
+ resource_type: z14.enum(["merge_request", "issue", "epic", "snippet"]).describe("Type of GitLab resource"),
4797
+ note_id: z14.number().describe("The ID of the note to update"),
4798
+ body: z14.string().describe("The new content for the note (supports Markdown)"),
4799
+ project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
4800
+ group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
4801
+ iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
4802
+ snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
4803
+ },
4804
+ execute: async (args, _ctx) => {
4805
+ validateNoteParams(args.resource_type, args, {
4806
+ allowedTypes: NOTE_RESOURCE_TYPES,
4807
+ requireNoteId: true
4808
+ });
4809
+ const client = getGitLabClient();
4810
+ switch (args.resource_type) {
4811
+ case "merge_request":
4812
+ return JSON.stringify(
4813
+ await client.updateMrNote(args.project_id, args.iid, args.note_id, args.body),
4814
+ null,
4815
+ 2
4816
+ );
4817
+ case "issue":
4818
+ return JSON.stringify(
4819
+ await client.updateIssueNote(args.project_id, args.iid, args.note_id, args.body),
4820
+ null,
4821
+ 2
4822
+ );
4823
+ case "epic":
4824
+ return JSON.stringify(
4825
+ await client.updateEpicNote(args.group_id, args.iid, args.note_id, args.body),
4826
+ null,
4827
+ 2
4828
+ );
4829
+ case "snippet":
4830
+ return JSON.stringify(
4831
+ await client.updateSnippetNote(
4832
+ args.project_id,
4833
+ args.snippet_id,
4834
+ args.note_id,
4835
+ args.body
4836
+ ),
4837
+ null,
4838
+ 2
4839
+ );
4840
+ default:
4841
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
4842
+ }
4843
+ }
4703
4844
  })
4704
4845
  };
4705
4846