gitlab-mcp 1.4.0 → 1.5.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.
Files changed (39) hide show
  1. package/README.md +45 -34
  2. package/dist/config/env.d.ts +7 -1
  3. package/dist/config/env.js +17 -0
  4. package/dist/config/env.js.map +1 -1
  5. package/dist/http-app.js +422 -17
  6. package/dist/http-app.js.map +1 -1
  7. package/dist/http.js +10 -1
  8. package/dist/http.js.map +1 -1
  9. package/dist/index.js +8 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/download-token.d.ts +24 -0
  12. package/dist/lib/download-token.js +84 -0
  13. package/dist/lib/download-token.js.map +1 -0
  14. package/dist/lib/gitlab-client.d.ts +78 -1
  15. package/dist/lib/gitlab-client.js +451 -7
  16. package/dist/lib/gitlab-client.js.map +1 -1
  17. package/dist/lib/http-auth-guard.d.ts +8 -0
  18. package/dist/lib/http-auth-guard.js +19 -0
  19. package/dist/lib/http-auth-guard.js.map +1 -0
  20. package/dist/lib/mcp-oauth-provider.d.ts +2 -0
  21. package/dist/lib/mcp-oauth-provider.js +61 -0
  22. package/dist/lib/mcp-oauth-provider.js.map +1 -0
  23. package/dist/lib/oauth.d.ts +3 -1
  24. package/dist/lib/oauth.js +5 -5
  25. package/dist/lib/oauth.js.map +1 -1
  26. package/dist/lib/patch-helper.d.ts +13 -0
  27. package/dist/lib/patch-helper.js +156 -0
  28. package/dist/lib/patch-helper.js.map +1 -0
  29. package/dist/lib/request-runtime.d.ts +1 -0
  30. package/dist/lib/request-runtime.js +42 -4
  31. package/dist/lib/request-runtime.js.map +1 -1
  32. package/dist/lib/tool-schema.js +1 -1
  33. package/dist/lib/tool-schema.js.map +1 -1
  34. package/dist/tools/gitlab.js +2690 -52
  35. package/dist/tools/gitlab.js.map +1 -1
  36. package/docs/authentication.md +37 -8
  37. package/docs/configuration.md +7 -4
  38. package/docs/tools.md +153 -51
  39. package/package.json +1 -1
@@ -21,6 +21,7 @@ export class GitLabClient {
21
21
  apiUrls;
22
22
  nextApiUrlIndex = 0;
23
23
  defaultToken;
24
+ defaultAuthHeader;
24
25
  timeoutMs;
25
26
  maxAttachmentBytes;
26
27
  maxLocalFileBytes;
@@ -33,6 +34,7 @@ export class GitLabClient {
33
34
  .filter((item) => item.length > 0) ?? [this.baseApiUrl];
34
35
  this.apiUrls = configuredApiUrls.length > 0 ? configuredApiUrls : [this.baseApiUrl];
35
36
  this.defaultToken = defaultToken;
37
+ this.defaultAuthHeader = options.defaultAuthHeader;
36
38
  this.timeoutMs = options.timeoutMs ?? 20_000;
37
39
  this.maxAttachmentBytes =
38
40
  options.maxAttachmentBytes ?? GitLabClient.DEFAULT_MAX_ATTACHMENT_BYTES;
@@ -58,6 +60,16 @@ export class GitLabClient {
58
60
  }
59
61
  });
60
62
  }
63
+ createGroup(payload, options = {}) {
64
+ return this.post("/groups", {
65
+ ...options,
66
+ body: JSON.stringify(payload),
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ ...(options.headers ?? {})
70
+ }
71
+ });
72
+ }
61
73
  listProjectMembers(projectId, options = {}) {
62
74
  return this.get(`/projects/${encode(projectId)}/members/all`, options);
63
75
  }
@@ -95,6 +107,16 @@ export class GitLabClient {
95
107
  }
96
108
  });
97
109
  }
110
+ searchCode(search, options = {}) {
111
+ return this.get("/search", {
112
+ ...options,
113
+ query: {
114
+ scope: "blobs",
115
+ search,
116
+ ...(options.query ?? {})
117
+ }
118
+ });
119
+ }
98
120
  searchCodeBlobs(projectId, search, options = {}) {
99
121
  return this.get(`/projects/${encode(projectId)}/search`, {
100
122
  ...options,
@@ -105,9 +127,67 @@ export class GitLabClient {
105
127
  }
106
128
  });
107
129
  }
