@gitlab/opencode-gitlab-plugin 1.4.0 → 1.5.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,13 @@
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
+ ## [1.5.0](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.4.0...v1.5.0) (2026-02-02)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * migrate discussions to GraphQL with cursor-based pagination ([1c61895](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/1c61895a8f0146f40d7a29a303cdc49e91280280))
11
+
5
12
  ## [1.4.0](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.3.0...v1.4.0) (2026-02-02)
6
13
 
7
14
 
package/dist/index.js CHANGED
@@ -132,6 +132,74 @@ var NOTES_CONNECTION_FRAGMENT = `
132
132
  }
133
133
  `;
134
134
 
135
+ // src/client/discussions-types.ts
136
+ function buildDiscussionsPaginationVariables(options) {
137
+ const variables = {};
138
+ if (options?.first !== void 0) {
139
+ variables.first = options.first;
140
+ } else if (options?.last == null) {
141
+ variables.first = 20;
142
+ }
143
+ if (options?.after) variables.after = options.after;
144
+ if (options?.last !== void 0) variables.last = options.last;
145
+ if (options?.before) variables.before = options.before;
146
+ return variables;
147
+ }
148
+ var DISCUSSION_NOTE_FRAGMENT = `
149
+ fragment DiscussionNoteFields on Note {
150
+ id
151
+ body
152
+ bodyHtml
153
+ createdAt
154
+ updatedAt
155
+ system
156
+ resolvable
157
+ resolved
158
+ resolvedAt
159
+ url
160
+ author {
161
+ id
162
+ username
163
+ name
164
+ avatarUrl
165
+ }
166
+ }
167
+ `;
168
+ var DISCUSSION_FRAGMENT = `
169
+ fragment DiscussionFields on Discussion {
170
+ id
171
+ replyId
172
+ createdAt
173
+ resolved
174
+ resolvable
175
+ resolvedAt
176
+ resolvedBy {
177
+ id
178
+ username
179
+ name
180
+ avatarUrl
181
+ }
182
+ notes {
183
+ nodes {
184
+ ...DiscussionNoteFields
185
+ }
186
+ }
187
+ }
188
+ `;
189
+ var DISCUSSIONS_CONNECTION_FRAGMENT = `
190
+ fragment DiscussionsConnectionFields on DiscussionConnection {
191
+ pageInfo {
192
+ hasNextPage
193
+ hasPreviousPage
194
+ startCursor
195
+ endCursor
196
+ }
197
+ nodes {
198
+ ...DiscussionFields
199
+ }
200
+ }
201
+ `;
202
+
135
203
  // src/client/merge-requests.ts
136
204
  var LIST_MR_NOTES_QUERY = `
137
205
  ${NOTES_FRAGMENT}
@@ -160,6 +228,32 @@ var LIST_MR_NOTES_QUERY = `
160
228
  }
161
229
  }
162
230
  `;
231
+ var LIST_MR_DISCUSSIONS_QUERY = `
232
+ ${DISCUSSION_NOTE_FRAGMENT}
233
+ ${DISCUSSION_FRAGMENT}
234
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
235
+ query listMrDiscussions(
236
+ $projectPath: ID!
237
+ $mrIid: String!
238
+ $first: Int
239
+ $after: String
240
+ $last: Int
241
+ $before: String
242
+ ) {
243
+ project(fullPath: $projectPath) {
244
+ mergeRequest(iid: $mrIid) {
245
+ discussions(
246
+ first: $first
247
+ after: $after
248
+ last: $last
249
+ before: $before
250
+ ) {
251
+ ...DiscussionsConnectionFields
252
+ }
253
+ }
254
+ }
255
+ }
256
+ `;
163
257
  var SET_AUTO_MERGE_MUTATION = `
164
258
  mutation setAutoMerge(
165
259
  $projectPath: ID!
@@ -218,12 +312,41 @@ var MergeRequestsClient = class extends GitLabApiClient {
218
312
  `/projects/${encodedProject}/merge_requests/${mrIid}/changes`
219
313
  );
220
314
  }
