@4da/mcp-server 4.6.1 → 4.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +3 -1
  2. package/dist/db.d.ts +21 -2
  3. package/dist/db.js +66 -16
  4. package/dist/doctor.js +5 -3
  5. package/dist/embeddings.d.ts +88 -0
  6. package/dist/embeddings.js +292 -0
  7. package/dist/index.d.ts +9 -9
  8. package/dist/index.js +21 -15
  9. package/dist/live/cvss.d.ts +16 -0
  10. package/dist/live/cvss.js +62 -0
  11. package/dist/live/index.d.ts +9 -1
  12. package/dist/live/index.js +40 -3
  13. package/dist/live/npm-registry.js +29 -6
  14. package/dist/live/osv-scanner.d.ts +8 -0
  15. package/dist/live/osv-scanner.js +115 -30
  16. package/dist/live/platform.d.ts +32 -0
  17. package/dist/live/platform.js +156 -0
  18. package/dist/live/semver-utils.d.ts +8 -0
  19. package/dist/live/semver-utils.js +24 -0
  20. package/dist/live/types.d.ts +31 -9
  21. package/dist/live/version-resolver.d.ts +12 -2
  22. package/dist/live/version-resolver.js +63 -20
  23. package/dist/llm.js +1 -1
  24. package/dist/project-scanner.d.ts +2 -0
  25. package/dist/project-scanner.js +34 -0
  26. package/dist/scheduler/core.d.ts +25 -0
  27. package/dist/scheduler/core.js +61 -0
  28. package/dist/scheduler/freshness.d.ts +17 -0
  29. package/dist/scheduler/freshness.js +41 -0
  30. package/dist/scheduler/index.d.ts +21 -0
  31. package/dist/scheduler/index.js +32 -0
  32. package/dist/scheduler/refreshers.d.ts +21 -0
  33. package/dist/scheduler/refreshers.js +21 -0
  34. package/dist/schemas/get-relevant-content.json +1 -1
  35. package/dist/schemas/upgrade-planner.json +1 -1
  36. package/dist/schemas/what-should-i-know.json +1 -1
  37. package/dist/tools/agent-memory.d.ts +11 -1
  38. package/dist/tools/agent-memory.js +120 -14
  39. package/dist/tools/decision-enforcement.d.ts +17 -1
  40. package/dist/tools/decision-enforcement.js +109 -81
  41. package/dist/tools/decision-memory.d.ts +1 -1
  42. package/dist/tools/decision-memory.js +58 -28
  43. package/dist/tools/decision-recall.d.ts +59 -0
  44. package/dist/tools/decision-recall.js +87 -0
  45. package/dist/tools/dependency-health.js +6 -4
  46. package/dist/tools/developer-dna.js +4 -2
  47. package/dist/tools/knowledge-gaps.js +16 -4
  48. package/dist/tools/recall.d.ts +33 -0
  49. package/dist/tools/recall.js +134 -0
  50. package/dist/tools/upgrade-planner.d.ts +25 -1
  51. package/dist/tools/upgrade-planner.js +111 -30
  52. package/dist/tools/vulnerability-scan.d.ts +46 -0
  53. package/dist/tools/vulnerability-scan.js +64 -24
  54. package/dist/tools/what-should-i-know.d.ts +3 -1
  55. package/dist/tools/what-should-i-know.js +210 -106
  56. package/dist/types.d.ts +9 -0
  57. package/package.json +8 -3
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js.map +0 -1
  60. package/dist/db.d.ts.map +0 -1
  61. package/dist/db.js.map +0 -1
  62. package/dist/doctor.d.ts.map +0 -1
  63. package/dist/doctor.js.map +0 -1
  64. package/dist/http-transport.d.ts.map +0 -1
  65. package/dist/http-transport.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/live/cache.d.ts.map +0 -1
  69. package/dist/live/cache.js.map +0 -1
  70. package/dist/live/crates-registry.d.ts.map +0 -1
  71. package/dist/live/crates-registry.js.map +0 -1
  72. package/dist/live/go-registry.d.ts.map +0 -1
  73. package/dist/live/go-registry.js.map +0 -1
  74. package/dist/live/hn-fetcher.d.ts.map +0 -1
  75. package/dist/live/hn-fetcher.js.map +0 -1
  76. package/dist/live/http-utils.d.ts.map +0 -1
  77. package/dist/live/http-utils.js.map +0 -1
  78. package/dist/live/index.d.ts.map +0 -1
  79. package/dist/live/index.js.map +0 -1
  80. package/dist/live/npm-registry.d.ts.map +0 -1
  81. package/dist/live/npm-registry.js.map +0 -1
  82. package/dist/live/osv-scanner.d.ts.map +0 -1
  83. package/dist/live/osv-scanner.js.map +0 -1
  84. package/dist/live/pypi-registry.d.ts.map +0 -1
  85. package/dist/live/pypi-registry.js.map +0 -1
  86. package/dist/live/rate-limiter.d.ts.map +0 -1
  87. package/dist/live/rate-limiter.js.map +0 -1
  88. package/dist/live/semver-utils.d.ts.map +0 -1
  89. package/dist/live/semver-utils.js.map +0 -1
  90. package/dist/live/types.d.ts.map +0 -1
  91. package/dist/live/types.js.map +0 -1
  92. package/dist/live/version-resolver.d.ts.map +0 -1
  93. package/dist/live/version-resolver.js.map +0 -1
  94. package/dist/live-singleton.d.ts.map +0 -1
  95. package/dist/live-singleton.js.map +0 -1
  96. package/dist/llm.d.ts.map +0 -1
  97. package/dist/llm.js.map +0 -1
  98. package/dist/output-manager.d.ts.map +0 -1
  99. package/dist/output-manager.js.map +0 -1
  100. package/dist/project-scanner.d.ts.map +0 -1
  101. package/dist/project-scanner.js.map +0 -1
  102. package/dist/schema-registry.d.ts.map +0 -1
  103. package/dist/schema-registry.js.map +0 -1
  104. package/dist/setup.d.ts.map +0 -1
  105. package/dist/setup.js.map +0 -1
  106. package/dist/tool-dispatch.d.ts.map +0 -1
  107. package/dist/tool-dispatch.js.map +0 -1
  108. package/dist/tools/agent-memory.d.ts.map +0 -1
  109. package/dist/tools/agent-memory.js.map +0 -1
  110. package/dist/tools/decision-enforcement.d.ts.map +0 -1
  111. package/dist/tools/decision-enforcement.js.map +0 -1
  112. package/dist/tools/decision-memory.d.ts.map +0 -1
  113. package/dist/tools/decision-memory.js.map +0 -1
  114. package/dist/tools/dependency-health.d.ts.map +0 -1
  115. package/dist/tools/dependency-health.js.map +0 -1
  116. package/dist/tools/developer-dna.d.ts.map +0 -1
  117. package/dist/tools/developer-dna.js.map +0 -1
  118. package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
  119. package/dist/tools/ecosystem-pulse.js.map +0 -1
  120. package/dist/tools/get-actionable-signals.d.ts.map +0 -1
  121. package/dist/tools/get-actionable-signals.js.map +0 -1
  122. package/dist/tools/get-context.d.ts.map +0 -1
  123. package/dist/tools/get-context.js.map +0 -1
  124. package/dist/tools/get-relevant-content.d.ts.map +0 -1
  125. package/dist/tools/get-relevant-content.js.map +0 -1
  126. package/dist/tools/index.d.ts.map +0 -1
  127. package/dist/tools/index.js.map +0 -1
  128. package/dist/tools/knowledge-gaps.d.ts.map +0 -1
  129. package/dist/tools/knowledge-gaps.js.map +0 -1
  130. package/dist/tools/record-feedback.d.ts.map +0 -1
  131. package/dist/tools/record-feedback.js.map +0 -1
  132. package/dist/tools/upgrade-planner.d.ts.map +0 -1
  133. package/dist/tools/upgrade-planner.js.map +0 -1
  134. package/dist/tools/vulnerability-scan.d.ts.map +0 -1
  135. package/dist/tools/vulnerability-scan.js.map +0 -1
  136. package/dist/tools/what-should-i-know.d.ts.map +0 -1
  137. package/dist/tools/what-should-i-know.js.map +0 -1
  138. package/dist/types.d.ts.map +0 -1
  139. package/dist/types.js.map +0 -1