130
+ searchGroupCodeBlobs(groupId, search, options = {}) {
131
+ return this.get(`/groups/${encode(groupId)}/search`, {
132
+ ...options,
133
+ query: {
134
+ scope: "blobs",
135
+ search,
136
+ ...(options.query ?? {})
137
+ }
138
+ });
139
+ }
108
140
  // repository/files
109
- getRepositoryTree(projectId, options = {}) {
110
- return this.get(`/projects/${encode(projectId)}/repository/tree`, options);
141
+ async getRepositoryTree(projectId, options = {}) {
142
+ const config = this.resolveRequestConfig(options);
143
+ const url = new URL(`projects/${encode(projectId)}/repository/tree`, `${config.apiUrl}/`);
144
+ for (const [key, value] of Object.entries(options.query ?? {})) {
145
+ if (value !== undefined && value !== null) {
146
+ url.searchParams.set(key, String(value));
147
+ }
148
+ }
149
+ const response = await this.fetchRawResponse(url, {
150
+ method: "GET",
151
+ headers: {
152
+ Accept: "application/json",
153
+ ...(options.headers ?? {})
154
+ },
155
+ token: config.token,
156
+ authHeader: config.authHeader
157
+ });
158
+ let body;
159
+ try {
160
+ body = await this.parseResponseBody(response);
161
+ }
162
+ catch (error) {
163
+ if (response.ok) {
164
+ throw error;
165
+ }
166
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, {
167
+ message: error instanceof Error ? error.message : "Failed to read GitLab error response"
168
+ });
169
+ }
170
+ if (!response.ok) {
171
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, body);
172
+ }
173
+ const usesKeyset = options.query?.pagination === "keyset";
174
+ const nextPageToken = response.headers.get("x-next-page-token") ??
175
+ (usesKeyset ? response.headers.get("x-next-page") : null) ??
176
+ undefined;
177
+ if (!usesKeyset && !nextPageToken) {
178
+ return body;
179
+ }
180
+ return {
181
+ items: Array.isArray(body) ? body : [],
182
+ ...(nextPageToken
183
+ ? {
184
+ next_page_token: nextPageToken,
185
+ pagination_note: "Pass next_page_token as page_token with pagination=keyset to retrieve the next page."
186
+ }
187
+ : {
188
+ pagination_note: "No next_page_token was returned; this is the final keyset page."
189
+ })
190
+ };
111
191
  }
112
192
  getFileContents(projectId, filePath, ref, options = {}) {
113
193
  return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
@@ -118,6 +198,15 @@ export class GitLabClient {
118
198
  }
119
199
  });
120
200
  }
201
+ getFileBlame(projectId, filePath, ref, options = {}) {
202
+ return this.get(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}/blame`, {
203
+ ...options,
204
+ query: {
205
+ ref,
206
+ ...(options.query ?? {})
207
+ }
208
+ });
209
+ }
121
210
  createOrUpdateFile(projectId, filePath, payload, options = {}) {
122
211
  return this.put(`/projects/${encode(projectId)}/repository/files/${encode(filePath)}`, {
123
212
  ...options,
@@ -144,6 +233,15 @@ export class GitLabClient {
144
233
  query: payload
145
234
  });
146
235
  }
236
+ listBranches(projectId, options = {}) {
237
+ return this.get(`/projects/${encode(projectId)}/repository/branches`, options);
238
+ }
239
+ getBranch(projectId, branch, options = {}) {
240
+ return this.get(`/projects/${encode(projectId)}/repository/branches/${encode(branch)}`, options);
241
+ }
242
+ deleteBranch(projectId, branch, options = {}) {
243
+ return this.delete(`/projects/${encode(projectId)}/repository/branches/${encode(branch)}`, options);
244
+ }
147
245
  getBranchDiffs(projectId, payload, options = {}) {
148
246
  return this.get(`/projects/${encode(projectId)}/repository/compare`, {
149
247
  ...options,
@@ -159,6 +257,18 @@ export class GitLabClient {
159
257
  getCommitDiff(projectId, sha, options = {}) {
160
258
  return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}/diff`, options);
161
259
  }
