@gitlab/opencode-gitlab-plugin 1.5.2 → 1.5.4

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.5.4](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.5.3...v1.5.4) (2026-02-03)
6
+
7
+
8
+ ### 📚 Documentation
9
+
10
+ * update README for unified discussion tools consolidation ([a832573](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/a832573159b157c0cfe806ab1f12047325e14c56))
11
+
12
+ ## [1.5.3](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.5.2...v1.5.3) (2026-02-03)
13
+
14
+
15
+ ### 🐛 Bug Fixes
16
+
17
+ * use null checks for stricter validation ([6c39c81](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/6c39c81aa839a99a0c12b3aa57d9376e5e3788d4))
18
+
19
+
20
+ ### ♻️ Code Refactoring
21
+
22
+ * consolidate discussion tools into unified interface ([aa0cefd](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/aa0cefdcc36fbd00c375ca17b0d75dbda3ecbe18))
23
+ * remove gitlab_create_commit tool ([4c4944d](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/commit/4c4944d8afcd710e7b0910d82197802eee75e6fd))
24
+
5
25
  ## [1.5.2](https://gitlab.com/gitlab-org/editor-extensions/opencode-gitlab-plugin/compare/v1.5.1...v1.5.2) (2026-02-02)
6
26
 
7
27
 
package/README.md CHANGED
@@ -53,19 +53,19 @@ A comprehensive GitLab API plugin for OpenCode that provides AI-powered access t
53
53
  - **Rate Limiting**: Built-in handling for GitLab API rate limits
54
54
  - **Caching**: Efficient API response handling
55
55
  - **Modular Architecture**: Clean separation of concerns with client and tool modules
56
- - **Comprehensive Testing**: 181 tests with full coverage of all features
56
+ - **Comprehensive Testing**: 180 tests with full coverage of all features
57
57
 
58
58
  ### GraphQL-Powered Tools
59
59
 
60
60
  The following tools use GitLab's GraphQL API for enhanced functionality:
61
61
 
62
- | Category | Tools | Benefits |
63
- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
64
- | **TODOs** | `gitlab_list_todos`, `gitlab_get_todo_count` | Cursor-based pagination, rich filtering |
65
- | **Notes** | `gitlab_list_mr_notes`, `gitlab_list_issue_notes`, `gitlab_list_epic_notes`, `gitlab_list_snippet_notes` | Efficient pagination, consistent response format |
66
- | **Discussions** | `gitlab_list_mr_discussions`, `gitlab_list_issue_discussions`, `gitlab_list_epic_discussions`, `gitlab_list_snippet_discussions` | Cursor-based pagination, nested note support |
67
- | **Auto-merge** | `gitlab_set_mr_auto_merge` | MWPS (Merge When Pipeline Succeeds), merge train support |
68
- | **Security** | All vulnerability management tools | Type-safe GID validation, mutation support |
62
+ | Category | Tools | Benefits |
63
+ | --------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
64
+ | **TODOs** | `gitlab_list_todos`, `gitlab_get_todo_count` | Cursor-based pagination, rich filtering |
65
+ | **Notes** | `gitlab_list_mr_notes`, `gitlab_list_issue_notes`, `gitlab_list_epic_notes`, `gitlab_list_snippet_notes` | Efficient pagination, consistent response format |
66
+ | **Discussions** | `gitlab_list_discussions`, `gitlab_get_discussion`, `gitlab_create_discussion`, `gitlab_resolve_discussion` | Unified interface for all resource types, cursor-based pagination, code position support |
67
+ | **Auto-merge** | `gitlab_set_mr_auto_merge` | MWPS (Merge When Pipeline Succeeds), merge train support |
68
+ | **Security** | All vulnerability management tools | Type-safe GID validation, mutation support |
69
69
 
70
70
  ## 🏗️ Architecture
71
71
 
@@ -134,7 +134,7 @@ graph LR
134
134
  B --> B1[readTokenFromAuthStorage]
135
135
  B --> B2[getGitLabClient]
136
136
 
137
- C --> C1[60+ Tool Definitions]
137
+ C --> C1[81 Tool Definitions]
138
138
 
139
139
  D --> A
140
140
  D --> B
@@ -287,9 +287,9 @@ Or for API tokens:
287
287
 
288
288
  ## 🛠️ Available Tools
289
289
 
290
- The plugin provides **102 tools** organized into the following categories:
290
+ The plugin provides **81 tools** organized into the following categories:
291
291
 
292
- ### Merge Request Tools (16 tools)
292
+ ### Merge Request Tools (10 tools)
293
293
 
294
294
  | Tool | Description |
295
295
  | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
@@ -298,34 +298,23 @@ The plugin provides **102 tools** organized into the following categories:
298
298
  | `gitlab_create_merge_request` | Create a new merge request |
299
299
  | `gitlab_update_merge_request` | Update merge request title, description, state, assignees, reviewers, and labels |
300
300
  | `gitlab_get_mr_changes` | Get file changes/diffs for a merge request |
301
- | `gitlab_list_mr_discussions` | List discussion threads on a merge request (GraphQL with cursor-based pagination) |
302
- | `gitlab_get_mr_discussion` | Get a specific discussion thread with all replies |
303
301
  | `gitlab_list_mr_notes` | List all comments with cursor-based pagination (GraphQL) |
304
- | `gitlab_create_mr_note` | Add a comment or reply to a merge request discussion |
305
- | `gitlab_create_mr_discussion` | Start a new discussion thread (optionally on specific code lines) |
306
- | `gitlab_resolve_mr_discussion` | Mark a discussion as resolved after addressing feedback |
307
- | `gitlab_unresolve_mr_discussion` | Reopen a resolved discussion |
308
302
  | `gitlab_get_mr_commits` | Get all commits in a merge request |
309
303
  | `gitlab_get_mr_pipelines` | Get all pipelines for a merge request |
310
304
  | `gitlab_list_merge_request_diffs` | List file diffs with pagination support for large changesets |
311
305
  | `gitlab_set_mr_auto_merge` | Enable auto-merge (MWPS) using GraphQL API when pipeline succeeds |
312
306
 
313
- ### Issue Tools (10 tools)
307
+ ### Issue Tools (5 tools)
314
308
 
315
- | Tool | Description |
316
- | ----------------------------------- | ---------------------------------------------------------------------------- |
317
- | `gitlab_create_issue` | Create a new issue with title, description, labels, assignees, and milestone |
318
- | `gitlab_get_issue` | Get issue details including state, author, assignees, labels, and comments |
319
- | `gitlab_list_issues` | List issues with filtering by state, labels, assignee, and milestone |
320
- | `gitlab_list_issue_notes` | List all comments with cursor-based pagination (GraphQL) |
321
- | `gitlab_list_issue_discussions` | List discussion threads with cursor-based pagination (GraphQL) |
322
- | `gitlab_get_issue_discussion` | Get a specific discussion thread with context |
323
- | `gitlab_create_issue_note` | Add a comment or reply to an issue discussion |
324
- | `gitlab_get_issue_note` | Get a specific note by ID with full details |
325
- | `gitlab_resolve_issue_discussion` | Mark an issue discussion as resolved |
326
- | `gitlab_unresolve_issue_discussion` | Reopen a resolved issue discussion |
309
+ | Tool | Description |
310
+ | ------------------------- | ---------------------------------------------------------------------------- |
311
+ | `gitlab_create_issue` | Create a new issue with title, description, labels, assignees, and milestone |
312
+ | `gitlab_get_issue` | Get issue details including state, author, assignees, labels, and comments |
313
+ | `gitlab_list_issues` | List issues with filtering by state, labels, assignee, and milestone |
314
+ | `gitlab_list_issue_notes` | List all comments with cursor-based pagination (GraphQL) |
315
+ | `gitlab_get_issue_note` | Get a specific note by ID with full details |
327
316
 