@@ -2,13 +2,27 @@
2
2
  /**
3
3
  * upgrade_planner tool
4
4
  *
5
- * Ranked upgrade recommendations for project dependencies.
5
+ * Ranked standalone upgrade recommendations for project dependencies.
6
6
  * Combines version freshness, vulnerability data, and deprecation status
7
7
  * to produce a prioritized upgrade plan.
8
+ *
9
+ * Honesty contract (Phase 0 hardening):
10
+ * - Transitive CVEs are surfaced as `waiting_on_upstream` steps instead of
11
+ * being counted-but-never-recommended. No dependency-graph parsing happens
12
+ * here — parent mapping is the desktop core's job; the honest label is ours.
13
+ * - Output carries explicit heuristic provenance: this is a point-in-time
14
+ * standalone plan, not the 4DA app's cross-project, version-confirmed plan.
15
+ * - `projectPath` reports the directory set the dependencies were actually
16
+ * resolved from, not whatever `process.cwd()` happens to be.
17
+ * - When no vulnerability scan has run, the plan says so instead of silently
18
+ * producing a CVE-blind ranking.
8
19
  */
20
+ import { maxSemver } from "../live/semver-utils.js";
21
+ const PROVENANCE_NOTE = "Point-in-time heuristic from local lockfiles plus live registry/OSV lookups. " +
22
+ "Not the 4DA desktop app's cross-project, version-confirmed plan.";
9
23
  export const upgradePlannerTool = {
10
24
  name: "upgrade_planner",
11
- description: "Ranked upgrade recommendations for dependencies. Prioritizes by vulnerability severity, deprecation, and version distance. Shows quick wins (patch/minor) vs breaking changes (major).",
25
+ description: "Ranked standalone upgrade recommendations for dependencies — call before upgrading, adding, or auditing any dependency to pick the safest order. Prioritizes by vulnerability severity (direct AND transitive), deprecation, and version distance. Splits quick wins (patch/minor) from breaking changes (major) and from transitive CVEs waiting on upstream. Run vulnerability_scan first for a CVE-aware plan. Privacy: live mode may query public registries and OSV with package names and versions; it never sends source code.",
12
26
  inputSchema: {
13
27
  type: "object",
14
28
  properties: {
@@ -28,6 +42,14 @@ export const upgradePlannerTool = {
28
42
  },
29
43
  },
30
44
  };
45
+ const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
46
+ function severityToRisk(severities) {
47
+ if (severities.includes("critical"))
48
+ return "critical";
49
+ if (severities.includes("high"))
50
+ return "high";
51
+ return "medium";
52
+ }
31
53
  export async function executeUpgradePlanner(_db, params, liveIntel) {
32
54
  if (!liveIntel || !liveIntel.isInitialized()) {
33
55
  return {
@@ -38,16 +60,20 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
38
60
  summary: "No project detected. Run from a directory with package.json, Cargo.toml, pyproject.toml, or go.mod.",
39
61
  quickWins: 0,
40
62
  breakingChanges: 0,
63
+ waitingOnUpstream: 0,
64
+ vulnerabilityDataAvailable: false,
65
+ provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
41
66
  };
42
67
  }
43
68
  const includeDev = params.include_dev ?? false;
44
69
  let deps = liveIntel.getResolvedDeps();
45
70
  if (!includeDev)
46
71
  deps = deps.filter((d) => !d.isDev);
47
- // Fetch registry data
72
+ // Fetch registry data (live lookups for direct deps; cached where fresh)
48
73
  const registryData = await liveIntel.fetchRegistryHealth(deps);
49
- // Get vulnerability data
74
+ // Vulnerability data from the last scan this session (no network here).
50
75
  const vulnResult = liveIntel.getVulnerabilities();
76
+ const vulnerabilityDataAvailable = vulnResult !== null;
51
77
  const vulnsByPackage = new Map();
52
78
  if (vulnResult) {
53
79
  for (const v of vulnResult.vulnerabilities) {
@@ -58,31 +84,32 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
58
84
  vulnId: v.vulnId,
59
85
  summary: v.summary,
60
86
  fixedVersion: v.fixedVersion,
87
+ platformActive: v.platformActive,
61
88
  });
62
89
  }
63
90
  }