260
+ listCommitStatuses(projectId, sha, options = {}) {
261
+ return this.get(`/projects/${encode(projectId)}/repository/commits/${encode(sha)}/statuses`, options);
262
+ }
263
+ createCommitStatus(projectId, sha, payload, options = {}) {
264
+ return this.post(`/projects/${encode(projectId)}/statuses/${encode(sha)}`, {
265
+ ...options,
266
+ query: {
267
+ ...payload,
268
+ ...(options.query ?? {})
269
+ }
270
+ });
271
+ }
162
272
  // merge requests
163
273
  listMergeRequests(projectId, options = {}) {
164
274
  return this.get(`/projects/${encode(projectId)}/merge_requests`, options);
@@ -167,7 +277,59 @@ export class GitLabClient {
167
277
  return this.get("/merge_requests", options);
168
278
  }
169
279
  getMergeRequest(projectId, mergeRequestIid, options = {}) {
170
- return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, options);
280
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}`, {
281
+ ...options,
282
+ query: {
283
+ include_diverged_commits_count: true,
284
+ ...(options.query ?? {})
285
+ }
286
+ });
287
+ }
288
+ async countMergeRequestCommits(projectId, mergeRequestIid, options = {}) {
289
+ const config = this.resolveRequestConfig(options);
290
+ let page = 1;
291
+ let totalCount = 0;
292
+ while (true) {
293
+ const url = new URL(`projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/commits`, `${config.apiUrl}/`);
294
+ for (const [key, value] of Object.entries(options.query ?? {})) {
295
+ if (value !== undefined && value !== null) {
296
+ url.searchParams.set(key, String(value));
297
+ }
298
+ }
299
+ if (!url.searchParams.has("per_page")) {
300
+ url.searchParams.set("per_page", "100");
301
+ }
302
+ url.searchParams.set("page", String(page));
303
+ const response = await this.fetchRawResponse(url, {
304
+ method: "GET",
305
+ headers: {
306
+ Accept: "application/json",
307
+ ...(options.headers ?? {})
308
+ },
309
+ token: config.token,
310
+ authHeader: config.authHeader
311
+ });
312
+ const body = await this.parseApiResponse(response);
313
+ if (!Array.isArray(body)) {
314
+ throw new Error("Unexpected merge request commits response format");
315
+ }
316
+ totalCount += body.length;
317
+ const nextPage = response.headers.get("x-next-page");
318
+ if (!nextPage) {
319
+ return totalCount;
320
+ }
321
+ const parsedNextPage = Number.parseInt(nextPage, 10);
322
+ if (!Number.isFinite(parsedNextPage) || parsedNextPage <= page) {
323
+ return totalCount;
324
+ }
325
+ page = parsedNextPage;
326
+ }
327
+ }
328
+ listMergeRequestCommits(projectId, mergeRequestIid, options = {}) {
329
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/commits`, options);
330
+ }
331
+ listMergeRequestPipelines(projectId, mergeRequestIid, options = {}) {
332
+ return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/pipelines`, options);
171
333
  }
172
334
  createMergeRequest(projectId, payload, options = {}) {
173
335
  return this.post(`/projects/${encode(projectId)}/merge_requests`, {
@@ -231,8 +393,28 @@ export class GitLabClient {
231
393
  }
232
394
  });
233
395
  }
234
- getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
235
- return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`, options);
396
+ async getMergeRequestApprovalState(projectId, mergeRequestIid, options = {}) {
397
+ const config = this.resolveRequestConfig(options);
398
+ const url = new URL(`projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approval_state`, `${config.apiUrl}/`);
399
+ for (const [key, value] of Object.entries(options.query ?? {})) {
400
+ if (value !== undefined && value !== null) {
401
+ url.searchParams.set(key, String(value));
402
+ }
403
+ }
404
+ const response = await this.fetchRawResponse(url, {
405
+ method: "GET",
406
+ headers: {
407
+ Accept: "application/json",
408
+ ...(options.headers ?? {})
409
+ },
410
+ token: config.token,
411
+ authHeader: config.authHeader
412
+ });
413
+ if (response.status === 404) {
414
+ const approvals = await this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/approvals`, options);
415
+ return normalizeMergeRequestApprovalsFallback(approvals);
416
+ }
417
+ return normalizeMergeRequestApprovalState(await this.parseApiResponse(response));
236
418
  }
237
419
  getMergeRequestConflicts(projectId, mergeRequestIid, options = {}) {
238
420
  return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/conflicts`, options);
@@ -299,6 +481,34 @@ export class GitLabClient {
299
481
  }
300
482
  });
