@4da/mcp-server 5.0.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
package/dist/live/index.js
CHANGED
|
@@ -12,12 +12,20 @@ import { LiveCache } from "./cache.js";
|
|
|
12
12
|
import { RateLimiter, DEFAULT_RATE_LIMITS } from "./rate-limiter.js";
|
|
13
13
|
import { OsvScanner } from "./osv-scanner.js";
|
|
14
14
|
import { HNFetcher } from "./hn-fetcher.js";
|
|
15
|
-
import { resolveAuditVersions, resolveVersions } from "./version-resolver.js";
|
|
16
15
|
import { NpmRegistry } from "./npm-registry.js";
|
|
17
16
|
import { CratesRegistry } from "./crates-registry.js";
|
|
18
17
|
import { PyPIRegistry } from "./pypi-registry.js";
|
|
19
18
|
import { GoRegistry } from "./go-registry.js";
|
|
19
|
+
import { NpmPackumentReader } from "./npm-packument.js";
|
|
20
|
+
import { CratesVersionsReader } from "./crates-versions.js";
|
|
21
|
+
import { fetchRegistryHealthFor } from "./registry-health.js";
|
|
22
|
+
import { commonPathRoot, dedupeDependencies, emptyVulnResult } from "./dependency-set.js";
|
|
23
|
+
import { groupIsStale, resolveGroup } from "./resolution.js";
|
|
24
|
+
import { applyInstanceDevScope } from "./dev-scope.js";
|
|
25
|
+
/** Minimum gap between vulnerability warmup attempts after an offline result. */
|
|
26
|
+
const WARMUP_RETRY_MS = 60_000;
|
|
20
27
|
export class LiveIntelligence {
|
|
28
|
+
db;
|
|
21
29
|
cache;
|
|
22
30
|
rateLimiter;
|
|
23
31
|
osvScanner;
|
|
@@ -26,14 +34,47 @@ export class LiveIntelligence {
|
|
|
26
34
|
cratesRegistry;
|
|
27
35
|
pypiRegistry;
|
|
28
36
|
goRegistry;
|
|
37
|
+
npmPackuments;
|
|
38
|
+
cratesVersions;
|
|
29
39
|
enabled;
|
|
30
40
|
lastVulnScan = null;
|
|
41
|
+
/** Whether the stored scan included direct devDependencies. */
|
|
42
|
+
lastVulnScanIncludesDev = false;
|
|
43
|
+
/**
|
|
44
|
+
* Order of scan REQUESTS. A scan's result is stored only if no later request
|
|
45
|
+
* has stored one already: the init warmup (runtime scope) used to finish
|
|
46
|
+
* after an agent's `include_dev` scan and replace it, so `upgrade_planner`
|
|
47
|
+
* read a scan without the dev dependencies the agent had just scanned and
|
|
48
|
+
* sent node-fetch 2.6.0 to the ESM-only 3.3.2 with no advisories (2 of 6
|
|
49
|
+
* runs, 2026-10-03 pre-publish verification).
|
|
50
|
+
*/
|
|
51
|
+
scanRequests = 0;
|
|
52
|
+
storedScanRequest = 0;
|
|
31
53
|
lastHeadlines = [];
|
|
32
54
|
resolvedDeps = [];
|
|
33
55
|
auditDeps = [];
|
|
34
56
|
initialized = false;
|
|
35
57
|
projectRoot = null;
|
|
58
|
+
/** One entry per resolution group: its inputs, result, and the file signatures that say when it went stale. */
|
|
59
|
+
groupResults = [];
|
|
60
|
+
/** When the dependency set in use was assembled: at init, or at the last re-resolution. */
|
|
61
|
+
resolvedAt = null;
|
|
62
|
+
/**
|
|
63
|
+
* Bumped by every re-resolution. A scan started under an older generation
|
|
64
|
+
* describes a dependency set that no longer exists: it may still be handed
|
|
65
|
+
* to the call that asked for it, but it is never stored or served later.
|
|
66
|
+
*/
|
|
67
|
+
generation = 0;
|
|
68
|
+
/**
|
|
69
|
+
* The background vulnerability scan started at server init. Tools that need
|
|
70
|
+
* scan data before answering (`what_should_i_know`) await this through
|
|
71
|
+
* `ensureVulnerabilities` instead of reading the cache and finding it empty.
|
|
72
|
+
*/
|
|
73
|
+
warmup = null;
|
|
74
|
+
/** When the last warmup came back offline; gates the retry so an offline host is not re-probed on every call. */
|
|
75
|
+
warmupFailedAt = null;
|
|
36
76
|
constructor(db) {
|
|
77
|
+
this.db = db;
|
|
37
78
|
this.enabled = process.env.FOURDA_OFFLINE !== "true";
|
|
38
79
|
this.cache = new LiveCache(db);
|
|
39
80
|
this.rateLimiter = new RateLimiter(DEFAULT_RATE_LIMITS);
|
|
@@ -43,16 +84,15 @@ export class LiveIntelligence {
|
|
|
43
84
|
this.cratesRegistry = new CratesRegistry(this.cache, this.rateLimiter);
|
|
44
85
|
this.pypiRegistry = new PyPIRegistry(this.cache, this.rateLimiter);
|
|
45
86
|
this.goRegistry = new GoRegistry(this.cache, this.rateLimiter);
|
|
87
|
+
this.npmPackuments = new NpmPackumentReader(this.cache, this.rateLimiter);
|
|
88
|
+
this.cratesVersions = new CratesVersionsReader(this.cache, this.rateLimiter);
|
|
46
89
|
}
|
|
47
90
|
/**
|
|
48
91
|
* Initialize with project data. Call once after project scan.
|
|
49
92
|
* @deprecated Use initFromMultiEcosystem for correct per-ecosystem resolution.
|
|
50
93
|
*/
|
|
51
94
|
initFromProject(projectPath, deps, devDeps, language) {
|
|
52
|
-
this.
|
|
53
|
-
this.auditDeps = resolveAuditVersions(projectPath, deps, devDeps, language);
|
|
54
|
-
this.projectRoot = projectPath;
|
|
55
|
-
this.initialized = true;
|
|
95
|
+
this.applyGroups([{ dir: projectPath, language, deps, devDeps, targets: {} }], projectPath);
|
|
56
96
|
}
|
|
57
97
|
/**
|
|
58
98
|
* Initialize with per-ecosystem project data. Resolves versions per language
|
|
@@ -60,16 +100,22 @@ export class LiveIntelligence {
|
|
|
60
100
|
* Call once after project scan.
|
|
61
101
|
*/
|
|
62
102
|
initFromMultiEcosystem(projectPath, depsByEcosystem, depTargets = {}) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
const groups = Object.entries(depsByEcosystem).map(([language, { deps, devDeps }]) => ({
|
|
104
|
+
dir: projectPath,
|
|
105
|
+
language,
|
|
106
|
+
deps,
|
|
107
|
+
devDeps,
|
|
108
|
+
targets: depTargets,
|
|
109
|
+
}));
|
|
110
|
+
this.applyGroups(groups, projectPath);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Initialize from every independently-locked project under `root` (see
|
|
114
|
+
* project-tree.ts): each group resolves from its own directory and keeps
|
|
115
|
+
* its manifest's platform targets; `root` is the reported project root.
|
|
116
|
+
*/
|
|
117
|
+
initFromProjectTree(root, groups) {
|
|
118
|
+
this.applyGroups(groups, root);
|
|
73
119
|
}
|
|
74
120
|
/**
|
|
75
121
|
* Initialize from dependency groups that each carry their own resolution
|
|
@@ -82,17 +128,67 @@ export class LiveIntelligence {
|
|
|
82
128
|
* is scanned once.
|
|
83
129
|
*/
|
|
84
130
|
initFromDependencyGroups(groups) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
93
|
-
this.projectRoot = commonPathRoot(groups.map((g) => g.dir));
|
|
131
|
+
// Each resolver stamps `sourceDirs: [dir]` at the point of resolution, so
|
|
132
|
+
// the dedupe can union provenance instead of discarding it.
|
|
133
|
+
this.applyGroups(groups.map((g) => ({ ...g, targets: {} })), commonPathRoot(groups.map((g) => g.dir)));
|
|
134
|
+
}
|
|
135
|
+
applyGroups(groups, projectRoot) {
|
|
136
|
+
this.groupResults = groups.map(resolveGroup);
|
|
137
|
+
this.rebuildDependencySet();
|
|
138
|
+
this.projectRoot = projectRoot;
|
|
94
139
|
this.initialized = true;
|
|
95
140
|
}
|
|
141
|
+
rebuildDependencySet() {
|
|
142
|
+
this.resolvedDeps = dedupeDependencies(this.groupResults.flatMap((r) => r.resolved));
|
|
143
|
+
this.auditDeps = dedupeDependencies(this.groupResults.flatMap((r) => r.audit));
|
|
144
|
+
this.resolvedAt = new Date().toISOString();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Re-resolve every group whose inputs changed since they were read (a
|
|
148
|
+
* lockfile rewritten, created or deleted; the fallback manifest; for npm,
|
|
149
|
+
* the node_modules install state), and drop every answer that described the
|
|
150
|
+
* old set: the stored scan and the warmup. Returns true when anything was
|
|
151
|
+
* re-resolved. Stat calls only, so it is cheap enough for the top of every
|
|
152
|
+
* tool call. A process that started before a `git pull` no longer answers
|
|
153
|
+
* for the dependency set it saw at startup.
|
|
154
|
+
*/
|
|
155
|
+
refreshIfLockfilesChanged() {
|
|
156
|
+
if (!this.initialized || this.groupResults.length === 0)
|
|
157
|
+
return false;
|
|
158
|
+
let changed = false;
|
|
159
|
+
this.groupResults = this.groupResults.map((result) => {
|
|
160
|
+
if (!groupIsStale(result))
|
|
161
|
+
return result;
|
|
162
|
+
changed = true;
|
|
163
|
+
return resolveGroup(result.group);
|
|
164
|
+
});
|
|
165
|
+
if (!changed)
|
|
166
|
+
return false;
|
|
167
|
+
this.rebuildDependencySet();
|
|
168
|
+
this.generation++;
|
|
169
|
+
this.lastVulnScan = null;
|
|
170
|
+
this.warmup = null;
|
|
171
|
+
this.warmupFailedAt = null;
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
/** When the dependency set in use was resolved, and from which files. Null before init. */
|
|
175
|
+
getResolutionProvenance() {
|
|
176
|
+
if (!this.initialized || !this.resolvedAt)
|
|
177
|
+
return null;
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const sources = [];
|
|
180
|
+
for (const result of this.groupResults) {
|
|
181
|
+
if (!result.source || seen.has(result.source.path))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(result.source.path);
|
|
184
|
+
sources.push(result.source);
|
|
185
|
+
}
|
|
186
|
+
return { resolvedAt: this.resolvedAt, sources };
|
|
187
|
+
}
|
|
188
|
+
/** npm direct dependencies whose installed copy differs from the lockfile, per manifest directory. */
|
|
189
|
+
getInstallDrift() {
|
|
190
|
+
return this.groupResults.flatMap((r) => r.drift);
|
|
191
|
+
}
|
|
96
192
|
/**
|
|
97
193
|
* Root directory the dependency set was resolved from, or null before init.
|
|
98
194
|
* For multi-manifest (grouped) init this is the deepest common ancestor of
|
|
@@ -104,31 +200,147 @@ export class LiveIntelligence {
|
|
|
104
200
|
}
|
|
105
201
|
/**
|
|
106
202
|
* Run vulnerability scan (returns cached if fresh, fetches otherwise).
|
|
203
|
+
*
|
|
204
|
+
* Without `includeDev`, only DIRECT devDependencies are left out (the
|
|
205
|
+
* documented contract). A transitive stays in whatever its scope: an
|
|
206
|
+
* unknown scope must not be hidden, and a known dev-only one is graded
|
|
207
|
+
* down by the shared severity rule, not dropped.
|
|
107
208
|
*/
|
|
108
209
|
async scanVulnerabilities(projectPath, options) {
|
|
109
210
|
if (!this.enabled) {
|
|
110
211
|
return emptyVulnResult(projectPath, true);
|
|
111
212
|
}
|
|
112
|
-
const
|
|
213
|
+
const scoped = options?.includeDev
|
|
113
214
|
? this.auditDeps
|
|
114
|
-
: this.auditDeps.filter((d) => !d.isDev);
|
|
215
|
+
: this.auditDeps.filter((d) => !(d.isDirect && d.isDev));
|
|
216
|
+
// Read at scan time, not init time: the app rewrites its inventory on
|
|
217
|
+
// every rescan, and the freshest determination is the one to grade by.
|
|
218
|
+
const deps = applyInstanceDevScope(this.db, scoped);
|
|
115
219
|
if (deps.length === 0) {
|
|
116
220
|
return emptyVulnResult(projectPath, false);
|
|
117
221
|
}
|
|
118
222
|
if (options?.forceRefresh) {
|
|
119
223
|
this.cache.invalidateSource("osv");
|
|
120
224
|
}
|
|
225
|
+
const generation = this.generation;
|
|
226
|
+
const request = ++this.scanRequests;
|
|
121
227
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
228
|
+
const result = await this.osvScanner.scan(deps, projectPath);
|
|
229
|
+
if (generation === this.generation && request > this.storedScanRequest) {
|
|
230
|
+
this.lastVulnScan = result;
|
|
231
|
+
this.lastVulnScanIncludesDev = options?.includeDev ?? false;
|
|
232
|
+
this.storedScanRequest = request;
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
124
235
|
}
|
|
125
236
|
catch {
|
|
126
237
|
// Network failure — return last known or empty
|
|
127
|
-
if (this.lastVulnScan)
|
|
238
|
+
if (generation === this.generation && this.lastVulnScan) {
|
|
128
239
|
return { ...this.lastVulnScan, offline: true, cached: true };
|
|
240
|
+
}
|
|
129
241
|
return emptyVulnResult(projectPath, true);
|
|
130
242
|
}
|
|
131
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Start the vulnerability scan in the background without blocking startup.
|
|
246
|
+
* Never rejects: a failed scan resolves to an empty OFFLINE result so the
|
|
247
|
+
* warmup can be awaited by anyone. Idempotent while a warmup is in flight.
|
|
248
|
+
*
|
|
249
|
+
* Before this existed, full-database mode initialised the dependency set
|
|
250
|
+
* and warmed headlines but never scanned, so `lastVulnScan` stayed null
|
|
251
|
+
* until some tool happened to call `vulnerability_scan` — and the first
|
|
252
|
+
* `what_should_i_know` of a session read an empty cache and reported
|
|
253
|
+
* "safe_to_delegate" for a task naming a package with an open advisory.
|
|
254
|
+
*/
|
|
255
|
+
startVulnerabilityWarmup(projectPath) {
|
|
256
|
+
if (this.warmup)
|
|
257
|
+
return;
|
|
258
|
+
this.warmupFailedAt = null;
|
|
259
|
+
this.warmup = this.scanVulnerabilities(projectPath).catch(() => emptyVulnResult(projectPath, true));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A usable vulnerability scan, or null. Re-resolves first when a lockfile
|
|
263
|
+
* changed. Returns the stored scan when one exists, otherwise awaits the
|
|
264
|
+
* warmup (starting one if none is running) for at most `timeoutMs`. Null
|
|
265
|
+
* means the scan is not available: disabled, still running past the
|
|
266
|
+
* deadline, finished offline (an offline scan makes no clean-dependency
|
|
267
|
+
* claims, so it cannot support a "safe" verdict), or overtaken by a
|
|
268
|
+
* re-resolution while it ran. Never throws. A timed-out warmup keeps running
|
|
269
|
+
* so a later call can use it.
|
|
270
|
+
*/
|
|
271
|
+
async ensureVulnerabilities(projectPath, timeoutMs, options) {
|
|
272
|
+
if (!this.enabled)
|
|
273
|
+
return null;
|
|
274
|
+
this.refreshIfLockfilesChanged();
|
|
275
|
+
const wantDev = options?.includeDev ?? false;
|
|
276
|
+
if (this.lastVulnScan && !this.lastVulnScan.offline && (!wantDev || this.lastVulnScanIncludesDev)) {
|
|
277
|
+
return this.lastVulnScan;
|
|
278
|
+
}
|
|
279
|
+
// A caller that needs direct devDependencies covered (a briefing whose task
|
|
280
|
+
// names a dev dependency) cannot use the runtime-scope warmup: it reported
|
|
281
|
+
// "vulnerabilities: []" for node-fetch 2.6.0, which has two advisories
|
|
282
|
+
// (2026-10-03 pre-publish verification). Scan at the wider scope, bounded.
|
|
283
|
+
if (wantDev)
|
|
284
|
+
return this.awaitScan(this.scanVulnerabilities(projectPath, { includeDev: true }), timeoutMs);
|
|
285
|
+
if (!this.warmup) {
|
|
286
|
+
if (this.warmupFailedAt !== null && Date.now() - this.warmupFailedAt < WARMUP_RETRY_MS) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
this.startVulnerabilityWarmup(projectPath);
|
|
290
|
+
}
|
|
291
|
+
const pending = this.warmup;
|
|
292
|
+
if (!pending)
|
|
293
|
+
return null;
|
|
294
|
+
const generation = this.generation;
|
|
295
|
+
let timer;
|
|
296
|
+
const deadline = new Promise((resolve) => {
|
|
297
|
+
timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const settled = await Promise.race([pending, deadline]);
|
|
301
|
+
if (settled === null)
|
|
302
|
+
return null; // still scanning; keep the warmup alive
|
|
303
|
+
// Re-resolved while waiting: that scan describes a set that no longer exists.
|
|
304
|
+
if (generation !== this.generation)
|
|
305
|
+
return null;
|
|
306
|
+
const result = this.lastVulnScan ?? settled;
|
|
307
|
+
if (result.offline) {
|
|
308
|
+
// Failed or rate-limited: allow a fresh attempt after the backoff.
|
|
309
|
+
this.warmup = null;
|
|
310
|
+
this.warmupFailedAt = Date.now();
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
if (timer !== undefined)
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** A scan's result within `timeoutMs`, or null when it times out, comes back offline, or is overtaken by a re-resolution. */
|
|
324
|
+
async awaitScan(pending, timeoutMs) {
|
|
325
|
+
const generation = this.generation;
|
|
326
|
+
let timer;
|
|
327
|
+
const deadline = new Promise((resolve) => {
|
|
328
|
+
timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
const settled = await Promise.race([pending, deadline]);
|
|
332
|
+
if (settled === null || generation !== this.generation || settled.offline)
|
|
333
|
+
return null;
|
|
334
|
+
return settled;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
if (timer !== undefined)
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
132
344
|
/**
|
|
133
345
|
* Fetch relevant headlines for the user's tech stack.
|
|
134
346
|
*/
|
|
@@ -149,6 +361,10 @@ export class LiveIntelligence {
|
|
|
149
361
|
getVulnerabilities() {
|
|
150
362
|
return this.lastVulnScan;
|
|
151
363
|
}
|
|
364
|
+
/** Whether the stored scan covered direct devDependencies (false when there is none). */
|
|
365
|
+
lastScanIncludesDev() {
|
|
366
|
+
return this.lastVulnScan !== null && this.lastVulnScanIncludesDev;
|
|
367
|
+
}
|
|
152
368
|
/**
|
|
153
369
|
* Get last headlines (from cache/memory, no network).
|
|
154
370
|
*/
|
|
@@ -171,63 +387,29 @@ export class LiveIntelligence {
|
|
|
171
387
|
return this.initialized;
|
|
172
388
|
}
|
|
173
389
|
async fetchRegistryHealth(deps) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
390
|
+
// Registries are read at call time (not captured at construction) so a
|
|
391
|
+
// test can substitute one.
|
|
392
|
+
return fetchRegistryHealthFor(deps, { npm: this.npmRegistry, crates: this.cratesRegistry, pypi: this.pypiRegistry, go: this.goRegistry }, this.enabled);
|
|
393
|
+
}
|
|
394
|
+
/** Per-release registry readers for dependency_check (name-only requests). */
|
|
395
|
+
getReleaseSources() {
|
|
396
|
+
return { npm: this.npmPackuments, "crates.io": this.cratesVersions };
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* OSV advisories for explicit (ecosystem, name, version) triples that need
|
|
400
|
+
* not be in the project, e.g. a version an agent is about to install. Same
|
|
401
|
+
* scanner, cache and rate limit as vulnerability_scan; never touches the
|
|
402
|
+
* stored project scan. Disabled or failed yields an `offline` result.
|
|
403
|
+
*/
|
|
404
|
+
async queryAdvisories(deps) {
|
|
405
|
+
if (!this.enabled)
|
|
406
|
+
return emptyVulnResult("dependency_check", true);
|
|
407
|
+
try {
|
|
408
|
+
return await this.osvScanner.scan(deps, "dependency_check");
|
|
181
409
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
case "npm": return this.npmRegistry;
|
|
185
|
-
case "crates.io": return this.cratesRegistry;
|
|
186
|
-
case "PyPI": return this.pypiRegistry;
|
|
187
|
-
case "Go": return this.goRegistry;
|
|
188
|
-
default: return null;
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
const results = await Promise.all(deps.map(async (dep) => {
|
|
192
|
-
const registry = registryForEcosystem(dep.ecosystem);
|
|
193
|
-
if (!registry) {
|
|
194
|
-
return {
|
|
195
|
-
name: dep.name, ecosystem: dep.ecosystem, currentVersion: dep.version,
|
|
196
|
-
latestVersion: null, latestStableVersion: null, versionsBehind: null,
|
|
197
|
-
deprecated: false, deprecationMessage: null, lastPublished: null,
|
|
198
|
-
license: null, weeklyDownloads: null, isDev: dep.isDev,
|
|
199
|
-
fetchError: `No registry fetcher for ${dep.ecosystem}`,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
return await registry.getPackageInfo(dep.name, dep.version, dep.isDev);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
return {
|
|
207
|
-
name: dep.name, ecosystem: dep.ecosystem, currentVersion: dep.version,
|
|
208
|
-
latestVersion: null, latestStableVersion: null, versionsBehind: null,
|
|
209
|
-
deprecated: false, deprecationMessage: null, lastPublished: null,
|
|
210
|
-
license: null, weeklyDownloads: null, isDev: dep.isDev,
|
|
211
|
-
fetchError: "Registry fetch failed",
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}));
|
|
215
|
-
// Bulk fetch npm downloads for npm deps
|
|
216
|
-
const npmDeps = deps.filter((d) => d.ecosystem === "npm");
|
|
217
|
-
if (npmDeps.length > 0) {
|
|
218
|
-
try {
|
|
219
|
-
const downloads = await this.npmRegistry.getBulkDownloads(npmDeps.map((d) => d.name));
|
|
220
|
-
for (const result of results) {
|
|
221
|
-
if (result.ecosystem === "npm" && downloads.has(result.name)) {
|
|
222
|
-
result.weeklyDownloads = downloads.get(result.name) || null;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Downloads are nice-to-have, not critical
|
|
228
|
-
}
|
|
410
|
+
catch {
|
|
411
|
+
return emptyVulnResult("dependency_check", true);
|
|
229
412
|
}
|
|
230
|
-
return results;
|
|
231
413
|
}
|
|
232
414
|
getStatus() {
|
|
233
415
|
return {
|
|
@@ -240,58 +422,3 @@ export class LiveIntelligence {
|
|
|
240
422
|
};
|
|
241
423
|
}
|
|
242
424
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Deepest common ancestor of a set of directories (segment-wise, both slash
|
|
245
|
-
* styles). Null for an empty set; a single dir is its own root.
|
|
246
|
-
*/
|
|
247
|
-
function commonPathRoot(dirs) {
|
|
248
|
-
if (dirs.length === 0)
|
|
249
|
-
return null;
|
|
250
|
-
const split = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
251
|
-
let common = split(dirs[0]);
|
|
252
|
-
for (const dir of dirs.slice(1)) {
|
|
253
|
-
const parts = split(dir);
|
|
254
|
-
let i = 0;
|
|
255
|
-
while (i < common.length && i < parts.length && common[i].toLowerCase() === parts[i].toLowerCase())
|
|
256
|
-
i++;
|
|
257
|
-
common = common.slice(0, i);
|
|
258
|
-
if (common.length === 0)
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
return common.length > 0 ? common.join("/") : null;
|
|
262
|
-
}
|
|
263
|
-
function dedupeDependencies(deps) {
|
|
264
|
-
const unique = new Map();
|
|
265
|
-
for (const dep of deps) {
|
|
266
|
-
const key = `${dep.ecosystem}\0${dep.name}\0${dep.version ?? ""}`;
|
|
267
|
-
const existing = unique.get(key);
|
|
268
|
-
if (existing) {
|
|
269
|
-
existing.isDirect ||= dep.isDirect;
|
|
270
|
-
existing.devScopeKnown &&= dep.devScopeKnown;
|
|
271
|
-
existing.isDev = existing.devScopeKnown && existing.isDev && dep.isDev;
|
|
272
|
-
// A crate reachable via ANY active path is active; keep a target label if present.
|
|
273
|
-
existing.platformActive ||= dep.platformActive;
|
|
274
|
-
existing.target = existing.target ?? dep.target;
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
unique.set(key, { ...dep });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return [...unique.values()];
|
|
281
|
-
}
|
|
282
|
-
function emptyVulnResult(projectPath, offline) {
|
|
283
|
-
return {
|
|
284
|
-
scannedAt: new Date().toISOString(),
|
|
285
|
-
projectPath,
|
|
286
|
-
ecosystemsScanned: [],
|
|
287
|
-
totalScanned: 0,
|
|
288
|
-
totalVulnerable: 0,
|
|
289
|
-
platformInactiveVulnerable: 0,
|
|
290
|
-
bySeverity: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 },
|
|
291
|
-
vulnerabilities: [],
|
|
292
|
-
cleanCount: 0,
|
|
293
|
-
scanDurationMs: 0,
|
|
294
|
-
cached: false,
|
|
295
|
-
offline,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install drift: what node_modules holds versus what the lockfile pins.
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-09-10: mcp-4da-server/pnpm-lock.yaml pinned hono 4.13.5 (PR
|
|
5
|
+
* #632, the fix for CVE-2026-84363/-84364/-84365) while
|
|
6
|
+
* mcp-4da-server/node_modules/hono held 4.13.1 for 25 days, because
|
|
7
|
+
* activation ran `pnpm install` only at the repo root. Every 4DA surface
|
|
8
|
+
* reported hono fixed, because every surface read the lockfile. A lockfile
|
|
9
|
+
* says what SHOULD be installed; only node_modules says what IS, and the
|
|
10
|
+
* installed copy is the one that runs.
|
|
11
|
+
*/
|
|
12
|
+
import { type FileSignature } from "./file-signature.js";
|
|
13
|
+
import type { InstallDriftRecord, InstallFixCommand, ResolvedDependency } from "./types.js";
|
|
14
|
+
/** The reinstall command for the lockfile a directory resolved from, or null for a non-npm file. */
|
|
15
|
+
export declare function installFixFor(lockfilePath: string): InstallFixCommand | null;
|
|
16
|
+
/**
|
|
17
|
+
* `<d>/node_modules/<name>/package.json` for the nearest `d` in the lookup
|
|
18
|
+
* chain that has it (scoped names included), or null. This is the copy
|
|
19
|
+
* Node's resolver would load from `dir`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findInstalledManifest(dir: string, name: string, chain?: string[]): string | null;
|
|
22
|
+
/** The installed version of `name` as seen from `dir`, or null when not installed or unreadable. Never throws. */
|
|
23
|
+
export declare function readInstalledVersion(dir: string, name: string): string | null;
|
|
24
|
+
export interface InstallCheck {
|
|
25
|
+
/** The direct deps with `installedVersion` filled in; a new array, inputs untouched. */
|
|
26
|
+
resolved: ResolvedDependency[];
|
|
27
|
+
/** One record per direct dep whose installed copy differs from its lockfile version. */
|
|
28
|
+
drift: InstallDriftRecord[];
|
|
29
|
+
/** Audit entries for installed versions the lockfile does not pin, flagged `installDriftOf`. */
|
|
30
|
+
driftAuditDeps: ResolvedDependency[];
|
|
31
|
+
/** Signatures, taken BEFORE reading, of every file whose change means the install state moved. */
|
|
32
|
+
signatures: Map<string, FileSignature>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compare each npm direct dependency's lockfile version with node_modules.
|
|
36
|
+
*
|
|
37
|
+
* Skipped entirely when `dir` has no node_modules of its own: a checkout that
|
|
38
|
+
* was never installed has no installed copy to disagree with the lockfile,
|
|
39
|
+
* and borrowing a parent project's node_modules would compare against a
|
|
40
|
+
* different project's install. The node_modules path is still tracked, so an
|
|
41
|
+
* install that creates it is noticed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkInstallState(dir: string, resolved: ResolvedDependency[], lockfilePath: string): InstallCheck;
|