@gitlab/opencode-gitlab-plugin 1.6.2 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
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.7.1](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.7.0...v1.7.1) (2026-02-24)
6
+
7
+
8
+ ### 📚 Documentation
9
+
10
+ * **discussions:** improve gitlab_create_discussion description for reply use case ([a0d2296](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/a0d2296cf2aa42258c7ca1749df86810c852174b))
11
+
12
+ ## [1.7.0](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.6.2...v1.7.0) (2026-02-24)
13
+
14
+
15
+ ### ✨ Features
16
+
17
+ * **award-emoji:** add tools for managing reactions on resources ([77b8ad5](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/77b8ad5656b681c3a6c9395f2a0025126e4c39bb))
18
+ * **discussions:** use GraphQL for discussion resolution ([b6ed342](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/b6ed342cea8f726b14a2850350d217a937406caa))
19
+
20
+
21
+ ### 🐛 Bug Fixes
22
+
23
+ * **todos:** handle 404 gracefully when marking todo as done ([2a26763](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/2a267632012d89884ebc3462ac0adafc5ffd9286))
24
+
5
25
  ## [1.6.2](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.6.1...v1.6.2) (2026-02-04)
6
26
 
7
27
 
package/dist/index.js CHANGED
@@ -277,6 +277,23 @@ var SET_AUTO_MERGE_MUTATION = `
277
277
  }
278
278
  }
279
279
  `;