301
483
  }
484
+ listMergeRequestEmojiReactions(projectId, mergeRequestIid, options = {}) {
485
+ return this.get(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid), options);
486
+ }
487
+ createMergeRequestEmojiReaction(projectId, mergeRequestIid, name, options = {}) {
488
+ return this.createAwardEmoji(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid), name, options);
489
+ }
490
+ deleteMergeRequestEmojiReaction(projectId, mergeRequestIid, awardId, options = {}) {
491
+ return this.delete(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, { awardId }), options);
492
+ }
493
+ listMergeRequestNoteEmojiReactions(projectId, mergeRequestIid, noteId, payload = {}, options = {}) {
494
+ return this.get(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
495
+ noteId,
496
+ discussionId: payload.discussion_id
497
+ }), options);
498
+ }
499
+ createMergeRequestNoteEmojiReaction(projectId, mergeRequestIid, noteId, payload, options = {}) {
500
+ return this.createAwardEmoji(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
501
+ noteId,
502
+ discussionId: payload.discussion_id
503
+ }), payload.name, options);
504
+ }
505
+ deleteMergeRequestNoteEmojiReaction(projectId, mergeRequestIid, noteId, payload, options = {}) {
506
+ return this.delete(this.awardEmojiPath("merge_requests", projectId, mergeRequestIid, {
507
+ noteId,
508
+ discussionId: payload.discussion_id,
509
+ awardId: payload.award_id
510
+ }), options);
511
+ }
302
512
  getDraftNote(projectId, mergeRequestIid, draftNoteId, options = {}) {
303
513
  return this.get(`/projects/${encode(projectId)}/merge_requests/${encode(mergeRequestIid)}/draft_notes/${encode(draftNoteId)}`, options);
304
514
  }
@@ -424,6 +634,15 @@ export class GitLabClient {
424
634
  }
425
635
  });
426
636
  }