328
- ### Epic Tools (12 tools)
317
+ ### Epic Tools (9 tools)
329
318
 
330
319
  | Tool | Description |
331
320
  | ------------------------------- | ----------------------------------------------------------------------------- |
@@ -337,9 +326,6 @@ The plugin provides **102 tools** organized into the following categories:
337
326
  | `gitlab_add_issue_to_epic` | Link an issue to an epic |
338
327
  | `gitlab_remove_issue_from_epic` | Unlink an issue from an epic |
339
328
  | `gitlab_list_epic_notes` | List all comments with cursor-based pagination (GraphQL) |
340
- | `gitlab_list_epic_discussions` | List discussion threads with cursor-based pagination (GraphQL) |
341
- | `gitlab_create_epic_note` | Add a comment or reply to an epic discussion |
342
- | `gitlab_get_epic_discussion` | Get a specific epic discussion thread |
343
329
  | `gitlab_get_epic_note` | Get a specific epic note by ID |
344
330
 
345
331
  ### Pipeline Tools (8 tools)
@@ -355,22 +341,17 @@ The plugin provides **102 tools** organized into the following categories:
355
341
  | `gitlab_lint_ci_config` | Validate CI/CD YAML configuration with project context |
356
342
  | `gitlab_lint_existing_ci_config` | Validate existing .gitlab-ci.yml from repository |
357
343
 
358
- ### Repository Tools (12 tools)
359
-
360
- | Tool | Description |
361
- | --------------------------------- | ---------------------------------------------------------------------------- |
362
- | `gitlab_get_file` | Get file contents from any branch, tag, or commit |
363
- | `gitlab_get_commit` | Get commit details with metadata, author, and stats |
364
- | `gitlab_list_commits` | List commits with filtering by branch, path, and dates |
365
- | `gitlab_get_commit_diff` | Get diff for a specific commit |
366
- | `gitlab_create_commit` | Create a commit with multiple file operations (create, update, delete, move) |
367
- | `gitlab_list_repository_tree` | List files and directories at a given path |
368
- | `gitlab_list_branches` | List all branches in a repository |
369
- | `gitlab_list_commit_discussions` | List discussion threads on a commit |
370
- | `gitlab_get_commit_discussion` | Get a specific commit discussion thread |
371
- | `gitlab_create_commit_note` | Add a comment to a commit (optionally line-specific) |
372
- | `gitlab_create_commit_discussion` | Start a new discussion on a commit |
373
- | `gitlab_get_commit_comments` | Get all commit comments in flat structure |
344
+ ### Repository Tools (7 tools)
345
+
346
+ | Tool | Description |
347
+ | ----------------------------- | ------------------------------------------------------ |
348
+ | `gitlab_get_file` | Get file contents from any branch, tag, or commit |
349
+ | `gitlab_get_commit` | Get commit details with metadata, author, and stats |
350
+ | `gitlab_list_commits` | List commits with filtering by branch, path, and dates |
351
+ | `gitlab_get_commit_diff` | Get diff for a specific commit |
352
+ | `gitlab_list_repository_tree` | List files and directories at a given path |
353
+ | `gitlab_list_branches` | List all branches in a repository |
354
+ | `gitlab_get_commit_comments` | Get all commit comments in flat structure |
374
355
 
375
356
  ### Search Tools (11 tools)
376
357
 
@@ -431,22 +412,20 @@ The plugin provides **102 tools** organized into the following categories:
431
412
  | `gitlab_list_project_members` | List all members of a project |
432
413
  | `gitlab_get_current_user` | Get authenticated user information |
433
414
 
434
- ### Snippet Tools (5 tools)
415
+ ### Snippet Tools (1 tool)
435
416
 
436
- | Tool | Description |
437
- | ---------------------------------- | -------------------------------------------------------------- |
438
- | `gitlab_list_snippet_discussions` | List discussion threads with cursor-based pagination (GraphQL) |
439
- | `gitlab_get_snippet_discussion` | Get a specific snippet discussion thread |
440
- | `gitlab_list_snippet_notes` | List all comments with cursor-based pagination (GraphQL) |
441
- | `gitlab_create_snippet_note` | Add a comment or reply to a snippet discussion |
442
- | `gitlab_create_snippet_discussion` | Start a new discussion on a snippet |
417
+ | Tool | Description |
418
+ | --------------------------- | -------------------------------------------------------- |
419
+ | `gitlab_list_snippet_notes` | List all comments with cursor-based pagination (GraphQL) |
443
420
 
444
- ### Discussion Tools (2 tools)
421
+ ### Discussion Tools (4 tools)
445
422
 
446
- | Tool | Description |
447
- | ---------------------------- | -------------------------------------------------------------------------------------------- |
448
- | `gitlab_reply_to_discussion` | Universal tool for replying to any discussion thread (MRs, issues, epics, commits, snippets) |
449
- | `gitlab_get_discussion` | Universal tool for fetching discussion details from any resource type |
423
+ | Tool | Description |
424
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
425
+ | `gitlab_list_discussions` | List discussions (comment threads) on any GitLab resource (MRs, issues, epics, commits, snippets) with cursor pagination |
426
+ | `gitlab_get_discussion` | Get a specific discussion thread with all replies from any resource type |
427
+ | `gitlab_create_discussion` | Create a new discussion thread OR reply to an existing one (supports code-position comments for MRs and commits) |
428
+ | `gitlab_resolve_discussion` | Mark a discussion thread as resolved or unresolve it (MRs and issues only) |
450
429
 
451
430
  ### Audit Event Tools (3 tools)
452
431
 
@@ -516,18 +495,29 @@ const mr = await plugin.tool.gitlab_get_merge_request.execute({
516
495
  include_changes: true,
517
496
  });
518
497
 
