mobbdev 1.4.36 → 1.4.41

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.
@@ -342,10 +342,6 @@ var init_client_generates = __esm({
342
342
  inputType
343
343
  options
344
344
  index
345
- extraContext {
346
- key
347
- value
348
- }
349
345
  content
350
346
  description
351
347
  guidance
@@ -583,10 +579,6 @@ var init_client_generates = __esm({
583
579
  patchOriginalEncodingBase64
584
580
  questions {
585
581
  defaultValue
586
- extraContext {
587
- key
588
- value
589
- }
590
582
  index
591
583
  inputType
592
584
  key
@@ -1090,10 +1082,6 @@ var init_client_generates = __esm({
1090
1082
  inputType
1091
1083
  options
1092
1084
  index
1093
- extraContext {
1094
- key
1095
- value
1096
- }
1097
1085
  content
1098
1086
  description
1099
1087
  guidance
@@ -1352,7 +1340,6 @@ var init_fix = __esm({
1352
1340
  index: z2.number(),
1353
1341
  defaultValue: z2.string(),
1354
1342
  value: z2.string().nullable(),
1355
- extraContext: z2.array(ExtraContextInternalZ),
1356
1343
  inputType: z2.nativeEnum(FixQuestionInputType),
1357
1344
  options: z2.array(z2.string()),
1358
1345
  // E-2015: analyzer-served question text. default('') so a query that
package/dist/index.mjs CHANGED
@@ -342,10 +342,6 @@ var init_client_generates = __esm({
342
342
  inputType
343
343
  options
344
344
  index
345
- extraContext {
346
- key
347
- value
348
- }
349
345
  content
350
346
  description
351
347
  guidance
@@ -583,10 +579,6 @@ var init_client_generates = __esm({
583
579
  patchOriginalEncodingBase64
584
580
  questions {
585
581
  defaultValue
586
- extraContext {
587
- key
588
- value
589
- }
590
582
  index
591
583
  inputType
592
584
  key
@@ -1090,10 +1082,6 @@ var init_client_generates = __esm({
1090
1082
  inputType
1091
1083
  options
1092
1084
  index
1093
- extraContext {
1094
- key
1095
- value
1096
- }
1097
1085
  content
1098
1086
  description
1099
1087
  guidance
@@ -1454,7 +1442,6 @@ var init_fix = __esm({
1454
1442
  index: z3.number(),
1455
1443
  defaultValue: z3.string(),
1456
1444
  value: z3.string().nullable(),
1457
- extraContext: z3.array(ExtraContextInternalZ),
1458
1445
  inputType: z3.nativeEnum(FixQuestionInputType),
1459
1446
  options: z3.array(z3.string()),
1460
1447
  // E-2015: analyzer-served question text. default('') so a query that
@@ -4744,22 +4731,21 @@ function getScmConfig({
4744
4731
  url,
4745
4732
  scmConfigs,
4746
4733
  brokerHosts,
4747
- includeOrgTokens = true
4734
+ includeOrgTokens = true,
4735
+ allowRelaxedProtocolMatch = false
4748
4736
  }) {
4749
4737
  const urlObject = new URL(url);
4750
- const filteredScmConfigs = scmConfigs.filter((scm) => {
4738
+ const matchesHostname = (configUrl) => urlObject.hostname.toLowerCase() === configUrl.hostname.toLowerCase() || urlObject.hostname.toLowerCase().endsWith(".visualstudio.com") && configUrl.hostname.toLowerCase() === "dev.azure.com";
4739
+ const exactScmConfigs = scmConfigs.filter((scm) => {
4751
4740
  const configUrl = new URL(scm.scmUrl);
4752
- return (
4753
- //if we the user does an ADO oauth flow then the token is saved for dev.azure.com but
4754
- //sometimes the user uses the url dev.azure.com and sometimes the url visualstudio.com
4755
- //so we need to check both
4756
- (urlObject.hostname.toLowerCase() === configUrl.hostname.toLowerCase() || urlObject.hostname.toLowerCase().endsWith(".visualstudio.com") && configUrl.hostname.toLowerCase() === "dev.azure.com") && urlObject.protocol === configUrl.protocol && urlObject.port === configUrl.port
4757
- );
4741
+ return matchesHostname(configUrl) && urlObject.protocol === configUrl.protocol && urlObject.port === configUrl.port;
4758
4742
  });
4759
4743
  const filteredBrokerHosts = brokerHosts.filter((broker) => {
4760
4744
  const urlObject2 = new URL(url);
4761
4745
  return urlObject2.hostname.toLowerCase() === broker.realDomain.toLowerCase();
4762
4746
  });
4747
+ const brokerBacked = filteredBrokerHosts.length > 0;
4748
+ const filteredScmConfigs = exactScmConfigs.length > 0 ? exactScmConfigs : allowRelaxedProtocolMatch && brokerBacked ? scmConfigs.filter((scm) => matchesHostname(new URL(scm.scmUrl))) : [];
4763
4749
  const virtualDomain = filteredBrokerHosts[0]?.virtualDomain;
4764
4750
  const virtualUrl = virtualDomain ? `https://${virtualDomain}${urlObject.pathname}${urlObject.search}` : void 0;
4765
4751
  const matched = filteredScmConfigs.find((scm) => {
@@ -5373,6 +5359,59 @@ async function getAdoSdk(params) {
5373
5359
  );
5374
5360
  return commits;
5375
5361
  },
5362
+ // Distinct commit authors since `since`, paged with $skip (the plain
5363
+ // getAdoRecentCommits above fetches only the first 100 — no pagination — so
5364
+ // it under-reports 90-day contributors on any active repo). Accumulates one
5365
+ // row per author as pages arrive → memory bounded by author count, uncapped
5366
+ // walk (the per-repo sync timeout bounds pathological repos). ADO commits
5367
+ // carry no stable author account id here, so authors are keyed by email.
5368
+ async getAdoRecentCommitAuthors({
5369
+ repoUrl,
5370
+ since
5371
+ }) {
5372
+ const { repo, projectName } = parseAdoOwnerAndRepo(repoUrl);
5373
+ const git = await api2.getGitApi();
5374
+ const repository = await git.getRepository(
5375
+ decodeURI(repo),
5376
+ projectName ? decodeURI(projectName) : void 0
5377
+ );
5378
+ const defaultBranch = repository.defaultBranch?.replace("refs/heads/", "");
5379
+ const byEmail = /* @__PURE__ */ new Map();
5380
+ const PAGE = 100;
5381
+ let skip = 0;
5382
+ while (true) {
5383
+ const page = await git.getCommits(
5384
+ repo,
5385
+ {
5386
+ fromDate: since,
5387
+ itemVersion: defaultBranch ? { version: defaultBranch } : void 0,
5388
+ $top: PAGE,
5389
+ $skip: skip
5390
+ },
5391
+ projectName
5392
+ );
5393
+ if (!page || page.length === 0) break;
5394
+ for (const c of page) {
5395
+ const email = c.author?.email;
5396
+ if (!email) continue;
5397
+ const date = c.author?.date ? new Date(c.author.date).toISOString() : since;
5398
+ const key = email.toLowerCase();
5399
+ const prev = byEmail.get(key);
5400
+ if (!prev || new Date(date).getTime() > new Date(prev.date).getTime()) {
5401
+ byEmail.set(key, {
5402
+ email: key,
5403
+ name: c.author?.name ?? null,
5404
+ date,
5405
+ externalId: null,
5406
+ isBot: false
5407
+ });
5408
+ }
5409
+ }
5410
+ if (page.length < PAGE) break;
5411
+ skip += PAGE;
5412
+ }
5413
+ return [...byEmail.values()];
5414
+ },
5376
5415
  async getAdoPrCommits({
5377
5416
  repoUrl,
5378
5417
  prNumber
@@ -6130,6 +6169,11 @@ var AdoSCMLib = class extends SCMLib {
6130
6169
  }))
6131
6170
  };
6132
6171
  }
6172
+ async getRecentCommitAuthors(since) {
6173
+ this._validateAccessTokenAndUrl();
6174
+ const adoSdk = await this.getAdoSdk();
6175
+ return adoSdk.getAdoRecentCommitAuthors({ repoUrl: this.url, since });
6176
+ }
6133
6177
  async getRateLimitStatus() {
6134
6178
  return {
6135
6179
  remaining: 1e4,
@@ -6168,6 +6212,11 @@ var BitbucketAuthResultZ = z15.object({
6168
6212
 
6169
6213
  // src/features/analysis/scm/bitbucket/bitbucket.ts
6170
6214
  var debug3 = Debug2("scm:bitbucket");
6215
+ function isBitbucketPermissionDenied(e) {
6216
+ const status = e?.status;
6217
+ const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();
6218
+ return status === 403 || /forbidden|insufficient/.test(msg);
6219
+ }
6171
6220
  var BITBUCKET_HOSTNAME = "bitbucket.org";
6172
6221
  var TokenExpiredErrorZ = z16.object({
6173
6222
  status: z16.number(),
@@ -6428,21 +6477,28 @@ function getBitbucketSdk(params) {
6428
6477
  return res.data;
6429
6478
  },
6430
6479
  async getWorkspaceMembers(params2) {
6431
- const allMembers = [];
6432
- let hasMore = true;
6433
- let page = 1;
6434
- while (hasMore) {
6435
- const res = await bitbucketClient.workspaces.getMembersForWorkspace({
6436
- workspace: params2.workspace,
6437
- page: String(page),
6438
- pagelen: 100
6439
- });
6440
- const values = res.data.values ?? [];
6441
- allMembers.push(...values);
6442
- hasMore = Boolean(res.data.next) && values.length > 0;
6443
- page++;
6480
+ try {
6481
+ const allMembers = [];
6482
+ let hasMore = true;
6483
+ let page = 1;
6484
+ while (hasMore) {
6485
+ const res = await bitbucketClient.workspaces.getMembersForWorkspace({
6486
+ workspace: params2.workspace,
6487
+ page: String(page),
6488
+ pagelen: 100
6489
+ });
6490
+ const values = res.data.values ?? [];
6491
+ allMembers.push(...values);
6492
+ hasMore = Boolean(res.data.next) && values.length > 0;
6493
+ page++;
6494
+ }
6495
+ return allMembers;
6496
+ } catch (e) {
6497
+ if (isBitbucketPermissionDenied(e)) {
6498
+ return [];
6499
+ }
6500
+ throw e;
6444
6501
  }
6445
- return allMembers;
6446
6502
  },
6447
6503
  async getCurrentUserWithEmail() {
6448
6504
  try {
@@ -6489,15 +6545,74 @@ function getBitbucketSdk(params) {
6489
6545
  }
6490
6546
  return commits;
6491
6547
  },
6548
+ // Distinct authors of commits in the window (since), streamed and
6549
+ // window-bounded by date rather than a fixed page cap, so the 90-day count
6550
+ // is exact even on a busy repo. Accumulates one row per author (latest date)
6551
+ // as pages arrive → memory bounded by author count. Errors propagate for
6552
+ // classification (the repo is the read-commits population source).
6553
+ async streamRecentCommitAuthors(params2) {
6554
+ const sinceMs = new Date(params2.since).getTime();
6555
+ const byKey = /* @__PURE__ */ new Map();
6556
+ let page = 1;
6557
+ const HARD_PAGE_CEILING = 1e4;
6558
+ while (page <= HARD_PAGE_CEILING) {
6559
+ const res = await bitbucketClient.repositories.listCommits({
6560
+ repo_slug: params2.repo_slug,
6561
+ workspace: params2.workspace,
6562
+ page: String(page),
6563
+ pagelen: 100
6564
+ });
6565
+ const data = res.data;
6566
+ const commits = data?.values ?? [];
6567
+ if (commits.length === 0) break;
6568
+ let sawInWindow = false;
6569
+ for (const commit of commits) {
6570
+ const record = commit;
6571
+ const dateStr = record["date"];
6572
+ if (dateStr && new Date(dateStr).getTime() < sinceMs) {
6573
+ continue;
6574
+ }
6575
+ sawInWindow = true;
6576
+ const author = record["author"];
6577
+ if (!author?.raw) continue;
6578
+ const match = author.raw.match(/^(.+?)\s*<([^>]+)>/);
6579
+ if (!match) continue;
6580
+ const [, name, email] = match;
6581
+ if (!email) continue;
6582
+ const externalId = author.user?.account_id ?? null;
6583
+ const date = dateStr ?? params2.since;
6584
+ const key = externalId ?? email.toLowerCase();
6585
+ const prev = byKey.get(key);
6586
+ if (!prev || new Date(date).getTime() > new Date(prev.date).getTime()) {
6587
+ byKey.set(key, {
6588
+ email: email.toLowerCase(),
6589
+ name: name.trim() || null,
6590
+ date,
6591
+ externalId,
6592
+ isBot: false
6593
+ });
6594
+ }
6595
+ }
6596
+ if (!sawInWindow) break;
6597
+ if (!data?.next) break;
6598
+ page++;
6599
+ }
6600
+ return [...byKey.values()];
6601
+ },
6492
6602
  async getRepoCommitAuthors(params2) {
6493
- try {
6603
+ const MAX_PAGES = 100;
6604
+ const authorMap = /* @__PURE__ */ new Map();
6605
+ let page = 1;
6606
+ while (page <= MAX_PAGES) {
6494
6607
  const res = await bitbucketClient.repositories.listCommits({
6495
6608
  repo_slug: params2.repo_slug,
6496
6609
  workspace: params2.workspace,
6610
+ page: String(page),
6497
6611
  pagelen: 100
6498
6612
  });
6499
- const commits = res.data?.values ?? [];
6500
- const authorMap = /* @__PURE__ */ new Map();
6613
+ const data = res.data;
6614
+ const commits = data?.values ?? [];
6615
+ if (commits.length === 0) break;
6501
6616
  for (const commit of commits) {
6502
6617
  const raw = commit?.["author"];
6503
6618
  if (!raw?.raw) continue;
@@ -6515,10 +6630,10 @@ function getBitbucketSdk(params) {
6515
6630
  });
6516
6631
  }
6517
6632
  }
6518
- return Array.from(authorMap.values());
6519
- } catch {
6520
- return [];
6633
+ if (!data?.next) break;
6634
+ page++;
6521
6635
  }
6636
+ return Array.from(authorMap.values());
6522
6637
  }
6523
6638
  };
6524
6639
  }
@@ -6865,17 +6980,33 @@ var BitbucketSCMLib = class extends SCMLib {
6865
6980
  })
6866
6981
  };
6867
6982
  }
6983
+ async getRecentCommitAuthors(since) {
6984
+ this._validateAccessTokenAndUrl();
6985
+ const { workspace, repo_slug } = parseBitbucketOrganizationAndRepo(this.url);
6986
+ const bitbucketSdk = createBitbucketSdk(this.accessToken);
6987
+ return bitbucketSdk.streamRecentCommitAuthors({
6988
+ workspace,
6989
+ repo_slug,
6990
+ since
6991
+ });
6992
+ }
6868
6993
  async getRateLimitStatus() {
6869
6994
  return null;
6870
6995
  }
6871
6996
  async getRepositoryContributors() {
6872
6997
  this._validateAccessTokenAndUrl();
6873
6998
  const { workspace, repo_slug } = parseBitbucketOrganizationAndRepo(this.url);
6874
- const [members, commitAuthors, currentUser] = await Promise.all([
6999
+ const [membersRes, commitAuthorsRes, currentUserRes] = await Promise.allSettled([
6875
7000
  this.bitbucketSdk.getWorkspaceMembers({ workspace }),
6876
7001
  this.bitbucketSdk.getRepoCommitAuthors({ workspace, repo_slug }),
6877
7002
  this.bitbucketSdk.getCurrentUserWithEmail()
6878
7003
  ]);
7004
+ if (membersRes.status === "rejected" && commitAuthorsRes.status === "rejected") {
7005
+ throw membersRes.reason;
7006
+ }
7007
+ const members = membersRes.status === "fulfilled" ? membersRes.value : [];
7008
+ const commitAuthors = commitAuthorsRes.status === "fulfilled" ? commitAuthorsRes.value : [];
7009
+ const currentUser = currentUserRes.status === "fulfilled" ? currentUserRes.value : { accountId: null, email: null };
6879
7010
  const emailByAccountId = /* @__PURE__ */ new Map();
6880
7011
  const emailByName = /* @__PURE__ */ new Map();
6881
7012
  for (const author of commitAuthors) {
@@ -6933,6 +7064,20 @@ var BitbucketSCMLib = class extends SCMLib {
6933
7064
  accessLevel: null
6934
7065
  };
6935
7066
  });
7067
+ if (result.length === 0 && commitAuthors.length > 0) {
7068
+ return commitAuthors.filter((a) => a.accountId || a.email || a.name).map((a) => ({
7069
+ // Real Bitbucket account id keeps the member identity; otherwise use a
7070
+ // synthetic commit key so email/display-name-only rows can't collide
7071
+ // with numeric member rows (or, on Bitbucket, merge two distinct
7072
+ // people who happen to share a display name). Lowercased to match the
7073
+ // 90-day stamp path (commit:<lowercased email>).
7074
+ externalId: a.accountId ?? `commit:${(a.email || a.name).toLowerCase()}`,
7075
+ username: null,
7076
+ displayName: a.name ?? null,
7077
+ email: a.email ?? null,
7078
+ accessLevel: null
7079
+ }));
7080
+ }
6936
7081
  const withEmail = result.filter((c) => c.email);
6937
7082
  contextLogger.info("[Bitbucket] Contributor enrichment summary", {
6938
7083
  total: result.length,
@@ -7721,6 +7866,42 @@ function getGithubSdk(params = {}) {
7721
7866
  });
7722
7867
  return { data: commits };
7723
7868
  },
7869
+ // Distinct commit authors in the window, streamed. Uses paginate.iterator so
7870
+ // only ONE page is in memory at a time and we accumulate one row per author
7871
+ // (keeping their latest commit date) — memory is bounded by author count, not
7872
+ // commit volume, and the walk is uncapped so the 90-day count is exact even
7873
+ // for a repo with hundreds of thousands of commits.
7874
+ async streamRecentCommitAuthors(params2) {
7875
+ const byAuthor = /* @__PURE__ */ new Map();
7876
+ for await (const { data } of octokit.paginate.iterator(
7877
+ octokit.rest.repos.listCommits,
7878
+ {
7879
+ owner: params2.owner,
7880
+ repo: params2.repo,
7881
+ since: params2.since,
7882
+ per_page: 100
7883
+ }
7884
+ )) {
7885
+ for (const c of data) {
7886
+ const email = c.commit?.author?.email;
7887
+ if (!email) continue;
7888
+ const externalId = c.author?.id != null ? String(c.author.id) : null;
7889
+ const date = c.commit?.committer?.date ?? c.commit?.author?.date ?? params2.since;
7890
+ const key = externalId ?? email.toLowerCase();
7891
+ const prev = byAuthor.get(key);
7892
+ if (!prev || new Date(date).getTime() > new Date(prev.date).getTime()) {
7893
+ byAuthor.set(key, {
7894
+ email: email.toLowerCase(),
7895
+ name: c.commit?.author?.name ?? null,
7896
+ date,
7897
+ externalId,
7898
+ isBot: c.author?.type === "Bot"
7899
+ });
7900
+ }
7901
+ }
7902
+ }
7903
+ return [...byAuthor.values()];
7904
+ },
7724
7905
  async getRateLimitStatus() {
7725
7906
  const response = await octokit.rest.rateLimit.get();
7726
7907
  return {
@@ -7886,15 +8067,46 @@ function getGithubSdk(params = {}) {
7886
8067
  };
7887
8068
  },
7888
8069
  async listRepositoryCollaborators(params2) {
7889
- const collaborators = await octokit.paginate(
7890
- octokit.rest.repos.listCollaborators,
8070
+ try {
8071
+ const collaborators = await octokit.paginate(
8072
+ octokit.rest.repos.listCollaborators,
8073
+ {
8074
+ owner: params2.owner,
8075
+ repo: params2.repo,
8076
+ per_page: 100
8077
+ }
8078
+ );
8079
+ return collaborators;
8080
+ } catch (error) {
8081
+ const status = error?.status;
8082
+ const message = error instanceof Error ? error.message : String(error);
8083
+ if (/rate limit|secondary rate|abuse detection/i.test(message)) {
8084
+ throw error;
8085
+ }
8086
+ if (status === 403 || status === 404 || /push access|forbidden/i.test(message)) {
8087
+ return [];
8088
+ }
8089
+ throw error;
8090
+ }
8091
+ },
8092
+ // Read-only contributor list (commit authors, aggregated). Unlike
8093
+ // listCollaborators this needs only read access, so it works for repos where
8094
+ // we can read code but aren't a push-level collaborator.
8095
+ async listRepositoryContributors(params2) {
8096
+ const contributors = await octokit.paginate(
8097
+ octokit.rest.repos.listContributors,
7891
8098
  {
7892
8099
  owner: params2.owner,
7893
8100
  repo: params2.repo,
7894
8101
  per_page: 100
7895
- }
8102
+ },
8103
+ (response) => response.data.map((c) => ({
8104
+ login: c.login,
8105
+ id: c.id,
8106
+ type: c.type
8107
+ }))
7896
8108
  );
7897
- return collaborators;
8109
+ return contributors;
7898
8110
  },
7899
8111
  async listOrgMembers(params2) {
7900
8112
  const members = await octokit.paginate(octokit.rest.orgs.listMembers, {
@@ -8154,6 +8366,11 @@ var GithubSCMLib = class extends SCMLib {
8154
8366
  }))
8155
8367
  };
8156
8368
  }
8369
+ async getRecentCommitAuthors(since) {
8370
+ this._validateAccessTokenAndUrl();
8371
+ const { owner, repo } = parseGithubOwnerAndRepo(this.url);
8372
+ return this.githubSdk.streamRecentCommitAuthors({ owner, repo, since });
8373
+ }
8157
8374
  async getRateLimitStatus() {
8158
8375
  const result = await this.githubSdk.getRateLimitStatus();
8159
8376
  return {
@@ -8165,11 +8382,25 @@ var GithubSCMLib = class extends SCMLib {
8165
8382
  async getRepositoryContributors() {
8166
8383
  this._validateAccessTokenAndUrl();
8167
8384
  const { owner, repo } = parseGithubOwnerAndRepo(this.url);
8168
- const [collaborators, authUser, authEmails] = await Promise.all([
8385
+ const [collaboratorsRaw, authUser, authEmails] = await Promise.all([
8169
8386
  this.githubSdk.listRepositoryCollaborators({ owner, repo }),
8170
8387
  this.githubSdk.getAuthenticatedUser(),
8171
8388
  this.githubSdk.getAuthenticatedUserEmails()
8172
8389
  ]);
8390
+ if (collaboratorsRaw.length === 0) {
8391
+ const fallback = await this.githubSdk.listRepositoryContributors({
8392
+ owner,
8393
+ repo
8394
+ });
8395
+ return fallback.map((c) => ({
8396
+ externalId: String(c.id),
8397
+ username: c.login ?? null,
8398
+ displayName: c.login ?? null,
8399
+ email: null,
8400
+ accessLevel: null
8401
+ }));
8402
+ }
8403
+ const collaborators = collaboratorsRaw;
8173
8404
  let authUserPrimaryEmail = null;
8174
8405
  if (authEmails.length > 0) {
8175
8406
  const primary = authEmails.find((e) => e.primary && e.verified);
@@ -9342,6 +9573,49 @@ async function getGitlabRecentCommits({
9342
9573
  }
9343
9574
  return allCommits;
9344
9575
  }
9576
+ async function getGitlabRecentCommitAuthors({
9577
+ repoUrl,
9578
+ accessToken,
9579
+ since
9580
+ }) {
9581
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9582
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9583
+ const byEmail = /* @__PURE__ */ new Map();
9584
+ const perPage = GITLAB_PER_PAGE;
9585
+ let page = 1;
9586
+ let hasMore = true;
9587
+ while (hasMore) {
9588
+ const response = await api2.Commits.all(projectPath, {
9589
+ since,
9590
+ perPage,
9591
+ page,
9592
+ showExpanded: true
9593
+ });
9594
+ const commits = response.data;
9595
+ if (commits.length === 0) {
9596
+ break;
9597
+ }
9598
+ for (const commit of commits) {
9599
+ const email = commit.author_email;
9600
+ if (!email) continue;
9601
+ const date = commit.committed_date ?? since;
9602
+ const key = email.toLowerCase();
9603
+ const prev = byEmail.get(key);
9604
+ if (!prev || new Date(date).getTime() > new Date(prev.date).getTime()) {
9605
+ byEmail.set(key, {
9606
+ email: key,
9607
+ name: commit.author_name ?? null,
9608
+ date,
9609
+ externalId: null,
9610
+ isBot: false
9611
+ });
9612
+ }
9613
+ }
9614
+ hasMore = response.paginationInfo.next !== null;
9615
+ page++;
9616
+ }
9617
+ return [...byEmail.values()];
9618
+ }
9345
9619
  async function getGitlabRateLimitStatus({
9346
9620
  repoUrl,
9347
9621
  accessToken
@@ -9444,15 +9718,28 @@ async function brokerRequestHandler(endpoint, options) {
9444
9718
  }
9445
9719
  throw lastError ?? new Error("gitbeaker: request failed");
9446
9720
  }
9721
+ function isGitlabPermissionDenied(e) {
9722
+ const status = e?.status;
9723
+ const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();
9724
+ return status === 403 || /forbidden|insufficient/.test(msg);
9725
+ }
9447
9726
  async function listGitlabProjectMembers({
9448
9727
  repoUrl,
9449
9728
  accessToken
9450
9729
  }) {
9451
9730
  const { owner, projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9452
9731
  const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9453
- const projectMembers = await api2.ProjectMembers.all(projectPath, {
9454
- includeInherited: true
9455
- });
9732
+ let projectMembers;
9733
+ try {
9734
+ projectMembers = await api2.ProjectMembers.all(projectPath, {
9735
+ includeInherited: true
9736
+ });
9737
+ } catch (e) {
9738
+ if (isGitlabPermissionDenied(e)) {
9739
+ return [];
9740
+ }
9741
+ throw e;
9742
+ }
9456
9743
  if (projectMembers.length > 1 || !owner) return projectMembers;
9457
9744
  try {
9458
9745
  const groupMembers = await api2.GroupMembers.all(owner, {
@@ -9482,18 +9769,14 @@ async function listGitlabRepoContributors({
9482
9769
  repoUrl,
9483
9770
  accessToken
9484
9771
  }) {
9485
- try {
9486
- const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9487
- const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9488
- const contributors = await api2.Repositories.allContributors(projectPath);
9489
- return contributors.map((c) => ({
9490
- name: c.name,
9491
- email: c.email,
9492
- commits: c.commits
9493
- }));
9494
- } catch {
9495
- return [];
9496
- }
9772
+ const { projectPath } = parseGitlabOwnerAndRepo(repoUrl);
9773
+ const api2 = getGitBeaker({ url: repoUrl, gitlabAuthToken: accessToken });
9774
+ const contributors = await api2.Repositories.allContributors(projectPath);
9775
+ return contributors.map((c) => ({
9776
+ name: c.name,
9777
+ email: c.email,
9778
+ commits: c.commits
9779
+ }));
9497
9780
  }
9498
9781
  async function getGitlabAuthenticatedUser({
9499
9782
  repoUrl,
@@ -9873,6 +10156,14 @@ var GitlabSCMLib = class extends SCMLib {
9873
10156
  });
9874
10157
  return { data: commits };
9875
10158
  }
10159
+ async getRecentCommitAuthors(since) {
10160
+ this._validateAccessTokenAndUrl();
10161
+ return getGitlabRecentCommitAuthors({
10162
+ repoUrl: this.url,
10163
+ accessToken: this.accessToken,
10164
+ since
10165
+ });
10166
+ }
9876
10167
  async getRateLimitStatus() {
9877
10168
  this._validateAccessTokenAndUrl();
9878
10169
  return getGitlabRateLimitStatus({
@@ -9882,7 +10173,7 @@ var GitlabSCMLib = class extends SCMLib {
9882
10173
  }
9883
10174
  async getRepositoryContributors() {
9884
10175
  this._validateAccessTokenAndUrl();
9885
- const [members, repoContributors, authUser] = await Promise.all([
10176
+ const [membersRes, repoContribRes, authUserRes] = await Promise.allSettled([
9886
10177
  listGitlabProjectMembers({
9887
10178
  repoUrl: this.url,
9888
10179
  accessToken: this.accessToken
@@ -9896,6 +10187,12 @@ var GitlabSCMLib = class extends SCMLib {
9896
10187
  accessToken: this.accessToken
9897
10188
  })
9898
10189
  ]);
10190
+ if (membersRes.status === "rejected" && repoContribRes.status === "rejected") {
10191
+ throw membersRes.reason;
10192
+ }
10193
+ const members = membersRes.status === "fulfilled" ? membersRes.value : [];
10194
+ const repoContributors = repoContribRes.status === "fulfilled" ? repoContribRes.value : [];
10195
+ const authUser = authUserRes.status === "fulfilled" ? authUserRes.value : null;
9899
10196
  contextLogger.info("[GitLab] Starting contributor enrichment", {
9900
10197
  memberCount: members.length,
9901
10198
  repoContributorCount: repoContributors.length
@@ -9959,6 +10256,20 @@ var GitlabSCMLib = class extends SCMLib {
9959
10256
  })
9960
10257
  )
9961
10258
  );
10259
+ if (enriched.length === 0 && repoContributors.length > 0) {
10260
+ return repoContributors.filter((rc) => rc.email || rc.name).map((rc) => ({
10261
+ // Synthetic key namespaced away from real numeric member ids so a
10262
+ // commit-author row can't collide with (or later duplicate) the same
10263
+ // person's member row once a privileged token lists them. Prefer email.
10264
+ // Lowercased to match the 90-day stamp path (commit:<lowercased email>),
10265
+ // so the same person isn't split into commit:John@X.com vs commit:john@x.com.
10266
+ externalId: `commit:${(rc.email || rc.name).toLowerCase()}`,
10267
+ username: null,
10268
+ displayName: rc.name ?? null,
10269
+ email: rc.email ?? null,
10270
+ accessLevel: null
10271
+ }));
10272
+ }
9962
10273
  const withEmail = enriched.filter((c) => c.email);
9963
10274
  contextLogger.info("[GitLab] Contributor enrichment summary", {
9964
10275
  total: enriched.length,
@@ -17732,7 +18043,7 @@ function createLogger(config2) {
17732
18043
 
17733
18044
  // src/features/claude_code/hook_logger.ts
17734
18045
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
17735
- var CLI_VERSION = true ? "1.4.36" : "unknown";
18046
+ var CLI_VERSION = true ? "1.4.41" : "unknown";
17736
18047
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
17737
18048
  var claudeCodeVersion;
17738
18049
  function buildDdTags() {
@@ -19304,7 +19615,6 @@ var FixQuestionSchema = z29.object({
19304
19615
  inputType: z29.nativeEnum(FixQuestionInputType),
19305
19616
  options: z29.array(z29.string()),
19306
19617
  index: z29.number(),
19307
- extraContext: z29.array(UnstructuredFixExtraContextSchema),
19308
19618
  // E-2015: analyzer-served question text. default('') so a query that omits it
19309
19619
  // still parses, while the output type stays a required string. The analyzer is
19310
19620
  // the sole source; consumers use the served value directly (no fallback).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobbdev",
3
- "version": "1.4.36",
3
+ "version": "1.4.41",
4
4
  "description": "Automated secure code remediation tool",
5
5
  "repository": "git+https://github.com/mobb-dev/bugsy.git",
6
6
  "main": "dist/index.mjs",