@4da/mcp-server 5.0.4 → 6.0.1
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 +698 -0
- package/README.md +70 -39
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -58
- package/dist/db.js +226 -299
- 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 +12 -1
- package/dist/live/osv-scanner.js +50 -15
- 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 -136
- package/package.json +35 -35
- 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
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One severity rule for every surface (AD-046).
|
|
3
|
+
*
|
|
4
|
+
* The desktop app grades an advisory by where the vulnerable package sits in
|
|
5
|
+
* the dependency graph, and every 4DA surface presents that one grade. This
|
|
6
|
+
* module is the MCP server's copy of the rule. Its Rust twin is
|
|
7
|
+
* `osv::identity::scope_adjusted_urgency` (src-tauri/src/osv/identity.rs):
|
|
8
|
+
* change one, change both, or the MCP server and the app disagree about the
|
|
9
|
+
* same advisory again.
|
|
10
|
+
*
|
|
11
|
+
* Measured 2026-09-10: `vulnerability_scan` graded `sandbox@3.1.2` (a
|
|
12
|
+
* transitive of paddle-webhook, dev/runtime scope unknown) CRITICAL
|
|
13
|
+
* (`by_severity.critical: 1`, and the briefing read "CRITICAL: Sandbox
|
|
14
|
+
* Breakout") while the app graded the same advisory High.
|
|
15
|
+
*
|
|
16
|
+
* The rule, applied in this order:
|
|
17
|
+
* (a) transitive-only clamps critical to high;
|
|
18
|
+
* (b) dev-only drops ONE level: critical to high, high to medium, medium to
|
|
19
|
+
* low; low stays low.
|
|
20
|
+
* `unknown` stays `unknown` through both steps. Unknown dev scope is not dev:
|
|
21
|
+
* no discount without evidence. The advisory's own tier is never discarded;
|
|
22
|
+
* surfaces carry it beside the presented grade as `advisory_severity`.
|
|
23
|
+
*/
|
|
24
|
+
import type { VulnerabilityEntry } from "./types.js";
|
|
25
|
+
export type SeverityTier = VulnerabilityEntry["severity"];
|
|
26
|
+
export interface DependencyScope {
|
|
27
|
+
isDirect: boolean;
|
|
28
|
+
isDev: boolean;
|
|
29
|
+
devScopeKnown: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Ordering for sorts and "at or above" filters; unknown ranks lowest. */
|
|
32
|
+
export declare const SEVERITY_RANK: Record<SeverityTier, number>;
|
|
33
|
+
/**
|
|
34
|
+
* THE rule. Twin of the Rust `osv::identity::scope_adjusted_urgency`.
|
|
35
|
+
*
|
|
36
|
+
* direct runtime critical -> critical; transitive runtime critical -> high;
|
|
37
|
+
* direct dev critical -> high; transitive dev critical -> medium;
|
|
38
|
+
* direct dev high -> medium; transitive dev high -> medium;
|
|
39
|
+
* transitive runtime high -> high.
|
|
40
|
+
*/
|
|
41
|
+
export declare function scopeAdjustedSeverity(advisory: SeverityTier, scope: DependencyScope): SeverityTier;
|
|
42
|
+
type ScopedEntry = Pick<VulnerabilityEntry, "severity" | "isDirect" | "isDev" | "devScopeKnown">;
|
|
43
|
+
/**
|
|
44
|
+
* The grade a surface presents for one scan entry. A missing `isDirect`
|
|
45
|
+
* (never produced today; defensive for foreign cache rows) is treated as
|
|
46
|
+
* direct, and a missing `devScopeKnown` as unknown: both mean no discount.
|
|
47
|
+
*/
|
|
48
|
+
export declare function presentedSeverity(v: ScopedEntry): SeverityTier;
|
|
49
|
+
/**
|
|
50
|
+
* Why the presented grade differs from the advisory's own tier, in words, or
|
|
51
|
+
* null when it does not differ.
|
|
52
|
+
*/
|
|
53
|
+
export declare function scopeAdjustmentReason(v: ScopedEntry): string | null;
|
|
54
|
+
export type SeverityCounts = Record<SeverityTier, number>;
|
|
55
|
+
/** Count entries per tier, using whichever tier function the caller names. */
|
|
56
|
+
export declare function countBySeverity<T>(entries: T[], tierOf: (entry: T) => SeverityTier): SeverityCounts;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* One severity rule for every surface (AD-046).
|
|
4
|
+
*
|
|
5
|
+
* The desktop app grades an advisory by where the vulnerable package sits in
|
|
6
|
+
* the dependency graph, and every 4DA surface presents that one grade. This
|
|
7
|
+
* module is the MCP server's copy of the rule. Its Rust twin is
|
|
8
|
+
* `osv::identity::scope_adjusted_urgency` (src-tauri/src/osv/identity.rs):
|
|
9
|
+
* change one, change both, or the MCP server and the app disagree about the
|
|
10
|
+
* same advisory again.
|
|
11
|
+
*
|
|
12
|
+
* Measured 2026-09-10: `vulnerability_scan` graded `sandbox@3.1.2` (a
|
|
13
|
+
* transitive of paddle-webhook, dev/runtime scope unknown) CRITICAL
|
|
14
|
+
* (`by_severity.critical: 1`, and the briefing read "CRITICAL: Sandbox
|
|
15
|
+
* Breakout") while the app graded the same advisory High.
|
|
16
|
+
*
|
|
17
|
+
* The rule, applied in this order:
|
|
18
|
+
* (a) transitive-only clamps critical to high;
|
|
19
|
+
* (b) dev-only drops ONE level: critical to high, high to medium, medium to
|
|
20
|
+
* low; low stays low.
|
|
21
|
+
* `unknown` stays `unknown` through both steps. Unknown dev scope is not dev:
|
|
22
|
+
* no discount without evidence. The advisory's own tier is never discarded;
|
|
23
|
+
* surfaces carry it beside the presented grade as `advisory_severity`.
|
|
24
|
+
*/
|
|
25
|
+
const ONE_LEVEL_DOWN = {
|
|
26
|
+
critical: "high",
|
|
27
|
+
high: "medium",
|
|
28
|
+
medium: "low",
|
|
29
|
+
low: "low",
|
|
30
|
+
unknown: "unknown",
|
|
31
|
+
};
|
|
32
|
+
/** Ordering for sorts and "at or above" filters; unknown ranks lowest. */
|
|
33
|
+
export const SEVERITY_RANK = {
|
|
34
|
+
critical: 4,
|
|
35
|
+
high: 3,
|
|
36
|
+
medium: 2,
|
|
37
|
+
low: 1,
|
|
38
|
+
unknown: 0,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* THE rule. Twin of the Rust `osv::identity::scope_adjusted_urgency`.
|
|
42
|
+
*
|
|
43
|
+
* direct runtime critical -> critical; transitive runtime critical -> high;
|
|
44
|
+
* direct dev critical -> high; transitive dev critical -> medium;
|
|
45
|
+
* direct dev high -> medium; transitive dev high -> medium;
|
|
46
|
+
* transitive runtime high -> high.
|
|
47
|
+
*/
|
|
48
|
+
export function scopeAdjustedSeverity(advisory, scope) {
|
|
49
|
+
let tier = advisory;
|
|
50
|
+
if (!scope.isDirect && tier === "critical")
|
|
51
|
+
tier = "high";
|
|
52
|
+
if (scope.devScopeKnown && scope.isDev)
|
|
53
|
+
tier = ONE_LEVEL_DOWN[tier];
|
|
54
|
+
return tier;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The grade a surface presents for one scan entry. A missing `isDirect`
|
|
58
|
+
* (never produced today; defensive for foreign cache rows) is treated as
|
|
59
|
+
* direct, and a missing `devScopeKnown` as unknown: both mean no discount.
|
|
60
|
+
*/
|
|
61
|
+
export function presentedSeverity(v) {
|
|
62
|
+
return scopeAdjustedSeverity(v.severity, {
|
|
63
|
+
isDirect: v.isDirect !== false,
|
|
64
|
+
isDev: v.isDev === true,
|
|
65
|
+
devScopeKnown: v.devScopeKnown === true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Why the presented grade differs from the advisory's own tier, in words, or
|
|
70
|
+
* null when it does not differ.
|
|
71
|
+
*/
|
|
72
|
+
export function scopeAdjustmentReason(v) {
|
|
73
|
+
if (presentedSeverity(v) === v.severity)
|
|
74
|
+
return null;
|
|
75
|
+
const transitive = v.isDirect === false;
|
|
76
|
+
const dev = v.devScopeKnown === true && v.isDev === true;
|
|
77
|
+
if (transitive && dev)
|
|
78
|
+
return "transitive, dev-only dependency";
|
|
79
|
+
if (dev)
|
|
80
|
+
return "dev-only dependency";
|
|
81
|
+
return "transitive-only dependency";
|
|
82
|
+
}
|
|
83
|
+
/** Count entries per tier, using whichever tier function the caller names. */
|
|
84
|
+
export function countBySeverity(entries, tierOf) {
|
|
85
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 };
|
|
86
|
+
for (const entry of entries)
|
|
87
|
+
counts[tierOf(entry)]++;
|
|
88
|
+
return counts;
|
|
89
|
+
}
|
package/dist/live/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist" | "Pub";
|
|
2
|
+
/** The reinstall command that brings node_modules back in line with a directory's lockfile. */
|
|
3
|
+
export type InstallFixCommand = "pnpm install" | "npm ci" | "yarn install" | "bun install";
|
|
2
4
|
export interface ResolvedDependency {
|
|
3
5
|
name: string;
|
|
4
6
|
version: string | null;
|
|
@@ -10,6 +12,56 @@ export interface ResolvedDependency {
|
|
|
10
12
|
target: string | null;
|
|
11
13
|
/** False when `target` is not active on the host platform (advisory is not relevant). */
|
|
12
14
|
platformActive: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Manifest directories this exact (ecosystem, name, version) resolved from.
|
|
17
|
+
*
|
|
18
|
+
* A scan root can span several independent workspaces — `src-tauri/`,
|
|
19
|
+
* `relay/`, `victauri-gauntlet/` all sit under one repo and pin their own
|
|
20
|
+
* versions. Without this, dedupe collapsed them and the report named a
|
|
21
|
+
* vulnerable version with no way to say WHICH workspace pinned it, so a
|
|
22
|
+
* patched primary crate looked vulnerable because a sibling lagged.
|
|
23
|
+
*/
|
|
24
|
+
sourceDirs: string[];
|
|
25
|
+
/**
|
|
26
|
+
* npm DIRECT deps resolved from a lockfile only: the version node_modules
|
|
27
|
+
* actually holds (walking up for hoisted workspaces), or null when it is not
|
|
28
|
+
* installed or unreadable. Undefined when the check did not run (another
|
|
29
|
+
* ecosystem, no node_modules, or versions read from a manifest).
|
|
30
|
+
*/
|
|
31
|
+
installedVersion?: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Set on the extra audit entry that asks OSV about an INSTALLED version the
|
|
34
|
+
* lockfile does not pin: the lockfile's version of the same package. The
|
|
35
|
+
* entry exists so `vulnerable_installed` is OSV's answer, not a guess.
|
|
36
|
+
*/
|
|
37
|
+
installDriftOf?: string;
|
|
38
|
+
/** On drift entries: the command that reinstalls from the lockfile. */
|
|
39
|
+
installFix?: InstallFixCommand;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One npm direct dependency whose installed copy differs from its lockfile.
|
|
43
|
+
* `dir` is the manifest directory, spelled as the resolution group spells it.
|
|
44
|
+
*/
|
|
45
|
+
export interface InstallDriftRecord {
|
|
46
|
+
package: string;
|
|
47
|
+
dir: string;
|
|
48
|
+
lockfileVersion: string;
|
|
49
|
+
installedVersion: string;
|
|
50
|
+
fix: InstallFixCommand;
|
|
51
|
+
isDev: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** A file one resolution group read its versions from. */
|
|
54
|
+
export interface ResolutionSourceRecord {
|
|
55
|
+
/** Absolute path. */
|
|
56
|
+
path: string;
|
|
57
|
+
kind: "lockfile" | "manifest";
|
|
58
|
+
/** Modification time when it was read, or null if it vanished since. */
|
|
59
|
+
mtimeMs: number | null;
|
|
60
|
+
}
|
|
61
|
+
/** When the dependency set in use was assembled, and from which files. */
|
|
62
|
+
export interface ResolutionProvenance {
|
|
63
|
+
resolvedAt: string;
|
|
64
|
+
sources: ResolutionSourceRecord[];
|
|
13
65
|
}
|
|
14
66
|
/**
|
|
15
67
|
* An OSV record. The `/v1/querybatch` endpoint returns only `{ id, modified }`
|
|
@@ -71,6 +123,19 @@ export interface VulnerabilityEntry {
|
|
|
71
123
|
target: string | null;
|
|
72
124
|
/** False when the affected dep is not active on the host platform. */
|
|
73
125
|
platformActive: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Manifest directories that pin this vulnerable version. Answers the first
|
|
128
|
+
* question a reader asks — *which project do I go fix?* — which a flat
|
|
129
|
+
* package-name list cannot.
|
|
130
|
+
*/
|
|
131
|
+
sourceDirs: string[];
|
|
132
|
+
/**
|
|
133
|
+
* Set when this row is about an INSTALLED version the lockfile does not pin
|
|
134
|
+
* (node_modules drift): the lockfile's version of the same package.
|
|
135
|
+
*/
|
|
136
|
+
installDriftOf?: string;
|
|
137
|
+
/** On drift rows: the command that reinstalls from the lockfile. */
|
|
138
|
+
installFix?: InstallFixCommand;
|
|
74
139
|
}
|
|
75
140
|
export interface VulnerabilityScanResult {
|
|
76
141
|
scannedAt: string;
|
|
@@ -126,6 +191,12 @@ export interface RegistryPackageInfo {
|
|
|
126
191
|
weeklyDownloads: number | null;
|
|
127
192
|
isDev: boolean;
|
|
128
193
|
fetchError: string | null;
|
|
194
|
+
/**
|
|
195
|
+
* The version node_modules actually holds, present only when it differs
|
|
196
|
+
* from `currentVersion` (the lockfile's). Several differing installs across
|
|
197
|
+
* workspaces are listed comma-separated.
|
|
198
|
+
*/
|
|
199
|
+
installedVersion?: string;
|
|
129
200
|
}
|
|
130
201
|
export interface SemverDistance {
|
|
131
202
|
major: number;
|
|
@@ -140,7 +211,19 @@ export interface DependencyHealthResult {
|
|
|
140
211
|
totalDeps: number;
|
|
141
212
|
outdatedCount: number;
|
|
142
213
|
deprecatedCount: number;
|
|
214
|
+
/**
|
|
215
|
+
* Distinct packages with an advisory that is built on this host and is not a
|
|
216
|
+
* maintenance notice — the same set `vulnerability_scan` reports as
|
|
217
|
+
* `total_vulnerable_packages`, so the two tools agree over one scan.
|
|
218
|
+
*/
|
|
143
219
|
vulnerableCount: number;
|
|
220
|
+
/**
|
|
221
|
+
* Every advisory row the scan returned, before the platform and maintenance
|
|
222
|
+
* splits and before alias collapsing. Present so nothing is hidden by the
|
|
223
|
+
* `vulnerableCount` filter: the difference is platform-inactive packages and
|
|
224
|
+
* unmaintained-dependency notices.
|
|
225
|
+
*/
|
|
226
|
+
advisoryCount: number;
|
|
144
227
|
healthScore: number;
|
|
145
228
|
dependencies: RegistryPackageInfo[];
|
|
146
229
|
vulnerabilitySummary: {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network sources for `upgrade_impact`: registry version index, the target
|
|
3
|
+
* version's changelog (read from its registry archive) and OSV advisories.
|
|
4
|
+
*
|
|
5
|
+
* Privacy rule (Decision 1, option A): the only hosts contacted are the
|
|
6
|
+
* package's own registry (registry.npmjs.org; crates.io + static.crates.io)
|
|
7
|
+
* and api.osv.dev. A changelog is never fetched from GitHub or a docs site,
|
|
8
|
+
* even when the archive ships none — the report then hands the agent a
|
|
9
|
+
* `release_notes_url` built from registry metadata, and the agent decides
|
|
10
|
+
* whether to fetch it. Every request goes through `UpgradeNet`, which records
|
|
11
|
+
* the host so `_meta.sources` is a fact, not a claim, and which tests replace
|
|
12
|
+
* with a fake fetch.
|
|
13
|
+
*
|
|
14
|
+
* Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
|
|
15
|
+
* each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
|
|
16
|
+
* is large (vite's is ~39 MB, measured 2026-10-02), so only a reduced index
|
|
17
|
+
* is cached — never the raw document.
|
|
18
|
+
*/
|
|
19
|
+
import { type FetchFn } from "./package-archive.js";
|
|
20
|
+
import { type ChangelogSection } from "./changelog.js";
|
|
21
|
+
export type UpgradeEcosystem = "npm" | "crates.io";
|
|
22
|
+
/** crates.io refuses API requests without a descriptive User-Agent. */
|
|
23
|
+
export declare const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
|
|
24
|
+
export interface PublishedVersion {
|
|
25
|
+
version: string;
|
|
26
|
+
published: string | null;
|
|
27
|
+
deprecated?: string;
|
|
28
|
+
yanked?: boolean;
|
|
29
|
+
/** Archive URL (npm `dist.tarball`); crates archives are derived from name + version. */
|
|
30
|
+
tarball?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RegistryIndex {
|
|
33
|
+
/** The registry's canonical spelling (crates.io folds `-`/`_`). */
|
|
34
|
+
name: string;
|
|
35
|
+
ecosystem: UpgradeEcosystem;
|
|
36
|
+
versions: PublishedVersion[];
|
|
37
|
+
repository: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ChangelogResult {
|
|
40
|
+
found: boolean;
|
|
41
|
+
file?: string;
|
|
42
|
+
sections?: ChangelogSection[];
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The cache surface used here; `LiveCache` satisfies it. */
|
|
46
|
+
export interface UpgradeCache {
|
|
47
|
+
get<T>(key: string): T | null;
|
|
48
|
+
set(key: string, data: unknown, source: string, ttlSeconds: number): void;
|
|
49
|
+
}
|
|
50
|
+
/** Network + cache seam. `contacted` collects every host actually requested. */
|
|
51
|
+
export interface UpgradeNet {
|
|
52
|
+
fetch: FetchFn;
|
|
53
|
+
cache: UpgradeCache | null;
|
|
54
|
+
contacted: Set<string>;
|
|
55
|
+
}
|
|
56
|
+
export declare function createUpgradeNet(cache: UpgradeCache | null, fetchFn?: FetchFn): UpgradeNet;
|
|
57
|
+
export declare function isValidPackageName(name: string, ecosystem: UpgradeEcosystem): boolean;
|
|
58
|
+
/** Registry index with versions in ascending precedence order (unreadable versions dropped). Null = unknown package. */
|
|
59
|
+
export declare function getRegistryIndex(net: UpgradeNet, ecosystem: UpgradeEcosystem, name: string): Promise<RegistryIndex | null>;
|
|
60
|
+
/** The registry archive URL for one version, or null when it is not on the package's own registry. */
|
|
61
|
+
export declare function archiveUrl(index: RegistryIndex, version: PublishedVersion): string | null;
|
|
62
|
+
/** Parsed changelog shipped inside one version's archive (cached 7 days per version). */
|
|
63
|
+
export declare function getChangelog(net: UpgradeNet, index: RegistryIndex, version: PublishedVersion): Promise<ChangelogResult>;
|
|
64
|
+
/** OSV advisory ids affecting one version, or null when OSV could not be reached. */
|
|
65
|
+
export declare function getOsvAdvisories(net: UpgradeNet, ecosystem: UpgradeEcosystem, name: string, version: string): Promise<string[] | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Release-notes page derived from the registry's own `repository` field: a
|
|
68
|
+
* GitHub repo becomes `https://github.com/<owner>/<repo>/releases`; any other
|
|
69
|
+
* https repository URL is returned as-is; anything else is null.
|
|
70
|
+
*/
|
|
71
|
+
export declare function releaseNotesUrl(repository: string | null): string | null;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Network sources for `upgrade_impact`: registry version index, the target
|
|
4
|
+
* version's changelog (read from its registry archive) and OSV advisories.
|
|
5
|
+
*
|
|
6
|
+
* Privacy rule (Decision 1, option A): the only hosts contacted are the
|
|
7
|
+
* package's own registry (registry.npmjs.org; crates.io + static.crates.io)
|
|
8
|
+
* and api.osv.dev. A changelog is never fetched from GitHub or a docs site,
|
|
9
|
+
* even when the archive ships none — the report then hands the agent a
|
|
10
|
+
* `release_notes_url` built from registry metadata, and the agent decides
|
|
11
|
+
* whether to fetch it. Every request goes through `UpgradeNet`, which records
|
|
12
|
+
* the host so `_meta.sources` is a fact, not a claim, and which tests replace
|
|
13
|
+
* with a fake fetch.
|
|
14
|
+
*
|
|
15
|
+
* Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
|
|
16
|
+
* each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
|
|
17
|
+
* is large (vite's is ~39 MB, measured 2026-10-02), so only a reduced index
|
|
18
|
+
* is cached — never the raw document.
|
|
19
|
+
*/
|
|
20
|
+
import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
|
|
21
|
+
import { ArchiveError, defaultFetch, fetchArchiveRootFiles } from "./package-archive.js";
|
|
22
|
+
import { findChangelogFile, isChangelogName, parseChangelog } from "./changelog.js";
|
|
23
|
+
/** crates.io refuses API requests without a descriptive User-Agent. */
|
|
24
|
+
export const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
|
|
25
|
+
const INDEX_TTL = 6 * 3600;
|
|
26
|
+
const CHANGELOG_TTL = 7 * 86_400;
|
|
27
|
+
const OSV_TTL = 3600;
|
|
28
|
+
const CACHE_SOURCE = "upgrade-impact";
|
|
29
|
+
const INDEX_TIMEOUT_MS = 30_000;
|
|
30
|
+
const OSV_TIMEOUT_MS = 10_000;
|
|
31
|
+
export function createUpgradeNet(cache, fetchFn = defaultFetch) {
|
|
32
|
+
const contacted = new Set();
|
|
33
|
+
return {
|
|
34
|
+
contacted,
|
|
35
|
+
cache,
|
|
36
|
+
fetch: (url, init, timeoutMs) => {
|
|
37
|
+
contacted.add(new URL(url).host);
|
|
38
|
+
return fetchFn(url, init, timeoutMs);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
|
|
43
|
+
const CRATE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
44
|
+
export function isValidPackageName(name, ecosystem) {
|
|
45
|
+
return ecosystem === "npm" ? NPM_NAME.test(name) && name.length <= 214 : CRATE_NAME.test(name);
|
|
46
|
+
}
|
|
47
|
+
async function cached(net, key, ttl, load) {
|
|
48
|
+
const hit = net.cache?.get(key);
|
|
49
|
+
if (hit !== null && hit !== undefined)
|
|
50
|
+
return hit;
|
|
51
|
+
const value = await load();
|
|
52
|
+
net.cache?.set(key, value, CACHE_SOURCE, ttl);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function repositoryUrl(repo) {
|
|
56
|
+
if (typeof repo === "string")
|
|
57
|
+
return repo;
|
|
58
|
+
if (repo && typeof repo === "object" && typeof repo.url === "string") {
|
|
59
|
+
return repo.url;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
async function loadNpmIndex(net, name) {
|
|
64
|
+
const url = `https://registry.npmjs.org/${name.replaceAll("/", "%2F")}`;
|
|
65
|
+
const res = await net.fetch(url, { headers: { Accept: "application/json" } }, INDEX_TIMEOUT_MS);
|
|
66
|
+
if (res.status === 404)
|
|
67
|
+
return null;
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`npm registry HTTP ${res.status} for ${name}`);
|
|
70
|
+
const doc = (await res.json());
|
|
71
|
+
const versions = Object.entries(doc.versions ?? {}).map(([version, meta]) => {
|
|
72
|
+
const entry = { version, published: doc.time?.[version] ?? null };
|
|
73
|
+
if (typeof meta.deprecated === "string" && meta.deprecated)
|
|
74
|
+
entry.deprecated = meta.deprecated;
|
|
75
|
+
if (meta.dist?.tarball)
|
|
76
|
+
entry.tarball = meta.dist.tarball;
|
|
77
|
+
return entry;
|
|
78
|
+
});
|
|
79
|
+
return { name: doc.name ?? name, ecosystem: "npm", versions, repository: repositoryUrl(doc.repository) };
|
|
80
|
+
}
|
|
81
|
+
async function loadCratesIndex(net, name) {
|
|
82
|
+
const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
|
|
83
|
+
const headers = { Accept: "application/json", "User-Agent": CRATES_USER_AGENT };
|
|
84
|
+
const res = await net.fetch(url, { headers }, INDEX_TIMEOUT_MS);
|
|
85
|
+
if (res.status === 404)
|
|
86
|
+
return null;
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
throw new Error(`crates.io HTTP ${res.status} for ${name}`);
|
|
89
|
+
const doc = (await res.json());
|
|
90
|
+
const versions = (doc.versions ?? [])
|
|
91
|
+
.filter((v) => typeof v.num === "string")
|
|
92
|
+
.map((v) => {
|
|
93
|
+
const entry = { version: v.num, published: v.created_at ?? null };
|
|
94
|
+
if (v.yanked)
|
|
95
|
+
entry.yanked = true;
|
|
96
|
+
return entry;
|
|
97
|
+
});
|
|
98
|
+
return { name: doc.crate?.name ?? name, ecosystem: "crates.io", versions, repository: doc.crate?.repository ?? null };
|
|
99
|
+
}
|
|
100
|
+
/** Registry index with versions in ascending precedence order (unreadable versions dropped). Null = unknown package. */
|
|
101
|
+
export async function getRegistryIndex(net, ecosystem, name) {
|
|
102
|
+
const key = `upgrade-impact:index:${ecosystem}:${ecosystem === "crates.io" ? name.replace(/-/g, "_") : name}`;
|
|
103
|
+
const index = await cached(net, key, INDEX_TTL, () => ecosystem === "npm" ? loadNpmIndex(net, name) : loadCratesIndex(net, name));
|
|
104
|
+
if (!index)
|
|
105
|
+
return null;
|
|
106
|
+
const versions = index.versions
|
|
107
|
+
.filter((v) => parseSemverPrecedence(v.version) !== null)
|
|
108
|
+
.sort((a, b) => compareVersionPrecedence(a.version, b.version) ?? 0);
|
|
109
|
+
return { ...index, versions };
|
|
110
|
+
}
|
|
111
|
+
const ARCHIVE_HOSTS = new Set(["registry.npmjs.org", "static.crates.io"]);
|
|
112
|
+
/** The registry archive URL for one version, or null when it is not on the package's own registry. */
|
|
113
|
+
export function archiveUrl(index, version) {
|
|
114
|
+
const url = index.ecosystem === "crates.io"
|
|
115
|
+
? `https://static.crates.io/crates/${index.name}/${index.name}-${version.version}.crate`
|
|
116
|
+
: version.tarball;
|
|
117
|
+
if (!url)
|
|
118
|
+
return null;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = new URL(url);
|
|
121
|
+
return parsed.protocol === "https:" && ARCHIVE_HOSTS.has(parsed.host) ? url : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Parsed changelog shipped inside one version's archive (cached 7 days per version). */
|
|
128
|
+
export async function getChangelog(net, index, version) {
|
|
129
|
+
const url = archiveUrl(index, version);
|
|
130
|
+
if (!url) {
|
|
131
|
+
return { found: false, reason: "the version's archive is not hosted on the package's own registry" };
|
|
132
|
+
}
|
|
133
|
+
const key = `upgrade-impact:changelog:${index.ecosystem}:${index.name}:${version.version}`;
|
|
134
|
+
const hit = net.cache?.get(key);
|
|
135
|
+
if (hit)
|
|
136
|
+
return hit;
|
|
137
|
+
const headers = index.ecosystem === "crates.io" ? { "User-Agent": CRATES_USER_AGENT } : {};
|
|
138
|
+
let files;
|
|
139
|
+
try {
|
|
140
|
+
files = await fetchArchiveRootFiles(url, isChangelogName, net.fetch, headers);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const result = { found: false, reason: `could not read the ${version.version} archive: ${err.message}` };
|
|
144
|
+
// A published archive never changes, so a cap or format refusal is permanent;
|
|
145
|
+
// a network failure is not, and must not be remembered for a week.
|
|
146
|
+
if (err instanceof ArchiveError)
|
|
147
|
+
net.cache?.set(key, result, CACHE_SOURCE, CHANGELOG_TTL);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
const file = findChangelogFile(files.keys());
|
|
151
|
+
const sections = file ? parseChangelog(files.get(file) ?? "") : [];
|
|
152
|
+
const result = !file
|
|
153
|
+
? { found: false, reason: `the ${version.version} archive ships no changelog file` }
|
|
154
|
+
: sections.length === 0
|
|
155
|
+
? { found: false, file, reason: `${file} has no version headings this parser recognises` }
|
|
156
|
+
: { found: true, file, sections };
|
|
157
|
+
net.cache?.set(key, result, CACHE_SOURCE, CHANGELOG_TTL);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
/** OSV advisory ids affecting one version, or null when OSV could not be reached. */
|
|
161
|
+
export async function getOsvAdvisories(net, ecosystem, name, version) {
|
|
162
|
+
const key = `upgrade-impact:osv:${ecosystem}:${name}:${version}`;
|
|
163
|
+
const hit = net.cache?.get(key);
|
|
164
|
+
if (hit)
|
|
165
|
+
return hit;
|
|
166
|
+
try {
|
|
167
|
+
const res = await net.fetch("https://api.osv.dev/v1/query", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
171
|
+
}, OSV_TIMEOUT_MS);
|
|
172
|
+
if (!res.ok)
|
|
173
|
+
return null;
|
|
174
|
+
const body = (await res.json());
|
|
175
|
+
const ids = (body.vulns ?? []).map((v) => v.id).filter((id) => typeof id === "string");
|
|
176
|
+
net.cache?.set(key, ids, CACHE_SOURCE, OSV_TTL);
|
|
177
|
+
return ids;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Release-notes page derived from the registry's own `repository` field: a
|
|
185
|
+
* GitHub repo becomes `https://github.com/<owner>/<repo>/releases`; any other
|
|
186
|
+
* https repository URL is returned as-is; anything else is null.
|
|
187
|
+
*/
|
|
188
|
+
export function releaseNotesUrl(repository) {
|
|
189
|
+
if (!repository)
|
|
190
|
+
return null;
|
|
191
|
+
const raw = repository.trim();
|
|
192
|
+
const shorthand = /^(?:github:)?([\w.-]+)\/([\w.-]+)$/.exec(raw);
|
|
193
|
+
const gh = shorthand ?? /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/#?].*)?$/i.exec(raw);
|
|
194
|
+
if (gh)
|
|
195
|
+
return `https://github.com/${gh[1]}/${gh[2].replace(/\.git$/, "")}/releases`;
|
|
196
|
+
const https = raw.replace(/^git\+/, "");
|
|
197
|
+
return /^https:\/\//i.test(https) ? https.replace(/\.git$/, "") : null;
|
|
198
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version ordering per ecosystem, for advisory ranges and fix selection.
|
|
3
|
+
*
|
|
4
|
+
* `compareSemver` reads MAJOR.MINOR.PATCH and calls anything else "equal".
|
|
5
|
+
* That is wrong outside npm and crates.io, and it was wrong in practice
|
|
6
|
+
* (measured 2026-10-02 against pip-audit and osv-scanner):
|
|
7
|
+
* - PyPI versions are PEP 440, not semver. `5.1` and `4.2` are ordinary
|
|
8
|
+
* releases; read as "equal" to everything, django 3.2.0 was sent to the 5.1
|
|
9
|
+
* line's fix (5.1.1) instead of its own line's (3.2.25 / 4.2.16).
|
|
10
|
+
* - Go versions carry a `v`, OSV's Go ranges do not, and pseudo-versions
|
|
11
|
+
* (`v0.0.0-20200622213623-75b288015ac9`) order as semver prereleases.
|
|
12
|
+
*
|
|
13
|
+
* `0` is OSV's "since the beginning" bound and orders below every version.
|
|
14
|
+
*/
|
|
15
|
+
import type { OsvEcosystem } from "./types.js";
|
|
16
|
+
interface Pep440 {
|
|
17
|
+
epoch: number;
|
|
18
|
+
release: number[];
|
|
19
|
+
/** Pre-release: [rank, n] with a=0, b=1, rc=2; null for none. */
|
|
20
|
+
pre: [number, number] | null;
|
|
21
|
+
post: number | null;
|
|
22
|
+
dev: number | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function parsePep440(version: string): Pep440 | null;
|
|
25
|
+
export declare function comparePep440(a: Pep440, b: Pep440): number;
|
|
26
|
+
/** Whether `version` is readable for this ecosystem's ordering. */
|
|
27
|
+
export declare function isComparable(version: string, ecosystem: OsvEcosystem | string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
|
|
30
|
+
* (the same contract as compareSemver, so an unreadable bound never decides).
|
|
31
|
+
*/
|
|
32
|
+
export declare function compareVersions(a: string, b: string, ecosystem: OsvEcosystem | string): number;
|
|
33
|
+
/** Highest of `versions` in this ecosystem's order, or null when empty. */
|
|
34
|
+
export declare function maxVersion(versions: string[], ecosystem: OsvEcosystem | string): string | null;
|
|
35
|
+
/** Package-name equality as the ecosystem's registry defines it. */
|
|
36
|
+
export declare function samePackageName(a: string, b: string, ecosystem: OsvEcosystem | string): boolean;
|
|
37
|
+
/** Whether a user-typed package name (`package` parameters) names this dependency: registry rules, then case-insensitive. */
|
|
38
|
+
export declare function namesPackage(query: string, name: string, ecosystem: OsvEcosystem | string): boolean;
|
|
39
|
+
export {};
|