637
+ listTodos(options = {}) {
638
+ return this.get("/todos", options);
639
+ }
640
+ markTodoDone(todoId, options = {}) {
641
+ return this.post(`/todos/${encode(todoId)}/mark_as_done`, options);
642
+ }
643
+ markAllTodosDone(options = {}) {
644
+ return this.post("/todos/mark_as_done", options);
645
+ }
427
646
  listIssueDiscussions(projectId, issueIid, options = {}) {
428
647
  return this.get(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions`, options);
429
648
  }
@@ -443,6 +662,34 @@ export class GitLabClient {
443
662
  }
444
663
  });
445
664
  }
665
+ listIssueEmojiReactions(projectId, issueIid, options = {}) {
666
+ return this.get(this.awardEmojiPath("issues", projectId, issueIid), options);
667
+ }
668
+ createIssueEmojiReaction(projectId, issueIid, name, options = {}) {
669
+ return this.createAwardEmoji(this.awardEmojiPath("issues", projectId, issueIid), name, options);
670
+ }
671
+ deleteIssueEmojiReaction(projectId, issueIid, awardId, options = {}) {
672
+ return this.delete(this.awardEmojiPath("issues", projectId, issueIid, { awardId }), options);
673
+ }
674
+ listIssueNoteEmojiReactions(projectId, issueIid, noteId, payload = {}, options = {}) {
675
+ return this.get(this.awardEmojiPath("issues", projectId, issueIid, {
676
+ noteId,
677
+ discussionId: payload.discussion_id
678
+ }), options);
679
+ }
680
+ createIssueNoteEmojiReaction(projectId, issueIid, noteId, payload, options = {}) {
681
+ return this.createAwardEmoji(this.awardEmojiPath("issues", projectId, issueIid, {
682
+ noteId,
683
+ discussionId: payload.discussion_id
684
+ }), payload.name, options);
685
+ }
686
+ deleteIssueNoteEmojiReaction(projectId, issueIid, noteId, payload, options = {}) {
687
+ return this.delete(this.awardEmojiPath("issues", projectId, issueIid, {
688
+ noteId,
689
+ discussionId: payload.discussion_id,
690
+ awardId: payload.award_id
691
+ }), options);
692
+ }
446
693
  updateIssueNote(projectId, issueIid, discussionId, noteId, payload, options = {}) {
447
694
  return this.put(`/projects/${encode(projectId)}/issues/${encode(issueIid)}/discussions/${encode(discussionId)}/notes/${encode(noteId)}`, {
448
695
  ...options,
@@ -502,6 +749,35 @@ export class GitLabClient {
502
749
  deleteWikiPage(projectId, slug, options = {}) {
503
750
  return this.delete(`/projects/${encode(projectId)}/wikis/${encode(slug)}`, options);
504
751
  }
752
+ listGroupWikiPages(groupId, options = {}) {
753
+ return this.get(`/groups/${encode(groupId)}/wikis`, options);
754
+ }
755
+ getGroupWikiPage(groupId, slug, options = {}) {
756
+ return this.get(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, options);
757
+ }
758
+ createGroupWikiPage(groupId, payload, options = {}) {
759
+ return this.post(`/groups/${encode(groupId)}/wikis`, {
760
+ ...options,
761
+ body: JSON.stringify(payload),
762
+ headers: {
763
+ "Content-Type": "application/json",
764
+ ...(options.headers ?? {})
765
+ }
766
+ });
767
+ }
768
+ updateGroupWikiPage(groupId, slug, payload, options = {}) {
769
+ return this.put(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, {
770
+ ...options,
771
+ body: JSON.stringify(payload),
772
+ headers: {
773
+ "Content-Type": "application/json",
774
+ ...(options.headers ?? {})
775
+ }
776
+ });
777
+ }
778
+ deleteGroupWikiPage(groupId, slug, options = {}) {
779
+ return this.delete(`/groups/${encode(groupId)}/wikis/${encode(slug)}`, options);
780
+ }
505
781
  // pipelines
506
782
  listPipelines(projectId, options = {}) {
507
783
  return this.get(`/projects/${encode(projectId)}/pipelines`, options);
@@ -533,6 +809,19 @@ export class GitLabClient {
533
809
  getPipelineJobOutput(projectId, jobId, options = {}) {
534
810
  return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/trace`, options);
535
811
  }
812
+ validateCiLint(projectId, payload, options = {}) {
813
+ return this.post(`/projects/${encode(projectId)}/ci/lint`, {
814
+ ...options,
815
+ body: JSON.stringify(payload),
816
+ headers: {
817
+ "Content-Type": "application/json",
818
+ ...(options.headers ?? {})
819
+ }
820
+ });
821
+ }
822
+ validateProjectCiLint(projectId, options = {}) {
823
+ return this.get(`/projects/${encode(projectId)}/ci/lint`, options);
824
+ }
536
825
  listJobArtifacts(projectId, jobId, options = {}) {
537
826
  return this.get(`/projects/${encode(projectId)}/jobs/${encode(jobId)}/artifacts/tree`, options);
538
827
  }
@@ -675,8 +964,37 @@ export class GitLabClient {
675
964
  return this.post(`/projects/${encode(projectId)}/releases/${encode(tagName)}/evidence`, options);
676
965
  }
677
966
  downloadReleaseAsset(projectId, tagName, directAssetPath, options = {}) {
967
+ const requestConfig = this.resolveRequestConfig(options);
678
968
  const safePath = encodeSlashPath(directAssetPath);
679
- return this.get(`/projects/${encode(projectId)}/releases/${encode(tagName)}/downloads/${safePath}`, options);
969
+ const url = new URL(`projects/${encode(projectId)}/releases/${encode(tagName)}/downloads/${safePath}`, `${requestConfig.apiUrl}/`);
970
+ return this.downloadFile(url, {
971
+ headers: options.headers,
972
+ token: requestConfig.token,
973
+ authHeader: requestConfig.authHeader
974
+ }, "Release asset", path.basename(directAssetPath) || "release-asset");
975
+ }
976
+ // tags
977
+ listTags(projectId, options = {}) {
978
+ return this.get(`/projects/${encode(projectId)}/repository/tags`, options);
979
+ }
980
+ getTag(projectId, tagName, options = {}) {
981
+ return this.get(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}`, options);
982
+ }
983
+ createTag(projectId, payload, options = {}) {
984
+ return this.post(`/projects/${encode(projectId)}/repository/tags`, {
985
+ ...options,
986
+ body: JSON.stringify(payload),
987
+ headers: {
988
+ "Content-Type": "application/json",
989
+ ...(options.headers ?? {})
990
+ }
991
+ });
992
+ }
993
+ deleteTag(projectId, tagName, options = {}) {
994
+ return this.delete(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}`, options);
995
+ }
996
+ getTagSignature(projectId, tagName, options = {}) {
997
+ return this.get(`/projects/${encode(projectId)}/repository/tags/${encode(tagName)}/signature`, options);
680
998
  }