280
+ var RESOLVE_DISCUSSION_MUTATION = `
281
+ mutation resolveDiscussion($discussionId: DiscussionID!, $resolve: Boolean!) {
282
+ discussionToggleResolve(input: { id: $discussionId, resolve: $resolve }) {
283
+ discussion {
284
+ id
285
+ resolved
286
+ resolvedAt
287
+ resolvedBy {
288
+ id
289
+ username
290
+ name
291
+ }
292
+ }
293
+ errors
294
+ }
295
+ }
296
+ `;
280
297
  var MergeRequestsClient = class extends GitLabApiClient {
281
298
  async getMergeRequest(projectId, mrIid, includeChanges) {
282
299
  const encodedProject = this.encodeProjectId(projectId);
@@ -368,6 +385,32 @@ var MergeRequestsClient = class extends GitLabApiClient {
368
385
  { resolved: false }
369
386
  );
370
387
  }
388
+ /**
389
+ * Resolve or unresolve a discussion using GraphQL API.
390
+ * This works for all discussions including outdated ones (resolved: null)
391
+ * which the REST API cannot handle.
392
+ *
393
+ * @param discussionId - The discussion ID (can be short ID or full gid://gitlab/Discussion/ID format)
394
+ * @param resolve - Whether to resolve (true) or unresolve (false) the discussion
395
+ */
396
+ async toggleDiscussionResolved(discussionId, resolve2) {
397
+ const gid = discussionId.startsWith("gid://") ? discussionId : `gid://gitlab/Discussion/${discussionId}`;
398
+ const result = await this.fetchGraphQL(RESOLVE_DISCUSSION_MUTATION, {
399
+ discussionId: gid,
400
+ resolve: resolve2
401
+ });
402
+ if (result.discussionToggleResolve.errors.length > 0) {
403
+ throw new Error(
404
+ `Failed to ${resolve2 ? "resolve" : "unresolve"} discussion: ${result.discussionToggleResolve.errors.join(", ")}`
405
+ );
406
+ }
407
+ if (!result.discussionToggleResolve.discussion) {
408
+ throw new Error(
409
+ `Failed to ${resolve2 ? "resolve" : "unresolve"} discussion: No discussion returned`
410
+ );
411
+ }
412
+ return result.discussionToggleResolve.discussion;
413
+ }
371
414
  async createMrDiscussion(projectId, mrIid, body, position) {
372
415
  const encodedProject = this.encodeProjectId(projectId);
373
416
  const requestBody = { body };
@@ -425,6 +468,32 @@ var MergeRequestsClient = class extends GitLabApiClient {
425
468
  `/projects/${encodedProject}/merge_requests/${mrIid}/pipelines`
426
469
  );
427
470
  }
471
+ /**
472
+ * Add a reviewer to a merge request without affecting existing reviewers
473
+ */
474
+ async addReviewer(projectId, mrIid, reviewerId) {
475
+ const mr = await this.getMergeRequest(projectId, mrIid);
476
+ const currentReviewers = mr.reviewers || [];
477
+ const currentReviewerIds = currentReviewers.map((r) => r.id);
478
+ if (currentReviewerIds.includes(reviewerId)) {
479
+ return mr;
480
+ }
481
+ const newReviewerIds = [...currentReviewerIds, reviewerId];
482
+ return this.updateMergeRequest(projectId, mrIid, { reviewer_ids: newReviewerIds });
483
+ }
484
+ /**
485
+ * Remove a reviewer from a merge request without affecting other reviewers
486
+ */
487
+ async removeReviewer(projectId, mrIid, reviewerId) {
488
+ const mr = await this.getMergeRequest(projectId, mrIid);
489
+ const currentReviewers = mr.reviewers || [];
490
+ const currentReviewerIds = currentReviewers.map((r) => r.id);
491
+ if (!currentReviewerIds.includes(reviewerId)) {
492
+ return mr;
493
+ }
494
+ const newReviewerIds = currentReviewerIds.filter((id) => id !== reviewerId);
495
+ return this.updateMergeRequest(projectId, mrIid, { reviewer_ids: newReviewerIds });
496
+ }
428
497
  async listMergeRequestDiffs(projectId, mrIid, options = {}) {
429
498
  const encodedProject = this.encodeProjectId(projectId);
430
499
  const params = new URLSearchParams();
@@ -1468,7 +1537,21 @@ var TodosClient = class extends GitLabApiClient {
1468
1537
  return typeMap[type] || type;
1469
1538
  }
1470
1539
  async markTodoAsDone(todoId) {
1471
- return this.fetch("POST", `/todos/${todoId}/mark_as_done`);
1540
+ try {
1541
+ const result = await this.fetch(
1542
+ "POST",
1543
+ `/todos/${todoId}/mark_as_done`
1544
+ );
1545
+ return { success: true, todo: result };
1546
+ } catch (error) {
1547
+ if (error instanceof Error && error.message.includes("404")) {
1548
+ return {
1549
+ success: false,
1550
+ message: `Todo ${todoId} not found. It may have already been marked as done or does not exist.`
1551
+ };
1552
+ }
1553
+ throw error;
1554
+ }
1472
1555
  }
1473
1556
  async markAllTodosAsDone() {
1474
1557
  return this.fetch("POST", "/todos/mark_as_done");
@@ -1963,6 +2046,63 @@ var AuditClient = class extends GitLabApiClient {
1963
2046
  }
1964
2047
  };
1965
2048
 
2049
+ // src/client/award-emoji.ts
2050
+ var AwardEmojiClient = class extends GitLabApiClient {
2051
+ /**
2052
+ * Build the base path for award emoji operations
2053
+ */
2054
+ buildAwardEmojiPath(resource) {
2055
+ const encodedProject = this.encodeProjectId(resource.projectId);
2056
+ let basePath;
2057
+ switch (resource.resourceType) {
2058
+ case "merge_request":
2059
+ basePath = `/projects/${encodedProject}/merge_requests/${resource.resourceIid}`;
2060
+ break;
2061
+ case "issue":
2062
+ basePath = `/projects/${encodedProject}/issues/${resource.resourceIid}`;
2063
+ break;
2064
+ case "snippet":
2065
+ basePath = `/projects/${encodedProject}/snippets/${resource.resourceIid}`;
2066
+ break;
2067
+ }
2068
+ if (resource.noteId) {
2069
+ return `${basePath}/notes/${resource.noteId}/award_emoji`;
2070
+ }
2071
+ return `${basePath}/award_emoji`;
2072
+ }
2073
+ /**
2074
+ * List all award emoji on a resource or note
2075
+ */
2076
+ async listAwardEmoji(resource) {
2077
+ const path2 = this.buildAwardEmojiPath(resource);
2078
+ return this.fetch("GET", path2);
2079
+ }
2080
+ /**
2081
+ * Get a single award emoji by ID
2082
+ */
2083
+ async getAwardEmoji(resource, awardId) {
2084
+ const path2 = `${this.buildAwardEmojiPath(resource)}/${awardId}`;
2085
+ return this.fetch("GET", path2);
2086
+ }
2087
+ /**
2088
+ * Add an award emoji (reaction) to a resource or note
2089
+ *
2090
+ * @param resource - The resource to add the emoji to
2091
+ * @param name - The emoji name without colons (e.g., 'thumbsup', 'rocket', 'eyes')
2092
+ */
2093
+ async createAwardEmoji(resource, name) {
2094
+ const path2 = this.buildAwardEmojiPath(resource);
2095
+ return this.fetch("POST", path2, { name });
2096
+ }
2097
+ /**
2098
+ * Remove an award emoji from a resource or note
2099
+ */
2100
+ async deleteAwardEmoji(resource, awardId) {
2101
+ const path2 = `${this.buildAwardEmojiPath(resource)}/${awardId}`;
2102
+ await this.fetch("DELETE", path2);
2103
+ }
2104
+ };
2105
+
1966
2106
  // src/client/index.ts
1967
2107
  var UnifiedGitLabClient = class extends GitLabApiClient {
1968
2108
  };
@@ -1992,7 +2132,8 @@ applyMixins(UnifiedGitLabClient, [
1992
2132
  EpicsClient,
1993
2133
  SnippetsClient,
1994
2134
  DiscussionsClient,
1995
- AuditClient
2135
+ AuditClient,
2136
+ AwardEmojiClient
1996
2137
  ]);
1997
2138
 
1998
2139
  // src/utils.ts
@@ -2203,6 +2344,36 @@ This is useful when you need to process diffs in chunks or when the MR has many
2203
2344
  return JSON.stringify(diffs, null, 2);
2204
2345
  }
2205
2346
  }),
