@ttpears/gitlab-mcp-server 1.7.3 → 1.9.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.
@@ -192,6 +192,35 @@ export class GitLabGraphQLClient {
192
192
  const client = this.getClient(userConfig, requiresWrite);
193
193
  return this.executeWithRetry(() => client.request(query, variables), 'GraphQL query');
194
194
  }
195
+ async fetchAllPages(query, variables, connectionPath, options = {}) {
196
+ const maxItems = options.maxItems ?? 100;
197
+ const pageSize = Math.min(options.pageSize ?? 50, this.config.maxPageSize);
198
+ const allNodes = [];
199
+ let after = undefined;
200
+ let lastPageInfo = { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null };
201
+ while (allNodes.length < maxItems) {
202
+ const remaining = maxItems - allNodes.length;
203
+ const currentPageSize = Math.min(pageSize, remaining);
204
+ const result = await this.query(query, { ...variables, first: currentPageSize, after }, options.userConfig);
205
+ // Navigate to the connection using the dot-separated path
206
+ const connection = connectionPath.split('.').reduce((obj, key) => obj?.[key], result);
207
+ if (!connection || !connection.nodes) {
208
+ break;
209
+ }
210
+ allNodes.push(...connection.nodes);
211
+ lastPageInfo = connection.pageInfo || lastPageInfo;
212
+ if (!lastPageInfo.hasNextPage || allNodes.length >= maxItems) {
213
+ break;
214
+ }
215
+ after = lastPageInfo.endCursor ?? undefined;
216
+ }
217
+ return {
218
+ nodes: allNodes,
219
+ totalFetched: allNodes.length,
220
+ hasMore: lastPageInfo.hasNextPage && allNodes.length >= maxItems,
221
+ pageInfo: lastPageInfo,
222
+ };
223
+ }
195
224
  async getCurrentUser(userConfig) {
196
225
  const query = gql `
197
226
  query getCurrentUser {
@@ -233,10 +262,10 @@ export class GitLabGraphQLClient {
233
262
  `;
234
263
  return this.query(query, { fullPath }, userConfig);
235
264
  }