221
- async listMrDiscussions(projectId, mrIid) {
222
- const encodedProject = this.encodeProjectId(projectId);
223
- return this.fetch(
224
- "GET",
225
- `/projects/${encodedProject}/merge_requests/${mrIid}/discussions`
226
- );
315
+ /**
316
+ * List notes on a merge request using GraphQL API with pagination support
317
+ */
318
+ async listMrNotes(projectId, mrIid, options) {
319
+ const variables = {
320
+ projectPath: projectId,
321
+ mrIid: String(mrIid),
322
+ ...buildPaginationVariables(options)
323
+ };
324
+ const result = await this.fetchGraphQL(LIST_MR_NOTES_QUERY, variables);
325
+ const notes = result.project?.mergeRequest?.notes;
326
+ if (!notes) {
327
+ throw new Error("Merge request not found or access denied");
328
+ }
329
+ return {
330
+ notes,
331
+ pageInfo: notes.pageInfo,
332
+ totalCount: notes.count
333
+ };
334
+ }
335
+ /**
336
+ * List discussions on a merge request using GraphQL API with pagination support
337
+ */
338
+ async listMrDiscussions(projectId, mrIid, options) {
339
+ const variables = {
340
+ projectPath: projectId,
341
+ mrIid: String(mrIid),
342
+ ...buildDiscussionsPaginationVariables(options)
343
+ };
344
+ const result = await this.fetchGraphQL(LIST_MR_DISCUSSIONS_QUERY, variables);
345
+ const discussions = result.project?.mergeRequest?.discussions;
346
+ if (!discussions) {
347
+ throw new Error("Merge request not found or access denied");
348
+ }
349
+ return { discussions };
227
350
  }
228
351
  async getMrDiscussion(projectId, mrIid, discussionId) {
229
352
  const encodedProject = this.encodeProjectId(projectId);
@@ -260,26 +383,6 @@ var MergeRequestsClient = class extends GitLabApiClient {
260
383
  requestBody
261
384
  );
262
385
  }
263
- /**
264
- * List notes on a merge request using GraphQL API with pagination support
265
- */
266
- async listMrNotes(projectId, mrIid, options) {
267
- const variables = {
268
- projectPath: projectId,
269
- mrIid: String(mrIid),
270
- ...buildPaginationVariables(options)
271
- };
272
- const result = await this.fetchGraphQL(LIST_MR_NOTES_QUERY, variables);
273
- const notes = result.project?.mergeRequest?.notes;
274
- if (!notes) {
275
- throw new Error("Merge request not found or access denied");
276
- }
277
- return {
278
- notes,
279
- pageInfo: notes.pageInfo,
280
- totalCount: notes.count
281
- };
282
- }
283
386
  async createMrNote(projectId, mrIid, body, discussionId) {
284
387
  const encodedProject = this.encodeProjectId(projectId);
285
388
  if (discussionId) {
@@ -383,6 +486,32 @@ var LIST_ISSUE_NOTES_QUERY = `
383
486
  }
384
487
  }
385
488
  `;
489
+ var LIST_ISSUE_DISCUSSIONS_QUERY = `
490
+ ${DISCUSSION_NOTE_FRAGMENT}
491
+ ${DISCUSSION_FRAGMENT}
492
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
493
+ query listIssueDiscussions(
494
+ $projectPath: ID!
495
+ $issueIid: String!
496
+ $first: Int
497
+ $after: String
498
+ $last: Int
499
+ $before: String
500
+ ) {
501
+ project(fullPath: $projectPath) {
502
+ issue(iid: $issueIid) {
503
+ discussions(
504
+ first: $first
505
+ after: $after
506
+ last: $last
507
+ before: $before
508
+ ) {
509
+ ...DiscussionsConnectionFields
510
+ }
511
+ }
512
+ }
513
+ }
514
+ `;
386
515
  var IssuesClient = class extends GitLabApiClient {
387
516
  async createIssue(projectId, title, options) {
388
517
  const encodedProject = this.encodeProjectId(projectId);
@@ -438,12 +567,21 @@ var IssuesClient = class extends GitLabApiClient {
438
567
  totalCount: notes.count
439
568
  };
440
569
  }
441
- async listIssueDiscussions(projectId, issueIid) {
442
- const encodedProject = this.encodeProjectId(projectId);
443
- return this.fetch(
444
- "GET",
445
- `/projects/${encodedProject}/issues/${issueIid}/discussions`
446
- );
570
+ /**
571
+ * List discussions on an issue using GraphQL API with pagination support
572
+ */
573
+ async listIssueDiscussions(projectId, issueIid, options) {
574
+ const variables = {
575
+ projectPath: projectId,
576
+ issueIid: String(issueIid),
577
+ ...buildDiscussionsPaginationVariables(options)
578
+ };
579
+ const result = await this.fetchGraphQL(LIST_ISSUE_DISCUSSIONS_QUERY, variables);
580
+ const discussions = result.project?.issue?.discussions;
581
+ if (!discussions) {
582
+ throw new Error("Issue not found or access denied");
583
+ }
584
+ return { discussions };
447
585
  }
448
586
  async getIssueDiscussion(projectId, issueIid, discussionId) {
449
587
  const encodedProject = this.encodeProjectId(projectId);
@@ -1382,7 +1520,7 @@ var LIST_EPIC_NOTES_QUERY = `
1382
1520
  ${NOTES_CONNECTION_FRAGMENT}
1383
1521
  query listEpicNotes(
1384
1522
  $groupPath: ID!
1385
- $epicIid: ID!
1523
+ $epicIid: String!
1386
1524
  $first: Int
1387
1525
  $after: String
1388
1526
  $last: Int
@@ -1402,6 +1540,32 @@ var LIST_EPIC_NOTES_QUERY = `
1402
1540
  }
1403
1541
  }
1404
1542
  `;
1543
+ var LIST_EPIC_DISCUSSIONS_QUERY = `
1544
+ ${DISCUSSION_NOTE_FRAGMENT}
1545
+ ${DISCUSSION_FRAGMENT}
1546
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
1547
+ query listEpicDiscussions(
1548
+ $groupPath: ID!
1549
+ $epicIid: String!
1550
+ $first: Int
1551
+ $after: String
1552
+ $last: Int
1553
+ $before: String
1554
+ ) {
1555
+ group(fullPath: $groupPath) {
1556
+ epic(iid: $epicIid) {
1557
+ discussions(
1558
+ first: $first
1559
+ after: $after
1560
+ last: $last
1561
+ before: $before
1562
+ ) {
1563
+ ...DiscussionsConnectionFields
1564
+ }
1565
+ }
1566
+ }
1567
+ }
1568
+ `;
1405
1569
  var EpicsClient = class extends GitLabApiClient {
1406
1570
  async getEpic(groupId, epicIid) {
1407
1571
  const encodedGroup = encodeURIComponent(groupId);
@@ -1470,12 +1634,21 @@ var EpicsClient = class extends GitLabApiClient {
1470
1634
  totalCount: notes.count
1471
1635
  };
1472
1636
  }
1473
- async listEpicDiscussions(groupId, epicIid) {
1474
- const encodedGroup = encodeURIComponent(groupId);
1475
- return this.fetch(
1476
- "GET",
1477
- `/groups/${encodedGroup}/epics/${epicIid}/discussions`
1478
- );
1637
+ /**
1638
+ * List discussions on an epic using GraphQL API with pagination support
1639
+ */
1640
+ async listEpicDiscussions(groupId, epicIid, options) {
1641
+ const variables = {
1642
+ groupPath: groupId,
1643
+ epicIid: String(epicIid),
1644
+ ...buildDiscussionsPaginationVariables(options)
1645
+ };
1646
+ const result = await this.fetchGraphQL(LIST_EPIC_DISCUSSIONS_QUERY, variables);
1647
+ const discussions = result.group?.epic?.discussions;
1648
+ if (!discussions) {
1649
+ throw new Error("Epic not found or access denied");
1650
+ }
1651
+ return { discussions };
1479
1652
  }
1480
1653
  async getEpicDiscussion(groupId, epicIid, discussionId) {
1481
1654
  const encodedGroup = encodeURIComponent(groupId);
@@ -1533,21 +1706,32 @@ var LIST_SNIPPET_NOTES_QUERY = `
1533
1706
  }
1534
1707
  }
1535
1708
  `;
1536
- var SnippetsClient = class extends GitLabApiClient {
1537
- async listSnippetDiscussions(projectId, snippetId) {
1538
- const encodedProject = this.encodeProjectId(projectId);
1539
- return this.fetch(
1540
- "GET",
1541
- `/projects/${encodedProject}/snippets/${snippetId}/discussions`
1542
- );
1543
- }
1544
- async getSnippetDiscussion(projectId, snippetId, discussionId) {
1545
- const encodedProject = this.encodeProjectId(projectId);
1546
- return this.fetch(
1547
- "GET",
1548
- `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}`
1549
- );
1709
+ var LIST_SNIPPET_DISCUSSIONS_QUERY = `
1710
+ ${DISCUSSION_NOTE_FRAGMENT}
1711
+ ${DISCUSSION_FRAGMENT}
1712
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
1713
+ query listSnippetDiscussions(
1714
+ $snippetGid: SnippetID!
1715
+ $first: Int
1716
+ $after: String
1717
+ $last: Int
1718
+ $before: String
1719
+ ) {
1720
+ snippets(ids: [$snippetGid]) {
1721
+ nodes {
1722
+ discussions(
1723
+ first: $first
1724
+ after: $after
1725
+ last: $last
1726
+ before: $before
1727
+ ) {
1728
+ ...DiscussionsConnectionFields
1729
+ }
1730
+ }
1731
+ }
1550
1732
  }
1733
+ `;
1734
+ var SnippetsClient = class extends GitLabApiClient {
1551
1735
  /**
1552
1736
  * List notes on a snippet using GraphQL API with pagination support
1553
1737
  */
@@ -1573,6 +1757,33 @@ var SnippetsClient = class extends GitLabApiClient {
1573
1757
  totalCount: notes.count
1574
1758
  };
1575
1759
  }
1760
+ /**
1761
+ * List discussions on a snippet using GraphQL API with pagination support
1762
+ */
1763
+ async listSnippetDiscussions(projectId, snippetId, options) {
1764
+ const variables = {
1765
+ snippetGid: `gid://gitlab/ProjectSnippet/${snippetId}`,
1766
+ ...buildDiscussionsPaginationVariables(options)
1767
+ };
1768
+ const result = await this.fetchGraphQL(LIST_SNIPPET_DISCUSSIONS_QUERY, variables);
1769
+ const discussions = result.snippets.nodes[0]?.discussions || {
1770
+ nodes: [],
1771
+ pageInfo: {
1772
+ hasNextPage: false,
1773
+ hasPreviousPage: false,
1774
+ startCursor: null,
1775
+ endCursor: null
1776
+ }
1777
+ };
1778
+ return { discussions };
1779
+ }
1780
+ async getSnippetDiscussion(projectId, snippetId, discussionId) {
1781
+ const encodedProject = this.encodeProjectId(projectId);
1782
+ return this.fetch(
1783
+ "GET",
1784
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}`
1785
+ );
1786
+ }
1576
1787
  async createSnippetNote(projectId, snippetId, body, discussionId) {
1577
1788
  const encodedProject = this.encodeProjectId(projectId);
1578
1789
  if (discussionId) {
@@ -1916,11 +2127,20 @@ Returns all discussion threads with nested notes. Each discussion contains a 'no
1916
2127
  Note: For a flattened list of all comments, use gitlab_list_mr_notes instead.`,
1917
2128
  args: {
1918
2129
  project_id: z.string().describe("The project ID or URL-encoded path"),
1919
- mr_iid: z.number().describe("The internal ID of the merge request")
2130
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2131
+ first: z.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2132
+ after: z.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2133
+ last: z.number().optional().describe("Number of discussions to fetch from the end"),
2134
+ before: z.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
1920
2135
  },
1921
2136
  execute: async (args, _ctx) => {
1922
2137
  const client = getGitLabClient();
1923
- const discussions = await client.listMrDiscussions(args.project_id, args.mr_iid);
2138
+ const discussions = await client.listMrDiscussions(args.project_id, args.mr_iid, {
2139
+ first: args.first,
2140
+ after: args.after,
2141
+ last: args.last,
2142
+ before: args.before
2143
+ });
1924
2144
  return JSON.stringify(discussions, null, 2);
1925
2145
  }
1926
2146
  }),