2347
+ gitlab_add_mr_reviewer: tool({
2348
+ description: `Add a reviewer to a merge request without affecting existing reviewers.
2349
+ This is safer than gitlab_update_merge_request when you only want to add a single reviewer,
2350
+ as it preserves all existing reviewers.`,
2351
+ args: {
2352
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2353
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2354
+ reviewer_id: z.number().describe("The user ID of the reviewer to add")
2355
+ },
2356
+ execute: async (args, _ctx) => {
2357
+ const client = getGitLabClient();
2358
+ const mr = await client.addReviewer(args.project_id, args.mr_iid, args.reviewer_id);
2359
+ return JSON.stringify(mr, null, 2);
2360
+ }
2361
+ }),
2362
+ gitlab_remove_mr_reviewer: tool({
2363
+ description: `Remove a reviewer from a merge request without affecting other reviewers.
2364
+ This is safer than gitlab_update_merge_request when you only want to remove a single reviewer,
2365
+ as it preserves all other reviewers.`,
2366
+ args: {
2367
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2368
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2369
+ reviewer_id: z.number().describe("The user ID of the reviewer to remove")
2370
+ },
2371
+ execute: async (args, _ctx) => {
2372
+ const client = getGitLabClient();
2373
+ const mr = await client.removeReviewer(args.project_id, args.mr_iid, args.reviewer_id);
2374
+ return JSON.stringify(mr, null, 2);
2375
+ }
2376
+ }),
2206
2377
  gitlab_set_mr_auto_merge: tool({
2207
2378
  description: `Enable auto-merge (MWPS - Merge When Pipeline Succeeds) on a merge request.
2208
2379
  Uses the GitLab GraphQL API mergeRequestAccept mutation with a merge strategy.
@@ -3227,7 +3398,24 @@ Examples:
3227
3398
  return JSON.stringify({ error: 'todo_id is required when action is "one"' }, null, 2);
3228
3399
  }
3229
3400
  const result = await client.markTodoAsDone(args.todo_id);
3230
- return JSON.stringify(result, null, 2);
3401
+ if (!result.success) {
3402
+ return JSON.stringify(
3403
+ {
3404
+ success: false,
3405
+ message: result.message
3406
+ },
3407
+ null,
3408
+ 2
3409
+ );
3410
+ }
3411
+ return JSON.stringify(
3412
+ {
3413
+ success: true,
3414
+ todo: result.todo
3415
+ },
3416
+ null,
3417
+ 2
3418
+ );
3231
3419
  }
3232
3420
  case "all": {
3233
3421
  const result = await client.markAllTodosAsDone();
@@ -3592,15 +3780,17 @@ Required parameters vary by resource type:
3592
3780
  gitlab_create_discussion: tool12({
3593
3781
  description: `Create a new discussion thread or reply to an existing one.
3594
3782
 
3595
- For NEW discussion: Omit discussion_id
3596
- For REPLY to existing thread: Include discussion_id (preferred over gitlab_create_note for threaded conversations)
3783
+ REPLYING TO EXISTING THREADS:
3784
+ To reply to an existing discussion, provide the discussion_id parameter.
3785
+ This is the recommended way to reply in-thread rather than creating a standalone comment.
3597
3786
 
3598
- For code-specific comments on MRs/commits, provide position information.
3787
+ STARTING A NEW DISCUSSION:
3788
+ Omit discussion_id to create a new thread. For code-specific comments on MRs/commits, provide position information.
3599
3789
 
3600
3790
  Examples:
3601
- - New MR comment: resource_type="merge_request", project_id="...", iid=123, body="..."
3602
- - Reply to thread: resource_type="merge_request", ..., discussion_id="...", body="..."
3603
- - Code comment: resource_type="merge_request", ..., body="...", position={base_sha, head_sha, ...}`,
3791
+ - Reply to thread: resource_type="merge_request", project_id="group/project", iid=123, discussion_id="abc123def", body="Thanks for the feedback!"
3792
+ - New comment: resource_type="merge_request", project_id="group/project", iid=123, body="General comment"
3793
+ - Code comment: resource_type="merge_request", project_id="group/project", iid=123, body="...", position={base_sha, head_sha, new_path, new_line, ...}`,
3604
3794
  args: {
3605
3795
  resource_type: z12.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3606
3796
  body: z12.string().describe("The comment text (Markdown supported)"),
@@ -3678,6 +3868,9 @@ Examples:
3678
3868
  description: `Mark a discussion thread as resolved or unresolve it.
3679
3869
  Only works for resolvable discussions (MRs and issues only).
3680
3870
 
3871
+ Uses GraphQL API which handles all discussion types including outdated discussions
3872
+ (those with resolved: null) that the REST API cannot handle.
3873
+
3681
3874
  Use after addressing feedback to indicate the discussion is complete.`,
3682
3875
  args: {
3683
3876
  resource_type: z12.enum(["merge_request", "issue"]).describe("Type of resource (only MR and issue discussions can be resolved)"),
@@ -3688,37 +3881,12 @@ Use after addressing feedback to indicate the discussion is complete.`,
3688
3881
  },
3689
3882
  execute: async (args, _ctx) => {
3690
3883
  const client = getGitLabClient();
3691
- if (args.action === "resolve") {
3692
- switch (args.resource_type) {
3693
- case "merge_request":
3694
- return JSON.stringify(
3695
- await client.resolveMrDiscussion(args.project_id, args.iid, args.discussion_id),
3696
- null,
3697
- 2
3698
- );
3699
- case "issue":
3700
- return JSON.stringify(
3701
- await client.resolveIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3702
- null,
3703
- 2
3704
- );
3705
- }
3706
- } else {
3707
- switch (args.resource_type) {
3708
- case "merge_request":
3709
- return JSON.stringify(
3710
- await client.unresolveMrDiscussion(args.project_id, args.iid, args.discussion_id),
3711
- null,
3712
- 2
3713
- );
3714
- case "issue":
3715
- return JSON.stringify(
3716
- await client.unresolveIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3717
- null,
3718
- 2
3719
- );
3720
- }
3721
- }
3884
+ const resolve2 = args.action === "resolve";
3885
+ return JSON.stringify(
3886
+ await client.toggleDiscussionResolved(args.discussion_id, resolve2),
3887
+ null,
3888
+ 2
3889
+ );
3722
3890
  }
3723
3891
  })
3724
3892
  };
@@ -4293,6 +4461,107 @@ Note: Requires administrator access.`,
4293
4461
  })
4294
4462
  };
4295
4463
 
4464
+ // src/tools/award-emoji.ts
4465
+ import { tool as tool16 } from "@opencode-ai/plugin";
4466
+ var z16 = tool16.schema;
4467
+ function validateAwardEmojiParams(args) {
4468
+ if (!args.project_id) {
4469
+ throw new Error("project_id is required");
4470
+ }
4471
+ if (args.resource_iid == null) {
4472
+ throw new Error("resource_iid is required");
4473
+ }
4474
+ }
4475
+ function buildResourceId(args) {
4476
+ return {
4477
+ projectId: args.project_id,
4478
+ resourceType: args.resource_type,
4479
+ resourceIid: args.resource_iid,
4480
+ noteId: args.note_id
4481
+ };
4482
+ }
4483
+ var awardEmojiTools = {
4484
+ /**
4485
+ * Add a reaction (award emoji) to a resource or note
4486
+ */
4487
+ gitlab_create_award_emoji: tool16({
4488
+ description: `Add a reaction (award emoji) to a merge request, issue, snippet, or a note/comment on these resources.
4489
+
4490
+ Common emoji names: thumbsup, thumbsdown, smile, tada, rocket, eyes, heart, +1, -1
4491
+
4492
+ To react to a specific comment/note, provide the note_id parameter.
4493
+
4494
+ Examples:
4495
+ - React to MR: resource_type="merge_request", project_id="group/project", resource_iid=123, name="thumbsup"
4496
+ - React to issue comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789, name="rocket"`,
4497
+ args: {
4498
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource to add the reaction to"),
4499
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4500
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4501
+ name: z16.string().describe(
4502
+ 'Emoji name without colons (e.g., "thumbsup", "rocket", "eyes", "heart", "tada")'
4503
+ ),
4504
+ note_id: z16.number().optional().describe("Note/comment ID to add the reaction to (if reacting to a specific comment)")
4505
+ },
4506
+ execute: async (args, _ctx) => {
4507
+ validateAwardEmojiParams(args);
4508
+ const client = getGitLabClient();
4509
+ const resource = buildResourceId(args);
4510
+ const result = await client.createAwardEmoji(resource, args.name);
4511
+ return JSON.stringify(result, null, 2);
4512
+ }
4513
+ }),
4514
+ /**
4515
+ * List all reactions on a resource or note
4516
+ */
4517
+ gitlab_list_award_emoji: tool16({
4518
+ description: `List all reactions (award emoji) on a merge request, issue, snippet, or a specific note/comment.
4519
+
4520
+ Examples:
4521
+ - List reactions on MR: resource_type="merge_request", project_id="group/project", resource_iid=123
4522
+ - List reactions on a comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789`,
4523
+ args: {
4524
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4525
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4526
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4527
+ note_id: z16.number().optional().describe("Note/comment ID to list reactions for (if listing for a specific comment)")
4528
+ },
4529
+ execute: async (args, _ctx) => {
4530
+ validateAwardEmojiParams(args);
4531
+ const client = getGitLabClient();
4532
+ const resource = buildResourceId(args);
4533
+ const result = await client.listAwardEmoji(resource);
4534
+ return JSON.stringify(result, null, 2);
4535
+ }
4536
+ }),
4537
+ /**
4538
+ * Remove a reaction from a resource or note
4539
+ */
4540
+ gitlab_delete_award_emoji: tool16({
4541
+ description: `Remove a reaction (award emoji) from a merge request, issue, snippet, or note/comment.
4542
+
4543
+ You need the award_id which can be found using gitlab_list_award_emoji.
4544
+
4545
+ Examples:
4546
+ - Remove reaction from MR: resource_type="merge_request", project_id="group/project", resource_iid=123, award_id=456
4547
+ - Remove from comment: resource_type="issue", ..., note_id=789, award_id=456`,
4548
+ args: {
4549
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4550
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4551
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4552
+ award_id: z16.number().describe("ID of the award emoji to remove"),
4553
+ note_id: z16.number().optional().describe("Note/comment ID if removing from a specific comment")
4554
+ },
4555
+ execute: async (args, _ctx) => {
4556
+ validateAwardEmojiParams(args);
4557
+ const client = getGitLabClient();
4558
+ const resource = buildResourceId(args);
4559
+ await client.deleteAwardEmoji(resource, args.award_id);
4560
+ return JSON.stringify({ success: true, message: "Award emoji removed" }, null, 2);
4561
+ }
4562
+ })
4563
+ };
4564
+
4296
4565
  // src/index.ts
4297
4566
  var gitlabPlugin = async (_input) => {
4298
4567
  return {
@@ -4328,7 +4597,9 @@ var gitlabPlugin = async (_input) => {
4328
4597
  // Git Tools
4329
4598
  ...gitTools,
4330
4599
  // Audit Tools
4331
- ...auditTools
4600
+ ...auditTools,
4601
+ // Award Emoji (Reactions) Tools
4602
+ ...awardEmojiTools
4332
4603
  }
4333
4604
  };
4334
4605
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/opencode-gitlab-plugin",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
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",