64
- // Build recommendations
91
+ // Build recommendations — direct deps (registry-informed)
65
92
  const recommendations = [];
93
+ const directPackages = new Set();
66
94
  for (const dep of registryData) {
95
+ directPackages.add(`${dep.ecosystem}\0${dep.name}`);
67
96
  const reasons = [];
68
97
  let risk = "low";
69
98
  let targetVersion = dep.latestStableVersion || dep.latestVersion;
99
+ let platformActive = true;
70
100
  // Check vulnerabilities
71
101
  const vulns = vulnsByPackage.get(dep.name);
72
102
  if (vulns && vulns.length > 0) {
73
- const hasCritical = vulns.some((v) => v.severity === "critical");
74
- const hasHigh = vulns.some((v) => v.severity === "high");
75
- if (hasCritical)
76
- risk = "critical";
77
- else if (hasHigh)
78
- risk = "high";
79
- else
80
- risk = "medium";
103
+ risk = severityToRisk(vulns.map((v) => v.severity));
81
104
  reasons.push(`${vulns.length} known CVE${vulns.length !== 1 ? "s" : ""} (${vulns.map((v) => v.vulnId).join(", ")})`);
105
+ if (vulns.every((v) => !v.platformActive)) {
106
+ platformActive = false;
107
+ reasons.push("Affected code is gated behind a target not active on this platform");
108
+ }
82
109
  // Use fixed version as target if available
83
- const fixedVersions = vulns.map((v) => v.fixedVersion).filter(Boolean);
110
+ const fixedVersions = vulns.map((v) => v.fixedVersion).filter((f) => Boolean(f));
84
111
  if (fixedVersions.length > 0 && !targetVersion) {
85
- targetVersion = fixedVersions[0];
112
+ targetVersion = maxSemver(fixedVersions);
86
113
  }
87
114
  }
88
115
  // Check deprecation
@@ -106,11 +133,9 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
106
133
  reasons.push(`${d.patch} patch${d.patch !== 1 ? "es" : ""} behind`);
107
134
  }