519
- // Get discussions
520
- const discussions = await plugin.tool.gitlab_list_mr_discussions.execute({
498
+ // Get discussions (unified tool supports all resource types)
499
+ const discussions = await plugin.tool.gitlab_list_discussions.execute({
500
+ resource_type: 'merge_request',
521
501
  project_id: 'gitlab-org/gitlab',
522
- mr_iid: 12345,
502
+ iid: 12345,
523
503
  });
524
504
 
525
- // Add a review comment
526
- await plugin.tool.gitlab_create_mr_note.execute({
505
+ // Add a review comment (creates a new discussion)
506
+ await plugin.tool.gitlab_create_discussion.execute({
507
+ resource_type: 'merge_request',
527
508
  project_id: 'gitlab-org/gitlab',
528
- mr_iid: 12345,
509
+ iid: 12345,
529
510
  body: 'LGTM! Great work on this feature.',
530
511
  });
512
+
513
+ // Reply to an existing discussion thread
514
+ await plugin.tool.gitlab_create_discussion.execute({
515
+ resource_type: 'merge_request',
516
+ project_id: 'gitlab-org/gitlab',
517
+ iid: 12345,
518
+ discussion_id: discussions.discussions.nodes[0].id,
519
+ body: 'Thanks for addressing the feedback!',
520
+ });
531
521
  ```
532
522
 
533
523
  ### Example 3: Debug Failed Pipeline
@@ -588,10 +578,11 @@ const epicIssues = await plugin.tool.gitlab_list_epic_issues.execute({
588
578
  epic_iid: epic.iid,
589
579
  });
590
580
 
591
- // Add a comment
592
- await plugin.tool.gitlab_create_epic_note.execute({
581
+ // Add a comment (creates a new discussion)
582
+ await plugin.tool.gitlab_create_discussion.execute({
583
+ resource_type: 'epic',
593
584
  group_id: 'my-group',
594
- epic_iid: epic.iid,
585
+ iid: epic.iid,
595
586
  body: 'Epic created and issues linked successfully!',
596
587
  });
597
588
  ```
@@ -774,11 +765,12 @@ opencode-gitlab-plugin/
774
765
  │ │ ├── merge-requests.ts # MR tool definitions
775
766
  │ │ ├── pipelines.ts # Pipeline tool definitions
776
767
  │ │ ├── repository.ts # Repository tool definitions
768
+ │ │ ├── discussions-unified.ts # Unified discussion tools (4 tools)
777
769
  │ │ └── ... # Other tool definitions
778
770
  │ ├── index.ts # Main plugin entry point
779
771
  │ ├── utils.ts # Utility functions
780
772
  │ └── validation.ts # GID validation utilities
781
- ├── tests/ # Test suite (142 tests)
773
+ ├── tests/ # Test suite (180 tests)
782
774
  │ ├── client/ # Client tests
783
775
  │ ├── tools/ # Tool tests
784
776
  │ ├── validation.test.ts # Validation tests
@@ -900,7 +892,7 @@ npm run test:coverage
900
892
 
901
893
  **Test Coverage:**
902
894
 
903
- - **181 tests** across 21 test files
895
+ - **180 tests** across 21 test files
904
896
  - Client tests for all API methods (REST and GraphQL)
905
897
  - Tool tests for all tool definitions
906
898
  - Validation tests for GID utilities
package/dist/index.d.ts CHANGED
@@ -4,9 +4,9 @@ import { Plugin } from '@opencode-ai/plugin';
4
4
  * GitLab Tools Plugin for OpenCode
5
5
  *
6
6
  * Provides tools for interacting with GitLab:
7
- * - Merge requests: get, list, changes, discussions, notes (list/create), diffs (paginated)
8
- * - Issues: create, get, list, notes (list/create/get), discussions
9
- * - Epics: get, list, create, update, manage issues, notes (list/create/get), discussions
7
+ * - Merge requests: get, list, changes, create, update, commits, pipelines, diffs (paginated), auto-merge
8
+ * - Issues: create, get, list
9
+ * - Epics: get, list, create, update, manage issues
10
10
  * - Pipelines: list, get, jobs, logs, retry, failing jobs, CI linter
11
11
  * - Repository: get file, commits, diff, branches, tree, create commit, commit comments
12
12
  * - Search: projects, issues, MRs, blobs, commits, etc.
@@ -17,8 +17,8 @@ import { Plugin } from '@opencode-ai/plugin';
17
17
  * - Audit: list project/group/instance audit events
18
18
  * - Wikis: get wiki page
19
19
  * - Work Items: get, list, create, update, notes
20
- * - Snippets: discussions, notes
21
- * - Discussions: universal discussion tools for all resource types
20
+ * - Discussions: unified tools for all resource types (list, get, create/reply, resolve)
21
+ * - Notes: list/get notes (flat view) for MR, issue, epic, snippet
22
22
  * - Git: execute safe, read-only git commands in repository
23
23
  */
24
24
  declare const gitlabPlugin: Plugin;
package/dist/index.js CHANGED
@@ -2086,29 +2086,6 @@ Returns the list of files changed with their diffs.`,
2086
2086
  return JSON.stringify(changes, null, 2);
2087
2087
  }
2088
2088
  }),
2089
- gitlab_list_mr_discussions: tool({
2090
- description: `List discussions (comment threads) on a merge request.
2091
- Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
2092
- Note: For a flattened list of all comments, use gitlab_list_mr_notes instead.`,
2093
- args: {
2094
- project_id: z.string().describe("The project ID or URL-encoded path"),
2095
- mr_iid: z.number().describe("The internal ID of the merge request"),
2096
- first: z.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2097
- after: z.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2098
- last: z.number().optional().describe("Number of discussions to fetch from the end"),
2099
- before: z.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
2100
- },
2101
- execute: async (args, _ctx) => {
2102
- const client = getGitLabClient();
2103
- const discussions = await client.listMrDiscussions(args.project_id, args.mr_iid, {
2104
- first: args.first,
2105
- after: args.after,
2106
- last: args.last,
2107
- before: args.before
2108
- });
2109
- return JSON.stringify(discussions, null, 2);
2110
- }
2111
- }),
2112
2089
  gitlab_list_mr_notes: tool({
2113
2090
  description: `List all notes/comments on a merge request using GraphQL API with pagination support.
2114
2091
  Returns all comments including system notes, code review comments, and general discussion.
@@ -2136,117 +2113,6 @@ Use 'before' with the 'startCursor' from pageInfo to get the previous page.`,
2136
2113
  return JSON.stringify(result, null, 2);
2137
2114
  }
2138
2115
  }),
2139
- gitlab_create_mr_note: tool({
2140
- description: `Add a comment/note to a merge request.
2141
- If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
2142
- Use gitlab_list_mr_discussions to find discussion IDs for existing threads.`,
2143
- args: {
2144
- project_id: z.string().describe("The project ID or URL-encoded path"),
2145
- mr_iid: z.number().describe("The internal ID of the merge request"),
2146
- body: z.string().describe("The content of the note/comment (supports Markdown)"),
2147
- discussion_id: z.string().optional().describe(
2148
- "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
2149
- )
2150
- },
2151
- execute: async (args, _ctx) => {
2152
- const client = getGitLabClient();
2153
- const note = await client.createMrNote(
2154
- args.project_id,
2155
- args.mr_iid,
2156
- args.body,
2157
- args.discussion_id
2158
- );
2159
- return JSON.stringify(note, null, 2);
2160
- }
2161
- }),
2162
- gitlab_get_mr_discussion: tool({
2163
- description: `Get a specific discussion thread from a merge request with all its replies.
2164
- Returns the discussion with its 'notes' array containing all comments in the thread.
2165
- Use this to get the full context of a specific conversation.`,
2166
- args: {
2167
- project_id: z.string().describe("The project ID or URL-encoded path"),
2168
- mr_iid: z.number().describe("The internal ID of the merge request"),
2169
- discussion_id: z.string().describe("The ID of the discussion thread")
2170
- },
2171
- execute: async (args, _ctx) => {
2172
- const client = getGitLabClient();
2173
- const discussion = await client.getMrDiscussion(
2174
- args.project_id,
2175
- args.mr_iid,
2176
- args.discussion_id
2177
- );
2178
- return JSON.stringify(discussion, null, 2);
2179
- }
2180
- }),
2181
- gitlab_resolve_mr_discussion: tool({
2182
- description: `Mark a merge request discussion thread as resolved.
2183
- Use this after addressing feedback in a code review to indicate the discussion is complete.
2184
- Only works for resolvable discussions (typically code review comments).`,
2185
- args: {
2186
- project_id: z.string().describe("The project ID or URL-encoded path"),
2187
- mr_iid: z.number().describe("The internal ID of the merge request"),
2188
- discussion_id: z.string().describe("The ID of the discussion thread to resolve")
2189
- },
2190
- execute: async (args, _ctx) => {
2191
- const client = getGitLabClient();
2192
- const discussion = await client.resolveMrDiscussion(
2193
- args.project_id,
2194
- args.mr_iid,
2195
- args.discussion_id
2196
- );
2197
- return JSON.stringify(discussion, null, 2);
2198
- }
2199
- }),
2200
- gitlab_unresolve_mr_discussion: tool({
2201
- description: `Reopen a resolved merge request discussion thread.
2202
- Use this to indicate that a previously resolved discussion needs more attention.`,
2203
- args: {
2204
- project_id: z.string().describe("The project ID or URL-encoded path"),
2205
- mr_iid: z.number().describe("The internal ID of the merge request"),
2206
- discussion_id: z.string().describe("The ID of the discussion thread to unresolve")
2207
- },
2208
- execute: async (args, _ctx) => {
2209
- const client = getGitLabClient();
2210
- const discussion = await client.unresolveMrDiscussion(
2211
- args.project_id,
2212
- args.mr_iid,
2213
- args.discussion_id
2214
- );
2215
- return JSON.stringify(discussion, null, 2);
2216
- }
2217
- }),
2218
- gitlab_create_mr_discussion: tool({
2219
- description: `Start a new discussion thread on a merge request.
2220
- Creates a new discussion with an initial comment. Optionally can be positioned on specific code.
2221
- For general comments, just provide the body. For code comments, provide position information.`,
2222
- args: {
2223
- project_id: z.string().describe("The project ID or URL-encoded path"),
2224
- mr_iid: z.number().describe("The internal ID of the merge request"),
2225
- body: z.string().describe("The content of the initial comment (supports Markdown)"),
2226
- position: z.object({
2227
- base_sha: z.string().describe("SHA of the base commit (merge base)"),
2228
- start_sha: z.string().describe("SHA of the commit when the MR was created"),
2229
- head_sha: z.string().describe("SHA of the HEAD commit"),
2230
- position_type: z.enum(["text", "image"]).describe("Type of position (text or image)"),
2231
- new_path: z.string().optional().describe("Path of the file after changes"),
2232
- old_path: z.string().optional().describe("Path of the file before changes"),
2233
- new_line: z.number().optional().describe("Line number in the new version"),
2234
- old_line: z.number().optional().describe("Line number in the old version")
2235
- }).optional().describe(
2236
- "Position information for code comments. Required fields: base_sha, start_sha, head_sha, position_type. For line comments also provide new_path/old_path and new_line/old_line."
2237
- )
2238
- },
2239
- execute: async (args, _ctx) => {
2240
- const client = getGitLabClient();
2241
- const discussion = await client.createMrDiscussion(
2242
- args.project_id,
2243
- args.mr_iid,
2244
- args.body,
2245
- args.position
2246
- );
2247
- return JSON.stringify(discussion, null, 2);
2248
- }
2249
- }),
2250
2116
  gitlab_create_merge_request: tool({
2251
2117
  description: `Create a new merge request.
2252
2118
  Returns the created merge request with all details.`,
@@ -2491,108 +2357,6 @@ Use 'before' with the 'startCursor' from pageInfo to get the previous page.`,
2491
2357
  return JSON.stringify(result, null, 2);
2492
2358
  }
2493
2359
  }),
2494
- gitlab_list_issue_discussions: tool2({
2495
- description: `List discussions (comment threads) on an issue.
2496
- Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
2497
- Use the discussion 'id' field to reply to a specific thread with gitlab_create_issue_note.`,
2498
- args: {
2499
- project_id: z2.string().describe("The project ID or URL-encoded path"),
2500
- issue_iid: z2.number().describe("The internal ID of the issue"),
2501
- first: z2.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2502
- after: z2.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2503
- last: z2.number().optional().describe("Number of discussions to fetch from the end"),
2504
- before: z2.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
2505
- },
2506
- execute: async (args, _ctx) => {
2507
- const client = getGitLabClient();
2508
- const discussions = await client.listIssueDiscussions(args.project_id, args.issue_iid, {
2509
- first: args.first,
2510
- after: args.after,
2511
- last: args.last,
2512
- before: args.before
2513
- });
2514
- return JSON.stringify(discussions, null, 2);
2515
- }
2516
- }),
2517
- gitlab_create_issue_note: tool2({
2518
- description: `Add a comment/note to an issue.
2519
- If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
2520
- Use gitlab_list_issue_discussions to find discussion IDs for existing threads.`,
2521
- args: {
2522
- project_id: z2.string().describe("The project ID or URL-encoded path"),
2523
- issue_iid: z2.number().describe("The internal ID of the issue"),
2524
- body: z2.string().describe("The content of the note/comment (supports Markdown)"),
2525
- discussion_id: z2.string().optional().describe(
2526
- "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
2527
- )
2528
- },
2529
- execute: async (args, _ctx) => {
2530
- const client = getGitLabClient();
2531
- const note = await client.createIssueNote(
2532
- args.project_id,
2533
- args.issue_iid,
2534
- args.body,
2535
- args.discussion_id
2536
- );
2537
- return JSON.stringify(note, null, 2);
2538
- }
2539
- }),
2540
- gitlab_get_issue_discussion: tool2({
2541
- description: `Get a specific discussion thread from an issue with all its replies.
2542
- Returns the discussion with its 'notes' array containing all comments in the thread.
2543
- Use this to get the full context of a specific conversation.`,
2544
- args: {
2545
- project_id: z2.string().describe("The project ID or URL-encoded path"),
2546
- issue_iid: z2.number().describe("The internal ID of the issue"),
2547
- discussion_id: z2.string().describe("The ID of the discussion thread")
2548
- },
2549
- execute: async (args, _ctx) => {
2550
- const client = getGitLabClient();
2551
- const discussion = await client.getIssueDiscussion(
2552
- args.project_id,
2553
- args.issue_iid,
2554
- args.discussion_id
2555
- );
2556
- return JSON.stringify(discussion, null, 2);
2557
- }
2558
- }),
2559
- gitlab_resolve_issue_discussion: tool2({
2560
- description: `Mark an issue discussion thread as resolved.
2561
- Use this after addressing feedback in a code review or issue discussion to indicate the discussion is complete.
2562
- Only works for resolvable discussions (typically multi-note threads, not individual notes).`,
2563
- args: {
2564
- project_id: z2.string().describe("The project ID or URL-encoded path"),
2565
- issue_iid: z2.number().describe("The internal ID of the issue"),
2566
- discussion_id: z2.string().describe("The ID of the discussion thread to resolve")
2567
- },
2568
- execute: async (args, _ctx) => {
2569
- const client = getGitLabClient();
2570
- const discussion = await client.resolveIssueDiscussion(
2571
- args.project_id,
2572
- args.issue_iid,
2573
- args.discussion_id
2574
- );
2575
- return JSON.stringify(discussion, null, 2);
2576
- }
2577
- }),
2578
- gitlab_unresolve_issue_discussion: tool2({
2579
- description: `Reopen a resolved issue discussion thread.
2580
- Use this to indicate that a previously resolved discussion needs more attention or further discussion.`,
2581
- args: {
2582
- project_id: z2.string().describe("The project ID or URL-encoded path"),
2583
- issue_iid: z2.number().describe("The internal ID of the issue"),
2584
- discussion_id: z2.string().describe("The ID of the discussion thread to unresolve")
2585
- },
2586
- execute: async (args, _ctx) => {
2587
- const client = getGitLabClient();
2588
- const discussion = await client.unresolveIssueDiscussion(
2589
- args.project_id,
2590
- args.issue_iid,
2591
- args.discussion_id
2592
- );
2593
- return JSON.stringify(discussion, null, 2);
2594
- }
2595
- }),
2596
2360
  gitlab_get_issue_note: tool2({
2597
2361
  description: `Get a single note/comment from an issue by its ID.
2598
2362
  Returns the full details of a specific note including author, body, timestamps, and metadata.
@@ -2777,71 +2541,6 @@ Use 'before' with the 'startCursor' from pageInfo to get the previous page.`,
2777
2541
  return JSON.stringify(result, null, 2);
2778
2542
  }
2779
2543
  }),
