mobbdev 1.4.10 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -109,6 +109,9 @@ function getSdk(client, withWrapper = defaultWrapper) {
109
109
  autoPrAnalysis(variables, requestHeaders, signal) {
110
110
  return withWrapper((wrappedRequestHeaders) => client.request({ document: AutoPrAnalysisDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "autoPrAnalysis", "mutation", variables);
111
111
  },
112
+ getFixWithAnswers(variables, requestHeaders, signal) {
113
+ return withWrapper((wrappedRequestHeaders) => client.request({ document: GetFixWithAnswersDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "getFixWithAnswers", "query", variables);
114
+ },
112
115
  GetFixReportsByRepoUrl(variables, requestHeaders, signal) {
113
116
  return withWrapper((wrappedRequestHeaders) => client.request({ document: GetFixReportsByRepoUrlDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), "GetFixReportsByRepoUrl", "query", variables);
114
117
  },
@@ -138,7 +141,7 @@ function getSdk(client, withWrapper = defaultWrapper) {
138
141
  }
139
142
  };
140
143
  }
141
- var AiBlameInferenceType, FixQuestionInputType, Language, ManifestAction, Effort_To_Apply_Fix_Enum, Fix_Rating_Tag_Enum, Fix_Report_State_Enum, Fix_State_Enum, IssueLanguage_Enum, IssueType_Enum, Pr_Status_Enum, Project_Role_Type_Enum, Vulnerability_Report_Issue_Category_Enum, Vulnerability_Report_Issue_State_Enum, Vulnerability_Report_Issue_Tag_Enum, Vulnerability_Report_Vendor_Enum, Vulnerability_Severity_Enum, FixDetailsFragmentDoc, FixReportSummaryFieldsFragmentDoc, MeDocument, GetLastOrgAndNamedProjectDocument, GetLastOrgDocument, GetEncryptedApiTokenDocument, FixReportStateDocument, GetVulnerabilityReportPathsDocument, GetAnalysisSubscriptionDocument, GetAnalysisDocument, GetFixesDocument, GetVulByNodesMetadataDocument, GetFalsePositiveDocument, UpdateScmTokenDocument, UploadS3BucketInfoDocument, GetTracyDiffUploadUrlDocument, AnalyzeCommitForExtensionAiBlameDocument, GetAiBlameInferenceDocument, GetAiBlameAttributionPromptDocument, GetPromptSummaryDocument, UploadAiBlameInferencesInitDocument, FinalizeAiBlameInferencesUploadDocument, UploadTracyRecordsDocument, GetTracyRawDataUploadUrlDocument, DigestVulnerabilityReportDocument, SubmitVulnerabilityReportDocument, CreateCommunityUserDocument, CreateCliLoginDocument, PerformCliLoginDocument, SetQuarantineEnabledDocument, CreateProjectDocument, ValidateRepoUrlDocument, GitReferenceDocument, AutoPrAnalysisDocument, GetFixReportsByRepoUrlDocument, GetReportFixesDocument, GetLatestReportByRepoUrlDocument, UpdateDownloadedFixDataDocument, GetUserMvsAutoFixDocument, StreamBlameAiAnalysisRequestsDocument, StreamCommitBlameRequestsDocument, ScanSkillDocument, SkillVerdictsByMd5Document, defaultWrapper;
144
+ var AiBlameInferenceType, FixQuestionInputType, Language, ManifestAction, Effort_To_Apply_Fix_Enum, Fix_Rating_Tag_Enum, Fix_Report_State_Enum, Fix_State_Enum, IssueLanguage_Enum, IssueType_Enum, Pr_Status_Enum, Project_Role_Type_Enum, Vulnerability_Report_Issue_Category_Enum, Vulnerability_Report_Issue_State_Enum, Vulnerability_Report_Issue_Tag_Enum, Vulnerability_Report_Vendor_Enum, Vulnerability_Severity_Enum, FixDetailsFragmentDoc, FixReportSummaryFieldsFragmentDoc, MeDocument, GetLastOrgAndNamedProjectDocument, GetLastOrgDocument, GetEncryptedApiTokenDocument, FixReportStateDocument, GetVulnerabilityReportPathsDocument, GetAnalysisSubscriptionDocument, GetAnalysisDocument, GetFixesDocument, GetVulByNodesMetadataDocument, GetFalsePositiveDocument, UpdateScmTokenDocument, UploadS3BucketInfoDocument, GetTracyDiffUploadUrlDocument, AnalyzeCommitForExtensionAiBlameDocument, GetAiBlameInferenceDocument, GetAiBlameAttributionPromptDocument, GetPromptSummaryDocument, UploadAiBlameInferencesInitDocument, FinalizeAiBlameInferencesUploadDocument, UploadTracyRecordsDocument, GetTracyRawDataUploadUrlDocument, DigestVulnerabilityReportDocument, SubmitVulnerabilityReportDocument, CreateCommunityUserDocument, CreateCliLoginDocument, PerformCliLoginDocument, SetQuarantineEnabledDocument, CreateProjectDocument, ValidateRepoUrlDocument, GitReferenceDocument, AutoPrAnalysisDocument, GetFixWithAnswersDocument, GetFixReportsByRepoUrlDocument, GetReportFixesDocument, GetLatestReportByRepoUrlDocument, UpdateDownloadedFixDataDocument, GetUserMvsAutoFixDocument, StreamBlameAiAnalysisRequestsDocument, StreamCommitBlameRequestsDocument, ScanSkillDocument, SkillVerdictsByMd5Document, defaultWrapper;
142
145
  var init_client_generates = __esm({
143
146
  "src/features/analysis/scm/generates/client_generates.ts"() {
144
147
  "use strict";
@@ -312,6 +315,7 @@ var init_client_generates = __esm({
312
315
  IssueType_Enum2["NoReturnInFinally"] = "NO_RETURN_IN_FINALLY";
313
316
  IssueType_Enum2["NoVar"] = "NO_VAR";
314
317
  IssueType_Enum2["NullDereference"] = "NULL_DEREFERENCE";
318
+ IssueType_Enum2["OftenMisusedBooleanGetBoolean"] = "OFTEN_MISUSED_BOOLEAN_GET_BOOLEAN";
315
319
  IssueType_Enum2["OpenRedirect"] = "OPEN_REDIRECT";
316
320
  IssueType_Enum2["OverlyBroadCatch"] = "OVERLY_BROAD_CATCH";
317
321
  IssueType_Enum2["OverlyLargeRange"] = "OVERLY_LARGE_RANGE";
@@ -442,6 +446,7 @@ var init_client_generates = __esm({
442
446
  id
443
447
  confidence
444
448
  safeIssueType
449
+ safeIssueLanguage
445
450
  severityText
446
451
  gitBlameLogin
447
452
  severityValue
@@ -464,6 +469,19 @@ var init_client_generates = __esm({
464
469
  ... on FixData {
465
470
  patch
466
471
  patchOriginalEncodingBase64
472
+ questions {
473
+ key
474
+ name
475
+ defaultValue
476
+ value
477
+ inputType
478
+ options
479
+ index
480
+ extraContext {
481
+ key
482
+ value
483
+ }
484
+ }
467
485
  extraContext {
468
486
  extraContext {
469
487
  key
@@ -1179,6 +1197,37 @@ var init_client_generates = __esm({
1179
1197
  error
1180
1198
  }
1181
1199
  }
1200
+ }
1201
+ `;
1202
+ GetFixWithAnswersDocument = `
1203
+ query getFixWithAnswers($fixId: uuid!, $userInput: [QuestionAnswer!]!) {
1204
+ fixData: getFix(fixId: $fixId, userInput: $userInput, loadAnswers: false) {
1205
+ __typename
1206
+ ... on FixData {
1207
+ patch
1208
+ patchOriginalEncodingBase64
1209
+ questions {
1210
+ key
1211
+ name
1212
+ defaultValue
1213
+ value
1214
+ inputType
1215
+ options
1216
+ index
1217
+ extraContext {
1218
+ key
1219
+ value
1220
+ }
1221
+ }
1222
+ extraContext {
1223
+ extraContext {
1224
+ key
1225
+ value
1226
+ }
1227
+ fixDescription
1228
+ }
1229
+ }
1230
+ }
1182
1231
  }
1183
1232
  `;
1184
1233
  GetFixReportsByRepoUrlDocument = `
@@ -1499,7 +1548,8 @@ var init_getIssueType = __esm({
1499
1548
  ["MISSING_X_FRAME_OPTIONS" /* MissingXFrameOptions */]: "Missing X-Frame-Options Header",
1500
1549
  ["IMPROPER_VALIDATION_OF_ARRAY_INDEX" /* ImproperValidationOfArrayIndex */]: "Improper Validation of Array Index",
1501
1550
  ["INCORRECT_INTEGER_CONVERSION" /* IncorrectIntegerConversion */]: "Incorrect Integer Conversion",
1502
- ["IMPROPER_CERTIFICATE_VALIDATION" /* ImproperCertificateValidation */]: "Improper Certificate Validation"
1551
+ ["IMPROPER_CERTIFICATE_VALIDATION" /* ImproperCertificateValidation */]: "Improper Certificate Validation",
1552
+ ["OFTEN_MISUSED_BOOLEAN_GET_BOOLEAN" /* OftenMisusedBooleanGetBoolean */]: "Often Misused: Boolean.getBoolean()"
1503
1553
  };
1504
1554
  issueTypeZ = z.nativeEnum(IssueType_Enum);
1505
1555
  getIssueTypeFriendlyString = (issueType) => {
@@ -3917,6 +3967,31 @@ var init_GitService = __esm({
3917
3967
  throw new Error(errorMessage);
3918
3968
  }
3919
3969
  }
3970
+ /**
3971
+ * Reads `{ branch, commitSha }` for tracy event attribution. Detached-HEAD
3972
+ * (rebase, bisect, "open this commit") returns `branch: null` rather than
3973
+ * the literal string `"HEAD"` that `getCurrentBranch()` produces — that
3974
+ * literal would silently corrupt downstream branch dashboards.
3975
+ *
3976
+ * The two reads run in parallel so the wall-time cost is one `git`
3977
+ * round-trip rather than two. Never throws — failures resolve to nulls so
3978
+ * the daemon hot path can rely on a value, not an exception.
3979
+ */
3980
+ async getCurrentRepoState() {
3981
+ const branchPromise = this.git.raw(["symbolic-ref", "--short", "-q", "HEAD"]).then((s) => {
3982
+ const trimmed = s.trim();
3983
+ return trimmed.length > 0 ? trimmed : null;
3984
+ }).catch(() => null);
3985
+ const commitShaPromise = this.git.raw(["rev-parse", "HEAD"]).then((s) => {
3986
+ const trimmed = s.trim().toLowerCase();
3987
+ return /^[0-9a-f]{40}$/.test(trimmed) ? trimmed : null;
3988
+ }).catch(() => null);
3989
+ const [branch, commitSha] = await Promise.all([
3990
+ branchPromise,
3991
+ commitShaPromise
3992
+ ]);
3993
+ return { branch, commitSha };
3994
+ }
3920
3995
  /**
3921
3996
  * Gets both the current commit hash and current branch name
3922
3997
  */
@@ -4725,7 +4800,8 @@ var fixDetailsData = {
4725
4800
  ["MISSING_X_FRAME_OPTIONS" /* MissingXFrameOptions */]: void 0,
4726
4801
  ["IMPROPER_VALIDATION_OF_ARRAY_INDEX" /* ImproperValidationOfArrayIndex */]: void 0,
4727
4802
  ["INCORRECT_INTEGER_CONVERSION" /* IncorrectIntegerConversion */]: void 0,
4728
- ["IMPROPER_CERTIFICATE_VALIDATION" /* ImproperCertificateValidation */]: void 0
4803
+ ["IMPROPER_CERTIFICATE_VALIDATION" /* ImproperCertificateValidation */]: void 0,
4804
+ ["OFTEN_MISUSED_BOOLEAN_GET_BOOLEAN" /* OftenMisusedBooleanGetBoolean */]: void 0
4729
4805
  };
4730
4806
 
4731
4807
  // src/features/analysis/scm/shared/src/commitDescriptionMarkup.ts
@@ -9280,6 +9356,52 @@ async function executeBatchGraphQL(octokit, owner, repo, config2) {
9280
9356
  }
9281
9357
  function getGithubSdk(params = {}) {
9282
9358
  const octokit = getOctoKit(params);
9359
+ async function openPrWithFiles(params2) {
9360
+ const { owner, repo } = parseGithubOwnerAndRepo(params2.userRepoUrl);
9361
+ const { data: repository } = await octokit.rest.repos.get({ owner, repo });
9362
+ const defaultBranch = repository.default_branch;
9363
+ const baseSha = await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }).then((r) => r.data.object.sha);
9364
+ await octokit.rest.git.createRef({
9365
+ owner,
9366
+ repo,
9367
+ ref: `refs/heads/${params2.branch}`,
9368
+ sha: baseSha
9369
+ });
9370
+ const tree = await octokit.rest.git.createTree({
9371
+ owner,
9372
+ repo,
9373
+ base_tree: baseSha,
9374
+ tree: params2.files.map((f) => ({
9375
+ path: f.path,
9376
+ mode: "100644",
9377
+ type: "blob",
9378
+ content: f.content
9379
+ }))
9380
+ });
9381
+ const commit = await octokit.rest.git.createCommit({
9382
+ owner,
9383
+ repo,
9384
+ message: params2.commitMessage ?? params2.title,
9385
+ tree: tree.data.sha,
9386
+ parents: [baseSha]
9387
+ });
9388
+ await octokit.rest.git.updateRef({
9389
+ owner,
9390
+ repo,
9391
+ ref: `heads/${params2.branch}`,
9392
+ sha: commit.data.sha
9393
+ });
9394
+ const pr = await octokit.rest.pulls.create({
9395
+ owner,
9396
+ repo,
9397
+ title: params2.title,
9398
+ head: params2.branch,
9399
+ ...params2.headRepo ? { head_repo: params2.headRepo } : {},
9400
+ body: safeBody(params2.body, MAX_GH_PR_BODY_LENGTH),
9401
+ base: defaultBranch
9402
+ });
9403
+ return { pull_request_url: pr.data.html_url };
9404
+ }
9283
9405
  return {
9284
9406
  async postPrComment(params2) {
9285
9407
  return octokit.request(POST_COMMENT_PATH, params2);
@@ -9576,86 +9698,26 @@ function getGithubSdk(params = {}) {
9576
9698
  async createPr(params2) {
9577
9699
  const { sourceRepoUrl, filesPaths, userRepoUrl, title, body } = params2;
9578
9700
  const { owner: sourceOwner, repo: sourceRepo } = parseGithubOwnerAndRepo(sourceRepoUrl);
9579
- const { owner, repo } = parseGithubOwnerAndRepo(userRepoUrl);
9580
- const [sourceFilePath, secondFilePath] = filesPaths;
9581
- const sourceFileContentResponse = await octokit.rest.repos.getContent({
9582
- owner: sourceOwner,
9583
- repo: sourceRepo,
9584
- path: `/${sourceFilePath}`
9585
- });
9586
- const { data: repository } = await octokit.rest.repos.get({ owner, repo });
9587
- const defaultBranch = repository.default_branch;
9588
- const newBranchName = `mobb/workflow-${Date.now()}`;
9589
- await octokit.rest.git.createRef({
9590
- owner,
9591
- repo,
9592
- ref: `refs/heads/${newBranchName}`,
9593
- sha: await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }).then((response) => response.data.object.sha)
9594
- });
9595
- const decodedContent = Buffer.from(
9596
- // Check if file content exists and handle different response types
9597
- typeof sourceFileContentResponse.data === "object" && !Array.isArray(sourceFileContentResponse.data) && "content" in sourceFileContentResponse.data && typeof sourceFileContentResponse.data.content === "string" ? sourceFileContentResponse.data.content : "",
9598
- "base64"
9599
- ).toString("utf-8");
9600
- const tree = [
9601
- {
9602
- path: sourceFilePath,
9603
- mode: "100644",
9604
- type: "blob",
9605
- content: decodedContent
9606
- }
9607
- ];
9608
- if (secondFilePath) {
9609
- const secondFileContentResponse = await octokit.rest.repos.getContent({
9610
- owner: sourceOwner,
9611
- repo: sourceRepo,
9612
- path: `/${secondFilePath}`
9613
- });
9614
- const secondDecodedContent = Buffer.from(
9615
- // Check if file content exists and handle different response types
9616
- typeof secondFileContentResponse.data === "object" && !Array.isArray(secondFileContentResponse.data) && "content" in secondFileContentResponse.data && typeof secondFileContentResponse.data.content === "string" ? secondFileContentResponse.data.content : "",
9617
- "base64"
9618
- ).toString("utf-8");
9619
- tree.push({
9620
- path: secondFilePath,
9621
- mode: "100644",
9622
- type: "blob",
9623
- content: secondDecodedContent
9624
- });
9625
- }
9626
- const createTreeResponse = await octokit.rest.git.createTree({
9627
- owner,
9628
- repo,
9629
- base_tree: await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }).then((response) => response.data.object.sha),
9630
- tree
9631
- });
9632
- const createCommitResponse = await octokit.rest.git.createCommit({
9633
- owner,
9634
- repo,
9635
- message: "Add new yaml file",
9636
- tree: createTreeResponse.data.sha,
9637
- parents: [
9638
- await octokit.rest.git.getRef({ owner, repo, ref: `heads/${defaultBranch}` }).then((response) => response.data.object.sha)
9639
- ]
9640
- });
9641
- await octokit.rest.git.updateRef({
9642
- owner,
9643
- repo,
9644
- ref: `heads/${newBranchName}`,
9645
- sha: createCommitResponse.data.sha
9646
- });
9647
- const createPRResponse = await octokit.rest.pulls.create({
9648
- owner,
9649
- repo,
9701
+ const files = await Promise.all(
9702
+ filesPaths.map(async (filePath) => {
9703
+ const response = await octokit.rest.repos.getContent({
9704
+ owner: sourceOwner,
9705
+ repo: sourceRepo,
9706
+ path: `/${filePath}`
9707
+ });
9708
+ const content = typeof response.data === "object" && !Array.isArray(response.data) && "content" in response.data && typeof response.data.content === "string" ? Buffer.from(response.data.content, "base64").toString("utf-8") : "";
9709
+ return { path: filePath, content };
9710
+ })
9711
+ );
9712
+ return openPrWithFiles({
9713
+ userRepoUrl,
9714
+ files,
9715
+ branch: `mobb/workflow-${Date.now()}`,
9650
9716
  title,
9651
- head: newBranchName,
9652
- head_repo: sourceRepo,
9653
- body: safeBody(body, MAX_GH_PR_BODY_LENGTH),
9654
- base: defaultBranch
9717
+ body,
9718
+ commitMessage: "Add new yaml file",
9719
+ headRepo: sourceRepo
9655
9720
  });
9656
- return {
9657
- pull_request_url: createPRResponse.data.html_url
9658
- };
9659
9721
  },
9660
9722
  async getGithubBranchList(repoUrl) {
9661
9723
  const { owner, repo } = parseGithubOwnerAndRepo(repoUrl);
@@ -9666,6 +9728,35 @@ function getGithubSdk(params = {}) {
9666
9728
  page: 1
9667
9729
  });
9668
9730
  },
9731
+ // T-500 — open a PR adding a single inline file. Used by the
9732
+ // openSecuritySkillPR resolver to deliver `.claude/skills/<slug>/SKILL.md`.
9733
+ async createPrWithContent(params2) {
9734
+ return openPrWithFiles({
9735
+ userRepoUrl: params2.userRepoUrl,
9736
+ files: [{ path: params2.filePath, content: params2.content }],
9737
+ branch: params2.branch,
9738
+ title: params2.title,
9739
+ body: params2.body
9740
+ });
9741
+ },
9742
+ // T-500 — best-effort branch cleanup for openSecuritySkillPR retry.
9743
+ // Swallows 422/404 so callers can call it unconditionally before
9744
+ // a fresh PR-creation attempt.
9745
+ async deleteBranchIfExists(params2) {
9746
+ const { owner, repo } = parseGithubOwnerAndRepo(params2.userRepoUrl);
9747
+ try {
9748
+ await octokit.rest.git.deleteRef({
9749
+ owner,
9750
+ repo,
9751
+ ref: `heads/${params2.branch}`
9752
+ });
9753
+ } catch (err) {
9754
+ if (err instanceof RequestError && (err.status === 422 || err.status === 404)) {
9755
+ return;
9756
+ }
9757
+ throw err;
9758
+ }
9759
+ },
9669
9760
  async createPullRequest(options) {
9670
9761
  const { owner, repo } = parseGithubOwnerAndRepo(options.repoUrl);
9671
9762
  return octokit.rest.pulls.create({
@@ -10036,6 +10127,20 @@ var GithubSCMLib = class extends SCMLib {
10036
10127
  });
10037
10128
  return { pull_request_url };
10038
10129
  }
10130
+ // T-500 — sibling of `createPullRequestWithNewFile` that takes inline
10131
+ // content rather than reading from a source repo. Used by
10132
+ // openSecuritySkillPR to deliver `.claude/skills/<slug>/SKILL.md`.
10133
+ async createPullRequestWithInlineFile(params) {
10134
+ const { pull_request_url } = await this.githubSdk.createPrWithContent(params);
10135
+ return { pull_request_url };
10136
+ }
10137
+ // T-500 — used by the openSecuritySkillPR resolver to clean up a
10138
+ // branch left behind by a prior failed PR-creation attempt before
10139
+ // retrying. Swallows missing-branch responses; only real network
10140
+ // errors propagate.
10141
+ async deleteBranchIfExists(params) {
10142
+ return this.githubSdk.deleteBranchIfExists(params);
10143
+ }
10039
10144
  async validateParams() {
10040
10145
  return await githubValidateParams(this.url, this.accessToken);
10041
10146
  }
@@ -14049,8 +14154,16 @@ var ADO_PAT_PATTERN = {
14049
14154
  severity: "high",
14050
14155
  validator: (match) => match.length >= 52 && match.length <= 100
14051
14156
  };
14157
+ var DATADOG_APP_KEY_PATTERN = {
14158
+ type: "DATADOG_APP_KEY",
14159
+ regex: /\bddapp_[a-zA-Z0-9]{30,}\b/g,
14160
+ priority: 95,
14161
+ placeholder: "[DATADOG_APP_KEY_{n}]",
14162
+ description: "Datadog Application Key",
14163
+ severity: "high"
14164
+ };
14052
14165
  var openRedaction = new OpenRedaction({
14053
- customPatterns: [ADO_PAT_PATTERN],
14166
+ customPatterns: [ADO_PAT_PATTERN, DATADOG_APP_KEY_PATTERN],
14054
14167
  patterns: [
14055
14168
  // Core Personal Data
14056
14169
  // Removed EMAIL - causes false positives in code/test snippets (e.g. --author="Eve Author <eve@example.com>")
@@ -14257,19 +14370,33 @@ var PromptItemZ = z27.object({
14257
14370
  }).optional()
14258
14371
  });
14259
14372
  var PromptItemArrayZ = z27.array(PromptItemZ);
14260
- async function getRepositoryUrl(workingDir) {
14373
+ var NULL_REPO_STATE = {
14374
+ repositoryUrl: null,
14375
+ branch: null,
14376
+ commitSha: null
14377
+ };
14378
+ async function readRepoState(workingDir) {
14379
+ const dir = workingDir ?? process.cwd();
14380
+ let gitService;
14261
14381
  try {
14262
- const gitService = new GitService(workingDir ?? process.cwd());
14263
- const isRepo = await gitService.isGitRepository();
14264
- if (!isRepo) {
14265
- return null;
14266
- }
14267
- const remoteUrl = await gitService.getRemoteUrl();
14268
- const parsed = parseScmURL(remoteUrl);
14269
- return parsed?.scmType && parsed.scmType !== "Unknown" ? remoteUrl : null;
14382
+ gitService = new GitService(dir);
14270
14383
  } catch {
14271
- return null;
14272
- }
14384
+ return NULL_REPO_STATE;
14385
+ }
14386
+ const repoStatePromise = gitService.getCurrentRepoState().catch(() => ({ branch: null, commitSha: null }));
14387
+ const repositoryUrlPromise = gitService.getRemoteUrl().then((url) => {
14388
+ if (!url) return null;
14389
+ const parsed = parseScmURL(url);
14390
+ return parsed?.scmType && parsed.scmType !== "Unknown" ? url : null;
14391
+ }).catch(() => null);
14392
+ const [{ branch, commitSha }, repositoryUrl] = await Promise.all([
14393
+ repoStatePromise,
14394
+ repositoryUrlPromise
14395
+ ]);
14396
+ return { repositoryUrl, branch, commitSha };
14397
+ }
14398
+ async function getRepositoryUrl(workingDir) {
14399
+ return (await readRepoState(workingDir)).repositoryUrl;
14273
14400
  }
14274
14401
  function getSystemInfo() {
14275
14402
  let userName;
@@ -14602,7 +14729,7 @@ async function prepareAndSendTracyRecords(client, rawRecords, workingDir, option
14602
14729
  const { computerName, userName } = getSystemInfo();
14603
14730
  const defaultClientVersion = packageJson.version;
14604
14731
  const shouldSanitize = options?.sanitize ?? true;
14605
- const defaultRepoUrl = rawRecords[0]?.repositoryUrl ? void 0 : await getRepositoryUrl(workingDir) ?? void 0;
14732
+ const defaults = workingDir != null ? await readRepoState(workingDir) : { repositoryUrl: null, branch: null, commitSha: null };
14606
14733
  debug10(
14607
14734
  "[step:sanitize] %s %d records",
14608
14735
  shouldSanitize ? "Sanitizing" : "Serializing",
@@ -14622,7 +14749,9 @@ async function prepareAndSendTracyRecords(client, rawRecords, workingDir, option
14622
14749
  const { rawData: _rawData, ...rest } = record;
14623
14750
  results.push({
14624
14751
  ...rest,
14625
- repositoryUrl: record.repositoryUrl ?? defaultRepoUrl,
14752
+ repositoryUrl: record.repositoryUrl ?? defaults.repositoryUrl ?? void 0,
14753
+ branch: record.branch ?? defaults.branch ?? void 0,
14754
+ commitSha: record.commitSha ?? defaults.commitSha ?? void 0,
14626
14755
  computerName,
14627
14756
  userName,
14628
14757
  clientVersion: record.clientVersion ?? defaultClientVersion
@@ -18760,6 +18889,8 @@ async function uploadContextRecords(opts) {
18760
18889
  now,
18761
18890
  platform: platform2,
18762
18891
  repositoryUrl,
18892
+ branch,
18893
+ commitSha,
18763
18894
  clientVersion,
18764
18895
  onFileError,
18765
18896
  onSkillError
@@ -18770,6 +18901,8 @@ async function uploadContextRecords(opts) {
18770
18901
  const limit = pLimit7(UPLOAD_CONCURRENCY);
18771
18902
  const extraFields = {
18772
18903
  ...repositoryUrl !== void 0 && { repositoryUrl },
18904
+ ...branch !== void 0 && { branch },
18905
+ ...commitSha !== void 0 && { commitSha },
18773
18906
  ...clientVersion !== void 0 && { clientVersion }
18774
18907
  };
18775
18908
  const tasks = [
@@ -18855,6 +18988,8 @@ async function runContextFileUploadPipeline(opts) {
18855
18988
  uploadFieldsJSON,
18856
18989
  keyPrefix,
18857
18990
  repositoryUrl,
18991
+ branch,
18992
+ commitSha,
18858
18993
  clientVersion,
18859
18994
  submitRecords,
18860
18995
  onFileError,
@@ -18877,6 +19012,8 @@ async function runContextFileUploadPipeline(opts) {
18877
19012
  now,
18878
19013
  platform: platform2,
18879
19014
  repositoryUrl,
19015
+ branch,
19016
+ commitSha,
18880
19017
  clientVersion,
18881
19018
  onFileError,
18882
19019
  onSkillError
@@ -19232,7 +19369,7 @@ function createLogger(config2) {
19232
19369
 
19233
19370
  // src/features/claude_code/hook_logger.ts
19234
19371
  var DD_RUM_TOKEN = true ? "pubf59c0182545bfb4c299175119f1abf9b" : "";
19235
- var CLI_VERSION = true ? "1.4.10" : "unknown";
19372
+ var CLI_VERSION = true ? "1.4.12" : "unknown";
19236
19373
  var NAMESPACE = "mobbdev-claude-code-hook-logs";
19237
19374
  var claudeCodeVersion;
19238
19375
  function buildDdTags() {
@@ -19699,6 +19836,7 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
19699
19836
  }
19700
19837
  const cursorForModel = sessionStore.get(cursorKey);
19701
19838
  let lastSeenModel = cursorForModel?.lastModel ?? null;
19839
+ const sampledRepoState = await readRepoState(input.cwd);
19702
19840
  const records = entries.map((entry) => {
19703
19841
  const { _recordId, ...rawEntry } = entry;
19704
19842
  const message = rawEntry["message"];
@@ -19720,7 +19858,10 @@ async function processTranscript(input, sessionStore, log2, maxEntries = DAEMON_
19720
19858
  recordId: _recordId,
19721
19859
  recordTimestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
19722
19860
  blameType: "CHAT" /* Chat */,
19723
- rawData: rawEntry
19861
+ rawData: rawEntry,
19862
+ repositoryUrl: sampledRepoState.repositoryUrl ?? void 0,
19863
+ branch: sampledRepoState.branch,
19864
+ commitSha: sampledRepoState.commitSha
19724
19865
  };
19725
19866
  });
19726
19867
  let totalRawDataBytes = 0;
@@ -20714,10 +20855,22 @@ var FixExtraContextResponseSchema = z33.object({
20714
20855
  extraContext: z33.array(UnstructuredFixExtraContextSchema),
20715
20856
  fixDescription: z33.string()
20716
20857
  });
20858
+ var FixQuestionSchema = z33.object({
20859
+ __typename: z33.literal("FixQuestion").optional(),
20860
+ key: z33.string(),
20861
+ name: z33.string(),
20862
+ defaultValue: z33.string(),
20863
+ value: z33.string().nullable().optional(),
20864
+ inputType: z33.nativeEnum(FixQuestionInputType),
20865
+ options: z33.array(z33.string()),
20866
+ index: z33.number(),
20867
+ extraContext: z33.array(UnstructuredFixExtraContextSchema)
20868
+ });
20717
20869
  var FixDataSchema = z33.object({
20718
20870
  __typename: z33.literal("FixData"),
20719
20871
  patch: z33.string(),
20720
20872
  patchOriginalEncodingBase64: z33.string(),
20873
+ questions: z33.array(FixQuestionSchema),
20721
20874
  extraContext: FixExtraContextResponseSchema
20722
20875
  });
20723
20876
  var GetFixNoFixErrorSchema = z33.object({
@@ -20730,6 +20883,7 @@ var McpFixSchema = z33.object({
20730
20883
  // GraphQL uses `any` type for UUID
20731
20884
  confidence: z33.number(),
20732
20885
  safeIssueType: z33.string().nullable(),
20886
+ safeIssueLanguage: z33.string().nullable().optional(),
20733
20887
  severityText: z33.string().nullable(),
20734
20888
  gitBlameLogin: z33.string().nullable().optional(),
20735
20889
  // Optional in GraphQL
@@ -20809,6 +20963,63 @@ var GetLatestReportByRepoUrlResponseSchema = z33.object({
20809
20963
  expiredReport: z33.array(ExpiredReportSchema)
20810
20964
  });
20811
20965
 
20966
+ // src/mcp/services/InteractiveFixFilter.ts
20967
+ var isInteractiveFix = (fix) => {
20968
+ if (fix.patchAndQuestions.__typename !== "FixData") {
20969
+ return false;
20970
+ }
20971
+ return fix.patchAndQuestions.questions.length > 0;
20972
+ };
20973
+ var ruleIdFor = (fix) => fix.safeIssueType ?? "UNKNOWN";
20974
+ var countByRule = (ruleIds) => {
20975
+ const counts = {};
20976
+ for (const ruleId of ruleIds) {
20977
+ counts[ruleId] = (counts[ruleId] ?? 0) + 1;
20978
+ }
20979
+ return counts;
20980
+ };
20981
+ var MOBB_MCP_DISABLE_INTERACTIVE_FILTER_DEFAULT = false;
20982
+ var isInteractiveRoutingDisabled = () => {
20983
+ const raw = process.env["MOBB_MCP_DISABLE_INTERACTIVE_FILTER"];
20984
+ if (!raw) return MOBB_MCP_DISABLE_INTERACTIVE_FILTER_DEFAULT;
20985
+ const normalized = raw.toLowerCase();
20986
+ return normalized === "1" || normalized === "true";
20987
+ };
20988
+ var partitionInteractiveFixes = (fixes) => {
20989
+ const disabled = isInteractiveRoutingDisabled();
20990
+ const applicableFixes = [];
20991
+ const interactiveFixes = [];
20992
+ const droppedInteractive = [];
20993
+ for (const fix of fixes) {
20994
+ if (isInteractiveFix(fix)) {
20995
+ if (disabled) {
20996
+ droppedInteractive.push(fix);
20997
+ } else {
20998
+ interactiveFixes.push(fix);
20999
+ }
21000
+ } else {
21001
+ applicableFixes.push(fix);
21002
+ }
21003
+ }
21004
+ if (disabled && droppedInteractive.length > 0) {
21005
+ logInfo(
21006
+ "[InteractiveFixFilter] Dropping interactive fixes (MOBB_MCP_DISABLE_INTERACTIVE_FILTER=true)",
21007
+ {
21008
+ totalFixes: fixes.length,
21009
+ droppedCount: droppedInteractive.length,
21010
+ droppedByRule: countByRule(droppedInteractive.map(ruleIdFor))
21011
+ }
21012
+ );
21013
+ } else if (interactiveFixes.length > 0) {
21014
+ logInfo("[InteractiveFixFilter] Routing interactive fixes to LLM", {
21015
+ totalFixes: fixes.length,
21016
+ interactiveCount: interactiveFixes.length,
21017
+ interactiveByRule: countByRule(interactiveFixes.map(ruleIdFor))
21018
+ });
21019
+ }
21020
+ return { applicableFixes, interactiveFixes };
21021
+ };
21022
+
20812
21023
  // src/mcp/services/McpGQLClient.ts
20813
21024
  var McpGQLClient = class extends GQLClient {
20814
21025
  constructor(args) {
@@ -21091,7 +21302,7 @@ var McpGQLClient = class extends GQLClient {
21091
21302
  reportData,
21092
21303
  limit
21093
21304
  }) {
21094
- if (!reportData) return [];
21305
+ if (!reportData) return { applicableFixes: [], interactiveFixes: [] };
21095
21306
  const reportMetadata = {
21096
21307
  id: reportData.id,
21097
21308
  organizationId: reportData.vulnerabilityReport?.project?.organizationId,
@@ -21127,7 +21338,12 @@ var McpGQLClient = class extends GQLClient {
21127
21338
  fixMap.set(fix.id, fixWithUrl);
21128
21339
  }
21129
21340
  }
21130
- return Array.from(fixMap.values()).slice(0, limit);
21341
+ const merged = Array.from(fixMap.values());
21342
+ const { applicableFixes, interactiveFixes } = partitionInteractiveFixes(merged);
21343
+ return {
21344
+ applicableFixes: applicableFixes.slice(0, limit),
21345
+ interactiveFixes: interactiveFixes.slice(0, limit)
21346
+ };
21131
21347
  }
21132
21348
  async updateFixesDownloadStatus(fixIds) {
21133
21349
  if (fixIds.length > 0) {
@@ -21231,14 +21447,15 @@ var McpGQLClient = class extends GQLClient {
21231
21447
  reportCount: resp.fixReport?.length || 0
21232
21448
  });
21233
21449
  const latestReport = resp.fixReport?.[0] && FixReportSummarySchema.parse(resp.fixReport?.[0]);
21234
- const fixes = this.mergeUserAndSystemFixes({
21450
+ const { applicableFixes, interactiveFixes } = this.mergeUserAndSystemFixes({
21235
21451
  reportData: latestReport,
21236
21452
  limit
21237
21453
  });
21238
21454
  return {
21239
21455
  fixReport: latestReport ? {
21240
21456
  ...latestReport,
21241
- fixes
21457
+ fixes: applicableFixes,
21458
+ interactiveFixes
21242
21459
  } : null,
21243
21460
  expiredReport: resp.expiredReport?.[0] || null
21244
21461
  };
@@ -21308,13 +21525,17 @@ var McpGQLClient = class extends GQLClient {
21308
21525
  return null;
21309
21526
  }
21310
21527
  const latestReport = FixReportSummarySchema.parse(res.fixReport?.[0]);
21311
- const fixes = this.mergeUserAndSystemFixes({
21528
+ const { applicableFixes, interactiveFixes } = this.mergeUserAndSystemFixes({
21312
21529
  reportData: latestReport,
21313
21530
  limit
21314
21531
  });
21315
- logDebug("[GraphQL] GetReportFixes response parsed", { fixes });
21532
+ logDebug("[GraphQL] GetReportFixes response parsed", {
21533
+ fixes: applicableFixes,
21534
+ interactiveCount: interactiveFixes.length
21535
+ });
21316
21536
  return {
21317
- fixes,
21537
+ fixes: applicableFixes,
21538
+ interactiveFixes,
21318
21539
  totalCount: res.fixReport?.[0]?.filteredFixesCount?.aggregate?.count || 0,
21319
21540
  expiredReport: res.expiredReport?.[0] || null,
21320
21541
  fixReport: res.fixReport?.[0] ? {
@@ -21332,6 +21553,36 @@ var McpGQLClient = class extends GQLClient {
21332
21553
  throw e;
21333
21554
  }
21334
21555
  }
21556
+ /** Root getFix recomputes the patch; fix_by_pk.patchAndQuestions(userInput) does not (stale questions looked like cascading). */
21557
+ async getFixWithAnswers({
21558
+ fixId,
21559
+ answers
21560
+ }) {
21561
+ try {
21562
+ logDebug("[GraphQL] Calling getFixWithAnswers query", {
21563
+ fixId,
21564
+ answerCount: answers.length,
21565
+ userInput: answers
21566
+ });
21567
+ const resp = await this._clientSdk.getFixWithAnswers({
21568
+ fixId,
21569
+ userInput: answers
21570
+ });
21571
+ logDebug("[GraphQL] getFixWithAnswers successful", {
21572
+ fixId,
21573
+ responseTypename: resp.fixData?.__typename,
21574
+ remainingQuestionKeys: resp.fixData?.__typename === "FixData" ? resp.fixData.questions.map((q) => q.key) : void 0
21575
+ });
21576
+ return { fixData: resp.fixData ?? null };
21577
+ } catch (e) {
21578
+ logError("[GraphQL] getFixWithAnswers failed", {
21579
+ error: e,
21580
+ fixId,
21581
+ ...this.getErrorContext()
21582
+ });
21583
+ throw e;
21584
+ }
21585
+ }
21335
21586
  };
21336
21587
  async function createAuthenticatedMcpGQLClient({
21337
21588
  isBackgroundCall = false,
@@ -24374,6 +24625,7 @@ init_client_generates();
24374
24625
  init_configs();
24375
24626
 
24376
24627
  // src/mcp/core/prompts.ts
24628
+ init_client_generates();
24377
24629
  init_configs();
24378
24630
  function friendlyType(s) {
24379
24631
  const withoutUnderscores = s.replace(/_/g, " ");
@@ -24382,6 +24634,133 @@ function friendlyType(s) {
24382
24634
  }
24383
24635
  var noFixesReturnedForParameters = `No fixes returned for the given offset and limit parameters.
24384
24636
  `;
24637
+ var resolveQuestionText = ({
24638
+ fix,
24639
+ question
24640
+ }) => {
24641
+ const language = fix.safeIssueLanguage ?? void 0;
24642
+ const issueType = fix.safeIssueType ?? void 0;
24643
+ if (!language || !issueType) {
24644
+ return { content: question.name, description: "" };
24645
+ }
24646
+ const item = storedQuestionData_default[language]?.[issueType]?.[question.name];
24647
+ if (!item) {
24648
+ return { content: question.name, description: "" };
24649
+ }
24650
+ const args = question.extraContext.reduce(
24651
+ (acc, ctx) => {
24652
+ acc[ctx.key] = ctx.value;
24653
+ return acc;
24654
+ },
24655
+ {}
24656
+ );
24657
+ try {
24658
+ return {
24659
+ content: item.content(args) || question.name,
24660
+ description: item.description(args) || ""
24661
+ };
24662
+ } catch {
24663
+ return { content: question.name, description: "" };
24664
+ }
24665
+ };
24666
+ var formatQuestionInputContract = (question) => {
24667
+ switch (question.inputType) {
24668
+ case "SELECT" /* Select */:
24669
+ return `Pick exactly ONE of: ${question.options.map((o) => `\`${o}\``).join(", ")}`;
24670
+ case "NUMBER" /* Number */:
24671
+ return 'Provide a numeric string (e.g. "60").';
24672
+ case "TEXT" /* Text */:
24673
+ return "Provide a free-form string (or an empty string to accept the default).";
24674
+ }
24675
+ };
24676
+ var renderInteractiveFix = (fix, index) => {
24677
+ if (fix.patchAndQuestions.__typename !== "FixData") return "";
24678
+ const { questions, extraContext } = fix.patchAndQuestions;
24679
+ const vulnerabilityType = friendlyType(fix.safeIssueType ?? "Unknown");
24680
+ const questionsBlock = questions.slice().sort((a, b) => a.index - b.index).map((q, qIdx) => {
24681
+ const { content, description } = resolveQuestionText({ fix, question: q });
24682
+ const desc = description ? `
24683
+ *Why it matters:* ${description}` : "";
24684
+ const defaultLine = q.defaultValue ? `
24685
+ *Default if you don't decide:* \`${q.defaultValue}\`` : "";
24686
+ return `${qIdx + 1}. **\`${q.key}\`** \u2014 ${content}
24687
+ *Input:* ${formatQuestionInputContract(q)}${defaultLine}${desc}`;
24688
+ }).join("\n\n");
24689
+ return `### Interactive fix ${index + 1}: ${vulnerabilityType}
24690
+
24691
+ **Fix id:** \`${fix.id}\`
24692
+ **Description:** ${extraContext?.fixDescription || "Security vulnerability fix that requires answers before it can be tailored."}
24693
+
24694
+ **Questions you must answer before this fix can be applied:**
24695
+
24696
+ ${questionsBlock}
24697
+ `;
24698
+ };
24699
+ var interactiveFixesPrompt = ({
24700
+ interactiveFixes,
24701
+ repositoryPath
24702
+ }) => {
24703
+ if (interactiveFixes.length === 0) return "";
24704
+ const fixesBlock = interactiveFixes.map((fix, idx) => renderInteractiveFix(fix, idx)).join("\n---\n");
24705
+ return `
24706
+ ## Interactive fixes \u2014 decide from code, then apply or abstain
24707
+
24708
+ The Mobb backend produced ${interactiveFixes.length} fix${interactiveFixes.length === 1 ? "" : "es"} that need context before the patch can be tailored. **Infer answers only from the real code**: touched files, imports, framework, and how untrusted data flows (SSR/XSS/PT/SSRF, etc.). **Do NOT ask the end user** \u2014 use tooling/read-only repo inspection only.
24709
+
24710
+ ### CRITICAL INSTRUCTIONS \u2014 READ CAREFULLY
24711
+
24712
+ 1. **Defaults are hints from Mobb, not autopilot.** The line *Default if you don't decide* shows what the backend would fall back to. Prefer answers **consistent with the codebase**. Do **not** pick an answer **only** because it equals that default \u2014 a mismatched default can produce the wrong tailored patch.
24713
+ 2. **Time budget \u2014 ~5 seconds per interactive fix, as one shared pool.** With ${interactiveFixes.length} interactive fix${interactiveFixes.length === 1 ? "" : "es"} above, spend **at most ~${interactiveFixes.length * 5} seconds total** inspecting the codebase before re-calling \`${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES}\` with \`interactiveAnswers\`. The budget is a pool \u2014 one fix may take 8s if it's genuinely ambiguous while another takes 2s if the call site is obvious, as long as the total stays near the bound. If a fix is still uncertain when its share runs out, **omit it from \`interactiveAnswers\`** (rule 4) rather than over-deliberating.
24714
+ 3. **Confidence required.** Include in \`interactiveAnswers\` **only** fixes where your answers are justified by what you see in code (exact SELECT strings where applicable).
24715
+ 4. **Abstain rather than guess.** If you **cannot** justify any responsible answer after inspecting the code (ambiguous flows, missing callers, isomorphic bundles, unclear SSRF allowlists, etc.), **omit that fix id entirely** from \`interactiveAnswers\`. Tell the user in prose what was skipped and why so they can fix manually or follow up later \u2014 **do not fabricate answers**.
24716
+ 5. **Skipping everything.** If you skip **all** interactive fixes, still call **\`${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES}\`** once with \`"interactiveAnswers": []\` (empty array) together with \`path\`. That acknowledges abstention **without** starting a new scan. Omitting \`interactiveAnswers\` entirely falls back to scan mode instead \u2014 avoid that when you intend purely to abstain.
24717
+ 6. **Use exact option strings for SELECT questions.** Copy them character-for-character from the option list. Do **not** append explanation, rationale, or commentary to the value \u2014 that turns the answer into a non-matching string and the backend silently falls back to its default.
24718
+ 7. **Use the \`key\` verbatim, not the human label.** Each question shows a backtick-quoted key (e.g. \`is_server_side_code\`, \`tainted_term_type\`). That exact string goes into \`answers[].key\`. The display name (e.g. \`isServerSideCode\`, \`taintedTermType\`) is for humans only \u2014 sending it as a key means the backend won't recognise the answer and falls back to the default.
24719
+ 8. **After the tool call**, summarize: fixes applied with reasoning; fixes skipped (confidence/abstention/time-budget); tool failures.
24720
+
24721
+ ### Decision heuristics for common questions
24722
+
24723
+ (Keys shown in snake_case \u2014 copy them verbatim from each fix's question block.)
24724
+
24725
+ - **\`is_server_side_code\` (XSS)** \u2014 \`yes\` when server-render or Node HTTP handlers dominate; \`no\` when clearly browser-only. If bundle/context is genuinely ambiguous after inspection, **omit** this fix from \`interactiveAnswers\`.
24726
+ - **\`tainted_term_type\` (Path Traversal)** \u2014 match how user input is joined/consumed (single filename vs path segments vs absolute). If usage cannot be determined, **omit**.
24727
+ - **\`iframe_restrictions\`** \u2014 strict sandbox (\`""\`) unless embedded content clearly needs listed capabilities.
24728
+
24729
+ ### How to call \`${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES}\`
24730
+
24731
+ Apply fixes you are confident about (subset allowed):
24732
+
24733
+ \`\`\`json
24734
+ {
24735
+ "path": "${repositoryPath}",
24736
+ "interactiveAnswers": [
24737
+ {
24738
+ "fixId": "<fix id from below>",
24739
+ "answers": [
24740
+ { "key": "<question key, exactly as shown>", "value": "<your decided value>" }
24741
+ ]
24742
+ }
24743
+ ]
24744
+ }
24745
+ \`\`\`
24746
+
24747
+ Explicit abstention \u2014 skip **all** interactive fixes without rescanning:
24748
+
24749
+ \`\`\`json
24750
+ {
24751
+ "path": "${repositoryPath}",
24752
+ "interactiveAnswers": []
24753
+ }
24754
+ \`\`\`
24755
+
24756
+ ${fixesBlock}
24757
+ `;
24758
+ };
24759
+ var interactiveAnswersAbstainAllToolResponse = `## Interactive fixes \u2014 none applied
24760
+
24761
+ \`interactiveAnswers\` was an empty array: **no** tailored patches were requested and **no** scan was run.
24762
+
24763
+ State clearly for the user which interactive fixes you **skipped**, why the code did not support a confident answer, and that they can apply those manually or re-run after clarifying the codebase.`;
24385
24764
  var noFixesReturnedForParametersWithGuidance = ({
24386
24765
  offset,
24387
24766
  limit,
@@ -24440,9 +24819,13 @@ var applyFixesPrompt = ({
24440
24819
  currentTool,
24441
24820
  offset,
24442
24821
  limit,
24443
- gqlClient
24822
+ gqlClient,
24823
+ hasInteractiveFixes = false
24444
24824
  }) => {
24445
24825
  if (fixes.length === 0) {
24826
+ if (hasInteractiveFixes) {
24827
+ return "";
24828
+ }
24446
24829
  if (totalCount > 0) {
24447
24830
  return noFixesReturnedForParametersWithGuidance({
24448
24831
  offset,
@@ -24628,11 +25011,17 @@ var fixesFoundPrompt = ({
24628
25011
  fixReport,
24629
25012
  offset,
24630
25013
  limit,
24631
- gqlClient
25014
+ gqlClient,
25015
+ interactiveFixes = [],
25016
+ repositoryPath
24632
25017
  }) => {
24633
25018
  const totalFixes = fixReport.filteredFixesCount.aggregate?.count || 0;
25019
+ const interactiveBlock = interactiveFixesPrompt({
25020
+ interactiveFixes,
25021
+ repositoryPath
25022
+ });
24634
25023
  if (totalFixes === 0) {
24635
- return noFixesAvailablePrompt;
25024
+ return noFixesAvailablePrompt + interactiveBlock;
24636
25025
  }
24637
25026
  const criticalFixes = fixReport.CRITICAL?.aggregate?.count || 0;
24638
25027
  const highFixes = fixReport.HIGH?.aggregate?.count || 0;
@@ -24674,8 +25063,9 @@ ${applyFixesPrompt({
24674
25063
  currentTool: MCP_TOOL_FETCH_AVAILABLE_FIXES,
24675
25064
  offset,
24676
25065
  limit,
24677
- gqlClient
24678
- })}`;
25066
+ gqlClient,
25067
+ hasInteractiveFixes: interactiveFixes.length > 0
25068
+ })}${interactiveBlock}`;
24679
25069
  };
24680
25070
  var nextStepsPrompt = ({ scannedFiles }) => `
24681
25071
  ### \u{1F4C1} Scanned Files
@@ -24721,10 +25111,16 @@ var fixesPrompt = ({
24721
25111
  offset,
24722
25112
  scannedFiles,
24723
25113
  limit,
24724
- gqlClient
25114
+ gqlClient,
25115
+ interactiveFixes = [],
25116
+ repositoryPath
24725
25117
  }) => {
25118
+ const interactiveBlock = interactiveFixesPrompt({
25119
+ interactiveFixes,
25120
+ repositoryPath
25121
+ });
24726
25122
  if (totalCount === 0) {
24727
- return noFixesFoundPrompt({ scannedFiles });
25123
+ return noFixesFoundPrompt({ scannedFiles }) + interactiveBlock;
24728
25124
  }
24729
25125
  const shownCount = fixes.length;
24730
25126
  const nextOffset = offset + shownCount;
@@ -24740,9 +25136,10 @@ ${applyFixesPrompt({
24740
25136
  currentTool: MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES,
24741
25137
  offset,
24742
25138
  limit,
24743
- gqlClient
25139
+ gqlClient,
25140
+ hasInteractiveFixes: interactiveFixes.length > 0
24744
25141
  })}
24745
-
25142
+ ${interactiveBlock}
24746
25143
  ${nextStepsPrompt({ scannedFiles })}
24747
25144
  `;
24748
25145
  };
@@ -24815,7 +25212,9 @@ For assistance:
24815
25212
  var freshFixesPrompt = ({
24816
25213
  fixes,
24817
25214
  limit,
24818
- gqlClient
25215
+ gqlClient,
25216
+ interactiveFixes = [],
25217
+ repositoryPath
24819
25218
  }) => {
24820
25219
  return `Here are the fresh fixes to the vulnerabilities discovered by Mobb MCP
24821
25220
 
@@ -24828,8 +25227,10 @@ ${applyFixesPrompt({
24828
25227
  currentTool: MCP_TOOL_FETCH_AVAILABLE_FIXES,
24829
25228
  offset: 0,
24830
25229
  limit,
24831
- gqlClient
25230
+ gqlClient,
25231
+ hasInteractiveFixes: interactiveFixes.length > 0
24832
25232
  })}
25233
+ ${interactiveFixesPrompt({ interactiveFixes, repositoryPath })}
24833
25234
  `;
24834
25235
  };
24835
25236
  function extractTargetFileFromPatch(patch) {
@@ -24848,7 +25249,9 @@ function formatSeverity(severityText, severityValue) {
24848
25249
  }
24849
25250
  var appliedFixesSummaryPrompt = ({
24850
25251
  fixes,
24851
- gqlClient
25252
+ gqlClient,
25253
+ interactiveFixes = [],
25254
+ repositoryPath
24852
25255
  }) => {
24853
25256
  const fixIds = fixes.map((fix) => fix.id);
24854
25257
  void gqlClient.updateFixesDownloadStatus(fixIds);
@@ -24883,11 +25286,11 @@ ${fixes.map((fix, index) => {
24883
25286
  ${continuousMonitoringSection}
24884
25287
 
24885
25288
  ${autoFixSettingsSection}
24886
-
25289
+ ${interactiveFixesPrompt({ interactiveFixes, repositoryPath })}
24887
25290
  ## \u{1F4CB} Next Steps
24888
25291
 
24889
25292
  1. **Review the changes** - Check the modified files to understand what was fixed
24890
- 2. **Test your application** - Ensure the fixes don't break existing functionality
25293
+ 2. **Test your application** - Ensure the fixes don't break existing functionality
24891
25294
  3. **Commit the changes** - Add and commit the security fixes to your repository
24892
25295
  4. **Continue coding** - Mobb will keep protecting your code automatically
24893
25296
 
@@ -25790,6 +26193,7 @@ var LocalMobbFolderService = class {
25790
26193
  };
25791
26194
 
25792
26195
  // src/mcp/services/PatchApplicationService.ts
26196
+ init_client_generates();
25793
26197
  init_configs();
25794
26198
  import {
25795
26199
  existsSync as existsSync6,
@@ -26355,7 +26759,8 @@ var PatchApplicationService = class {
26355
26759
  repositoryPath,
26356
26760
  scanStartTime,
26357
26761
  gqlClient,
26358
- scanContext
26762
+ scanContext,
26763
+ downloadSource = "AUTO_MVS" /* AutoMvs */
26359
26764
  }) {
26360
26765
  const appliedFixes = [];
26361
26766
  const failedFixes = [];
@@ -26414,20 +26819,26 @@ var PatchApplicationService = class {
26414
26819
  if (appliedFixes.length > 0 && gqlClient) {
26415
26820
  try {
26416
26821
  const appliedFixIds = appliedFixes.map((fix) => fix.id).filter(Boolean);
26417
- await gqlClient.updateAutoAppliedFixesStatus(appliedFixIds);
26822
+ if (downloadSource === "MCP" /* Mcp */) {
26823
+ await gqlClient.updateFixesDownloadStatus(appliedFixIds);
26824
+ } else {
26825
+ await gqlClient.updateAutoAppliedFixesStatus(appliedFixIds);
26826
+ }
26418
26827
  logDebug(
26419
- `[${scanContext}] Successfully updated download status for auto-applied fixes`,
26828
+ `[${scanContext}] Successfully updated download status for applied fixes`,
26420
26829
  {
26421
26830
  appliedFixIds,
26422
- count: appliedFixIds.length
26831
+ count: appliedFixIds.length,
26832
+ downloadSource
26423
26833
  }
26424
26834
  );
26425
26835
  } catch (error) {
26426
26836
  logError(
26427
- `[${scanContext}] Failed to update download status for auto-applied fixes`,
26837
+ `[${scanContext}] Failed to update download status for applied fixes`,
26428
26838
  {
26429
26839
  error: error instanceof Error ? error.message : String(error),
26430
- appliedFixCount: appliedFixes.length
26840
+ appliedFixCount: appliedFixes.length,
26841
+ downloadSource
26431
26842
  }
26432
26843
  );
26433
26844
  }
@@ -27169,6 +27580,7 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
27169
27580
  __publicField(this, "path", "");
27170
27581
  __publicField(this, "filesLastScanned", {});
27171
27582
  __publicField(this, "freshFixes", []);
27583
+ __publicField(this, "interactiveFixes", []);
27172
27584
  __publicField(this, "reportedFixes", []);
27173
27585
  __publicField(this, "intervalId", null);
27174
27586
  __publicField(this, "isInitialScanComplete", false);
@@ -27190,6 +27602,7 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
27190
27602
  reset() {
27191
27603
  this.filesLastScanned = {};
27192
27604
  this.freshFixes = [];
27605
+ this.interactiveFixes = [];
27193
27606
  this.reportedFixes = [];
27194
27607
  this.hasAuthenticationFailed = false;
27195
27608
  this.fullScanPathsScanned = configStore.get("fullScanPathsScanned") || [];
@@ -27275,6 +27688,16 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
27275
27688
  const newFixes = fixes?.fixes?.filter(
27276
27689
  (fix) => !this.isFixAlreadyReported(fix)
27277
27690
  );
27691
+ const newInteractiveFixes = fixes?.interactiveFixes?.filter(
27692
+ (fix) => !this.isFixAlreadyReported(fix)
27693
+ ) ?? [];
27694
+ if (newInteractiveFixes.length > 0) {
27695
+ this.interactiveFixes.push(...newInteractiveFixes);
27696
+ logInfo(
27697
+ `[${scanContext}] Buffered ${newInteractiveFixes.length} interactive fixes for next response`,
27698
+ { totalBuffered: this.interactiveFixes.length }
27699
+ );
27700
+ }
27278
27701
  logInfo(
27279
27702
  `[${scanContext}] Security fixes retrieved, total: ${fixes?.fixes?.length || 0}, new: ${newFixes?.length || 0}`
27280
27703
  );
@@ -27704,7 +28127,9 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
27704
28127
  return freshFixesPrompt({
27705
28128
  fixes: freshFixes,
27706
28129
  limit: MCP_DEFAULT_LIMIT,
27707
- gqlClient: this.gqlClient
28130
+ gqlClient: this.gqlClient,
28131
+ interactiveFixes: this.interactiveFixes.splice(0, MCP_DEFAULT_LIMIT),
28132
+ repositoryPath: this.path
27708
28133
  });
27709
28134
  }
27710
28135
  logInfo(`[${scanContext}] No fresh fixes to report`);
@@ -27718,7 +28143,9 @@ var _CheckForNewAvailableFixesService = class _CheckForNewAvailableFixesService
27718
28143
  );
27719
28144
  return appliedFixesSummaryPrompt({
27720
28145
  fixes: appliedFixesToShow,
27721
- gqlClient: this.gqlClient
28146
+ gqlClient: this.gqlClient,
28147
+ interactiveFixes: this.interactiveFixes.splice(0, MCP_DEFAULT_LIMIT),
28148
+ repositoryPath: this.path
27722
28149
  });
27723
28150
  }
27724
28151
  logInfo(`[${scanContext}] No applied fixes to report`);
@@ -27825,6 +28252,7 @@ var _FetchAvailableFixesService = class _FetchAvailableFixesService {
27825
28252
  }
27826
28253
  async checkForAvailableFixes({
27827
28254
  repoUrl,
28255
+ repositoryPath,
27828
28256
  limit = MCP_DEFAULT_LIMIT,
27829
28257
  offset,
27830
28258
  fileFilter
@@ -27861,7 +28289,9 @@ var _FetchAvailableFixesService = class _FetchAvailableFixesService {
27861
28289
  fixReport,
27862
28290
  offset: effectiveOffset,
27863
28291
  limit,
27864
- gqlClient
28292
+ gqlClient,
28293
+ interactiveFixes: fixReport.interactiveFixes ?? [],
28294
+ repositoryPath
27865
28295
  });
27866
28296
  this.currentOffset = effectiveOffset + (fixReport.fixes?.length || 0);
27867
28297
  return prompt;
@@ -28003,6 +28433,7 @@ Call this tool instead of ${MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES} when you only
28003
28433
  }
28004
28434
  const fixResult = await this.availableFixesService.checkForAvailableFixes({
28005
28435
  repoUrl: originUrl,
28436
+ repositoryPath: path37,
28006
28437
  limit: args.limit,
28007
28438
  offset: args.offset,
28008
28439
  fileFilter: actualFileFilter
@@ -28106,6 +28537,7 @@ import z45 from "zod";
28106
28537
  init_configs();
28107
28538
 
28108
28539
  // src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesService.ts
28540
+ init_client_generates();
28109
28541
  init_configs();
28110
28542
  var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService {
28111
28543
  constructor() {
@@ -28196,7 +28628,9 @@ var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService
28196
28628
  offset: effectiveOffset,
28197
28629
  scannedFiles: [...fileList],
28198
28630
  limit: effectiveLimit,
28199
- gqlClient: this.gqlClient
28631
+ gqlClient: this.gqlClient,
28632
+ interactiveFixes: fixes.interactiveFixes,
28633
+ repositoryPath
28200
28634
  });
28201
28635
  this.currentOffset = effectiveOffset + (fixes.fixes?.length || 0);
28202
28636
  return prompt;
@@ -28240,12 +28674,165 @@ var _ScanAndFixVulnerabilitiesService = class _ScanAndFixVulnerabilitiesService
28240
28674
  logDebug(`${fixes?.fixes?.length} fixes retrieved`);
28241
28675
  return {
28242
28676
  fixes: fixes?.fixes || [],
28243
- totalCount: fixes?.totalCount || 0
28677
+ totalCount: fixes?.totalCount || 0,
28678
+ interactiveFixes: fixes?.interactiveFixes || []
28244
28679
  };
28245
28680
  }
28681
+ /** Applies patches from interactiveAnswers only (no scan). */
28682
+ async applyInteractiveAnswers({
28683
+ interactiveAnswers,
28684
+ repositoryPath
28685
+ }) {
28686
+ this.gqlClient = await this.initializeGqlClient();
28687
+ logInfo(
28688
+ `Applying ${interactiveAnswers.length} interactive fix(es) with LLM-supplied answers`,
28689
+ { repositoryPath }
28690
+ );
28691
+ const applied = [];
28692
+ const failed = [];
28693
+ const skipped = [];
28694
+ for (const entry of interactiveAnswers) {
28695
+ try {
28696
+ const { fixData } = await this.gqlClient.getFixWithAnswers({
28697
+ fixId: entry.fixId,
28698
+ answers: entry.answers
28699
+ });
28700
+ if (!fixData) {
28701
+ failed.push({
28702
+ fixId: entry.fixId,
28703
+ reason: "Fix not found on the server (may have expired)"
28704
+ });
28705
+ continue;
28706
+ }
28707
+ if (fixData.__typename !== "FixData") {
28708
+ failed.push({
28709
+ fixId: entry.fixId,
28710
+ reason: `Backend returned ${fixData.__typename} \u2014 could not produce a patch with the supplied answers`
28711
+ });
28712
+ continue;
28713
+ }
28714
+ if (!fixData.patch) {
28715
+ failed.push({
28716
+ fixId: entry.fixId,
28717
+ reason: "Backend returned FixData with no patch \u2014 answers did not yield an applicable fix"
28718
+ });
28719
+ continue;
28720
+ }
28721
+ const sentByKey = new Map(entry.answers.map((a) => [a.key, a.value]));
28722
+ const invalidSelectAnswers = [];
28723
+ for (const q of fixData.questions) {
28724
+ if (q.inputType !== "SELECT" /* Select */) continue;
28725
+ const sentValue = sentByKey.get(q.key);
28726
+ if (sentValue === void 0) continue;
28727
+ if (!q.options.includes(sentValue)) {
28728
+ invalidSelectAnswers.push({
28729
+ key: q.key,
28730
+ sentValue,
28731
+ options: [...q.options]
28732
+ });
28733
+ }
28734
+ }
28735
+ if (invalidSelectAnswers.length > 0) {
28736
+ skipped.push({
28737
+ fixId: entry.fixId,
28738
+ invalidSelectAnswers
28739
+ });
28740
+ continue;
28741
+ }
28742
+ const newPendingKeys = fixData.questions.map((q) => q.key).filter((k) => !sentByKey.has(k));
28743
+ const mcpFix = McpFixSchema.parse({
28744
+ __typename: "fix",
28745
+ id: entry.fixId,
28746
+ confidence: 0,
28747
+ safeIssueType: null,
28748
+ safeIssueLanguage: null,
28749
+ severityText: null,
28750
+ severityValue: null,
28751
+ vulnerabilityReportIssues: [],
28752
+ patchAndQuestions: fixData
28753
+ });
28754
+ const result = await PatchApplicationService.applyFixes({
28755
+ fixes: [mcpFix],
28756
+ repositoryPath,
28757
+ gqlClient: this.gqlClient,
28758
+ scanContext: ScanContext.USER_REQUEST,
28759
+ downloadSource: "MCP" /* Mcp */
28760
+ });
28761
+ if (result.appliedFixes.length > 0) {
28762
+ const targetFile = extractTargetFile(fixData.patch) ?? "unknown file";
28763
+ applied.push({
28764
+ fixId: entry.fixId,
28765
+ targetFile,
28766
+ newPendingKeys
28767
+ });
28768
+ } else {
28769
+ failed.push({
28770
+ fixId: entry.fixId,
28771
+ reason: result.failedFixes[0]?.error ?? "patch application failed"
28772
+ });
28773
+ }
28774
+ } catch (error) {
28775
+ failed.push({
28776
+ fixId: entry.fixId,
28777
+ reason: error.message
28778
+ });
28779
+ }
28780
+ }
28781
+ return formatApplyAnswersSummary({ applied, failed, skipped });
28782
+ }
28246
28783
  };
28247
28784
  __publicField(_ScanAndFixVulnerabilitiesService, "instance");
28248
28785
  var ScanAndFixVulnerabilitiesService = _ScanAndFixVulnerabilitiesService;
28786
+ function extractTargetFile(patch) {
28787
+ const match = patch.match(/^\+\+\+ b\/(.+)$/m);
28788
+ return match?.[1] ?? null;
28789
+ }
28790
+ function formatApplyAnswersSummary({
28791
+ applied,
28792
+ failed,
28793
+ skipped
28794
+ }) {
28795
+ const sections = [];
28796
+ if (applied.length > 0) {
28797
+ sections.push(
28798
+ `## \u2705 Applied ${applied.length} fix${applied.length === 1 ? "" : "es"}
28799
+
28800
+ ` + applied.map((a) => {
28801
+ const hint = a.newPendingKeys.length > 0 ? `
28802
+ \u26A0\uFE0F The backend returned additional question key(s) we didn't send: [${a.newPendingKeys.map((k) => `\`${k}\``).join(
28803
+ ", "
28804
+ )}]. This is either (a) a true cascading question \u2014 re-call \`scan_and_fix_vulnerabilities\` with those added to \`interactiveAnswers\` if you have a confident answer; or (b) the key we sent was wrong (e.g. camelCase vs snake_case) and the backend fell back to its default \u2014 copy the echoed key verbatim and retry. The patch above was already applied using defaults.` : "";
28805
+ return `- **\`${a.fixId}\`** \u2192 \`${a.targetFile}\`${hint}`;
28806
+ }).join("\n")
28807
+ );
28808
+ }
28809
+ if (skipped.length > 0) {
28810
+ sections.push(
28811
+ `## \u23ED\uFE0F Skipped ${skipped.length} fix${skipped.length === 1 ? "" : "es"} \u2014 invalid SELECT answer value(s)
28812
+
28813
+ ` + skipped.map((s) => {
28814
+ const detail = s.invalidSelectAnswers.map(
28815
+ (a) => `\`${a.key}\` got \`"${a.sentValue}"\` \u2014 allowed options: [${a.options.map((o) => `\`"${o}"\``).join(", ")}]`
28816
+ ).join("; ");
28817
+ return `- **\`${s.fixId}\`** \u2014 ${detail}
28818
+ The file was **not modified**. Re-call \`scan_and_fix_vulnerabilities\` with one of the exact allowed option strings (copy character-for-character, no commentary appended) or omit this fix entirely if no option is justified by the code.`;
28819
+ }).join("\n") + `
28820
+
28821
+ This guardrail exists to prevent the backend from silently falling back to its default sanitizer for an answer it didn't recognize \u2014 which could apply a semantically-wrong patch and break legitimate code paths.`
28822
+ );
28823
+ }
28824
+ if (failed.length > 0) {
28825
+ sections.push(
28826
+ `## \u274C Failed
28827
+
28828
+ ` + failed.map((f) => `- **\`${f.fixId}\`** \u2014 ${f.reason}`).join("\n")
28829
+ );
28830
+ }
28831
+ if (sections.length === 0) {
28832
+ return "No fixes were processed.";
28833
+ }
28834
+ return sections.join("\n\n");
28835
+ }
28249
28836
 
28250
28837
  // src/mcp/tools/scanAndFixVulnerabilities/ScanAndFixVulnerabilitiesTool.ts
28251
28838
  var ScanAndFixVulnerabilitiesTool = class extends BaseTool {
@@ -28253,13 +28840,23 @@ var ScanAndFixVulnerabilitiesTool = class extends BaseTool {
28253
28840
  super();
28254
28841
  __publicField(this, "name", MCP_TOOL_SCAN_AND_FIX_VULNERABILITIES);
28255
28842
  __publicField(this, "displayName", "Scan and Fix Vulnerabilities");
28256
- // A detailed description to guide the LLM on when and how to invoke this tool.
28257
- __publicField(this, "description", `Scans a given local repository for security vulnerabilities and returns auto-generated code fixes.
28843
+ __publicField(this, "description", `Scans a given local repository for security vulnerabilities, applies the auto-fixable ones, and surfaces any fix that needs your input as an "Interactive fix". Re-invoke with "interactiveAnswers" to apply those.
28844
+
28845
+ Two modes of operation:
28846
+
28847
+ A) SCAN MODE (default \u2014 interactiveAnswers omitted)
28848
+ - Scans changed/recent files at "path"
28849
+ - Auto-applies fixes that need no input
28850
+ - Returns "Interactive fix" entries for fixes that need decisions; you (the AI) decide answers from the surrounding code
28851
+
28852
+ B) APPLY-WITH-ANSWERS MODE (interactiveAnswers field supplied \u2014 array may be empty or partial)
28853
+ - SKIPS scanning entirely (does NOT fall back to scan mode just because some fixes were skipped)
28854
+ - Include ONLY fixes where answers are justified by code/context; omit fix IDs you are not confident about
28855
+ - Empty array []: abstain from applying ALL interactive fixes (no patches fetched \u2014 summarize skips for the user)
28258
28856
 
28259
28857
  When to invoke:
28260
- \u2022 Use when the user explicitly asks to "scan for vulnerabilities", "run a security check", or "test for security issues" in a local repository.
28261
- \u2022 The repository must exist on disk; supply its absolute path with the required "path" argument.
28262
- \u2022 Ideal after the user makes code changes (added/modified/staged files) but before committing, or whenever they request a full rescan.
28858
+ \u2022 Mode A \u2014 when the user asks to "scan for vulnerabilities", "run a security check", or after they make code changes.
28859
+ \u2022 Mode B \u2014 immediately after Mode A returns interactive fixes; pass confident answers only; use [] only when abstaining from every interactive fix.
28263
28860
 
28264
28861
  How to invoke:
28265
28862
  \u2022 Required argument:
@@ -28269,25 +28866,32 @@ How to invoke:
28269
28866
  \u2013 limit (number): maximum number of fixes to include in the response.
28270
28867
  \u2013 maxFiles (number): maximum number of files to scan (default: ${MCP_DEFAULT_MAX_FILES_TO_SCAN}). Provide this value to increase the scope of the scan.
28271
28868
  \u2013 rescan (boolean): true to force a complete rescan even if cached results exist.
28869
+ \u2013 interactiveAnswers (array): triggers Mode B. Each entry: { fixId, answers: [{ key, value }] }. SELECT values MUST be exact strings from the option list. Omit fixes you cannot answer confidently. Use [] to abstain from all interactive fixes without rescanning.
28272
28870
 
28273
- Behaviour:
28274
- \u2022 If the directory is a valid Git repository, the tool scans the changed files in the repository. If there are no changes, it scans the files included in the las commit.
28871
+ Behaviour (Mode A):
28872
+ \u2022 If the directory is a valid Git repository, the tool scans the changed files in the repository. If there are no changes, it scans the files included in the last commit.
28275
28873
  \u2022 If the directory is not a valid Git repository, the tool falls back to scanning recently changed files in the folder.
28276
28874
  \u2022 If maxFiles is provided, the tool scans the maxFiles most recently changed files in the repository.
28277
- \u2022 By default, only new, modified, or staged files are scanned; if none are found, it checks recently changed files.
28278
- \u2022 The tool NEVER commits or pushes changes; it only returns proposed diffs/fixes as text.
28875
+ \u2022 The tool NEVER commits or pushes changes.
28279
28876
 
28280
28877
  Return value:
28281
- The response is an object with a single "content" array containing one text element. The text is either:
28282
- \u2022 A human-readable summary of the fixes / patches, or
28283
- \u2022 A diagnostic or error message if the scan fails or finds nothing to fix.
28878
+ A "content" array with one text element. Either a human-readable summary of fixes/patches, an interactive-fix prompt, an apply-with-answers result, or an error message.
28284
28879
 
28285
- Example payload:
28880
+ Example payload (Mode A):
28881
+ { "path": "/home/user/my-project", "limit": 20, "maxFiles": 50 }
28882
+
28883
+ Example payload (Mode B \u2014 subset or abstain):
28286
28884
  {
28287
28885
  "path": "/home/user/my-project",
28288
- "limit": 20,
28289
- "maxFiles": 50,
28290
- "rescan": false
28886
+ "interactiveAnswers": [
28887
+ { "fixId": "abc-123", "answers": [{ "key": "isServerSideCode", "value": "yes" }] }
28888
+ ]
28889
+ }
28890
+
28891
+ Example payload (Mode B \u2014 abstain from every interactive fix, no rescan):
28892
+ {
28893
+ "path": "/home/user/my-project",
28894
+ "interactiveAnswers": []
28291
28895
  }`);
28292
28896
  __publicField(this, "hasAuthentication", true);
28293
28897
  __publicField(this, "inputValidationSchema", z45.object({
@@ -28302,6 +28906,21 @@ Example payload:
28302
28906
  rescan: z45.boolean().optional().describe("Optional whether to rescan the repository"),
28303
28907
  scanRecentlyChangedFiles: z45.boolean().optional().describe(
28304
28908
  "Optional whether to automatically scan recently changed files when no changed files are found in git status. If false, the tool will prompt the user instead."
28909
+ ),
28910
+ interactiveAnswers: z45.array(
28911
+ z45.object({
28912
+ fixId: z45.string().min(1).describe('Fix id from a previous "Interactive fix" prompt block.'),
28913
+ answers: z45.array(
28914
+ z45.object({
28915
+ key: z45.string().min(1).describe("FixQuestion key."),
28916
+ value: z45.string().describe(
28917
+ "For SELECT questions MUST be one of the listed options; for TEXT/NUMBER, a free-form value."
28918
+ )
28919
+ })
28920
+ ).min(1)
28921
+ })
28922
+ ).optional().describe(
28923
+ "When supplied (including []), SKIPS scanning. Non-empty: apply each listed interactive fix. Empty []: abstain from all interactive fixes \u2014 no patches applied. Omit entirely for scan mode."
28305
28924
  )
28306
28925
  }));
28307
28926
  __publicField(this, "inputSchema", {
@@ -28330,6 +28949,34 @@ Example payload:
28330
28949
  scanRecentlyChangedFiles: {
28331
28950
  type: "boolean",
28332
28951
  description: "[Optional] whether to automatically scan recently changed files when no changed files are found in git status. If false, the tool will prompt the user instead."
28952
+ },
28953
+ interactiveAnswers: {
28954
+ type: "array",
28955
+ items: {
28956
+ type: "object",
28957
+ properties: {
28958
+ fixId: {
28959
+ type: "string",
28960
+ description: 'Fix id from a previous "Interactive fix" prompt.'
28961
+ },
28962
+ answers: {
28963
+ type: "array",
28964
+ items: {
28965
+ type: "object",
28966
+ properties: {
28967
+ key: { type: "string", description: "FixQuestion key." },
28968
+ value: {
28969
+ type: "string",
28970
+ description: "Decided value (SELECT must match an option exactly)."
28971
+ }
28972
+ },
28973
+ required: ["key", "value"]
28974
+ }
28975
+ }
28976
+ },
28977
+ required: ["fixId", "answers"]
28978
+ },
28979
+ description: "[Optional] When supplied (including []), skips scanning. Non-empty: apply interactive fixes with answers. Empty []: abstain from all interactive fixes without rescanning. Omit for scan mode."
28333
28980
  }
28334
28981
  },
28335
28982
  required: ["path"]
@@ -28340,7 +28987,8 @@ Example payload:
28340
28987
  }
28341
28988
  async executeInternal(args) {
28342
28989
  logDebug(`Executing tool: ${this.name}`, {
28343
- path: args.path
28990
+ path: args.path,
28991
+ mode: args.interactiveAnswers === void 0 ? "scan" : args.interactiveAnswers.length === 0 ? "apply-interactive-abstain-all" : "apply-with-answers"
28344
28992
  });
28345
28993
  if (!args.path) {
28346
28994
  throw new Error("Invalid arguments: Missing required parameter 'path'");
@@ -28352,6 +29000,26 @@ Example payload:
28352
29000
  );
28353
29001
  }
28354
29002
  const path37 = pathValidationResult.path;
29003
+ if (args.interactiveAnswers !== void 0) {
29004
+ if (args.interactiveAnswers.length === 0) {
29005
+ return this.createSuccessResponse(
29006
+ interactiveAnswersAbstainAllToolResponse
29007
+ );
29008
+ }
29009
+ try {
29010
+ const result = await this.vulnerabilityFixService.applyInteractiveAnswers({
29011
+ interactiveAnswers: args.interactiveAnswers,
29012
+ repositoryPath: path37
29013
+ });
29014
+ return this.createSuccessResponse(result);
29015
+ } catch (error) {
29016
+ const message = error.message;
29017
+ logError("Tool execution failed (apply-with-answers)", {
29018
+ error: message
29019
+ });
29020
+ return this.createSuccessResponse(message);
29021
+ }
29022
+ }
28355
29023
  const files = await getLocalFiles({
28356
29024
  path: path37,
28357
29025
  maxFileSize: MCP_MAX_FILE_SIZE,