@@ -2316,11 +2536,20 @@ Returns all discussion threads with nested notes. Each discussion contains a 'no
2316
2536
  Use the discussion 'id' field to reply to a specific thread with gitlab_create_issue_note.`,
2317
2537
  args: {
2318
2538
  project_id: z2.string().describe("The project ID or URL-encoded path"),
2319
- issue_iid: z2.number().describe("The internal ID of the issue")
2539
+ issue_iid: z2.number().describe("The internal ID of the issue"),
2540
+ first: z2.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2541
+ after: z2.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2542
+ last: z2.number().optional().describe("Number of discussions to fetch from the end"),
2543
+ before: z2.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
2320
2544
  },
2321
2545
  execute: async (args, _ctx) => {
2322
2546
  const client = getGitLabClient();
2323
- const discussions = await client.listIssueDiscussions(args.project_id, args.issue_iid);
2547
+ const discussions = await client.listIssueDiscussions(args.project_id, args.issue_iid, {
2548
+ first: args.first,
2549
+ after: args.after,
2550
+ last: args.last,
2551
+ before: args.before
2552
+ });
2324
2553
  return JSON.stringify(discussions, null, 2);
2325
2554
  }
2326
2555
  }),
@@ -2593,11 +2822,20 @@ Returns all discussion threads with nested notes. Each discussion contains a 'no
2593
2822
  Use the discussion 'id' field to reply to a specific thread with gitlab_create_epic_note.`,