681
999
  // labels
682
1000
  listLabels(projectId, options = {}) {
@@ -730,12 +1048,24 @@ export class GitLabClient {
730
1048
  getUsers(options = {}) {
731
1049
  return this.get("/users", options);
732
1050
  }
1051
+ getUser(userId, options = {}) {
1052
+ return this.get(`/users/${encode(userId)}`, options);
1053
+ }
1054
+ whoami(options = {}) {
1055
+ return this.get("/user", options);
1056
+ }
733
1057
  listEvents(options = {}) {
734
1058
  return this.get("/events", options);
735
1059
  }
736
1060
  getProjectEvents(projectId, options = {}) {
737
1061
  return this.get(`/projects/${encode(projectId)}/events`, options);
738
1062
  }
1063
+ listWebhooks(scope, options = {}) {
1064
+ return this.get(`${this.webhookBasePath(scope)}/hooks`, options);
1065
+ }
1066
+ listWebhookEvents(scope, hookId, options = {}) {
1067
+ return this.get(`${this.webhookBasePath(scope)}/hooks/${encode(hookId)}/events`, options);
1068
+ }
739
1069
  // attachments / markdown
740
1070
  uploadMarkdown(projectId, content, filename, options = {}) {
741
1071
  const form = new FormData();
@@ -1003,6 +1333,24 @@ export class GitLabClient {
1003
1333
  }
1004
1334
  return body;
1005
1335
  }
1336
+ async parseApiResponse(response) {
1337
+ let body;
1338
+ try {
1339
+ body = await this.parseResponseBody(response);
1340
+ }
1341
+ catch (error) {
1342
+ if (response.ok) {
1343
+ throw error;
1344
+ }
1345
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, {
1346
+ message: error instanceof Error ? error.message : "Failed to read GitLab error response"
1347
+ });
1348
+ }
1349
+ if (!response.ok) {
1350
+ throw new GitLabApiError(`GitLab API request failed: ${response.status} ${response.statusText}`, response.status, body);
1351
+ }
1352
+ return body;
1353
+ }
1006
1354
  async parseResponseBody(response) {
1007
1355
  assertContentLengthWithinLimit(response, this.maxResponseBodyBytes, "Response body");
1008
1356
  const text = await readResponseTextWithLimit(response, this.maxResponseBodyBytes, "Response body");
@@ -1021,7 +1369,7 @@ export class GitLabClient {
1021
1369
  const sessionAuth = getSessionAuth();
1022
1370
  const apiUrl = options.apiUrl ?? sessionAuth?.apiUrl ?? this.pickApiUrl();
1023
1371
  const token = options.token ?? sessionAuth?.token ?? this.defaultToken;
1024
- const authHeader = options.authHeader ?? sessionAuth?.header;
1372
+ const authHeader = options.authHeader ?? sessionAuth?.header ?? this.defaultAuthHeader;
1025
1373
  return {
1026
1374
  apiUrl: normalizeApiUrl(apiUrl),
1027
1375
  token,
@@ -1036,6 +1384,38 @@ export class GitLabClient {
1036
1384
  this.nextApiUrlIndex = (this.nextApiUrlIndex + 1) % this.apiUrls.length;
1037
1385
  return this.apiUrls[index] ?? this.baseApiUrl;
1038
1386
  }
1387
+ webhookBasePath(scope) {
1388
+ if (scope.projectId) {
1389
+ return `/projects/${encode(scope.projectId)}`;
1390
+ }
1391
+ if (scope.groupId) {
1392
+ return `/groups/${encode(scope.groupId)}`;
1393
+ }
1394
+ throw new Error("Either projectId or groupId is required");
1395
+ }
1396
+ awardEmojiPath(entity, projectId, entityIid, options = {}) {
1397
+ let path = `/projects/${encode(projectId)}/${entity}/${encode(entityIid)}`;
1398
+ if (options.noteId) {
1399
+ path += options.discussionId
1400
+ ? `/discussions/${encode(options.discussionId)}/notes/${encode(options.noteId)}`
1401
+ : `/notes/${encode(options.noteId)}`;
1402
+ }
1403
+ path += "/award_emoji";
1404
+ if (options.awardId) {
1405
+ path += `/${encode(options.awardId)}`;
1406
+ }
1407
+ return path;
1408
+ }
1409
+ createAwardEmoji(path, name, options) {
1410
+ return this.post(path, {
1411
+ ...options,
1412
+ body: JSON.stringify({ name }),
1413
+ headers: {
1414
+ "Content-Type": "application/json",
1415
+ ...(options.headers ?? {})
1416
+ }
1417
+ });
1418
+ }
1039
1419
  resolveAbsoluteUrl(raw, apiUrl) {
1040
1420
  if (/^https?:\/\//i.test(raw)) {
1041
1421
  return new URL(raw);
@@ -1093,6 +1473,70 @@ function encodeSlashPath(pathValue) {
1093
1473
  .map((segment) => encode(segment))
1094
1474
  .join("/");
1095
1475
  }
1476
+ function normalizeMergeRequestApprovalState(value) {
1477
+ if (!isRecord(value)) {
1478
+ return value;
1479
+ }
1480
+ const approvedBy = uniqueApprovalUsers(extractApprovalRules(value.rules).flatMap((rule) => extractApprovalUsers(rule.approved_by)));
1481
+ return {
1482
+ ...value,
1483
+ approved_by: approvedBy,
1484
+ approved_by_usernames: approvedBy
1485
+ .map((user) => user.username)
1486
+ .filter((username) => typeof username === "string"),
1487
+ source_endpoint: "approval_state"
1488
+ };
1489
+ }
1490
+ function normalizeMergeRequestApprovalsFallback(value) {
1491
+ if (!isRecord(value)) {
1492
+ return value;
1493
+ }
1494
+ const approvedBy = uniqueApprovalUsers(extractApprovalEntries(value.approved_by).flatMap((entry) => [entry.user]));
1495
+ return {
1496
+ approved: typeof value.approved === "boolean" ? value.approved : undefined,
1497
+ user_has_approved: typeof value.user_has_approved === "boolean" ? value.user_has_approved : undefined,
1498
+ user_can_approve: typeof value.user_can_approve === "boolean" ? value.user_can_approve : undefined,
1499
+ approved_by: approvedBy,
1500
+ approved_by_usernames: approvedBy
1501
+ .map((user) => user.username)
1502
+ .filter((username) => typeof username === "string"),
1503
+ source_endpoint: "approvals"
1504
+ };
1505
+ }
1506
+ function extractApprovalRules(value) {
1507
+ return Array.isArray(value) ? value.filter(isRecord) : [];
1508
+ }
1509
+ function extractApprovalUsers(value) {
1510
+ return Array.isArray(value) ? value.filter(isRecord) : [];
1511
+ }
1512
+ function extractApprovalEntries(value) {
1513
+ if (!Array.isArray(value)) {
1514
+ return [];
1515
+ }
1516
+ return value.filter((item) => isRecord(item) && isRecord(item.user));
1517
+ }
1518
+ function uniqueApprovalUsers(users) {
1519
+ const seen = new Set();
1520
+ const unique = [];
1521
+ for (const user of users) {
1522
+ const id = user.id;
1523
+ const username = user.username;
1524
+ const key = typeof id === "string" || typeof id === "number"
1525
+ ? `id:${id}`
1526
+ : typeof username === "string"
1527
+ ? `username:${username}`
1528
+ : undefined;
1529
+ if (!key || seen.has(key)) {
1530
+ continue;
1531
+ }
1532
+ seen.add(key);
1533
+ unique.push(user);
1534
+ }
1535
+ return unique;
1536
+ }
1537
+ function isRecord(value) {
1538
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1539
+ }
1096
1540
  function normalizeApiUrl(rawUrl) {
1097
1541
  const url = new URL(rawUrl);
1098
1542
  const pathname = url.pathname.replace(/\/+$/, "");