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