2594
2823
  args: {
2595
2824
  group_id: z3.string().describe("The group ID or URL-encoded path"),
2596
- epic_iid: z3.number().describe("The internal ID of the epic")
2825
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2826
+ first: z3.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2827
+ after: z3.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2828
+ last: z3.number().optional().describe("Number of discussions to fetch from the end"),
2829
+ before: z3.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
2597
2830
  },
2598
2831
  execute: async (args, _ctx) => {
2599
2832
  const client = getGitLabClient();
2600
- const discussions = await client.listEpicDiscussions(args.group_id, args.epic_iid);
2833
+ const discussions = await client.listEpicDiscussions(args.group_id, args.epic_iid, {
2834
+ first: args.first,
2835
+ after: args.after,
2836
+ last: args.last,
2837
+ before: args.before
2838
+ });
2601
2839
  return JSON.stringify(discussions, null, 2);
2602
2840
  }
2603
2841
  }),
@@ -3567,11 +3805,20 @@ Returns all discussion threads with nested notes. Each discussion contains a 'no
3567
3805
  Use the discussion 'id' field to reply to a specific thread with gitlab_create_snippet_note.`,
3568
3806
  args: {
3569
3807
  project_id: z9.string().describe("The project ID or URL-encoded path"),
3570
- snippet_id: z9.number().describe("The ID of the snippet")
3808
+ snippet_id: z9.number().describe("The ID of the snippet"),
3809
+ first: z9.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
3810
+ after: z9.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
3811
+ last: z9.number().optional().describe("Number of discussions to fetch from the end"),
3812
+ before: z9.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
3571
3813
  },
3572
3814
  execute: async (args, _ctx) => {
3573
3815
  const client = getGitLabClient();
3574
- const discussions = await client.listSnippetDiscussions(args.project_id, args.snippet_id);
3816
+ const discussions = await client.listSnippetDiscussions(args.project_id, args.snippet_id, {
3817
+ first: args.first,
3818
+ after: args.after,
3819
+ last: args.last,
3820
+ before: args.before
3821
+ });
3575
3822
  return JSON.stringify(discussions, null, 2);
3576
3823
  }
3577
3824
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/opencode-gitlab-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "GitLab tools plugin for OpenCode - provides GitLab API access for merge requests, issues, pipelines, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",