108
135
  }
109
- // Skip if no upgrade needed
136
+ // Skip if no upgrade needed (no vuln, not deprecated, not behind => no reasons)
110
137
  if (reasons.length === 0)
111
138
  continue;
112
- if (dep.versionsBehind?.label === "up-to-date" && !dep.deprecated && !vulns)
113
- continue;
114
139
  const label = dep.versionsBehind?.label;
115
140
  const upgradeType = !label || label === "up-to-date" ? "patch" : label;
116
141
  recommendations.push({
@@ -123,45 +148,101 @@ export async function executeUpgradePlanner(_db, params, liveIntel) {
123
148
  reasons,
124
149
  breaking: dep.versionsBehind?.label === "major",
125
150
  isDev: dep.isDev,
151
+ scope: "direct",
152
+ action: "upgrade_direct",
153
+ platformActive,
126
154
  });
127
155
  }
156
+ // Transitive vulnerable packages — previously counted in scan totals but
157
+ // never recommended. Surfaced as waiting_on_upstream with honest labels.
158
+ if (vulnResult) {
159
+ const transitive = new Map();
160
+ for (const v of vulnResult.vulnerabilities) {
161
+ if (v.isDirect)
162
+ continue;
163
+ if (directPackages.has(`${v.ecosystem}\0${v.package}`))
164
+ continue;
165
+ if (!includeDev && v.isDev && v.devScopeKnown)
166
+ continue;
167
+ const key = `${v.ecosystem}\0${v.package}`;
168
+ if (!transitive.has(key))
169
+ transitive.set(key, []);
170
+ transitive.get(key).push(v);
171
+ }
172
+ for (const entries of transitive.values()) {
173
+ const first = entries[0];
174
+ const fixedVersions = entries.map((e) => e.fixedVersion).filter((f) => Boolean(f));
175
+ const platformActive = !entries.every((e) => !e.platformActive);
176
+ const reasons = [
177
+ `${entries.length} known CVE${entries.length !== 1 ? "s" : ""} (${entries.map((e) => e.vulnId).join(", ")})`,
178
+ "Transitive dependency — not declared in your manifest; the fix arrives via a parent package update or a lockfile refresh",
179
+ ];
180
+ if (!platformActive) {
181
+ reasons.push("Affected code is gated behind a target not active on this platform");
182
+ }
183
+ recommendations.push({
184
+ package: first.package,
185
+ ecosystem: first.ecosystem,
186
+ currentVersion: first.currentVersion,
187
+ targetVersion: fixedVersions.length > 0 ? maxSemver(fixedVersions) : null,
188
+ upgradeType: "unknown",
189
+ risk: severityToRisk(entries.map((e) => e.severity)),
190
+ reasons,
191
+ breaking: false,
192
+ isDev: entries.every((e) => e.isDev),
193
+ scope: "transitive",
194
+ action: "waiting_on_upstream",
195
+ platformActive,
196
+ });
197
+ }
198
+ }
128
199
  // Filter by risk threshold