236
- async getProjects(first = 20, after, userConfig) {
265
+ async getProjects(first = 20, after, fetchAll = false, userConfig, sort) {
237
266
  const query = gql `
238
- query getProjects($first: Int!, $after: String) {
239
- projects(first: $first, after: $after) {
267
+ query getProjects($first: Int!, $after: String, $sort: String) {
268
+ projects(first: $first, after: $after, sort: $sort) {
240
269
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
241
270
  nodes {
242
271
  id
@@ -249,13 +278,20 @@ export class GitLabGraphQLClient {
249
278
  }
250
279
  }
251
280
  `;
252
- return this.query(query, { first: Math.min(first, 50), after }, userConfig);
281
+ if (fetchAll) {
282
+ return this.fetchAllPages(query, { sort }, 'projects', {
283
+ maxItems: first,
284
+ pageSize: this.config.maxPageSize,
285
+ userConfig,
286
+ });
287
+ }
288
+ return this.query(query, { first: Math.min(first, this.config.maxPageSize), after, sort }, userConfig);
253
289
  }
254
- async getIssues(projectPath, first = 20, after, userConfig) {
290
+ async getIssues(projectPath, first = 20, after, fetchAll = false, userConfig, sort) {
255
291
  const query = gql `
256
- query getIssues($projectPath: ID!, $first: Int!, $after: String) {
292
+ query getIssues($projectPath: ID!, $first: Int!, $after: String, $sort: IssueSort) {
257
293
  project(fullPath: $projectPath) {
258
- issues(first: $first, after: $after) {
294
+ issues(first: $first, after: $after, sort: $sort) {
259
295
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
260
296
  nodes {
261
297
  id
@@ -271,13 +307,20 @@ export class GitLabGraphQLClient {
271
307
  }
272
308
  }
273
309
  `;
274
- return this.query(query, { projectPath, first: Math.min(first, 50), after }, userConfig);
310
+ if (fetchAll) {
311
+ return this.fetchAllPages(query, { projectPath, sort: sort || 'UPDATED_DESC' }, 'project.issues', {
312
+ maxItems: first,
313
+ pageSize: this.config.maxPageSize,
314
+ userConfig,
315
+ });
316
+ }
317
+ return this.query(query, { projectPath, first: Math.min(first, this.config.maxPageSize), after, sort: sort || 'UPDATED_DESC' }, userConfig);
275
318
  }
276
- async getMergeRequests(projectPath, first = 20, after, userConfig) {
319
+ async getMergeRequests(projectPath, first = 20, after, fetchAll = false, userConfig, sort) {
277
320
  const query = gql `
278
- query getMergeRequests($projectPath: ID!, $first: Int!, $after: String) {
321
+ query getMergeRequests($projectPath: ID!, $first: Int!, $after: String, $sort: MergeRequestSort) {
279
322
  project(fullPath: $projectPath) {
280
- mergeRequests(first: $first, after: $after) {
323
+ mergeRequests(first: $first, after: $after, sort: $sort) {
281
324
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
282
325
  nodes {
283
326
  id
@@ -296,7 +339,108 @@ export class GitLabGraphQLClient {
296
339
  }
297
340
  }
298
341
  `;
299
- return this.query(query, { projectPath, first: Math.min(first, 50), after }, userConfig);
342
+ if (fetchAll) {
343
+ return this.fetchAllPages(query, { projectPath, sort: sort || 'UPDATED_DESC' }, 'project.mergeRequests', {
344
+ maxItems: first,
345
+ pageSize: this.config.maxPageSize,
346
+ userConfig,
347
+ });
348
+ }
349
+ return this.query(query, { projectPath, first: Math.min(first, this.config.maxPageSize), after, sort: sort || 'UPDATED_DESC' }, userConfig);
350
+ }
351
+ async getWorkItem(id, userConfig) {
352
+ const gid = id.startsWith('gid://') ? id : `gid://gitlab/WorkItem/${id}`;
353
+ const query = gql `
354
+ query getWorkItem($id: WorkItemID!) {
355
+ workItem(id: $id) {
356
+ id
357
+ iid
358
+ title
359
+ state
360
+ confidential
361
+ createdAt
362
+ updatedAt
363
+ closedAt
364
+ webUrl
365
+ workItemType { id name iconName }
366
+ author { username name }
367
+ namespace { id fullPath }
368
+ widgets {
369
+ type
370
+ ... on WorkItemWidgetDescription { description descriptionHtml }
371
+ ... on WorkItemWidgetAssignees {
372
+ assignees { nodes { username name webUrl } }
373
+ }
374
+ ... on WorkItemWidgetLabels {
375
+ labels { nodes { id title color description } }
376
+ }
377
+ ... on WorkItemWidgetHierarchy {
378
+ hasChildren
379
+ hasParent
380
+ parent { id iid title workItemType { name } webUrl }
381
+ children {
382
+ nodes { id iid title state workItemType { name } webUrl }
383
+ }
384
+ }
385
+ ... on WorkItemWidgetMilestone {
386
+ milestone { id title state dueDate webPath }
387
+ }
388
+ ... on WorkItemWidgetStartAndDueDate { startDate dueDate }
389
+ ... on WorkItemWidgetNotes {
390
+ discussions(first: 5) {
391
+ nodes {
392
+ id
393
+ notes { nodes { id body author { username } createdAt } }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ }
399
+ }
400
+ `;
401
+ return this.query(query, { id: gid }, userConfig);
402
+ }
403
+ async listWorkItems(fullPath, opts = {}, userConfig) {
404
+ const { first = 20, after, fetchAll = false, types, state, sort = 'UPDATED_DESC' } = opts;
405
+ const query = gql `
406
+ query listWorkItems(
407
+ $fullPath: ID!
408
+ $first: Int!
409
+ $after: String
410
+ $types: [IssueType!]
411
+ $state: IssuableState
412
+ $sort: WorkItemSort
413
+ ) {
414
+ namespace(fullPath: $fullPath) {
415
+ id
416
+ fullPath
417
+ workItems(first: $first, after: $after, types: $types, state: $state, sort: $sort) {
418
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
419
+ nodes {
420
+ id
421
+ iid
422
+ title
423
+ state
424
+ confidential
425
+ createdAt
426
+ updatedAt
427
+ webUrl
428
+ workItemType { name iconName }
429
+ author { username name }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ `;
435
+ const variables = { fullPath, types, state, sort };
436
+ if (fetchAll) {
437
+ return this.fetchAllPages(query, variables, 'namespace.workItems', {
438
+ maxItems: first,
439
+ pageSize: this.config.maxPageSize,
440
+ userConfig,
441
+ });
442
+ }
443
+ return this.query(query, { ...variables, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
300
444
  }
301
445
  async createIssue(projectPath, title, description, userConfig) {
302
446
  await this.introspectSchema(userConfig);
@@ -623,10 +767,11 @@ export class GitLabGraphQLClient {
623
767
  const fields = type.getFields();
624
768
  return Object.keys(fields);
625
769
  }
626
- async globalSearch(searchTerm, scope, userConfig) {
770
+ async globalSearch(searchTerm, first = 20, after, userConfig) {
627
771
  const query = gql `
628
- query globalSearch($search: String, $first: Int!) {
629
- projects(search: $search, first: $first) {
772
+ query globalSearch($search: String, $first: Int!, $after: String) {
773
+ projects(search: $search, first: $first, after: $after) {
774
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
630
775
  nodes {
631
776
  id
632
777
  name
@@ -635,7 +780,8 @@ export class GitLabGraphQLClient {
635
780
  visibility
636
781
  }
637
782
  }
638
- issues(search: $search, first: $first) {
783
+ issues(search: $search, first: $first, after: $after) {
784
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
639
785
  nodes {
640
786
  id
641
787
  iid
@@ -649,10 +795,34 @@ export class GitLabGraphQLClient {
649
795
  `;
650
796
  return this.query(query, {
651
797
  search: searchTerm || undefined,
652
- first: Math.min(this.config.maxPageSize, 25)
798
+ first: Math.min(first, this.config.maxPageSize),
799
+ after
653
800
  }, userConfig);
654
801
  }
655
- async searchProjects(searchTerm, first = 20, after, userConfig) {
802
+ async globalSearchAll(searchTerm, maxItems = 100, userConfig) {
803
+ const projectsQuery = gql `
804
+ query globalSearchProjects($search: String, $first: Int!, $after: String) {
805
+ projects(search: $search, first: $first, after: $after) {
806
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
807
+ nodes { id name fullPath webUrl visibility }
808
+ }
809
+ }
810
+ `;
811
+ const issuesQuery = gql `
812
+ query globalSearchIssues($search: String, $first: Int!, $after: String) {
813
+ issues(search: $search, first: $first, after: $after) {
814
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
815
+ nodes { id iid title state webUrl createdAt }
816
+ }
817
+ }
818
+ `;
819
+ const [projects, issues] = await Promise.all([
820
+ this.fetchAllPages(projectsQuery, { search: searchTerm || undefined }, 'projects', { maxItems, userConfig }),
821
+ this.fetchAllPages(issuesQuery, { search: searchTerm || undefined }, 'issues', { maxItems, userConfig }),
822
+ ]);
823
+ return { projects, issues };
824
+ }
825
+ async searchProjects(searchTerm, first = 20, after, fetchAll = false, userConfig) {
656
826
  const query = gql `
657
827
  query searchProjects($search: String!, $first: Int!, $after: String) {
658
828
  projects(search: $search, first: $first, after: $after) {
@@ -669,7 +839,14 @@ export class GitLabGraphQLClient {
669
839
  }
670
840
  }
671
841
  `;
672
- return this.query(query, { search: searchTerm, first: Math.min(first, 50), after }, userConfig);
842
+ if (fetchAll) {
843
+ return this.fetchAllPages(query, { search: searchTerm }, 'projects', {
844
+ maxItems: first,
845
+ pageSize: this.config.maxPageSize,
846
+ userConfig,
847
+ });
848
+ }
849
+ return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
673
850
  }
674
851
  getTypeName(t) {
675
852
  if (!t)
@@ -683,7 +860,7 @@ export class GitLabGraphQLClient {
683
860
  const values = (enumType && typeof enumType.getValues === 'function') ? enumType.getValues() : [];
684
861
  return Array.isArray(values) ? values.map((v) => v.name) : [];
685
862
  }
686
- async searchIssues(searchTerm, projectPath, state, first = 20, after, userConfig, assigneeUsernames, authorUsername, labelNames) {
863
+ async searchIssues(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, assigneeUsernames, authorUsername, labelNames, sort) {
687
864
  await this.introspectSchema(userConfig);
688
865
  const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
689
866
  if (projectPath) {
@@ -697,9 +874,9 @@ export class GitLabGraphQLClient {
697
874
  ? (allowed.find(v => v.toLowerCase() === state.toLowerCase()) || undefined)
698
875
  : undefined;
699
876
  const query = gql `
700
- query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!]) {
877
+ query searchIssuesProject($projectPath: ID!, $search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
701
878
  project(fullPath: $projectPath) {
702
- issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName) {
879
+ issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
703
880
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
704
881
  nodes {
705
882
  id
@@ -716,6 +893,21 @@ export class GitLabGraphQLClient {
716
893
  }
717
894
  }
718
895
  `;
896
+ if (fetchAll) {
897
+ return this.fetchAllPages(query, {
898
+ projectPath,
899
+ search: searchTerm,
900
+ state: mapped,
901
+ assigneeUsernames,
902
+ authorUsername,
903
+ labelName: labelNames,
904
+ sort: sort || 'UPDATED_DESC'
905
+ }, 'project.issues', {
906
+ maxItems: first,
907
+ pageSize: this.config.maxPageSize,
908
+ userConfig,
909
+ });
910
+ }
719
911
  return this.query(query, {
720
912
  projectPath,
721
913
  search: searchTerm,
@@ -724,7 +916,8 @@ export class GitLabGraphQLClient {
724
916
  after,
725
917
  assigneeUsernames,
726
918
  authorUsername,
727
- labelName: labelNames
919
+ labelName: labelNames,
920
+ sort: sort || 'UPDATED_DESC'
728
921
  }, userConfig);
729
922
  }
730
923
  else {
@@ -742,8 +935,8 @@ export class GitLabGraphQLClient {
742
935
  // We keep description for AI context but limit nested collections (assignees/labels)
743
936
  // Note: Global Issue type doesn't have 'project' field - only project-scoped queries do
744
937
  const query = gql `
745
- query searchIssuesGlobal($search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!]) {
746
- issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName) {
938
+ query searchIssuesGlobal($search: String, $state: ${stateEnum}, $first: Int!, $after: String, $assigneeUsernames: [String!], $authorUsername: String, $labelName: [String!], $sort: IssueSort) {
939
+ issues(search: $search, state: $state, first: $first, after: $after, assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, labelName: $labelName, sort: $sort) {
747
940
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
748
941
  nodes {
749
942
  id
@@ -762,6 +955,20 @@ export class GitLabGraphQLClient {
762
955
  `;
763
956
  // Note: Global search returns streamlined fields (no assignees/labels/project) for performance.
764
957
  // For full details and project attribution, search within a specific project using projectPath.
958
+ if (fetchAll) {
959
+ return this.fetchAllPages(query, {
960
+ search: searchTerm,
961
+ state: mapped,
962
+ assigneeUsernames,
963
+ authorUsername,
964
+ labelName: labelNames,
965
+ sort: sort || 'UPDATED_DESC'
966
+ }, 'issues', {
967
+ maxItems: first,
968
+ pageSize: this.config.maxPageSize,
969
+ userConfig,
970
+ });
971
+ }
765
972
  return this.query(query, {
766
973
  search: searchTerm,
767
974
  state: mapped,
@@ -769,17 +976,18 @@ export class GitLabGraphQLClient {
769
976
  after,
770
977
  assigneeUsernames,
771
978
  authorUsername,
772
- labelName: labelNames
979
+ labelName: labelNames,
980
+ sort: sort || 'UPDATED_DESC'
773
981
  }, userConfig);
774
982
  }
775
983
  }
776
- async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, userConfig) {
984
+ async searchMergeRequests(searchTerm, projectPath, state, first = 20, after, fetchAll = false, userConfig, sort) {
777
985
  const mappedState = state && state.toLowerCase() !== 'all' ? state.toUpperCase() : undefined;
778
986
  if (projectPath) {
779
987
  const query = gql `
780
- query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String) {
988
+ query searchMergeRequestsProject($projectPath: ID!, $search: String, $state: MergeRequestState, $first: Int!, $after: String, $sort: MergeRequestSort) {
781
989
  project(fullPath: $projectPath) {
782
- mergeRequests(search: $search, state: $state, first: $first, after: $after) {
990
+ mergeRequests(search: $search, state: $state, first: $first, after: $after, sort: $sort) {
783
991
  pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
784
992
  nodes {
785
993
  id
@@ -798,12 +1006,25 @@ export class GitLabGraphQLClient {
798
1006
  }
799
1007
  }
800
1008
  `;
1009
+ if (fetchAll) {
1010
+ return this.fetchAllPages(query, {
1011
+ projectPath,
1012
+ search: searchTerm,
1013
+ state: mappedState,
1014
+ sort: sort || 'UPDATED_DESC'
1015
+ }, 'project.mergeRequests', {
1016
+ maxItems: first,
1017
+ pageSize: this.config.maxPageSize,
1018
+ userConfig,
1019
+ });
1020
+ }
801
1021
  return this.query(query, {
802
1022
  projectPath,
803
1023
  search: searchTerm,
804
1024
  state: mappedState,
805
- first: Math.min(first, 50),
806
- after
1025
+ first: Math.min(first, this.config.maxPageSize),
1026
+ after,
1027
+ sort: sort || 'UPDATED_DESC'
807
1028
  }, userConfig);
808
1029
  }
809
1030
  else {
@@ -851,9 +1072,20 @@ export class GitLabGraphQLClient {
851
1072
  }
852
1073
  `;
853
1074
  try {
1075
+ if (fetchAll) {
1076
+ const connectionPath = `user.${fieldName}`;
1077
+ return this.fetchAllPages(query, {
1078
+ username,
1079
+ state: mappedState
1080
+ }, connectionPath, {
1081
+ maxItems: first,
1082
+ pageSize: this.config.maxPageSize,
1083
+ userConfig,
1084
+ });
1085
+ }
854
1086
  const result = await this.query(query, {
855
1087
  username,
856
- first: Math.min(first, 50),
1088
+ first: Math.min(first, this.config.maxPageSize),
857
1089
  after,
858
1090
  state: mappedState
859
1091
  }, userConfig);
@@ -997,10 +1229,16 @@ export class GitLabGraphQLClient {
997
1229
  ref: ref || "HEAD"
998
1230
  }, userConfig);
999
1231
  }
1000
- async searchUsers(searchTerm, first = 20, userConfig) {
1232
+ async searchUsers(searchTerm, first = 20, after, fetchAll = false, userConfig) {
1001
1233
  const query = gql `
1002
- query searchUsers($search: String!, $first: Int!) {
1003
- users(search: $search, first: $first) {
1234
+ query searchUsers($search: String!, $first: Int!, $after: String) {
1235
+ users(search: $search, first: $first, after: $after) {
1236
+ pageInfo {
1237
+ hasNextPage
1238
+ hasPreviousPage
1239
+ startCursor
1240
+ endCursor
1241
+ }
1004
1242
  nodes {
1005
1243
  id
1006
1244
  username
@@ -1012,12 +1250,25 @@ export class GitLabGraphQLClient {
1012
1250
  }
1013
1251
  }
1014
1252
  `;
1015
- return this.query(query, { search: searchTerm, first: Math.min(first, 50) }, userConfig);
1253
+ if (fetchAll) {
1254
+ return this.fetchAllPages(query, { search: searchTerm }, 'users', {
1255
+ maxItems: first,
1256
+ pageSize: this.config.maxPageSize,
1257
+ userConfig,
1258
+ });
1259
+ }
1260
+ return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1016
1261
  }
1017
- async searchGroups(searchTerm, first = 20, userConfig) {
1262
+ async searchGroups(searchTerm, first = 20, after, fetchAll = false, userConfig) {
1018
1263
  const query = gql `
1019
- query searchGroups($search: String!, $first: Int!) {
1020
- groups(search: $search, first: $first) {
1264
+ query searchGroups($search: String!, $first: Int!, $after: String) {
1265
+ groups(search: $search, first: $first, after: $after) {
1266
+ pageInfo {
1267
+ hasNextPage
1268
+ hasPreviousPage
1269
+ startCursor
1270
+ endCursor
1271
+ }
1021
1272
  nodes {
1022
1273
  id
1023
1274
  name
@@ -1030,7 +1281,811 @@ export class GitLabGraphQLClient {
1030
1281
  }
1031
1282
  }
1032
1283
  `;
1033
- return this.query(query, { search: searchTerm, first: Math.min(first, 50) }, userConfig);
1284
+ if (fetchAll) {
1285
+ return this.fetchAllPages(query, { search: searchTerm }, 'groups', {
1286
+ maxItems: first,
1287
+ pageSize: this.config.maxPageSize,
1288
+ userConfig,
1289
+ });
1290
+ }
1291
+ return this.query(query, { search: searchTerm, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1292
+ }
1293
+ // ── CI/CD Pipelines ──────────────────────────────────────────────────
1294
+ async getMergeRequestPipelines(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
1295
+ const query = gql `
1296
+ query getMergeRequestPipelines($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1297
+ project(fullPath: $projectPath) {
1298
+ mergeRequest(iid: $iid) {
1299
+ pipelines(first: $first, after: $after) {
1300
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1301
+ nodes {
1302
+ id
1303
+ iid
1304
+ status
1305
+ duration
1306
+ createdAt
1307
+ finishedAt
1308
+ ref
1309
+ sha
1310
+ detailedStatus {
1311
+ text
1312
+ label
1313
+ icon
1314
+ group
1315
+ detailsPath
1316
+ }
1317
+ stages {
1318
+ nodes {
1319
+ name
1320
+ status
1321
+ detailedStatus {
1322
+ text
1323
+ label
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ }
1331
+ }
1332
+ `;
1333
+ if (fetchAll) {
1334
+ return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.pipelines', {
1335
+ maxItems: first,
1336
+ pageSize: this.config.maxPageSize,
1337
+ userConfig,
1338
+ });
1339
+ }
1340
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1341
+ }
1342
+ async getPipelineJobs(projectPath, pipelineIid, first = 20, after, fetchAll = false, userConfig) {
1343
+ const query = gql `
1344
+ query getPipelineJobs($projectPath: ID!, $pipelineIid: ID!, $first: Int!, $after: String) {
1345
+ project(fullPath: $projectPath) {
1346
+ pipeline(iid: $pipelineIid) {
1347
+ id
1348
+ iid
1349
+ status
1350
+ jobs(first: $first, after: $after) {
1351
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1352
+ nodes {
1353
+ id
1354
+ name
1355
+ status
1356
+ stage {
1357
+ name
1358
+ }
1359
+ duration
1360
+ queuedDuration
1361
+ createdAt
1362
+ startedAt
1363
+ finishedAt
1364
+ retryable
1365
+ cancelable
1366
+ webPath
1367
+ detailedStatus {
1368
+ text
1369
+ label
1370
+ icon
1371
+ group
1372
+ detailsPath
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ `;
1380
+ if (fetchAll) {
1381
+ return this.fetchAllPages(query, { projectPath, pipelineIid }, 'project.pipeline.jobs', {
1382
+ maxItems: first,
1383
+ pageSize: this.config.maxPageSize,
1384
+ userConfig,
1385
+ });
1386
+ }
1387
+ return this.query(query, { projectPath, pipelineIid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1388
+ }
1389
+ async getPipelineId(projectPath, pipelineIid, userConfig) {
1390
+ const query = gql `
1391
+ query pipelineId($projectPath: ID!, $pipelineIid: ID!) {
1392
+ project(fullPath: $projectPath) {
1393
+ pipeline(iid: $pipelineIid) { id }
1394
+ }
1395
+ }
1396
+ `;
1397
+ const result = await this.query(query, { projectPath, pipelineIid }, userConfig);
1398
+ const id = result?.project?.pipeline?.id;
1399
+ if (!id)
1400
+ throw new Error('Pipeline not found');
1401
+ return id;
1402
+ }
1403
+ async managePipeline(projectPath, pipelineIid, action, userConfig) {
1404
+ await this.introspectSchema(userConfig);
1405
+ const mutationType = this.schema?.getMutationType();
1406
+ const fields = mutationType ? mutationType.getFields() : {};
1407
+ const pipelineGlobalId = await this.getPipelineId(projectPath, pipelineIid, userConfig);
1408
+ if (action === 'retry') {
1409
+ const fieldName = fields['pipelineRetry'] ? 'pipelineRetry' : null;
1410
+ if (!fieldName) {
1411
+ throw new Error('pipelineRetry mutation is not available on this GitLab instance');
1412
+ }
1413
+ const mutation = gql `
1414
+ mutation retryPipeline($input: PipelineRetryInput!) {
1415
+ pipelineRetry(input: $input) {
1416
+ pipeline { id iid status }
1417
+ errors
1418
+ }
1419
+ }
1420
+ `;
1421
+ const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
1422
+ const payload = result.pipelineRetry;
1423
+ if (payload.errors && payload.errors.length > 0) {
1424
+ throw new Error(`Failed to retry pipeline: ${payload.errors.join(', ')}`);
1425
+ }
1426
+ return payload;
1427
+ }
1428
+ else {
1429
+ const fieldName = fields['pipelineCancel'] ? 'pipelineCancel' : null;
1430
+ if (!fieldName) {
1431
+ throw new Error('pipelineCancel mutation is not available on this GitLab instance');
1432
+ }
1433
+ const mutation = gql `
1434
+ mutation cancelPipeline($input: PipelineCancelInput!) {
1435
+ pipelineCancel(input: $input) {
1436
+ errors
1437
+ }
1438
+ }
1439
+ `;
1440
+ const result = await this.query(mutation, { input: { id: pipelineGlobalId } }, userConfig, true);
1441
+ const payload = result.pipelineCancel;
1442
+ if (payload.errors && payload.errors.length > 0) {
1443
+ throw new Error(`Failed to cancel pipeline: ${payload.errors.join(', ')}`);
1444
+ }
1445
+ return payload;
1446
+ }
1447
+ }
1448
+ // ── MR Diffs & Commits ───────────────────────────────────────────────
1449
+ async getMergeRequestDiffs(projectPath, iid, userConfig) {
1450
+ const query = gql `
1451
+ query getMergeRequestDiffs($projectPath: ID!, $iid: String!) {
1452
+ project(fullPath: $projectPath) {
1453
+ mergeRequest(iid: $iid) {
1454
+ diffStatsSummary {
1455
+ additions
1456
+ deletions
1457
+ fileCount
1458
+ }
1459
+ diffStats {
1460
+ path
1461
+ additions
1462
+ deletions
1463
+ }
1464
+ diffRefs {
1465
+ baseSha
1466
+ headSha
1467
+ startSha
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ `;
1473
+ return this.query(query, { projectPath, iid }, userConfig);
1474
+ }
1475
+ async getMergeRequestCommits(projectPath, iid, first = 20, after, fetchAll = false, userConfig) {
1476
+ const query = gql `
1477
+ query getMergeRequestCommits($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1478
+ project(fullPath: $projectPath) {
1479
+ mergeRequest(iid: $iid) {
1480
+ commitCount
1481
+ commitsWithoutMergeCommits(first: $first, after: $after) {
1482
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1483
+ nodes {
1484
+ sha
1485
+ shortId
1486
+ title
1487
+ message
1488
+ authorName
1489
+ authoredDate
1490
+ webUrl
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+ `;
1497
+ if (fetchAll) {
1498
+ return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.commitsWithoutMergeCommits', {
1499
+ maxItems: first,
1500
+ pageSize: this.config.maxPageSize,
1501
+ userConfig,
1502
+ });
1503
+ }
1504
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1505
+ }
1506
+ // ── Work Item Notes ──────────────────────────────────────────────────
1507
+ async getNotes(projectPath, noteableType, iid, first = 20, after, fetchAll = false, userConfig) {
1508
+ if (noteableType === 'issue') {
1509
+ const query = gql `
1510
+ query getIssueNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1511
+ project(fullPath: $projectPath) {
1512
+ issue(iid: $iid) {
1513
+ notes(first: $first, after: $after) {
1514
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1515
+ nodes {
1516
+ id
1517
+ body
1518
+ author { username name }
1519
+ createdAt
1520
+ updatedAt
1521
+ system
1522
+ internal
1523
+ url
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ `;
1530
+ if (fetchAll) {
1531
+ return this.fetchAllPages(query, { projectPath, iid }, 'project.issue.notes', {
1532
+ maxItems: first,
1533
+ pageSize: this.config.maxPageSize,
1534
+ userConfig,
1535
+ });
1536
+ }
1537
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1538
+ }
1539
+ else {
1540
+ const query = gql `
1541
+ query getMergeRequestNotes($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1542
+ project(fullPath: $projectPath) {
1543
+ mergeRequest(iid: $iid) {
1544
+ notes(first: $first, after: $after) {
1545
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1546
+ nodes {
1547
+ id
1548
+ body
1549
+ author { username name }
1550
+ createdAt
1551
+ updatedAt
1552
+ system
1553
+ internal
1554
+ url
1555
+ position {
1556
+ filePath
1557
+ newLine
1558
+ oldLine
1559
+ }
1560
+ }
1561
+ }
1562
+ }
1563
+ }
1564
+ }
1565
+ `;
1566
+ if (fetchAll) {
1567
+ return this.fetchAllPages(query, { projectPath, iid }, 'project.mergeRequest.notes', {
1568
+ maxItems: first,
1569
+ pageSize: this.config.maxPageSize,
1570
+ userConfig,
1571
+ });
1572
+ }
1573
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1574
+ }
1575
+ }
1576
+ async createNote(projectPath, noteableType, iid, body, internal = false, userConfig) {
1577
+ await this.introspectSchema(userConfig);
1578
+ const mutationType = this.schema?.getMutationType();
1579
+ const fields = mutationType ? mutationType.getFields() : {};
1580
+ const fieldName = fields['createNote'] ? 'createNote' : null;
1581
+ if (!fieldName) {
1582
+ throw new Error('createNote mutation is not available on this GitLab instance');
1583
+ }
1584
+ // Resolve the noteable IID to a global ID
1585
+ const noteableId = noteableType === 'issue'
1586
+ ? await this.getIssueId(projectPath, iid, userConfig)
1587
+ : await this.getMergeRequestId(projectPath, iid, userConfig);
1588
+ const mutation = gql `
1589
+ mutation createNote($input: CreateNoteInput!) {
1590
+ createNote(input: $input) {
1591
+ note {
1592
+ id
1593
+ body
1594
+ author { username name }
1595
+ createdAt
1596
+ url
1597
+ internal
1598
+ }
1599
+ errors
1600
+ }
1601
+ }
1602
+ `;
1603
+ const input = {
1604
+ noteableId,
1605
+ body,
1606
+ };
1607
+ if (internal) {
1608
+ input.internal = true;
1609
+ }
1610
+ const result = await this.query(mutation, { input }, userConfig, true);
1611
+ return result.createNote;
1612
+ }
1613
+ // ── Milestones ─────────────────────────────────────────────────────
1614
+ async listMilestones(fullPath, isProject, state, search, includeAncestors = false, first = 20, after, fetchAll = false, userConfig) {
1615
+ const milestoneFields = `
1616
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1617
+ nodes {
1618
+ id
1619
+ iid
1620
+ title
1621
+ description
1622
+ state
1623
+ dueDate
1624
+ startDate
1625
+ expired
1626
+ upcoming
1627
+ webPath
1628
+ stats {
1629
+ totalIssuesCount
1630
+ closedIssuesCount
1631
+ }
1632
+ createdAt
1633
+ updatedAt
1634
+ }
1635
+ `;
1636
+ if (isProject) {
1637
+ const query = gql `
1638
+ query listProjectMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
1639
+ project(fullPath: $fullPath) {
1640
+ milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
1641
+ ${milestoneFields}
1642
+ }
1643
+ }
1644
+ }
1645
+ `;
1646
+ if (fetchAll) {
1647
+ return this.fetchAllPages(query, {
1648
+ fullPath,
1649
+ state: state?.toLowerCase(),
1650
+ searchTitle: search,
1651
+ includeAncestors,
1652
+ }, 'project.milestones', {
1653
+ maxItems: first,
1654
+ pageSize: this.config.maxPageSize,
1655
+ userConfig,
1656
+ });
1657
+ }
1658
+ return this.query(query, {
1659
+ fullPath,
1660
+ state: state?.toLowerCase(),
1661
+ searchTitle: search,
1662
+ includeAncestors,
1663
+ first: Math.min(first, this.config.maxPageSize),
1664
+ after,
1665
+ }, userConfig);
1666
+ }
1667
+ else {
1668
+ const query = gql `
1669
+ query listGroupMilestones($fullPath: ID!, $state: MilestoneStateEnum, $searchTitle: String, $includeAncestors: Boolean, $first: Int!, $after: String) {
1670
+ group(fullPath: $fullPath) {
1671
+ milestones(state: $state, searchTitle: $searchTitle, includeAncestors: $includeAncestors, first: $first, after: $after) {
1672
+ ${milestoneFields}
1673
+ }
1674
+ }
1675
+ }
1676
+ `;
1677
+ if (fetchAll) {
1678
+ return this.fetchAllPages(query, {
1679
+ fullPath,
1680
+ state: state?.toLowerCase(),
1681
+ searchTitle: search,
1682
+ includeAncestors,
1683
+ }, 'group.milestones', {
1684
+ maxItems: first,
1685
+ pageSize: this.config.maxPageSize,
1686
+ userConfig,
1687
+ });
1688
+ }
1689
+ return this.query(query, {
1690
+ fullPath,
1691
+ state: state?.toLowerCase(),
1692
+ searchTitle: search,
1693
+ includeAncestors,
1694
+ first: Math.min(first, this.config.maxPageSize),
1695
+ after,
1696
+ }, userConfig);
1697
+ }
1698
+ }
1699
+ // ── Iterations ────────────────────────────────────────────────────
1700
+ async listIterations(groupPath, state, first = 20, after, fetchAll = false, userConfig) {
1701
+ const query = gql `
1702
+ query listIterations($groupPath: ID!, $state: IterationState, $first: Int!, $after: String) {
1703
+ group(fullPath: $groupPath) {
1704
+ iterations(state: $state, first: $first, after: $after) {
1705
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1706
+ nodes {
1707
+ id
1708
+ iid
1709
+ title
1710
+ description
1711
+ state
1712
+ startDate
1713
+ dueDate
1714
+ webUrl
1715
+ scopedPath
1716
+ iterationCadence {
1717
+ id
1718
+ title
1719
+ active
1720
+ durationInWeeks
1721
+ }
1722
+ createdAt
1723
+ updatedAt
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ `;
1729
+ if (fetchAll) {
1730
+ return this.fetchAllPages(query, {
1731
+ groupPath,
1732
+ state: state?.toLowerCase(),
1733
+ }, 'group.iterations', {
1734
+ maxItems: first,
1735
+ pageSize: this.config.maxPageSize,
1736
+ userConfig,
1737
+ });
1738
+ }
1739
+ return this.query(query, {
1740
+ groupPath,
1741
+ state: state?.toLowerCase(),
1742
+ first: Math.min(first, this.config.maxPageSize),
1743
+ after,
1744
+ }, userConfig);
1745
+ }
1746
+ // ── Time Tracking ─────────────────────────────────────────────────
1747
+ async getTimeTracking(projectPath, resourceType, iid, includeTimelogs = true, first = 20, after, userConfig) {
1748
+ const timelogFields = includeTimelogs ? `
1749
+ timelogs(first: $first, after: $after) {
1750
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1751
+ nodes {
1752
+ id
1753
+ timeSpent
1754
+ summary
1755
+ spentAt
1756
+ user {
1757
+ username
1758
+ name
1759
+ }
1760
+ }
1761
+ }
1762
+ ` : '';
1763
+ if (resourceType === 'issue') {
1764
+ const query = gql `
1765
+ query getIssueTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1766
+ project(fullPath: $projectPath) {
1767
+ issue(iid: $iid) {
1768
+ iid
1769
+ title
1770
+ webUrl
1771
+ timeEstimate
1772
+ totalTimeSpent
1773
+ humanTimeEstimate
1774
+ humanTotalTimeSpent
1775
+ ${timelogFields}
1776
+ }
1777
+ }
1778
+ }
1779
+ `;
1780
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1781
+ }
1782
+ else {
1783
+ const query = gql `
1784
+ query getMergeRequestTimeTracking($projectPath: ID!, $iid: String!, $first: Int!, $after: String) {
1785
+ project(fullPath: $projectPath) {
1786
+ mergeRequest(iid: $iid) {
1787
+ iid
1788
+ title
1789
+ webUrl
1790
+ timeEstimate
1791
+ totalTimeSpent
1792
+ humanTimeEstimate
1793
+ humanTotalTimeSpent
1794
+ ${timelogFields}
1795
+ }
1796
+ }
1797
+ }
1798
+ `;
1799
+ return this.query(query, { projectPath, iid, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1800
+ }
1801
+ }
1802
+ // ── MR Reviewers & Approvals ──────────────────────────────────────
1803
+ async getMergeRequestReviewers(projectPath, iid, userConfig) {
1804
+ const query = gql `
1805
+ query getMergeRequestReviewers($projectPath: ID!, $iid: String!) {
1806
+ project(fullPath: $projectPath) {
1807
+ mergeRequest(iid: $iid) {
1808
+ iid
1809
+ title
1810
+ webUrl
1811
+ state
1812
+ approved
1813
+ approvalsRequired
1814
+ approvalsLeft
1815
+ approvedBy {
1816
+ nodes {
1817
+ username
1818
+ name
1819
+ avatarUrl
1820
+ webUrl
1821
+ }
1822
+ }
1823
+ reviewers {
1824
+ nodes {
1825
+ username
1826
+ name
1827
+ avatarUrl
1828
+ webUrl
1829
+ mergeRequestInteraction {
1830
+ reviewState
1831
+ approved
1832
+ reviewed
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ }
1838
+ }
1839
+ `;
1840
+ return this.query(query, { projectPath, iid }, userConfig);
1841
+ }
1842
+ // ── Project Statistics ────────────────────────────────────────────
1843
+ async getProjectStatistics(projectPath, userConfig) {
1844
+ const query = gql `
1845
+ query getProjectStatistics($projectPath: ID!) {
1846
+ project(fullPath: $projectPath) {
1847
+ id
1848
+ name
1849
+ fullPath
1850
+ webUrl
1851
+ starCount
1852
+ forksCount
1853
+ openIssuesCount
1854
+ statistics {
1855
+ repositorySize
1856
+ lfsObjectsSize
1857
+ buildArtifactsSize
1858
+ packagesSize
1859
+ wikiSize
1860
+ snippetsSize
1861
+ uploadsSize
1862
+ containerRegistrySize
1863
+ commitCount
1864
+ }
1865
+ openMergeRequests: mergeRequests(state: opened) {
1866
+ count
1867
+ }
1868
+ lastPipeline: pipelines(first: 1) {
1869
+ nodes {
1870
+ id
1871
+ iid
1872
+ status
1873
+ createdAt
1874
+ finishedAt
1875
+ ref
1876
+ detailedStatus {
1877
+ text
1878
+ label
1879
+ }
1880
+ }
1881
+ }
1882
+ releaseCount: releases {
1883
+ count
1884
+ }
1885
+ languages {
1886
+ name
1887
+ share
1888
+ color
1889
+ }
1890
+ }
1891
+ }
1892
+ `;
1893
+ return this.query(query, { projectPath }, userConfig);
1894
+ }
1895
+ // ── Group Members ─────────────────────────────────────────────────
1896
+ async listGroupMembers(groupPath, search, first = 20, after, fetchAll = false, userConfig) {
1897
+ const query = gql `
1898
+ query listGroupMembers($groupPath: ID!, $search: String, $first: Int!, $after: String) {
1899
+ group(fullPath: $groupPath) {
1900
+ groupMembers(search: $search, first: $first, after: $after) {
1901
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1902
+ nodes {
1903
+ id
1904
+ accessLevel {
1905
+ stringValue
1906
+ integerValue
1907
+ }
1908
+ createdAt
1909
+ expiresAt
1910
+ user {
1911
+ username
1912
+ name
1913
+ avatarUrl
1914
+ webUrl
1915
+ state
1916
+ }
1917
+ }
1918
+ }
1919
+ }
1920
+ }
1921
+ `;
1922
+ if (fetchAll) {
1923
+ return this.fetchAllPages(query, {
1924
+ groupPath,
1925
+ search: search || undefined,
1926
+ }, 'group.groupMembers', {
1927
+ maxItems: first,
1928
+ pageSize: this.config.maxPageSize,
1929
+ userConfig,
1930
+ });
1931
+ }
1932
+ return this.query(query, {
1933
+ groupPath,
1934
+ search: search || undefined,
1935
+ first: Math.min(first, this.config.maxPageSize),
1936
+ after,
1937
+ }, userConfig);
1938
+ }
1939
+ // ── Label Search ─────────────────────────────────────────────────────
1940
+ async searchLabels(fullPath, isProject, search, first = 20, after, fetchAll = false, userConfig) {
1941
+ if (isProject) {
1942
+ const query = gql `
1943
+ query searchProjectLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
1944
+ project(fullPath: $fullPath) {
1945
+ labels(searchTerm: $search, first: $first, after: $after) {
1946
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1947
+ nodes {
1948
+ id
1949
+ title
1950
+ description
1951
+ color
1952
+ textColor
1953
+ createdAt
1954
+ updatedAt
1955
+ }
1956
+ }
1957
+ }
1958
+ }
1959
+ `;
1960
+ if (fetchAll) {
1961
+ return this.fetchAllPages(query, { fullPath, search }, 'project.labels', {
1962
+ maxItems: first,
1963
+ pageSize: this.config.maxPageSize,
1964
+ userConfig,
1965
+ });
1966
+ }
1967
+ return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1968
+ }
1969
+ else {
1970
+ const query = gql `
1971
+ query searchGroupLabels($fullPath: ID!, $search: String, $first: Int!, $after: String) {
1972
+ group(fullPath: $fullPath) {
1973
+ labels(searchTerm: $search, first: $first, after: $after) {
1974
+ pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
1975
+ nodes {
1976
+ id
1977
+ title
1978
+ description
1979
+ color
1980
+ textColor
1981
+ createdAt
1982
+ updatedAt
1983
+ }
1984
+ }
1985
+ }
1986
+ }
1987
+ `;
1988
+ if (fetchAll) {
1989
+ return this.fetchAllPages(query, { fullPath, search }, 'group.labels', {
1990
+ maxItems: first,
1991
+ pageSize: this.config.maxPageSize,
1992
+ userConfig,
1993
+ });
1994
+ }
1995
+ return this.query(query, { fullPath, search, first: Math.min(first, this.config.maxPageSize), after }, userConfig);
1996
+ }
1997
+ }
1998
+ /**
1999
+ * Resolve the GitLab base URL and access token for a REST call, honoring
2000
+ * per-request user credentials and falling back to the shared token.
2001
+ */
2002
+ resolveRestAuth(userConfig, requiresWrite = false) {
2003
+ if (userConfig) {
2004
+ return {
2005
+ baseUrl: userConfig.gitlabUrl || this.config.gitlabUrl,
2006
+ token: userConfig.accessToken,
2007
+ };
2008
+ }
2009
+ if (requiresWrite) {
2010
+ throw new Error('Write operations require user authentication. Please provide your GitLab credentials.');
2011
+ }
2012
+ if (this.config.sharedAccessToken && this.config.authMode !== 'per-user') {
2013
+ return { baseUrl: this.config.gitlabUrl, token: this.config.sharedAccessToken };
2014
+ }
2015
+ throw new Error('This operation requires user authentication. Please provide your GitLab credentials.');
2016
+ }
2017
+ /**
2018
+ * Perform a GitLab REST API v4 request. Used for endpoints not exposed via GraphQL
2019
+ * (e.g., broadcast messages).
2020
+ */
2021
+ async restRequest(method, path, options = {}) {
2022
+ const { baseUrl, token } = this.resolveRestAuth(options.userConfig, options.requiresWrite);
2023
+ const url = new URL(`${baseUrl.replace(/\/$/, '')}/api/v4${path}`);
2024
+ if (options.query) {
2025
+ for (const [k, v] of Object.entries(options.query)) {
2026
+ if (v !== undefined && v !== null)
2027
+ url.searchParams.set(k, String(v));
2028
+ }
2029
+ }
2030
+ const timeoutMs = this.config.defaultTimeout || 30000;
2031
+ return this.executeWithRetry(async () => {
2032
+ const controller = new AbortController();
2033
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2034
+ try {
2035
+ const response = await fetch(url.toString(), {
2036
+ method,
2037
+ headers: {
2038
+ Authorization: `Bearer ${token}`,
2039
+ 'Content-Type': 'application/json',
2040
+ Accept: 'application/json',
2041
+ },
2042
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
2043
+ signal: controller.signal,
2044
+ });
2045
+ if (!response.ok) {
2046
+ const statusCode = response.status;
2047
+ let message = `${response.statusText}`;
2048
+ try {
2049
+ const errBody = await response.json();
2050
+ message = errBody?.message || errBody?.error || JSON.stringify(errBody);
2051
+ }
2052
+ catch {
2053
+ // non-JSON body
2054
+ }
2055
+ const retryable = RETRY_CONFIG.retryableStatusCodes.includes(statusCode);
2056
+ throw new GitLabAPIError(`GitLab REST ${method} ${path} failed (${statusCode}): ${message}`, {
2057
+ code: statusCode === 401 ? 'AUTH_FAILED' : statusCode === 403 ? 'FORBIDDEN' : 'HTTP_ERROR',
2058
+ statusCode,
2059
+ isRetryable: retryable,
2060
+ });
2061
+ }
2062
+ if (response.status === 204)
2063
+ return undefined;
2064
+ const text = await response.text();
2065
+ return (text ? JSON.parse(text) : undefined);
2066
+ }
2067
+ finally {
2068
+ clearTimeout(timeoutId);
2069
+ }
2070
+ }, `REST ${method} ${path}`);
2071
+ }
2072
+ async listBroadcastMessages(page = 1, perPage = 20, userConfig) {
2073
+ return this.restRequest('GET', '/broadcast_messages', {
2074
+ query: { page, per_page: Math.min(perPage, this.config.maxPageSize) },
2075
+ userConfig,
2076
+ });
2077
+ }
2078
+ async getBroadcastMessage(id, userConfig) {
2079
+ return this.restRequest('GET', `/broadcast_messages/${id}`, { userConfig });
2080
+ }
2081
+ async createBroadcastMessage(input, userConfig) {
2082
+ return this.restRequest('POST', '/broadcast_messages', { body: input, userConfig, requiresWrite: true });
2083
+ }
2084
+ async updateBroadcastMessage(id, input, userConfig) {
2085
+ return this.restRequest('PUT', `/broadcast_messages/${id}`, { body: input, userConfig, requiresWrite: true });
2086
+ }
2087
+ async deleteBroadcastMessage(id, userConfig) {
2088
+ return this.restRequest('DELETE', `/broadcast_messages/${id}`, { userConfig, requiresWrite: true });
1034
2089
  }
1035
2090
  }
1036
2091
  //# sourceMappingURL=gitlab-client.js.map