2780
- gitlab_list_epic_discussions: tool3({
2781
- description: `List discussions (comment threads) on an epic.
2782
- Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
2783
- Use the discussion 'id' field to reply to a specific thread with gitlab_create_epic_note.`,
2784
- args: {
2785
- group_id: z3.string().describe("The group ID or URL-encoded path"),
2786
- epic_iid: z3.number().describe("The internal ID of the epic"),
2787
- first: z3.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
2788
- after: z3.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
2789
- last: z3.number().optional().describe("Number of discussions to fetch from the end"),
2790
- before: z3.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
2791
- },
2792
- execute: async (args, _ctx) => {
2793
- const client = getGitLabClient();
2794
- const discussions = await client.listEpicDiscussions(args.group_id, args.epic_iid, {
2795
- first: args.first,
2796
- after: args.after,
2797
- last: args.last,
2798
- before: args.before
2799
- });
2800
- return JSON.stringify(discussions, null, 2);
2801
- }
2802
- }),
2803
- gitlab_create_epic_note: tool3({
2804
- description: `Add a comment/note to an epic.
2805
- If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
2806
- Use gitlab_list_epic_discussions to find discussion IDs for existing threads.`,
2807
- args: {
2808
- group_id: z3.string().describe("The group ID or URL-encoded path"),
2809
- epic_iid: z3.number().describe("The internal ID of the epic"),
2810
- body: z3.string().describe("The content of the note/comment (supports Markdown)"),
2811
- discussion_id: z3.string().optional().describe(
2812
- "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
2813
- )
2814
- },
2815
- execute: async (args, _ctx) => {
2816
- const client = getGitLabClient();
2817
- const note = await client.createEpicNote(
2818
- args.group_id,
2819
- args.epic_iid,
2820
- args.body,
2821
- args.discussion_id
2822
- );
2823
- return JSON.stringify(note, null, 2);
2824
- }
2825
- }),
2826
- gitlab_get_epic_discussion: tool3({
2827
- description: `Get a specific discussion thread from an epic with all its replies.
2828
- Returns the discussion with its 'notes' array containing all comments in the thread.
2829
- Use this to get the full context of a specific conversation.`,
2830
- args: {
2831
- group_id: z3.string().describe("The group ID or URL-encoded path"),
2832
- epic_iid: z3.number().describe("The internal ID of the epic"),
2833
- discussion_id: z3.string().describe("The ID of the discussion thread")
2834
- },
2835
- execute: async (args, _ctx) => {
2836
- const client = getGitLabClient();
2837
- const discussion = await client.getEpicDiscussion(
2838
- args.group_id,
2839
- args.epic_iid,
2840
- args.discussion_id
2841
- );
2842
- return JSON.stringify(discussion, null, 2);
2843
- }
2844
- }),
2845
2544
  gitlab_get_epic_note: tool3({
2846
2545
  description: `Get a single note/comment from an epic by its ID.
2847
2546
  Returns the full details of a specific note including author, body, timestamps, and metadata.
@@ -3092,40 +2791,6 @@ Returns commit metadata including author, message, stats, and parent commits.`,
3092
2791
  return JSON.stringify(diff, null, 2);
3093
2792
  }
3094
2793
  }),