129
- const riskLevels = { low: 0, medium: 1, high: 2, critical: 3 };
130
200
  const threshold = params.risk_threshold ?? "all";
131
201
  const filtered = threshold === "all"
132
202
  ? recommendations
133
- : recommendations.filter((r) => riskLevels[r.risk] >= riskLevels[threshold]);
134
- // Sort: critical > high > medium > low, then by breaking (non-breaking first for quick wins)
203
+ : recommendations.filter((r) => RISK_LEVELS[r.risk] >= RISK_LEVELS[threshold]);
204
+ // Sort: critical > high > medium > low, then fixable-now (direct) before
205
+ // waiting-on-upstream (transitive), then non-breaking first (quick wins).
135
206
  filtered.sort((a, b) => {
136
- const riskDiff = riskLevels[b.risk] - riskLevels[a.risk];
207
+ const riskDiff = RISK_LEVELS[b.risk] - RISK_LEVELS[a.risk];
137
208
  if (riskDiff !== 0)
138
209
  return riskDiff;
210
+ if (a.scope !== b.scope)
211
+ return a.scope === "direct" ? -1 : 1;
139
212
  if (a.breaking !== b.breaking)
140
213
  return a.breaking ? 1 : -1;
141
214
  return 0;
142
215
  });
143
216
  const maxRecs = params.max_recommendations ?? 20;
144
217
  const limited = filtered.slice(0, maxRecs);
145
- const quickWins = limited.filter((r) => !r.breaking).length;
146
- const breakingChanges = limited.filter((r) => r.breaking).length;
218
+ const direct = limited.filter((r) => r.scope === "direct");
219
+ const quickWins = direct.filter((r) => !r.breaking).length;
220
+ const breakingChanges = direct.filter((r) => r.breaking).length;
221
+ const waitingOnUpstream = limited.filter((r) => r.scope === "transitive").length;
147
222
  // Summary
148
223
  const parts = [];
149
- parts.push(`${limited.length} upgrade${limited.length !== 1 ? "s" : ""} recommended`);
150
- if (quickWins > 0)
151
- parts.push(`${quickWins} quick win${quickWins !== 1 ? "s" : ""} (patch/minor)`);
152
- if (breakingChanges > 0)
153
- parts.push(`${breakingChanges} breaking change${breakingChanges !== 1 ? "s" : ""} (major)`);
224
+ parts.push(`${limited.length} recommendation${limited.length !== 1 ? "s" : ""}`);
225
+ if (direct.length > 0)
226
+ parts.push(`${direct.length} fixable directly (${quickWins} quick win${quickWins !== 1 ? "s" : ""}, ${breakingChanges} breaking)`);
227
+ if (waitingOnUpstream > 0)
228
+ parts.push(`${waitingOnUpstream} transitive, waiting on upstream`);
154
229
  const criticalCount = limited.filter((r) => r.risk === "critical").length;
155
230
  if (criticalCount > 0)
156
231
  parts.push(`${criticalCount} critical`);
232
+ if (!vulnerabilityDataAvailable) {
233
+ parts.push("CVE data not loaded — run vulnerability_scan first for a security-aware plan");
234
+ }
157
235
  return {
158
236
  generatedAt: new Date().toISOString(),
159
- projectPath: process.cwd(),
237
+ projectPath: liveIntel.getProjectRoot() ?? vulnResult?.projectPath ?? process.cwd(),
160
238
  totalDeps: deps.length,
161
239
  recommendations: limited,
162
240
  summary: parts.join(". ") + ".",
163
241
  quickWins,
164
242
  breakingChanges,
243
+ waitingOnUpstream,
244
+ vulnerabilityDataAvailable,
245
+ provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
165
246
  };
