@gitlab/opencode-gitlab-plugin 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3853 @@
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/merge-requests.ts
86
+ var MergeRequestsClient = class extends GitLabApiClient {
87
+ async getMergeRequest(projectId, mrIid, includeChanges) {
88
+ const encodedProject = this.encodeProjectId(projectId);
89
+ let path2 = `/projects/${encodedProject}/merge_requests/${mrIid}`;
90
+ if (includeChanges) {
91
+ path2 += "?include_diverged_commits_count=true";
92
+ }
93
+ return this.fetch("GET", path2);
94
+ }
95
+ async listMergeRequests(options) {
96
+ const params = new URLSearchParams();
97
+ params.set("per_page", String(options.limit || 20));
98
+ if (options.state) params.set("state", options.state);
99
+ if (options.scope) params.set("scope", options.scope);
100
+ if (options.search) params.set("search", options.search);
101
+ if (options.labels) params.set("labels", options.labels);
102
+ let path2;
103
+ if (options.projectId) {
104
+ const encodedProject = this.encodeProjectId(options.projectId);
105
+ path2 = `/projects/${encodedProject}/merge_requests?${params}`;
106
+ } else {
107
+ path2 = `/merge_requests?${params}`;
108
+ }
109
+ return this.fetch("GET", path2);
110
+ }
111
+ async getMrChanges(projectId, mrIid) {
112
+ const encodedProject = this.encodeProjectId(projectId);
113
+ return this.fetch(
114
+ "GET",
115
+ `/projects/${encodedProject}/merge_requests/${mrIid}/changes`
116
+ );
117
+ }
118
+ async listMrDiscussions(projectId, mrIid) {
119
+ const encodedProject = this.encodeProjectId(projectId);
120
+ return this.fetch(
121
+ "GET",
122
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions`
123
+ );
124
+ }
125
+ async getMrDiscussion(projectId, mrIid, discussionId) {
126
+ const encodedProject = this.encodeProjectId(projectId);
127
+ return this.fetch(
128
+ "GET",
129
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`
130
+ );
131
+ }
132
+ async resolveMrDiscussion(projectId, mrIid, discussionId) {
133
+ const encodedProject = this.encodeProjectId(projectId);
134
+ return this.fetch(
135
+ "PUT",
136
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`,
137
+ { resolved: true }
138
+ );
139
+ }
140
+ async unresolveMrDiscussion(projectId, mrIid, discussionId) {
141
+ const encodedProject = this.encodeProjectId(projectId);
142
+ return this.fetch(
143
+ "PUT",
144
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}`,
145
+ { resolved: false }
146
+ );
147
+ }
148
+ async createMrDiscussion(projectId, mrIid, body, position) {
149
+ const encodedProject = this.encodeProjectId(projectId);
150
+ const requestBody = { body };
151
+ if (position) {
152
+ requestBody.position = position;
153
+ }
154
+ return this.fetch(
155
+ "POST",
156
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions`,
157
+ requestBody
158
+ );
159
+ }
160
+ async listMrNotes(projectId, mrIid) {
161
+ const encodedProject = this.encodeProjectId(projectId);
162
+ return this.fetch(
163
+ "GET",
164
+ `/projects/${encodedProject}/merge_requests/${mrIid}/notes`
165
+ );
166
+ }
167
+ async createMrNote(projectId, mrIid, body, discussionId) {
168
+ const encodedProject = this.encodeProjectId(projectId);
169
+ if (discussionId) {
170
+ return this.fetch(
171
+ "POST",
172
+ `/projects/${encodedProject}/merge_requests/${mrIid}/discussions/${discussionId}/notes`,
173
+ { body }
174
+ );
175
+ }
176
+ return this.fetch(
177
+ "POST",
178
+ `/projects/${encodedProject}/merge_requests/${mrIid}/notes`,
179
+ { body }
180
+ );
181
+ }
182
+ async createMergeRequest(projectId, options) {
183
+ const encodedProject = this.encodeProjectId(projectId);
184
+ return this.fetch(
185
+ "POST",
186
+ `/projects/${encodedProject}/merge_requests`,
187
+ options
188
+ );
189
+ }
190
+ async updateMergeRequest(projectId, mrIid, options) {
191
+ const encodedProject = this.encodeProjectId(projectId);
192
+ return this.fetch(
193
+ "PUT",
194
+ `/projects/${encodedProject}/merge_requests/${mrIid}`,
195
+ options
196
+ );
197
+ }
198
+ async getMrCommits(projectId, mrIid) {
199
+ const encodedProject = this.encodeProjectId(projectId);
200
+ return this.fetch(
201
+ "GET",
202
+ `/projects/${encodedProject}/merge_requests/${mrIid}/commits`
203
+ );
204
+ }
205
+ async getMrPipelines(projectId, mrIid) {
206
+ const encodedProject = this.encodeProjectId(projectId);
207
+ return this.fetch(
208
+ "GET",
209
+ `/projects/${encodedProject}/merge_requests/${mrIid}/pipelines`
210
+ );
211
+ }
212
+ /**
213
+ * List merge request diffs with pagination support
214
+ * API: GET /projects/:id/merge_requests/:merge_request_iid/diffs
215
+ */
216
+ async listMergeRequestDiffs(projectId, mrIid, options = {}) {
217
+ const encodedProject = this.encodeProjectId(projectId);
218
+ const params = new URLSearchParams();
219
+ if (options.page) params.append("page", options.page.toString());
220
+ if (options.per_page) params.append("per_page", options.per_page.toString());
221
+ const query = params.toString();
222
+ const path2 = `/projects/${encodedProject}/merge_requests/${mrIid}/diffs${query ? `?${query}` : ""}`;
223
+ return this.fetch("GET", path2);
224
+ }
225
+ };
226
+
227
+ // src/client/issues.ts
228
+ var IssuesClient = class extends GitLabApiClient {
229
+ async createIssue(projectId, title, options) {
230
+ const encodedProject = this.encodeProjectId(projectId);
231
+ const body = {
232
+ title,
233
+ ...Object.fromEntries(
234
+ Object.entries(options || {}).filter(([_, value]) => value !== void 0)
235
+ )
236
+ };
237
+ return this.fetch("POST", `/projects/${encodedProject}/issues`, body);
238
+ }
239
+ async getIssue(projectId, issueIid) {
240
+ const encodedProject = this.encodeProjectId(projectId);
241
+ return this.fetch(
242
+ "GET",
243
+ `/projects/${encodedProject}/issues/${issueIid}`
244
+ );
245
+ }
246
+ async listIssues(options) {
247
+ const params = new URLSearchParams();
248
+ params.set("per_page", String(options.limit || 20));
249
+ if (options.state) params.set("state", options.state);
250
+ if (options.scope) params.set("scope", options.scope);
251
+ if (options.search) params.set("search", options.search);
252
+ if (options.labels) params.set("labels", options.labels);
253
+ if (options.milestone) params.set("milestone", options.milestone);
254
+ let path2;
255
+ if (options.projectId) {
256
+ const encodedProject = this.encodeProjectId(options.projectId);
257
+ path2 = `/projects/${encodedProject}/issues?${params}`;
258
+ } else {
259
+ path2 = `/issues?${params}`;
260
+ }
261
+ return this.fetch("GET", path2);
262
+ }
263
+ async listIssueNotes(projectId, issueIid) {
264
+ const encodedProject = this.encodeProjectId(projectId);
265
+ return this.fetch(
266
+ "GET",
267
+ `/projects/${encodedProject}/issues/${issueIid}/notes`
268
+ );
269
+ }
270
+ async listIssueDiscussions(projectId, issueIid) {
271
+ const encodedProject = this.encodeProjectId(projectId);
272
+ return this.fetch(
273
+ "GET",
274
+ `/projects/${encodedProject}/issues/${issueIid}/discussions`
275
+ );
276
+ }
277
+ async getIssueDiscussion(projectId, issueIid, discussionId) {
278
+ const encodedProject = this.encodeProjectId(projectId);
279
+ return this.fetch(
280
+ "GET",
281
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`
282
+ );
283
+ }
284
+ async createIssueNote(projectId, issueIid, body, discussionId) {
285
+ const encodedProject = this.encodeProjectId(projectId);
286
+ if (discussionId) {
287
+ return this.fetch(
288
+ "POST",
289
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}/notes`,
290
+ { body }
291
+ );
292
+ }
293
+ return this.fetch(
294
+ "POST",
295
+ `/projects/${encodedProject}/issues/${issueIid}/notes`,
296
+ { body }
297
+ );
298
+ }
299
+ async resolveIssueDiscussion(projectId, issueIid, discussionId) {
300
+ const encodedProject = this.encodeProjectId(projectId);
301
+ return this.fetch(
302
+ "PUT",
303
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`,
304
+ { resolved: true }
305
+ );
306
+ }
307
+ async unresolveIssueDiscussion(projectId, issueIid, discussionId) {
308
+ const encodedProject = this.encodeProjectId(projectId);
309
+ return this.fetch(
310
+ "PUT",
311
+ `/projects/${encodedProject}/issues/${issueIid}/discussions/${discussionId}`,
312
+ { resolved: false }
313
+ );
314
+ }
315
+ /**
316
+ * Get a single note from an issue
317
+ * API: GET /projects/:id/issues/:issue_iid/notes/:note_id
318
+ */
319
+ async getIssueNote(projectId, issueIid, noteId) {
320
+ const encodedProject = this.encodeProjectId(projectId);
321
+ return this.fetch(
322
+ "GET",
323
+ `/projects/${encodedProject}/issues/${issueIid}/notes/${noteId}`
324
+ );
325
+ }
326
+ };
327
+
328
+ // src/client/work-items.ts
329
+ var WorkItemsClient = class extends GitLabApiClient {
330
+ async getWorkItem(projectId, workItemId) {
331
+ const encodedProject = this.encodeProjectId(projectId);
332
+ return this.fetch(
333
+ "GET",
334
+ `/projects/${encodedProject}/work_items/${workItemId}`
335
+ );
336
+ }
337
+ async listWorkItems(options) {
338
+ const params = new URLSearchParams();
339
+ params.set("per_page", String(options.limit || 20));
340
+ if (options.state) params.set("state", options.state);
341
+ if (options.search) params.set("search", options.search);
342
+ if (options.labels) params.set("labels", options.labels);
343
+ if (options.work_item_type) params.set("type", options.work_item_type);
344
+ let path2;
345
+ if (options.projectId) {
346
+ const encodedProject = this.encodeProjectId(options.projectId);
347
+ path2 = `/projects/${encodedProject}/work_items?${params}`;
348
+ } else if (options.groupId) {
349
+ const encodedGroup = encodeURIComponent(options.groupId);
350
+ path2 = `/groups/${encodedGroup}/work_items?${params}`;
351
+ } else {
352
+ throw new Error("Either projectId or groupId must be provided");
353
+ }
354
+ return this.fetch("GET", path2);
355
+ }
356
+ async getWorkItemNotes(projectId, workItemId) {
357
+ const encodedProject = this.encodeProjectId(projectId);
358
+ return this.fetch(
359
+ "GET",
360
+ `/projects/${encodedProject}/work_items/${workItemId}/notes`
361
+ );
362
+ }
363
+ async createWorkItem(projectId, options) {
364
+ const encodedProject = this.encodeProjectId(projectId);
365
+ return this.fetch(
366
+ "POST",
367
+ `/projects/${encodedProject}/work_items`,
368
+ options
369
+ );
370
+ }
371
+ async updateWorkItem(projectId, workItemId, options) {
372
+ const encodedProject = this.encodeProjectId(projectId);
373
+ return this.fetch(
374
+ "PUT",
375
+ `/projects/${encodedProject}/work_items/${workItemId}`,
376
+ options
377
+ );
378
+ }
379
+ async createWorkItemNote(projectId, workItemId, body) {
380
+ const encodedProject = this.encodeProjectId(projectId);
381
+ return this.fetch(
382
+ "POST",
383
+ `/projects/${encodedProject}/work_items/${workItemId}/notes`,
384
+ { body }
385
+ );
386
+ }
387
+ };
388
+
389
+ // src/client/pipelines.ts
390
+ var PipelinesClient = class extends GitLabApiClient {
391
+ async listPipelines(projectId, options) {
392
+ const encodedProject = this.encodeProjectId(projectId);
393
+ const params = new URLSearchParams();
394
+ params.set("per_page", String(options?.limit || 20));
395
+ if (options?.status) params.set("status", options.status);
396
+ if (options?.ref) params.set("ref", options.ref);
397
+ return this.fetch(
398
+ "GET",
399
+ `/projects/${encodedProject}/pipelines?${params}`
400
+ );
401
+ }
402
+ async getPipeline(projectId, pipelineId) {
403
+ const encodedProject = this.encodeProjectId(projectId);
404
+ return this.fetch(
405
+ "GET",
406
+ `/projects/${encodedProject}/pipelines/${pipelineId}`
407
+ );
408
+ }
409
+ async listPipelineJobs(projectId, pipelineId, scope) {
410
+ const encodedProject = this.encodeProjectId(projectId);
411
+ const params = new URLSearchParams();
412
+ if (scope) params.set("scope[]", scope);
413
+ return this.fetch(
414
+ "GET",
415
+ `/projects/${encodedProject}/pipelines/${pipelineId}/jobs?${params}`
416
+ );
417
+ }
418
+ async getJobLog(projectId, jobId) {
419
+ const encodedProject = this.encodeProjectId(projectId);
420
+ const log = await this.fetchText("GET", `/projects/${encodedProject}/jobs/${jobId}/trace`);
421
+ const maxLength = 5e4;
422
+ if (log.length > maxLength) {
423
+ return `[Log truncated, showing last ${maxLength} characters]
424
+
425
+ ${log.slice(-maxLength)}`;
426
+ }
427
+ return log;
428
+ }
429
+ async retryJob(projectId, jobId) {
430
+ const encodedProject = this.encodeProjectId(projectId);
431
+ return this.fetch(
432
+ "POST",
433
+ `/projects/${encodedProject}/jobs/${jobId}/retry`
434
+ );
435
+ }
436
+ async getPipelineFailingJobs(projectId, pipelineId) {
437
+ const encodedProject = this.encodeProjectId(projectId);
438
+ const jobs = await this.fetch(
439
+ "GET",
440
+ `/projects/${encodedProject}/pipelines/${pipelineId}/jobs?scope[]=failed`
441
+ );
442
+ return jobs;
443
+ }
444
+ /**
445
+ * Validate a CI/CD configuration
446
+ * @param projectId - The project ID or URL-encoded path
447
+ * @param content - The CI/CD configuration content (YAML as string)
448
+ * @param options - Optional parameters
449
+ * @param options.dry_run - Run pipeline creation simulation (default: false)
450
+ * @param options.include_jobs - Include list of jobs in response (default: false)
451
+ * @param options.ref - Branch or tag context for validation (defaults to project's default branch)
452
+ * @returns Validation result with errors, warnings, and optionally merged YAML and jobs
453
+ */
454
+ async lintCiConfig(projectId, content, options) {
455
+ const encodedProject = this.encodeProjectId(projectId);
456
+ const body = {
457
+ content
458
+ };
459
+ if (options?.dry_run !== void 0) body.dry_run = options.dry_run;
460
+ if (options?.include_jobs !== void 0) body.include_jobs = options.include_jobs;
461
+ if (options?.ref) body.ref = options.ref;
462
+ return this.fetch("POST", `/projects/${encodedProject}/ci/lint`, body);
463
+ }
464
+ /**
465
+ * Validate an existing CI/CD configuration from the repository
466
+ * @param projectId - The project ID or URL-encoded path
467
+ * @param options - Optional parameters
468
+ * @param options.content_ref - Commit SHA, branch or tag to get CI config from (defaults to default branch)
469
+ * @param options.dry_run - Run pipeline creation simulation (default: false)
470
+ * @param options.dry_run_ref - Branch or tag context for validation (defaults to project's default branch)
471
+ * @param options.include_jobs - Include list of jobs in response (default: false)
472
+ * @returns Validation result with errors, warnings, and optionally merged YAML and jobs
473
+ */
474
+ async lintExistingCiConfig(projectId, options) {
475
+ const encodedProject = this.encodeProjectId(projectId);
476
+ const params = new URLSearchParams();
477
+ if (options?.content_ref) params.set("content_ref", options.content_ref);
478
+ if (options?.dry_run !== void 0) params.set("dry_run", String(options.dry_run));
479
+ if (options?.dry_run_ref) params.set("dry_run_ref", options.dry_run_ref);
480
+ if (options?.include_jobs !== void 0)
481
+ params.set("include_jobs", String(options.include_jobs));
482
+ const queryString = params.toString();
483
+ const path2 = queryString ? `/projects/${encodedProject}/ci/lint?${queryString}` : `/projects/${encodedProject}/ci/lint`;
484
+ return this.fetch("GET", path2);
485
+ }
486
+ };
487
+
488
+ // src/client/repository.ts
489
+ var RepositoryClient = class extends GitLabApiClient {
490
+ async getFile(projectId, filePath, ref) {
491
+ const encodedProject = this.encodeProjectId(projectId);
492
+ const encodedPath = encodeURIComponent(filePath);
493
+ let url = `/projects/${encodedProject}/repository/files/${encodedPath}`;
494
+ if (ref) {
495
+ url += `?ref=${encodeURIComponent(ref)}`;
496
+ }
497
+ const file = await this.fetch("GET", url);
498
+ if (file.encoding === "base64") {
499
+ return Buffer.from(file.content, "base64").toString("utf-8");
500
+ }
501
+ return file.content;
502
+ }
503
+ async listCommits(projectId, options) {
504
+ const encodedProject = this.encodeProjectId(projectId);
505
+ const params = new URLSearchParams();
506
+ params.set("per_page", String(options?.limit || 20));
507
+ if (options?.ref) params.set("ref_name", options.ref);
508
+ if (options?.path) params.set("path", options.path);
509
+ if (options?.since) params.set("since", options.since);
510
+ if (options?.until) params.set("until", options.until);
511
+ return this.fetch(
512
+ "GET",
513
+ `/projects/${encodedProject}/repository/commits?${params}`
514
+ );
515
+ }
516
+ async getCommit(projectId, sha) {
517
+ const encodedProject = this.encodeProjectId(projectId);
518
+ return this.fetch(
519
+ "GET",
520
+ `/projects/${encodedProject}/repository/commits/${sha}`
521
+ );
522
+ }
523
+ async getCommitDiff(projectId, sha) {
524
+ const encodedProject = this.encodeProjectId(projectId);
525
+ return this.fetch(
526
+ "GET",
527
+ `/projects/${encodedProject}/repository/commits/${sha}/diff`
528
+ );
529
+ }
530
+ async createCommit(projectId, options) {
531
+ const encodedProject = this.encodeProjectId(projectId);
532
+ return this.fetch(
533
+ "POST",
534
+ `/projects/${encodedProject}/repository/commits`,
535
+ options
536
+ );
537
+ }
538
+ async listRepositoryTree(projectId, options) {
539
+ const encodedProject = this.encodeProjectId(projectId);
540
+ const params = new URLSearchParams();
541
+ if (options?.path) params.set("path", options.path);
542
+ if (options?.ref) params.set("ref", options.ref);
543
+ if (options?.recursive) params.set("recursive", "true");
544
+ if (options?.per_page) params.set("per_page", String(options.per_page));
545
+ return this.fetch(
546
+ "GET",
547
+ `/projects/${encodedProject}/repository/tree?${params}`
548
+ );
549
+ }
550
+ async listBranches(projectId, search) {
551
+ const encodedProject = this.encodeProjectId(projectId);
552
+ const params = new URLSearchParams();
553
+ if (search) params.set("search", search);
554
+ return this.fetch(
555
+ "GET",
556
+ `/projects/${encodedProject}/repository/branches?${params}`
557
+ );
558
+ }
559
+ // ========== Commit Discussion Methods ==========
560
+ async listCommitDiscussions(projectId, sha) {
561
+ const encodedProject = this.encodeProjectId(projectId);
562
+ return this.fetch(
563
+ "GET",
564
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions`
565
+ );
566
+ }
567
+ async getCommitDiscussion(projectId, sha, discussionId) {
568
+ const encodedProject = this.encodeProjectId(projectId);
569
+ return this.fetch(
570
+ "GET",
571
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions/${discussionId}`
572
+ );
573
+ }
574
+ async createCommitNote(projectId, sha, body, options) {
575
+ const encodedProject = this.encodeProjectId(projectId);
576
+ if (options?.discussion_id) {
577
+ return this.fetch(
578
+ "POST",
579
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions/${options.discussion_id}/notes`,
580
+ { body }
581
+ );
582
+ }
583
+ const requestBody = { body };
584
+ if (options?.path) requestBody.path = options.path;
585
+ if (options?.line) requestBody.line = options.line;
586
+ if (options?.line_type) requestBody.line_type = options.line_type;
587
+ return this.fetch(
588
+ "POST",
589
+ `/projects/${encodedProject}/repository/commits/${sha}/comments`,
590
+ requestBody
591
+ );
592
+ }
593
+ async createCommitDiscussion(projectId, sha, body, position) {
594
+ const encodedProject = this.encodeProjectId(projectId);
595
+ const requestBody = { body };
596
+ if (position) {
597
+ requestBody.position = position;
598
+ }
599
+ return this.fetch(
600
+ "POST",
601
+ `/projects/${encodedProject}/repository/commits/${sha}/discussions`,
602
+ requestBody
603
+ );
604
+ }
605
+ /**
606
+ * Get comments on a specific commit
607
+ * API: GET /projects/:id/repository/commits/:sha/comments
608
+ */
609
+ async getCommitComments(projectId, sha) {
610
+ const encodedProject = this.encodeProjectId(projectId);
611
+ return this.fetch(
612
+ "GET",
613
+ `/projects/${encodedProject}/repository/commits/${sha}/comments`
614
+ );
615
+ }
616
+ };
617
+
618
+ // src/client/search.ts
619
+ var SearchClient = class extends GitLabApiClient {
620
+ async search(scope, searchQuery, projectId, limit) {
621
+ const params = new URLSearchParams();
622
+ params.set("scope", scope);
623
+ params.set("search", searchQuery);
624
+ params.set("per_page", String(limit || 20));
625
+ let path2;
626
+ if (projectId) {
627
+ const encodedProject = this.encodeProjectId(projectId);
628
+ path2 = `/projects/${encodedProject}/search?${params}`;
629
+ } else {
630
+ path2 = `/search?${params}`;
631
+ }
632
+ return this.fetch("GET", path2);
633
+ }
634
+ /**
635
+ * Search for commits in a project or globally
636
+ * @param searchQuery - The search term
637
+ * @param projectId - Optional project ID to limit search
638
+ * @param options - Optional search parameters
639
+ */
640
+ async searchCommits(searchQuery, projectId, options) {
641
+ const params = new URLSearchParams();
642
+ params.set("scope", "commits");
643
+ params.set("search", searchQuery);
644
+ params.set("per_page", String(options?.limit || 20));
645
+ if (options?.ref) params.set("ref", options.ref);
646
+ if (options?.order_by) params.set("order_by", options.order_by);
647
+ if (options?.sort) params.set("sort", options.sort);
648
+ let path2;
649
+ if (projectId) {
650
+ const encodedProject = this.encodeProjectId(projectId);
651
+ path2 = `/projects/${encodedProject}/search?${params}`;
652
+ } else {
653
+ path2 = `/search?${params}`;
654
+ }
655
+ return this.fetch("GET", path2);
656
+ }
657
+ /**
658
+ * Search for projects within a specific group
659
+ * @param groupId - The group ID or URL-encoded path
660
+ * @param searchQuery - The search term
661
+ * @param options - Optional search parameters
662
+ */
663
+ async searchGroupProjects(groupId, searchQuery, options) {
664
+ const encodedGroup = this.encodeProjectId(groupId);
665
+ const params = new URLSearchParams();
666
+ params.set("scope", "projects");
667
+ params.set("search", searchQuery);
668
+ params.set("per_page", String(options?.limit || 20));
669
+ if (options?.order_by) params.set("order_by", options.order_by);
670
+ if (options?.sort) params.set("sort", options.sort);
671
+ return this.fetch("GET", `/groups/${encodedGroup}/search?${params}`);
672
+ }
673
+ /**
674
+ * Search for milestones in a project or globally
675
+ * @param searchQuery - The search term
676
+ * @param projectId - Optional project ID to limit search
677
+ * @param options - Optional search parameters
678
+ */
679
+ async searchMilestones(searchQuery, projectId, options) {
680
+ const params = new URLSearchParams();
681
+ params.set("scope", "milestones");
682
+ params.set("search", searchQuery);
683
+ params.set("per_page", String(options?.limit || 20));
684
+ if (options?.state) params.set("state", options.state);
685
+ if (options?.order_by) params.set("order_by", options.order_by);
686
+ if (options?.sort) params.set("sort", options.sort);
687
+ let path2;
688
+ if (projectId) {
689
+ const encodedProject = this.encodeProjectId(projectId);
690
+ path2 = `/projects/${encodedProject}/search?${params}`;
691
+ } else {
692
+ path2 = `/search?${params}`;
693
+ }
694
+ return this.fetch("GET", path2);
695
+ }
696
+ /**
697
+ * Search for notes/comments in a project
698
+ * @param searchQuery - The search term
699
+ * @param projectId - The project ID to search in
700
+ * @param options - Optional search parameters
701
+ */
702
+ async searchNotes(searchQuery, projectId, options) {
703
+ const encodedProject = this.encodeProjectId(projectId);
704
+ const params = new URLSearchParams();
705
+ params.set("scope", "notes");
706
+ params.set("search", searchQuery);
707
+ params.set("per_page", String(options?.limit || 20));
708
+ if (options?.order_by) params.set("order_by", options.order_by);
709
+ if (options?.sort) params.set("sort", options.sort);
710
+ return this.fetch(
711
+ "GET",
712
+ `/projects/${encodedProject}/search?${params}`
713
+ );
714
+ }
715
+ /**
716
+ * Search for users by name or email
717
+ * @param searchQuery - The search term (name or email)
718
+ * @param projectId - Optional project ID to limit search
719
+ * @param options - Optional search parameters
720
+ */
721
+ async searchUsers(searchQuery, projectId, options) {
722
+ const params = new URLSearchParams();
723
+ params.set("scope", "users");
724
+ params.set("search", searchQuery);
725
+ params.set("per_page", String(options?.limit || 20));
726
+ if (options?.order_by) params.set("order_by", options.order_by);
727
+ if (options?.sort) params.set("sort", options.sort);
728
+ let path2;
729
+ if (projectId) {
730
+ const encodedProject = this.encodeProjectId(projectId);
731
+ path2 = `/projects/${encodedProject}/search?${params}`;
732
+ } else {
733
+ path2 = `/search?${params}`;
734
+ }
735
+ return this.fetch("GET", path2);
736
+ }
737
+ /**
738
+ * Search for wiki blobs (wiki content)
739
+ * @param searchQuery - The search term (supports filters like filename:, path:, extension:)
740
+ * @param projectId - Optional project ID to limit search
741
+ * @param options - Optional search parameters
742
+ */
743
+ async searchWikiBlobs(searchQuery, projectId, options) {
744
+ const params = new URLSearchParams();
745
+ params.set("scope", "wiki_blobs");
746
+ params.set("search", searchQuery);
747
+ params.set("per_page", String(options?.limit || 20));
748
+ if (options?.ref) params.set("ref", options.ref);
749
+ if (options?.order_by) params.set("order_by", options.order_by);
750
+ if (options?.sort) params.set("sort", options.sort);
751
+ let path2;
752
+ if (projectId) {
753
+ const encodedProject = this.encodeProjectId(projectId);
754
+ path2 = `/projects/${encodedProject}/search?${params}`;
755
+ } else {
756
+ path2 = `/search?${params}`;
757
+ }
758
+ return this.fetch("GET", path2);
759
+ }
760
+ /**
761
+ * Search GitLab documentation
762
+ * Note: This uses the public GitLab docs search API
763
+ * @param searchQuery - The search term
764
+ * @param limit - Maximum number of results (default: 10)
765
+ */
766
+ async searchDocumentation(searchQuery, limit) {
767
+ const url = `https://docs.gitlab.com/search.json?q=${encodeURIComponent(searchQuery)}&limit=${limit || 10}`;
768
+ const response = await fetch(url);
769
+ if (!response.ok) {
770
+ throw new Error(`Documentation search error ${response.status}: ${await response.text()}`);
771
+ }
772
+ return response.json();
773
+ }
774
+ };
775
+
776
+ // src/client/projects.ts
777
+ var ProjectsClient = class extends GitLabApiClient {
778
+ async getProject(projectId) {
779
+ const encodedProject = this.encodeProjectId(projectId);
780
+ return this.fetch("GET", `/projects/${encodedProject}`);
781
+ }
782
+ async listProjectMembers(projectId) {
783
+ const encodedProject = this.encodeProjectId(projectId);
784
+ return this.fetch("GET", `/projects/${encodedProject}/members`);
785
+ }
786
+ };
787
+
788
+ // src/client/users.ts
789
+ var UsersClient = class extends GitLabApiClient {
790
+ async getCurrentUser() {
791
+ return this.fetch("GET", "/user");
792
+ }
793
+ };
794
+
795
+ // src/client/wikis.ts
796
+ var WikisClient = class extends GitLabApiClient {
797
+ async getWikiPage(projectId, slug) {
798
+ const encodedProject = this.encodeProjectId(projectId);
799
+ const encodedSlug = encodeURIComponent(slug);
800
+ return this.fetch(
801
+ "GET",
802
+ `/projects/${encodedProject}/wikis/${encodedSlug}`
803
+ );
804
+ }
805
+ };
806
+
807
+ // src/validation.ts
808
+ function isValidGid(gid, expectedType) {
809
+ const gidPattern = /^gid:\/\/gitlab\/([A-Za-z]+)\/(\d+)$/;
810
+ const match = gid.match(gidPattern);
811
+ if (!match) {
812
+ return false;
813
+ }
814
+ if (expectedType && match[1] !== expectedType) {
815
+ return false;
816
+ }
817
+ return true;
818
+ }
819
+ function validateGid(gid, expectedType) {
820
+ if (!isValidGid(gid, expectedType)) {
821
+ const typeMsg = expectedType ? ` of type '${expectedType}'` : "";
822
+ throw new Error(
823
+ `Invalid GitLab Global ID${typeMsg}: '${gid}'. Expected format: gid://gitlab/${expectedType || "ResourceType"}/{id}`
824
+ );
825
+ }
826
+ }
827
+
828
+ // src/client/security.ts
829
+ var CREATE_VULNERABILITY_ISSUE_MUTATION = `
830
+ mutation($projectPath: ID!, $vulnerabilityIds: [VulnerabilityID!]!) {
831
+ createVulnerabilityIssueLink(input: {
832
+ projectPath: $projectPath
833
+ vulnerabilityIds: $vulnerabilityIds
834
+ }) {
835
+ issue {
836
+ id
837
+ iid
838
+ title
839
+ webUrl
840
+ }
841
+ errors
842
+ }
843
+ }
844
+ `;
845
+ var DISMISS_VULNERABILITY_MUTATION = `
846
+ mutation($id: VulnerabilityID!, $reason: VulnerabilityDismissalReason!, $comment: String) {
847
+ vulnerabilityDismiss(input: {
848
+ id: $id
849
+ dismissalReason: $reason
850
+ comment: $comment
851
+ }) {
852
+ vulnerability {
853
+ id
854
+ state
855
+ dismissalReason
856
+ }
857
+ errors
858
+ }
859
+ }
860
+ `;
861
+ var CONFIRM_VULNERABILITY_MUTATION = `
862
+ mutation($id: VulnerabilityID!, $comment: String) {
863
+ vulnerabilityConfirm(input: {
864
+ id: $id
865
+ comment: $comment
866
+ }) {
867
+ vulnerability {
868
+ id
869
+ state
870
+ }
871
+ errors
872
+ }
873
+ }
874
+ `;
875
+ var REVERT_VULNERABILITY_MUTATION = `
876
+ mutation($id: VulnerabilityID!, $comment: String) {
877
+ vulnerabilityRevertToDetected(input: {
878
+ id: $id
879
+ comment: $comment
880
+ }) {
881
+ vulnerability {
882
+ id
883
+ state
884
+ }
885
+ errors
886
+ }
887
+ }
888
+ `;
889
+ var UPDATE_VULNERABILITY_SEVERITY_MUTATION = `
890
+ mutation($ids: [VulnerabilityID!]!, $severity: VulnerabilitySeverity!, $comment: String!) {
891
+ vulnerabilitiesUpdateSeverity(input: {
892
+ ids: $ids
893
+ severity: $severity
894
+ comment: $comment
895
+ }) {
896
+ vulnerabilities {
897
+ id
898
+ severity
899
+ }
900
+ errors
901
+ }
902
+ }
903
+ `;
904
+ var LINK_VULNERABILITY_TO_ISSUE_MUTATION = `
905
+ mutation($issueId: IssueID!, $vulnerabilityIds: [VulnerabilityID!]!) {
906
+ vulnerabilityIssueLinksCreate(input: {
907
+ issueId: $issueId
908
+ vulnerabilityIds: $vulnerabilityIds
909
+ }) {
910
+ issue {
911
+ id
912
+ iid
913
+ title
914
+ webUrl
915
+ }
916
+ errors
917
+ }
918
+ }
919
+ `;
920
+ var SecurityClient = class extends GitLabApiClient {
921
+ async listVulnerabilities(projectId, options) {
922
+ const encodedProject = this.encodeProjectId(projectId);
923
+ const params = new URLSearchParams();
924
+ params.set("per_page", String(options?.limit || 20));
925
+ if (options?.state) params.set("state", options.state);
926
+ if (options?.severity) params.set("severity", options.severity);
927
+ if (options?.report_type) params.set("report_type", options.report_type);
928
+ return this.fetch(
929
+ "GET",
930
+ `/projects/${encodedProject}/vulnerabilities?${params}`
931
+ );
932
+ }
933
+ async getVulnerabilityDetails(projectId, vulnerabilityId) {
934
+ const encodedProject = this.encodeProjectId(projectId);
935
+ return this.fetch(
936
+ "GET",
937
+ `/projects/${encodedProject}/vulnerabilities/${vulnerabilityId}`
938
+ );
939
+ }
940
+ /**
941
+ * Create an issue linked to one or more vulnerabilities
942
+ * @param projectPath - Full path of the project (e.g., 'group/project')
943
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
944
+ * @returns The created issue
945
+ */
946
+ async createVulnerabilityIssue(projectPath, vulnerabilityIds) {
947
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
948
+ const result = await this.fetchGraphQL(CREATE_VULNERABILITY_ISSUE_MUTATION, { projectPath, vulnerabilityIds });
949
+ if (result.createVulnerabilityIssueLink.errors.length > 0) {
950
+ throw new Error(
951
+ `Failed to create vulnerability issue: ${result.createVulnerabilityIssueLink.errors.join(", ")}`
952
+ );
953
+ }
954
+ return result.createVulnerabilityIssueLink.issue;
955
+ }
956
+ /**
957
+ * Dismiss a vulnerability with a reason
958
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
959
+ * @param reason - Dismissal reason
960
+ * @param comment - Optional comment explaining the dismissal
961
+ * @returns The updated vulnerability
962
+ */
963
+ async dismissVulnerability(vulnerabilityId, reason, comment) {
964
+ validateGid(vulnerabilityId, "Vulnerability");
965
+ const result = await this.fetchGraphQL(DISMISS_VULNERABILITY_MUTATION, { id: vulnerabilityId, reason, comment });
966
+ if (result.vulnerabilityDismiss.errors.length > 0) {
967
+ throw new Error(
968
+ `Failed to dismiss vulnerability: ${result.vulnerabilityDismiss.errors.join(", ")}`
969
+ );
970
+ }
971
+ return result.vulnerabilityDismiss.vulnerability;
972
+ }
973
+ /**
974
+ * Confirm a vulnerability
975
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
976
+ * @param comment - Optional comment
977
+ * @returns The updated vulnerability
978
+ */
979
+ async confirmVulnerability(vulnerabilityId, comment) {
980
+ validateGid(vulnerabilityId, "Vulnerability");
981
+ const result = await this.fetchGraphQL(CONFIRM_VULNERABILITY_MUTATION, { id: vulnerabilityId, comment });
982
+ if (result.vulnerabilityConfirm.errors.length > 0) {
983
+ throw new Error(
984
+ `Failed to confirm vulnerability: ${result.vulnerabilityConfirm.errors.join(", ")}`
985
+ );
986
+ }
987
+ return result.vulnerabilityConfirm.vulnerability;
988
+ }
989
+ /**
990
+ * Revert a vulnerability back to detected state
991
+ * @param vulnerabilityId - Vulnerability ID in format 'gid://gitlab/Vulnerability/{id}'
992
+ * @param comment - Optional comment
993
+ * @returns The updated vulnerability
994
+ */
995
+ async revertVulnerability(vulnerabilityId, comment) {
996
+ validateGid(vulnerabilityId, "Vulnerability");
997
+ const result = await this.fetchGraphQL(REVERT_VULNERABILITY_MUTATION, { id: vulnerabilityId, comment });
998
+ if (result.vulnerabilityRevertToDetected.errors.length > 0) {
999
+ throw new Error(
1000
+ `Failed to revert vulnerability: ${result.vulnerabilityRevertToDetected.errors.join(", ")}`
1001
+ );
1002
+ }
1003
+ return result.vulnerabilityRevertToDetected.vulnerability;
1004
+ }
1005
+ /**
1006
+ * Update the severity of one or more vulnerabilities
1007
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
1008
+ * @param severity - New severity level
1009
+ * @param comment - Comment explaining the severity change
1010
+ * @returns Array of updated vulnerabilities
1011
+ */
1012
+ async updateVulnerabilitySeverity(vulnerabilityIds, severity, comment) {
1013
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
1014
+ const result = await this.fetchGraphQL(UPDATE_VULNERABILITY_SEVERITY_MUTATION, { ids: vulnerabilityIds, severity, comment });
1015
+ if (result.vulnerabilitiesUpdateSeverity.errors.length > 0) {
1016
+ throw new Error(
1017
+ `Failed to update vulnerability severity: ${result.vulnerabilitiesUpdateSeverity.errors.join(", ")}`
1018
+ );
1019
+ }
1020
+ return result.vulnerabilitiesUpdateSeverity.vulnerabilities;
1021
+ }
1022
+ /**
1023
+ * Link an existing issue to one or more vulnerabilities
1024
+ * @param issueId - Issue ID in format 'gid://gitlab/Issue/{id}'
1025
+ * @param vulnerabilityIds - Array of vulnerability IDs in format 'gid://gitlab/Vulnerability/{id}'
1026
+ * @returns The linked issue
1027
+ */
1028
+ async linkVulnerabilityToIssue(issueId, vulnerabilityIds) {
1029
+ validateGid(issueId, "Issue");
1030
+ vulnerabilityIds.forEach((id) => validateGid(id, "Vulnerability"));
1031
+ const result = await this.fetchGraphQL(LINK_VULNERABILITY_TO_ISSUE_MUTATION, { issueId, vulnerabilityIds });
1032
+ if (result.vulnerabilityIssueLinksCreate.errors.length > 0) {
1033
+ throw new Error(
1034
+ `Failed to link vulnerability to issue: ${result.vulnerabilityIssueLinksCreate.errors.join(", ")}`
1035
+ );
1036
+ }
1037
+ return result.vulnerabilityIssueLinksCreate.issue;
1038
+ }
1039
+ };
1040
+
1041
+ // src/client/todos.ts
1042
+ var TodosClient = class extends GitLabApiClient {
1043
+ async listTodos(options) {
1044
+ const params = new URLSearchParams();
1045
+ params.set("per_page", String(options?.limit || 20));
1046
+ if (options?.action) params.set("action", options.action);
1047
+ if (options?.author_id) params.set("author_id", String(options.author_id));
1048
+ if (options?.project_id) {
1049
+ const encodedProject = this.encodeProjectId(options.project_id);
1050
+ params.set("project_id", encodedProject);
1051
+ }
1052
+ if (options?.group_id) params.set("group_id", options.group_id);
1053
+ if (options?.state) params.set("state", options.state);
1054
+ if (options?.type) params.set("type", options.type);
1055
+ return this.fetch("GET", `/todos?${params}`);
1056
+ }
1057
+ async markTodoAsDone(todoId) {
1058
+ return this.fetch("POST", `/todos/${todoId}/mark_as_done`);
1059
+ }
1060
+ async markAllTodosAsDone() {
1061
+ return this.fetch("POST", "/todos/mark_as_done");
1062
+ }
1063
+ async getTodoCount() {
1064
+ return this.fetch("GET", "/todos/count");
1065
+ }
1066
+ };
1067
+
1068
+ // src/client/epics.ts
1069
+ var EpicsClient = class extends GitLabApiClient {
1070
+ async getEpic(groupId, epicIid) {
1071
+ const encodedGroup = encodeURIComponent(groupId);
1072
+ return this.fetch("GET", `/groups/${encodedGroup}/epics/${epicIid}`);
1073
+ }
1074
+ async listEpics(options) {
1075
+ const encodedGroup = encodeURIComponent(options.groupId);
1076
+ const params = new URLSearchParams();
1077
+ params.set("per_page", String(options.limit || 20));
1078
+ if (options.state) params.set("state", options.state);
1079
+ if (options.author_id) params.set("author_id", String(options.author_id));
1080
+ if (options.labels) params.set("labels", options.labels);
1081
+ if (options.search) params.set("search", options.search);
1082
+ return this.fetch("GET", `/groups/${encodedGroup}/epics?${params}`);
1083
+ }
1084
+ async createEpic(groupId, options) {
1085
+ const encodedGroup = encodeURIComponent(groupId);
1086
+ return this.fetch("POST", `/groups/${encodedGroup}/epics`, options);
1087
+ }
1088
+ async updateEpic(groupId, epicIid, options) {
1089
+ const encodedGroup = encodeURIComponent(groupId);
1090
+ return this.fetch(
1091
+ "PUT",
1092
+ `/groups/${encodedGroup}/epics/${epicIid}`,
1093
+ options
1094
+ );
1095
+ }
1096
+ async listEpicIssues(groupId, epicIid) {
1097
+ const encodedGroup = encodeURIComponent(groupId);
1098
+ return this.fetch(
1099
+ "GET",
1100
+ `/groups/${encodedGroup}/epics/${epicIid}/issues`
1101
+ );
1102
+ }
1103
+ async addIssueToEpic(groupId, epicIid, issueId) {
1104
+ const encodedGroup = encodeURIComponent(groupId);
1105
+ return this.fetch(
1106
+ "POST",
1107
+ `/groups/${encodedGroup}/epics/${epicIid}/issues/${issueId}`
1108
+ );
1109
+ }
1110
+ async removeIssueFromEpic(groupId, epicIid, epicIssueId) {
1111
+ const encodedGroup = encodeURIComponent(groupId);
1112
+ return this.fetch(
1113
+ "DELETE",
1114
+ `/groups/${encodedGroup}/epics/${epicIid}/issues/${epicIssueId}`
1115
+ );
1116
+ }
1117
+ async listEpicNotes(groupId, epicIid) {
1118
+ const encodedGroup = encodeURIComponent(groupId);
1119
+ return this.fetch(
1120
+ "GET",
1121
+ `/groups/${encodedGroup}/epics/${epicIid}/notes`
1122
+ );
1123
+ }
1124
+ async listEpicDiscussions(groupId, epicIid) {
1125
+ const encodedGroup = encodeURIComponent(groupId);
1126
+ return this.fetch(
1127
+ "GET",
1128
+ `/groups/${encodedGroup}/epics/${epicIid}/discussions`
1129
+ );
1130
+ }
1131
+ async getEpicDiscussion(groupId, epicIid, discussionId) {
1132
+ const encodedGroup = encodeURIComponent(groupId);
1133
+ return this.fetch(
1134
+ "GET",
1135
+ `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}`
1136
+ );
1137
+ }
1138
+ async createEpicNote(groupId, epicIid, body, discussionId) {
1139
+ const encodedGroup = encodeURIComponent(groupId);
1140
+ if (discussionId) {
1141
+ return this.fetch(
1142
+ "POST",
1143
+ `/groups/${encodedGroup}/epics/${epicIid}/discussions/${discussionId}/notes`,
1144
+ { body }
1145
+ );
1146
+ }
1147
+ return this.fetch(
1148
+ "POST",
1149
+ `/groups/${encodedGroup}/epics/${epicIid}/notes`,
1150
+ { body }
1151
+ );
1152
+ }
1153
+ /**
1154
+ * Get a single note from an epic
1155
+ * API: GET /groups/:id/epics/:epic_iid/notes/:note_id
1156
+ */
1157
+ async getEpicNote(groupId, epicIid, noteId) {
1158
+ const encodedGroup = encodeURIComponent(groupId);
1159
+ return this.fetch(
1160
+ "GET",
1161
+ `/groups/${encodedGroup}/epics/${epicIid}/notes/${noteId}`
1162
+ );
1163
+ }
1164
+ };
1165
+
1166
+ // src/client/snippets.ts
1167
+ var SnippetsClient = class extends GitLabApiClient {
1168
+ async listSnippetDiscussions(projectId, snippetId) {
1169
+ const encodedProject = this.encodeProjectId(projectId);
1170
+ return this.fetch(
1171
+ "GET",
1172
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions`
1173
+ );
1174
+ }
1175
+ async getSnippetDiscussion(projectId, snippetId, discussionId) {
1176
+ const encodedProject = this.encodeProjectId(projectId);
1177
+ return this.fetch(
1178
+ "GET",
1179
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}`
1180
+ );
1181
+ }
1182
+ async listSnippetNotes(projectId, snippetId) {
1183
+ const encodedProject = this.encodeProjectId(projectId);
1184
+ return this.fetch(
1185
+ "GET",
1186
+ `/projects/${encodedProject}/snippets/${snippetId}/notes`
1187
+ );
1188
+ }
1189
+ async createSnippetNote(projectId, snippetId, body, discussionId) {
1190
+ const encodedProject = this.encodeProjectId(projectId);
1191
+ if (discussionId) {
1192
+ return this.fetch(
1193
+ "POST",
1194
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions/${discussionId}/notes`,
1195
+ { body }
1196
+ );
1197
+ }
1198
+ return this.fetch(
1199
+ "POST",
1200
+ `/projects/${encodedProject}/snippets/${snippetId}/notes`,
1201
+ { body }
1202
+ );
1203
+ }
1204
+ async createSnippetDiscussion(projectId, snippetId, body) {
1205
+ const encodedProject = this.encodeProjectId(projectId);
1206
+ return this.fetch(
1207
+ "POST",
1208
+ `/projects/${encodedProject}/snippets/${snippetId}/discussions`,
1209
+ { body }
1210
+ );
1211
+ }
1212
+ };
1213
+
1214
+ // src/client/discussions.ts
1215
+ var DiscussionsClient = class extends GitLabApiClient {
1216
+ /**
1217
+ * Universal method to reply to any discussion thread
1218
+ * Supports: merge requests, issues, epics, commits, snippets
1219
+ */
1220
+ async replyToDiscussion(resourceType, resourceId, discussionId, body) {
1221
+ switch (resourceType) {
1222
+ case "merge_request": {
1223
+ if (!resourceId.projectId || !resourceId.iid) {
1224
+ throw new Error("projectId and iid are required for merge_request discussions");
1225
+ }
1226
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1227
+ return this.fetch(
1228
+ "POST",
1229
+ `/projects/${encodedProject}/merge_requests/${resourceId.iid}/discussions/${discussionId}/notes`,
1230
+ { body }
1231
+ );
1232
+ }
1233
+ case "issue": {
1234
+ if (!resourceId.projectId || !resourceId.iid) {
1235
+ throw new Error("projectId and iid are required for issue discussions");
1236
+ }
1237
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1238
+ return this.fetch(
1239
+ "POST",
1240
+ `/projects/${encodedProject}/issues/${resourceId.iid}/discussions/${discussionId}/notes`,
1241
+ { body }
1242
+ );
1243
+ }
1244
+ case "epic": {
1245
+ if (!resourceId.groupId || !resourceId.iid) {
1246
+ throw new Error("groupId and iid are required for epic discussions");
1247
+ }
1248
+ const encodedGroup = encodeURIComponent(resourceId.groupId);
1249
+ return this.fetch(
1250
+ "POST",
1251
+ `/groups/${encodedGroup}/epics/${resourceId.iid}/discussions/${discussionId}/notes`,
1252
+ { body }
1253
+ );
1254
+ }
1255
+ case "commit": {
1256
+ if (!resourceId.projectId || !resourceId.sha) {
1257
+ throw new Error("projectId and sha are required for commit discussions");
1258
+ }
1259
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1260
+ return this.fetch(
1261
+ "POST",
1262
+ `/projects/${encodedProject}/repository/commits/${resourceId.sha}/discussions/${discussionId}/notes`,
1263
+ { body }
1264
+ );
1265
+ }
1266
+ case "snippet": {
1267
+ if (!resourceId.projectId || !resourceId.snippetId) {
1268
+ throw new Error("projectId and snippetId are required for snippet discussions");
1269
+ }
1270
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1271
+ return this.fetch(
1272
+ "POST",
1273
+ `/projects/${encodedProject}/snippets/${resourceId.snippetId}/discussions/${discussionId}/notes`,
1274
+ { body }
1275
+ );
1276
+ }
1277
+ default:
1278
+ throw new Error(`Unsupported resource type: ${resourceType}`);
1279
+ }
1280
+ }
1281
+ /**
1282
+ * Universal method to get a specific discussion from any resource type
1283
+ * Supports: merge requests, issues, epics, commits, snippets
1284
+ */
1285
+ async getDiscussion(resourceType, resourceId, discussionId) {
1286
+ switch (resourceType) {
1287
+ case "merge_request": {
1288
+ if (!resourceId.projectId || !resourceId.iid) {
1289
+ throw new Error("projectId and iid are required for merge_request discussions");
1290
+ }
1291
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1292
+ return this.fetch(
1293
+ "GET",
1294
+ `/projects/${encodedProject}/merge_requests/${resourceId.iid}/discussions/${discussionId}`
1295
+ );
1296
+ }
1297
+ case "issue": {
1298
+ if (!resourceId.projectId || !resourceId.iid) {
1299
+ throw new Error("projectId and iid are required for issue discussions");
1300
+ }
1301
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1302
+ return this.fetch(
1303
+ "GET",
1304
+ `/projects/${encodedProject}/issues/${resourceId.iid}/discussions/${discussionId}`
1305
+ );
1306
+ }
1307
+ case "epic": {
1308
+ if (!resourceId.groupId || !resourceId.iid) {
1309
+ throw new Error("groupId and iid are required for epic discussions");
1310
+ }
1311
+ const encodedGroup = encodeURIComponent(resourceId.groupId);
1312
+ return this.fetch(
1313
+ "GET",
1314
+ `/groups/${encodedGroup}/epics/${resourceId.iid}/discussions/${discussionId}`
1315
+ );
1316
+ }
1317
+ case "commit": {
1318
+ if (!resourceId.projectId || !resourceId.sha) {
1319
+ throw new Error("projectId and sha are required for commit discussions");
1320
+ }
1321
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1322
+ return this.fetch(
1323
+ "GET",
1324
+ `/projects/${encodedProject}/repository/commits/${resourceId.sha}/discussions/${discussionId}`
1325
+ );
1326
+ }
1327
+ case "snippet": {
1328
+ if (!resourceId.projectId || !resourceId.snippetId) {
1329
+ throw new Error("projectId and snippetId are required for snippet discussions");
1330
+ }
1331
+ const encodedProject = this.encodeProjectId(resourceId.projectId);
1332
+ return this.fetch(
1333
+ "GET",
1334
+ `/projects/${encodedProject}/snippets/${resourceId.snippetId}/discussions/${discussionId}`
1335
+ );
1336
+ }
1337
+ default:
1338
+ throw new Error(`Unsupported resource type: ${resourceType}`);
1339
+ }
1340
+ }
1341
+ };
1342
+
1343
+ // src/client/audit.ts
1344
+ var AuditClient = class extends GitLabApiClient {
1345
+ /**
1346
+ * List audit events for a project
1347
+ * Requires: Project owner role
1348
+ * API: GET /projects/:id/audit_events
1349
+ */
1350
+ async listProjectAuditEvents(projectId, options = {}) {
1351
+ const encodedId = this.encodeProjectId(projectId);
1352
+ const params = new URLSearchParams();
1353
+ if (options.created_after) params.append("created_after", options.created_after);
1354
+ if (options.created_before) params.append("created_before", options.created_before);
1355
+ if (options.entity_type) params.append("entity_type", options.entity_type);
1356
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
1357
+ if (options.author_id) params.append("author_id", options.author_id.toString());
1358
+ if (options.per_page) params.append("per_page", options.per_page.toString());
1359
+ if (options.page) params.append("page", options.page.toString());
1360
+ const query = params.toString();
1361
+ const path2 = `/projects/${encodedId}/audit_events${query ? `?${query}` : ""}`;
1362
+ return this.fetch("GET", path2);
1363
+ }
1364
+ /**
1365
+ * List audit events for a group
1366
+ * Requires: Group owner role
1367
+ * API: GET /groups/:id/audit_events
1368
+ */
1369
+ async listGroupAuditEvents(groupId, options = {}) {
1370
+ const encodedId = encodeURIComponent(groupId);
1371
+ const params = new URLSearchParams();
1372
+ if (options.created_after) params.append("created_after", options.created_after);
1373
+ if (options.created_before) params.append("created_before", options.created_before);
1374
+ if (options.entity_type) params.append("entity_type", options.entity_type);
1375
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
1376
+ if (options.author_id) params.append("author_id", options.author_id.toString());
1377
+ if (options.per_page) params.append("per_page", options.per_page.toString());
1378
+ if (options.page) params.append("page", options.page.toString());
1379
+ const query = params.toString();
1380
+ const path2 = `/groups/${encodedId}/audit_events${query ? `?${query}` : ""}`;
1381
+ return this.fetch("GET", path2);
1382
+ }
1383
+ /**
1384
+ * List instance-level audit events
1385
+ * Requires: Administrator access
1386
+ * API: GET /audit_events
1387
+ */
1388
+ async listInstanceAuditEvents(options = {}) {
1389
+ const params = new URLSearchParams();
1390
+ if (options.created_after) params.append("created_after", options.created_after);
1391
+ if (options.created_before) params.append("created_before", options.created_before);
1392
+ if (options.entity_type) params.append("entity_type", options.entity_type);
1393
+ if (options.entity_id) params.append("entity_id", options.entity_id.toString());
1394
+ if (options.author_id) params.append("author_id", options.author_id.toString());
1395
+ if (options.per_page) params.append("per_page", options.per_page.toString());
1396
+ if (options.page) params.append("page", options.page.toString());
1397
+ const query = params.toString();
1398
+ const path2 = `/audit_events${query ? `?${query}` : ""}`;
1399
+ return this.fetch("GET", path2);
1400
+ }
1401
+ };
1402
+
1403
+ // src/client/index.ts
1404
+ var UnifiedGitLabClient = class extends GitLabApiClient {
1405
+ };
1406
+ function applyMixins(derivedCtor, constructors) {
1407
+ constructors.forEach((baseCtor) => {
1408
+ Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
1409
+ Object.defineProperty(
1410
+ derivedCtor.prototype,
1411
+ name,
1412
+ Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || /* @__PURE__ */ Object.create(null)
1413
+ );
1414
+ });
1415
+ });
1416
+ }
1417
+ applyMixins(UnifiedGitLabClient, [
1418
+ MergeRequestsClient,
1419
+ IssuesClient,
1420
+ WorkItemsClient,
1421
+ PipelinesClient,
1422
+ RepositoryClient,
1423
+ SearchClient,
1424
+ ProjectsClient,
1425
+ UsersClient,
1426
+ WikisClient,
1427
+ SecurityClient,
1428
+ TodosClient,
1429
+ EpicsClient,
1430
+ SnippetsClient,
1431
+ DiscussionsClient,
1432
+ AuditClient
1433
+ ]);
1434
+
1435
+ // src/utils.ts
1436
+ function readTokenFromAuthStorage() {
1437
+ try {
1438
+ const authPath = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
1439
+ if (!fs.existsSync(authPath)) {
1440
+ return void 0;
1441
+ }
1442
+ const authData = JSON.parse(fs.readFileSync(authPath, "utf-8"));
1443
+ const gitlabAuth = authData?.gitlab;
1444
+ if (!gitlabAuth) {
1445
+ return void 0;
1446
+ }
1447
+ if (gitlabAuth.type === "oauth" && gitlabAuth.access) {
1448
+ return gitlabAuth.access;
1449
+ }
1450
+ if (gitlabAuth.type === "api" && gitlabAuth.key) {
1451
+ return gitlabAuth.key;
1452
+ }
1453
+ return gitlabAuth.token || void 0;
1454
+ } catch (error) {
1455
+ return void 0;
1456
+ }
1457
+ }
1458
+ function getGitLabClient() {
1459
+ let token = process.env["GITLAB_TOKEN"];
1460
+ if (!token) {
1461
+ token = readTokenFromAuthStorage();
1462
+ }
1463
+ if (!token) {
1464
+ throw new Error(
1465
+ "GitLab API token not found. Set GITLAB_TOKEN environment variable or configure it in OpenCode auth storage (~/.local/share/opencode/auth.json)."
1466
+ );
1467
+ }
1468
+ const instanceUrl = process.env["GITLAB_INSTANCE_URL"] || "https://gitlab.com";
1469
+ return new UnifiedGitLabClient(instanceUrl, token);
1470
+ }
1471
+
1472
+ // src/tools/merge-requests.ts
1473
+ var z = tool.schema;
1474
+ var mergeRequestTools = {
1475
+ gitlab_get_merge_request: tool({
1476
+ description: `Get details of a specific merge request by project and MR IID.
1477
+ Returns: title, description, state, author, assignees, reviewers, labels, diff stats, and discussion notes.`,
1478
+ args: {
1479
+ project_id: z.string().describe('The project ID or URL-encoded path (e.g., "gitlab-org/gitlab" or "123")'),
1480
+ mr_iid: z.number().describe("The internal ID of the merge request within the project"),
1481
+ include_changes: z.boolean().optional().describe("Whether to include the list of changed files (default: false)")
1482
+ },
1483
+ execute: async (args, _ctx) => {
1484
+ const client = getGitLabClient();
1485
+ const mr = await client.getMergeRequest(args.project_id, args.mr_iid, args.include_changes);
1486
+ return JSON.stringify(mr, null, 2);
1487
+ }
1488
+ }),
1489
+ gitlab_list_merge_requests: tool({
1490
+ description: `List merge requests for a project or search globally.
1491
+ Can filter by state (opened, closed, merged, all), scope (assigned_to_me, created_by_me), and labels.`,
1492
+ args: {
1493
+ project_id: z.string().optional().describe("The project ID or path. If not provided, searches globally."),
1494
+ state: z.enum(["opened", "closed", "merged", "all"]).optional().describe("Filter by MR state (default: opened)"),
1495
+ scope: z.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
1496
+ search: z.string().optional().describe("Search MRs by title or description"),
1497
+ labels: z.string().optional().describe("Comma-separated list of labels to filter by"),
1498
+ limit: z.number().optional().describe("Maximum number of results (default: 20)")
1499
+ },
1500
+ execute: async (args, _ctx) => {
1501
+ const client = getGitLabClient();
1502
+ const mrs = await client.listMergeRequests({
1503
+ projectId: args.project_id,
1504
+ state: args.state,
1505
+ scope: args.scope,
1506
+ search: args.search,
1507
+ labels: args.labels,
1508
+ limit: args.limit
1509
+ });
1510
+ return JSON.stringify(mrs, null, 2);
1511
+ }
1512
+ }),
1513
+ gitlab_get_mr_changes: tool({
1514
+ description: `Get the file changes (diff) for a merge request.
1515
+ Returns the list of files changed with their diffs.`,
1516
+ args: {
1517
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1518
+ mr_iid: z.number().describe("The internal ID of the merge request")
1519
+ },
1520
+ execute: async (args, _ctx) => {
1521
+ const client = getGitLabClient();
1522
+ const changes = await client.getMrChanges(args.project_id, args.mr_iid);
1523
+ return JSON.stringify(changes, null, 2);
1524
+ }
1525
+ }),
1526
+ gitlab_list_mr_discussions: tool({
1527
+ description: `List discussions (comment threads) on a merge request.
1528
+ Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
1529
+ Note: For a flattened list of all comments, use gitlab_list_mr_notes instead.`,
1530
+ args: {
1531
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1532
+ mr_iid: z.number().describe("The internal ID of the merge request")
1533
+ },
1534
+ execute: async (args, _ctx) => {
1535
+ const client = getGitLabClient();
1536
+ const discussions = await client.listMrDiscussions(args.project_id, args.mr_iid);
1537
+ return JSON.stringify(discussions, null, 2);
1538
+ }
1539
+ }),
1540
+ gitlab_list_mr_notes: tool({
1541
+ description: `List all notes/comments on a merge request in a flat structure.
1542
+ Returns all comments including system notes, code review comments, and general discussion.
1543
+ This is easier to read than discussions which have nested structure.`,
1544
+ args: {
1545
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1546
+ mr_iid: z.number().describe("The internal ID of the merge request")
1547
+ },
1548
+ execute: async (args, _ctx) => {
1549
+ const client = getGitLabClient();
1550
+ const notes = await client.listMrNotes(args.project_id, args.mr_iid);
1551
+ return JSON.stringify(notes, null, 2);
1552
+ }
1553
+ }),
1554
+ gitlab_create_mr_note: tool({
1555
+ description: `Add a comment/note to a merge request.
1556
+ If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
1557
+ Use gitlab_list_mr_discussions to find discussion IDs for existing threads.`,
1558
+ args: {
1559
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1560
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1561
+ body: z.string().describe("The content of the note/comment (supports Markdown)"),
1562
+ discussion_id: z.string().optional().describe(
1563
+ "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
1564
+ )
1565
+ },
1566
+ execute: async (args, _ctx) => {
1567
+ const client = getGitLabClient();
1568
+ const note = await client.createMrNote(
1569
+ args.project_id,
1570
+ args.mr_iid,
1571
+ args.body,
1572
+ args.discussion_id
1573
+ );
1574
+ return JSON.stringify(note, null, 2);
1575
+ }
1576
+ }),
1577
+ gitlab_get_mr_discussion: tool({
1578
+ description: `Get a specific discussion thread from a merge request with all its replies.
1579
+ Returns the discussion with its 'notes' array containing all comments in the thread.
1580
+ Use this to get the full context of a specific conversation.`,
1581
+ args: {
1582
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1583
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1584
+ discussion_id: z.string().describe("The ID of the discussion thread")
1585
+ },
1586
+ execute: async (args, _ctx) => {
1587
+ const client = getGitLabClient();
1588
+ const discussion = await client.getMrDiscussion(
1589
+ args.project_id,
1590
+ args.mr_iid,
1591
+ args.discussion_id
1592
+ );
1593
+ return JSON.stringify(discussion, null, 2);
1594
+ }
1595
+ }),
1596
+ gitlab_resolve_mr_discussion: tool({
1597
+ description: `Mark a merge request discussion thread as resolved.
1598
+ Use this after addressing feedback in a code review to indicate the discussion is complete.
1599
+ Only works for resolvable discussions (typically code review comments).`,
1600
+ args: {
1601
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1602
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1603
+ discussion_id: z.string().describe("The ID of the discussion thread to resolve")
1604
+ },
1605
+ execute: async (args, _ctx) => {
1606
+ const client = getGitLabClient();
1607
+ const discussion = await client.resolveMrDiscussion(
1608
+ args.project_id,
1609
+ args.mr_iid,
1610
+ args.discussion_id
1611
+ );
1612
+ return JSON.stringify(discussion, null, 2);
1613
+ }
1614
+ }),
1615
+ gitlab_unresolve_mr_discussion: tool({
1616
+ description: `Reopen a resolved merge request discussion thread.
1617
+ Use this to indicate that a previously resolved discussion needs more attention.`,
1618
+ args: {
1619
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1620
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1621
+ discussion_id: z.string().describe("The ID of the discussion thread to unresolve")
1622
+ },
1623
+ execute: async (args, _ctx) => {
1624
+ const client = getGitLabClient();
1625
+ const discussion = await client.unresolveMrDiscussion(
1626
+ args.project_id,
1627
+ args.mr_iid,
1628
+ args.discussion_id
1629
+ );
1630
+ return JSON.stringify(discussion, null, 2);
1631
+ }
1632
+ }),
1633
+ gitlab_create_mr_discussion: tool({
1634
+ description: `Start a new discussion thread on a merge request.
1635
+ Creates a new discussion with an initial comment. Optionally can be positioned on specific code.
1636
+ For general comments, just provide the body. For code comments, provide position information.`,
1637
+ args: {
1638
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1639
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1640
+ body: z.string().describe("The content of the initial comment (supports Markdown)"),
1641
+ position: z.object({
1642
+ base_sha: z.string().describe("SHA of the base commit (merge base)"),
1643
+ start_sha: z.string().describe("SHA of the commit when the MR was created"),
1644
+ head_sha: z.string().describe("SHA of the HEAD commit"),
1645
+ position_type: z.enum(["text", "image"]).describe("Type of position (text or image)"),
1646
+ new_path: z.string().optional().describe("Path of the file after changes"),
1647
+ old_path: z.string().optional().describe("Path of the file before changes"),
1648
+ new_line: z.number().optional().describe("Line number in the new version"),
1649
+ old_line: z.number().optional().describe("Line number in the old version")
1650
+ }).optional().describe(
1651
+ "Position information for code comments. Required fields: base_sha, start_sha, head_sha, position_type. For line comments also provide new_path/old_path and new_line/old_line."
1652
+ )
1653
+ },
1654
+ execute: async (args, _ctx) => {
1655
+ const client = getGitLabClient();
1656
+ const discussion = await client.createMrDiscussion(
1657
+ args.project_id,
1658
+ args.mr_iid,
1659
+ args.body,
1660
+ args.position
1661
+ );
1662
+ return JSON.stringify(discussion, null, 2);
1663
+ }
1664
+ }),
1665
+ gitlab_create_merge_request: tool({
1666
+ description: `Create a new merge request.
1667
+ Returns the created merge request with all details.`,
1668
+ args: {
1669
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1670
+ source_branch: z.string().describe("The source branch name"),
1671
+ target_branch: z.string().describe("The target branch name"),
1672
+ title: z.string().describe("The title of the merge request"),
1673
+ description: z.string().optional().describe("The description of the merge request (supports Markdown)"),
1674
+ assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
1675
+ reviewer_ids: z.array(z.number()).optional().describe("Array of user IDs to review"),
1676
+ labels: z.string().optional().describe("Comma-separated list of labels"),
1677
+ milestone_id: z.number().optional().describe("The ID of a milestone"),
1678
+ remove_source_branch: z.boolean().optional().describe("Remove source branch after merge (default: false)"),
1679
+ squash: z.boolean().optional().describe("Squash commits on merge (default: false)"),
1680
+ allow_collaboration: z.boolean().optional().describe("Allow commits from members who can merge to the target branch")
1681
+ },
1682
+ execute: async (args, _ctx) => {
1683
+ const client = getGitLabClient();
1684
+ const mr = await client.createMergeRequest(args.project_id, {
1685
+ source_branch: args.source_branch,
1686
+ target_branch: args.target_branch,
1687
+ title: args.title,
1688
+ description: args.description,
1689
+ assignee_ids: args.assignee_ids,
1690
+ reviewer_ids: args.reviewer_ids,
1691
+ labels: args.labels,
1692
+ milestone_id: args.milestone_id,
1693
+ remove_source_branch: args.remove_source_branch,
1694
+ squash: args.squash,
1695
+ allow_collaboration: args.allow_collaboration
1696
+ });
1697
+ return JSON.stringify(mr, null, 2);
1698
+ }
1699
+ }),
1700
+ gitlab_update_merge_request: tool({
1701
+ description: `Update an existing merge request.
1702
+ Can update title, description, state, assignees, reviewers, labels, and more.`,
1703
+ args: {
1704
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1705
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1706
+ title: z.string().optional().describe("The new title"),
1707
+ description: z.string().optional().describe("The new description (supports Markdown)"),
1708
+ state_event: z.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
1709
+ assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
1710
+ reviewer_ids: z.array(z.number()).optional().describe("Array of user IDs to review"),
1711
+ labels: z.string().optional().describe("Comma-separated list of labels"),
1712
+ milestone_id: z.number().optional().describe("The ID of a milestone"),
1713
+ remove_source_branch: z.boolean().optional().describe("Remove source branch after merge"),
1714
+ squash: z.boolean().optional().describe("Squash commits on merge"),
1715
+ target_branch: z.string().optional().describe("The target branch name")
1716
+ },
1717
+ execute: async (args, _ctx) => {
1718
+ const client = getGitLabClient();
1719
+ const mr = await client.updateMergeRequest(args.project_id, args.mr_iid, {
1720
+ title: args.title,
1721
+ description: args.description,
1722
+ state_event: args.state_event,
1723
+ assignee_ids: args.assignee_ids,
1724
+ reviewer_ids: args.reviewer_ids,
1725
+ labels: args.labels,
1726
+ milestone_id: args.milestone_id,
1727
+ remove_source_branch: args.remove_source_branch,
1728
+ squash: args.squash,
1729
+ target_branch: args.target_branch
1730
+ });
1731
+ return JSON.stringify(mr, null, 2);
1732
+ }
1733
+ }),
1734
+ gitlab_get_mr_commits: tool({
1735
+ description: `Get the list of commits in a merge request.
1736
+ Returns all commits that are part of the merge request.`,
1737
+ args: {
1738
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1739
+ mr_iid: z.number().describe("The internal ID of the merge request")
1740
+ },
1741
+ execute: async (args, _ctx) => {
1742
+ const client = getGitLabClient();
1743
+ const commits = await client.getMrCommits(args.project_id, args.mr_iid);
1744
+ return JSON.stringify(commits, null, 2);
1745
+ }
1746
+ }),
1747
+ gitlab_get_mr_pipelines: tool({
1748
+ description: `Get the list of pipelines for a merge request.
1749
+ Returns all pipelines that ran for the merge request.`,
1750
+ args: {
1751
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1752
+ mr_iid: z.number().describe("The internal ID of the merge request")
1753
+ },
1754
+ execute: async (args, _ctx) => {
1755
+ const client = getGitLabClient();
1756
+ const pipelines = await client.getMrPipelines(args.project_id, args.mr_iid);
1757
+ return JSON.stringify(pipelines, null, 2);
1758
+ }
1759
+ }),
1760
+ gitlab_list_merge_request_diffs: tool({
1761
+ description: `List merge request diffs with pagination support.
1762
+ Returns the list of file diffs for a merge request, with support for pagination to handle large changesets.
1763
+ This is useful when you need to process diffs in chunks or when the MR has many changed files.`,
1764
+ args: {
1765
+ project_id: z.string().describe("The project ID or URL-encoded path"),
1766
+ mr_iid: z.number().describe("The internal ID of the merge request"),
1767
+ page: z.number().optional().describe("Page number for pagination (default: 1)"),
1768
+ per_page: z.number().optional().describe("Number of diffs per page (default: 20, max: 100)")
1769
+ },
1770
+ execute: async (args, _ctx) => {
1771
+ const client = getGitLabClient();
1772
+ const diffs = await client.listMergeRequestDiffs(args.project_id, args.mr_iid, {
1773
+ page: args.page,
1774
+ per_page: args.per_page
1775
+ });
1776
+ return JSON.stringify(diffs, null, 2);
1777
+ }
1778
+ })
1779
+ };
1780
+
1781
+ // src/tools/issues.ts
1782
+ import { tool as tool2 } from "@opencode-ai/plugin";
1783
+ var z2 = tool2.schema;
1784
+ var issueTools = {
1785
+ gitlab_create_issue: tool2({
1786
+ description: `Create a new issue in a GitLab project.
1787
+ Returns the created issue with all details including IID, web URL, and metadata.`,
1788
+ args: {
1789
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1790
+ title: z2.string().describe("The title of the issue"),
1791
+ description: z2.string().optional().describe("The description of the issue (supports Markdown)"),
1792
+ assignee_ids: z2.array(z2.number()).optional().describe("Array of user IDs to assign the issue to"),
1793
+ milestone_id: z2.number().optional().describe("The ID of the milestone to assign the issue to"),
1794
+ labels: z2.string().optional().describe('Comma-separated list of label names (e.g., "bug,critical")'),
1795
+ due_date: z2.string().optional().describe('Due date for the issue in YYYY-MM-DD format (e.g., "2025-12-31")'),
1796
+ confidential: z2.boolean().optional().describe("Whether the issue should be confidential"),
1797
+ weight: z2.number().optional().describe("The weight of the issue (for issue boards)"),
1798
+ epic_id: z2.number().optional().describe("The ID of the epic to add the issue to (Premium/Ultimate)"),
1799
+ issue_type: z2.enum(["issue", "incident", "test_case", "task"]).optional().describe("The type of issue (default: issue)")
1800
+ },
1801
+ execute: async (args, _ctx) => {
1802
+ const client = getGitLabClient();
1803
+ const issue = await client.createIssue(args.project_id, args.title, {
1804
+ description: args.description,
1805
+ assignee_ids: args.assignee_ids,
1806
+ milestone_id: args.milestone_id,
1807
+ labels: args.labels,
1808
+ due_date: args.due_date,
1809
+ confidential: args.confidential,
1810
+ weight: args.weight,
1811
+ epic_id: args.epic_id,
1812
+ issue_type: args.issue_type
1813
+ });
1814
+ return JSON.stringify(issue, null, 2);
1815
+ }
1816
+ }),
1817
+ gitlab_get_issue: tool2({
1818
+ description: `Get details of a specific issue by project and issue IID.
1819
+ Returns: title, description, state, author, assignees, labels, milestone, weight, and comments.`,
1820
+ args: {
1821
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1822
+ issue_iid: z2.number().describe("The internal ID of the issue within the project")
1823
+ },
1824
+ execute: async (args, _ctx) => {
1825
+ const client = getGitLabClient();
1826
+ const issue = await client.getIssue(args.project_id, args.issue_iid);
1827
+ return JSON.stringify(issue, null, 2);
1828
+ }
1829
+ }),
1830
+ gitlab_list_issues: tool2({
1831
+ description: `List issues for a project or search globally.
1832
+ Can filter by state, labels, assignee, milestone.`,
1833
+ args: {
1834
+ project_id: z2.string().optional().describe("The project ID or path. If not provided, searches globally."),
1835
+ state: z2.enum(["opened", "closed", "all"]).optional().describe("Filter by issue state (default: opened)"),
1836
+ scope: z2.enum(["assigned_to_me", "created_by_me", "all"]).optional().describe("Filter by scope"),
1837
+ search: z2.string().optional().describe("Search issues by title or description"),
1838
+ labels: z2.string().optional().describe("Comma-separated list of labels to filter by"),
1839
+ milestone: z2.string().optional().describe("Filter by milestone title"),
1840
+ limit: z2.number().optional().describe("Maximum number of results (default: 20)")
1841
+ },
1842
+ execute: async (args, _ctx) => {
1843
+ const client = getGitLabClient();
1844
+ const issues = await client.listIssues({
1845
+ projectId: args.project_id,
1846
+ state: args.state,
1847
+ scope: args.scope,
1848
+ search: args.search,
1849
+ labels: args.labels,
1850
+ milestone: args.milestone,
1851
+ limit: args.limit
1852
+ });
1853
+ return JSON.stringify(issues, null, 2);
1854
+ }
1855
+ }),
1856
+ gitlab_list_issue_notes: tool2({
1857
+ description: `List all notes/comments on an issue.
1858
+ Returns all comments including system notes in chronological order.`,
1859
+ args: {
1860
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1861
+ issue_iid: z2.number().describe("The internal ID of the issue")
1862
+ },
1863
+ execute: async (args, _ctx) => {
1864
+ const client = getGitLabClient();
1865
+ const notes = await client.listIssueNotes(args.project_id, args.issue_iid);
1866
+ return JSON.stringify(notes, null, 2);
1867
+ }
1868
+ }),
1869
+ gitlab_list_issue_discussions: tool2({
1870
+ description: `List discussions (comment threads) on an issue.
1871
+ Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
1872
+ Use the discussion 'id' field to reply to a specific thread with gitlab_create_issue_note.`,
1873
+ args: {
1874
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1875
+ issue_iid: z2.number().describe("The internal ID of the issue")
1876
+ },
1877
+ execute: async (args, _ctx) => {
1878
+ const client = getGitLabClient();
1879
+ const discussions = await client.listIssueDiscussions(args.project_id, args.issue_iid);
1880
+ return JSON.stringify(discussions, null, 2);
1881
+ }
1882
+ }),
1883
+ gitlab_create_issue_note: tool2({
1884
+ description: `Add a comment/note to an issue.
1885
+ If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
1886
+ Use gitlab_list_issue_discussions to find discussion IDs for existing threads.`,
1887
+ args: {
1888
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1889
+ issue_iid: z2.number().describe("The internal ID of the issue"),
1890
+ body: z2.string().describe("The content of the note/comment (supports Markdown)"),
1891
+ discussion_id: z2.string().optional().describe(
1892
+ "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
1893
+ )
1894
+ },
1895
+ execute: async (args, _ctx) => {
1896
+ const client = getGitLabClient();
1897
+ const note = await client.createIssueNote(
1898
+ args.project_id,
1899
+ args.issue_iid,
1900
+ args.body,
1901
+ args.discussion_id
1902
+ );
1903
+ return JSON.stringify(note, null, 2);
1904
+ }
1905
+ }),
1906
+ gitlab_get_issue_discussion: tool2({
1907
+ description: `Get a specific discussion thread from an issue with all its replies.
1908
+ Returns the discussion with its 'notes' array containing all comments in the thread.
1909
+ Use this to get the full context of a specific conversation.`,
1910
+ args: {
1911
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1912
+ issue_iid: z2.number().describe("The internal ID of the issue"),
1913
+ discussion_id: z2.string().describe("The ID of the discussion thread")
1914
+ },
1915
+ execute: async (args, _ctx) => {
1916
+ const client = getGitLabClient();
1917
+ const discussion = await client.getIssueDiscussion(
1918
+ args.project_id,
1919
+ args.issue_iid,
1920
+ args.discussion_id
1921
+ );
1922
+ return JSON.stringify(discussion, null, 2);
1923
+ }
1924
+ }),
1925
+ gitlab_resolve_issue_discussion: tool2({
1926
+ description: `Mark an issue discussion thread as resolved.
1927
+ Use this after addressing feedback in a code review or issue discussion to indicate the discussion is complete.
1928
+ Only works for resolvable discussions (typically multi-note threads, not individual notes).`,
1929
+ args: {
1930
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1931
+ issue_iid: z2.number().describe("The internal ID of the issue"),
1932
+ discussion_id: z2.string().describe("The ID of the discussion thread to resolve")
1933
+ },
1934
+ execute: async (args, _ctx) => {
1935
+ const client = getGitLabClient();
1936
+ const discussion = await client.resolveIssueDiscussion(
1937
+ args.project_id,
1938
+ args.issue_iid,
1939
+ args.discussion_id
1940
+ );
1941
+ return JSON.stringify(discussion, null, 2);
1942
+ }
1943
+ }),
1944
+ gitlab_unresolve_issue_discussion: tool2({
1945
+ description: `Reopen a resolved issue discussion thread.
1946
+ Use this to indicate that a previously resolved discussion needs more attention or further discussion.`,
1947
+ args: {
1948
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1949
+ issue_iid: z2.number().describe("The internal ID of the issue"),
1950
+ discussion_id: z2.string().describe("The ID of the discussion thread to unresolve")
1951
+ },
1952
+ execute: async (args, _ctx) => {
1953
+ const client = getGitLabClient();
1954
+ const discussion = await client.unresolveIssueDiscussion(
1955
+ args.project_id,
1956
+ args.issue_iid,
1957
+ args.discussion_id
1958
+ );
1959
+ return JSON.stringify(discussion, null, 2);
1960
+ }
1961
+ }),
1962
+ gitlab_get_issue_note: tool2({
1963
+ description: `Get a single note/comment from an issue by its ID.
1964
+ Returns the full details of a specific note including author, body, timestamps, and metadata.
1965
+ Useful when you need to retrieve a specific comment without fetching all notes.`,
1966
+ args: {
1967
+ project_id: z2.string().describe("The project ID or URL-encoded path"),
1968
+ issue_iid: z2.number().describe("The internal ID of the issue"),
1969
+ note_id: z2.number().describe("The ID of the note to retrieve")
1970
+ },
1971
+ execute: async (args, _ctx) => {
1972
+ const client = getGitLabClient();
1973
+ const note = await client.getIssueNote(args.project_id, args.issue_iid, args.note_id);
1974
+ return JSON.stringify(note, null, 2);
1975
+ }
1976
+ })
1977
+ };
1978
+
1979
+ // src/tools/epics.ts
1980
+ import { tool as tool3 } from "@opencode-ai/plugin";
1981
+ var z3 = tool3.schema;
1982
+ var epicTools = {
1983
+ gitlab_get_epic: tool3({
1984
+ description: `Get details of a specific epic by group and epic IID.
1985
+ Returns: title, description, state, author, start/end dates, labels, associated issues, and child epics.`,
1986
+ args: {
1987
+ group_id: z3.string().describe(
1988
+ 'The group ID or URL-encoded path (e.g., "gitlab-org" or "my-group/my-subgroup")'
1989
+ ),
1990
+ epic_iid: z3.number().describe("The internal ID of the epic within the group")
1991
+ },
1992
+ execute: async (args, _ctx) => {
1993
+ const client = getGitLabClient();
1994
+ const epic = await client.getEpic(args.group_id, args.epic_iid);
1995
+ return JSON.stringify(epic, null, 2);
1996
+ }
1997
+ }),
1998
+ gitlab_list_epics: tool3({
1999
+ description: `List epics for a group with filtering capabilities.
2000
+ Can filter by state (opened, closed, all), author, labels, and search query.`,
2001
+ args: {
2002
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2003
+ state: z3.enum(["opened", "closed", "all"]).optional().describe("Filter by epic state (default: opened)"),
2004
+ author_id: z3.number().optional().describe("Filter by author user ID"),
2005
+ labels: z3.string().optional().describe("Comma-separated list of labels to filter by"),
2006
+ search: z3.string().optional().describe("Search epics by title or description"),
2007
+ limit: z3.number().optional().describe("Maximum number of results (default: 20)")
2008
+ },
2009
+ execute: async (args, _ctx) => {
2010
+ const client = getGitLabClient();
2011
+ const epics = await client.listEpics({
2012
+ groupId: args.group_id,
2013
+ state: args.state,
2014
+ author_id: args.author_id,
2015
+ labels: args.labels,
2016
+ search: args.search,
2017
+ limit: args.limit
2018
+ });
2019
+ return JSON.stringify(epics, null, 2);
2020
+ }
2021
+ }),
2022
+ gitlab_create_epic: tool3({
2023
+ description: `Create a new epic in a group.
2024
+ Returns the created epic with all details.`,
2025
+ args: {
2026
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2027
+ title: z3.string().describe("The title of the epic"),
2028
+ description: z3.string().optional().describe("The description of the epic (supports Markdown)"),
2029
+ labels: z3.string().optional().describe("Comma-separated list of labels"),
2030
+ start_date: z3.string().optional().describe("Start date in YYYY-MM-DD format"),
2031
+ end_date: z3.string().optional().describe("Due/end date in YYYY-MM-DD format"),
2032
+ confidential: z3.boolean().optional().describe("Whether the epic is confidential (default: false)")
2033
+ },
2034
+ execute: async (args, _ctx) => {
2035
+ const client = getGitLabClient();
2036
+ const epic = await client.createEpic(args.group_id, {
2037
+ title: args.title,
2038
+ description: args.description,
2039
+ labels: args.labels,
2040
+ start_date: args.start_date,
2041
+ end_date: args.end_date,
2042
+ confidential: args.confidential
2043
+ });
2044
+ return JSON.stringify(epic, null, 2);
2045
+ }
2046
+ }),
2047
+ gitlab_update_epic: tool3({
2048
+ description: `Update an existing epic.
2049
+ Can update title, description, labels, dates, state, and confidentiality.`,
2050
+ args: {
2051
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2052
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2053
+ title: z3.string().optional().describe("The new title"),
2054
+ description: z3.string().optional().describe("The new description (supports Markdown)"),
2055
+ labels: z3.string().optional().describe("Comma-separated list of labels"),
2056
+ start_date: z3.string().optional().describe("Start date in YYYY-MM-DD format"),
2057
+ end_date: z3.string().optional().describe("Due/end date in YYYY-MM-DD format"),
2058
+ state_event: z3.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
2059
+ confidential: z3.boolean().optional().describe("Whether the epic is confidential")
2060
+ },
2061
+ execute: async (args, _ctx) => {
2062
+ const client = getGitLabClient();
2063
+ const epic = await client.updateEpic(args.group_id, args.epic_iid, {
2064
+ title: args.title,
2065
+ description: args.description,
2066
+ labels: args.labels,
2067
+ start_date: args.start_date,
2068
+ end_date: args.end_date,
2069
+ state_event: args.state_event,
2070
+ confidential: args.confidential
2071
+ });
2072
+ return JSON.stringify(epic, null, 2);
2073
+ }
2074
+ }),
2075
+ gitlab_list_epic_issues: tool3({
2076
+ description: `Get all issues associated with an epic.
2077
+ Returns the list of issues that are linked to the epic.`,
2078
+ args: {
2079
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2080
+ epic_iid: z3.number().describe("The internal ID of the epic")
2081
+ },
2082
+ execute: async (args, _ctx) => {
2083
+ const client = getGitLabClient();
2084
+ const issues = await client.listEpicIssues(args.group_id, args.epic_iid);
2085
+ return JSON.stringify(issues, null, 2);
2086
+ }
2087
+ }),
2088
+ gitlab_add_issue_to_epic: tool3({
2089
+ description: `Link an existing issue to an epic.
2090
+ The issue can be from any project within the group hierarchy.`,
2091
+ args: {
2092
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2093
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2094
+ issue_id: z3.number().describe("The global ID of the issue to add")
2095
+ },
2096
+ execute: async (args, _ctx) => {
2097
+ const client = getGitLabClient();
2098
+ const result = await client.addIssueToEpic(args.group_id, args.epic_iid, args.issue_id);
2099
+ return JSON.stringify(result, null, 2);
2100
+ }
2101
+ }),
2102
+ gitlab_remove_issue_from_epic: tool3({
2103
+ description: `Unlink an issue from an epic.
2104
+ Removes the association between the issue and the epic.`,
2105
+ args: {
2106
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2107
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2108
+ epic_issue_id: z3.number().describe("The ID of the epic-issue association (from list_epic_issues)")
2109
+ },
2110
+ execute: async (args, _ctx) => {
2111
+ const client = getGitLabClient();
2112
+ const result = await client.removeIssueFromEpic(
2113
+ args.group_id,
2114
+ args.epic_iid,
2115
+ args.epic_issue_id
2116
+ );
2117
+ return JSON.stringify(result, null, 2);
2118
+ }
2119
+ }),
2120
+ gitlab_list_epic_notes: tool3({
2121
+ description: `List all comments/notes on an epic in a flat structure.
2122
+ Returns all comments in chronological order.`,
2123
+ args: {
2124
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2125
+ epic_iid: z3.number().describe("The internal ID of the epic")
2126
+ },
2127
+ execute: async (args, _ctx) => {
2128
+ const client = getGitLabClient();
2129
+ const notes = await client.listEpicNotes(args.group_id, args.epic_iid);
2130
+ return JSON.stringify(notes, null, 2);
2131
+ }
2132
+ }),
2133
+ gitlab_list_epic_discussions: tool3({
2134
+ description: `List discussions (comment threads) on an epic.
2135
+ Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
2136
+ Use the discussion 'id' field to reply to a specific thread with gitlab_create_epic_note.`,
2137
+ args: {
2138
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2139
+ epic_iid: z3.number().describe("The internal ID of the epic")
2140
+ },
2141
+ execute: async (args, _ctx) => {
2142
+ const client = getGitLabClient();
2143
+ const discussions = await client.listEpicDiscussions(args.group_id, args.epic_iid);
2144
+ return JSON.stringify(discussions, null, 2);
2145
+ }
2146
+ }),
2147
+ gitlab_create_epic_note: tool3({
2148
+ description: `Add a comment/note to an epic.
2149
+ If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
2150
+ Use gitlab_list_epic_discussions to find discussion IDs for existing threads.`,
2151
+ args: {
2152
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2153
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2154
+ body: z3.string().describe("The content of the note/comment (supports Markdown)"),
2155
+ discussion_id: z3.string().optional().describe(
2156
+ "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
2157
+ )
2158
+ },
2159
+ execute: async (args, _ctx) => {
2160
+ const client = getGitLabClient();
2161
+ const note = await client.createEpicNote(
2162
+ args.group_id,
2163
+ args.epic_iid,
2164
+ args.body,
2165
+ args.discussion_id
2166
+ );
2167
+ return JSON.stringify(note, null, 2);
2168
+ }
2169
+ }),
2170
+ gitlab_get_epic_discussion: tool3({
2171
+ description: `Get a specific discussion thread from an epic with all its replies.
2172
+ Returns the discussion with its 'notes' array containing all comments in the thread.
2173
+ Use this to get the full context of a specific conversation.`,
2174
+ args: {
2175
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2176
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2177
+ discussion_id: z3.string().describe("The ID of the discussion thread")
2178
+ },
2179
+ execute: async (args, _ctx) => {
2180
+ const client = getGitLabClient();
2181
+ const discussion = await client.getEpicDiscussion(
2182
+ args.group_id,
2183
+ args.epic_iid,
2184
+ args.discussion_id
2185
+ );
2186
+ return JSON.stringify(discussion, null, 2);
2187
+ }
2188
+ }),
2189
+ gitlab_get_epic_note: tool3({
2190
+ description: `Get a single note/comment from an epic by its ID.
2191
+ Returns the full details of a specific note including author, body, timestamps, and metadata.
2192
+ Useful when you need to retrieve a specific comment without fetching all notes.`,
2193
+ args: {
2194
+ group_id: z3.string().describe("The group ID or URL-encoded path"),
2195
+ epic_iid: z3.number().describe("The internal ID of the epic"),
2196
+ note_id: z3.number().describe("The ID of the note to retrieve")
2197
+ },
2198
+ execute: async (args, _ctx) => {
2199
+ const client = getGitLabClient();
2200
+ const note = await client.getEpicNote(args.group_id, args.epic_iid, args.note_id);
2201
+ return JSON.stringify(note, null, 2);
2202
+ }
2203
+ })
2204
+ };
2205
+
2206
+ // src/tools/pipelines.ts
2207
+ import { tool as tool4 } from "@opencode-ai/plugin";
2208
+ var z4 = tool4.schema;
2209
+ var pipelineTools = {
2210
+ gitlab_list_pipelines: tool4({
2211
+ description: `List pipelines for a project.
2212
+ Can filter by status, ref (branch/tag), username.`,
2213
+ args: {
2214
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2215
+ status: z4.enum(["running", "pending", "success", "failed", "canceled", "skipped", "manual"]).optional().describe("Filter by pipeline status"),
2216
+ ref: z4.string().optional().describe("Filter by branch or tag name"),
2217
+ limit: z4.number().optional().describe("Maximum number of results (default: 20)")
2218
+ },
2219
+ execute: async (args, _ctx) => {
2220
+ const client = getGitLabClient();
2221
+ const pipelines = await client.listPipelines(args.project_id, {
2222
+ status: args.status,
2223
+ ref: args.ref,
2224
+ limit: args.limit
2225
+ });
2226
+ return JSON.stringify(pipelines, null, 2);
2227
+ }
2228
+ }),
2229
+ gitlab_get_pipeline: tool4({
2230
+ description: `Get details of a specific pipeline including its jobs.`,
2231
+ args: {
2232
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2233
+ pipeline_id: z4.number().describe("The ID of the pipeline")
2234
+ },
2235
+ execute: async (args, _ctx) => {
2236
+ const client = getGitLabClient();
2237
+ const pipeline = await client.getPipeline(args.project_id, args.pipeline_id);
2238
+ return JSON.stringify(pipeline, null, 2);
2239
+ }
2240
+ }),
2241
+ gitlab_list_pipeline_jobs: tool4({
2242
+ description: `List jobs for a pipeline, optionally filter by scope (failed, success, etc).`,
2243
+ args: {
2244
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2245
+ pipeline_id: z4.number().describe("The ID of the pipeline"),
2246
+ scope: z4.enum([
2247
+ "created",
2248
+ "pending",
2249
+ "running",
2250
+ "failed",
2251
+ "success",
2252
+ "canceled",
2253
+ "skipped",
2254
+ "manual"
2255
+ ]).optional().describe("Filter jobs by scope/status")
2256
+ },
2257
+ execute: async (args, _ctx) => {
2258
+ const client = getGitLabClient();
2259
+ const jobs = await client.listPipelineJobs(args.project_id, args.pipeline_id, args.scope);
2260
+ return JSON.stringify(jobs, null, 2);
2261
+ }
2262
+ }),
2263
+ gitlab_get_job_log: tool4({
2264
+ description: `Get the log/trace output of a specific CI job.`,
2265
+ args: {
2266
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2267
+ job_id: z4.number().describe("The ID of the job")
2268
+ },
2269
+ execute: async (args, _ctx) => {
2270
+ const client = getGitLabClient();
2271
+ return client.getJobLog(args.project_id, args.job_id);
2272
+ }
2273
+ }),
2274
+ gitlab_retry_job: tool4({
2275
+ description: `Retry a failed or canceled CI job.`,
2276
+ args: {
2277
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2278
+ job_id: z4.number().describe("The ID of the job to retry")
2279
+ },
2280
+ execute: async (args, _ctx) => {
2281
+ const client = getGitLabClient();
2282
+ const job = await client.retryJob(args.project_id, args.job_id);
2283
+ return JSON.stringify(job, null, 2);
2284
+ }
2285
+ }),
2286
+ gitlab_get_pipeline_failing_jobs: tool4({
2287
+ description: `Get all failed jobs in a pipeline.
2288
+ Returns only the jobs that have failed, making it easier to debug pipeline failures.`,
2289
+ args: {
2290
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2291
+ pipeline_id: z4.number().describe("The ID of the pipeline")
2292
+ },
2293
+ execute: async (args, _ctx) => {
2294
+ const client = getGitLabClient();
2295
+ const jobs = await client.getPipelineFailingJobs(args.project_id, args.pipeline_id);
2296
+ return JSON.stringify(jobs, null, 2);
2297
+ }
2298
+ }),
2299
+ gitlab_lint_ci_config: tool4({
2300
+ description: `Validate a CI/CD YAML configuration against GitLab CI syntax rules.
2301
+ This validates the configuration in the context of the project, including:
2302
+ - Using the project's CI/CD variables
2303
+ - Searching the project's files for include:local entries
2304
+ - Optionally simulating pipeline creation (dry_run)
2305
+ - Optionally including the list of jobs that would be created
2306
+
2307
+ Returns validation result with:
2308
+ - valid: boolean indicating if configuration is valid
2309
+ - errors: array of error messages
2310
+ - warnings: array of warning messages
2311
+ - merged_yaml: the final merged YAML after processing includes (optional)
2312
+ - jobs: list of jobs that would be created (optional, requires include_jobs=true)`,
2313
+ args: {
2314
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2315
+ content: z4.string().describe("The CI/CD configuration content (YAML as string)"),
2316
+ dry_run: z4.boolean().optional().describe("Run pipeline creation simulation instead of just static check (default: false)"),
2317
+ include_jobs: z4.boolean().optional().describe("Include list of jobs that would be created in the response (default: false)"),
2318
+ ref: z4.string().optional().describe(
2319
+ "Branch or tag context to use for validation (defaults to project default branch)"
2320
+ )
2321
+ },
2322
+ execute: async (args, _ctx) => {
2323
+ const client = getGitLabClient();
2324
+ const result = await client.lintCiConfig(args.project_id, args.content, {
2325
+ dry_run: args.dry_run,
2326
+ include_jobs: args.include_jobs,
2327
+ ref: args.ref
2328
+ });
2329
+ return JSON.stringify(result, null, 2);
2330
+ }
2331
+ }),
2332
+ gitlab_lint_existing_ci_config: tool4({
2333
+ description: `Validate an existing .gitlab-ci.yml configuration from the repository.
2334
+ This validates the configuration in the context of the project, including:
2335
+ - Using the project's CI/CD variables
2336
+ - Searching the project's files for include:local entries
2337
+ - Optionally simulating pipeline creation (dry_run)
2338
+ - Optionally including the list of jobs that would be created
2339
+
2340
+ Returns validation result with:
2341
+ - valid: boolean indicating if configuration is valid
2342
+ - errors: array of error messages
2343
+ - warnings: array of warning messages
2344
+ - merged_yaml: the final merged YAML after processing includes (optional)
2345
+ - includes: list of included files with their locations (optional)
2346
+ - jobs: list of jobs that would be created (optional, requires include_jobs=true)`,
2347
+ args: {
2348
+ project_id: z4.string().describe("The project ID or URL-encoded path"),
2349
+ content_ref: z4.string().optional().describe(
2350
+ "Commit SHA, branch or tag to get CI config from (defaults to project default branch)"
2351
+ ),
2352
+ dry_run: z4.boolean().optional().describe("Run pipeline creation simulation instead of just static check (default: false)"),
2353
+ dry_run_ref: z4.string().optional().describe(
2354
+ "Branch or tag context to use for validation when dry_run is true (defaults to project default branch)"
2355
+ ),
2356
+ include_jobs: z4.boolean().optional().describe("Include list of jobs that would be created in the response (default: false)")
2357
+ },
2358
+ execute: async (args, _ctx) => {
2359
+ const client = getGitLabClient();
2360
+ const result = await client.lintExistingCiConfig(args.project_id, {
2361
+ content_ref: args.content_ref,
2362
+ dry_run: args.dry_run,
2363
+ dry_run_ref: args.dry_run_ref,
2364
+ include_jobs: args.include_jobs
2365
+ });
2366
+ return JSON.stringify(result, null, 2);
2367
+ }
2368
+ })
2369
+ };
2370
+
2371
+ // src/tools/repository.ts
2372
+ import { tool as tool5 } from "@opencode-ai/plugin";
2373
+ var z5 = tool5.schema;
2374
+ var repositoryTools = {
2375
+ gitlab_get_file: tool5({
2376
+ description: `Get the contents of a file from a repository.
2377
+ Supports fetching files from any branch, tag, or commit SHA.
2378
+ If ref is not specified, uses the project's default branch.
2379
+ Note: Invalid refs will result in a 404 error from the GitLab API.`,
2380
+ args: {
2381
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2382
+ file_path: z5.string().describe("Path to the file in the repository"),
2383
+ ref: z5.string().optional().describe(
2384
+ `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").`
2385
+ )
2386
+ },
2387
+ execute: async (args, _ctx) => {
2388
+ const client = getGitLabClient();
2389
+ return client.getFile(args.project_id, args.file_path, args.ref);
2390
+ }
2391
+ }),
2392
+ gitlab_get_commit: tool5({
2393
+ description: `Get a single commit with full details.
2394
+ Returns commit metadata including author, message, stats, and parent commits.`,
2395
+ args: {
2396
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2397
+ sha: z5.string().describe("The commit SHA")
2398
+ },
2399
+ execute: async (args, _ctx) => {
2400
+ const client = getGitLabClient();
2401
+ const commit = await client.getCommit(args.project_id, args.sha);
2402
+ return JSON.stringify(commit, null, 2);
2403
+ }
2404
+ }),
2405
+ gitlab_list_commits: tool5({
2406
+ description: `List commits in a repository. Can filter by branch/ref and path.`,
2407
+ args: {
2408
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2409
+ ref: z5.string().optional().describe("Branch or tag name"),
2410
+ path: z5.string().optional().describe("File or directory path to filter commits"),
2411
+ since: z5.string().optional().describe("Only commits after this date (ISO 8601 format)"),
2412
+ until: z5.string().optional().describe("Only commits before this date (ISO 8601 format)"),
2413
+ limit: z5.number().optional().describe("Maximum number of results (default: 20)")
2414
+ },
2415
+ execute: async (args, _ctx) => {
2416
+ const client = getGitLabClient();
2417
+ const commits = await client.listCommits(args.project_id, {
2418
+ ref: args.ref,
2419
+ path: args.path,
2420
+ since: args.since,
2421
+ until: args.until,
2422
+ limit: args.limit
2423
+ });
2424
+ return JSON.stringify(commits, null, 2);
2425
+ }
2426
+ }),
2427
+ gitlab_get_commit_diff: tool5({
2428
+ description: `Get the diff for a specific commit.`,
2429
+ args: {
2430
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2431
+ sha: z5.string().describe("The commit SHA")
2432
+ },
2433
+ execute: async (args, _ctx) => {
2434
+ const client = getGitLabClient();
2435
+ const diff = await client.getCommitDiff(args.project_id, args.sha);
2436
+ return JSON.stringify(diff, null, 2);
2437
+ }
2438
+ }),
2439
+ gitlab_create_commit: tool5({
2440
+ description: `Create a commit with multiple file actions.
2441
+ Supports creating, updating, deleting, moving files, and changing permissions.`,
2442
+ args: {
2443
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2444
+ branch: z5.string().describe("Name of the branch to commit into"),
2445
+ commit_message: z5.string().describe("Commit message"),
2446
+ actions: z5.array(
2447
+ z5.object({
2448
+ action: z5.enum(["create", "delete", "move", "update", "chmod"]).describe("The action to perform"),
2449
+ file_path: z5.string().describe("Full path to the file"),
2450
+ content: z5.string().optional().describe("File content (required for create/update)"),
2451
+ encoding: z5.enum(["text", "base64"]).optional().describe("Encoding of content (default: text)"),
2452
+ previous_path: z5.string().optional().describe("Original path (required for move)"),
2453
+ execute_filemode: z5.boolean().optional().describe("Enable/disable execute flag (for chmod)")
2454
+ })
2455
+ ).describe("Array of file actions to perform"),
2456
+ author_email: z5.string().optional().describe("Author email address"),
2457
+ author_name: z5.string().optional().describe("Author name"),
2458
+ start_branch: z5.string().optional().describe("Name of the branch to start from (if different from target branch)")
2459
+ },
2460
+ execute: async (args, _ctx) => {
2461
+ const client = getGitLabClient();
2462
+ const commit = await client.createCommit(args.project_id, {
2463
+ branch: args.branch,
2464
+ commit_message: args.commit_message,
2465
+ actions: args.actions,
2466
+ author_email: args.author_email,
2467
+ author_name: args.author_name,
2468
+ start_branch: args.start_branch
2469
+ });
2470
+ return JSON.stringify(commit, null, 2);
2471
+ }
2472
+ }),
2473
+ gitlab_list_repository_tree: tool5({
2474
+ description: `List files and directories in a repository.
2475
+ Returns the tree structure of the repository at a given path and ref.`,
2476
+ args: {
2477
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2478
+ path: z5.string().optional().describe("Path inside repository (default: root)"),
2479
+ ref: z5.string().optional().describe("Branch, tag, or commit SHA (default: default branch)"),
2480
+ recursive: z5.boolean().optional().describe("Get recursive tree (default: false)"),
2481
+ per_page: z5.number().optional().describe("Number of results per page (default: 20)")
2482
+ },
2483
+ execute: async (args, _ctx) => {
2484
+ const client = getGitLabClient();
2485
+ const tree = await client.listRepositoryTree(args.project_id, {
2486
+ path: args.path,
2487
+ ref: args.ref,
2488
+ recursive: args.recursive,
2489
+ per_page: args.per_page
2490
+ });
2491
+ return JSON.stringify(tree, null, 2);
2492
+ }
2493
+ }),
2494
+ gitlab_list_branches: tool5({
2495
+ description: `List branches in a repository.`,
2496
+ args: {
2497
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2498
+ search: z5.string().optional().describe("Search branches by name")
2499
+ },
2500
+ execute: async (args, _ctx) => {
2501
+ const client = getGitLabClient();
2502
+ const branches = await client.listBranches(args.project_id, args.search);
2503
+ return JSON.stringify(branches, null, 2);
2504
+ }
2505
+ }),
2506
+ // ========== Commit Discussion Tools ==========
2507
+ gitlab_list_commit_discussions: tool5({
2508
+ description: `List discussions (comment threads) on a commit.
2509
+ Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
2510
+ Use the discussion 'id' field to reply to a specific thread with gitlab_create_commit_note.`,
2511
+ args: {
2512
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2513
+ sha: z5.string().describe("The commit SHA")
2514
+ },
2515
+ execute: async (args, _ctx) => {
2516
+ const client = getGitLabClient();
2517
+ const discussions = await client.listCommitDiscussions(args.project_id, args.sha);
2518
+ return JSON.stringify(discussions, null, 2);
2519
+ }
2520
+ }),
2521
+ gitlab_get_commit_discussion: tool5({
2522
+ description: `Get a specific discussion thread from a commit with all its replies.
2523
+ Returns the discussion with its 'notes' array containing all comments in the thread.
2524
+ Use this to get the full context of a specific conversation.`,
2525
+ args: {
2526
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2527
+ sha: z5.string().describe("The commit SHA"),
2528
+ discussion_id: z5.string().describe("The ID of the discussion thread")
2529
+ },
2530
+ execute: async (args, _ctx) => {
2531
+ const client = getGitLabClient();
2532
+ const discussion = await client.getCommitDiscussion(
2533
+ args.project_id,
2534
+ args.sha,
2535
+ args.discussion_id
2536
+ );
2537
+ return JSON.stringify(discussion, null, 2);
2538
+ }
2539
+ }),
2540
+ gitlab_create_commit_note: tool5({
2541
+ description: `Add a comment/note to a commit.
2542
+ If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
2543
+ Optionally, you can create a line-specific comment by providing path, line, and line_type.
2544
+ Use gitlab_list_commit_discussions to find discussion IDs for existing threads.`,
2545
+ args: {
2546
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2547
+ sha: z5.string().describe("The commit SHA"),
2548
+ body: z5.string().describe("The content of the note/comment (supports Markdown)"),
2549
+ discussion_id: z5.string().optional().describe(
2550
+ "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
2551
+ ),
2552
+ path: z5.string().optional().describe("The file path to comment on (for line-specific comments)"),
2553
+ line: z5.number().optional().describe("The line number to comment on (for line-specific comments)"),
2554
+ line_type: z5.enum(["new", "old"]).optional().describe(
2555
+ 'The type of line being commented on: "new" for added lines, "old" for removed lines'
2556
+ )
2557
+ },
2558
+ execute: async (args, _ctx) => {
2559
+ const client = getGitLabClient();
2560
+ const note = await client.createCommitNote(args.project_id, args.sha, args.body, {
2561
+ discussion_id: args.discussion_id,
2562
+ path: args.path,
2563
+ line: args.line,
2564
+ line_type: args.line_type
2565
+ });
2566
+ return JSON.stringify(note, null, 2);
2567
+ }
2568
+ }),
2569
+ gitlab_create_commit_discussion: tool5({
2570
+ description: `Start a new discussion thread on a commit.
2571
+ Creates a new discussion with an initial comment. Optionally can be positioned on specific code.
2572
+ For general comments, just provide the body. For code comments, provide position information.`,
2573
+ args: {
2574
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2575
+ sha: z5.string().describe("The commit SHA"),
2576
+ body: z5.string().describe("The content of the initial comment (supports Markdown)"),
2577
+ position: z5.object({
2578
+ base_sha: z5.string().describe("SHA of the base commit"),
2579
+ start_sha: z5.string().describe("SHA of the start commit"),
2580
+ head_sha: z5.string().describe("SHA of the HEAD commit"),
2581
+ position_type: z5.enum(["text", "image"]).describe("Type of position (text or image)"),
2582
+ new_path: z5.string().optional().describe("Path of the file after changes"),
2583
+ old_path: z5.string().optional().describe("Path of the file before changes"),
2584
+ new_line: z5.number().optional().describe("Line number in the new version"),
2585
+ old_line: z5.number().optional().describe("Line number in the old version")
2586
+ }).optional().describe(
2587
+ "Position information for code comments. Required fields: base_sha, start_sha, head_sha, position_type. For line comments also provide new_path/old_path and new_line/old_line."
2588
+ )
2589
+ },
2590
+ execute: async (args, _ctx) => {
2591
+ const client = getGitLabClient();
2592
+ const discussion = await client.createCommitDiscussion(
2593
+ args.project_id,
2594
+ args.sha,
2595
+ args.body,
2596
+ args.position
2597
+ );
2598
+ return JSON.stringify(discussion, null, 2);
2599
+ }
2600
+ }),
2601
+ gitlab_get_commit_comments: tool5({
2602
+ description: `Get all comments on a specific commit.
2603
+ Returns all comments (notes) that have been added to a commit, including line-specific comments.
2604
+ This is different from discussions - it returns individual comments in a flat structure.`,
2605
+ args: {
2606
+ project_id: z5.string().describe("The project ID or URL-encoded path"),
2607
+ sha: z5.string().describe("The commit SHA")
2608
+ },
2609
+ execute: async (args, _ctx) => {
2610
+ const client = getGitLabClient();
2611
+ const comments = await client.getCommitComments(args.project_id, args.sha);
2612
+ return JSON.stringify(comments, null, 2);
2613
+ }
2614
+ })
2615
+ };
2616
+ var commitDiscussionTools = {
2617
+ gitlab_list_commit_discussions: repositoryTools.gitlab_list_commit_discussions,
2618
+ gitlab_get_commit_discussion: repositoryTools.gitlab_get_commit_discussion,
2619
+ gitlab_create_commit_note: repositoryTools.gitlab_create_commit_note,
2620
+ gitlab_create_commit_discussion: repositoryTools.gitlab_create_commit_discussion
2621
+ };
2622
+
2623
+ // src/tools/search.ts
2624
+ import { tool as tool6 } from "@opencode-ai/plugin";
2625
+ var z6 = tool6.schema;
2626
+ var searchTools = {
2627
+ gitlab_search: tool6({
2628
+ description: `Search across GitLab for various resources.
2629
+ Scopes: projects, issues, merge_requests, milestones, users, blobs (code), commits, notes, wiki_blobs`,
2630
+ args: {
2631
+ scope: z6.enum([
2632
+ "projects",
2633
+ "issues",
2634
+ "merge_requests",
2635
+ "milestones",
2636
+ "users",
2637
+ "blobs",
2638
+ "commits",
2639
+ "notes",
2640
+ "wiki_blobs"
2641
+ ]).describe("The scope of the search"),
2642
+ search: z6.string().describe("The search query"),
2643
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2644
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2645
+ },
2646
+ execute: async (args, _ctx) => {
2647
+ const client = getGitLabClient();
2648
+ const results = await client.search(args.scope, args.search, args.project_id, args.limit);
2649
+ return JSON.stringify(results, null, 2);
2650
+ }
2651
+ }),
2652
+ gitlab_issue_search: tool6({
2653
+ description: `Search issues by keyword across GitLab.
2654
+ Specialized search for issues with better filtering than the generic search.`,
2655
+ args: {
2656
+ search: z6.string().describe("The search query"),
2657
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2658
+ state: z6.enum(["opened", "closed", "all"]).optional().describe("Filter by issue state"),
2659
+ labels: z6.string().optional().describe("Comma-separated list of labels to filter by"),
2660
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2661
+ },
2662
+ execute: async (args, _ctx) => {
2663
+ const client = getGitLabClient();
2664
+ const results = await client.search("issues", args.search, args.project_id, args.limit);
2665
+ return JSON.stringify(results, null, 2);
2666
+ }
2667
+ }),
2668
+ gitlab_blob_search: tool6({
2669
+ description: `Search file content in repositories.
2670
+ Search for code/text within files across projects.`,
2671
+ args: {
2672
+ search: z6.string().describe("The search query (code/text to find)"),
2673
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2674
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2675
+ },
2676
+ execute: async (args, _ctx) => {
2677
+ const client = getGitLabClient();
2678
+ const results = await client.search("blobs", args.search, args.project_id, args.limit);
2679
+ return JSON.stringify(results, null, 2);
2680
+ }
2681
+ }),
2682
+ gitlab_merge_request_search: tool6({
2683
+ description: `Search merge requests by keyword.
2684
+ Specialized search for merge requests with better filtering than the generic search.`,
2685
+ args: {
2686
+ search: z6.string().describe("The search query"),
2687
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2688
+ state: z6.enum(["opened", "closed", "merged", "all"]).optional().describe("Filter by MR state"),
2689
+ labels: z6.string().optional().describe("Comma-separated list of labels to filter by"),
2690
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2691
+ },
2692
+ execute: async (args, _ctx) => {
2693
+ const client = getGitLabClient();
2694
+ const results = await client.search(
2695
+ "merge_requests",
2696
+ args.search,
2697
+ args.project_id,
2698
+ args.limit
2699
+ );
2700
+ return JSON.stringify(results, null, 2);
2701
+ }
2702
+ }),
2703
+ gitlab_commit_search: tool6({
2704
+ description: `Search for commits in a project or across GitLab.
2705
+ Returns commits matching the search query with commit details including SHA, title, message, author, and dates.
2706
+
2707
+ Supports searching by:
2708
+ - Commit message content
2709
+ - Author name or email
2710
+ - Commit SHA (partial or full)
2711
+
2712
+ Note: Advanced search features (Premium/Ultimate) provide better commit search capabilities.`,
2713
+ args: {
2714
+ search: z6.string().describe("The search query (commit message, author, or SHA)"),
2715
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2716
+ ref: z6.string().optional().describe("Branch or tag name to search on (defaults to default branch)"),
2717
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2718
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2719
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2720
+ },
2721
+ execute: async (args, _ctx) => {
2722
+ const client = getGitLabClient();
2723
+ const results = await client.searchCommits(args.search, args.project_id, {
2724
+ ref: args.ref,
2725
+ order_by: args.order_by,
2726
+ sort: args.sort,
2727
+ limit: args.limit
2728
+ });
2729
+ return JSON.stringify(results, null, 2);
2730
+ }
2731
+ }),
2732
+ gitlab_group_project_search: tool6({
2733
+ description: `Search for projects within a specific group.
2734
+ Returns projects matching the search query within the specified group and its subgroups.
2735
+
2736
+ Useful for:
2737
+ - Finding projects by name or description within a group
2738
+ - Discovering projects in large group hierarchies
2739
+ - Filtering group projects by keywords`,
2740
+ args: {
2741
+ group_id: z6.string().describe("The group ID or URL-encoded path"),
2742
+ search: z6.string().describe("The search query (project name or description)"),
2743
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2744
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2745
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2746
+ },
2747
+ execute: async (args, _ctx) => {
2748
+ const client = getGitLabClient();
2749
+ const results = await client.searchGroupProjects(args.group_id, args.search, {
2750
+ order_by: args.order_by,
2751
+ sort: args.sort,
2752
+ limit: args.limit
2753
+ });
2754
+ return JSON.stringify(results, null, 2);
2755
+ }
2756
+ }),
2757
+ gitlab_milestone_search: tool6({
2758
+ description: `Search for milestones in a project or across GitLab.
2759
+ Returns milestones matching the search query with details including title, description, state, and dates.
2760
+
2761
+ Useful for:
2762
+ - Finding milestones by title or description
2763
+ - Discovering active or closed milestones
2764
+ - Planning and tracking project milestones`,
2765
+ args: {
2766
+ search: z6.string().describe("The search query (milestone title or description)"),
2767
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2768
+ state: z6.enum(["active", "closed", "all"]).optional().describe("Filter by milestone state"),
2769
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2770
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2771
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2772
+ },
2773
+ execute: async (args, _ctx) => {
2774
+ const client = getGitLabClient();
2775
+ const results = await client.searchMilestones(args.search, args.project_id, {
2776
+ state: args.state,
2777
+ order_by: args.order_by,
2778
+ sort: args.sort,
2779
+ limit: args.limit
2780
+ });
2781
+ return JSON.stringify(results, null, 2);
2782
+ }
2783
+ }),
2784
+ gitlab_note_search: tool6({
2785
+ description: `Search for notes/comments in a project.
2786
+ Returns notes (comments) matching the search query from issues, merge requests, commits, and snippets.
2787
+
2788
+ Useful for:
2789
+ - Finding specific comments or discussions
2790
+ - Tracking feedback and review comments
2791
+ - Searching for mentions or keywords in conversations
2792
+
2793
+ Note: This scope requires Premium or Ultimate tier with advanced search enabled.`,
2794
+ args: {
2795
+ search: z6.string().describe("The search query (comment text)"),
2796
+ project_id: z6.string().describe("The project ID or URL-encoded path to search in"),
2797
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2798
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2799
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2800
+ },
2801
+ execute: async (args, _ctx) => {
2802
+ const client = getGitLabClient();
2803
+ const results = await client.searchNotes(args.search, args.project_id, {
2804
+ order_by: args.order_by,
2805
+ sort: args.sort,
2806
+ limit: args.limit
2807
+ });
2808
+ return JSON.stringify(results, null, 2);
2809
+ }
2810
+ }),
2811
+ gitlab_user_search: tool6({
2812
+ description: `Search for users by name or email across GitLab.
2813
+ Returns users matching the search query with details including username, name, state, and avatar.
2814
+
2815
+ Useful for:
2816
+ - Finding users to assign to issues or merge requests
2817
+ - Looking up user information by name or email
2818
+ - Discovering team members and collaborators`,
2819
+ args: {
2820
+ search: z6.string().describe("The search query (user name or email)"),
2821
+ project_id: z6.string().optional().describe("Limit search to project members (optional)"),
2822
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2823
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2824
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2825
+ },
2826
+ execute: async (args, _ctx) => {
2827
+ const client = getGitLabClient();
2828
+ const results = await client.searchUsers(args.search, args.project_id, {
2829
+ order_by: args.order_by,
2830
+ sort: args.sort,
2831
+ limit: args.limit
2832
+ });
2833
+ return JSON.stringify(results, null, 2);
2834
+ }
2835
+ }),
2836
+ gitlab_wiki_blob_search: tool6({
2837
+ description: `Search for wiki content (blobs) in a project or across GitLab.
2838
+ Returns wiki pages matching the search query with content snippets and file information.
2839
+
2840
+ Supports advanced filters (use in search query):
2841
+ - filename:some_name* - Filter by filename with wildcards
2842
+ - path:some/path* - Filter by path with wildcards
2843
+ - extension:md - Filter by file extension
2844
+
2845
+ Examples:
2846
+ - "installation" - Search for "installation" in wiki content
2847
+ - "setup filename:getting-started*" - Search in files starting with "getting-started"
2848
+ - "api extension:md" - Search for "api" in markdown files
2849
+
2850
+ Note: Advanced search (Premium/Ultimate) provides better wiki search capabilities.`,
2851
+ args: {
2852
+ search: z6.string().describe(
2853
+ "The search query (supports filters: filename:, path:, extension: with wildcards)"
2854
+ ),
2855
+ project_id: z6.string().optional().describe("Limit search to a specific project (optional)"),
2856
+ ref: z6.string().optional().describe("Branch or tag name to search on (defaults to default branch)"),
2857
+ order_by: z6.enum(["created_at"]).optional().describe("Order results by created_at (default: created_at desc)"),
2858
+ sort: z6.enum(["asc", "desc"]).optional().describe("Sort order (asc or desc)"),
2859
+ limit: z6.number().optional().describe("Maximum number of results (default: 20)")
2860
+ },
2861
+ execute: async (args, _ctx) => {
2862
+ const client = getGitLabClient();
2863
+ const results = await client.searchWikiBlobs(args.search, args.project_id, {
2864
+ ref: args.ref,
2865
+ order_by: args.order_by,
2866
+ sort: args.sort,
2867
+ limit: args.limit
2868
+ });
2869
+ return JSON.stringify(results, null, 2);
2870
+ }
2871
+ }),
2872
+ gitlab_documentation_search: tool6({
2873
+ description: `Search GitLab official documentation at docs.gitlab.com.
2874
+ Returns relevant documentation pages matching the search query.
2875
+
2876
+ Useful for:
2877
+ - Finding GitLab feature documentation
2878
+ - Learning about GitLab APIs and integrations
2879
+ - Discovering best practices and guides
2880
+ - Troubleshooting GitLab issues
2881
+
2882
+ Note: This searches the public GitLab documentation site, not your instance's documentation.`,
2883
+ args: {
2884
+ search: z6.string().describe("The search query (documentation topic or keyword)"),
2885
+ limit: z6.number().optional().describe("Maximum number of results (default: 10)")
2886
+ },
2887
+ execute: async (args, _ctx) => {
2888
+ const client = getGitLabClient();
2889
+ const results = await client.searchDocumentation(args.search, args.limit);
2890
+ return JSON.stringify(results, null, 2);
2891
+ }
2892
+ })
2893
+ };
2894
+
2895
+ // src/tools/projects.ts
2896
+ import { tool as tool7 } from "@opencode-ai/plugin";
2897
+ var z7 = tool7.schema;
2898
+ var projectTools = {
2899
+ gitlab_get_project: tool7({
2900
+ description: `Get details of a specific project.`,
2901
+ args: {
2902
+ project_id: z7.string().describe('The project ID or URL-encoded path (e.g., "gitlab-org/gitlab")')
2903
+ },
2904
+ execute: async (args, _ctx) => {
2905
+ const client = getGitLabClient();
2906
+ const project = await client.getProject(args.project_id);
2907
+ return JSON.stringify(project, null, 2);
2908
+ }
2909
+ }),
2910
+ gitlab_list_project_members: tool7({
2911
+ description: `List members of a project.`,
2912
+ args: {
2913
+ project_id: z7.string().describe("The project ID or URL-encoded path")
2914
+ },
2915
+ execute: async (args, _ctx) => {
2916
+ const client = getGitLabClient();
2917
+ const members = await client.listProjectMembers(args.project_id);
2918
+ return JSON.stringify(members, null, 2);
2919
+ }
2920
+ })
2921
+ };
2922
+ var userTools = {
2923
+ gitlab_get_current_user: tool7({
2924
+ description: `Get current user information.
2925
+ Returns details about the authenticated user including username, email, and permissions.`,
2926
+ args: {},
2927
+ execute: async (_args, _ctx) => {
2928
+ const client = getGitLabClient();
2929
+ const user = await client.getCurrentUser();
2930
+ return JSON.stringify(user, null, 2);
2931
+ }
2932
+ })
2933
+ };
2934
+
2935
+ // src/tools/security.ts
2936
+ import { tool as tool8 } from "@opencode-ai/plugin";
2937
+ var z8 = tool8.schema;
2938
+ var securityTools = {
2939
+ gitlab_list_vulnerabilities: tool8({
2940
+ description: `List persisted vulnerabilities for a project.
2941
+ Returns security vulnerabilities detected in the project.`,
2942
+ args: {
2943
+ project_id: z8.string().describe("The project ID or URL-encoded path"),
2944
+ state: z8.enum(["detected", "confirmed", "dismissed", "resolved"]).optional().describe("Filter by vulnerability state"),
2945
+ severity: z8.enum(["undefined", "info", "unknown", "low", "medium", "high", "critical"]).optional().describe("Filter by severity level"),
2946
+ report_type: z8.enum([
2947
+ "sast",
2948
+ "dast",
2949
+ "dependency_scanning",
2950
+ "container_scanning",
2951
+ "secret_detection",
2952
+ "coverage_fuzzing",
2953
+ "api_fuzzing"
2954
+ ]).optional().describe("Filter by report type"),
2955
+ limit: z8.number().optional().describe("Maximum number of results (default: 20)")
2956
+ },
2957
+ execute: async (args, _ctx) => {
2958
+ const client = getGitLabClient();
2959
+ const vulnerabilities = await client.listVulnerabilities(args.project_id, {
2960
+ state: args.state,
2961
+ severity: args.severity,
2962
+ report_type: args.report_type,
2963
+ limit: args.limit
2964
+ });
2965
+ return JSON.stringify(vulnerabilities, null, 2);
2966
+ }
2967
+ }),
2968
+ gitlab_get_vulnerability_details: tool8({
2969
+ description: `Get details for a specific vulnerability.
2970
+ Returns full information about a security vulnerability including description, location, and remediation.`,
2971
+ args: {
2972
+ project_id: z8.string().describe("The project ID or URL-encoded path"),
2973
+ vulnerability_id: z8.number().describe("The ID of the vulnerability")
2974
+ },
2975
+ execute: async (args, _ctx) => {
2976
+ const client = getGitLabClient();
2977
+ const vulnerability = await client.getVulnerabilityDetails(
2978
+ args.project_id,
2979
+ args.vulnerability_id
2980
+ );
2981
+ return JSON.stringify(vulnerability, null, 2);
2982
+ }
2983
+ }),
2984
+ gitlab_create_vulnerability_issue: tool8({
2985
+ description: `Create a new issue linked to one or more security vulnerabilities.
2986
+ This creates an issue in the project and automatically links it to the specified vulnerabilities.
2987
+ Requires Developer role or higher.`,
2988
+ args: {
2989
+ project_path: z8.string().describe('Full path of the project (e.g., "group/project" or "group/subgroup/project")'),
2990
+ vulnerability_ids: z8.array(z8.string()).describe(
2991
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
2992
+ )
2993
+ },
2994
+ execute: async (args, _ctx) => {
2995
+ const client = getGitLabClient();
2996
+ const issue = await client.createVulnerabilityIssue(
2997
+ args.project_path,
2998
+ args.vulnerability_ids
2999
+ );
3000
+ return JSON.stringify(issue, null, 2);
3001
+ }
3002
+ }),
3003
+ gitlab_dismiss_vulnerability: tool8({
3004
+ description: `Dismiss a security vulnerability with a reason.
3005
+ Use this when a vulnerability is not applicable, is a false positive, or has been mitigated.
3006
+ Requires Developer role or higher.`,
3007
+ args: {
3008
+ vulnerability_id: z8.string().describe(
3009
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3010
+ ),
3011
+ reason: z8.enum([
3012
+ "ACCEPTABLE_RISK",
3013
+ "FALSE_POSITIVE",
3014
+ "MITIGATING_CONTROL",
3015
+ "USED_IN_TESTS",
3016
+ "NOT_APPLICABLE"
3017
+ ]).describe("Reason for dismissing the vulnerability"),
3018
+ comment: z8.string().optional().describe("Optional comment explaining the dismissal")
3019
+ },
3020
+ execute: async (args, _ctx) => {
3021
+ const client = getGitLabClient();
3022
+ const vulnerability = await client.dismissVulnerability(
3023
+ args.vulnerability_id,
3024
+ args.reason,
3025
+ args.comment
3026
+ );
3027
+ return JSON.stringify(vulnerability, null, 2);
3028
+ }
3029
+ }),
3030
+ gitlab_confirm_vulnerability: tool8({
3031
+ description: `Confirm a security vulnerability.
3032
+ Use this to acknowledge that a vulnerability is valid and needs attention.
3033
+ Requires Developer role or higher.`,
3034
+ args: {
3035
+ vulnerability_id: z8.string().describe(
3036
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3037
+ ),
3038
+ comment: z8.string().optional().describe("Optional comment about the confirmation")
3039
+ },
3040
+ execute: async (args, _ctx) => {
3041
+ const client = getGitLabClient();
3042
+ const vulnerability = await client.confirmVulnerability(args.vulnerability_id, args.comment);
3043
+ return JSON.stringify(vulnerability, null, 2);
3044
+ }
3045
+ }),
3046
+ gitlab_revert_vulnerability_to_detected: tool8({
3047
+ description: `Revert a vulnerability back to detected state.
3048
+ Use this to undo a previous confirmation or dismissal.
3049
+ Requires Developer role or higher.`,
3050
+ args: {
3051
+ vulnerability_id: z8.string().describe(
3052
+ 'Vulnerability ID in format "gid://gitlab/Vulnerability/{id}". Get this from gitlab_list_vulnerabilities.'
3053
+ ),
3054
+ comment: z8.string().optional().describe("Optional comment about reverting the state")
3055
+ },
3056
+ execute: async (args, _ctx) => {
3057
+ const client = getGitLabClient();
3058
+ const vulnerability = await client.revertVulnerability(args.vulnerability_id, args.comment);
3059
+ return JSON.stringify(vulnerability, null, 2);
3060
+ }
3061
+ }),
3062
+ gitlab_update_vulnerability_severity: tool8({
3063
+ description: `Update the severity level of one or more vulnerabilities.
3064
+ Use this to adjust the severity rating based on your assessment.
3065
+ Requires Developer role or higher.`,
3066
+ args: {
3067
+ vulnerability_ids: z8.array(z8.string()).describe(
3068
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
3069
+ ),
3070
+ severity: z8.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "UNKNOWN"]).describe("New severity level for the vulnerabilities"),
3071
+ comment: z8.string().describe("Comment explaining the severity change (required)")
3072
+ },
3073
+ execute: async (args, _ctx) => {
3074
+ const client = getGitLabClient();
3075
+ const vulnerabilities = await client.updateVulnerabilitySeverity(
3076
+ args.vulnerability_ids,
3077
+ args.severity,
3078
+ args.comment
3079
+ );
3080
+ return JSON.stringify(vulnerabilities, null, 2);
3081
+ }
3082
+ }),
3083
+ gitlab_link_vulnerability_to_issue: tool8({
3084
+ description: `Link an existing issue to one or more vulnerabilities.
3085
+ Use this to associate vulnerabilities with an existing issue for tracking.
3086
+ Requires Developer role or higher.`,
3087
+ args: {
3088
+ issue_id: z8.string().describe(
3089
+ 'Issue ID in format "gid://gitlab/Issue/{id}". You can construct this from the issue IID.'
3090
+ ),
3091
+ vulnerability_ids: z8.array(z8.string()).describe(
3092
+ 'Array of vulnerability IDs in format "gid://gitlab/Vulnerability/{id}". Get these from gitlab_list_vulnerabilities.'
3093
+ )
3094
+ },
3095
+ execute: async (args, _ctx) => {
3096
+ const client = getGitLabClient();
3097
+ const issue = await client.linkVulnerabilityToIssue(args.issue_id, args.vulnerability_ids);
3098
+ return JSON.stringify(issue, null, 2);
3099
+ }
3100
+ })
3101
+ };
3102
+
3103
+ // src/tools/snippets.ts
3104
+ import { tool as tool9 } from "@opencode-ai/plugin";
3105
+ var z9 = tool9.schema;
3106
+ var snippetTools = {
3107
+ gitlab_list_snippet_discussions: tool9({
3108
+ description: `List discussions (comment threads) on a project snippet.
3109
+ Returns all discussion threads with nested notes. Each discussion contains a 'notes' array with individual comments.
3110
+ Use the discussion 'id' field to reply to a specific thread with gitlab_create_snippet_note.`,
3111
+ args: {
3112
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3113
+ snippet_id: z9.number().describe("The ID of the snippet")
3114
+ },
3115
+ execute: async (args, _ctx) => {
3116
+ const client = getGitLabClient();
3117
+ const discussions = await client.listSnippetDiscussions(args.project_id, args.snippet_id);
3118
+ return JSON.stringify(discussions, null, 2);
3119
+ }
3120
+ }),
3121
+ gitlab_get_snippet_discussion: tool9({
3122
+ description: `Get a specific discussion thread from a snippet with all its replies.
3123
+ Returns the discussion with its 'notes' array containing all comments in the thread.
3124
+ Use this to get the full context of a specific conversation.`,
3125
+ args: {
3126
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3127
+ snippet_id: z9.number().describe("The ID of the snippet"),
3128
+ discussion_id: z9.string().describe("The ID of the discussion thread")
3129
+ },
3130
+ execute: async (args, _ctx) => {
3131
+ const client = getGitLabClient();
3132
+ const discussion = await client.getSnippetDiscussion(
3133
+ args.project_id,
3134
+ args.snippet_id,
3135
+ args.discussion_id
3136
+ );
3137
+ return JSON.stringify(discussion, null, 2);
3138
+ }
3139
+ }),
3140
+ gitlab_list_snippet_notes: tool9({
3141
+ description: `List all notes/comments on a project snippet in a flat structure.
3142
+ Returns all comments including system notes in chronological order.`,
3143
+ args: {
3144
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3145
+ snippet_id: z9.number().describe("The ID of the snippet")
3146
+ },
3147
+ execute: async (args, _ctx) => {
3148
+ const client = getGitLabClient();
3149
+ const notes = await client.listSnippetNotes(args.project_id, args.snippet_id);
3150
+ return JSON.stringify(notes, null, 2);
3151
+ }
3152
+ }),
3153
+ gitlab_create_snippet_note: tool9({
3154
+ description: `Add a comment/note to a project snippet.
3155
+ If discussion_id is provided, the note will be added as a reply to an existing discussion thread.
3156
+ Use gitlab_list_snippet_discussions to find discussion IDs for existing threads.`,
3157
+ args: {
3158
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3159
+ snippet_id: z9.number().describe("The ID of the snippet"),
3160
+ body: z9.string().describe("The content of the note/comment (supports Markdown)"),
3161
+ discussion_id: z9.string().optional().describe(
3162
+ "The ID of a discussion thread to reply to. If provided, the note will be added as a reply to that discussion."
3163
+ )
3164
+ },
3165
+ execute: async (args, _ctx) => {
3166
+ const client = getGitLabClient();
3167
+ const note = await client.createSnippetNote(
3168
+ args.project_id,
3169
+ args.snippet_id,
3170
+ args.body,
3171
+ args.discussion_id
3172
+ );
3173
+ return JSON.stringify(note, null, 2);
3174
+ }
3175
+ }),
3176
+ gitlab_create_snippet_discussion: tool9({
3177
+ description: `Start a new discussion thread on a project snippet.
3178
+ Creates a new discussion with an initial comment.`,
3179
+ args: {
3180
+ project_id: z9.string().describe("The project ID or URL-encoded path"),
3181
+ snippet_id: z9.number().describe("The ID of the snippet"),
3182
+ body: z9.string().describe("The content of the initial comment (supports Markdown)")
3183
+ },
3184
+ execute: async (args, _ctx) => {
3185
+ const client = getGitLabClient();
3186
+ const discussion = await client.createSnippetDiscussion(
3187
+ args.project_id,
3188
+ args.snippet_id,
3189
+ args.body
3190
+ );
3191
+ return JSON.stringify(discussion, null, 2);
3192
+ }
3193
+ })
3194
+ };
3195
+
3196
+ // src/tools/todos.ts
3197
+ import { tool as tool10 } from "@opencode-ai/plugin";
3198
+ var z10 = tool10.schema;
3199
+ var todoTools = {
3200
+ gitlab_list_todos: tool10({
3201
+ description: `List TODO items for the current user.
3202
+ Returns a list of pending or done TODO items assigned to the authenticated user.
3203
+ TODOs are created when you are assigned to an issue/MR, mentioned in a comment, or when someone requests your review.`,
3204
+ args: {
3205
+ action: z10.enum([
3206
+ "assigned",
3207
+ "mentioned",
3208
+ "build_failed",
3209
+ "marked",
3210
+ "approval_required",
3211
+ "unmergeable",
3212
+ "directly_addressed",
3213
+ "merge_train_removed",
3214
+ "review_requested"
3215
+ ]).optional().describe("Filter by action type"),
3216
+ author_id: z10.number().optional().describe("Filter by author ID"),
3217
+ project_id: z10.string().optional().describe("Filter by project ID or path"),
3218
+ group_id: z10.string().optional().describe("Filter by group ID"),
3219
+ state: z10.enum(["pending", "done"]).optional().describe("Filter by state (default: pending)"),
3220
+ type: z10.enum(["Issue", "MergeRequest", "DesignManagement::Design", "Alert"]).optional().describe("Filter by target type"),
3221
+ limit: z10.number().optional().describe("Maximum number of results (default: 20)")
3222
+ },
3223
+ execute: async (args, _ctx) => {
3224
+ const client = getGitLabClient();
3225
+ const todos = await client.listTodos({
3226
+ action: args.action,
3227
+ author_id: args.author_id,
3228
+ project_id: args.project_id,
3229
+ group_id: args.group_id,
3230
+ state: args.state,
3231
+ type: args.type,
3232
+ limit: args.limit
3233
+ });
3234
+ return JSON.stringify(todos, null, 2);
3235
+ }
3236
+ }),
3237
+ gitlab_mark_todo_done: tool10({
3238
+ description: `Mark a specific TODO item as done.
3239
+ Use this to mark a single TODO as completed.`,
3240
+ args: {
3241
+ todo_id: z10.number().describe("The ID of the TODO item to mark as done")
3242
+ },
3243
+ execute: async (args, _ctx) => {
3244
+ const client = getGitLabClient();
3245
+ const result = await client.markTodoAsDone(args.todo_id);
3246
+ return JSON.stringify(result, null, 2);
3247
+ }
3248
+ }),
3249
+ gitlab_mark_all_todos_done: tool10({
3250
+ description: `Mark all TODO items as done.
3251
+ Use this to mark all pending TODOs for the current user as completed.`,
3252
+ args: {},
3253
+ execute: async (_args, _ctx) => {
3254
+ const client = getGitLabClient();
3255
+ const result = await client.markAllTodosAsDone();
3256
+ return JSON.stringify(result, null, 2);
3257
+ }
3258
+ }),
3259
+ gitlab_get_todo_count: tool10({
3260
+ description: `Get the count of pending TODO items.
3261
+ Returns the total number of pending TODOs for the current user.`,
3262
+ args: {},
3263
+ execute: async (_args, _ctx) => {
3264
+ const client = getGitLabClient();
3265
+ const count = await client.getTodoCount();
3266
+ return JSON.stringify(count, null, 2);
3267
+ }
3268
+ })
3269
+ };
3270
+
3271
+ // src/tools/wikis.ts
3272
+ import { tool as tool11 } from "@opencode-ai/plugin";
3273
+ var z11 = tool11.schema;
3274
+ var wikiTools = {
3275
+ gitlab_get_wiki_page: tool11({
3276
+ description: `Get a wiki page with its content.
3277
+ Returns the wiki page content and metadata.`,
3278
+ args: {
3279
+ project_id: z11.string().describe("The project ID or URL-encoded path"),
3280
+ slug: z11.string().describe("The slug (URL-friendly name) of the wiki page")
3281
+ },
3282
+ execute: async (args, _ctx) => {
3283
+ const client = getGitLabClient();
3284
+ const page = await client.getWikiPage(args.project_id, args.slug);
3285
+ return JSON.stringify(page, null, 2);
3286
+ }
3287
+ })
3288
+ };
3289
+
3290
+ // src/tools/work-items.ts
3291
+ import { tool as tool12 } from "@opencode-ai/plugin";
3292
+ var z12 = tool12.schema;
3293
+ var workItemTools = {
3294
+ gitlab_get_work_item: tool12({
3295
+ description: `Get a single work item (issue, epic, task, etc.).
3296
+ Work items are the new unified model for issues, epics, tasks, and other work tracking items in GitLab.`,
3297
+ args: {
3298
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3299
+ work_item_id: z12.number().describe("The ID of the work item")
3300
+ },
3301
+ execute: async (args, _ctx) => {
3302
+ const client = getGitLabClient();
3303
+ const workItem = await client.getWorkItem(args.project_id, args.work_item_id);
3304
+ return JSON.stringify(workItem, null, 2);
3305
+ }
3306
+ }),
3307
+ gitlab_list_work_items: tool12({
3308
+ description: `List work items in a project or group.
3309
+ Work items include issues, epics, tasks, and other work tracking items.`,
3310
+ args: {
3311
+ project_id: z12.string().optional().describe("The project ID or URL-encoded path"),
3312
+ group_id: z12.string().optional().describe("The group ID or URL-encoded path"),
3313
+ state: z12.enum(["opened", "closed", "all"]).optional().describe("Filter by state (default: opened)"),
3314
+ search: z12.string().optional().describe("Search work items by title or description"),
3315
+ labels: z12.string().optional().describe("Comma-separated list of labels to filter by"),
3316
+ work_item_type: z12.string().optional().describe("Filter by work item type (e.g., 'Issue', 'Epic', 'Task')"),
3317
+ limit: z12.number().optional().describe("Maximum number of results (default: 20)")
3318
+ },
3319
+ execute: async (args, _ctx) => {
3320
+ const client = getGitLabClient();
3321
+ const workItems = await client.listWorkItems({
3322
+ projectId: args.project_id,
3323
+ groupId: args.group_id,
3324
+ state: args.state,
3325
+ search: args.search,
3326
+ labels: args.labels,
3327
+ work_item_type: args.work_item_type,
3328
+ limit: args.limit
3329
+ });
3330
+ return JSON.stringify(workItems, null, 2);
3331
+ }
3332
+ }),
3333
+ gitlab_get_work_item_notes: tool12({
3334
+ description: `Get all comments for a work item.
3335
+ Returns all notes/comments on the work item in chronological order.`,
3336
+ args: {
3337
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3338
+ work_item_id: z12.number().describe("The ID of the work item")
3339
+ },
3340
+ execute: async (args, _ctx) => {
3341
+ const client = getGitLabClient();
3342
+ const notes = await client.getWorkItemNotes(args.project_id, args.work_item_id);
3343
+ return JSON.stringify(notes, null, 2);
3344
+ }
3345
+ }),
3346
+ gitlab_create_work_item: tool12({
3347
+ description: `Create a new work item (issue, task, etc.).
3348
+ Work items are the new unified model for issues, epics, tasks, and other work tracking items.`,
3349
+ args: {
3350
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3351
+ title: z12.string().describe("The title of the work item"),
3352
+ work_item_type_id: z12.number().describe("The ID of the work item type (e.g., 1 for Issue, 2 for Task)"),
3353
+ description: z12.string().optional().describe("The description of the work item (supports Markdown)"),
3354
+ labels: z12.array(z12.string()).optional().describe("Array of label names"),
3355
+ assignee_ids: z12.array(z12.number()).optional().describe("Array of user IDs to assign")
3356
+ },
3357
+ execute: async (args, _ctx) => {
3358
+ const client = getGitLabClient();
3359
+ const workItem = await client.createWorkItem(args.project_id, {
3360
+ title: args.title,
3361
+ work_item_type_id: args.work_item_type_id,
3362
+ description: args.description,
3363
+ labels: args.labels,
3364
+ assignee_ids: args.assignee_ids
3365
+ });
3366
+ return JSON.stringify(workItem, null, 2);
3367
+ }
3368
+ }),
3369
+ gitlab_update_work_item: tool12({
3370
+ description: `Update an existing work item.
3371
+ Can update title, description, state, labels, and assignees.`,
3372
+ args: {
3373
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3374
+ work_item_id: z12.number().describe("The ID of the work item"),
3375
+ title: z12.string().optional().describe("The new title"),
3376
+ description: z12.string().optional().describe("The new description (supports Markdown)"),
3377
+ state_event: z12.enum(["close", "reopen"]).optional().describe("Change the state (close or reopen)"),
3378
+ labels: z12.array(z12.string()).optional().describe("Array of label names"),
3379
+ assignee_ids: z12.array(z12.number()).optional().describe("Array of user IDs to assign")
3380
+ },
3381
+ execute: async (args, _ctx) => {
3382
+ const client = getGitLabClient();
3383
+ const workItem = await client.updateWorkItem(args.project_id, args.work_item_id, {
3384
+ title: args.title,
3385
+ description: args.description,
3386
+ state_event: args.state_event,
3387
+ labels: args.labels,
3388
+ assignee_ids: args.assignee_ids
3389
+ });
3390
+ return JSON.stringify(workItem, null, 2);
3391
+ }
3392
+ }),
3393
+ gitlab_create_work_item_note: tool12({
3394
+ description: `Create a comment on a work item.`,
3395
+ args: {
3396
+ project_id: z12.string().describe("The project ID or URL-encoded path"),
3397
+ work_item_id: z12.number().describe("The ID of the work item"),
3398
+ body: z12.string().describe("The content of the note/comment (supports Markdown)")
3399
+ },
3400
+ execute: async (args, _ctx) => {
3401
+ const client = getGitLabClient();
3402
+ const note = await client.createWorkItemNote(args.project_id, args.work_item_id, args.body);
3403
+ return JSON.stringify(note, null, 2);
3404
+ }
3405
+ })
3406
+ };
3407
+
3408
+ // src/tools/discussions.ts
3409
+ import { tool as tool13 } from "@opencode-ai/plugin";
3410
+ var z13 = tool13.schema;
3411
+ var discussionTools = {
3412
+ gitlab_reply_to_discussion: tool13({
3413
+ description: `Universal tool for replying to any discussion thread across GitLab resources.
3414
+ Supports replying to discussions on: merge requests, issues, epics, commits, and snippets.
3415
+ This provides a consistent interface for thread replies regardless of the resource type.`,
3416
+ args: {
3417
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("The type of resource the discussion belongs to"),
3418
+ project_id: z13.string().optional().describe("The project ID or URL-encoded path (required for all except epic)"),
3419
+ group_id: z13.string().optional().describe("The group ID or URL-encoded path (required for epic)"),
3420
+ iid: z13.number().optional().describe("The internal ID of the resource (required for merge_request, issue, epic)"),
3421
+ sha: z13.string().optional().describe("The commit SHA (required for commit)"),
3422
+ snippet_id: z13.number().optional().describe("The snippet ID (required for snippet)"),
3423
+ discussion_id: z13.string().describe("The ID of the discussion thread to reply to"),
3424
+ body: z13.string().describe("The content of the reply (supports Markdown)")
3425
+ },
3426
+ execute: async (args, _ctx) => {
3427
+ const client = getGitLabClient();
3428
+ const note = await client.replyToDiscussion(
3429
+ args.resource_type,
3430
+ {
3431
+ projectId: args.project_id,
3432
+ groupId: args.group_id,
3433
+ iid: args.iid,
3434
+ sha: args.sha,
3435
+ snippetId: args.snippet_id
3436
+ },
3437
+ args.discussion_id,
3438
+ args.body
3439
+ );
3440
+ return JSON.stringify(note, null, 2);
3441
+ }
3442
+ }),
3443
+ gitlab_get_discussion: tool13({
3444
+ description: `Universal tool for fetching discussion details from any GitLab resource.
3445
+ Supports getting discussions from: merge requests, issues, epics, commits, and snippets.
3446
+ Returns the discussion with its 'notes' array containing all comments in the thread.`,
3447
+ args: {
3448
+ resource_type: z13.enum(["merge_request", "issue", "epic", "commit", "snippet"]).describe("The type of resource the discussion belongs to"),
3449
+ project_id: z13.string().optional().describe("The project ID or URL-encoded path (required for all except epic)"),
3450
+ group_id: z13.string().optional().describe("The group ID or URL-encoded path (required for epic)"),
3451
+ iid: z13.number().optional().describe("The internal ID of the resource (required for merge_request, issue, epic)"),
3452
+ sha: z13.string().optional().describe("The commit SHA (required for commit)"),
3453
+ snippet_id: z13.number().optional().describe("The snippet ID (required for snippet)"),
3454
+ discussion_id: z13.string().describe("The ID of the discussion thread to fetch")
3455
+ },
3456
+ execute: async (args, _ctx) => {
3457
+ const client = getGitLabClient();
3458
+ const discussion = await client.getDiscussion(
3459
+ args.resource_type,
3460
+ {
3461
+ projectId: args.project_id,
3462
+ groupId: args.group_id,
3463
+ iid: args.iid,
3464
+ sha: args.sha,
3465
+ snippetId: args.snippet_id
3466
+ },
3467
+ args.discussion_id
3468
+ );
3469
+ return JSON.stringify(discussion, null, 2);
3470
+ }
3471
+ })
3472
+ };
3473
+
3474
+ // src/tools/git.ts
3475
+ import { tool as tool14 } from "@opencode-ai/plugin";
3476
+
3477
+ // src/client/git.ts
3478
+ import { execFile } from "child_process";
3479
+ import { promisify } from "util";
3480
+ import { existsSync, statSync } from "fs";
3481
+ import { resolve, join } from "path";
3482
+ var execFileAsync = promisify(execFile);
3483
+ var GitClient = class _GitClient {
3484
+ workingDirectory;
3485
+ // Whitelist of allowed git commands (read-only operations)
3486
+ static ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3487
+ "status",
3488
+ "log",
3489
+ "show",
3490
+ "diff",
3491
+ "branch",
3492
+ "tag",
3493
+ "remote",
3494
+ "ls-files",
3495
+ "ls-remote",
3496
+ "rev-parse",
3497
+ "describe",
3498
+ "config",
3499
+ "blame",
3500
+ "shortlog",
3501
+ "reflog",
3502
+ "show-ref",
3503
+ "ls-tree",
3504
+ "cat-file",
3505
+ "rev-list",
3506
+ "name-rev",
3507
+ "show-branch",
3508
+ "whatchanged",
3509
+ "grep",
3510
+ "annotate"
3511
+ ]);
3512
+ // Commands that are explicitly forbidden (destructive operations)
3513
+ static FORBIDDEN_COMMANDS = /* @__PURE__ */ new Set([
3514
+ "push",
3515
+ "pull",
3516
+ "fetch",
3517
+ "clone",
3518
+ "commit",
3519
+ "add",
3520
+ "rm",
3521
+ "mv",
3522
+ "reset",
3523
+ "rebase",
3524
+ "merge",
3525
+ "cherry-pick",
3526
+ "revert",
3527
+ "clean",
3528
+ "checkout",
3529
+ "switch",
3530
+ "restore",
3531
+ "stash",
3532
+ "submodule",
3533
+ "worktree"
3534
+ ]);
3535
+ // Shell operators that should be blocked
3536
+ static SHELL_OPERATORS = [";", "&&", "||", "|", "`", "$("];
3537
+ constructor(workingDirectory) {
3538
+ this.workingDirectory = workingDirectory || process.cwd();
3539
+ }
3540
+ /**
3541
+ * Execute a git command in the repository working directory
3542
+ *
3543
+ * Security: Uses execFile() instead of exec() to prevent command injection.
3544
+ * Arguments are passed directly to git without shell interpretation, making
3545
+ * attacks like `--format="$(malicious)"` impossible.
3546
+ *
3547
+ * @param command - The git command to execute (without 'git' prefix)
3548
+ * @param args - Array of arguments for the command
3549
+ * @returns Command output
3550
+ * @throws Error if command is not allowed or execution fails
3551
+ */
3552
+ async runGitCommand(command, args = []) {
3553
+ this.validateCommand(command, args);
3554
+ try {
3555
+ const { stdout, stderr } = await execFileAsync("git", [command, ...args], {
3556
+ cwd: this.workingDirectory,
3557
+ maxBuffer: 10 * 1024 * 1024,
3558
+ // 10MB buffer
3559
+ timeout: 3e4
3560
+ // 30 second timeout
3561
+ });
3562
+ return stderr ? `${stdout}
3563
+
3564
+ Warnings/Info:
3565
+ ${stderr}` : stdout;
3566
+ } catch (error) {
3567
+ if (error instanceof Error) {
3568
+ throw new Error(`Git command failed: ${error.message}`);
3569
+ }
3570
+ throw error;
3571
+ }
3572
+ }
3573
+ /**
3574
+ * Validate that a git command is safe to execute
3575
+ * @param command - The git command
3576
+ * @param args - Command arguments
3577
+ * @throws Error if command is not allowed
3578
+ */
3579
+ validateCommand(command, args) {
3580
+ if (_GitClient.FORBIDDEN_COMMANDS.has(command)) {
3581
+ throw new Error(
3582
+ `Git command '${command}' is not allowed. Only read-only operations are permitted.`
3583
+ );
3584
+ }
3585
+ if (!_GitClient.ALLOWED_COMMANDS.has(command)) {
3586
+ throw new Error(
3587
+ `Git command '${command}' is not in the allowed list. Only safe, read-only operations are permitted.`
3588
+ );
3589
+ }
3590
+ if (command === "branch" && args.some((arg) => arg === "-d" || arg === "-D" || arg === "-m")) {
3591
+ throw new Error("Branch deletion and renaming operations are not allowed.");
3592
+ }
3593
+ if (command === "tag" && args.some((arg) => arg === "-d" || arg === "-f")) {
3594
+ throw new Error("Tag deletion and force operations are not allowed.");
3595
+ }
3596
+ if (command === "remote" && args.some((arg) => arg === "add" || arg === "remove" || arg === "set-url")) {
3597
+ throw new Error("Remote modification operations are not allowed.");
3598
+ }
3599
+ if (command === "config" && args.length > 0 && !args.some((arg) => arg.startsWith("--get"))) {
3600
+ throw new Error("Only git config read operations (--get) are allowed.");
3601
+ }
3602
+ const allArgs = [command, ...args].join(" ");
3603
+ if (_GitClient.SHELL_OPERATORS.some((op) => allArgs.includes(op))) {
3604
+ throw new Error("Command contains potentially dangerous shell operators.");
3605
+ }
3606
+ }
3607
+ /**
3608
+ * Validate that a directory exists and is a git repository
3609
+ * @param directory - Path to validate
3610
+ * @throws Error if directory is invalid or not a git repository
3611
+ */
3612
+ validateDirectory(directory) {
3613
+ const absolutePath = resolve(directory);
3614
+ if (!existsSync(absolutePath)) {
3615
+ throw new Error(`Directory does not exist: ${absolutePath}`);
3616
+ }
3617
+ const stats = statSync(absolutePath);
3618
+ if (!stats.isDirectory()) {
3619
+ throw new Error(`Path is not a directory: ${absolutePath}`);
3620
+ }
3621
+ const gitDir = join(absolutePath, ".git");
3622
+ if (!existsSync(gitDir)) {
3623
+ throw new Error(`Not a git repository: ${absolutePath}`);
3624
+ }
3625
+ }
3626
+ /**
3627
+ * Get the current working directory
3628
+ */
3629
+ getWorkingDirectory() {
3630
+ return this.workingDirectory;
3631
+ }
3632
+ /**
3633
+ * Set the working directory for git commands
3634
+ * @param directory - Path to the git repository
3635
+ * @throws Error if directory is invalid or not a git repository
3636
+ */
3637
+ setWorkingDirectory(directory) {
3638
+ this.validateDirectory(directory);
3639
+ this.workingDirectory = resolve(directory);
3640
+ }
3641
+ };
3642
+
3643
+ // src/tools/git.ts
3644
+ var z14 = tool14.schema;
3645
+ var gitClient = null;
3646
+ function getGitClient() {
3647
+ if (!gitClient) {
3648
+ gitClient = new GitClient();
3649
+ }
3650
+ return gitClient;
3651
+ }
3652
+ var gitTools = {
3653
+ run_git_command: tool14({
3654
+ description: `Execute safe, read-only git commands in the repository working directory.
3655
+
3656
+ Security restrictions:
3657
+ - Only read-only git commands are allowed (status, log, show, diff, branch, tag, etc.)
3658
+ - Destructive operations are forbidden (push, pull, commit, add, rm, reset, merge, etc.)
3659
+ - Shell operators and command injection attempts are blocked
3660
+ - Commands are executed with a 30-second timeout and 10MB output buffer
3661
+
3662
+ Allowed commands include:
3663
+ - status: Show working tree status
3664
+ - log: Show commit logs
3665
+ - show: Show various types of objects
3666
+ - diff: Show changes between commits, commit and working tree, etc.
3667
+ - branch: List, create, or delete branches (read-only: list only)
3668
+ - tag: List tags (read-only: list only)
3669
+ - remote: List remote repositories (read-only: list only)
3670
+ - ls-files: Show information about files in the index and working tree
3671
+ - ls-remote: List references in a remote repository
3672
+ - rev-parse: Parse revision (or other objects) names
3673
+ - describe: Give an object a human readable name based on an available ref
3674
+ - config: Get repository configuration (read-only: --get operations only)
3675
+ - blame: Show what revision and author last modified each line of a file
3676
+ - shortlog: Summarize git log output
3677
+ - reflog: Show reference logs
3678
+ - show-ref: List references in a local repository
3679
+ - ls-tree: List the contents of a tree object
3680
+ - cat-file: Provide content or type and size information for repository objects
3681
+ - rev-list: List commit objects in reverse chronological order
3682
+ - name-rev: Find symbolic names for given revs
3683
+ - show-branch: Show branches and their commits
3684
+ - whatchanged: Show logs with difference each commit introduces
3685
+ - grep: Print lines matching a pattern
3686
+ - annotate: Annotate file lines with commit information
3687
+
3688
+ Examples:
3689
+ - run_git_command("status", []) - Show working tree status
3690
+ - run_git_command("log", ["--oneline", "-10"]) - Show last 10 commits
3691
+ - run_git_command("diff", ["HEAD~1", "HEAD"]) - Show diff between last two commits
3692
+ - run_git_command("branch", ["-a"]) - List all branches
3693
+ - run_git_command("show", ["HEAD:README.md"]) - Show README.md from HEAD commit`,
3694
+ args: {
3695
+ command: z14.string().describe('The git command to execute (without "git" prefix)'),
3696
+ args: z14.array(z14.string()).optional().describe("Array of arguments for the git command (default: [])"),
3697
+ working_directory: z14.string().optional().describe(
3698
+ "Path to the git repository (default: current working directory). Use this to execute git commands in a specific repository."
3699
+ )
3700
+ },
3701
+ execute: async (args, _ctx) => {
3702
+ const client = getGitClient();
3703
+ if (args.working_directory) {
3704
+ client.setWorkingDirectory(args.working_directory);
3705
+ }
3706
+ try {
3707
+ const output = await client.runGitCommand(args.command, args.args || []);
3708
+ return output;
3709
+ } catch (error) {
3710
+ if (error instanceof Error) {
3711
+ throw new Error(`Failed to execute git command: ${error.message}`);
3712
+ }
3713
+ throw error;
3714
+ }
3715
+ }
3716
+ })
3717
+ };
3718
+
3719
+ // src/tools/audit.ts
3720
+ import { tool as tool15 } from "@opencode-ai/plugin";
3721
+ var z15 = tool15.schema;
3722
+ var auditTools = {
3723
+ gitlab_list_project_audit_events: tool15({
3724
+ description: `List audit events for a project.
3725
+ Returns audit events including actions like project settings changes, member additions/removals, and other security-relevant activities.
3726
+ Note: Requires project owner role or higher.`,
3727
+ args: {
3728
+ project_id: z15.string().describe("The project ID or URL-encoded path"),
3729
+ created_after: z15.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
3730
+ created_before: z15.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
3731
+ entity_type: z15.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
3732
+ entity_id: z15.number().optional().describe("Filter by entity ID"),
3733
+ author_id: z15.number().optional().describe("Filter by author user ID"),
3734
+ per_page: z15.number().optional().describe("Number of results per page (default: 20)"),
3735
+ page: z15.number().optional().describe("Page number for pagination (default: 1)")
3736
+ },
3737
+ execute: async (args, _ctx) => {
3738
+ const client = getGitLabClient();
3739
+ const events = await client.listProjectAuditEvents(args.project_id, {
3740
+ created_after: args.created_after,
3741
+ created_before: args.created_before,
3742
+ entity_type: args.entity_type,
3743
+ entity_id: args.entity_id,
3744
+ author_id: args.author_id,
3745
+ per_page: args.per_page,
3746
+ page: args.page
3747
+ });
3748
+ return JSON.stringify(events, null, 2);
3749
+ }
3750
+ }),
3751
+ gitlab_list_group_audit_events: tool15({
3752
+ description: `List audit events for a group.
3753
+ Returns audit events including actions like group settings changes, member additions/removals, subgroup operations, and other security-relevant activities.
3754
+ Note: Requires group owner role or higher.`,
3755
+ args: {
3756
+ group_id: z15.string().describe("The group ID or URL-encoded path"),
3757
+ created_after: z15.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
3758
+ created_before: z15.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
3759
+ entity_type: z15.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
3760
+ entity_id: z15.number().optional().describe("Filter by entity ID"),
3761
+ author_id: z15.number().optional().describe("Filter by author user ID"),
3762
+ per_page: z15.number().optional().describe("Number of results per page (default: 20)"),
3763
+ page: z15.number().optional().describe("Page number for pagination (default: 1)")
3764
+ },
3765
+ execute: async (args, _ctx) => {
3766
+ const client = getGitLabClient();
3767
+ const events = await client.listGroupAuditEvents(args.group_id, {
3768
+ created_after: args.created_after,
3769
+ created_before: args.created_before,
3770
+ entity_type: args.entity_type,
3771
+ entity_id: args.entity_id,
3772
+ author_id: args.author_id,
3773
+ per_page: args.per_page,
3774
+ page: args.page
3775
+ });
3776
+ return JSON.stringify(events, null, 2);
3777
+ }
3778
+ }),
3779
+ gitlab_list_instance_audit_events: tool15({
3780
+ description: `List instance-level audit events.
3781
+ 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.
3782
+ Note: Requires administrator access.`,
3783
+ args: {
3784
+ created_after: z15.string().optional().describe("Return audit events created after this date (ISO 8601 format)"),
3785
+ created_before: z15.string().optional().describe("Return audit events created before this date (ISO 8601 format)"),
3786
+ entity_type: z15.string().optional().describe('Filter by entity type (e.g., "User", "Project", "Group")'),
3787
+ entity_id: z15.number().optional().describe("Filter by entity ID"),
3788
+ author_id: z15.number().optional().describe("Filter by author user ID"),
3789
+ per_page: z15.number().optional().describe("Number of results per page (default: 20)"),
3790
+ page: z15.number().optional().describe("Page number for pagination (default: 1)")
3791
+ },
3792
+ execute: async (args, _ctx) => {
3793
+ const client = getGitLabClient();
3794
+ const events = await client.listInstanceAuditEvents({
3795
+ created_after: args.created_after,
3796
+ created_before: args.created_before,
3797
+ entity_type: args.entity_type,
3798
+ entity_id: args.entity_id,
3799
+ author_id: args.author_id,
3800
+ per_page: args.per_page,
3801
+ page: args.page
3802
+ });
3803
+ return JSON.stringify(events, null, 2);
3804
+ }
3805
+ })
3806
+ };
3807
+
3808
+ // src/index.ts
3809
+ var gitlabPlugin = async (_input) => {
3810
+ return {
3811
+ tool: {
3812
+ // Merge Request Tools
3813
+ ...mergeRequestTools,
3814
+ // Issue Tools
3815
+ ...issueTools,
3816
+ // Epic Tools
3817
+ ...epicTools,
3818
+ // Pipeline Tools
3819
+ ...pipelineTools,
3820
+ // Repository Tools
3821
+ ...repositoryTools,
3822
+ // Search Tools
3823
+ ...searchTools,
3824
+ // Project Tools
3825
+ ...projectTools,
3826
+ // User Tools
3827
+ ...userTools,
3828
+ // Security Tools
3829
+ ...securityTools,
3830
+ // Snippet Tools
3831
+ ...snippetTools,
3832
+ // TODO Tools
3833
+ ...todoTools,
3834
+ // Wiki Tools
3835
+ ...wikiTools,
3836
+ // Work Item Tools
3837
+ ...workItemTools,
3838
+ // Commit Discussion Tools
3839
+ ...commitDiscussionTools,
3840
+ // Universal Discussion Tools
3841
+ ...discussionTools,
3842
+ // Git Tools
3843
+ ...gitTools,
3844
+ // Audit Tools
3845
+ ...auditTools
3846
+ }
3847
+ };
3848
+ };
3849
+ var index_default = gitlabPlugin;
3850
+ export {
3851
+ index_default as default,
3852
+ gitlabPlugin
3853
+ };