3095
- gitlab_create_commit: tool5({
3096
- description: `Create a commit with multiple file actions.
3097
- Supports creating, updating, deleting, moving files, and changing permissions.`,
3098
- args: {
3099
- project_id: z5.string().describe("The project ID or URL-encoded path"),
3100
- branch: z5.string().describe("Name of the branch to commit into"),
3101
- commit_message: z5.string().describe("Commit message"),
3102
- actions: z5.array(
3103
- z5.object({
3104
- action: z5.enum(["create", "delete", "move", "update", "chmod"]).describe("The action to perform"),
3105
- file_path: z5.string().describe("Full path to the file"),
3106
- content: z5.string().optional().describe("File content (required for create/update)"),
3107
- encoding: z5.enum(["text", "base64"]).optional().describe("Encoding of content (default: text)"),
3108
- previous_path: z5.string().optional().describe("Original path (required for move)"),
3109
- execute_filemode: z5.boolean().optional().describe("Enable/disable execute flag (for chmod)")
3110
- })
3111
- ).describe("Array of file actions to perform"),
3112
- author_email: z5.string().optional().describe("Author email address"),
3113
- author_name: z5.string().optional().describe("Author name"),
3114
- start_branch: z5.string().optional().describe("Name of the branch to start from (if different from target branch)")
3115
- },
3116
- execute: async (args, _ctx) => {
3117
- const client = getGitLabClient();
3118
- const commit = await client.createCommit(args.project_id, {
3119
- branch: args.branch,
3120
- commit_message: args.commit_message,
3121
- actions: args.actions,
3122
- author_email: args.author_email,
3123
- author_name: args.author_name,
3124
- start_branch: args.start_branch
3125
- });
3126
- return JSON.stringify(commit, null, 2);
3127
- }
3128
- }),
3129
2794
  gitlab_list_repository_tree: tool5({
3130
2795
  description: `List files and directories in a repository.
3131
2796
  Returns the tree structure of the repository at a given path and ref.`,
@@ -3159,101 +2824,6 @@ Returns the tree structure of the repository at a given path and ref.`,
3159
2824
  return JSON.stringify(branches, null, 2);
3160
2825
  }
3161
2826
  }),
3162
- // ========== Commit Discussion Tools ==========
3163
- gitlab_list_commit_discussions: tool5({
3164
- description: `List discussions (comment threads) on a commit.
3165
- Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
3166
- Use the discussion 'id' field to reply to a specific thread with gitlab_create_commit_note.`,
3167
- args: {
3168
- project_id: z5.string().describe("The project ID or URL-encoded path"),
3169
- sha: z5.string().describe("The commit SHA")
3170
- },
3171
- execute: async (args, _ctx) => {
3172
- const client = getGitLabClient();
3173
- const discussions = await client.listCommitDiscussions(args.project_id, args.sha);
3174
- return JSON.stringify(discussions, null, 2);
3175
- }
3176
- }),
3177
- gitlab_get_commit_discussion: tool5({
3178
- description: `Get a specific discussion thread from a commit with all its replies.
3179
- Returns the discussion with its 'notes' array containing all comments in the thread.
3180
- Use this to get the full context of a specific conversation.`,
3181
- args: {
3182
- project_id: z5.string().describe("The project ID or URL-encoded path"),
3183
- sha: z5.string().describe("The commit SHA"),
3184
- discussion_id: z5.string().describe("The ID of the discussion thread")
3185
- },
3186
- execute: async (args, _ctx) => {
3187
- const client = getGitLabClient();
3188
- const discussion = await client.getCommitDiscussion(
3189
- args.project_id,
3190
- args.sha,
3191
- args.discussion_id
3192
- );
3193
- return JSON.stringify(discussion, null, 2);
3194
- }
3195
- }),
3196
- gitlab_create_commit_note: tool5({
3197
- description: `Add a comment/note to a commit.
3198
- If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
3199
- Optionally, you can create a line-specific comment by providing path, line, and line_type.
3200
- Use gitlab_list_commit_discussions to find discussion IDs for existing threads.`,
3201
- args: {
3202
- project_id: z5.string().describe("The project ID or URL-encoded path"),
3203
- sha: z5.string().describe("The commit SHA"),
3204
- body: z5.string().describe("The content of the note/comment (supports Markdown)"),
3205
- discussion_id: z5.string().optional().describe(
3206
- "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
3207
- ),
3208
- path: z5.string().optional().describe("The file path to comment on (for line-specific comments)"),
3209
- line: z5.number().optional().describe("The line number to comment on (for line-specific comments)"),
3210
- line_type: z5.enum(["new", "old"]).optional().describe(
3211
- 'The type of line being commented on: "new" for added lines, "old" for removed lines'
3212
- )
3213
- },
3214
- execute: async (args, _ctx) => {
3215
- const client = getGitLabClient();
3216
- const note = await client.createCommitNote(args.project_id, args.sha, args.body, {
3217
- discussion_id: args.discussion_id,
3218
- path: args.path,
3219
- line: args.line,
3220
- line_type: args.line_type
3221
- });
3222
- return JSON.stringify(note, null, 2);
3223
- }
3224
- }),
3225
- gitlab_create_commit_discussion: tool5({
3226
- description: `Start a new discussion thread on a commit.
3227
- Creates a new discussion with an initial comment. Optionally can be positioned on specific code.
3228
- For general comments, just provide the body. For code comments, provide position information.`,
3229
- args: {
3230
- project_id: z5.string().describe("The project ID or URL-encoded path"),
3231
- sha: z5.string().describe("The commit SHA"),
3232
- body: z5.string().describe("The content of the initial comment (supports Markdown)"),
3233
- position: z5.object({
3234
- base_sha: z5.string().describe("SHA of the base commit"),
3235
- start_sha: z5.string().describe("SHA of the start commit"),
3236
- head_sha: z5.string().describe("SHA of the HEAD commit"),
3237
- position_type: z5.enum(["text", "image"]).describe("Type of position (text or image)"),
3238
- new_path: z5.string().optional().describe("Path of the file after changes"),
3239
- old_path: z5.string().optional().describe("Path of the file before changes"),
3240
- new_line: z5.number().optional().describe("Line number in the new version"),
3241
- old_line: z5.number().optional().describe("Line number in the old version")
3242
- }).optional().describe(
3243
- "Position information for code comments. Required fields: base_sha, start_sha, head_sha, position_type. For line comments also provide new_path/old_path and new_line/old_line."
3244
- )
3245
- },
3246
- execute: async (args, _ctx) => {
3247
- const client = getGitLabClient();
3248
- const discussion = await client.createCommitDiscussion(
3249
- args.project_id,
3250
- args.sha,
3251
- args.body,
3252
- args.position
3253
- );
3254
- return JSON.stringify(discussion, null, 2);
3255
- }
3256
- }),
3257
2827
  gitlab_get_commit_comments: tool5({
3258
2828
  description: `Get all comments on a specific commit.
3259
2829
  Returns all comments (notes) that have been added to a commit, including line-specific comments.
@@ -3269,12 +2839,6 @@ This is different from discussions - it returns individual comments in a flat st
3269
2839
  }
3270
2840
  })