166
247
  }
167
248
  //# sourceMappingURL=upgrade-planner.js.map
@@ -42,9 +42,11 @@ export declare const vulnerabilityScanTool: {
42
42
  export declare function executeVulnerabilityScan(db: FourDADatabase, params: VulnerabilityScanParams, liveIntelligence: LiveIntelligence | null): Promise<{
43
43
  scan_time: string;
44
44
  project: string;
45
+ host_platform: string;
45
46
  ecosystems_scanned: string[];
46
47
  total_dependencies_scanned: number;
47
48
  total_vulnerable_packages: number;
49
+ platform_inactive_packages: number;
48
50
  by_severity: {
49
51
  critical: number;
50
52
  high: number;
@@ -59,6 +61,26 @@ export declare function executeVulnerabilityScan(db: FourDADatabase, params: Vul
59
61
  is_dev: boolean;
60
62
  is_direct: boolean;
61
63
  dev_scope_known: boolean;
64
+ target: string | null;
65
+ platform_active: boolean;
66
+ vuln_id: string;
67
+ aliases: string[];
68
+ severity: "critical" | "high" | "medium" | "low" | "unknown";
69
+ cvss_score: number | null;
70
+ summary: string;
71
+ fixed_version: string | null;
72
+ published: string;
73
+ references: string[];
74
+ }[];
75
+ platform_inactive_vulnerabilities: {
76
+ package: string;
77
+ current_version: string;
78
+ ecosystem: import("../live/types.js").OsvEcosystem;
79
+ is_dev: boolean;
80
+ is_direct: boolean;
81
+ dev_scope_known: boolean;
82
+ target: string | null;
83
+ platform_active: boolean;
62
84
  vuln_id: string;
63
85
  aliases: string[];
64
86
  severity: "critical" | "high" | "medium" | "low" | "unknown";
@@ -76,6 +98,7 @@ export declare function executeVulnerabilityScan(db: FourDADatabase, params: Vul
76
98
  cached: boolean;
77
99
  offline: boolean;
78
100
  privacy: string;
101
+ relevance: string;
79
102
  };
80
103
  } | {
81
104
  error: null;
@@ -91,9 +114,11 @@ export declare function executeVulnerabilityScan(db: FourDADatabase, params: Vul
91
114
  note: string;
92
115
  scan_time: string;
93
116
  project: string;
117
+ host_platform: string;
94
118
  ecosystems_scanned: string[];
95
119
  total_dependencies_scanned: number;
96
120
  total_vulnerable_packages: number;
121
+ platform_inactive_packages: number;
97
122
  by_severity: {
98
123
  critical: number;
99
124
  high: number;
@@ -108,6 +133,26 @@ export declare function executeVulnerabilityScan(db: FourDADatabase, params: Vul
108
133
  is_dev: boolean;
109
134
  is_direct: boolean;
110
135
  dev_scope_known: boolean;
136
+ target: string | null;
137
+ platform_active: boolean;
138
+ vuln_id: string;
139
+ aliases: string[];
140
+ severity: "critical" | "high" | "medium" | "low" | "unknown";
141
+ cvss_score: number | null;
142
+ summary: string;
143
+ fixed_version: string | null;
144
+ published: string;
145
+ references: string[];
146
+ }[];
147
+ platform_inactive_vulnerabilities: {
148
+ package: string;
149
+ current_version: string;
150
+ ecosystem: import("../live/types.js").OsvEcosystem;
151
+ is_dev: boolean;
152
+ is_direct: boolean;
153
+ dev_scope_known: boolean;
154
+ target: string | null;
155
+ platform_active: boolean;
111
156
  vuln_id: string;
112
157
  aliases: string[];
113
158
  severity: "critical" | "high" | "medium" | "low" | "unknown";
@@ -125,6 +170,7 @@ export declare function executeVulnerabilityScan(db: FourDADatabase, params: Vul
125
170
  cached: boolean;
126
171
  offline: boolean;
127
172
  privacy: string;
173
+ relevance: string;
128
174
  };
129
175
  error?: undefined;
130
176
  offline?: undefined;
@@ -10,6 +10,8 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { LiveIntelligence } from "../live/index.js";
12
12
  import { scanCurrentProject } from "../project-scanner.js";
13
+ import { hostTarget } from "../live/platform.js";
14
+ import { maxSemver } from "../live/semver-utils.js";
13
15
  export const vulnerabilityScanTool = {
14
16
  name: "vulnerability_scan",
15
17
  description: "Scan project dependencies for known vulnerabilities (CVEs) using OSV.dev. " +
@@ -60,7 +62,7 @@ export async function executeVulnerabilityScan(db, params, liveIntelligence) {
60
62
  if (normalizedPath(projectPath) !== normalizedPath(process.cwd())) {
61
63
  const scan = scanCurrentProject(projectPath);
62
64
  scanner = new LiveIntelligence(db.getRawDb());
63
- scanner.initFromMultiEcosystem(projectPath, scan.depsByEcosystem);
65
+ scanner.initFromMultiEcosystem(projectPath, scan.depsByEcosystem, scan.depTargets);
64
66
  }
65
67
  if (!scanner.isInitialized()) {
66
68
  return {
@@ -121,47 +123,54 @@ function formatResult(result, severityFilter) {
121
123
  }
122
124
  // Sort: critical first, then high, etc.
123
125
  vulns.sort((a, b) => (SEVERITY_ORDER[b.severity] || 0) - (SEVERITY_ORDER[a.severity] || 0));
124
- // Generate recommendations
126
+ // Relevance split: advisories on deps NOT active on the host platform are not
127
+ // actionable for this user (e.g. a Linux-only crate's CVE shown to a Windows
128
+ // dev). Keep them, but separated and labeled so they don't pollute the signal.
129
+ const active = vulns.filter((v) => v.platformActive !== false);
130
+ const inactive = vulns.filter((v) => v.platformActive === false);
131
+ // Recommendations are derived from platform-active vulns only.
125
132
  const recommendations = [];
126
133
  const byPackage = new Map();
127
- for (const v of vulns) {
134
+ for (const v of active) {
128
135
  if (!byPackage.has(v.package))
129
136
  byPackage.set(v.package, []);
130
137
  byPackage.get(v.package).push(v);
131
138
  }
132
139
  for (const [pkg, pkgVulns] of byPackage) {
133
140
  const highest = pkgVulns[0];
134
- const fixVersion = pkgVulns.find((v) => v.fixedVersion)?.fixedVersion;
135
- if (fixVersion) {
136
- recommendations.push(`Upgrade ${pkg} ${highest.currentVersion} \u2192 ${fixVersion} (fixes ${pkgVulns.length} ${highest.severity} severity CVE${pkgVulns.length > 1 ? "s" : ""})`);
141
+ const breakdown = severityBreakdown(pkgVulns);
142
+ // Target the HIGHEST published fix so the upgrade resolves every fixable CVE,
143
+ // not just the first one. (The old code picked the first fix version, which
144
+ // could leave higher-version-gated CVEs unfixed while claiming to fix them.)
145
+ const target = maxSemver(pkgVulns.map((v) => v.fixedVersion).filter((v) => Boolean(v)));
146
+ const total = pkgVulns.length;
147
+ const fixable = pkgVulns.filter((v) => v.fixedVersion).length;
148
+ if (target) {
149
+ const scope = fixable === total
150
+ ? `${total} CVE${total > 1 ? "s" : ""}`
151
+ : `${fixable} of ${total} CVEs (${total - fixable} with no published fix)`;
152
+ recommendations.push(`Upgrade ${pkg} ${highest.currentVersion} \u2192 ${target} \u2014 resolves ${scope} [${breakdown}]`);
137
153
  }
138
154
  else {
139
- recommendations.push(`Review ${pkg} ${highest.currentVersion} \u2014 ${pkgVulns.length} known vulnerabilit${pkgVulns.length > 1 ? "ies" : "y"}, no fix version published`);
155
+ recommendations.push(`Review ${pkg} ${highest.currentVersion} \u2014 ${total} known vulnerabilit${total > 1 ? "ies" : "y"} [${breakdown}], no fix version published`);
140
156
  }
141
157
  }
158
+ const host = hostTarget();
159
+ const activePackages = new Set(active.map((v) => v.package)).size;
142
160
  return {
143
161
  scan_time: result.scannedAt,
144
162
  project: result.projectPath.split(/[/\\]/).pop() || result.projectPath,
163
+ host_platform: `${host.os}/${host.arch}`,
145
164
  ecosystems_scanned: result.ecosystemsScanned,
146
165
  total_dependencies_scanned: result.totalScanned,
147
- total_vulnerable_packages: result.totalVulnerable,
166
+ total_vulnerable_packages: activePackages,
167
+ platform_inactive_packages: result.platformInactiveVulnerable,
148
168
  by_severity: result.bySeverity,
149
- vulnerabilities: vulns.map((v) => ({
150
- package: v.package,
151
- current_version: v.currentVersion,
152
- ecosystem: v.ecosystem,
153
- is_dev: v.isDev,
154
- is_direct: v.isDirect ?? null,
155
- dev_scope_known: v.devScopeKnown ?? false,
156
- vuln_id: v.vulnId,
157
- aliases: v.aliases,
158
- severity: v.severity,
159
- cvss_score: v.cvssScore,
160
- summary: v.summary,
161
- fixed_version: v.fixedVersion,
162
- published: v.published,
163
- references: v.references,
164
- })),
169
+ vulnerabilities: active.map(formatVuln),
170
+ // Advisories suppressed from the main list because the affected crate is not
171
+ // built on this host (e.g. cfg(not(windows)) deps on Windows). Surfaced for
172
+ // cross-platform builds, clearly labeled with the gating target.
173
+ platform_inactive_vulnerabilities: inactive.map(formatVuln),
165
174
  recommendations,
166
175
  clean_dependencies: result.cleanCount,
167
176
  scan_duration_ms: result.scanDurationMs,
@@ -170,7 +179,38 @@ function formatResult(result, severityFilter) {
170
179
  cached: result.cached,
171
180
  offline: result.offline,
172
181
  privacy: "Only package names and versions were sent (public manifest data)",
182
+ relevance: "Advisories are filtered to the host platform; inactive-target advisories are listed separately.",
173
183
  },
174
184
  };
175
185
  }
186
+ /** "1 critical, 3 high, 2 medium" — severity-ordered, omitting empty buckets. */
187
+ function severityBreakdown(vulns) {
188
+ const counts = {};
189
+ for (const v of vulns)
190
+ counts[v.severity] = (counts[v.severity] || 0) + 1;
191
+ return ["critical", "high", "medium", "low", "unknown"]
192
+ .filter((s) => counts[s])
193
+ .map((s) => `${counts[s]} ${s}`)
194
+ .join(", ");
195
+ }
196
+ function formatVuln(v) {
197
+ return {
198
+ package: v.package,
199
+ current_version: v.currentVersion,
200
+ ecosystem: v.ecosystem,
201
+ is_dev: v.isDev,
202
+ is_direct: v.isDirect ?? null,
203
+ dev_scope_known: v.devScopeKnown ?? false,
204
+ target: v.target,
205
+ platform_active: v.platformActive,
206
+ vuln_id: v.vulnId,
207
+ aliases: v.aliases,
208
+ severity: v.severity,
209
+ cvss_score: v.cvssScore,
210
+ summary: v.summary,
211
+ fixed_version: v.fixedVersion,
212
+ published: v.published,
213
+ references: v.references,
214
+ };
215
+ }
176
216
  //# sourceMappingURL=vulnerability-scan.js.map
@@ -50,6 +50,8 @@ interface WhatShouldIKnowResult {
50
50
  reason: string;
51
51
  };
52
52
  summary: string;
53
+ /** How relevant_wisdom was retrieved: "hybrid" when an embedding provider is active. */
54
+ wisdom_recall_mode: "hybrid" | "ranked_lexical";
53
55
  }
54
56
  export declare const whatShouldIKnowTool: {
55
57
  name: string;
@@ -72,6 +74,6 @@ export declare const whatShouldIKnowTool: {
72
74
  required: string[];
73
75
  };
74
76
  };
75
- export declare function executeWhatShouldIKnow(db: FourDADatabase, params: WhatShouldIKnowParams): WhatShouldIKnowResult;
77
+ export declare function executeWhatShouldIKnow(db: FourDADatabase, params: WhatShouldIKnowParams): WhatShouldIKnowResult | Promise<WhatShouldIKnowResult>;
76
78
  export {};
77
79
  //# sourceMappingURL=what-should-i-know.d.ts.map