opencode-gitlab-plugin 2.0.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/dist/index.js ADDED
@@ -0,0 +1,4745 @@
1
+ // src/tools/merge-requests.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/utils.ts
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+
9
+ // src/client/base.ts
10
+ var GitLabApiClient = class {
11
+ instanceUrl;
12
+ token;
13
+ constructor(instanceUrl, token) {
14
+ this.instanceUrl = instanceUrl.replace(/\/$/, "");
15
+ this.token = token;
16
+ }
17
+ get headers() {
18
+ return {
19
+ Authorization: `Bearer ${this.token}`,
20
+ "Content-Type": "application/json"
21
+ };
22
+ }
23
+ encodeProjectId(projectId) {
24
+ if (projectId.includes("/")) {
25
+ return encodeURIComponent(projectId);
26
+ }
27
+ return projectId;
28
+ }
29
+ async fetch(method, path2, body) {
30
+ const url = `${this.instanceUrl}/api/v4${path2}`;
31
+ const response = await fetch(url, {
32
+ method,
33
+ headers: this.headers,
34
+ body: body ? JSON.stringify(body) : void 0
35
+ });
36
+ if (!response.ok) {
37
+ const errorText = await response.text();
38
+ throw new Error(`GitLab API error ${response.status}: ${errorText}`);
39
+ }
40
+ const text = await response.text();
41
+ if (!text) {
42
+ return {};
43
+ }
44
+ return JSON.parse(text);
45
+ }
46
+ async fetchText(method, path2) {
47
+ const url = `${this.instanceUrl}/api/v4${path2}`;
48
+ const response = await fetch(url, {
49
+ method,
50
+ headers: this.headers
51
+ });
52
+ if (!response.ok) {
53
+ const errorText = await response.text();
54
+ throw new Error(`GitLab API error ${response.status}: ${errorText}`);
55
+ }
56
+ return response.text();
57
+ }
58
+ /**
59
+ * Execute a GraphQL query or mutation
60
+ * @template T - The expected type of the data field in the GraphQL response
61
+ * @param query - The GraphQL query or mutation string
62
+ * @param variables - Optional variables for the query
63
+ * @returns The data from the GraphQL response
64
+ */
65
+ async fetchGraphQL(query, variables) {
66
+ const url = `${this.instanceUrl}/api/graphql`;
67
+ const response = await fetch(url, {
68
+ method: "POST",
69
+ headers: this.headers,
70
+ body: JSON.stringify({ query, variables })
71
+ });
72
+ if (!response.ok) {
73
+ const errorText = await response.text();
74
+ throw new Error(`GitLab GraphQL error ${response.status}: ${errorText}`);
75
+ }
76
+ const result = await response.json();
77
+ if (result.errors && result.errors.length > 0) {
78
+ const errorMessages = result.errors.map((e) => e.message).join(", ");
79
+ throw new Error(`GraphQL errors: ${errorMessages}`);
80
+ }
81
+ return result.data;
82
+ }
83
+ };
84
+
85
+ // src/client/notes-types.ts
86
+ function buildPaginationVariables(options) {
87
+ const variables = {};
88
+ if (options?.first !== void 0) {
89
+ variables.first = options.first;
90
+ } else if (options?.last == null) {
91
+ variables.first = 20;
92
+ }
93
+ if (options?.after) variables.after = options.after;
94
+ if (options?.last !== void 0) variables.last = options.last;
95
+ if (options?.before) variables.before = options.before;
96
+ return variables;
97
+ }
98
+ var NOTES_FRAGMENT = `
99
+ fragment NoteFields on Note {
100
+ id
101
+ body
102
+ bodyHtml
103
+ createdAt
104
+ updatedAt
105
+ system
106
+ internal
107
+ resolvable
108
+ resolved
109
+ resolvedAt
110
+ url
111
+ author {
112
+ id
113
+ username
114
+ name
115
+ avatarUrl
116
+ }
117
+ }
118
+ `;
119
+ var NOTES_CONNECTION_FRAGMENT = `
120
+ fragment NotesConnectionFields on NoteConnection {
121
+ count
122
+ pageInfo {
123
+ hasNextPage
124
+ hasPreviousPage
125
+ startCursor
126
+ endCursor
127
+ }
128
+ nodes {
129
+ ...NoteFields
130
+ }
131
+ }
132
+ `;
133
+
134
+ // src/client/discussions-types.ts
135
+ function buildDiscussionsPaginationVariables(options) {
136
+ const variables = {};
137
+ if (options?.first !== void 0) {
138
+ variables.first = options.first;
139
+ } else if (options?.last == null) {
140
+ variables.first = 20;
141
+ }
142
+ if (options?.after) variables.after = options.after;
143
+ if (options?.last !== void 0) variables.last = options.last;
144
+ if (options?.before) variables.before = options.before;
145
+ return variables;
146
+ }
147
+ var DISCUSSION_NOTE_FRAGMENT = `
148
+ fragment DiscussionNoteFields on Note {
149
+ id
150
+ body
151
+ bodyHtml
152
+ createdAt
153
+ updatedAt
154
+ system
155
+ resolvable
156
+ resolved
157
+ resolvedAt
158
+ url
159
+ author {
160
+ id
161
+ username
162
+ name
163
+ avatarUrl
164
+ }
165
+ }
166
+ `;
167
+ var DISCUSSION_FRAGMENT = `
168
+ fragment DiscussionFields on Discussion {
169
+ id
170
+ replyId
171
+ createdAt
172
+ resolved
173
+ resolvable
174
+ resolvedAt
175
+ resolvedBy {
176
+ id
177
+ username
178
+ name
179
+ avatarUrl
180
+ }
181
+ notes {
182
+ nodes {
183
+ ...DiscussionNoteFields
184
+ }
185
+ }
186
+ }
187
+ `;
188
+ var DISCUSSIONS_CONNECTION_FRAGMENT = `
189
+ fragment DiscussionsConnectionFields on DiscussionConnection {
190
+ pageInfo {
191
+ hasNextPage
192
+ hasPreviousPage
193
+ startCursor
194
+ endCursor
195
+ }
196
+ nodes {
197
+ ...DiscussionFields
198
+ }
199
+ }
200
+ `;
201
+
202
+ // src/client/merge-requests.ts
203
+ var LIST_MR_NOTES_QUERY = `
204
+ ${NOTES_FRAGMENT}
205
+ ${NOTES_CONNECTION_FRAGMENT}
206
+ query listMrNotes(
207
+ $projectPath: ID!
208
+ $mrIid: String!
209
+ $first: Int
210
+ $after: String
211
+ $last: Int
212
+ $before: String
213
+ ) {
214
+ project(fullPath: $projectPath) {
215
+ mergeRequest(iid: $mrIid) {
216
+ notes(
217
+ first: $first
218
+ after: $after
219
+ last: $last
220
+ before: $before
221
+ ) {
222
+ ...NotesConnectionFields
223
+ }
224
+ }
225
+ }
226
+ }
227
+ `;
228
+ var LIST_MR_DISCUSSIONS_QUERY = `
229
+ ${DISCUSSION_NOTE_FRAGMENT}
230
+ ${DISCUSSION_FRAGMENT}
231
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
232
+ query listMrDiscussions(
233
+ $projectPath: ID!
234
+ $mrIid: String!
235
+ $first: Int
236
+ $after: String
237
+ $last: Int
238
+ $before: String
239
+ ) {
240
+ project(fullPath: $projectPath) {
241
+ mergeRequest(iid: $mrIid) {
242
+ discussions(
243
+ first: $first
244
+ after: $after
245
+ last: $last
246
+ before: $before
247
+ ) {
248
+ ...DiscussionsConnectionFields
249
+ }
250
+ }
251
+ }
252
+ }
253
+ `;
254
+ var SET_AUTO_MERGE_MUTATION = `
255
+ mutation setAutoMerge(
256
+ $projectPath: ID!
257
+ $iid: String!
258
+ $sha: String!
259
+ $strategy: MergeStrategyEnum
260
+ ) {
261
+ mergeRequestAccept(
262
+ input: {
263
+ projectPath: $projectPath
264
+ iid: $iid
265
+ sha: $sha
266
+ strategy: $strategy
267
+ }
268
+ ) {
269
+ mergeRequest {
270
+ id
271
+ iid
272
+ title
273
+ autoMergeEnabled
274
+ autoMergeStrategy
275
+ }
276
+ errors
277
+ }
278
+ }
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
+ `;
297
+ var MergeRequestsClient = class extends GitLabApiClient {
298
+ async getMergeRequest(projectId, mrIid, includeChanges) {
299
+ const encodedProject = this.encodeProjectId(projectId);
300
+ let path2 = `/projects/${encodedProject}/merge_requests/${mrIid}`;
301
+ if (includeChanges) {
302
+ path2 += "?include_diverged_commits_count=true";
303
+ }
304
+ return this.fetch("GET", path2);
305
+ }
306
+ async listMergeRequests(options) {
307
+ const params = new URLSearchParams();
308
+ params.set("per_page", String(options.limit || 20));
309
+ if (options.state) params.set("state", options.state);
310
+ if (options.scope) params.set("scope", options.scope);
311
+ if (options.search) params.set("search", options.search);
312
+ if (options.labels) params.set("labels", options.labels);
313
+ let path2;
314
+ if (options.projectId) {
315
+ const encodedProject = this.encodeProjectId(options.projectId);
316
+ path2 = `/projects/${encodedProject}/merge_requests?${params}`;
317
+ } else {
318
+ path2 = `/merge_requests?${params}`;
319
+ }
320
+ return this.fetch("GET", path2);
321
+ }
322
+ async getMrChanges(projectId, mrIid) {
323
+ const encodedProject = this.encodeProjectId(projectId);
324
+ return this.fetch(
325
+ "GET",
326
+ `/projects/${encodedProject}/merge_requests/${mrIid}/changes`
327
+ );
328
+ }
329
+ /**
330
+ * List notes on a merge request using GraphQL API with pagination support
331
+ */
332
+ async listMrNotes(projectId, mrIid, options) {
333
+ const variables = {
334
+ projectPath: projectId,
335
+ mrIid: String(mrIid),
336
+ ...buildPaginationVariables(options)
337
+ };
338
+ const result = await this.fetchGraphQL(LIST_MR_NOTES_QUERY, variables);
339
+ const notes = result.project?.mergeRequest?.notes;
340
+ if (!notes) {
341
+ throw new Error("Merge request not found or access denied");
342
+ }
343
+ return {
344
+ notes,
345
+ pageInfo: notes.pageInfo,
346
+ totalCount: notes.count
347
+ };
348
+ }
349
+ /**
350
+ * List discussions on a merge request using GraphQL API with pagination support
351
+ */
352
+ async listMrDiscussions(projectId, mrIid, options) {
353
+ const variables = {
354
+ projectPath: projectId,
355
+ mrIid: String(mrIid),
356
+ ...buildDiscussionsPaginationVariables(options)
357
+ };
358
+ const result = await this.fetchGraphQL(LIST_MR_DISCUSSIONS_QUERY, variables);
359
+ const discussions = result.project?.mergeRequest?.discussions;
360
+ if (!discussions) {
361
+ throw new Error("Merge request not found or access denied");
362
+ }
363
+ return { discussions };
364
+ }
365
+ async getMrDiscussion(projectId, mrIid, discussionId) {
366
+ const encodedProject = this.encodeProjectId(projectId);
367
+ return this.fetch(
368
+ "GET",
369
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`
370
+ );
371
+ }
372
+ async resolveMrDiscussion(projectId, mrIid, discussionId) {
373
+ const encodedProject = this.encodeProjectId(projectId);
374
+ return this.fetch(
375
+ "PUT",
376
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`,
377
+ { resolved: true }
378
+ );
379
+ }
380
+ async unresolveMrDiscussion(projectId, mrIid, discussionId) {
381
+ const encodedProject = this.encodeProjectId(projectId);
382
+ return this.fetch(
383
+ "PUT",
384
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`,
385
+ { resolved: false }
386
+ );
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
+ }
414
+ async createMrDiscussion(projectId, mrIid, body, position) {
415
+ const encodedProject = this.encodeProjectId(projectId);
416
+ const requestBody = { body };
417
+ if (position) {
418
+ requestBody.position = position;
419
+ }
420
+ return this.fetch(
421
+ "POST",
422
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions`,
423
+ requestBody
424
+ );
425
+ }
426
+ async createMrNote(projectId, mrIid, body, discussionId) {
427
+ const encodedProject = this.encodeProjectId(projectId);
428
+ if (discussionId) {
429
+ return this.fetch(
430
+ "POST",
431
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}/notes`,
432
+ { body }
433
+ );
434
+ }
435
+ return this.fetch(
436
+ "POST",
437
+ `/projects/${encodedProject}/merge_requests/${mrIid}/notes`,
438
+ { body }
439
+ );
440
+ }
441
+ async createMergeRequest(projectId, options) {
442
+ const encodedProject = this.encodeProjectId(projectId);
443
+ return this.fetch(
444
+ "POST",
445
+ `/projects/${encodedProject}/merge_requests`,
446
+ options
447
+ );
448
+ }
449
+ async updateMergeRequest(projectId, mrIid, options) {
450
+ const encodedProject = this.encodeProjectId(projectId);
451
+ return this.fetch(
452
+ "PUT",
453
+ `/projects/${encodedProject}/merge_requests/${mrIid}`,
454
+ options
455
+ );
456
+ }
457
+ async getMrCommits(projectId, mrIid) {
458
+ const encodedProject = this.encodeProjectId(projectId);
459
+ return this.fetch(
460
+ "GET",
461
+ `/projects/${encodedProject}/merge_requests/${mrIid}/commits`
462
+ );
463
+ }
464
+ async getMrPipelines(projectId, mrIid) {
465
+ const encodedProject = this.encodeProjectId(projectId);
466
+ return this.fetch(
467
+ "GET",
468
+ `/projects/${encodedProject}/merge_requests/${mrIid}/pipelines`
469
+ );
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
+ }
497
+ async listMergeRequestDiffs(projectId, mrIid, options = {}) {
498
+ const encodedProject = this.encodeProjectId(projectId);
499
+ const params = new URLSearchParams();
500
+ if (options.page) params.append("page", options.page.toString());
501
+ if (options.per_page) params.append("per_page", options.per_page.toString());
502
+ const query = params.toString();
503
+ const path2 = `/projects/${encodedProject}/merge_requests/${mrIid}/diffs${query ? `?${query}` : ""}`;
504
+ return this.fetch("GET", path2);
505
+ }
506
+ /**
507
+ * Set auto-merge (MWPS) on a merge request using GraphQL API
508
+ * Uses the mergeRequestAccept mutation with a merge strategy
509
+ */
510
+ async setAutoMerge(projectId, mrIid, sha, strategy = "MERGE_WHEN_CHECKS_PASS") {
511
+ const result = await this.fetchGraphQL(SET_AUTO_MERGE_MUTATION, {
512
+ projectPath: projectId,
513
+ iid: String(mrIid),
514
+ sha,
515
+ strategy
516
+ });
517
+ if (result.mergeRequestAccept.errors.length > 0) {
518
+ throw new Error(`Failed to set auto-merge: ${result.mergeRequestAccept.errors.join(", ")}`);
519
+ }
520
+ if (!result.mergeRequestAccept.mergeRequest) {
521
+ throw new Error("Failed to set auto-merge: No merge request returned");
522
+ }
523
+ return result.mergeRequestAccept.mergeRequest;
524
+ }
525
+ /**
526
+ * Approve a merge request
527
+ * Requires at least Developer role on the project
528
+ */
529
+ async approveMergeRequest(projectId, mrIid, sha) {
530
+ const encodedProject = this.encodeProjectId(projectId);
531
+ return this.fetch(
532
+ "POST",
533
+ `/projects/${encodedProject}/merge_requests/${mrIid}/approve`,
534
+ sha ? { sha } : void 0
535
+ );
536
+ }
537
+ /**
538
+ * Unapprove (revoke approval from) a merge request
539
+ * Only removes the current user's approval
540
+ */
541
+ async unapproveMergeRequest(projectId, mrIid) {
542
+ const encodedProject = this.encodeProjectId(projectId);
543
+ return this.fetch(
544
+ "POST",
545
+ `/projects/${encodedProject}/merge_requests/${mrIid}/unapprove`
546
+ );
547
+ }
548
+ };
549
+
550
+ // src/client/issues.ts
551
+ var LIST_ISSUE_NOTES_QUERY = `
552
+ ${NOTES_FRAGMENT}
553
+ ${NOTES_CONNECTION_FRAGMENT}
554
+ query listIssueNotes(
555
+ $projectPath: ID!
556
+ $issueIid: String!
557
+ $first: Int
558
+ $after: String
559
+ $last: Int
560
+ $before: String
561
+ ) {
562
+ project(fullPath: $projectPath) {
563
+ issue(iid: $issueIid) {
564
+ notes(
565
+ first: $first
566
+ after: $after
567
+ last: $last
568
+ before: $before
569
+ ) {
570
+ ...NotesConnectionFields
571
+ }
572
+ }
573
+ }
574
+ }
575
+ `;
576
+ var LIST_ISSUE_DISCUSSIONS_QUERY = `
577
+ ${DISCUSSION_NOTE_FRAGMENT}
578
+ ${DISCUSSION_FRAGMENT}
579
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
580
+ query listIssueDiscussions(
581
+ $projectPath: ID!
582
+ $issueIid: String!
583
+ $first: Int
584
+ $after: String
585
+ $last: Int
586
+ $before: String
587
+ ) {
588
+ project(fullPath: $projectPath) {
589
+ issue(iid: $issueIid) {
590
+ discussions(
591
+ first: $first
592
+ after: $after
593
+ last: $last
594
+ before: $before
595
+ ) {
596
+ ...DiscussionsConnectionFields
597
+ }
598
+ }
599
+ }
600
+ }
601
+ `;
602
+ var IssuesClient = class extends GitLabApiClient {
603
+ async createIssue(projectId, title, options) {
604
+ const encodedProject = this.encodeProjectId(projectId);
605
+ const body = {
606
+ title,
607
+ ...Object.fromEntries(
608
+ Object.entries(options || {}).filter(([_, value]) => value !== void 0)
609
+ )
610
+ };
611
+ return this.fetch("POST", `/projects/${encodedProject}/issues`, body);
612
+ }
613
+ async getIssue(projectId, issueIid) {
614
+ const encodedProject = this.encodeProjectId(projectId);
615
+ return this.fetch(
616
+ "GET",
617
+ `/projects/${encodedProject}/issues/${issueIid}`
618
+ );
619
+ }
620
+ async listIssues(options) {
621
+ const params = new URLSearchParams();
622
+ params.set("per_page", String(options.limit || 20));
623
+ if (options.state) params.set("state", options.state);
624
+ if (options.scope) params.set("scope", options.scope);
625
+ if (options.search) params.set("search", options.search);
626
+ if (options.labels) params.set("labels", options.labels);
627
+ if (options.milestone) params.set("milestone", options.milestone);
628
+ let path2;
629
+ if (options.projectId) {
630
+ const encodedProject = this.encodeProjectId(options.projectId);
631
+ path2 = `/projects/${encodedProject}/issues?${params}`;
632
+ } else {
633
+ path2 = `/issues?${params}`;
634
+ }
635
+ return this.fetch("GET", path2);
636
+ }
637
+ /**
638
+ * List notes on an issue using GraphQL API with pagination support
639
+ */
640
+ async listIssueNotes(projectId, issueIid, options) {
641
+ const variables = {
642
+ projectPath: projectId,
643
+ issueIid: String(issueIid),
644
+ ...buildPaginationVariables(options)
645
+ };
646
+ const result = await this.fetchGraphQL(LIST_ISSUE_NOTES_QUERY, variables);
647
+ const notes = result.project?.issue?.notes;
648
+ if (!notes) {
649
+ throw new Error("Issue not found or access denied");
650
+ }
651
+ return {
652
+ notes,
653
+ pageInfo: notes.pageInfo,
654
+ totalCount: notes.count
655
+ };
656
+ }
657
+ /**
658
+ * List discussions on an issue using GraphQL API with pagination support
659
+ */
660
+ async listIssueDiscussions(projectId, issueIid, options) {
661
+ const variables = {
662
+ projectPath: projectId,
663
+ issueIid: String(issueIid),
664
+ ...buildDiscussionsPaginationVariables(options)
665
+ };
666
+ const result = await this.fetchGraphQL(LIST_ISSUE_DISCUSSIONS_QUERY, variables);
667
+ const discussions = result.project?.issue?.discussions;
668
+ if (!discussions) {
669
+ throw new Error("Issue not found or access denied");
670
+ }
671
+ return { discussions };
672
+ }
673
+ async getIssueDiscussion(projectId, issueIid, discussionId) {
674
+ const encodedProject = this.encodeProjectId(projectId);
675
+ return this.fetch(
676
+ "GET",
677
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`
678
+ );
679
+ }
680
+ async createIssueNote(projectId, issueIid, body, discussionId) {
681
+ const encodedProject = this.encodeProjectId(projectId);
682
+ if (discussionId) {
683
+ return this.fetch(
684
+ "POST",
685
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}/notes`,
686
+ { body }
687
+ );
688
+ }
689
+ return this.fetch(
690
+ "POST",
691
+ `/projects/${encodedProject}/issues/${issueIid}/notes`,
692
+ { body }
693
+ );
694
+ }
695
+ async resolveIssueDiscussion(projectId, issueIid, discussionId) {
696
+ const encodedProject = this.encodeProjectId(projectId);
697
+ return this.fetch(
698
+ "PUT",
699
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`,
700
+ { resolved: true }
701
+ );
702
+ }
703
+ async unresolveIssueDiscussion(projectId, issueIid, discussionId) {
704
+ const encodedProject = this.encodeProjectId(projectId);
705
+ return this.fetch(
706
+ "PUT",
707
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`,
708
+ { resolved: false }
709
+ );
710
+ }
711
+ async getIssueNote(projectId, issueIid, noteId) {
712
+ const encodedProject = this.encodeProjectId(projectId);
713
+ return this.fetch(
714
+ "GET",
715
+ `/projects/${encodedProject}/issues/${issueIid}/notes/${noteId}`
716
+ );
717
+ }
718
+ };
719
+
720
+ // src/client/work-items.ts
721
+ var WorkItemsClient = class extends GitLabApiClient {
722
+ async getWorkItem(projectId, workItemId) {
723
+ const encodedProject = this.encodeProjectId(projectId);
724
+ return this.fetch(
725
+ "GET",
726
+ `/projects/${encodedProject}/work_items/${workItemId}`
727
+ );
728
+ }
729
+ async listWorkItems(options) {
730
+ const params = new URLSearchParams();
731
+ params.set("per_page", String(options.limit || 20));
732
+ if (options.state) params.set("state", options.state);
733
+ if (options.search) params.set("search", options.search);
734
+ if (options.labels) params.set("labels", options.labels);
735
+ if (options.work_item_type) params.set("type", options.work_item_type);
736
+ let path2;
737
+ if (options.projectId) {
738
+ const encodedProject = this.encodeProjectId(options.projectId);
739
+ path2 = `/projects/${encodedProject}/work_items?${params}`;
740
+ } else if (options.groupId) {
741
+ const encodedGroup = encodeURIComponent(options.groupId);
742
+ path2 = `/groups/${encodedGroup}/work_items?${params}`;
743
+ } else {
744
+ throw new Error("Either projectId or groupId must be provided");
745
+ }
746
+ return this.fetch("GET", path2);
747
+ }
748
+ async getWorkItemNotes(projectId, workItemId) {
749
+ const encodedProject = this.encodeProjectId(projectId);
750
+ return this.fetch(
751
+ "GET",
752
+ `/projects/${encodedProject}/work_items/${workItemId}/notes`
753
+ );
754
+ }
755
+ async createWorkItem(projectId, options) {
756
+ const encodedProject = this.encodeProjectId(projectId);
757
+ return this.fetch(
758
+ "POST",
759
+ `/projects/${encodedProject}/work_items`,
760
+ options
761
+ );
762
+ }
763
+ async updateWorkItem(projectId, workItemId, options) {
764
+ const encodedProject = this.encodeProjectId(projectId);
765
+ return this.fetch(
766
+ "PUT",
767
+ `/projects/${encodedProject}/work_items/${workItemId}`,
768
+ options
769
+ );
770
+ }
771
+ async createWorkItemNote(projectId, workItemId, body) {
772
+ const encodedProject = this.encodeProjectId(projectId);
773
+ return this.fetch(
774
+ "POST",
775
+ `/projects/${encodedProject}/work_items/${workItemId}/notes`,
776
+ { body }
777
+ );
778
+ }
779
+ };
780
+
781
+ // src/client/pipelines.ts
782
+ var PipelinesClient = class extends GitLabApiClient {
783
+ async listPipelines(projectId, options) {
784
+ const encodedProject = this.encodeProjectId(projectId);
785
+ const params = new URLSearchParams();
786
+ params.set("per_page", String(options?.limit || 20));
787
+ if (options?.status) params.set("status", options.status);
788
+ if (options?.ref) params.set("ref", options.ref);
789
+ return this.fetch(
790
+ "GET",
791
+ `/projects/${encodedProject}/pipelines?${params}`
792
+ );
793
+ }
794
+ async getPipeline(projectId, pipelineId) {
795
+ const encodedProject = this.encodeProjectId(projectId);
796
+ return this.fetch(
797
+ "GET",
798
+ `/projects/${encodedProject}/pipelines/${pipelineId}`
799
+ );
800
+ }
801
+ async listPipelineJobs(projectId, pipelineId, scope) {
802
+ const encodedProject = this.encodeProjectId(projectId);
803
+ const params = new URLSearchParams();
804
+ if (scope) params.set("scope[]", scope);
805
+ return this.fetch(
806
+ "GET",
807
+ `/projects/${encodedProject}/pipelines/${pipelineId}/jobs?${params}`
808
+ );
809
+ }
810
+ async getJobLog(projectId, jobId) {
811
+ const encodedProject = this.encodeProjectId(projectId);
812
+ const log = await this.fetchText("GET", `/projects/${encodedProject}/jobs/${jobId}/trace`);
813
+ const maxLength = 5e4;
814
+ if (log.length > maxLength) {
815
+ return `[Log truncated, showing last ${maxLength} characters]
816
+
817
+ ${log.slice(-maxLength)}`;
818
+ }
819
+ return log;
820
+ }
821
+ async retryJob(projectId, jobId) {
822
+ const encodedProject = this.encodeProjectId(projectId);
823
+ return this.fetch(
824
+ "POST",
825
+ `/projects/${encodedProject}/jobs/${jobId}/retry`
826
+ );
827
+ }
828
+ async getPipelineFailingJobs(projectId, pipelineId) {
829
+ const encodedProject = this.encodeProjectId(projectId);
830
+ const jobs = await this.fetch(
831
+ "GET",
832
+ `/projects/${encodedProject}/pipelines/${pipelineId}/jobs?scope[]=failed`
833
+ );
834
+ return jobs;
835
+ }
836
+ /**
837
+ * Validate a CI/CD configuration
838
+ * @param projectId - The project ID or URL-encoded path
839
+ * @param content - The CI/CD configuration content (YAML as string)
840
+ * @param options - Optional parameters
841
+ * @param options.dry_run - Run pipeline creation simulation (default: false)
842
+ * @param options.include_jobs - Include list of jobs in response (default: false)
843
+ * @param options.ref - Branch or tag context for validation (defaults to project's default branch)
844
+ * @returns Validation result with errors, warnings, and optionally merged YAML and jobs
845
+ */
846
+ async lintCiConfig(projectId, content, options) {
847
+ const encodedProject = this.encodeProjectId(projectId);
848
+ const body = {
849
+ content
850
+ };
851
+ if (options?.dry_run !== void 0) body.dry_run = options.dry_run;
852
+ if (options?.include_jobs !== void 0) body.include_jobs = options.include_jobs;
853
+ if (options?.ref) body.ref = options.ref;
854
+ return this.fetch("POST", `/projects/${encodedProject}/ci/lint`, body);
855
+ }
856
+ /**
857
+ * Validate an existing CI/CD configuration from the repository
858
+ * @param projectId - The project ID or URL-encoded path
859
+ * @param options - Optional parameters
860
+ * @param options.content_ref - Commit SHA, branch or tag to get CI config from (defaults to default branch)
861
+ * @param options.dry_run - Run pipeline creation simulation (default: false)
862
+ * @param options.dry_run_ref - Branch or tag context for validation (defaults to project's default branch)
863
+ * @param options.include_jobs - Include list of jobs in response (default: false)
864
+ * @returns Validation result with errors, warnings, and optionally merged YAML and jobs
865
+ */
866
+ async lintExistingCiConfig(projectId, options) {
867
+ const encodedProject = this.encodeProjectId(projectId);
868
+ const params = new URLSearchParams();
869
+ if (options?.content_ref) params.set("content_ref", options.content_ref);
870
+ if (options?.dry_run !== void 0) params.set("dry_run", String(options.dry_run));
871
+ if (options?.dry_run_ref) params.set("dry_run_ref", options.dry_run_ref);
872
+ if (options?.include_jobs !== void 0)
873
+ params.set("include_jobs", String(options.include_jobs));
874
+ const queryString = params.toString();
875
+ const path2 = queryString ? `/projects/${encodedProject}/ci/lint?${queryString}` : `/projects/${encodedProject}/ci/lint`;
876
+ return this.fetch("GET", path2);
877
+ }
878
+ };
879
+
880
+ // src/client/repository.ts
881
+ var RepositoryClient = class extends GitLabApiClient {
882
+ async getFile(projectId, filePath, ref) {
883
+ const encodedProject = this.encodeProjectId(projectId);
884
+ const encodedPath = encodeURIComponent(filePath);
885
+ let url = `/projects/${encodedProject}/repository/files/${encodedPath}`;
886
+ if (ref) {
887
+ url += `?ref=${encodeURIComponent(ref)}`;
888
+ }
889
+ const file = await this.fetch("GET", url);
890
+ if (file.encoding === "base64") {
891
+ return Buffer.from(file.content, "base64").toString("utf-8");
892
+ }
893
+ return file.content;
894
+ }
895
+ async listCommits(projectId, options) {
896
+ const encodedProject = this.encodeProjectId(projectId);
897
+ const params = new URLSearchParams();
898
+ params.set("per_page", String(options?.limit || 20));
899
+ if (options?.ref) params.set("ref_name", options.ref);
900
+ if (options?.path) params.set("path", options.path);
901
+ if (options?.since) params.set("since", options.since);
902
+ if (options?.until) params.set("until", options.until);
903
+ return this.fetch(
904
+ "GET",
905
+ `/projects/${encodedProject}/repository/commits?${params}`
906
+ );
907
+ }
908
+ async getCommit(projectId, sha) {
909
+ const encodedProject = this.encodeProjectId(projectId);
910
+ return this.fetch(
911
+ "GET",
912
+ `/projects/${encodedProject}/repository/commits/${sha}`
913
+ );
914
+ }
915
+ async getCommitDiff(projectId, sha) {
916
+ const encodedProject = this.encodeProjectId(projectId);
917
+ return this.fetch(
918
+ "GET",
919
+ `/projects/${encodedProject}/repository/commits/${sha}/diff`
920
+ );
921
+ }
922
+ async createCommit(projectId, options) {
923
+ const encodedProject = this.encodeProjectId(projectId);
924
+ return this.fetch(
925
+ "POST",
926
+ `/projects/${encodedProject}/repository/commits`,
927
+ options
928
+ );
929
+ }
930
+ async listRepositoryTree(projectId, options) {
931
+ const encodedProject = this.encodeProjectId(projectId);
932
+ const params = new URLSearchParams();
933
+ if (options?.path) params.set("path", options.path);
934
+ if (options?.ref) params.set("ref", options.ref);
935
+ if (options?.recursive) params.set("recursive", "true");
936
+ if (options?.per_page) params.set("per_page", String(options.per_page));
937
+ return this.fetch(
938
+ "GET",
939
+ `/projects/${encodedProject}/repository/tree?${params}`
940
+ );
941
+ }
942
+ async listBranches(projectId, search) {
943
+ const encodedProject = this.encodeProjectId(projectId);
944
+ const params = new URLSearchParams();
945
+ if (search) params.set("search", search);
946
+ return this.fetch(
947
+ "GET",
948
+ `/projects/${encodedProject}/repository/branches?${params}`
949
+ );
950
+ }
951
+ // ========== Commit Discussion Methods ==========
952
+ async listCommitDiscussions(projectId, sha) {
953
+ const encodedProject = this.encodeProjectId(projectId);
954
+ return this.fetch(
955
+ "GET",
956
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions`
957
+ );
958
+ }
959
+ async getCommitDiscussion(projectId, sha, discussionId) {
960
+ const encodedProject = this.encodeProjectId(projectId);
961
+ return this.fetch(
962
+ "GET",
963
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions/${discussionId}`
964
+ );
965
+ }
966
+ async createCommitNote(projectId, sha, body, options) {
967
+ const encodedProject = this.encodeProjectId(projectId);
968
+ if (options?.discussion_id) {
969
+ return this.fetch(
970
+ "POST",
971
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions/${options.discussion_id}/notes`,
972
+ { body }
973
+ );
974
+ }
975
+ const requestBody = { body };
976
+ if (options?.path) requestBody.path = options.path;
977
+ if (options?.line) requestBody.line = options.line;
978
+ if (options?.line_type) requestBody.line_type = options.line_type;
979
+ return this.fetch(
980
+ "POST",
981
+ `/projects/${encodedProject}/repository/commits/${sha}/comments`,
982
+ requestBody
983
+ );
984
+ }
985
+ async createCommitDiscussion(projectId, sha, body, position) {
986
+ const encodedProject = this.encodeProjectId(projectId);
987
+ const requestBody = { body };
988
+ if (position) {
989
+ requestBody.position = position;
990
+ }
991
+ return this.fetch(
992
+ "POST",
993
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions`,
994
+ requestBody
995
+ );
996
+ }
997
+ /**
998
+ * Get comments on a specific commit
999
+ * API: GET /projects/:id/repository/commits/:sha/comments
1000
+ */
1001
+ async getCommitComments(projectId, sha) {
1002
+ const encodedProject = this.encodeProjectId(projectId);
1003
+ return this.fetch(
1004
+ "GET",
1005
+ `/projects/${encodedProject}/repository/commits/${sha}/comments`
1006
+ );
1007
+ }
1008
+ };
1009
+
1010
+ // src/client/search.ts
1011
+ var SearchClient = class extends GitLabApiClient {
1012
+ async search(scope, searchQuery, projectId, limit) {
1013
+ const params = new URLSearchParams();
1014
+ params.set("scope", scope);
1015
+ params.set("search", searchQuery);
1016
+ params.set("per_page", String(limit || 20));
1017
+ let path2;
1018
+ if (projectId) {
1019
+ const encodedProject = this.encodeProjectId(projectId);
1020
+ path2 = `/projects/${encodedProject}/search?${params}`;
1021
+ } else {
1022
+ path2 = `/search?${params}`;
1023
+ }
1024
+ return this.fetch("GET", path2);
1025
+ }
1026
+ /**
1027
+ * Search for commits in a project or globally
1028
+ * @param searchQuery - The search term
1029
+ * @param projectId - Optional project ID to limit search
1030
+ * @param options - Optional search parameters
1031
+ */
1032
+ async searchCommits(searchQuery, projectId, options) {
1033
+ const params = new URLSearchParams();
1034
+ params.set("scope", "commits");
1035
+ params.set("search", searchQuery);
1036
+ params.set("per_page", String(options?.limit || 20));
1037
+ if (options?.ref) params.set("ref", options.ref);
1038
+ if (options?.order_by) params.set("order_by", options.order_by);
1039
+ if (options?.sort) params.set("sort", options.sort);
1040
+ let path2;
1041
+ if (projectId) {
1042
+ const encodedProject = this.encodeProjectId(projectId);
1043
+ path2 = `/projects/${encodedProject}/search?${params}`;
1044
+ } else {
1045
+ path2 = `/search?${params}`;
1046
+ }
1047
+ return this.fetch("GET", path2);
1048
+ }
1049
+ /**
1050
+ * Search for projects within a specific group
1051
+ * @param groupId - The group ID or URL-encoded path
1052
+ * @param searchQuery - The search term
1053
+ * @param options - Optional search parameters
1054
+ */
1055
+ async searchGroupProjects(groupId, searchQuery, options) {
1056
+ const encodedGroup = this.encodeProjectId(groupId);
1057
+ const params = new URLSearchParams();
1058
+ params.set("scope", "projects");
1059
+ params.set("search", searchQuery);
1060
+ params.set("per_page", String(options?.limit || 20));
1061
+ if (options?.order_by) params.set("order_by", options.order_by);
1062
+ if (options?.sort) params.set("sort", options.sort);
1063
+ return this.fetch("GET", `/groups/${encodedGroup}/search?${params}`);
1064
+ }
1065
+ /**
1066
+ * Search for milestones in a project or globally
1067
+ * @param searchQuery - The search term
1068
+ * @param projectId - Optional project ID to limit search
1069
+ * @param options - Optional search parameters
1070
+ */
1071
+ async searchMilestones(searchQuery, projectId, options) {
1072
+ const params = new URLSearchParams();
1073
+ params.set("scope", "milestones");
1074
+ params.set("search", searchQuery);
1075
+ params.set("per_page", String(options?.limit || 20));
1076
+ if (options?.state) params.set("state", options.state);
1077
+ if (options?.order_by) params.set("order_by", options.order_by);
1078
+ if (options?.sort) params.set("sort", options.sort);
1079
+ let path2;
1080
+ if (projectId) {
1081
+ const encodedProject = this.encodeProjectId(projectId);
1082
+ path2 = `/projects/${encodedProject}/search?${params}`;
1083
+ } else {
1084
+ path2 = `/search?${params}`;
1085
+ }
1086
+ return this.fetch("GET", path2);
1087
+ }
1088
+ /**
1089
+ * Search for notes/comments in a project
1090
+ * @param searchQuery - The search term
1091
+ * @param projectId - The project ID to search in
1092
+ * @param options - Optional search parameters
1093
+ */
1094
+ async searchNotes(searchQuery, projectId, options) {
1095
+ const encodedProject = this.encodeProjectId(projectId);
1096
+ const params = new URLSearchParams();
1097
+ params.set("scope", "notes");
1098
+ params.set("search", searchQuery);
1099
+ params.set("per_page", String(options?.limit || 20));
1100
+ if (options?.order_by) params.set("order_by", options.order_by);
1101
+ if (options?.sort) params.set("sort", options.sort);
1102
+ return this.fetch(
1103
+ "GET",
1104
+ `/projects/${encodedProject}/search?${params}`
1105
+ );
1106
+ }
1107
+ /**
1108
+ * Search for users by name or email
1109
+ * @param searchQuery - The search term (name or email)
1110
+ * @param projectId - Optional project ID to limit search
1111
+ * @param options - Optional search parameters
1112
+ */
1113
+ async searchUsers(searchQuery, projectId, options) {
1114
+ const params = new URLSearchParams();
1115
+ params.set("scope", "users");
1116
+ params.set("search", searchQuery);
1117
+ params.set("per_page", String(options?.limit || 20));
1118
+ if (options?.order_by) params.set("order_by", options.order_by);
1119
+ if (options?.sort) params.set("sort", options.sort);
1120
+ let path2;
1121
+ if (projectId) {
1122
+ const encodedProject = this.encodeProjectId(projectId);
1123
+ path2 = `/projects/${encodedProject}/search?${params}`;
1124
+ } else {
1125
+ path2 = `/search?${params}`;
1126
+ }
1127
+ return this.fetch("GET", path2);
1128
+ }
1129
+ /**
1130
+ * Search for wiki blobs (wiki content)
1131
+ * @param searchQuery - The search term (supports filters like filename:, path:, extension:)
1132
+ * @param projectId - Optional project ID to limit search
1133
+ * @param options - Optional search parameters
1134
+ */
1135
+ async searchWikiBlobs(searchQuery, projectId, options) {
1136
+ const params = new URLSearchParams();
1137
+ params.set("scope", "wiki_blobs");
1138
+ params.set("search", searchQuery);
1139
+ params.set("per_page", String(options?.limit || 20));
1140
+ if (options?.ref) params.set("ref", options.ref);
1141
+ if (options?.order_by) params.set("order_by", options.order_by);
1142
+ if (options?.sort) params.set("sort", options.sort);
1143
+ let path2;
1144
+ if (projectId) {
1145
+ const encodedProject = this.encodeProjectId(projectId);
1146
+ path2 = `/projects/${encodedProject}/search?${params}`;
1147
+ } else {
1148
+ path2 = `/search?${params}`;
1149
+ }
1150
+ return this.fetch("GET", path2);
1151
+ }
1152
+ /**
1153
+ * Search GitLab documentation
1154
+ * Note: This uses the public GitLab docs search API
1155
+ * @param searchQuery - The search term
1156
+ * @param limit - Maximum number of results (default: 10)
1157
+ */
1158
+ async searchDocumentation(searchQuery, limit) {
1159
+ const url = `https://docs.gitlab.com/search.json?q=${encodeURIComponent(searchQuery)}&limit=${limit || 10}`;
1160
+ const response = await fetch(url);
1161
+ if (!response.ok) {
1162
+ throw new Error(`Documentation search error ${response.status}: ${await response.text()}`);
1163
+ }
1164
+ return response.json();
1165
+ }
1166
+ };
1167
+
1168
+ // src/client/projects.ts
1169
+ var ProjectsClient = class extends GitLabApiClient {
1170
+ async getProject(projectId) {
1171
+ const encodedProject = this.encodeProjectId(projectId);
1172
+ return this.fetch("GET", `/projects/${encodedProject}`);
1173
+ }
1174
+ async listProjectMembers(projectId) {
1175
+ const encodedProject = this.encodeProjectId(projectId);
1176
+ return this.fetch("GET", `/projects/${encodedProject}/members`);
1177
+ }
1178
+ };
1179
+
1180
+ // src/client/users.ts
1181
+ var UsersClient = class extends GitLabApiClient {
1182
+ async getCurrentUser() {
1183
+ return this.fetch("GET", "/user");
1184
+ }
1185
+ /**
1186
+ * Get a user by ID
1187
+ * @param userId - The user ID
1188
+ */
1189
+ async getUser(userId) {
1190
+ return this.fetch("GET", `/users/${userId}`);
1191
+ }
1192
+ /**
1193
+ * Find users by username
1194
+ * @param username - The username to search for (exact match)
1195
+ */
1196
+ async getUserByUsername(username) {
1197
+ const params = new URLSearchParams();
1198
+ params.set("username", username);
1199
+ return this.fetch("GET", `/users?${params}`);
1200
+ }
1201
+ /**
1202
+ * Get a user's status
1203
+ * @param userId - The user ID
1204
+ */
1205
+ async getUserStatus(userId) {
1206
+ return this.fetch("GET", `/users/${userId}/status`);
1207
+ }
1208
+ };
1209
+
1210
+ // src/client/wikis.ts
1211
+ var WikisClient = class extends GitLabApiClient {
1212
+ async getWikiPage(projectId, slug) {
1213
+ const encodedProject = this.encodeProjectId(projectId);
1214
+ const encodedSlug = encodeURIComponent(slug);
1215
+ return this.fetch(
1216
+ "GET",
1217
+ `/projects/${encodedProject}/wikis/${encodedSlug}`
1218
+ );
1219
+ }
1220
+ };
1221
+
1222
+ // src/validation.ts
1223
+ function isValidGid(gid, expectedType) {
1224
+ const gidPattern = /^gid:\/\/gitlab\/([A-Za-z]+)\/(\d+)$/;
1225
+ const match = gid.match(gidPattern);
1226
+ if (!match) {
1227
+ return false;
1228
+ }
1229
+ if (expectedType && match[1] !== expectedType) {
1230
+ return false;
1231
+ }
1232
+ return true;
1233
+ }
1234
+ function validateGid(gid, expectedType) {
1235
+ if (!isValidGid(gid, expectedType)) {
1236
+ const typeMsg = expectedType ? ` of type '${expectedType}'` : "";
1237
+ throw new Error(
1238
+ `Invalid GitLab Global ID${typeMsg}: '${gid}'. Expected format: gid://gitlab/${expectedType || "ResourceType"}/{id}`
1239
+ );
1240
+ }
1241
+ }
1242
+
1243
+ // src/client/security.ts
1244
+ var CREATE_VULNERABILITY_ISSUE_MUTATION = `
1245
+ mutation($projectPath: ID!, $vulnerabilityIds: [VulnerabilityID!]!) {
1246
+ createVulnerabilityIssueLink(input: {
1247
+ projectPath: $projectPath
1248
+ vulnerabilityIds: $vulnerabilityIds
1249
+ }) {
1250
+ issue {
1251
+ id
1252
+ iid
1253
+ title
1254
+ webUrl
1255
+ }
1256
+ errors
1257
+ }
1258
+ }
1259
+ `;
1260
+ var DISMISS_VULNERABILITY_MUTATION = `
1261
+ mutation($id: VulnerabilityID!, $reason: VulnerabilityDismissalReason!, $comment: String) {
1262
+ vulnerabilityDismiss(input: {
1263
+ id: $id
1264
+ dismissalReason: $reason
1265
+ comment: $comment
1266
+ }) {
1267
+ vulnerability {
1268
+ id
1269
+ state
1270
+ dismissalReason
1271
+ }
1272
+ errors
1273
+ }
1274
+ }
1275
+ `;
1276
+ var CONFIRM_VULNERABILITY_MUTATION = `
1277
+ mutation($id: VulnerabilityID!, $comment: String) {
1278
+ vulnerabilityConfirm(input: {
1279
+ id: $id
1280
+ comment: $comment
1281
+ }) {
1282
+ vulnerability {
1283
+ id
1284
+ state
1285
+ }
1286
+ errors
1287
+ }
1288
+ }
1289
+ `;
1290
+ var REVERT_VULNERABILITY_MUTATION = `
1291
+ mutation($id: VulnerabilityID!, $comment: String) {
1292
+ vulnerabilityRevertToDetected(input: {
1293
+ id: $id
1294
+ comment: $comment
1295
+ }) {
1296
+ vulnerability {
1297
+ id
1298
+ state
1299
+ }
1300
+ errors
1301
+ }
1302
+ }
1303
+ `;
1304
+ var UPDATE_VULNERABILITY_SEVERITY_MUTATION = `
1305
+ mutation($ids: [VulnerabilityID!]!, $severity: VulnerabilitySeverity!, $comment: String!) {
1306
+ vulnerabilitiesUpdateSeverity(input: {
1307
+ ids: $ids
1308
+ severity: $severity
1309
+ comment: $comment
1310
+ }) {
1311
+ vulnerabilities {
1312
+ id
1313
+ severity
1314
+ }
1315
+ errors
1316
+ }
1317
+ }
1318
+ `;
1319
+ var LINK_VULNERABILITY_TO_ISSUE_MUTATION = `
1320
+ mutation($issueId: IssueID!, $vulnerabilityIds: [VulnerabilityID!]!) {
1321
+ vulnerabilityIssueLinksCreate(input: {
1322
+ issueId: $issueId
1323
+ vulnerabilityIds: $vulnerabilityIds
1324
+ }) {
1325
+ issue {
1326
+ id
1327
+ iid
1328
+ title
1329
+ webUrl
1330
+ }
1331
+ errors
1332
+ }
1333
+ }
1334
+ `;
1335
+ var SecurityClient = class extends GitLabApiClient {
1336
+ async listVulnerabilities(projectId, options) {
1337
+ const encodedProject = this.encodeProjectId(projectId);
1338
+ const params = new URLSearchParams();
1339
+ params.set("per_page", String(options?.limit || 20));
1340
+ if (options?.state) params.set("state", options.state);
1341
+ if (options?.severity) params.set("severity", options.severity);
1342
+ if (options?.report_type) params.set("report_type", options.report_type);
1343
+ return this.fetch(
1344
+ "GET",
1345
+ `/projects/${encodedProject}/vulnerabilities?${params}`
1346
+ );
1347
+ }
1348
+ async getVulnerabilityDetails(projectId, vulnerabilityId) {
1349
+ const encodedProject = this.encodeProjectId(projectId);
1350
+ return this.fetch(
1351
+ "GET",
1352
+ `/projects/${encodedProject}/vulnerabilities/${vulnerabilityId}`
1353
+ );
1354
+ }
1355
+ /**
1356
+ * Create an issue linked to one or more vulnerabilities
1357
+ * @param projectPath - Full path of the project (e.g., 'group/project')
1358
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
1359
+ * @returns The created issue
1360
+ */
1361
+ async createVulnerabilityIssue(projectPath, vulnerabilityIds) {
1362
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
1363
+ const result = await this.fetchGraphQL(CREATE_VULNERABILITY_ISSUE_MUTATION, { projectPath, vulnerabilityIds });
1364
+ if (result.createVulnerabilityIssueLink.errors.length > 0) {
1365
+ throw new Error(
1366
+ `Failed to create vulnerability issue: ${result.createVulnerabilityIssueLink.errors.join(", ")}`
1367
+ );
1368
+ }
1369
+ return result.createVulnerabilityIssueLink.issue;
1370
+ }
1371
+ /**
1372
+ * Dismiss a vulnerability with a reason
1373
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
1374
+ * @param reason - Dismissal reason
1375
+ * @param comment - Optional comment explaining the dismissal
1376
+ * @returns The updated vulnerability
1377
+ */
1378
+ async dismissVulnerability(vulnerabilityId, reason, comment) {
1379
+ validateGid(vulnerabilityId, "Vulnerability");
1380
+ const result = await this.fetchGraphQL(DISMISS_VULNERABILITY_MUTATION, { id: vulnerabilityId, reason, comment });
1381
+ if (result.vulnerabilityDismiss.errors.length > 0) {
1382
+ throw new Error(
1383
+ `Failed to dismiss vulnerability: ${result.vulnerabilityDismiss.errors.join(", ")}`
1384
+ );
1385
+ }
1386
+ return result.vulnerabilityDismiss.vulnerability;
1387
+ }
1388
+ /**
1389
+ * Confirm a vulnerability
1390
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
1391
+ * @param comment - Optional comment
1392
+ * @returns The updated vulnerability
1393
+ */
1394
+ async confirmVulnerability(vulnerabilityId, comment) {
1395
+ validateGid(vulnerabilityId, "Vulnerability");
1396
+ const result = await this.fetchGraphQL(CONFIRM_VULNERABILITY_MUTATION, { id: vulnerabilityId, comment });
1397
+ if (result.vulnerabilityConfirm.errors.length > 0) {
1398
+ throw new Error(
1399
+ `Failed to confirm vulnerability: ${result.vulnerabilityConfirm.errors.join(", ")}`
1400
+ );
1401
+ }
1402
+ return result.vulnerabilityConfirm.vulnerability;
1403
+ }
1404
+ /**
1405
+ * Revert a vulnerability back to detected state
1406
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
1407
+ * @param comment - Optional comment
1408
+ * @returns The updated vulnerability
1409
+ */
1410
+ async revertVulnerability(vulnerabilityId, comment) {
1411
+ validateGid(vulnerabilityId, "Vulnerability");
1412
+ const result = await this.fetchGraphQL(REVERT_VULNERABILITY_MUTATION, { id: vulnerabilityId, comment });
1413
+ if (result.vulnerabilityRevertToDetected.errors.length > 0) {
1414
+ throw new Error(
1415
+ `Failed to revert vulnerability: ${result.vulnerabilityRevertToDetected.errors.join(", ")}`
1416
+ );
1417
+ }
1418
+ return result.vulnerabilityRevertToDetected.vulnerability;
1419
+ }
1420
+ /**
1421
+ * Update the severity of one or more vulnerabilities
1422
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
1423
+ * @param severity - New severity level
1424
+ * @param comment - Comment explaining the severity change
1425
+ * @returns Array of updated vulnerabilities
1426
+ */
1427
+ async updateVulnerabilitySeverity(vulnerabilityIds, severity, comment) {
1428
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
1429
+ const result = await this.fetchGraphQL(UPDATE_VULNERABILITY_SEVERITY_MUTATION, { ids: vulnerabilityIds, severity, comment });
1430
+ if (result.vulnerabilitiesUpdateSeverity.errors.length > 0) {
1431
+ throw new Error(
1432
+ `Failed to update vulnerability severity: ${result.vulnerabilitiesUpdateSeverity.errors.join(", ")}`
1433
+ );
1434
+ }
1435
+ return result.vulnerabilitiesUpdateSeverity.vulnerabilities;
1436
+ }
1437
+ /**
1438
+ * Link an existing issue to one or more vulnerabilities
1439
+ * @param issueId - Issue ID in format 'gid://gitlab/Issue/{id}'
1440
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
1441
+ * @returns The linked issue
1442
+ */
1443
+ async linkVulnerabilityToIssue(issueId, vulnerabilityIds) {
1444
+ validateGid(issueId, "Issue");
1445
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
1446
+ const result = await this.fetchGraphQL(LINK_VULNERABILITY_TO_ISSUE_MUTATION, { issueId, vulnerabilityIds });
1447
+ if (result.vulnerabilityIssueLinksCreate.errors.length > 0) {
1448
+ throw new Error(
1449
+ `Failed to link vulnerability to issue: ${result.vulnerabilityIssueLinksCreate.errors.join(", ")}`
1450
+ );
1451
+ }
1452
+ return result.vulnerabilityIssueLinksCreate.issue;
1453
+ }
1454
+ };
1455
+
1456
+ // src/client/todos.ts
1457
+ var LIST_TODOS_QUERY = `
1458
+ query listTodos(
1459
+ $state: [TodoStateEnum!]
1460
+ $action: [TodoActionEnum!]
1461
+ $type: [TodoTargetEnum!]
1462
+ $projectId: [ID!]
1463
+ $groupId: [ID!]
1464
+ $authorId: [ID!]
1465
+ $first: Int
1466
+ $after: String
1467
+ $last: Int
1468
+ $before: String
1469
+ ) {
1470
+ currentUser {
1471
+ todos(
1472
+ state: $state
1473
+ action: $action
1474
+ type: $type
1475
+ projectId: $projectId
1476
+ groupId: $groupId
1477
+ authorId: $authorId
1478
+ first: $first
1479
+ after: $after
1480
+ last: $last
1481
+ before: $before
1482
+ ) {
1483
+ count
1484
+ pageInfo {
1485
+ hasNextPage
1486
+ hasPreviousPage
1487
+ startCursor
1488
+ endCursor
1489
+ }
1490
+ nodes {
1491
+ id
1492
+ body
1493
+ state
1494
+ action
1495
+ createdAt
1496
+ targetType
1497
+ targetUrl
1498
+ snoozedUntil
1499
+ project {
1500
+ id
1501
+ name
1502
+ fullPath
1503
+ }
1504
+ group {
1505
+ id
1506
+ name
1507
+ fullPath
1508
+ }
1509
+ author {
1510
+ id
1511
+ username
1512
+ name
1513
+ avatarUrl
1514
+ }
1515
+ }
1516
+ }
1517
+ }
1518
+ }
1519
+ `;
1520
+ var TodosClient = class extends GitLabApiClient {
1521
+ async listTodos(options) {
1522
+ if (options?.first !== void 0 && options?.last !== void 0) {
1523
+ throw new Error(
1524
+ 'Cannot specify both "first" and "last" pagination parameters. Use "first"/"after" for forward pagination or "last"/"before" for backward pagination.'
1525
+ );
1526
+ }
1527
+ const variables = {};
1528
+ if (options?.first !== void 0) {
1529
+ variables.first = options.first;
1530
+ } else if (options?.last === void 0) {
1531
+ variables.first = 20;
1532
+ }
1533
+ if (options?.after) variables.after = options.after;
1534
+ if (options?.last !== void 0) variables.last = options.last;
1535
+ if (options?.before) variables.before = options.before;
1536
+ if (options?.action) {
1537
+ variables.action = [this.mapTodoAction(options.action)];
1538
+ }
1539
+ if (options?.author_id) {
1540
+ variables.authorId = [`gid://gitlab/User/${options.author_id}`];
1541
+ }
1542
+ if (options?.project_id) {
1543
+ variables.projectId = [options.project_id];
1544
+ }
1545
+ if (options?.group_id) {
1546
+ variables.groupId = [options.group_id];
1547
+ }
1548
+ if (options?.state) {
1549
+ variables.state = [options.state.toLowerCase()];
1550
+ }
1551
+ if (options?.type) {
1552
+ variables.type = [this.mapTodoTargetType(options.type)];
1553
+ }
1554
+ const result = await this.fetchGraphQL(LIST_TODOS_QUERY, variables);
1555
+ const todos = result.currentUser.todos;
1556
+ return {
1557
+ todos
1558
+ };
1559
+ }
1560
+ mapTodoAction(action) {
1561
+ const actionMap = {
1562
+ assigned: "ASSIGNED",
1563
+ mentioned: "MENTIONED",
1564
+ build_failed: "BUILD_FAILED",
1565
+ marked: "MARKED",
1566
+ approval_required: "APPROVAL_REQUIRED",
1567
+ unmergeable: "UNMERGEABLE",
1568
+ directly_addressed: "DIRECTLY_ADDRESSED",
1569
+ merge_train_removed: "MERGE_TRAIN_REMOVED",
1570
+ review_requested: "REVIEW_REQUESTED"
1571
+ };
1572
+ return actionMap[action] || action.toUpperCase();
1573
+ }
1574
+ mapTodoTargetType(type) {
1575
+ const typeMap = {
1576
+ Issue: "ISSUE",
1577
+ MergeRequest: "MERGEREQUEST",
1578
+ "DesignManagement::Design": "DESIGN",
1579
+ Alert: "ALERT",
1580
+ Commit: "COMMIT",
1581
+ Epic: "EPIC"
1582
+ };
1583
+ return typeMap[type] || type;
1584
+ }
1585
+ async markTodoAsDone(todoId) {
1586
+ try {
1587
+ const result = await this.fetch(
1588
+ "POST",
1589
+ `/todos/${todoId}/mark_as_done`
1590
+ );
1591
+ return { success: true, todo: result };
1592
+ } catch (error) {
1593
+ if (error instanceof Error && error.message.includes("404")) {
1594
+ return {
1595
+ success: false,
1596
+ message: `Todo ${todoId} not found. It may have already been marked as done or does not exist.`
1597
+ };
1598
+ }
1599
+ throw error;
1600
+ }
1601
+ }
1602
+ async markAllTodosAsDone() {
1603
+ return this.fetch("POST", "/todos/mark_as_done");
1604
+ }
1605
+ async getTodoCount() {
1606
+ const result = await this.fetchGraphQL(`query { currentUser { todos(state: [pending]) { count } } }`);
1607
+ return { count: result.currentUser.todos.count };
1608
+ }
1609
+ };
1610
+
1611
+ // src/client/epics.ts
1612
+ var LIST_EPIC_NOTES_QUERY = `
1613
+ ${NOTES_FRAGMENT}
1614
+ ${NOTES_CONNECTION_FRAGMENT}
1615
+ query listEpicNotes(
1616
+ $groupPath: ID!
1617
+ $epicIid: String!
1618
+ $first: Int
1619
+ $after: String
1620
+ $last: Int
1621
+ $before: String
1622
+ ) {
1623
+ group(fullPath: $groupPath) {
1624
+ epic(iid: $epicIid) {
1625
+ notes(
1626
+ first: $first
1627
+ after: $after
1628
+ last: $last
1629
+ before: $before
1630
+ ) {
1631
+ ...NotesConnectionFields
1632
+ }
1633
+ }
1634
+ }
1635
+ }
1636
+ `;
1637
+ var LIST_EPIC_DISCUSSIONS_QUERY = `
1638
+ ${DISCUSSION_NOTE_FRAGMENT}
1639
+ ${DISCUSSION_FRAGMENT}
1640
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
1641
+ query listEpicDiscussions(
1642
+ $groupPath: ID!
1643
+ $epicIid: String!
1644
+ $first: Int
1645
+ $after: String
1646
+ $last: Int
1647
+ $before: String
1648
+ ) {
1649
+ group(fullPath: $groupPath) {
1650
+ epic(iid: $epicIid) {
1651
+ discussions(
1652
+ first: $first
1653
+ after: $after
1654
+ last: $last
1655
+ before: $before
1656
+ ) {
1657
+ ...DiscussionsConnectionFields
1658
+ }
1659
+ }
1660
+ }
1661
+ }
1662
+ `;
1663
+ var EpicsClient = class extends GitLabApiClient {
1664
+ async getEpic(groupId, epicIid) {
1665
+ const encodedGroup = encodeURIComponent(groupId);
1666
+ return this.fetch("GET", `/groups/${encodedGroup}/epics/${epicIid}`);
1667
+ }
1668
+ async listEpics(options) {
1669
+ const encodedGroup = encodeURIComponent(options.groupId);
1670
+ const params = new URLSearchParams();
1671
+ params.set("per_page", String(options.limit || 20));
1672
+ if (options.state) params.set("state", options.state);
1673
+ if (options.author_id) params.set("author_id", String(options.author_id));
1674
+ if (options.labels) params.set("labels", options.labels);
1675
+ if (options.search) params.set("search", options.search);
1676
+ return this.fetch("GET", `/groups/${encodedGroup}/epics?${params}`);
1677
+ }
1678
+ async createEpic(groupId, options) {
1679
+ const encodedGroup = encodeURIComponent(groupId);
1680
+ return this.fetch("POST", `/groups/${encodedGroup}/epics`, options);
1681
+ }
1682
+ async updateEpic(groupId, epicIid, options) {
1683
+ const encodedGroup = encodeURIComponent(groupId);
1684
+ return this.fetch(
1685
+ "PUT",
1686
+ `/groups/${encodedGroup}/epics/${epicIid}`,
1687
+ options
1688
+ );
1689
+ }
1690
+ async listEpicIssues(groupId, epicIid) {
1691
+ const encodedGroup = encodeURIComponent(groupId);
1692
+ return this.fetch(
1693
+ "GET",
1694
+ `/groups/${encodedGroup}/epics/${epicIid}/issues`
1695
+ );
1696
+ }
1697
+ async addIssueToEpic(groupId, epicIid, issueId) {
1698
+ const encodedGroup = encodeURIComponent(groupId);
1699
+ return this.fetch(
1700
+ "POST",
1701
+ `/groups/${encodedGroup}/epics/${epicIid}/issues/${issueId}`
1702
+ );
1703
+ }
1704
+ async removeIssueFromEpic(groupId, epicIid, epicIssueId) {
1705
+ const encodedGroup = encodeURIComponent(groupId);
1706
+ return this.fetch(
1707
+ "DELETE",
1708
+ `/groups/${encodedGroup}/epics/${epicIid}/issues/${epicIssueId}`
1709
+ );
1710
+ }
1711
+ /**
1712
+ * List notes on an epic using GraphQL API with pagination support
1713
+ */
1714
+ async listEpicNotes(groupId, epicIid, options) {
1715
+ const variables = {
1716
+ groupPath: groupId,
1717
+ epicIid: String(epicIid),
1718
+ ...buildPaginationVariables(options)
1719
+ };
1720
+ const result = await this.fetchGraphQL(LIST_EPIC_NOTES_QUERY, variables);
1721
+ const notes = result.group?.epic?.notes;
1722
+ if (!notes) {
1723
+ throw new Error("Epic not found or access denied");
1724
+ }
1725
+ return {
1726
+ notes,
1727
+ pageInfo: notes.pageInfo,
1728
+ totalCount: notes.count
1729
+ };
1730
+ }
1731
+ /**
1732
+ * List discussions on an epic using GraphQL API with pagination support
1733
+ */
1734
+ async listEpicDiscussions(groupId, epicIid, options) {
1735
+ const variables = {
1736
+ groupPath: groupId,
1737
+ epicIid: String(epicIid),
1738
+ ...buildDiscussionsPaginationVariables(options)
1739
+ };
1740
+ const result = await this.fetchGraphQL(LIST_EPIC_DISCUSSIONS_QUERY, variables);
1741
+ const discussions = result.group?.epic?.discussions;
1742
+ if (!discussions) {
1743
+ throw new Error("Epic not found or access denied");
1744
+ }
1745
+ return { discussions };
1746
+ }
1747
+ async getEpicDiscussion(groupId, epicIid, discussionId) {
1748
+ const encodedGroup = encodeURIComponent(groupId);
1749
+ return this.fetch(
1750
+ "GET",
1751
+ `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}`
1752
+ );
1753
+ }
1754
+ async createEpicNote(groupId, epicIid, body, discussionId) {
1755
+ const encodedGroup = encodeURIComponent(groupId);
1756
+ if (discussionId) {
1757
+ return this.fetch(
1758
+ "POST",
1759
+ `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}/notes`,
1760
+ { body }
1761
+ );
1762
+ }
1763
+ return this.fetch(
1764
+ "POST",
1765
+ `/groups/${encodedGroup}/epics/${epicIid}/notes`,
1766
+ { body }
1767
+ );
1768
+ }
1769
+ async getEpicNote(groupId, epicIid, noteId) {
1770
+ const encodedGroup = encodeURIComponent(groupId);
1771
+ return this.fetch(
1772
+ "GET",
1773
+ `/groups/${encodedGroup}/epics/${epicIid}/notes/${noteId}`
1774
+ );
1775
+ }
1776
+ };
1777
+
1778
+ // src/client/snippets.ts
1779
+ var LIST_SNIPPET_NOTES_QUERY = `
1780
+ ${NOTES_FRAGMENT}
1781
+ ${NOTES_CONNECTION_FRAGMENT}
1782
+ query listSnippetNotes(
1783
+ $snippetGid: SnippetID!
1784
+ $first: Int
1785
+ $after: String
1786
+ $last: Int
1787
+ $before: String
1788
+ ) {
1789
+ snippets(ids: [$snippetGid]) {
1790
+ nodes {
1791
+ notes(
1792
+ first: $first
1793
+ after: $after
1794
+ last: $last
1795
+ before: $before
1796
+ ) {
1797
+ ...NotesConnectionFields
1798
+ }
1799
+ }
1800
+ }
1801
+ }
1802
+ `;
1803
+ var LIST_SNIPPET_DISCUSSIONS_QUERY = `
1804
+ ${DISCUSSION_NOTE_FRAGMENT}
1805
+ ${DISCUSSION_FRAGMENT}
1806
+ ${DISCUSSIONS_CONNECTION_FRAGMENT}
1807
+ query listSnippetDiscussions(
1808
+ $snippetGid: SnippetID!
1809
+ $first: Int
1810
+ $after: String
1811
+ $last: Int
1812
+ $before: String
1813
+ ) {
1814
+ snippets(ids: [$snippetGid]) {
1815
+ nodes {
1816
+ discussions(
1817
+ first: $first
1818
+ after: $after
1819
+ last: $last
1820
+ before: $before
1821
+ ) {
1822
+ ...DiscussionsConnectionFields
1823
+ }
1824
+ }
1825
+ }
1826
+ }
1827
+ `;
1828
+ var SnippetsClient = class extends GitLabApiClient {
1829
+ /**
1830
+ * List notes on a snippet using GraphQL API with pagination support
1831
+ */
1832
+ async listSnippetNotes(projectId, snippetId, options) {
1833
+ const variables = {
1834
+ snippetGid: `gid://gitlab/ProjectSnippet/${snippetId}`,
1835
+ ...buildPaginationVariables(options)
1836
+ };
1837
+ const result = await this.fetchGraphQL(LIST_SNIPPET_NOTES_QUERY, variables);
1838
+ const notes = result.snippets.nodes[0]?.notes || {
1839
+ nodes: [],
1840
+ pageInfo: {
1841
+ hasNextPage: false,
1842
+ hasPreviousPage: false,
1843
+ startCursor: null,
1844
+ endCursor: null
1845
+ },
1846
+ count: 0
1847
+ };
1848
+ return {
1849
+ notes,
1850
+ pageInfo: notes.pageInfo,
1851
+ totalCount: notes.count
1852
+ };
1853
+ }
1854
+ /**
1855
+ * List discussions on a snippet using GraphQL API with pagination support
1856
+ */
1857
+ async listSnippetDiscussions(projectId, snippetId, options) {
1858
+ const variables = {
1859
+ snippetGid: `gid://gitlab/ProjectSnippet/${snippetId}`,
1860
+ ...buildDiscussionsPaginationVariables(options)
1861
+ };
1862
+ const result = await this.fetchGraphQL(LIST_SNIPPET_DISCUSSIONS_QUERY, variables);
1863
+ const discussions = result.snippets.nodes[0]?.discussions || {
1864
+ nodes: [],
1865
+ pageInfo: {
1866
+ hasNextPage: false,
1867
+ hasPreviousPage: false,
1868
+ startCursor: null,
1869
+ endCursor: null
1870
+ }
1871
+ };
1872
+ return { discussions };
1873
+ }
1874
+ async getSnippetDiscussion(projectId, snippetId, discussionId) {
1875
+ const encodedProject = this.encodeProjectId(projectId);
1876
+ return this.fetch(
1877
+ "GET",
1878
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}`
1879
+ );
1880
+ }
1881
+ async createSnippetNote(projectId, snippetId, body, discussionId) {
1882
+ const encodedProject = this.encodeProjectId(projectId);
1883
+ if (discussionId) {
1884
+ return this.fetch(
1885
+ "POST",
1886
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}/notes`,
1887
+ { body }
1888
+ );
1889
+ }
1890
+ return this.fetch(
1891
+ "POST",
1892
+ `/projects/${encodedProject}/snippets/${snippetId}/notes`,
1893
+ { body }
1894
+ );
1895
+ }
1896
+ async createSnippetDiscussion(projectId, snippetId, body) {
1897
+ const encodedProject = this.encodeProjectId(projectId);
1898
+ return this.fetch(
1899
+ "POST",
1900
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions`,
1901
+ { body }
1902
+ );
1903
+ }
1904
+ };
1905
+
1906
+ // src/client/discussions.ts
1907
+ var DiscussionsClient = class extends GitLabApiClient {
1908
+ /**
1909
+ * Universal method to reply to any discussion thread
1910
+ * Supports: merge requests, issues, epics, commits, snippets
1911
+ */
1912
+ async replyToDiscussion(resourceType, resourceId, discussionId, body) {
1913
+ switch (resourceType) {
1914
+ case "merge_request": {
1915
+ if (!resourceId.projectId || !resourceId.iid) {
1916
+ throw new Error("projectId and iid are required for merge_request discussions");
1917
+ }
1918
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1919
+ return this.fetch(
1920
+ "POST",
1921
+ `/projects/${encodedProject}/merge_requests/${resourceId.iid}/discussions/${discussionId}/notes`,
1922
+ { body }
1923
+ );
1924
+ }
1925
+ case "issue": {
1926
+ if (!resourceId.projectId || !resourceId.iid) {
1927
+ throw new Error("projectId and iid are required for issue discussions");
1928
+ }
1929
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1930
+ return this.fetch(
1931
+ "POST",
1932
+ `/projects/${encodedProject}/issues/${resourceId.iid}/discussions/${discussionId}/notes`,
1933
+ { body }
1934
+ );
1935
+ }
1936
+ case "epic": {
1937
+ if (!resourceId.groupId || !resourceId.iid) {
1938
+ throw new Error("groupId and iid are required for epic discussions");
1939
+ }
1940
+ const encodedGroup = encodeURIComponent(resourceId.groupId);
1941
+ return this.fetch(
1942
+ "POST",
1943
+ `/groups/${encodedGroup}/epics/${resourceId.iid}/discussions/${discussionId}/notes`,
1944
+ { body }
1945
+ );
1946
+ }
1947
+ case "commit": {
1948
+ if (!resourceId.projectId || !resourceId.sha) {
1949
+ throw new Error("projectId and sha are required for commit discussions");
1950
+ }
1951
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1952
+ return this.fetch(
1953
+ "POST",
1954
+ `/projects/${encodedProject}/repository/commits/${resourceId.sha}/discussions/${discussionId}/notes`,
1955
+ { body }
1956
+ );
1957
+ }
1958
+ case "snippet": {
1959
+ if (!resourceId.projectId || !resourceId.snippetId) {
1960
+ throw new Error("projectId and snippetId are required for snippet discussions");
1961
+ }
1962
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1963
+ return this.fetch(
1964
+ "POST",
1965
+ `/projects/${encodedProject}/snippets/${resourceId.snippetId}/discussions/${discussionId}/notes`,
1966
+ { body }
1967
+ );
1968
+ }
1969
+ default:
1970
+ throw new Error(`Unsupported resource type: ${resourceType}`);
1971
+ }
1972
+ }
1973
+ /**
1974
+ * Universal method to get a specific discussion from any resource type
1975
+ * Supports: merge requests, issues, epics, commits, snippets
1976
+ */
1977
+ async getDiscussion(resourceType, resourceId, discussionId) {
1978
+ switch (resourceType) {
1979
+ case "merge_request": {
1980
+ if (!resourceId.projectId || !resourceId.iid) {
1981
+ throw new Error("projectId and iid are required for merge_request discussions");
1982
+ }
1983
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1984
+ return this.fetch(
1985
+ "GET",
1986
+ `/projects/${encodedProject}/merge_requests/${resourceId.iid}/discussions/${discussionId}`
1987
+ );
1988
+ }
1989
+ case "issue": {
1990
+ if (!resourceId.projectId || !resourceId.iid) {
1991
+ throw new Error("projectId and iid are required for issue discussions");
1992
+ }
1993
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1994
+ return this.fetch(
1995
+ "GET",
1996
+ `/projects/${encodedProject}/issues/${resourceId.iid}/discussions/${discussionId}`
1997
+ );
1998
+ }
1999
+ case "epic": {
2000
+ if (!resourceId.groupId || !resourceId.iid) {
2001
+ throw new Error("groupId and iid are required for epic discussions");
2002
+ }
2003
+ const encodedGroup = encodeURIComponent(resourceId.groupId);
2004
+ return this.fetch(
2005
+ "GET",
2006
+ `/groups/${encodedGroup}/epics/${resourceId.iid}/discussions/${discussionId}`
2007
+ );
2008
+ }
2009
+ case "commit": {
2010
+ if (!resourceId.projectId || !resourceId.sha) {
2011
+ throw new Error("projectId and sha are required for commit discussions");
2012
+ }
2013
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
2014
+ return this.fetch(
2015
+ "GET",
2016
+ `/projects/${encodedProject}/repository/commits/${resourceId.sha}/discussions/${discussionId}`
2017
+ );
2018
+ }
2019
+ case "snippet": {
2020
+ if (!resourceId.projectId || !resourceId.snippetId) {
2021
+ throw new Error("projectId and snippetId are required for snippet discussions");
2022
+ }
2023
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
2024
+ return this.fetch(
2025
+ "GET",
2026
+ `/projects/${encodedProject}/snippets/${resourceId.snippetId}/discussions/${discussionId}`
2027
+ );
2028
+ }
2029
+ default:
2030
+ throw new Error(`Unsupported resource type: ${resourceType}`);
2031
+ }
2032
+ }
2033
+ };
2034
+
2035
+ // src/client/audit.ts
2036
+ var AuditClient = class extends GitLabApiClient {
2037
+ /**
2038
+ * List audit events for a project
2039
+ * Requires: Project owner role
2040
+ * API: GET /projects/:id/audit_events
2041
+ */
2042
+ async listProjectAuditEvents(projectId, options = {}) {
2043
+ const encodedId = this.encodeProjectId(projectId);
2044
+ const params = new URLSearchParams();
2045
+ if (options.created_after) params.append("created_after", options.created_after);
2046
+ if (options.created_before) params.append("created_before", options.created_before);
2047
+ if (options.entity_type) params.append("entity_type", options.entity_type);
2048
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
2049
+ if (options.author_id) params.append("author_id", options.author_id.toString());
2050
+ if (options.per_page) params.append("per_page", options.per_page.toString());
2051
+ if (options.page) params.append("page", options.page.toString());
2052
+ const query = params.toString();
2053
+ const path2 = `/projects/${encodedId}/audit_events${query ? `?${query}` : ""}`;
2054
+ return this.fetch("GET", path2);
2055
+ }
2056
+ /**
2057
+ * List audit events for a group
2058
+ * Requires: Group owner role
2059
+ * API: GET /groups/:id/audit_events
2060
+ */
2061
+ async listGroupAuditEvents(groupId, options = {}) {
2062
+ const encodedId = encodeURIComponent(groupId);
2063
+ const params = new URLSearchParams();
2064
+ if (options.created_after) params.append("created_after", options.created_after);
2065
+ if (options.created_before) params.append("created_before", options.created_before);
2066
+ if (options.entity_type) params.append("entity_type", options.entity_type);
2067
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
2068
+ if (options.author_id) params.append("author_id", options.author_id.toString());
2069
+ if (options.per_page) params.append("per_page", options.per_page.toString());
2070
+ if (options.page) params.append("page", options.page.toString());
2071
+ const query = params.toString();
2072
+ const path2 = `/groups/${encodedId}/audit_events${query ? `?${query}` : ""}`;
2073
+ return this.fetch("GET", path2);
2074
+ }
2075
+ /**
2076
+ * List instance-level audit events
2077
+ * Requires: Administrator access
2078
+ * API: GET /audit_events
2079
+ */
2080
+ async listInstanceAuditEvents(options = {}) {
2081
+ const params = new URLSearchParams();
2082
+ if (options.created_after) params.append("created_after", options.created_after);
2083
+ if (options.created_before) params.append("created_before", options.created_before);
2084
+ if (options.entity_type) params.append("entity_type", options.entity_type);
2085
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
2086
+ if (options.author_id) params.append("author_id", options.author_id.toString());
2087
+ if (options.per_page) params.append("per_page", options.per_page.toString());
2088
+ if (options.page) params.append("page", options.page.toString());
2089
+ const query = params.toString();
2090
+ const path2 = `/audit_events${query ? `?${query}` : ""}`;
2091
+ return this.fetch("GET", path2);
2092
+ }
2093
+ };
2094
+
2095
+ // src/client/award-emoji.ts
2096
+ var AwardEmojiClient = class extends GitLabApiClient {
2097
+ /**
2098
+ * Build the base path for award emoji operations
2099
+ */
2100
+ buildAwardEmojiPath(resource) {
2101
+ const encodedProject = this.encodeProjectId(resource.projectId);
2102
+ let basePath;
2103
+ switch (resource.resourceType) {
2104
+ case "merge_request":
2105
+ basePath = `/projects/${encodedProject}/merge_requests/${resource.resourceIid}`;
2106
+ break;
2107
+ case "issue":
2108
+ basePath = `/projects/${encodedProject}/issues/${resource.resourceIid}`;
2109
+ break;
2110
+ case "snippet":
2111
+ basePath = `/projects/${encodedProject}/snippets/${resource.resourceIid}`;
2112
+ break;
2113
+ }
2114
+ if (resource.noteId) {
2115
+ return `${basePath}/notes/${resource.noteId}/award_emoji`;
2116
+ }
2117
+ return `${basePath}/award_emoji`;
2118
+ }
2119
+ /**
2120
+ * List all award emoji on a resource or note
2121
+ */
2122
+ async listAwardEmoji(resource) {
2123
+ const path2 = this.buildAwardEmojiPath(resource);
2124
+ return this.fetch("GET", path2);
2125
+ }
2126
+ /**
2127
+ * Get a single award emoji by ID
2128
+ */
2129
+ async getAwardEmoji(resource, awardId) {
2130
+ const path2 = `${this.buildAwardEmojiPath(resource)}/${awardId}`;
2131
+ return this.fetch("GET", path2);
2132
+ }
2133
+ /**
2134
+ * Add an award emoji (reaction) to a resource or note
2135
+ *
2136
+ * @param resource - The resource to add the emoji to
2137
+ * @param name - The emoji name without colons (e.g., 'thumbsup', 'rocket', 'eyes')
2138
+ */
2139
+ async createAwardEmoji(resource, name) {
2140
+ const path2 = this.buildAwardEmojiPath(resource);
2141
+ return this.fetch("POST", path2, { name });
2142
+ }
2143
+ /**
2144
+ * Remove an award emoji from a resource or note
2145
+ */
2146
+ async deleteAwardEmoji(resource, awardId) {
2147
+ const path2 = `${this.buildAwardEmojiPath(resource)}/${awardId}`;
2148
+ await this.fetch("DELETE", path2);
2149
+ }
2150
+ };
2151
+
2152
+ // src/client/index.ts
2153
+ var UnifiedGitLabClient = class extends GitLabApiClient {
2154
+ };
2155
+ function applyMixins(derivedCtor, constructors) {
2156
+ constructors.forEach((baseCtor) => {
2157
+ Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
2158
+ Object.defineProperty(
2159
+ derivedCtor.prototype,
2160
+ name,
2161
+ Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || /* @__PURE__ */ Object.create(null)
2162
+ );
2163
+ });
2164
+ });
2165
+ }
2166
+ applyMixins(UnifiedGitLabClient, [
2167
+ MergeRequestsClient,
2168
+ IssuesClient,
2169
+ WorkItemsClient,
2170
+ PipelinesClient,
2171
+ RepositoryClient,
2172
+ SearchClient,
2173
+ ProjectsClient,
2174
+ UsersClient,
2175
+ WikisClient,
2176
+ SecurityClient,
2177
+ TodosClient,
2178
+ EpicsClient,
2179
+ SnippetsClient,
2180
+ DiscussionsClient,
2181
+ AuditClient,
2182
+ AwardEmojiClient
2183
+ ]);
2184
+
2185
+ // src/utils.ts
2186
+ function readTokenFromAuthStorage() {
2187
+ try {
2188
+ const authPath = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
2189
+ if (!fs.existsSync(authPath)) {
2190
+ return void 0;
2191
+ }
2192
+ const authData = JSON.parse(fs.readFileSync(authPath, "utf-8"));
2193
+ const gitlabAuth = authData?.gitlab;
2194
+ if (!gitlabAuth) {
2195
+ return void 0;
2196
+ }
2197
+ if (gitlabAuth.type === "oauth" && gitlabAuth.access) {
2198
+ return gitlabAuth.access;
2199
+ }
2200
+ if (gitlabAuth.type === "api" && gitlabAuth.key) {
2201
+ return gitlabAuth.key;
2202
+ }
2203
+ return gitlabAuth.token || void 0;
2204
+ } catch (error) {
2205
+ return void 0;
2206
+ }
2207
+ }
2208
+ function getGitLabClient() {
2209
+ let token = process.env["GITLAB_TOKEN"];
2210
+ if (!token) {
2211
+ token = readTokenFromAuthStorage();
2212
+ }
2213
+ if (!token) {
2214
+ throw new Error(
2215
+ "GitLab API token not found. Set GITLAB_TOKEN environment variable or configure it in OpenCode auth storage (~/.local/share/opencode/auth.json)."
2216
+ );
2217
+ }
2218
+ const instanceUrl = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
2219
+ return new UnifiedGitLabClient(instanceUrl, token);
2220
+ }
2221
+
2222
+ // src/tools/merge-requests.ts
2223
+ var z = tool.schema;
2224
+ var mergeRequestTools = {
2225
+ gitlab_get_merge_request: tool({
2226
+ description: `Get details of a specific merge request by project and MR IID.
2227
+ Returns: title, description, state, author, assignees, reviewers, labels, diff stats, and discussion notes.`,
2228
+ args: {
2229
+ project_id: z.string().describe('The project ID or URL-encoded path (e.g., "gitlab-org/gitlab" or "123")'),
2230
+ mr_iid: z.number().describe("The internal ID of the merge request within the project"),
2231
+ include_changes: z.boolean().optional().describe("Whether to include the list of changed files (default: false)")
2232
+ },
2233
+ execute: async (args, _ctx) => {
2234
+ const client = getGitLabClient();
2235
+ const mr = await client.getMergeRequest(args.project_id, args.mr_iid, args.include_changes);
2236
+ return JSON.stringify(mr, null, 2);
2237
+ }
2238
+ }),
2239
+ gitlab_list_merge_requests: tool({
2240
+ description: `List merge requests for a project or search globally.
2241
+ Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.`,
2242
+ args: {
2243
+ project_id: z.string().optional().describe("The project ID or path. If not provided, searches globally."),
2244
+ state: z.enum(["opened", "closed", "merged", "all"]).optional().describe("Filter by MR state (default: opened)"),
2245
+ scope: z.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
2246
+ search: z.string().optional().describe("Search MRs by title or description"),
2247
+ labels: z.string().optional().describe("Comma-separated list of labels to filter by"),
2248
+ limit: z.number().optional().describe("Maximum number of results (default: 20)")
2249
+ },
2250
+ execute: async (args, _ctx) => {
2251
+ const client = getGitLabClient();
2252
+ const mrs = await client.listMergeRequests({
2253
+ projectId: args.project_id,
2254
+ state: args.state,
2255
+ scope: args.scope,
2256
+ search: args.search,
2257
+ labels: args.labels,
2258
+ limit: args.limit
2259
+ });
2260
+ return JSON.stringify(mrs, null, 2);
2261
+ }
2262
+ }),
2263
+ gitlab_get_mr_changes: tool({
2264
+ description: `Get the file changes (diff) for a merge request.
2265
+ Returns the list of files changed with their diffs.`,
2266
+ args: {
2267
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2268
+ mr_iid: z.number().describe("The internal ID of the merge request")
2269
+ },
2270
+ execute: async (args, _ctx) => {
2271
+ const client = getGitLabClient();
2272
+ const changes = await client.getMrChanges(args.project_id, args.mr_iid);
2273
+ return JSON.stringify(changes, null, 2);
2274
+ }
2275
+ }),
2276
+ gitlab_create_merge_request: tool({
2277
+ description: `Create a new merge request.
2278
+ Returns the created merge request with all details.`,
2279
+ args: {
2280
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2281
+ source_branch: z.string().describe("The source branch name"),
2282
+ target_branch: z.string().describe("The target branch name"),
2283
+ title: z.string().describe("The title of the merge request"),
2284
+ description: z.string().optional().describe("The description of the merge request (supports Markdown)"),
2285
+ assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
2286
+ reviewer_ids: z.array(z.number()).optional().describe("Array of user IDs to review"),
2287
+ labels: z.string().optional().describe("Comma-separated list of labels"),
2288
+ milestone_id: z.number().optional().describe("The ID of a milestone"),
2289
+ remove_source_branch: z.boolean().optional().describe("Remove source branch after merge (default: false)"),
2290
+ squash: z.boolean().optional().describe("Squash commits on merge (default: false)"),
2291
+ allow_collaboration: z.boolean().optional().describe("Allow commits from members who can merge to the target branch")
2292
+ },
2293
+ execute: async (args, _ctx) => {
2294
+ const client = getGitLabClient();
2295
+ const mr = await client.createMergeRequest(args.project_id, {
2296
+ source_branch: args.source_branch,
2297
+ target_branch: args.target_branch,
2298
+ title: args.title,
2299
+ description: args.description,
2300
+ assignee_ids: args.assignee_ids,
2301
+ reviewer_ids: args.reviewer_ids,
2302
+ labels: args.labels,
2303
+ milestone_id: args.milestone_id,
2304
+ remove_source_branch: args.remove_source_branch,
2305
+ squash: args.squash,
2306
+ allow_collaboration: args.allow_collaboration
2307
+ });
2308
+ return JSON.stringify(mr, null, 2);
2309
+ }
2310
+ }),
2311
+ gitlab_update_merge_request: tool({
2312
+ description: `Update an existing merge request.
2313
+ Can update title, description, state, assignees, reviewers, labels, and more.`,
2314
+ args: {
2315
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2316
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2317
+ title: z.string().optional().describe("The new title"),
2318
+ description: z.string().optional().describe("The new description (supports Markdown)"),
2319
+ state_event: z.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
2320
+ assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
2321
+ reviewer_ids: z.array(z.number()).optional().describe("Array of user IDs to review"),
2322
+ labels: z.string().optional().describe("Comma-separated list of labels"),
2323
+ milestone_id: z.number().optional().describe("The ID of a milestone"),
2324
+ remove_source_branch: z.boolean().optional().describe("Remove source branch after merge"),
2325
+ squash: z.boolean().optional().describe("Squash commits on merge"),
2326
+ target_branch: z.string().optional().describe("The target branch name")
2327
+ },
2328
+ execute: async (args, _ctx) => {
2329
+ const client = getGitLabClient();
2330
+ const mr = await client.updateMergeRequest(args.project_id, args.mr_iid, {
2331
+ title: args.title,
2332
+ description: args.description,
2333
+ state_event: args.state_event,
2334
+ assignee_ids: args.assignee_ids,
2335
+ reviewer_ids: args.reviewer_ids,
2336
+ labels: args.labels,
2337
+ milestone_id: args.milestone_id,
2338
+ remove_source_branch: args.remove_source_branch,
2339
+ squash: args.squash,
2340
+ target_branch: args.target_branch
2341
+ });
2342
+ return JSON.stringify(mr, null, 2);
2343
+ }
2344
+ }),
2345
+ gitlab_get_mr_details: tool({
2346
+ description: `Get additional details for a merge request.
2347
+
2348
+ Detail types:
2349
+ - commits: List all commits in the MR
2350
+ - pipelines: List all pipelines that ran for the MR
2351
+
2352
+ Examples:
2353
+ - Get commits: detail_type="commits", project_id="group/project", mr_iid=123
2354
+ - Get pipelines: detail_type="pipelines", project_id="group/project", mr_iid=123`,
2355
+ args: {
2356
+ detail_type: z.enum(["commits", "pipelines"]).describe("Type of details to fetch"),
2357
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2358
+ mr_iid: z.number().describe("The internal ID of the merge request")
2359
+ },
2360
+ execute: async (args, _ctx) => {
2361
+ const client = getGitLabClient();
2362
+ switch (args.detail_type) {
2363
+ case "commits": {
2364
+ const commits = await client.getMrCommits(args.project_id, args.mr_iid);
2365
+ return JSON.stringify(commits, null, 2);
2366
+ }
2367
+ case "pipelines": {
2368
+ const pipelines = await client.getMrPipelines(args.project_id, args.mr_iid);
2369
+ return JSON.stringify(pipelines, null, 2);
2370
+ }
2371
+ }
2372
+ }
2373
+ }),
2374
+ gitlab_list_merge_request_diffs: tool({
2375
+ description: `List merge request diffs with pagination support.
2376
+ Returns the list of file diffs for a merge request, with support for pagination to handle large changesets.
2377
+ This is useful when you need to process diffs in chunks or when the MR has many changed files.`,
2378
+ args: {
2379
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2380
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2381
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
2382
+ per_page: z.number().optional().describe("Number of diffs per page (default: 20, max: 100)")
2383
+ },
2384
+ execute: async (args, _ctx) => {
2385
+ const client = getGitLabClient();
2386
+ const diffs = await client.listMergeRequestDiffs(args.project_id, args.mr_iid, {
2387
+ page: args.page,
2388
+ per_page: args.per_page
2389
+ });
2390
+ return JSON.stringify(diffs, null, 2);
2391
+ }
2392
+ }),
2393
+ gitlab_add_mr_reviewer: tool({
2394
+ description: `Add a reviewer to a merge request without affecting existing reviewers.
2395
+ This is safer than gitlab_update_merge_request when you only want to add a single reviewer,
2396
+ as it preserves all existing reviewers.`,
2397
+ args: {
2398
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2399
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2400
+ reviewer_id: z.number().describe("The user ID of the reviewer to add")
2401
+ },
2402
+ execute: async (args, _ctx) => {
2403
+ const client = getGitLabClient();
2404
+ const mr = await client.addReviewer(args.project_id, args.mr_iid, args.reviewer_id);
2405
+ return JSON.stringify(mr, null, 2);
2406
+ }
2407
+ }),
2408
+ gitlab_remove_mr_reviewer: tool({
2409
+ description: `Remove a reviewer from a merge request without affecting other reviewers.
2410
+ This is safer than gitlab_update_merge_request when you only want to remove a single reviewer,
2411
+ as it preserves all other reviewers.`,
2412
+ args: {
2413
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2414
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2415
+ reviewer_id: z.number().describe("The user ID of the reviewer to remove")
2416
+ },
2417
+ execute: async (args, _ctx) => {
2418
+ const client = getGitLabClient();
2419
+ const mr = await client.removeReviewer(args.project_id, args.mr_iid, args.reviewer_id);
2420
+ return JSON.stringify(mr, null, 2);
2421
+ }
2422
+ }),
2423
+ gitlab_set_mr_auto_merge: tool({
2424
+ description: `Enable auto-merge (MWPS - Merge When Pipeline Succeeds) on a merge request.
2425
+ Uses the GitLab GraphQL API mergeRequestAccept mutation with a merge strategy.
2426
+ The MR will be automatically merged when all required conditions are met.
2427
+ Requires the MR to be approved and have a passing pipeline (depending on project settings).
2428
+ Note: You must provide the current HEAD SHA of the MR to prevent race conditions.`,
2429
+ args: {
2430
+ project_id: z.string().describe('The project ID or path (e.g., "gitlab-org/gitlab")'),
2431
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2432
+ sha: z.string().describe(
2433
+ "The HEAD SHA of the merge request. Get this from the MR details (diff_refs.head_sha or sha field)."
2434
+ ),
2435
+ strategy: z.enum(["MERGE_WHEN_CHECKS_PASS", "ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS"]).optional().describe(
2436
+ "Auto-merge strategy. MERGE_WHEN_CHECKS_PASS (default) waits for all checks including approvals. ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS adds to merge train when checks pass."
2437
+ )
2438
+ },
2439
+ execute: async (args, _ctx) => {
2440
+ const client = getGitLabClient();
2441
+ const result = await client.setAutoMerge(
2442
+ args.project_id,
2443
+ args.mr_iid,
2444
+ args.sha,
2445
+ args.strategy || "MERGE_WHEN_CHECKS_PASS"
2446
+ );
2447
+ return JSON.stringify(result, null, 2);
2448
+ }
2449
+ }),
2450
+ gitlab_approve_merge_request: tool({
2451
+ description: `Approve a merge request.
2452
+ Adds the current user's approval to the merge request.
2453
+ Requires at least Developer role on the project.
2454
+ Returns the updated approval state including approved_by list.`,
2455
+ args: {
2456
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2457
+ mr_iid: z.number().describe("The internal ID of the merge request"),
2458
+ sha: z.string().optional().describe(
2459
+ "The HEAD SHA of the MR. If provided, the approval will only succeed if this matches the current HEAD, preventing approval of outdated code."
2460
+ )
2461
+ },
2462
+ execute: async (args, _ctx) => {
2463
+ const client = getGitLabClient();
2464
+ const result = await client.approveMergeRequest(args.project_id, args.mr_iid, args.sha);
2465
+ return JSON.stringify(result, null, 2);
2466
+ }
2467
+ }),
2468
+ gitlab_unapprove_merge_request: tool({
2469
+ description: `Unapprove (revoke approval from) a merge request.
2470
+ Removes the current user's approval from the merge request.
2471
+ Only the user who approved can unapprove; you cannot remove others' approvals.
2472
+ Returns the updated approval state.`,
2473
+ args: {
2474
+ project_id: z.string().describe("The project ID or URL-encoded path"),
2475
+ mr_iid: z.number().describe("The internal ID of the merge request")
2476
+ },
2477
+ execute: async (args, _ctx) => {
2478
+ const client = getGitLabClient();
2479
+ const result = await client.unapproveMergeRequest(args.project_id, args.mr_iid);
2480
+ return JSON.stringify(result, null, 2);
2481
+ }
2482
+ })
2483
+ };
2484
+
2485
+ // src/tools/issues.ts
2486
+ import { tool as tool2 } from "@opencode-ai/plugin";
2487
+ var z2 = tool2.schema;
2488
+ var issueTools = {
2489
+ gitlab_create_issue: tool2({
2490
+ description: `Create a new issue in a GitLab project.
2491
+ Returns the created issue with all details including IID, web URL, and metadata.`,
2492
+ args: {
2493
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
2494
+ title: z2.string().describe("The title of the issue"),
2495
+ description: z2.string().optional().describe("The description of the issue (supports Markdown)"),
2496
+ assignee_ids: z2.array(z2.number()).optional().describe("Array of user IDs to assign the issue to"),
2497
+ milestone_id: z2.number().optional().describe("The ID of the milestone to assign the issue to"),
2498
+ labels: z2.string().optional().describe('Comma-separated list of label names (e.g., "bug,critical")'),
2499
+ due_date: z2.string().optional().describe('Due date for the issue in YYYY-MM-DD format (e.g., "2025-12-31")'),
2500
+ confidential: z2.boolean().optional().describe("Whether the issue should be confidential"),
2501
+ weight: z2.number().optional().describe("The weight of the issue (for issue boards)"),
2502
+ epic_id: z2.number().optional().describe("The ID of the epic to add the issue to (Premium/Ultimate)"),
2503
+ issue_type: z2.enum(["issue", "incident", "test_case", "task"]).optional().describe("The type of issue (default: issue)")
2504
+ },
2505
+ execute: async (args, _ctx) => {
2506
+ const client = getGitLabClient();
2507
+ const issue = await client.createIssue(args.project_id, args.title, {
2508
+ description: args.description,
2509
+ assignee_ids: args.assignee_ids,
2510
+ milestone_id: args.milestone_id,
2511
+ labels: args.labels,
2512
+ due_date: args.due_date,
2513
+ confidential: args.confidential,
2514
+ weight: args.weight,
2515
+ epic_id: args.epic_id,
2516
+ issue_type: args.issue_type
2517
+ });
2518
+ return JSON.stringify(issue, null, 2);
2519
+ }
2520
+ }),
2521
+ gitlab_get_issue: tool2({
2522
+ description: `Get details of a specific issue by project and issue IID.
2523
+ Returns: title, description, state, author, assignees, labels, milestone, weight, and comments.`,
2524
+ args: {
2525
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
2526
+ issue_iid: z2.number().describe("The internal ID of the issue within the project")
2527
+ },
2528
+ execute: async (args, _ctx) => {
2529
+ const client = getGitLabClient();
2530
+ const issue = await client.getIssue(args.project_id, args.issue_iid);
2531
+ return JSON.stringify(issue, null, 2);
2532
+ }
2533
+ }),
2534
+ gitlab_list_issues: tool2({
2535
+ description: `List issues for a project or search globally.
2536
+ Can filter by state, labels, assignee, milestone.`,
2537
+ args: {
2538
+ project_id: z2.string().optional().describe("The project ID or path. If not provided, searches globally."),
2539
+ state: z2.enum(["opened", "closed", "all"]).optional().describe("Filter by issue state (default: opened)"),
2540
+ scope: z2.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
2541
+ search: z2.string().optional().describe("Search issues by title or description"),
2542
+ labels: z2.string().optional().describe("Comma-separated list of labels to filter by"),
2543
+ milestone: z2.string().optional().describe("Filter by milestone title"),
2544
+ limit: z2.number().optional().describe("Maximum number of results (default: 20)")
2545
+ },
2546
+ execute: async (args, _ctx) => {
2547
+ const client = getGitLabClient();
2548
+ const issues = await client.listIssues({
2549
+ projectId: args.project_id,
2550
+ state: args.state,
2551
+ scope: args.scope,
2552
+ search: args.search,
2553
+ labels: args.labels,
2554
+ milestone: args.milestone,
2555
+ limit: args.limit
2556
+ });
2557
+ return JSON.stringify(issues, null, 2);
2558
+ }
2559
+ })
2560
+ };
2561
+
2562
+ // src/tools/epics.ts
2563
+ import { tool as tool3 } from "@opencode-ai/plugin";
2564
+ var z3 = tool3.schema;
2565
+ function validationError(message) {
2566
+ return JSON.stringify({ error: message }, null, 2);
2567
+ }
2568
+ var epicTools = {
2569
+ gitlab_get_epic: tool3({
2570
+ description: `Get details of a specific epic by group and epic IID.
2571
+ Returns: title, description, state, author, start/end dates, labels, associated issues, and child epics.`,
2572
+ args: {
2573
+ group_id: z3.string().describe(
2574
+ 'The group ID or URL-encoded path (e.g., "gitlab-org" or "my-group/my-subgroup")'
2575
+ ),
2576
+ epic_iid: z3.number().describe("The internal ID of the epic within the group")
2577
+ },
2578
+ execute: async (args, _ctx) => {
2579
+ const client = getGitLabClient();
2580
+ const epic = await client.getEpic(args.group_id, args.epic_iid);
2581
+ return JSON.stringify(epic, null, 2);
2582
+ }
2583
+ }),
2584
+ gitlab_list_epics: tool3({
2585
+ description: `List epics for a group with filtering capabilities.
2586
+ Can filter by state (opened, closed, all), author, labels, and search query.`,
2587
+ args: {
2588
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2589
+ state: z3.enum(["opened", "closed", "all"]).optional().describe("Filter by epic state (default: opened)"),
2590
+ author_id: z3.number().optional().describe("Filter by author user ID"),
2591
+ labels: z3.string().optional().describe("Comma-separated list of labels to filter by"),
2592
+ search: z3.string().optional().describe("Search epics by title or description"),
2593
+ limit: z3.number().optional().describe("Maximum number of results (default: 20)")
2594
+ },
2595
+ execute: async (args, _ctx) => {
2596
+ const client = getGitLabClient();
2597
+ const epics = await client.listEpics({
2598
+ groupId: args.group_id,
2599
+ state: args.state,
2600
+ author_id: args.author_id,
2601
+ labels: args.labels,
2602
+ search: args.search,
2603
+ limit: args.limit
2604
+ });
2605
+ return JSON.stringify(epics, null, 2);
2606
+ }
2607
+ }),
2608
+ gitlab_create_epic: tool3({
2609
+ description: `Create a new epic in a group.
2610
+ Returns the created epic with all details.`,
2611
+ args: {
2612
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2613
+ title: z3.string().describe("The title of the epic"),
2614
+ description: z3.string().optional().describe("The description of the epic (supports Markdown)"),
2615
+ labels: z3.string().optional().describe("Comma-separated list of labels"),
2616
+ start_date: z3.string().optional().describe("Start date in YYYY-MM-DD format"),
2617
+ end_date: z3.string().optional().describe("Due/end date in YYYY-MM-DD format"),
2618
+ confidential: z3.boolean().optional().describe("Whether the epic is confidential (default: false)")
2619
+ },
2620
+ execute: async (args, _ctx) => {
2621
+ const client = getGitLabClient();
2622
+ const epic = await client.createEpic(args.group_id, {
2623
+ title: args.title,
2624
+ description: args.description,
2625
+ labels: args.labels,
2626
+ start_date: args.start_date,
2627
+ end_date: args.end_date,
2628
+ confidential: args.confidential
2629
+ });
2630
+ return JSON.stringify(epic, null, 2);
2631
+ }
2632
+ }),
2633
+ gitlab_update_epic: tool3({
2634
+ description: `Update an existing epic.
2635
+ Can update title, description, labels, dates, state, and confidentiality.`,
2636
+ args: {
2637
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2638
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2639
+ title: z3.string().optional().describe("The new title"),
2640
+ description: z3.string().optional().describe("The new description (supports Markdown)"),
2641
+ labels: z3.string().optional().describe("Comma-separated list of labels"),
2642
+ start_date: z3.string().optional().describe("Start date in YYYY-MM-DD format"),
2643
+ end_date: z3.string().optional().describe("Due/end date in YYYY-MM-DD format"),
2644
+ state_event: z3.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
2645
+ confidential: z3.boolean().optional().describe("Whether the epic is confidential")
2646
+ },
2647
+ execute: async (args, _ctx) => {
2648
+ const client = getGitLabClient();
2649
+ const epic = await client.updateEpic(args.group_id, args.epic_iid, {
2650
+ title: args.title,
2651
+ description: args.description,
2652
+ labels: args.labels,
2653
+ start_date: args.start_date,
2654
+ end_date: args.end_date,
2655
+ state_event: args.state_event,
2656
+ confidential: args.confidential
2657
+ });
2658
+ return JSON.stringify(epic, null, 2);
2659
+ }
2660
+ }),
2661
+ gitlab_manage_epic_issues: tool3({
2662
+ description: `Manage issues linked to a GitLab epic.
2663
+
2664
+ Actions:
2665
+ - list: Get all issues associated with the epic
2666
+ - add: Link an existing issue to the epic (issue can be from any project within the group hierarchy)
2667
+ - remove: Unlink an issue from the epic
2668
+
2669
+ Examples:
2670
+ - List issues: action="list", group_id="gitlab-org", epic_iid=123
2671
+ - Add issue: action="add", group_id="gitlab-org", epic_iid=123, issue_id=456
2672
+ - Remove issue: action="remove", group_id="gitlab-org", epic_iid=123, epic_issue_id=789
2673
+
2674
+ Note: The epic_issue_id (for remove) is the ID of the epic-issue association, which can be obtained from the list action response.`,
2675
+ args: {
2676
+ action: z3.enum(["list", "add", "remove"]).describe("The action to perform"),
2677
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2678
+ epic_iid: z3.number().describe("The internal ID of the epic within the group"),
2679
+ issue_id: z3.number().optional().describe('The global ID of the issue to add (required for "add" action)'),
2680
+ epic_issue_id: z3.number().optional().describe(
2681
+ 'The ID of the epic-issue association to remove (required for "remove" action, from list response)'
2682
+ )
2683
+ },
2684
+ execute: async (args, _ctx) => {
2685
+ const client = getGitLabClient();
2686
+ switch (args.action) {
2687
+ case "list": {
2688
+ const issues = await client.listEpicIssues(args.group_id, args.epic_iid);
2689
+ return JSON.stringify(issues, null, 2);
2690
+ }
2691
+ case "add": {
2692
+ if (args.issue_id === void 0) {
2693
+ return validationError('issue_id is required for "add" action');
2694
+ }
2695
+ const result = await client.addIssueToEpic(args.group_id, args.epic_iid, args.issue_id);
2696
+ return JSON.stringify(result, null, 2);
2697
+ }
2698
+ case "remove": {
2699
+ if (args.epic_issue_id === void 0) {
2700
+ return validationError('epic_issue_id is required for "remove" action');
2701
+ }
2702
+ const result = await client.removeIssueFromEpic(
2703
+ args.group_id,
2704
+ args.epic_iid,
2705
+ args.epic_issue_id
2706
+ );
2707
+ return JSON.stringify(result, null, 2);
2708
+ }
2709
+ }
2710
+ }
2711
+ })
2712
+ };
2713
+
2714
+ // src/tools/pipelines.ts
2715
+ import { tool as tool4 } from "@opencode-ai/plugin";
2716
+ var z4 = tool4.schema;
2717
+ var pipelineTools = {
2718
+ gitlab_list_pipelines: tool4({
2719
+ description: `List pipelines for a project.
2720
+ Can filter by status, ref (branch/tag), username.`,
2721
+ args: {
2722
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2723
+ status: z4.enum(["running", "pending", "success", "failed", "canceled", "skipped", "manual"]).optional().describe("Filter by pipeline status"),
2724
+ ref: z4.string().optional().describe("Filter by branch or tag name"),
2725
+ limit: z4.number().optional().describe("Maximum number of results (default: 20)")
2726
+ },
2727
+ execute: async (args, _ctx) => {
2728
+ const client = getGitLabClient();
2729
+ const pipelines = await client.listPipelines(args.project_id, {
2730
+ status: args.status,
2731
+ ref: args.ref,
2732
+ limit: args.limit
2733
+ });
2734
+ return JSON.stringify(pipelines, null, 2);
2735
+ }
2736
+ }),
2737
+ gitlab_get_pipeline: tool4({
2738
+ description: `Get details of a specific pipeline including its jobs.`,
2739
+ args: {
2740
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2741
+ pipeline_id: z4.number().describe("The ID of the pipeline")
2742
+ },
2743
+ execute: async (args, _ctx) => {
2744
+ const client = getGitLabClient();
2745
+ const pipeline = await client.getPipeline(args.project_id, args.pipeline_id);
2746
+ return JSON.stringify(pipeline, null, 2);
2747
+ }
2748
+ }),
2749
+ gitlab_list_pipeline_jobs: tool4({
2750
+ description: `List jobs for a pipeline, optionally filter by scope (failed, success, etc).`,
2751
+ args: {
2752
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2753
+ pipeline_id: z4.number().describe("The ID of the pipeline"),
2754
+ scope: z4.enum([
2755
+ "created",
2756
+ "pending",
2757
+ "running",
2758
+ "failed",
2759
+ "success",
2760
+ "canceled",
2761
+ "skipped",
2762
+ "manual"
2763
+ ]).optional().describe("Filter jobs by scope/status")
2764
+ },
2765
+ execute: async (args, _ctx) => {
2766
+ const client = getGitLabClient();
2767
+ const jobs = await client.listPipelineJobs(args.project_id, args.pipeline_id, args.scope);
2768
+ return JSON.stringify(jobs, null, 2);
2769
+ }
2770
+ }),
2771
+ gitlab_get_job_log: tool4({
2772
+ description: `Get the log/trace output of a specific CI job.`,
2773
+ args: {
2774
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2775
+ job_id: z4.number().describe("The ID of the job")
2776
+ },
2777
+ execute: async (args, _ctx) => {
2778
+ const client = getGitLabClient();
2779
+ return client.getJobLog(args.project_id, args.job_id);
2780
+ }
2781
+ }),
2782
+ gitlab_retry_job: tool4({
2783
+ description: `Retry a failed or canceled CI job.`,
2784
+ args: {
2785
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2786
+ job_id: z4.number().describe("The ID of the job to retry")
2787
+ },
2788
+ execute: async (args, _ctx) => {
2789
+ const client = getGitLabClient();
2790
+ const job = await client.retryJob(args.project_id, args.job_id);
2791
+ return JSON.stringify(job, null, 2);
2792
+ }
2793
+ }),
2794
+ gitlab_get_pipeline_failing_jobs: tool4({
2795
+ description: `Get all failed jobs in a pipeline.
2796
+ Returns only the jobs that have failed, making it easier to debug pipeline failures.`,
2797
+ args: {
2798
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2799
+ pipeline_id: z4.number().describe("The ID of the pipeline")
2800
+ },
2801
+ execute: async (args, _ctx) => {
2802
+ const client = getGitLabClient();
2803
+ const jobs = await client.getPipelineFailingJobs(args.project_id, args.pipeline_id);
2804
+ return JSON.stringify(jobs, null, 2);
2805
+ }
2806
+ }),
2807
+ gitlab_lint_ci_config: tool4({
2808
+ description: `Validate a CI/CD YAML configuration against GitLab CI syntax rules.
2809
+ This validates the configuration in the context of the project, including:
2810
+ - Using the project's CI/CD variables
2811
+ - Searching the project's files for include:local entries
2812
+ - Optionally simulating pipeline creation (dry_run)
2813
+ - Optionally including the list of jobs that would be created
2814
+
2815
+ Modes:
2816
+ - content: Validate provided YAML content (requires 'content' parameter)
2817
+ - existing: Validate the existing .gitlab-ci.yml from the repository
2818
+
2819
+ Returns validation result with:
2820
+ - valid: boolean indicating if configuration is valid
2821
+ - errors: array of error messages
2822
+ - warnings: array of warning messages
2823
+ - merged_yaml: the final merged YAML after processing includes (optional)
2824
+ - includes: list of included files (for existing mode)
2825
+ - jobs: list of jobs that would be created (optional, requires include_jobs=true)
2826
+
2827
+ Examples:
2828
+ - Validate content: mode="content", project_id="group/project", content="stages: [build]..."
2829
+ - Validate existing: mode="existing", project_id="group/project"`,
2830
+ args: {
2831
+ mode: z4.enum(["content", "existing"]).describe('Validation mode: "content" for provided YAML, "existing" for repo file'),
2832
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2833
+ content: z4.string().optional().describe('The CI/CD configuration content (YAML as string) - required for mode="content"'),
2834
+ dry_run: z4.boolean().optional().describe("Run pipeline creation simulation instead of just static check (default: false)"),
2835
+ include_jobs: z4.boolean().optional().describe("Include list of jobs that would be created in the response (default: false)"),
2836
+ ref: z4.string().optional().describe(
2837
+ "Branch or tag context to use for validation (defaults to project default branch)"
2838
+ ),
2839
+ content_ref: z4.string().optional().describe(
2840
+ 'For mode="existing": Commit SHA, branch or tag to get CI config from (defaults to project default branch)'
2841
+ )
2842
+ },
2843
+ execute: async (args, _ctx) => {
2844
+ const client = getGitLabClient();
2845
+ switch (args.mode) {
2846
+ case "content": {
2847
+ if (!args.content) {
2848
+ return JSON.stringify({ error: 'content is required when mode is "content"' }, null, 2);
2849
+ }
2850
+ const result = await client.lintCiConfig(args.project_id, args.content, {
2851
+ dry_run: args.dry_run,
2852
+ include_jobs: args.include_jobs,
2853
+ ref: args.ref
2854
+ });
2855
+ return JSON.stringify(result, null, 2);
2856
+ }
2857
+ case "existing": {
2858
+ const result = await client.lintExistingCiConfig(args.project_id, {
2859
+ content_ref: args.content_ref,
2860
+ dry_run: args.dry_run,
2861
+ dry_run_ref: args.ref,
2862
+ include_jobs: args.include_jobs
2863
+ });
2864
+ return JSON.stringify(result, null, 2);
2865
+ }
2866
+ }
2867
+ }
2868
+ })
2869
+ };
2870
+
2871
+ // src/tools/repository.ts
2872
+ import { tool as tool5 } from "@opencode-ai/plugin";
2873
+ var z5 = tool5.schema;
2874
+ var repositoryTools = {
2875
+ gitlab_get_file: tool5({
2876
+ description: `Get the contents of a file from a repository.
2877
+ Supports fetching files from any branch, tag, or commit SHA.
2878
+ If ref is not specified, uses the project's default branch.
2879
+ Note: Invalid refs will result in a 404 error from the GitLab API.`,
2880
+ args: {
2881
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2882
+ file_path: z5.string().describe("Path to the file in the repository"),
2883
+ ref: z5.string().optional().describe(
2884
+ `Branch name, tag, or commit SHA to fetch the file from. Supports full or short commit SHAs. If omitted, uses the project's default branch (e.g., "main" or "master").`
2885
+ )
2886
+ },
2887
+ execute: async (args, _ctx) => {
2888
+ const client = getGitLabClient();
2889
+ return client.getFile(args.project_id, args.file_path, args.ref);
2890
+ }
2891
+ }),
2892
+ gitlab_get_commit: tool5({
2893
+ description: `Get a single commit with full details.
2894
+ Returns commit metadata including author, message, stats, and parent commits.`,
2895
+ args: {
2896
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2897
+ sha: z5.string().describe("The commit SHA")
2898
+ },
2899
+ execute: async (args, _ctx) => {
2900
+ const client = getGitLabClient();
2901
+ const commit = await client.getCommit(args.project_id, args.sha);
2902
+ return JSON.stringify(commit, null, 2);
2903
+ }
2904
+ }),
2905
+ gitlab_list_commits: tool5({
2906
+ description: `List commits in a repository. Can filter by branch/ref and path.`,
2907
+ args: {
2908
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2909
+ ref: z5.string().optional().describe("Branch or tag name"),
2910
+ path: z5.string().optional().describe("File or directory path to filter commits"),
2911
+ since: z5.string().optional().describe("Only commits after this date (ISO 8601 format)"),
2912
+ until: z5.string().optional().describe("Only commits before this date (ISO 8601 format)"),
2913
+ limit: z5.number().optional().describe("Maximum number of results (default: 20)")
2914
+ },
2915
+ execute: async (args, _ctx) => {
2916
+ const client = getGitLabClient();
2917
+ const commits = await client.listCommits(args.project_id, {
2918
+ ref: args.ref,
2919
+ path: args.path,
2920
+ since: args.since,
2921
+ until: args.until,
2922
+ limit: args.limit
2923
+ });
2924
+ return JSON.stringify(commits, null, 2);
2925
+ }
2926
+ }),
2927
+ gitlab_get_commit_diff: tool5({
2928
+ description: `Get the diff for a specific commit.`,
2929
+ args: {
2930
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2931
+ sha: z5.string().describe("The commit SHA")
2932
+ },
2933
+ execute: async (args, _ctx) => {
2934
+ const client = getGitLabClient();
2935
+ const diff = await client.getCommitDiff(args.project_id, args.sha);
2936
+ return JSON.stringify(diff, null, 2);
2937
+ }
2938
+ }),
2939
+ gitlab_list_repository_tree: tool5({
2940
+ description: `List files and directories in a repository.
2941
+ Returns the tree structure of the repository at a given path and ref.`,
2942
+ args: {
2943
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2944
+ path: z5.string().optional().describe("Path inside repository (default: root)"),
2945
+ ref: z5.string().optional().describe("Branch, tag, or commit SHA (default: default branch)"),
2946
+ recursive: z5.boolean().optional().describe("Get recursive tree (default: false)"),
2947
+ per_page: z5.number().optional().describe("Number of results per page (default: 20)")
2948
+ },
2949
+ execute: async (args, _ctx) => {
2950
+ const client = getGitLabClient();
2951
+ const tree = await client.listRepositoryTree(args.project_id, {
2952
+ path: args.path,
2953
+ ref: args.ref,
2954
+ recursive: args.recursive,
2955
+ per_page: args.per_page
2956
+ });
2957
+ return JSON.stringify(tree, null, 2);
2958
+ }
2959
+ }),
2960
+ gitlab_list_branches: tool5({
2961
+ description: `List branches in a repository.`,
2962
+ args: {
2963
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2964
+ search: z5.string().optional().describe("Search branches by name")
2965
+ },
2966
+ execute: async (args, _ctx) => {
2967
+ const client = getGitLabClient();
2968
+ const branches = await client.listBranches(args.project_id, args.search);
2969
+ return JSON.stringify(branches, null, 2);
2970
+ }
2971
+ }),
2972
+ gitlab_get_commit_comments: tool5({
2973
+ description: `Get all comments on a specific commit.
2974
+ Returns all comments (notes) that have been added to a commit, including line-specific comments.
2975
+ This is different from discussions - it returns individual comments in a flat structure.`,
2976
+ args: {
2977
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2978
+ sha: z5.string().describe("The commit SHA")
2979
+ },
2980
+ execute: async (args, _ctx) => {
2981
+ const client = getGitLabClient();
2982
+ const comments = await client.getCommitComments(args.project_id, args.sha);
2983
+ return JSON.stringify(comments, null, 2);
2984
+ }
2985
+ })
2986
+ };
2987
+
2988
+ // src/tools/search.ts
2989
+ import { tool as tool6 } from "@opencode-ai/plugin";
2990
+ var z6 = tool6.schema;
2991
+ var GENERIC_SEARCH_SCOPES = ["projects", "issues", "merge_requests", "blobs"];
2992
+ var SPECIALIZED_SCOPES = [
2993
+ "milestones",
2994
+ "users",
2995
+ "commits",
2996
+ "notes",
2997
+ "wiki_blobs",
2998
+ "group_projects"
2999
+ ];
3000
+ var ALL_SCOPES = [...GENERIC_SEARCH_SCOPES, ...SPECIALIZED_SCOPES];
3001
+ var REF_SUPPORTED_SCOPES = ["commits", "wiki_blobs"];
3002
+ var STATE_SUPPORTED_SCOPES = ["milestones"];
3003
+ function validationError2(param, scope) {
3004
+ return new Error(`Missing required parameter: '${param}' is required for scope '${scope}'`);
3005
+ }
3006
+ function invalidParamError(param, validScopes) {
3007
+ return new Error(
3008
+ `Invalid parameter: '${param}' is only valid for scopes: ${validScopes.join(", ")}`
3009
+ );
3010
+ }
3011
+ function validateSearchParams(scope, args) {
3012
+ switch (scope) {
3013
+ case "notes":
3014
+ if (!args.project_id) throw validationError2("project_id", scope);
3015
+ break;
3016
+ case "group_projects":
3017
+ if (!args.group_id) throw validationError2("group_id", scope);
3018
+ break;
3019
+ }
3020
+ if (args.ref && !REF_SUPPORTED_SCOPES.includes(scope)) {
3021
+ throw invalidParamError("ref", REF_SUPPORTED_SCOPES);
3022
+ }
3023
+ if (args.state && !STATE_SUPPORTED_SCOPES.includes(scope)) {
3024
+ throw invalidParamError("state", STATE_SUPPORTED_SCOPES);
3025
+ }
3026
+ }
3027
+ var searchTools = {
3028
+ /**
3029
+ * Unified search across all GitLab resources
3030
+ *
3031
+ * @example
3032
+ * // Search for issues in a project
3033
+ * gitlab_search({ scope: "issues", search: "bug", project_id: "my-group/project" })
3034
+ *
3035
+ * @example
3036
+ * // Search commits on specific branch
3037
+ * gitlab_search({ scope: "commits", search: "fix", ref: "main" })
3038
+ *
3039
+ * @example
3040
+ * // Search for projects within a group
3041
+ * gitlab_search({ scope: "group_projects", group_id: "gitlab-org", search: "runner" })
3042
+ *
3043
+ * @example
3044
+ * // Search notes/comments in a project
3045
+ * gitlab_search({ scope: "notes", search: "LGTM", project_id: "my-group/project" })
3046
+ */
3047
+ gitlab_search: tool6({
3048
+ description: `Search across GitLab for various resources with scope-specific options.
3049
+
3050
+ Scopes and their requirements:
3051
+ - projects: Search projects by name/description
3052
+ - issues: Search issues by title/description
3053
+ - merge_requests: Search MRs by title/description
3054
+ - milestones: Search milestones (supports state filter)
3055
+ - users: Search users by name/email
3056
+ - blobs: Search file content (code search)
3057
+ - commits: Search commits by message/author/SHA (supports ref filter)
3058
+ - notes: Search comments/notes (requires project_id)
3059
+ - wiki_blobs: Search wiki content (supports ref filter)
3060
+ - group_projects: Search projects within a group (requires group_id)
3061
+
3062
+ Examples:
3063
+ - Issues: scope="issues", search="bug", project_id="my-group/my-project"
3064
+ - Code: scope="blobs", search="function calculateTotal"
3065
+ - Commits: scope="commits", search="fix login", ref="main"
3066
+ - Group projects: scope="group_projects", group_id="gitlab-org", search="runner"
3067
+ - Notes: scope="notes", search="LGTM", project_id="my-group/my-project"`,
3068
+ args: {
3069
+ scope: z6.enum([
3070
+ "projects",
3071
+ "issues",
3072
+ "merge_requests",
3073
+ "milestones",
3074
+ "users",
3075
+ "blobs",
3076
+ "commits",
3077
+ "notes",
3078
+ "wiki_blobs",
3079
+ "group_projects"
3080
+ ]).describe("The type of resource to search"),
3081
+ search: z6.string().describe("The search query"),
3082
+ // Context parameters
3083
+ project_id: z6.string().optional().describe("Limit search to a specific project. Required for notes scope"),
3084
+ group_id: z6.string().optional().describe("Group ID or path. Required for group_projects scope"),
3085
+ // Common options
3086
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)"),
3087
+ order_by: z6.enum(["created_at"]).optional().describe(
3088
+ "Order results by field (for milestones, users, commits, notes, wiki_blobs, group_projects)"
3089
+ ),
3090
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (for milestones, users, commits, notes, wiki_blobs, group_projects)"),
3091
+ // Scope-specific options
3092
+ ref: z6.string().optional().describe("Branch or tag name (for commits, wiki_blobs scopes)"),
3093
+ state: z6.enum(["active", "closed", "all"]).optional().describe("Filter by state (for milestones scope)")
3094
+ },
3095
+ execute: async (args, _ctx) => {
3096
+ validateSearchParams(args.scope, {
3097
+ project_id: args.project_id,
3098
+ group_id: args.group_id,
3099
+ ref: args.ref,
3100
+ state: args.state
3101
+ });
3102
+ const client = getGitLabClient();
3103
+ const commonOptions = {
3104
+ order_by: args.order_by,
3105
+ sort: args.sort,
3106
+ limit: args.limit
3107
+ };
3108
+ switch (args.scope) {
3109
+ // Generic search API scopes
3110
+ case "projects":
3111
+ case "issues":
3112
+ case "merge_requests":
3113
+ case "blobs":
3114
+ return JSON.stringify(
3115
+ await client.search(args.scope, args.search, args.project_id, args.limit),
3116
+ null,
3117
+ 2
3118
+ );
3119
+ // Specialized scopes with dedicated client methods
3120
+ case "milestones":
3121
+ return JSON.stringify(
3122
+ await client.searchMilestones(args.search, args.project_id, {
3123
+ ...commonOptions,
3124
+ state: args.state
3125
+ }),
3126
+ null,
3127
+ 2
3128
+ );
3129
+ case "users":
3130
+ return JSON.stringify(
3131
+ await client.searchUsers(args.search, args.project_id, commonOptions),
3132
+ null,
3133
+ 2
3134
+ );
3135
+ case "commits":
3136
+ return JSON.stringify(
3137
+ await client.searchCommits(args.search, args.project_id, {
3138
+ ...commonOptions,
3139
+ ref: args.ref
3140
+ }),
3141
+ null,
3142
+ 2
3143
+ );
3144
+ case "notes":
3145
+ return JSON.stringify(
3146
+ await client.searchNotes(args.search, args.project_id, commonOptions),
3147
+ null,
3148
+ 2
3149
+ );
3150
+ case "wiki_blobs":
3151
+ return JSON.stringify(
3152
+ await client.searchWikiBlobs(args.search, args.project_id, {
3153
+ ...commonOptions,
3154
+ ref: args.ref
3155
+ }),
3156
+ null,
3157
+ 2
3158
+ );
3159
+ case "group_projects":
3160
+ return JSON.stringify(
3161
+ await client.searchGroupProjects(args.group_id, args.search, commonOptions),
3162
+ null,
3163
+ 2
3164
+ );
3165
+ default:
3166
+ throw new Error(
3167
+ `Invalid scope '${args.scope}'. Must be one of: ${ALL_SCOPES.join(", ")}`
3168
+ );
3169
+ }
3170
+ }
3171
+ }),
3172
+ /**
3173
+ * Search GitLab official documentation
3174
+ * Separate tool because it uses a completely different API (docs.gitlab.com)
3175
+ */
3176
+ gitlab_documentation_search: tool6({
3177
+ description: `Search GitLab official documentation at docs.gitlab.com.
3178
+ Returns relevant documentation pages matching the search query.
3179
+
3180
+ Useful for:
3181
+ - Finding GitLab feature documentation
3182
+ - Learning about GitLab APIs and integrations
3183
+ - Discovering best practices and guides
3184
+ - Troubleshooting GitLab issues
3185
+
3186
+ Note: This searches the public GitLab documentation site, not your instance's documentation.`,
3187
+ args: {
3188
+ search: z6.string().describe("The search query (documentation topic or keyword)"),
3189
+ limit: z6.number().optional().describe("Maximum number of results (default: 10)")
3190
+ },
3191
+ execute: async (args, _ctx) => {
3192
+ const client = getGitLabClient();
3193
+ const results = await client.searchDocumentation(args.search, args.limit);
3194
+ return JSON.stringify(results, null, 2);
3195
+ }
3196
+ })
3197
+ };
3198
+
3199
+ // src/tools/projects.ts
3200
+ import { tool as tool7 } from "@opencode-ai/plugin";
3201
+ var z7 = tool7.schema;
3202
+ var projectTools = {
3203
+ gitlab_get_project: tool7({
3204
+ description: `Get details of a specific project.`,
3205
+ args: {
3206
+ project_id: z7.string().describe('The project ID or URL-encoded path (e.g., "gitlab-org/gitlab")')
3207
+ },
3208
+ execute: async (args, _ctx) => {
3209
+ const client = getGitLabClient();
3210
+ const project = await client.getProject(args.project_id);
3211
+ return JSON.stringify(project, null, 2);
3212
+ }
3213
+ }),
3214
+ gitlab_list_project_members: tool7({
3215
+ description: `List members of a project.`,
3216
+ args: {
3217
+ project_id: z7.string().describe("The project ID or URL-encoded path")
3218
+ },
3219
+ execute: async (args, _ctx) => {
3220
+ const client = getGitLabClient();
3221
+ const members = await client.listProjectMembers(args.project_id);
3222
+ return JSON.stringify(members, null, 2);
3223
+ }
3224
+ })
3225
+ };
3226
+
3227
+ // src/tools/users.ts
3228
+ import { tool as tool8 } from "@opencode-ai/plugin";
3229
+ var z8 = tool8.schema;
3230
+ var userTools = {
3231
+ gitlab_get_current_user: tool8({
3232
+ description: `Get current user information.
3233
+ Returns details about the authenticated user including username, email, and permissions.`,
3234
+ args: {},
3235
+ execute: async (_args, _ctx) => {
3236
+ const client = getGitLabClient();
3237
+ const user = await client.getCurrentUser();
3238
+ return JSON.stringify(user, null, 2);
3239
+ }
3240
+ }),
3241
+ gitlab_get_user: tool8({
3242
+ description: `Get a user by their ID.
3243
+ Returns user details including username, name, state, and profile information.`,
3244
+ args: {
3245
+ user_id: z8.number().describe("The user ID")
3246
+ },
3247
+ execute: async (args, _ctx) => {
3248
+ const client = getGitLabClient();
3249
+ const user = await client.getUser(args.user_id);
3250
+ return JSON.stringify(user, null, 2);
3251
+ }
3252
+ }),
3253
+ gitlab_get_user_by_username: tool8({
3254
+ description: `Find a user by their username.
3255
+ Returns user details including ID, name, state, and profile information.
3256
+ Useful for looking up a user's ID when you only know their username.`,
3257
+ args: {
3258
+ username: z8.string().describe("The username to look up (without @ prefix)")
3259
+ },
3260
+ execute: async (args, _ctx) => {
3261
+ const client = getGitLabClient();
3262
+ const username = args.username.replace(/^@/, "");
3263
+ const users = await client.getUserByUsername(username);
3264
+ if (users.length === 0) {
3265
+ return JSON.stringify({ error: `No user found with username: ${username}` });
3266
+ }
3267
+ if (users.length > 1) {
3268
+ return JSON.stringify(
3269
+ {
3270
+ warning: `Multiple users found for username: ${username}`,
3271
+ users
3272
+ },
3273
+ null,
3274
+ 2
3275
+ );
3276
+ }
3277
+ return JSON.stringify(users[0], null, 2);
3278
+ }
3279
+ }),
3280
+ gitlab_get_user_status: tool8({
3281
+ description: `Get a user's status including availability.
3282
+ Returns the user's status emoji, message, and availability (e.g., "busy").
3283
+ Useful for checking if someone is available before requesting a review.`,
3284
+ args: {
3285
+ user_id: z8.number().describe("The user ID")
3286
+ },
3287
+ execute: async (args, _ctx) => {
3288
+ const client = getGitLabClient();
3289
+ const status = await client.getUserStatus(args.user_id);
3290
+ return JSON.stringify(status, null, 2);
3291
+ }
3292
+ })
3293
+ };
3294
+
3295
+ // src/tools/security.ts
3296
+ import { tool as tool9 } from "@opencode-ai/plugin";
3297
+ var z9 = tool9.schema;
3298
+ var securityTools = {
3299
+ gitlab_list_vulnerabilities: tool9({
3300
+ description: `List persisted vulnerabilities for a project.
3301
+ Returns security vulnerabilities detected in the project.`,
3302
+ args: {
3303
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3304
+ state: z9.enum(["detected", "confirmed", "dismissed", "resolved"]).optional().describe("Filter by vulnerability state"),
3305
+ severity: z9.enum(["undefined", "info", "unknown", "low", "medium", "high", "critical"]).optional().describe("Filter by severity level"),
3306
+ report_type: z9.enum([
3307
+ "sast",
3308
+ "dast",
3309
+ "dependency_scanning",
3310
+ "container_scanning",
3311
+ "secret_detection",
3312
+ "coverage_fuzzing",
3313
+ "api_fuzzing"
3314
+ ]).optional().describe("Filter by report type"),
3315
+ limit: z9.number().optional().describe("Maximum number of results (default: 20)")
3316
+ },
3317
+ execute: async (args, _ctx) => {
3318
+ const client = getGitLabClient();
3319
+ const vulnerabilities = await client.listVulnerabilities(args.project_id, {
3320
+ state: args.state,
3321
+ severity: args.severity,
3322
+ report_type: args.report_type,
3323
+ limit: args.limit
3324
+ });
3325
+ return JSON.stringify(vulnerabilities, null, 2);
3326
+ }
3327
+ }),
3328
+ gitlab_get_vulnerability_details: tool9({
3329
+ description: `Get details for a specific vulnerability.
3330
+ Returns full information about a security vulnerability including description, location, and remediation.`,
3331
+ args: {
3332
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3333
+ vulnerability_id: z9.number().describe("The ID of the vulnerability")
3334
+ },
3335
+ execute: async (args, _ctx) => {
3336
+ const client = getGitLabClient();
3337
+ const vulnerability = await client.getVulnerabilityDetails(
3338
+ args.project_id,
3339
+ args.vulnerability_id
3340
+ );
3341
+ return JSON.stringify(vulnerability, null, 2);
3342
+ }
3343
+ }),
3344
+ gitlab_create_vulnerability_issue: tool9({
3345
+ description: `Create a new issue linked to one or more security vulnerabilities.
3346
+ This creates an issue in the project and automatically links it to the specified vulnerabilities.
3347
+ Requires Developer role or higher.`,
3348
+ args: {
3349
+ project_path: z9.string().describe('Full path of the project (e.g., "group/project" or "group/subgroup/project")'),
3350
+ vulnerability_ids: z9.array(z9.string()).describe(
3351
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
3352
+ )
3353
+ },
3354
+ execute: async (args, _ctx) => {
3355
+ const client = getGitLabClient();
3356
+ const issue = await client.createVulnerabilityIssue(
3357
+ args.project_path,
3358
+ args.vulnerability_ids
3359
+ );
3360
+ return JSON.stringify(issue, null, 2);
3361
+ }
3362
+ }),
3363
+ gitlab_dismiss_vulnerability: tool9({
3364
+ description: `Dismiss a security vulnerability with a reason.
3365
+ Use this when a vulnerability is not applicable, is a false positive, or has been mitigated.
3366
+ Requires Developer role or higher.`,
3367
+ args: {
3368
+ vulnerability_id: z9.string().describe(
3369
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3370
+ ),
3371
+ reason: z9.enum([
3372
+ "ACCEPTABLE_RISK",
3373
+ "FALSE_POSITIVE",
3374
+ "MITIGATING_CONTROL",
3375
+ "USED_IN_TESTS",
3376
+ "NOT_APPLICABLE"
3377
+ ]).describe("Reason for dismissing the vulnerability"),
3378
+ comment: z9.string().optional().describe("Optional comment explaining the dismissal")
3379
+ },
3380
+ execute: async (args, _ctx) => {
3381
+ const client = getGitLabClient();
3382
+ const vulnerability = await client.dismissVulnerability(
3383
+ args.vulnerability_id,
3384
+ args.reason,
3385
+ args.comment
3386
+ );
3387
+ return JSON.stringify(vulnerability, null, 2);
3388
+ }
3389
+ }),
3390
+ gitlab_confirm_vulnerability: tool9({
3391
+ description: `Confirm a security vulnerability.
3392
+ Use this to acknowledge that a vulnerability is valid and needs attention.
3393
+ Requires Developer role or higher.`,
3394
+ args: {
3395
+ vulnerability_id: z9.string().describe(
3396
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3397
+ ),
3398
+ comment: z9.string().optional().describe("Optional comment about the confirmation")
3399
+ },
3400
+ execute: async (args, _ctx) => {
3401
+ const client = getGitLabClient();
3402
+ const vulnerability = await client.confirmVulnerability(args.vulnerability_id, args.comment);
3403
+ return JSON.stringify(vulnerability, null, 2);
3404
+ }
3405
+ }),
3406
+ gitlab_revert_vulnerability_to_detected: tool9({
3407
+ description: `Revert a vulnerability back to detected state.
3408
+ Use this to undo a previous confirmation or dismissal.
3409
+ Requires Developer role or higher.`,
3410
+ args: {
3411
+ vulnerability_id: z9.string().describe(
3412
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3413
+ ),
3414
+ comment: z9.string().optional().describe("Optional comment about reverting the state")
3415
+ },
3416
+ execute: async (args, _ctx) => {
3417
+ const client = getGitLabClient();
3418
+ const vulnerability = await client.revertVulnerability(args.vulnerability_id, args.comment);
3419
+ return JSON.stringify(vulnerability, null, 2);
3420
+ }
3421
+ }),
3422
+ gitlab_update_vulnerability_severity: tool9({
3423
+ description: `Update the severity level of one or more vulnerabilities.
3424
+ Use this to adjust the severity rating based on your assessment.
3425
+ Requires Developer role or higher.`,
3426
+ args: {
3427
+ vulnerability_ids: z9.array(z9.string()).describe(
3428
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
3429
+ ),
3430
+ severity: z9.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "UNKNOWN"]).describe("New severity level for the vulnerabilities"),
3431
+ comment: z9.string().describe("Comment explaining the severity change (required)")
3432
+ },
3433
+ execute: async (args, _ctx) => {
3434
+ const client = getGitLabClient();
3435
+ const vulnerabilities = await client.updateVulnerabilitySeverity(
3436
+ args.vulnerability_ids,
3437
+ args.severity,
3438
+ args.comment
3439
+ );
3440
+ return JSON.stringify(vulnerabilities, null, 2);
3441
+ }
3442
+ }),
3443
+ gitlab_link_vulnerability_to_issue: tool9({
3444
+ description: `Link an existing issue to one or more vulnerabilities.
3445
+ Use this to associate vulnerabilities with an existing issue for tracking.
3446
+ Requires Developer role or higher.`,
3447
+ args: {
3448
+ issue_id: z9.string().describe(
3449
+ 'Issue ID in format "gid://gitlab/Issue/{id}". You can construct this from the issue IID.'
3450
+ ),
3451
+ vulnerability_ids: z9.array(z9.string()).describe(
3452
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
3453
+ )
3454
+ },
3455
+ execute: async (args, _ctx) => {
3456
+ const client = getGitLabClient();
3457
+ const issue = await client.linkVulnerabilityToIssue(args.issue_id, args.vulnerability_ids);
3458
+ return JSON.stringify(issue, null, 2);
3459
+ }
3460
+ })
3461
+ };
3462
+
3463
+ // src/tools/todos.ts
3464
+ import { tool as tool10 } from "@opencode-ai/plugin";
3465
+ var z10 = tool10.schema;
3466
+ var todoTools = {
3467
+ gitlab_list_todos: tool10({
3468
+ description: `List TODO items for the current user using GraphQL API with pagination support.
3469
+ Returns a list of pending or done TODO items assigned to the authenticated user.
3470
+ TODOs are created when you are assigned to an issue/MR, mentioned in a comment, or when someone requests your review.
3471
+
3472
+ The response includes pagination information (pageInfo) with cursors for fetching additional pages.
3473
+ Use 'after' with the 'endCursor' from pageInfo to get the next page.
3474
+ Use 'before' with the 'startCursor' from pageInfo to get the previous page.`,
3475
+ args: {
3476
+ action: z10.enum([
3477
+ "assigned",
3478
+ "mentioned",
3479
+ "build_failed",
3480
+ "marked",
3481
+ "approval_required",
3482
+ "unmergeable",
3483
+ "directly_addressed",
3484
+ "merge_train_removed",
3485
+ "review_requested"
3486
+ ]).optional().describe("Filter by action type"),
3487
+ author_id: z10.number().optional().describe("Filter by author ID"),
3488
+ project_id: z10.string().optional().describe("Filter by project ID or path"),
3489
+ group_id: z10.string().optional().describe("Filter by group ID"),
3490
+ state: z10.enum(["pending", "done"]).optional().describe("Filter by state (default: pending)"),
3491
+ type: z10.enum(["Issue", "MergeRequest", "DesignManagement::Design", "Alert", "Epic", "Commit"]).optional().describe("Filter by target type"),
3492
+ first: z10.number().optional().describe("Number of items to return from the beginning (default: 20, max: 100)"),
3493
+ after: z10.string().optional().describe("Cursor for forward pagination - use endCursor from previous response"),
3494
+ last: z10.number().optional().describe("Number of items to return from the end (for backward pagination)"),
3495
+ before: z10.string().optional().describe("Cursor for backward pagination - use startCursor from previous response")
3496
+ },
3497
+ execute: async (args, _ctx) => {
3498
+ const client = getGitLabClient();
3499
+ const result = await client.listTodos({
3500
+ action: args.action,
3501
+ author_id: args.author_id,
3502
+ project_id: args.project_id,
3503
+ group_id: args.group_id,
3504
+ state: args.state,
3505
+ type: args.type,
3506
+ first: args.first,
3507
+ after: args.after,
3508
+ last: args.last,
3509
+ before: args.before
3510
+ });
3511
+ return JSON.stringify(result, null, 2);
3512
+ }
3513
+ }),
3514
+ gitlab_mark_todo_done: tool10({
3515
+ description: `Mark TODO items as done.
3516
+
3517
+ Actions:
3518
+ - one: Mark a specific TODO item as done (requires todo_id)
3519
+ - all: Mark all pending TODO items as done
3520
+
3521
+ Examples:
3522
+ - Mark one: action="one", todo_id=123
3523
+ - Mark all: action="all"`,
3524
+ args: {
3525
+ action: z10.enum(["one", "all"]).describe('Action to perform: "one" for single TODO, "all" for all TODOs'),
3526
+ todo_id: z10.number().optional().describe('The ID of the TODO item (required for action="one")')
3527
+ },
3528
+ execute: async (args, _ctx) => {
3529
+ const client = getGitLabClient();
3530
+ switch (args.action) {
3531
+ case "one": {
3532
+ if (args.todo_id === void 0) {
3533
+ return JSON.stringify({ error: 'todo_id is required when action is "one"' }, null, 2);
3534
+ }
3535
+ const result = await client.markTodoAsDone(args.todo_id);
3536
+ if (!result.success) {
3537
+ return JSON.stringify(
3538
+ {
3539
+ success: false,
3540
+ message: result.message
3541
+ },
3542
+ null,
3543
+ 2
3544
+ );
3545
+ }
3546
+ return JSON.stringify(
3547
+ {
3548
+ success: true,
3549
+ todo: result.todo
3550
+ },
3551
+ null,
3552
+ 2
3553
+ );
3554
+ }
3555
+ case "all": {
3556
+ const result = await client.markAllTodosAsDone();
3557
+ return JSON.stringify(result, null, 2);
3558
+ }
3559
+ }
3560
+ }
3561
+ }),
3562
+ gitlab_get_todo_count: tool10({
3563
+ description: `Get the count of pending TODO items.
3564
+ Returns the total number of pending TODOs for the current user.`,
3565
+ args: {},
3566
+ execute: async (_args, _ctx) => {
3567
+ const client = getGitLabClient();
3568
+ const count = await client.getTodoCount();
3569
+ return JSON.stringify(count, null, 2);
3570
+ }
3571
+ })
3572
+ };
3573
+
3574
+ // src/tools/wikis.ts
3575
+ import { tool as tool11 } from "@opencode-ai/plugin";
3576
+ var z11 = tool11.schema;
3577
+ var wikiTools = {
3578
+ gitlab_get_wiki_page: tool11({
3579
+ description: `Get a wiki page with its content.
3580
+ Returns the wiki page content and metadata.`,
3581
+ args: {
3582
+ project_id: z11.string().describe("The project ID or URL-encoded path"),
3583
+ slug: z11.string().describe("The slug (URL-friendly name) of the wiki page")
3584
+ },
3585
+ execute: async (args, _ctx) => {
3586
+ const client = getGitLabClient();
3587
+ const page = await client.getWikiPage(args.project_id, args.slug);
3588
+ return JSON.stringify(page, null, 2);
3589
+ }
3590
+ })
3591
+ };
3592
+
3593
+ // src/tools/work-items.ts
3594
+ import { tool as tool12 } from "@opencode-ai/plugin";
3595
+ var z12 = tool12.schema;
3596
+ var workItemTools = {
3597
+ gitlab_get_work_item: tool12({
3598
+ description: `Get a single work item (issue, epic, task, etc.).
3599
+ Work items are the new unified model for issues, epics, tasks, and other work tracking items in GitLab.`,
3600
+ args: {
3601
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3602
+ work_item_id: z12.number().describe("The ID of the work item")
3603
+ },
3604
+ execute: async (args, _ctx) => {
3605
+ const client = getGitLabClient();
3606
+ const workItem = await client.getWorkItem(args.project_id, args.work_item_id);
3607
+ return JSON.stringify(workItem, null, 2);
3608
+ }
3609
+ }),
3610
+ gitlab_list_work_items: tool12({
3611
+ description: `List work items in a project or group.
3612
+ Work items include issues, epics, tasks, and other work tracking items.`,
3613
+ args: {
3614
+ project_id: z12.string().optional().describe("The project ID or URL-encoded path"),
3615
+ group_id: z12.string().optional().describe("The group ID or URL-encoded path"),
3616
+ state: z12.enum(["opened", "closed", "all"]).optional().describe("Filter by state (default: opened)"),
3617
+ search: z12.string().optional().describe("Search work items by title or description"),
3618
+ labels: z12.string().optional().describe("Comma-separated list of labels to filter by"),
3619
+ work_item_type: z12.string().optional().describe("Filter by work item type (e.g., 'Issue', 'Epic', 'Task')"),
3620
+ limit: z12.number().optional().describe("Maximum number of results (default: 20)")
3621
+ },
3622
+ execute: async (args, _ctx) => {
3623
+ const client = getGitLabClient();
3624
+ const workItems = await client.listWorkItems({
3625
+ projectId: args.project_id,
3626
+ groupId: args.group_id,
3627
+ state: args.state,
3628
+ search: args.search,
3629
+ labels: args.labels,
3630
+ work_item_type: args.work_item_type,
3631
+ limit: args.limit
3632
+ });
3633
+ return JSON.stringify(workItems, null, 2);
3634
+ }
3635
+ }),
3636
+ gitlab_get_work_item_notes: tool12({
3637
+ description: `Get all comments for a work item.
3638
+ Returns all notes/comments on the work item in chronological order.`,
3639
+ args: {
3640
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3641
+ work_item_id: z12.number().describe("The ID of the work item")
3642
+ },
3643
+ execute: async (args, _ctx) => {
3644
+ const client = getGitLabClient();
3645
+ const notes = await client.getWorkItemNotes(args.project_id, args.work_item_id);
3646
+ return JSON.stringify(notes, null, 2);
3647
+ }
3648
+ }),
3649
+ gitlab_create_work_item: tool12({
3650
+ description: `Create a new work item (issue, task, etc.).
3651
+ Work items are the new unified model for issues, epics, tasks, and other work tracking items.`,
3652
+ args: {
3653
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3654
+ title: z12.string().describe("The title of the work item"),
3655
+ work_item_type_id: z12.number().describe("The ID of the work item type (e.g., 1 for Issue, 2 for Task)"),
3656
+ description: z12.string().optional().describe("The description of the work item (supports Markdown)"),
3657
+ labels: z12.array(z12.string()).optional().describe("Array of label names"),
3658
+ assignee_ids: z12.array(z12.number()).optional().describe("Array of user IDs to assign")
3659
+ },
3660
+ execute: async (args, _ctx) => {
3661
+ const client = getGitLabClient();
3662
+ const workItem = await client.createWorkItem(args.project_id, {
3663
+ title: args.title,
3664
+ work_item_type_id: args.work_item_type_id,
3665
+ description: args.description,
3666
+ labels: args.labels,
3667
+ assignee_ids: args.assignee_ids
3668
+ });
3669
+ return JSON.stringify(workItem, null, 2);
3670
+ }
3671
+ }),
3672
+ gitlab_update_work_item: tool12({
3673
+ description: `Update an existing work item.
3674
+ Can update title, description, state, labels, and assignees.`,
3675
+ args: {
3676
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3677
+ work_item_id: z12.number().describe("The ID of the work item"),
3678
+ title: z12.string().optional().describe("The new title"),
3679
+ description: z12.string().optional().describe("The new description (supports Markdown)"),
3680
+ state_event: z12.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
3681
+ labels: z12.array(z12.string()).optional().describe("Array of label names"),
3682
+ assignee_ids: z12.array(z12.number()).optional().describe("Array of user IDs to assign")
3683
+ },
3684
+ execute: async (args, _ctx) => {
3685
+ const client = getGitLabClient();
3686
+ const workItem = await client.updateWorkItem(args.project_id, args.work_item_id, {
3687
+ title: args.title,
3688
+ description: args.description,
3689
+ state_event: args.state_event,
3690
+ labels: args.labels,
3691
+ assignee_ids: args.assignee_ids
3692
+ });
3693
+ return JSON.stringify(workItem, null, 2);
3694
+ }
3695
+ }),
3696
+ gitlab_create_work_item_note: tool12({
3697
+ description: `Create a comment on a work item.`,
3698
+ args: {
3699
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3700
+ work_item_id: z12.number().describe("The ID of the work item"),
3701
+ body: z12.string().describe("The content of the note/comment (supports Markdown)")
3702
+ },
3703
+ execute: async (args, _ctx) => {
3704
+ const client = getGitLabClient();
3705
+ const note = await client.createWorkItemNote(args.project_id, args.work_item_id, args.body);
3706
+ return JSON.stringify(note, null, 2);
3707
+ }
3708
+ })
3709
+ };
3710
+
3711
+ // src/tools/discussions-unified.ts
3712
+ import { tool as tool13 } from "@opencode-ai/plugin";
3713
+ var z13 = tool13.schema;
3714
+ function normalizeBoolean(value) {
3715
+ if (typeof value === "boolean") return value;
3716
+ if (value === "true") return true;
3717
+ if (value === "false") return false;
3718
+ return void 0;
3719
+ }
3720
+ function filterDiscussionsByResolved(result, resolved) {
3721
+ const normalizedResolved = normalizeBoolean(resolved);
3722
+ if (normalizedResolved === void 0) {
3723
+ return result;
3724
+ }
3725
+ return {
3726
+ discussions: {
3727
+ ...result.discussions,
3728
+ nodes: result.discussions.nodes.filter(
3729
+ (d) => d.resolvable && d.resolved === normalizedResolved
3730
+ )
3731
+ }
3732
+ };
3733
+ }
3734
+ var positionSchema = z13.object({
3735
+ base_sha: z13.string().describe("SHA of the base commit"),
3736
+ start_sha: z13.string().describe("SHA of the start commit"),
3737
+ head_sha: z13.string().describe("SHA of the head commit"),
3738
+ position_type: z13.enum(["text", "image"]).describe("Type of position"),
3739
+ old_path: z13.string().optional().describe("Path of the file before changes"),
3740
+ new_path: z13.string().optional().describe("Path of the file after changes"),
3741
+ old_line: z13.number().optional().describe("Line number in the old version"),
3742
+ new_line: z13.number().optional().describe("Line number in the new version")
3743
+ });
3744
+ function validateResourceParams(resourceType, args) {
3745
+ switch (resourceType) {
3746
+ case "merge_request":
3747
+ case "issue":
3748
+ if (!args.project_id) throw new Error(`project_id is required for ${resourceType}`);
3749
+ if (args.iid == null) throw new Error(`iid is required for ${resourceType}`);
3750
+ break;
3751
+ case "epic":
3752
+ if (!args.group_id) throw new Error("group_id is required for epic");
3753
+ if (args.iid == null) throw new Error("iid is required for epic");
3754
+ break;
3755
+ case "commit":
3756
+ if (!args.project_id) throw new Error("project_id is required for commit");
3757
+ if (!args.sha) throw new Error("sha is required for commit");
3758
+ break;
3759
+ case "snippet":
3760
+ if (!args.project_id) throw new Error("project_id is required for snippet");
3761
+ if (args.snippet_id == null) throw new Error("snippet_id is required for snippet");
3762
+ break;
3763
+ }
3764
+ }
3765
+ var discussionsUnifiedTools = {
3766
+ /**
3767
+ * List discussions for any GitLab resource type
3768
+ */
3769
+ gitlab_list_discussions: tool13({
3770
+ description: `List discussions (comment threads) on any GitLab resource.
3771
+ Supports: merge_requests, issues, epics, commits, snippets.
3772
+
3773
+ Returns discussion threads with nested notes. Each discussion contains
3774
+ a 'notes' array with individual comments.
3775
+
3776
+ For pagination, use 'after' with the 'endCursor' from pageInfo to get the next page.
3777
+
3778
+ Examples:
3779
+ - MR: resource_type="merge_request", project_id="group/project", iid=123
3780
+ - Issue: resource_type="issue", project_id="group/project", iid=456
3781
+ - Epic: resource_type="epic", group_id="my-group", iid=1
3782
+ - Commit: resource_type="commit", project_id="group/project", sha="abc123"
3783
+ - Snippet: resource_type="snippet", project_id="group/project", snippet_id=789`,
3784
+ args: {
3785
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3786
+ project_id: z13.string().optional().describe("Project ID or path. Required for merge_request, issue, commit, snippet"),
3787
+ group_id: z13.string().optional().describe("Group ID or path. Required for epic"),
3788
+ iid: z13.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
3789
+ sha: z13.string().optional().describe("Commit SHA (required for commit)"),
3790
+ snippet_id: z13.number().optional().describe("Snippet ID (required for snippet)"),
3791
+ // Filtering
3792
+ resolved: z13.boolean().optional().describe(
3793
+ "Filter by resolved status: true for resolved, false for unresolved. Only returns resolvable discussions (excludes system notes). Client-side filtering."
3794
+ ),
3795
+ // Pagination
3796
+ first: z13.number().optional().describe("Number of items to return (default: 20)"),
3797
+ after: z13.string().optional().describe("Cursor for pagination - use endCursor from previous response"),
3798
+ before: z13.string().optional().describe("Cursor for backward pagination"),
3799
+ last: z13.number().optional().describe("Number of items from the end")
3800
+ },
3801
+ execute: async (args, _ctx) => {
3802
+ validateResourceParams(args.resource_type, args);
3803
+ const client = getGitLabClient();
3804
+ const paginationOptions = {
3805
+ first: args.first,
3806
+ after: args.after,
3807
+ before: args.before,
3808
+ last: args.last
3809
+ };
3810
+ let result;
3811
+ switch (args.resource_type) {
3812
+ case "merge_request":
3813
+ result = await client.listMrDiscussions(args.project_id, args.iid, paginationOptions);
3814
+ break;
3815
+ case "issue":
3816
+ result = await client.listIssueDiscussions(
3817
+ args.project_id,
3818
+ args.iid,
3819
+ paginationOptions
3820
+ );
3821
+ break;
3822
+ case "epic":
3823
+ result = await client.listEpicDiscussions(args.group_id, args.iid, paginationOptions);
3824
+ break;
3825
+ case "commit":
3826
+ return JSON.stringify(
3827
+ await client.listCommitDiscussions(args.project_id, args.sha),
3828
+ null,
3829
+ 2
3830
+ );
3831
+ case "snippet":
3832
+ result = await client.listSnippetDiscussions(
3833
+ args.project_id,
3834
+ args.snippet_id,
3835
+ paginationOptions
3836
+ );
3837
+ break;
3838
+ default:
3839
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3840
+ }
3841
+ const filteredResult = filterDiscussionsByResolved(result, args.resolved);
3842
+ return JSON.stringify(filteredResult, null, 2);
3843
+ }
3844
+ }),
3845
+ /**
3846
+ * Get a specific discussion thread from any GitLab resource
3847
+ */
3848
+ gitlab_get_discussion: tool13({
3849
+ description: `Get a specific discussion thread with all its replies.
3850
+ Returns the discussion with its 'notes' array containing all comments.
3851
+
3852
+ Use this to get full context of a specific conversation.
3853
+
3854
+ Required parameters vary by resource type:
3855
+ - merge_request: project_id, iid, discussion_id
3856
+ - issue: project_id, iid, discussion_id
3857
+ - epic: group_id, iid, discussion_id
3858
+ - commit: project_id, sha, discussion_id
3859
+ - snippet: project_id, snippet_id, discussion_id`,
3860
+ args: {
3861
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3862
+ discussion_id: z13.string().describe("The ID of the discussion thread"),
3863
+ project_id: z13.string().optional().describe("Project ID or path"),
3864
+ group_id: z13.string().optional().describe("Group ID or path (for epic)"),
3865
+ iid: z13.number().optional().describe("Internal ID (for merge_request, issue, epic)"),
3866
+ sha: z13.string().optional().describe("Commit SHA (for commit)"),
3867
+ snippet_id: z13.number().optional().describe("Snippet ID (for snippet)")
3868
+ },
3869
+ execute: async (args, _ctx) => {
3870
+ validateResourceParams(args.resource_type, args);
3871
+ const client = getGitLabClient();
3872
+ switch (args.resource_type) {
3873
+ case "merge_request":
3874
+ return JSON.stringify(
3875
+ await client.getMrDiscussion(args.project_id, args.iid, args.discussion_id),
3876
+ null,
3877
+ 2
3878
+ );
3879
+ case "issue":
3880
+ return JSON.stringify(
3881
+ await client.getIssueDiscussion(args.project_id, args.iid, args.discussion_id),
3882
+ null,
3883
+ 2
3884
+ );
3885
+ case "epic":
3886
+ return JSON.stringify(
3887
+ await client.getEpicDiscussion(args.group_id, args.iid, args.discussion_id),
3888
+ null,
3889
+ 2
3890
+ );
3891
+ case "commit":
3892
+ return JSON.stringify(
3893
+ await client.getCommitDiscussion(args.project_id, args.sha, args.discussion_id),
3894
+ null,
3895
+ 2
3896
+ );
3897
+ case "snippet":
3898
+ return JSON.stringify(
3899
+ await client.getSnippetDiscussion(
3900
+ args.project_id,
3901
+ args.snippet_id,
3902
+ args.discussion_id
3903
+ ),
3904
+ null,
3905
+ 2
3906
+ );
3907
+ default:
3908
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3909
+ }
3910
+ }
3911
+ }),
3912
+ /**
3913
+ * Create a new discussion thread or reply to an existing one
3914
+ */
3915
+ gitlab_create_discussion: tool13({
3916
+ description: `Create a new discussion thread or reply to an existing one.
3917
+
3918
+ REPLYING TO EXISTING THREADS:
3919
+ To reply to an existing discussion, provide the discussion_id parameter.
3920
+ This is the recommended way to reply in-thread rather than creating a standalone comment.
3921
+
3922
+ STARTING A NEW DISCUSSION:
3923
+ Omit discussion_id to create a new thread. For code-specific comments on MRs/commits, provide position information.
3924
+
3925
+ Examples:
3926
+ - Reply to thread: resource_type="merge_request", project_id="group/project", iid=123, discussion_id="abc123def", body="Thanks for the feedback!"
3927
+ - New comment: resource_type="merge_request", project_id="group/project", iid=123, body="General comment"
3928
+ - Code comment: resource_type="merge_request", project_id="group/project", iid=123, body="...", position={base_sha, head_sha, new_path, new_line, ...}`,
3929
+ args: {
3930
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("Type of GitLab resource"),
3931
+ body: z13.string().describe("The comment text (Markdown supported)"),
3932
+ project_id: z13.string().optional().describe("Project ID or path"),
3933
+ group_id: z13.string().optional().describe("Group ID or path (for epic)"),
3934
+ iid: z13.number().optional().describe("Internal ID (for merge_request, issue, epic)"),
3935
+ sha: z13.string().optional().describe("Commit SHA (for commit)"),
3936
+ snippet_id: z13.number().optional().describe("Snippet ID (for snippet)"),
3937
+ discussion_id: z13.string().optional().describe("If provided, replies to existing discussion. If omitted, creates new thread"),
3938
+ position: positionSchema.optional().describe("Position for code-specific comments (MR/commit only)")
3939
+ },
3940
+ execute: async (args, _ctx) => {
3941
+ validateResourceParams(args.resource_type, args);
3942
+ const client = getGitLabClient();
3943
+ if (args.discussion_id) {
3944
+ const note = await client.replyToDiscussion(
3945
+ args.resource_type,
3946
+ {
3947
+ projectId: args.project_id,
3948
+ groupId: args.group_id,
3949
+ iid: args.iid,
3950
+ sha: args.sha,
3951
+ snippetId: args.snippet_id
3952
+ },
3953
+ args.discussion_id,
3954
+ args.body
3955
+ );
3956
+ return JSON.stringify(note, null, 2);
3957
+ }
3958
+ switch (args.resource_type) {
3959
+ case "merge_request":
3960
+ return JSON.stringify(
3961
+ await client.createMrDiscussion(args.project_id, args.iid, args.body, args.position),
3962
+ null,
3963
+ 2
3964
+ );
3965
+ case "issue":
3966
+ return JSON.stringify(
3967
+ await client.createIssueNote(args.project_id, args.iid, args.body),
3968
+ null,
3969
+ 2
3970
+ );
3971
+ case "epic":
3972
+ return JSON.stringify(
3973
+ await client.createEpicNote(args.group_id, args.iid, args.body),
3974
+ null,
3975
+ 2
3976
+ );
3977
+ case "commit":
3978
+ return JSON.stringify(
3979
+ await client.createCommitDiscussion(
3980
+ args.project_id,
3981
+ args.sha,
3982
+ args.body,
3983
+ args.position
3984
+ ),
3985
+ null,
3986
+ 2
3987
+ );
3988
+ case "snippet":
3989
+ return JSON.stringify(
3990
+ await client.createSnippetDiscussion(args.project_id, args.snippet_id, args.body),
3991
+ null,
3992
+ 2
3993
+ );
3994
+ default:
3995
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
3996
+ }
3997
+ }
3998
+ }),
3999
+ /**
4000
+ * Resolve or unresolve a discussion thread
4001
+ */
4002
+ gitlab_resolve_discussion: tool13({
4003
+ description: `Mark a discussion thread as resolved or unresolve it.
4004
+ Only works for resolvable discussions (MRs and issues only).
4005
+
4006
+ Uses GraphQL API which handles all discussion types including outdated discussions
4007
+ (those with resolved: null) that the REST API cannot handle.
4008
+
4009
+ Use after addressing feedback to indicate the discussion is complete.`,
4010
+ args: {
4011
+ resource_type: z13.enum(["merge_request", "issue"]).describe("Type of resource (only MR and issue discussions can be resolved)"),
4012
+ action: z13.enum(["resolve", "unresolve"]).describe("Whether to resolve or unresolve"),
4013
+ discussion_id: z13.string().describe("The ID of the discussion thread"),
4014
+ project_id: z13.string().describe("Project ID or path"),
4015
+ iid: z13.number().describe("Internal ID of the MR or issue")
4016
+ },
4017
+ execute: async (args, _ctx) => {
4018
+ const client = getGitLabClient();
4019
+ const resolve2 = args.action === "resolve";
4020
+ return JSON.stringify(
4021
+ await client.toggleDiscussionResolved(args.discussion_id, resolve2),
4022
+ null,
4023
+ 2
4024
+ );
4025
+ }
4026
+ })
4027
+ };
4028
+
4029
+ // src/tools/notes-unified.ts
4030
+ import { tool as tool14 } from "@opencode-ai/plugin";
4031
+ var z14 = tool14.schema;
4032
+ function normalizeBoolean2(value) {
4033
+ if (typeof value === "boolean") return value;
4034
+ if (value === "true") return true;
4035
+ if (value === "false") return false;
4036
+ return void 0;
4037
+ }
4038
+ function filterNotesByResolved(result, resolved) {
4039
+ const normalizedResolved = normalizeBoolean2(resolved);
4040
+ if (normalizedResolved === void 0) {
4041
+ return result;
4042
+ }
4043
+ return {
4044
+ ...result,
4045
+ notes: {
4046
+ ...result.notes,
4047
+ nodes: result.notes.nodes.filter((n) => n.resolvable && n.resolved === normalizedResolved)
4048
+ }
4049
+ };
4050
+ }
4051
+ var VALID_LIST_CREATE_TYPES = ["merge_request", "issue", "epic", "snippet"];
4052
+ var VALID_GET_NOTE_TYPES = ["issue", "epic"];
4053
+ function validationError3(param, resourceType) {
4054
+ return new Error(
4055
+ `Missing required parameter: '${param}' is required for resource_type '${resourceType}'`
4056
+ );
4057
+ }
4058
+ function validateListCreateParams(resourceType, args) {
4059
+ if (!VALID_LIST_CREATE_TYPES.includes(resourceType)) {
4060
+ throw new Error(
4061
+ `Invalid resource_type '${resourceType}'. Must be one of: ${VALID_LIST_CREATE_TYPES.join(", ")}`
4062
+ );
4063
+ }
4064
+ switch (resourceType) {
4065
+ case "merge_request":
4066
+ case "issue":
4067
+ if (!args.project_id) throw validationError3("project_id", resourceType);
4068
+ if (args.iid == null) throw validationError3("iid", resourceType);
4069
+ break;
4070
+ case "epic":
4071
+ if (!args.group_id) throw validationError3("group_id", resourceType);
4072
+ if (args.iid == null) throw validationError3("iid", resourceType);
4073
+ break;
4074
+ case "snippet":
4075
+ if (!args.project_id) throw validationError3("project_id", resourceType);
4076
+ if (args.snippet_id == null) throw validationError3("snippet_id", resourceType);
4077
+ break;
4078
+ }
4079
+ }
4080
+ function validateGetNoteParams(resourceType, args) {
4081
+ if (!VALID_GET_NOTE_TYPES.includes(resourceType)) {
4082
+ throw new Error(
4083
+ `Invalid resource_type '${resourceType}'. Must be one of: ${VALID_GET_NOTE_TYPES.join(", ")}`
4084
+ );
4085
+ }
4086
+ if (args.note_id == null) throw validationError3("note_id", resourceType);
4087
+ switch (resourceType) {
4088
+ case "issue":
4089
+ if (!args.project_id) throw validationError3("project_id", resourceType);
4090
+ if (args.iid == null) throw validationError3("iid", resourceType);
4091
+ break;
4092
+ case "epic":
4093
+ if (!args.group_id) throw validationError3("group_id", resourceType);
4094
+ if (args.iid == null) throw validationError3("iid", resourceType);
4095
+ break;
4096
+ }
4097
+ }
4098
+ var notesUnifiedTools = {
4099
+ /**
4100
+ * List notes/comments for any GitLab resource type
4101
+ */
4102
+ gitlab_list_notes: tool14({
4103
+ description: `List all notes/comments on any GitLab resource using GraphQL API with pagination support.
4104
+ Returns all comments including system notes in chronological order.
4105
+ This is easier to read than discussions which have nested structure.
4106
+
4107
+ The response includes pagination information (pageInfo) with cursors for fetching additional pages.
4108
+ Use 'after' with the 'endCursor' from pageInfo to get the next page.
4109
+ Use 'before' with the 'startCursor' from pageInfo to get the previous page.
4110
+
4111
+ Examples:
4112
+ - MR: resource_type="merge_request", project_id="group/project", iid=123
4113
+ - Issue: resource_type="issue", project_id="group/project", iid=456
4114
+ - Epic: resource_type="epic", group_id="my-group", iid=1
4115
+ - Snippet: resource_type="snippet", project_id="group/project", snippet_id=789`,
4116
+ args: {
4117
+ resource_type: z14.enum(["merge_request", "issue", "epic", "snippet"]).describe("Type of GitLab resource"),
4118
+ project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
4119
+ group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
4120
+ iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
4121
+ snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)"),
4122
+ // Filtering
4123
+ resolved: z14.boolean().optional().describe(
4124
+ "Filter by resolved status: true for resolved, false for unresolved. Only returns resolvable notes (excludes system notes). Client-side filtering."
4125
+ ),
4126
+ // Pagination
4127
+ first: z14.number().optional().describe("Number of items to return from the beginning (default: 20, max: 100)"),
4128
+ after: z14.string().optional().describe("Cursor for forward pagination - use endCursor from previous response"),
4129
+ last: z14.number().optional().describe("Number of items to return from the end (for backward pagination)"),
4130
+ before: z14.string().optional().describe("Cursor for backward pagination - use startCursor from previous response")
4131
+ },
4132
+ execute: async (args, _ctx) => {
4133
+ validateListCreateParams(args.resource_type, args);
4134
+ const client = getGitLabClient();
4135
+ const paginationOptions = {
4136
+ first: args.first,
4137
+ after: args.after,
4138
+ last: args.last,
4139
+ before: args.before
4140
+ };
4141
+ let result;
4142
+ switch (args.resource_type) {
4143
+ case "merge_request":
4144
+ result = await client.listMrNotes(args.project_id, args.iid, paginationOptions);
4145
+ break;
4146
+ case "issue":
4147
+ result = await client.listIssueNotes(args.project_id, args.iid, paginationOptions);
4148
+ break;
4149
+ case "epic":
4150
+ result = await client.listEpicNotes(args.group_id, args.iid, paginationOptions);
4151
+ break;
4152
+ case "snippet":
4153
+ result = await client.listSnippetNotes(
4154
+ args.project_id,
4155
+ args.snippet_id,
4156
+ paginationOptions
4157
+ );
4158
+ break;
4159
+ default:
4160
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
4161
+ }
4162
+ const filteredResult = filterNotesByResolved(result, args.resolved);
4163
+ return JSON.stringify(filteredResult, null, 2);
4164
+ }
4165
+ }),
4166
+ /**
4167
+ * Get a single note/comment by its ID
4168
+ */
4169
+ gitlab_get_note: tool14({
4170
+ description: `Get a single note/comment from an issue or epic by its ID.
4171
+ Returns the full details of a specific note including author, body, timestamps, and metadata.
4172
+ Useful when you need to retrieve a specific comment without fetching all notes.
4173
+
4174
+ Supports: issues, epics (MR notes use discussions API)
4175
+
4176
+ Examples:
4177
+ - Issue note: resource_type="issue", project_id="group/project", iid=456, note_id=123
4178
+ - Epic note: resource_type="epic", group_id="my-group", iid=1, note_id=456`,
4179
+ args: {
4180
+ resource_type: z14.enum(["issue", "epic"]).describe("Type of GitLab resource (issue or epic only)"),
4181
+ note_id: z14.number().describe("The ID of the note to retrieve"),
4182
+ project_id: z14.string().optional().describe("Project ID or path. Required for issue"),
4183
+ group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
4184
+ iid: z14.number().describe("Internal ID of the issue or epic")
4185
+ },
4186
+ execute: async (args, _ctx) => {
4187
+ validateGetNoteParams(args.resource_type, args);
4188
+ const client = getGitLabClient();
4189
+ switch (args.resource_type) {
4190
+ case "issue":
4191
+ return JSON.stringify(
4192
+ await client.getIssueNote(args.project_id, args.iid, args.note_id),
4193
+ null,
4194
+ 2
4195
+ );
4196
+ case "epic":
4197
+ return JSON.stringify(
4198
+ await client.getEpicNote(args.group_id, args.iid, args.note_id),
4199
+ null,
4200
+ 2
4201
+ );
4202
+ default:
4203
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
4204
+ }
4205
+ }
4206
+ }),
4207
+ /**
4208
+ * Create a simple note/comment on any GitLab resource
4209
+ */
4210
+ gitlab_create_note: tool14({
4211
+ description: `Add a simple comment/note to any GitLab resource.
4212
+ Creates a standalone comment (not part of a thread).
4213
+
4214
+ For replying to existing discussion threads, use gitlab_create_discussion
4215
+ with discussion_id parameter instead.
4216
+
4217
+ Examples:
4218
+ - MR comment: resource_type="merge_request", project_id="group/project", iid=123, body="LGTM!"
4219
+ - Issue comment: resource_type="issue", project_id="group/project", iid=456, body="Working on this"
4220
+ - Epic comment: resource_type="epic", group_id="my-group", iid=1, body="Planning complete"
4221
+ - Snippet comment: resource_type="snippet", project_id="group/project", snippet_id=789, body="Nice code!"`,
4222
+ args: {
4223
+ resource_type: z14.enum(["merge_request", "issue", "epic", "snippet"]).describe("Type of GitLab resource"),
4224
+ body: z14.string().describe("The content of the note/comment (supports Markdown)"),
4225
+ project_id: z14.string().optional().describe("Project ID or path. Required for merge_request, issue, snippet"),
4226
+ group_id: z14.string().optional().describe("Group ID or path. Required for epic"),
4227
+ iid: z14.number().optional().describe("Internal ID of the resource (for merge_request, issue, epic)"),
4228
+ snippet_id: z14.number().optional().describe("Snippet ID (required for snippet)")
4229
+ },
4230
+ execute: async (args, _ctx) => {
4231
+ validateListCreateParams(args.resource_type, args);
4232
+ const client = getGitLabClient();
4233
+ switch (args.resource_type) {
4234
+ case "merge_request":
4235
+ return JSON.stringify(
4236
+ await client.createMrNote(args.project_id, args.iid, args.body),
4237
+ null,
4238
+ 2
4239
+ );
4240
+ case "issue":
4241
+ return JSON.stringify(
4242
+ await client.createIssueNote(args.project_id, args.iid, args.body),
4243
+ null,
4244
+ 2
4245
+ );
4246
+ case "epic":
4247
+ return JSON.stringify(
4248
+ await client.createEpicNote(args.group_id, args.iid, args.body),
4249
+ null,
4250
+ 2
4251
+ );
4252
+ case "snippet":
4253
+ return JSON.stringify(
4254
+ await client.createSnippetNote(args.project_id, args.snippet_id, args.body),
4255
+ null,
4256
+ 2
4257
+ );
4258
+ default:
4259
+ throw new Error(`Unsupported resource type: ${args.resource_type}`);
4260
+ }
4261
+ }
4262
+ })
4263
+ };
4264
+
4265
+ // src/tools/git.ts
4266
+ import { tool as tool15 } from "@opencode-ai/plugin";
4267
+
4268
+ // src/client/git.ts
4269
+ import { execFile } from "child_process";
4270
+ import { promisify } from "util";
4271
+ import { existsSync, statSync } from "fs";
4272
+ import { resolve, join } from "path";
4273
+ var execFileAsync = promisify(execFile);
4274
+ var GitClient = class _GitClient {
4275
+ workingDirectory;
4276
+ // Whitelist of allowed git commands (read-only operations)
4277
+ static ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
4278
+ "status",
4279
+ "log",
4280
+ "show",
4281
+ "diff",
4282
+ "branch",
4283
+ "tag",
4284
+ "remote",
4285
+ "ls-files",
4286
+ "ls-remote",
4287
+ "rev-parse",
4288
+ "describe",
4289
+ "config",
4290
+ "blame",
4291
+ "shortlog",
4292
+ "reflog",
4293
+ "show-ref",
4294
+ "ls-tree",
4295
+ "cat-file",
4296
+ "rev-list",
4297
+ "name-rev",
4298
+ "show-branch",
4299
+ "whatchanged",
4300
+ "grep",
4301
+ "annotate"
4302
+ ]);
4303
+ // Commands that are explicitly forbidden (destructive operations)
4304
+ static FORBIDDEN_COMMANDS = /* @__PURE__ */ new Set([
4305
+ "push",
4306
+ "pull",
4307
+ "fetch",
4308
+ "clone",
4309
+ "commit",
4310
+ "add",
4311
+ "rm",
4312
+ "mv",
4313
+ "reset",
4314
+ "rebase",
4315
+ "merge",
4316
+ "cherry-pick",
4317
+ "revert",
4318
+ "clean",
4319
+ "checkout",
4320
+ "switch",
4321
+ "restore",
4322
+ "stash",
4323
+ "submodule",
4324
+ "worktree"
4325
+ ]);
4326
+ // Shell operators that should be blocked
4327
+ static SHELL_OPERATORS = [";", "&&", "||", "|", "`", "$("];
4328
+ constructor(workingDirectory) {
4329
+ this.workingDirectory = workingDirectory || process.cwd();
4330
+ }
4331
+ /**
4332
+ * Execute a git command in the repository working directory
4333
+ *
4334
+ * Security: Uses execFile() instead of exec() to prevent command injection.
4335
+ * Arguments are passed directly to git without shell interpretation, making
4336
+ * attacks like `--format="$(malicious)"` impossible.
4337
+ *
4338
+ * @param command - The git command to execute (without 'git' prefix)
4339
+ * @param args - Array of arguments for the command
4340
+ * @returns Command output
4341
+ * @throws Error if command is not allowed or execution fails
4342
+ */
4343
+ async runGitCommand(command, args = []) {
4344
+ this.validateCommand(command, args);
4345
+ try {
4346
+ const { stdout, stderr } = await execFileAsync("git", [command, ...args], {
4347
+ cwd: this.workingDirectory,
4348
+ maxBuffer: 10 * 1024 * 1024,
4349
+ // 10MB buffer
4350
+ timeout: 3e4
4351
+ // 30 second timeout
4352
+ });
4353
+ return stderr ? `${stdout}
4354
+
4355
+ Warnings/Info:
4356
+ ${stderr}` : stdout;
4357
+ } catch (error) {
4358
+ if (error instanceof Error) {
4359
+ throw new Error(`Git command failed: ${error.message}`);
4360
+ }
4361
+ throw error;
4362
+ }
4363
+ }
4364
+ /**
4365
+ * Validate that a git command is safe to execute
4366
+ * @param command - The git command
4367
+ * @param args - Command arguments
4368
+ * @throws Error if command is not allowed
4369
+ */
4370
+ validateCommand(command, args) {
4371
+ if (_GitClient.FORBIDDEN_COMMANDS.has(command)) {
4372
+ throw new Error(
4373
+ `Git command '${command}' is not allowed. Only read-only operations are permitted.`
4374
+ );
4375
+ }
4376
+ if (!_GitClient.ALLOWED_COMMANDS.has(command)) {
4377
+ throw new Error(
4378
+ `Git command '${command}' is not in the allowed list. Only safe, read-only operations are permitted.`
4379
+ );
4380
+ }
4381
+ if (command === "branch" && args.some((arg) => arg === "-d" || arg === "-D" || arg === "-m")) {
4382
+ throw new Error("Branch deletion and renaming operations are not allowed.");
4383
+ }
4384
+ if (command === "tag" && args.some((arg) => arg === "-d" || arg === "-f")) {
4385
+ throw new Error("Tag deletion and force operations are not allowed.");
4386
+ }
4387
+ if (command === "remote" && args.some((arg) => arg === "add" || arg === "remove" || arg === "set-url")) {
4388
+ throw new Error("Remote modification operations are not allowed.");
4389
+ }
4390
+ if (command === "config" && args.length > 0 && !args.some((arg) => arg.startsWith("--get"))) {
4391
+ throw new Error("Only git config read operations (--get) are allowed.");
4392
+ }
4393
+ const allArgs = [command, ...args].join(" ");
4394
+ if (_GitClient.SHELL_OPERATORS.some((op) => allArgs.includes(op))) {
4395
+ throw new Error("Command contains potentially dangerous shell operators.");
4396
+ }
4397
+ }
4398
+ /**
4399
+ * Validate that a directory exists and is a git repository
4400
+ * @param directory - Path to validate
4401
+ * @throws Error if directory is invalid or not a git repository
4402
+ */
4403
+ validateDirectory(directory) {
4404
+ const absolutePath = resolve(directory);
4405
+ if (!existsSync(absolutePath)) {
4406
+ throw new Error(`Directory does not exist: ${absolutePath}`);
4407
+ }
4408
+ const stats = statSync(absolutePath);
4409
+ if (!stats.isDirectory()) {
4410
+ throw new Error(`Path is not a directory: ${absolutePath}`);
4411
+ }
4412
+ const gitDir = join(absolutePath, ".git");
4413
+ if (!existsSync(gitDir)) {
4414
+ throw new Error(`Not a git repository: ${absolutePath}`);
4415
+ }
4416
+ }
4417
+ /**
4418
+ * Get the current working directory
4419
+ */
4420
+ getWorkingDirectory() {
4421
+ return this.workingDirectory;
4422
+ }
4423
+ /**
4424
+ * Set the working directory for git commands
4425
+ * @param directory - Path to the git repository
4426
+ * @throws Error if directory is invalid or not a git repository
4427
+ */
4428
+ setWorkingDirectory(directory) {
4429
+ this.validateDirectory(directory);
4430
+ this.workingDirectory = resolve(directory);
4431
+ }
4432
+ };
4433
+
4434
+ // src/tools/git.ts
4435
+ var z15 = tool15.schema;
4436
+ var gitClient = null;
4437
+ function getGitClient() {
4438
+ if (!gitClient) {
4439
+ gitClient = new GitClient();
4440
+ }
4441
+ return gitClient;
4442
+ }
4443
+ var gitTools = {
4444
+ run_git_command: tool15({
4445
+ description: `Execute safe, read-only git commands in the repository working directory.
4446
+
4447
+ Security restrictions:
4448
+ - Only read-only git commands are allowed (status, log, show, diff, branch, tag, etc.)
4449
+ - Destructive operations are forbidden (push, pull, commit, add, rm, reset, merge, etc.)
4450
+ - Shell operators and command injection attempts are blocked
4451
+ - Commands are executed with a 30-second timeout and 10MB output buffer
4452
+
4453
+ Allowed commands include:
4454
+ - status: Show working tree status
4455
+ - log: Show commit logs
4456
+ - show: Show various types of objects
4457
+ - diff: Show changes between commits, commit and working tree, etc.
4458
+ - branch: List, create, or delete branches (read-only: list only)
4459
+ - tag: List tags (read-only: list only)
4460
+ - remote: List remote repositories (read-only: list only)
4461
+ - ls-files: Show information about files in the index and working tree
4462
+ - ls-remote: List references in a remote repository
4463
+ - rev-parse: Parse revision (or other objects) names
4464
+ - describe: Give an object a human readable name based on an available ref
4465
+ - config: Get repository configuration (read-only: --get operations only)
4466
+ - blame: Show what revision and author last modified each line of a file
4467
+ - shortlog: Summarize git log output
4468
+ - reflog: Show reference logs
4469
+ - show-ref: List references in a local repository
4470
+ - ls-tree: List the contents of a tree object
4471
+ - cat-file: Provide content or type and size information for repository objects
4472
+ - rev-list: List commit objects in reverse chronological order
4473
+ - name-rev: Find symbolic names for given revs
4474
+ - show-branch: Show branches and their commits
4475
+ - whatchanged: Show logs with difference each commit introduces
4476
+ - grep: Print lines matching a pattern
4477
+ - annotate: Annotate file lines with commit information
4478
+
4479
+ Examples:
4480
+ - run_git_command("status", []) - Show working tree status
4481
+ - run_git_command("log", ["--oneline", "-10"]) - Show last 10 commits
4482
+ - run_git_command("diff", ["HEAD~1", "HEAD"]) - Show diff between last two commits
4483
+ - run_git_command("branch", ["-a"]) - List all branches
4484
+ - run_git_command("show", ["HEAD:README.md"]) - Show README.md from HEAD commit`,
4485
+ args: {
4486
+ command: z15.string().describe('The git command to execute (without "git" prefix)'),
4487
+ args: z15.array(z15.string()).optional().describe("Array of arguments for the git command (default: [])"),
4488
+ working_directory: z15.string().optional().describe(
4489
+ "Path to the git repository (default: current working directory). Use this to execute git commands in a specific repository."
4490
+ )
4491
+ },
4492
+ execute: async (args, _ctx) => {
4493
+ const client = getGitClient();
4494
+ if (args.working_directory) {
4495
+ client.setWorkingDirectory(args.working_directory);
4496
+ }
4497
+ try {
4498
+ const output = await client.runGitCommand(args.command, args.args || []);
4499
+ return output;
4500
+ } catch (error) {
4501
+ if (error instanceof Error) {
4502
+ throw new Error(`Failed to execute git command: ${error.message}`);
4503
+ }
4504
+ throw error;
4505
+ }
4506
+ }
4507
+ })
4508
+ };
4509
+
4510
+ // src/tools/audit.ts
4511
+ import { tool as tool16 } from "@opencode-ai/plugin";
4512
+ var z16 = tool16.schema;
4513
+ var auditTools = {
4514
+ gitlab_list_project_audit_events: tool16({
4515
+ description: `List audit events for a project.
4516
+ Returns audit events including actions like project settings changes, member additions/removals, and other security-relevant activities.
4517
+ Note: Requires project owner role or higher.`,
4518
+ args: {
4519
+ project_id: z16.string().describe("The project ID or URL-encoded path"),
4520
+ created_after: z16.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
4521
+ created_before: z16.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
4522
+ entity_type: z16.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
4523
+ entity_id: z16.number().optional().describe("Filter by entity ID"),
4524
+ author_id: z16.number().optional().describe("Filter by author user ID"),
4525
+ per_page: z16.number().optional().describe("Number of results per page (default: 20)"),
4526
+ page: z16.number().optional().describe("Page number for pagination (default: 1)")
4527
+ },
4528
+ execute: async (args, _ctx) => {
4529
+ const client = getGitLabClient();
4530
+ const events = await client.listProjectAuditEvents(args.project_id, {
4531
+ created_after: args.created_after,
4532
+ created_before: args.created_before,
4533
+ entity_type: args.entity_type,
4534
+ entity_id: args.entity_id,
4535
+ author_id: args.author_id,
4536
+ per_page: args.per_page,
4537
+ page: args.page
4538
+ });
4539
+ return JSON.stringify(events, null, 2);
4540
+ }
4541
+ }),
4542
+ gitlab_list_group_audit_events: tool16({
4543
+ description: `List audit events for a group.
4544
+ Returns audit events including actions like group settings changes, member additions/removals, subgroup operations, and other security-relevant activities.
4545
+ Note: Requires group owner role or higher.`,
4546
+ args: {
4547
+ group_id: z16.string().describe("The group ID or URL-encoded path"),
4548
+ created_after: z16.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
4549
+ created_before: z16.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
4550
+ entity_type: z16.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
4551
+ entity_id: z16.number().optional().describe("Filter by entity ID"),
4552
+ author_id: z16.number().optional().describe("Filter by author user ID"),
4553
+ per_page: z16.number().optional().describe("Number of results per page (default: 20)"),
4554
+ page: z16.number().optional().describe("Page number for pagination (default: 1)")
4555
+ },
4556
+ execute: async (args, _ctx) => {
4557
+ const client = getGitLabClient();
4558
+ const events = await client.listGroupAuditEvents(args.group_id, {
4559
+ created_after: args.created_after,
4560
+ created_before: args.created_before,
4561
+ entity_type: args.entity_type,
4562
+ entity_id: args.entity_id,
4563
+ author_id: args.author_id,
4564
+ per_page: args.per_page,
4565
+ page: args.page
4566
+ });
4567
+ return JSON.stringify(events, null, 2);
4568
+ }
4569
+ }),
4570
+ gitlab_list_instance_audit_events: tool16({
4571
+ description: `List instance-level audit events.
4572
+ Returns audit events for the entire GitLab instance including actions like instance settings changes, user management, license changes, and other system-wide security-relevant activities.
4573
+ Note: Requires administrator access.`,
4574
+ args: {
4575
+ created_after: z16.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
4576
+ created_before: z16.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
4577
+ entity_type: z16.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
4578
+ entity_id: z16.number().optional().describe("Filter by entity ID"),
4579
+ author_id: z16.number().optional().describe("Filter by author user ID"),
4580
+ per_page: z16.number().optional().describe("Number of results per page (default: 20)"),
4581
+ page: z16.number().optional().describe("Page number for pagination (default: 1)")
4582
+ },
4583
+ execute: async (args, _ctx) => {
4584
+ const client = getGitLabClient();
4585
+ const events = await client.listInstanceAuditEvents({
4586
+ created_after: args.created_after,
4587
+ created_before: args.created_before,
4588
+ entity_type: args.entity_type,
4589
+ entity_id: args.entity_id,
4590
+ author_id: args.author_id,
4591
+ per_page: args.per_page,
4592
+ page: args.page
4593
+ });
4594
+ return JSON.stringify(events, null, 2);
4595
+ }
4596
+ })
4597
+ };
4598
+
4599
+ // src/tools/award-emoji.ts
4600
+ import { tool as tool17 } from "@opencode-ai/plugin";
4601
+ var z17 = tool17.schema;
4602
+ function validateAwardEmojiParams(args) {
4603
+ if (!args.project_id) {
4604
+ throw new Error("project_id is required");
4605
+ }
4606
+ if (args.resource_iid == null) {
4607
+ throw new Error("resource_iid is required");
4608
+ }
4609
+ }
4610
+ function buildResourceId(args) {
4611
+ return {
4612
+ projectId: args.project_id,
4613
+ resourceType: args.resource_type,
4614
+ resourceIid: args.resource_iid,
4615
+ noteId: args.note_id
4616
+ };
4617
+ }
4618
+ var awardEmojiTools = {
4619
+ /**
4620
+ * Add a reaction (award emoji) to a resource or note
4621
+ */
4622
+ gitlab_create_award_emoji: tool17({
4623
+ description: `Add a reaction (award emoji) to a merge request, issue, snippet, or a note/comment on these resources.
4624
+
4625
+ Common emoji names: thumbsup, thumbsdown, smile, tada, rocket, eyes, heart, +1, -1
4626
+
4627
+ To react to a specific comment/note, provide the note_id parameter.
4628
+
4629
+ Examples:
4630
+ - React to MR: resource_type="merge_request", project_id="group/project", resource_iid=123, name="thumbsup"
4631
+ - React to issue comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789, name="rocket"`,
4632
+ args: {
4633
+ resource_type: z17.enum(["merge_request", "issue", "snippet"]).describe("Type of resource to add the reaction to"),
4634
+ project_id: z17.string().describe("Project ID or URL-encoded path"),
4635
+ resource_iid: z17.number().describe("Internal ID of the merge request, issue, or snippet"),
4636
+ name: z17.string().describe(
4637
+ 'Emoji name without colons (e.g., "thumbsup", "rocket", "eyes", "heart", "tada")'
4638
+ ),
4639
+ note_id: z17.number().optional().describe("Note/comment ID to add the reaction to (if reacting to a specific comment)")
4640
+ },
4641
+ execute: async (args, _ctx) => {
4642
+ validateAwardEmojiParams(args);
4643
+ const client = getGitLabClient();
4644
+ const resource = buildResourceId(args);
4645
+ const result = await client.createAwardEmoji(resource, args.name);
4646
+ return JSON.stringify(result, null, 2);
4647
+ }
4648
+ }),
4649
+ /**
4650
+ * List all reactions on a resource or note
4651
+ */
4652
+ gitlab_list_award_emoji: tool17({
4653
+ description: `List all reactions (award emoji) on a merge request, issue, snippet, or a specific note/comment.
4654
+
4655
+ Examples:
4656
+ - List reactions on MR: resource_type="merge_request", project_id="group/project", resource_iid=123
4657
+ - List reactions on a comment: resource_type="issue", project_id="group/project", resource_iid=456, note_id=789`,
4658
+ args: {
4659
+ resource_type: z17.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4660
+ project_id: z17.string().describe("Project ID or URL-encoded path"),
4661
+ resource_iid: z17.number().describe("Internal ID of the merge request, issue, or snippet"),
4662
+ note_id: z17.number().optional().describe("Note/comment ID to list reactions for (if listing for a specific comment)")
4663
+ },
4664
+ execute: async (args, _ctx) => {
4665
+ validateAwardEmojiParams(args);
4666
+ const client = getGitLabClient();
4667
+ const resource = buildResourceId(args);
4668
+ const result = await client.listAwardEmoji(resource);
4669
+ return JSON.stringify(result, null, 2);
4670
+ }
4671
+ }),
4672
+ /**
4673
+ * Remove a reaction from a resource or note
4674
+ */
4675
+ gitlab_delete_award_emoji: tool17({
4676
+ description: `Remove a reaction (award emoji) from a merge request, issue, snippet, or note/comment.
4677
+
4678
+ You need the award_id which can be found using gitlab_list_award_emoji.
4679
+
4680
+ Examples:
4681
+ - Remove reaction from MR: resource_type="merge_request", project_id="group/project", resource_iid=123, award_id=456
4682
+ - Remove from comment: resource_type="issue", ..., note_id=789, award_id=456`,
4683
+ args: {
4684
+ resource_type: z17.enum(["merge_request", "issue", "snippet"]).describe("Type of resource"),
4685
+ project_id: z17.string().describe("Project ID or URL-encoded path"),
4686
+ resource_iid: z17.number().describe("Internal ID of the merge request, issue, or snippet"),
4687
+ award_id: z17.number().describe("ID of the award emoji to remove"),
4688
+ note_id: z17.number().optional().describe("Note/comment ID if removing from a specific comment")
4689
+ },
4690
+ execute: async (args, _ctx) => {
4691
+ validateAwardEmojiParams(args);
4692
+ const client = getGitLabClient();
4693
+ const resource = buildResourceId(args);
4694
+ await client.deleteAwardEmoji(resource, args.award_id);
4695
+ return JSON.stringify({ success: true, message: "Award emoji removed" }, null, 2);
4696
+ }
4697
+ })
4698
+ };
4699
+
4700
+ // src/index.ts
4701
+ var gitlabPlugin = async (_input) => {
4702
+ return {
4703
+ tool: {
4704
+ // Merge Request Tools
4705
+ ...mergeRequestTools,
4706
+ // Issue Tools
4707
+ ...issueTools,
4708
+ // Epic Tools
4709
+ ...epicTools,
4710
+ // Pipeline Tools
4711
+ ...pipelineTools,
4712
+ // Repository Tools
4713
+ ...repositoryTools,
4714
+ // Search Tools
4715
+ ...searchTools,
4716
+ // Project Tools
4717
+ ...projectTools,
4718
+ // User Tools
4719
+ ...userTools,
4720
+ // Security Tools
4721
+ ...securityTools,
4722
+ // TODO Tools
4723
+ ...todoTools,
4724
+ // Wiki Tools
4725
+ ...wikiTools,
4726
+ // Work Item Tools
4727
+ ...workItemTools,
4728
+ // Unified Discussion Tools (covers MR, issue, epic, commit, snippet discussions)
4729
+ ...discussionsUnifiedTools,
4730
+ // Unified Notes Tools (covers MR, issue, epic, snippet notes)
4731
+ ...notesUnifiedTools,
4732
+ // Git Tools
4733
+ ...gitTools,
4734
+ // Audit Tools
4735
+ ...auditTools,
4736
+ // Award Emoji (Reactions) Tools
4737
+ ...awardEmojiTools
4738
+ }
4739
+ };
4740
+ };
4741
+ var index_default = gitlabPlugin;
4742
+ export {
4743
+ index_default as default,
4744
+ gitlabPlugin
4745
+ };