3271
2841
  };
3272
- var commitDiscussionTools = {
3273
- gitlab_list_commit_discussions: repositoryTools.gitlab_list_commit_discussions,
3274
- gitlab_get_commit_discussion: repositoryTools.gitlab_get_commit_discussion,
3275
- gitlab_create_commit_note: repositoryTools.gitlab_create_commit_note,
3276
- gitlab_create_commit_discussion: repositoryTools.gitlab_create_commit_discussion
3277
- };
3278
2842
 
3279
2843
  // src/tools/search.ts
3280
2844
  import { tool as tool6 } from "@opencode-ai/plugin";
@@ -3760,48 +3324,6 @@ Requires Developer role or higher.`,
3760
3324
  import { tool as tool9 } from "@opencode-ai/plugin";
3761
3325
  var z9 = tool9.schema;
3762
3326
  var snippetTools = {
3763
- gitlab_list_snippet_discussions: tool9({
3764
- description: `List discussions (comment threads) on a project snippet.
3765
- Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
3766
- Use the discussion 'id' field to reply to a specific thread with gitlab_create_snippet_note.`,
3767
- args: {
3768
- project_id: z9.string().describe("The project ID or URL-encoded path"),
3769
- snippet_id: z9.number().describe("The ID of the snippet"),
3770
- first: z9.number().optional().describe("Number of discussions to fetch from the beginning (default: 20)"),
3771
- after: z9.string().optional().describe("Cursor for forward pagination (from pageInfo.endCursor)"),
3772
- last: z9.number().optional().describe("Number of discussions to fetch from the end"),
3773
- before: z9.string().optional().describe("Cursor for backward pagination (from pageInfo.startCursor)")
3774
- },
3775
- execute: async (args, _ctx) => {
3776
- const client = getGitLabClient();
3777
- const discussions = await client.listSnippetDiscussions(args.project_id, args.snippet_id, {
3778
- first: args.first,
3779
- after: args.after,
3780
- last: args.last,
3781
- before: args.before
3782
- });
3783
- return JSON.stringify(discussions, null, 2);
3784
- }
3785
- }),
3786
- gitlab_get_snippet_discussion: tool9({
3787
- description: `Get a specific discussion thread from a snippet with all its replies.
3788
- Returns the discussion with its 'notes' array containing all comments in the thread.
3789
- Use this to get the full context of a specific conversation.`,
3790
- args: {
3791
- project_id: z9.string().describe("The project ID or URL-encoded path"),
3792
- snippet_id: z9.number().describe("The ID of the snippet"),
3793
- discussion_id: z9.string().describe("The ID of the discussion thread")
3794
- },
3795
- execute: async (args, _ctx) => {
3796
- const client = getGitLabClient();
3797
- const discussion = await client.getSnippetDiscussion(
3798
- args.project_id,
3799
- args.snippet_id,
3800
- args.discussion_id
3801
- );
3802
- return JSON.stringify(discussion, null, 2);
3803
- }
3804
- }),
3805
3327
  gitlab_list_snippet_notes: tool9({
3806
3328
  description: `List all notes/comments on a project snippet using GraphQL API with pagination support.
3807
3329
  Returns all comments including system notes in chronological order.
@@ -3827,47 +3349,6 @@ Use 'before' with the 'startCursor' from pageInfo to get the previous page.`,
3827
3349
  });
3828
3350
  return JSON.stringify(result, null, 2);
3829
3351
  }
3830
- }),
3831
- gitlab_create_snippet_note: tool9({
3832
- description: `Add a comment/note to a project snippet.
3833
- If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
3834
- Use gitlab_list_snippet_discussions to find discussion IDs for existing threads.`,
3835
- args: {
3836
- project_id: z9.string().describe("The project ID or URL-encoded path"),
3837
- snippet_id: z9.number().describe("The ID of the snippet"),
3838
- body: z9.string().describe("The content of the note/comment (supports Markdown)"),
3839
- discussion_id: z9.string().optional().describe(
3840
- "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
3841
- )
3842
- },
3843
- execute: async (args, _ctx) => {
3844
- const client = getGitLabClient();
3845
- const note = await client.createSnippetNote(
3846
- args.project_id,
3847
- args.snippet_id,
3848
- args.body,
3849
- args.discussion_id
3850
- );
3851
- return JSON.stringify(note, null, 2);
3852
- }
3853
- }),
3854
- gitlab_create_snippet_discussion: tool9({
3855
- description: `Start a new discussion thread on a project snippet.
3856
- Creates a new discussion with an initial comment.`,
3857
- args: {
3858
- project_id: z9.string().describe("The project ID or URL-encoded path"),
3859
- snippet_id: z9.number().describe("The ID of the snippet"),
3860
- body: z9.string().describe("The content of the initial comment (supports Markdown)")
3861
- },
3862
- execute: async (args, _ctx) => {
3863
- const client = getGitLabClient();
3864
- const discussion = await client.createSnippetDiscussion(
3865
- args.project_id,
3866
- args.snippet_id,
3867
- args.body
3868
- );
3869
- return JSON.stringify(discussion, null, 2);
3870
- }
3871
3352
  })
3872
3353
  };
3873
3354
 
@@ -4093,68 +3574,321 @@ Can update title, description, state, labels, and assignees.`,
4093
3574
  })
4094
3575
  };
4095
3576
 
4096
- // src/tools/discussions.ts
3577
+ // src/tools/discussions-unified.ts
4097
3578
  import { tool as tool13 } from "@opencode-ai/plugin";
4098
3579
  var z13 = tool13.schema;
