@gitlab/opencode-gitlab-plugin 1.6.2 → 1.7.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,19 @@
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.0](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.6.2...v1.7.0) (2026-02-24)
6
+
7
+
8
+ ### ✨ Features
9
+
10
+ * **award-emoji:** add tools for managing reactions on resources ([77b8ad5](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/77b8ad5656b681c3a6c9395f2a0025126e4c39bb))
11
+ * **discussions:** use GraphQL for discussion resolution ([b6ed342](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/b6ed342cea8f726b14a2850350d217a937406caa))
12
+
13
+
14
+ ### 🐛 Bug Fixes
15
+
16
+ * **todos:** handle 404 gracefully when marking todo as done ([2a26763](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/2a267632012d89884ebc3462ac0adafc5ffd9286))
17
+
5
18
  ## [1.6.2](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.6.1...v1.6.2) (2026-02-04)
6
19
 
7
20
 
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();
@@ -3678,6 +3866,9 @@ Examples:
3678
3866
  description: `Mark a discussion thread as resolved or unresolve it.
3679
3867
  Only works for resolvable discussions (MRs and issues only).
3680
3868
 
3869
+ Uses GraphQL API which handles all discussion types including outdated discussions
3870
+ (those with resolved: null) that the REST API cannot handle.
3871
+
3681
3872
  Use after addressing feedback to indicate the discussion is complete.`,
3682
3873
  args: {
3683
3874
  resource_type: z12.enum(["merge_request", "issue"]).describe("Type of resource (only MR and issue discussions can be resolved)"),
@@ -3688,37 +3879,12 @@ Use after addressing feedback to indicate the discussion is complete.`,
3688
3879
  },
3689
3880
  execute: async (args, _ctx) => {
3690
3881
  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
- }
3882
+ const resolve2 = args.action === "resolve";
3883
+ return JSON.stringify(
3884
+ await client.toggleDiscussionResolved(args.discussion_id, resolve2),
3885
+ null,
3886
+ 2
3887
+ );
3722
3888
  }
3723
3889
  })
3724
3890
  };
@@ -4293,6 +4459,107 @@ Note: Requires administrator access.`,
4293
4459
  })
4294
4460
  };
4295
4461
 
4462
+ // src/tools/award-emoji.ts
4463
+ import { tool as tool16 } from "@opencode-ai/plugin";
4464
+ var z16 = tool16.schema;
4465
+ function validateAwardEmojiParams(args) {
4466
+ if (!args.project_id) {
4467
+ throw new Error("project_id is required");
4468
+ }
4469
+ if (args.resource_iid == null) {
4470
+ throw new Error("resource_iid is required");
4471
+ }
4472
+ }
4473
+ function buildResourceId(args) {
4474
+ return {
4475
+ projectId: args.project_id,
4476
+ resourceType: args.resource_type,
4477
+ resourceIid: args.resource_iid,
4478
+ noteId: args.note_id
4479
+ };
4480
+ }
4481
+ var awardEmojiTools = {
4482
+ /**
4483
+ * Add a reaction (award emoji) to a resource or note
4484
+ */
4485
+ gitlab_create_award_emoji: tool16({
4486
+ description: `Add a reaction (award emoji) to a merge request, issue, snippet, or a note/comment on these resources.
4487
+
4488
+ Common emoji names: thumbsup, thumbsdown, smile, tada, rocket, eyes, heart, +1, -1
4489
+
4490
+ To react to a specific comment/note, provide the note_id parameter.
4491
+
4492
+ Examples:
4493
+ - React to MR: resource_type="merge_request", project_id="group/project", resource_iid=123, name="thumbsup"
4494
+ - React to issue comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789, name="rocket"`,
4495
+ args: {
4496
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource to add the reaction to"),
4497
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4498
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4499
+ name: z16.string().describe(
4500
+ 'Emoji name without colons (e.g., "thumbsup", "rocket", "eyes", "heart", "tada")'
4501
+ ),
4502
+ note_id: z16.number().optional().describe("Note/comment ID to add the reaction to (if reacting to a specific comment)")
4503
+ },
4504
+ execute: async (args, _ctx) => {
4505
+ validateAwardEmojiParams(args);
4506
+ const client = getGitLabClient();
4507
+ const resource = buildResourceId(args);
4508
+ const result = await client.createAwardEmoji(resource, args.name);
4509
+ return JSON.stringify(result, null, 2);
4510
+ }
4511
+ }),
4512
+ /**
4513
+ * List all reactions on a resource or note
4514
+ */
4515
+ gitlab_list_award_emoji: tool16({
4516
+ description: `List all reactions (award emoji) on a merge request, issue, snippet, or a specific note/comment.
4517
+
4518
+ Examples:
4519
+ - List reactions on MR: resource_type="merge_request", project_id="group/project", resource_iid=123
4520
+ - List reactions on a comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789`,
4521
+ args: {
4522
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4523
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4524
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4525
+ note_id: z16.number().optional().describe("Note/comment ID to list reactions for (if listing for a specific comment)")
4526
+ },
4527
+ execute: async (args, _ctx) => {
4528
+ validateAwardEmojiParams(args);
4529
+ const client = getGitLabClient();
4530
+ const resource = buildResourceId(args);
4531
+ const result = await client.listAwardEmoji(resource);
4532
+ return JSON.stringify(result, null, 2);
4533
+ }
4534
+ }),
4535
+ /**
4536
+ * Remove a reaction from a resource or note
4537
+ */
4538
+ gitlab_delete_award_emoji: tool16({
4539
+ description: `Remove a reaction (award emoji) from a merge request, issue, snippet, or note/comment.
4540
+
4541
+ You need the award_id which can be found using gitlab_list_award_emoji.
4542
+
4543
+ Examples:
4544
+ - Remove reaction from MR: resource_type="merge_request", project_id="group/project", resource_iid=123, award_id=456
4545
+ - Remove from comment: resource_type="issue", ..., note_id=789, award_id=456`,
4546
+ args: {
4547
+ resource_type: z16.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4548
+ project_id: z16.string().describe("Project ID or URL-encoded path"),
4549
+ resource_iid: z16.number().describe("Internal ID of the merge request, issue, or snippet"),
4550
+ award_id: z16.number().describe("ID of the award emoji to remove"),
4551
+ note_id: z16.number().optional().describe("Note/comment ID if removing from a specific comment")
4552
+ },
4553
+ execute: async (args, _ctx) => {
4554
+ validateAwardEmojiParams(args);
4555
+ const client = getGitLabClient();
4556
+ const resource = buildResourceId(args);
4557
+ await client.deleteAwardEmoji(resource, args.award_id);
4558
+ return JSON.stringify({ success: true, message: "Award emoji removed" }, null, 2);
4559
+ }
4560
+ })
4561
+ };
4562
+
4296
4563
  // src/index.ts
4297
4564
  var gitlabPlugin = async (_input) => {
4298
4565
  return {
@@ -4328,7 +4595,9 @@ var gitlabPlugin = async (_input) => {
4328
4595
  // Git Tools
4329
4596
  ...gitTools,
4330
4597
  // Audit Tools
4331
- ...auditTools
4598
+ ...auditTools,
4599
+ // Award Emoji (Reactions) Tools
4600
+ ...awardEmojiTools
4332
4601
  }
4333
4602
  };
4334
4603
  };
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.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",