4099
- var discussionTools = {
4100
- gitlab_reply_to_discussion: tool13({
4101
- description: `Universal tool for replying to any discussion thread across GitLab resources.
4102
- Supports replying to discussions on: merge requests, issues, epics, commits, and snippets.
4103
- This provides a consistent interface for thread replies regardless of the resource type.`,
3580
+ var positionSchema = z13.object({
3581
+ base_sha: z13.string().describe("SHA of the base commit"),
3582
+ start_sha: z13.string().describe("SHA of the start commit"),
3583
+ head_sha: z13.string().describe("SHA of the head commit"),
3584
+ position_type: z13.enum(["text", "image"]).describe("Type of position"),
3585
+ old_path: z13.string().optional().describe("Path of the file before changes"),
3586
+ new_path: z13.string().optional().describe("Path of the file after changes"),
3587
+ old_line: z13.number().optional().describe("Line number in the old version"),
3588
+ new_line: z13.number().optional().describe("Line number in the new version")
3589
+ });
3590
+ function validateResourceParams(resourceType, args) {
3591
+ switch (resourceType) {
3592
+ case "merge_request":
3593
+ case "issue":
3594
+ if (!args.project_id) throw new Error(`project_id is required for ${resourceType}`);
3595
+ if (args.iid == null) throw new Error(`iid is required for ${resourceType}`);
3596
+ break;
3597
+ case "epic":
3598
+ if (!args.group_id) throw new Error("group_id is required for epic");
3599
+ if (args.iid == null) throw new Error("iid is required for epic");
3600
+ break;
3601
+ case "commit":
3602
+ if (!args.project_id) throw new Error("project_id is required for commit");
3603
+ if (!args.sha) throw new Error("sha is required for commit");
3604
+ break;
3605
+ case "snippet":
3606
+ if (!args.project_id) throw new Error("project_id is required for snippet");
3607
+ if (args.snippet_id == null) throw new Error("snippet_id is required for snippet");
3608
+ break;
3609
+ }
3610
+ }
3611
+ var discussionsUnifiedTools = {
3612
+ /**
3613
+ * List discussions for any GitLab resource type
3614
+ */
3615
+ gitlab_list_discussions: tool13({
3616
+ description: `List discussions (comment threads) on any GitLab resource.
3617
+ Supports: merge_requests, issues, epics, commits, snippets.
3618
+
3619
+ Returns discussion threads with nested notes. Each discussion contains
3620
+ a 'notes' array with individual comments.
3621
+
3622
+ For pagination, use 'after' with the 'endCursor' from pageInfo to get the next page.
3623
+
3624
+ Examples:
3625
+ - MR: resource_type="merge_request", project_id="group/project", iid=123
3626
+ - Issue: resource_type="issue", project_id="group/project", iid=456
3627
+ - Epic: resource_type="epic", group_id="my-group", iid=1
3628
+ - Commit: resource_type="commit", project_id="group/project", sha="abc123"
3629
+ - Snippet: resource_type="snippet", project_id="group/project", snippet_id=789`,
4104
3630
  args: {
4105
- resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("The type of resource the discussion belongs to"),
4106
- project_id: z13.string().optional().describe("The project ID or URL-encoded path (required for all except epic)"),
4107
- group_id: z13.string().optional().describe("The group ID or URL-encoded path (required for epic)"),
4108
- iid: z13.number().optional().describe("The internal ID of the resource (required for merge_request, issue, epic)"),
4109
- sha: z13.string().optional().describe("The commit SHA (required for commit)"),
4110
- snippet_id: z13.number().optional().describe("The snippet ID (required for snippet)"),
4111
- discussion_id: z13.string().describe("The ID of the discussion thread to reply to"),
4112
- body: z13.string().describe("The content of the reply (supports Markdown)")
3631
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3632
+ project_id: z13.string().optional().describe("Project ID or path. Required for merge_request, issue, commit, snippet"),
3633
+ group_id: z13.string().optional().describe("Group ID or path. Required for epic"),
3634
+ iid: z13.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
3635
+ sha: z13.string().optional().describe("Commit SHA (required for commit)"),
3636
+ snippet_id: z13.number().optional().describe("Snippet ID (required for snippet)"),
3637
+ // Pagination
3638
+ first: z13.number().optional().describe("Number of items to return (default: 20)"),
3639
+ after: z13.string().optional().describe("Cursor for pagination - use endCursor from previous response"),
3640
+ before: z13.string().optional().describe("Cursor for backward pagination"),
3641
+ last: z13.number().optional().describe("Number of items from the end")
4113
3642
  },
4114
3643
  execute: async (args, _ctx) => {
3644
+ validateResourceParams(args.resource_type, args);
4115
3645
  const client = getGitLabClient();
4116
- const note = await client.replyToDiscussion(
4117
- args.resource_type,
4118
- {
4119
- projectId: args.project_id,
4120
- groupId: args.group_id,
4121
- iid: args.iid,
4122
- sha: args.sha,
4123
- snippetId: args.snippet_id
4124
- },
4125
- args.discussion_id,
4126
- args.body
4127
- );
4128
- return JSON.stringify(note, null, 2);
3646
+ const paginationOptions = {
3647
+ first: args.first,
3648
+ after: args.after,
3649
+ before: args.before,
3650
+ last: args.last
3651
+ };
3652
+ switch (args.resource_type) {
3653
+ case "merge_request":
3654
+ return JSON.stringify(
3655
+ await client.listMrDiscussions(args.project_id, args.iid, paginationOptions),
3656
+ null,
3657
+ 2
3658
+ );
3659
+ case "issue":
3660
+ return JSON.stringify(
3661
+ await client.listIssueDiscussions(args.project_id, args.iid, paginationOptions),
3662
+ null,
3663
+ 2
3664
+ );
3665
+ case "epic":
3666
+ return JSON.stringify(
3667
+ await client.listEpicDiscussions(args.group_id, args.iid, paginationOptions),
3668
+ null,
3669
+ 2
3670
+ );
3671
+ case "commit":
3672
+ return JSON.stringify(
3673
+ await client.listCommitDiscussions(args.project_id, args.sha),
3674
+ null,
3675
+ 2
3676
+ );
3677
+ case "snippet":
3678
+ return JSON.stringify(
3679
+ await client.listSnippetDiscussions(
3680
+ args.project_id,
3681
+ args.snippet_id,
3682
+ paginationOptions
3683
+ ),
3684
+ null,
3685
+ 2
3686
+ );
3687
+ default:
3688
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3689
+ }
4129
3690
  }
4130
3691
  }),
3692
+ /**
3693
+ * Get a specific discussion thread from any GitLab resource
3694
+ */
4131
3695
  gitlab_get_discussion: tool13({
4132
- description: `Universal tool for fetching discussion details from any GitLab resource.
4133
- Supports getting discussions from: merge requests, issues, epics, commits, and snippets.
4134
- Returns the discussion with its 'notes' array containing all comments in the thread.`,
3696
+ description: `Get a specific discussion thread with all its replies.
3697
+ Returns the discussion with its 'notes' array containing all comments.
3698
+
3699
+ Use this to get full context of a specific conversation.
3700
+
3701
+ Required parameters vary by resource type:
3702
+ - merge_request: project_id, iid, discussion_id
3703
+ - issue: project_id, iid, discussion_id
3704
+ - epic: group_id, iid, discussion_id
3705
+ - commit: project_id, sha, discussion_id
3706
+ - snippet: project_id, snippet_id, discussion_id`,
4135
3707
  args: {
4136
- resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("The type of resource the discussion belongs to"),
4137
- project_id: z13.string().optional().describe("The project ID or URL-encoded path (required for all except epic)"),
4138
- group_id: z13.string().optional().describe("The group ID or URL-encoded path (required for epic)"),
4139
- iid: z13.number().optional().describe("The internal ID of the resource (required for merge_request, issue, epic)"),
4140
- sha: z13.string().optional().describe("The commit SHA (required for commit)"),
4141
- snippet_id: z13.number().optional().describe("The snippet ID (required for snippet)"),
4142
- discussion_id: z13.string().describe("The ID of the discussion thread to fetch")
3708
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3709
+ discussion_id: z13.string().describe("The ID of the discussion thread"),
3710
+ project_id: z13.string().optional().describe("Project ID or path"),
3711
+ group_id: z13.string().optional().describe("Group ID or path (for epic)"),
3712
+ iid: z13.number().optional().describe("Internal ID (for merge_request, issue, epic)"),
3713
+ sha: z13.string().optional().describe("Commit SHA (for commit)"),
3714
+ snippet_id: z13.number().optional().describe("Snippet ID (for snippet)")
4143
3715
  },
4144
3716
  execute: async (args, _ctx) => {
3717
+ validateResourceParams(args.resource_type, args);
4145
3718
  const client = getGitLabClient();
4146
- const discussion = await client.getDiscussion(
4147
- args.resource_type,
4148
- {
4149
- projectId: args.project_id,
4150
- groupId: args.group_id,
4151
- iid: args.iid,
4152
- sha: args.sha,
4153
- snippetId: args.snippet_id
4154
- },
4155
- args.discussion_id
4156
- );
4157
- return JSON.stringify(discussion, null, 2);
3719
+ switch (args.resource_type) {
3720
+ case "merge_request":
3721
+ return JSON.stringify(
3722
+ await client.getMrDiscussion(args.project_id, args.iid, args.discussion_id),
3723
+ null,
3724
+ 2
3725
+ );
3726
+ case "issue":
3727
+ return JSON.stringify(
3728
+ await client.getIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3729
+ null,
3730
+ 2
3731
+ );
3732
+ case "epic":
3733
+ return JSON.stringify(
3734
+ await client.getEpicDiscussion(args.group_id, args.iid, args.discussion_id),
3735
+ null,
3736
+ 2
3737
+ );
3738
+ case "commit":
3739
+ return JSON.stringify(
3740
+ await client.getCommitDiscussion(args.project_id, args.sha, args.discussion_id),
3741
+ null,
3742
+ 2
3743
+ );
3744
+ case "snippet":
3745
+ return JSON.stringify(
3746
+ await client.getSnippetDiscussion(
3747
+ args.project_id,
3748
+ args.snippet_id,
3749
+ args.discussion_id
3750
+ ),
3751
+ null,
3752
+ 2
3753
+ );
3754
+ default:
3755
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3756
+ }
3757
+ }
3758
+ }),
3759
+ /**
3760
+ * Create a new discussion thread or reply to an existing one
3761
+ */
3762
+ gitlab_create_discussion: tool13({
3763
+ description: `Create a new discussion thread or reply to an existing one.
3764
+
3765
+ For NEW discussion: Omit discussion_id
3766
+ For REPLY to existing thread: Include discussion_id (preferred over gitlab_create_note for threaded conversations)
3767
+
3768
+ For code-specific comments on MRs/commits, provide position information.
3769
+
3770
+ Examples:
3771
+ - New MR comment: resource_type="merge_request", project_id="...", iid=123, body="..."
3772
+ - Reply to thread: resource_type="merge_request", ..., discussion_id="...", body="..."
3773
+ - Code comment: resource_type="merge_request", ..., body="...", position={base_sha, head_sha, ...}`,
3774
+ args: {
3775
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3776
+ body: z13.string().describe("The comment text (Markdown supported)"),
3777
+ project_id: z13.string().optional().describe("Project ID or path"),
3778
+ group_id: z13.string().optional().describe("Group ID or path (for epic)"),
3779
+ iid: z13.number().optional().describe("Internal ID (for merge_request, issue, epic)"),
3780
+ sha: z13.string().optional().describe("Commit SHA (for commit)"),
3781
+ snippet_id: z13.number().optional().describe("Snippet ID (for snippet)"),
3782
+ discussion_id: z13.string().optional().describe("If provided, replies to existing discussion. If omitted, creates new thread"),
3783
+ position: positionSchema.optional().describe("Position for code-specific comments (MR/commit only)")
3784
+ },
3785
+ execute: async (args, _ctx) => {
3786
+ validateResourceParams(args.resource_type, args);
3787
+ const client = getGitLabClient();
3788
+ if (args.discussion_id) {
3789
+ const note = await client.replyToDiscussion(
3790
+ args.resource_type,
3791
+ {
3792
+ projectId: args.project_id,
3793
+ groupId: args.group_id,
3794
+ iid: args.iid,
3795
+ sha: args.sha,
3796
+ snippetId: args.snippet_id
3797
+ },
3798
+ args.discussion_id,
3799
+ args.body
3800
+ );
3801
+ return JSON.stringify(note, null, 2);
3802
+ }
3803
+ switch (args.resource_type) {
3804
+ case "merge_request":
3805
+ return JSON.stringify(
3806
+ await client.createMrDiscussion(args.project_id, args.iid, args.body, args.position),
3807
+ null,
3808
+ 2
3809
+ );
3810
+ case "issue":
3811
+ return JSON.stringify(
3812
+ await client.createIssueNote(args.project_id, args.iid, args.body),
3813
+ null,
3814
+ 2
3815
+ );
3816
+ case "epic":
3817
+ return JSON.stringify(
3818
+ await client.createEpicNote(args.group_id, args.iid, args.body),
3819
+ null,
3820
+ 2
3821
+ );
3822
+ case "commit":
3823
+ return JSON.stringify(
3824
+ await client.createCommitDiscussion(
3825
+ args.project_id,
3826
+ args.sha,
3827
+ args.body,
3828
+ args.position
3829
+ ),
3830
+ null,
3831
+ 2
3832
+ );
3833
+ case "snippet":
3834
+ return JSON.stringify(
3835
+ await client.createSnippetDiscussion(args.project_id, args.snippet_id, args.body),
3836
+ null,
3837
+ 2
3838
+ );
3839
+ default:
3840
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3841
+ }
3842
+ }
3843
+ }),
3844
+ /**
3845
+ * Resolve or unresolve a discussion thread
3846
+ */
3847
+ gitlab_resolve_discussion: tool13({
3848
+ description: `Mark a discussion thread as resolved or unresolve it.
3849
+ Only works for resolvable discussions (MRs and issues only).
3850
+
3851
+ Use after addressing feedback to indicate the discussion is complete.`,
3852
+ args: {
3853
+ resource_type: z13.enum(["merge_request", "issue"]).describe("Type of resource (only MR and issue discussions can be resolved)"),
3854
+ action: z13.enum(["resolve", "unresolve"]).describe("Whether to resolve or unresolve"),
3855
+ discussion_id: z13.string().describe("The ID of the discussion thread"),
3856
+ project_id: z13.string().describe("Project ID or path"),
3857
+ iid: z13.number().describe("Internal ID of the MR or issue")
3858
+ },
3859
+ execute: async (args, _ctx) => {
3860
+ const client = getGitLabClient();
3861
+ if (args.action === "resolve") {
3862
+ switch (args.resource_type) {
3863
+ case "merge_request":
3864
+ return JSON.stringify(
3865
+ await client.resolveMrDiscussion(args.project_id, args.iid, args.discussion_id),
3866
+ null,
3867
+ 2
3868
+ );
3869
+ case "issue":
3870
+ return JSON.stringify(
3871
+ await client.resolveIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3872
+ null,
3873
+ 2
3874
+ );
3875
+ }
3876
+ } else {
3877
+ switch (args.resource_type) {
3878
+ case "merge_request":
3879
+ return JSON.stringify(
3880
+ await client.unresolveMrDiscussion(args.project_id, args.iid, args.discussion_id),
3881
+ null,
3882
+ 2
3883
+ );
3884
+ case "issue":
3885
+ return JSON.stringify(
3886
+ await client.unresolveIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3887
+ null,
3888
+ 2
3889
+ );
3890
+ }
3891
+ }
4158
3892
  }
4159
3893
  })
4160
3894
  };
@@ -4523,10 +4257,8 @@ var gitlabPlugin = async (_input) => {
4523
4257
  ...wikiTools,
4524
4258
  // Work Item Tools
4525
4259
  ...workItemTools,
4526
- // Commit Discussion Tools
4527
- ...commitDiscussionTools,
4528
- // Universal Discussion Tools
4529
- ...discussionTools,
4260
+ // Unified Discussion Tools (covers MR, issue, epic, commit, snippet discussions)
4261
+ ...discussionsUnifiedTools,
4530
4262
  // Git Tools
4531
4263
  ...gitTools,
4532
4264
  // Audit Tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/opencode-gitlab-plugin",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
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",