@4da/mcp-server 6.1.0 → 6.2.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 +183 -0
- package/README.md +14 -9
- package/dist/db.d.ts +15 -0
- package/dist/db.js +37 -2
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +122 -12
- package/dist/live/changelog.d.ts +8 -4
- package/dist/live/changelog.js +67 -29
- package/dist/live/fix-paths.js +1 -1
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +4 -5
- package/dist/live/index.js +7 -1
- package/dist/live/osv-ranges.d.ts +6 -0
- package/dist/live/osv-ranges.js +3 -0
- package/dist/live/package-archive.d.ts +2 -0
- package/dist/live/package-archive.js +7 -4
- package/dist/live/pypi-releases.d.ts +44 -0
- package/dist/live/pypi-releases.js +103 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/upgrade-go.d.ts +62 -0
- package/dist/live/upgrade-go.js +130 -0
- package/dist/live/upgrade-pypi.d.ts +42 -0
- package/dist/live/upgrade-pypi.js +85 -0
- package/dist/live/upgrade-sources.d.ts +28 -11
- package/dist/live/upgrade-sources.js +86 -19
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/schema-registry.js +3 -3
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/tool-dispatch.d.ts +2 -0
- package/dist/tool-dispatch.js +41 -1
- package/dist/tools/briefing-stored-advisories.js +1 -1
- package/dist/tools/briefing-task-scope.js +2 -2
- package/dist/tools/dependency-check-signals.d.ts +16 -5
- package/dist/tools/dependency-check-signals.js +46 -18
- package/dist/tools/dependency-check-versions.d.ts +94 -0
- package/dist/tools/dependency-check-versions.js +213 -0
- package/dist/tools/dependency-check.d.ts +26 -10
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/knowledge-gap-citations.js +1 -1
- package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
- package/dist/tools/upgrade-impact-callsites.js +23 -5
- package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
- package/dist/tools/upgrade-impact-changelog.js +79 -0
- package/dist/tools/upgrade-impact-imports.d.ts +42 -0
- package/dist/tools/upgrade-impact-imports.js +260 -0
- package/dist/tools/upgrade-impact-report.d.ts +13 -0
- package/dist/tools/upgrade-impact-report.js +13 -4
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +235 -84
- package/dist/tools/what-should-i-know.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* dependency_check version handling across npm, crates.io, PyPI and Go:
|
|
4
|
+
* reading `to` as an exact version, "latest", or a constraint, and finding
|
|
5
|
+
* the safe releases a constraint admits.
|
|
6
|
+
*
|
|
7
|
+
* Why a constraint: an agent told "pin ws 7.x" writes the 7.x release it
|
|
8
|
+
* remembers. In the G7 agent evaluation (2026-10-11) that was ws 7.5.10 and
|
|
9
|
+
* body-parser 1.20.3, both affected by an advisory their next patch fixes.
|
|
10
|
+
* Asked with `to: "7.x"`, the check answers with the smallest and newest
|
|
11
|
+
* release in the constraint that no known advisory affects.
|
|
12
|
+
*
|
|
13
|
+
* Constraint syntax is the ecosystem's own (npm ranges, Cargo requirements,
|
|
14
|
+
* PEP 440 specifiers), plus the shorthand every agent writes: "7.x", "3.1.*",
|
|
15
|
+
* "v1" for a Go major. Pure functions, no I/O.
|
|
16
|
+
*/
|
|
17
|
+
import { cargoReqAdmits, npmRangeAdmits } from "../live/range-satisfies.js";
|
|
18
|
+
import { advisoryAffects, isPrereleaseVersion } from "../live/osv-ranges.js";
|
|
19
|
+
import { compareVersions, parsePep440 } from "../live/version-compare.js";
|
|
20
|
+
import { parseSemverPrecedence } from "../live/semver-precedence.js";
|
|
21
|
+
import { GO_VERSION, goVersion } from "../live/go-releases.js";
|
|
22
|
+
const FULL_SEMVER = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
23
|
+
/** Read a `to` value; null when it is neither a version nor a constraint this ecosystem understands. */
|
|
24
|
+
export function parseTarget(ecosystem, raw) {
|
|
25
|
+
const text = raw.trim();
|
|
26
|
+
if (!text)
|
|
27
|
+
return null;
|
|
28
|
+
if (text.toLowerCase() === "latest")
|
|
29
|
+
return { kind: "latest" };
|
|
30
|
+
switch (ecosystem) {
|
|
31
|
+
case "npm":
|
|
32
|
+
case "crates.io": {
|
|
33
|
+
// "1.2.3", and the exact-pin spellings "=1.2.3" / "==1.2.3" / "v1.2.3".
|
|
34
|
+
const bare = text.replace(/^={1,2}\s*/, "");
|
|
35
|
+
if (FULL_SEMVER.test(bare))
|
|
36
|
+
return { kind: "exact", version: bare.replace(/^v(?=\d)/, "") };
|
|
37
|
+
return rangeAdmits(ecosystem, text, "1.0.0") === null ? null : { kind: "range", range: text };
|
|
38
|
+
}
|
|
39
|
+
case "PyPI": {
|
|
40
|
+
const pinned = /^={2,3}\s*([^,*\s]+)$/.exec(text);
|
|
41
|
+
if (pinned && parsePep440(pinned[1]))
|
|
42
|
+
return { kind: "exact", version: pinned[1] };
|
|
43
|
+
const p = parsePep440(text);
|
|
44
|
+
if (p) {
|
|
45
|
+
// "3.1.4" is a release; "3.1" may be a release (PyPI has "2.0"s) or mean the 3.1 series.
|
|
46
|
+
const plain = p.release.length < 3 && p.pre === null && p.post === null && p.dev === null && !text.includes("+");
|
|
47
|
+
return plain ? { kind: "exact_or_series", version: text, range: `==${text}.*` } : { kind: "exact", version: text };
|
|
48
|
+
}
|
|
49
|
+
const range = pypiShorthand(text);
|
|
50
|
+
return pep440SpecAdmits(range, "1.0") === null ? null : { kind: "range", range };
|
|
51
|
+
}
|
|
52
|
+
case "Go": {
|
|
53
|
+
const v = goVersion(text);
|
|
54
|
+
if (GO_VERSION.test(v))
|
|
55
|
+
return { kind: "exact", version: v };
|
|
56
|
+
return rangeAdmits("Go", text, "v1.0.0") === null ? null : { kind: "range", range: text };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** "3.1.x" / "3.x" / "3.1.*" (no operator) -> "==3.1.*". */
|
|
61
|
+
function pypiShorthand(text) {
|
|
62
|
+
const m = /^v?(\d+(?:\.\d+)*)\.[xX*]$/.exec(text);
|
|
63
|
+
return m ? `==${m[1]}.*` : text;
|
|
64
|
+
}
|
|
65
|
+
/** Whether a constraint admits a version, in the ecosystem's own syntax. Null = unreadable constraint or version. */
|
|
66
|
+
export function rangeAdmits(ecosystem, range, version) {
|
|
67
|
+
switch (ecosystem) {
|
|
68
|
+
case "npm":
|
|
69
|
+
return npmRangeAdmits(range, version);
|
|
70
|
+
case "crates.io":
|
|
71
|
+
// Cargo's own syntax first ("0.17" means ^0.17); then npm-style spellings agents also write ("0.17.x").
|
|
72
|
+
return cargoReqAdmits(range, version) ?? npmRangeAdmits(range, version);
|
|
73
|
+
case "PyPI":
|
|
74
|
+
return pep440SpecAdmits(pypiShorthand(range.trim()), version);
|
|
75
|
+
case "Go": {
|
|
76
|
+
// Go has no range syntax of its own; read semver ranges over `v`-less versions ("v1" = the v1 major).
|
|
77
|
+
const unv = range.replace(/(^|[\s,<>=~^|-])v(?=\d)/g, "$1");
|
|
78
|
+
return npmRangeAdmits(unv.replace(/,\s*/g, " "), version.replace(/^v(?=\d)/, "").replace(/\+incompatible$/, ""));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* PEP 440 version specifiers: comma-separated clauses of `==`, `!=` (both
|
|
84
|
+
* with optional `.*` prefix matching), `>=`, `<=`, `>`, `<`, `~=` and `===`.
|
|
85
|
+
* Null when a clause or the version is unreadable.
|
|
86
|
+
*/
|
|
87
|
+
export function pep440SpecAdmits(spec, version) {
|
|
88
|
+
const v = parsePep440(version);
|
|
89
|
+
if (!v)
|
|
90
|
+
return null;
|
|
91
|
+
const clauses = spec.split(",").map((c) => c.trim()).filter(Boolean);
|
|
92
|
+
if (clauses.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
for (const clause of clauses) {
|
|
95
|
+
const m = /^(===|~=|==|!=|>=|<=|>|<)\s*(\S+)$/.exec(clause);
|
|
96
|
+
if (!m)
|
|
97
|
+
return null;
|
|
98
|
+
const [, op, rawTarget] = m;
|
|
99
|
+
if (op === "===") {
|
|
100
|
+
if (rawTarget !== version)
|
|
101
|
+
return false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const prefix = rawTarget.endsWith(".*");
|
|
105
|
+
const target = parsePep440(prefix ? rawTarget.slice(0, -2) : rawTarget);
|
|
106
|
+
if (!target)
|
|
107
|
+
return null;
|
|
108
|
+
let ok;
|
|
109
|
+
if (prefix) {
|
|
110
|
+
if (op !== "==" && op !== "!=")
|
|
111
|
+
return null;
|
|
112
|
+
ok = matchesPrefix(v, target) === (op === "==");
|
|
113
|
+
}
|
|
114
|
+
else if (op === "~=") {
|
|
115
|
+
if (target.release.length < 2)
|
|
116
|
+
return null;
|
|
117
|
+
const series = { ...target, release: target.release.slice(0, -1), pre: null, post: null, dev: null };
|
|
118
|
+
ok = cmp(v, target) >= 0 && matchesPrefix(v, series);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const d = cmp(v, target);
|
|
122
|
+
ok = op === "==" ? d === 0 : op === "!=" ? d !== 0 : op === ">=" ? d >= 0 : op === "<=" ? d <= 0 : op === ">" ? d > 0 : d < 0;
|
|
123
|
+
}
|
|
124
|
+
if (!ok)
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
function cmp(a, b) {
|
|
130
|
+
return compareVersions(fmtPep440(a), fmtPep440(b), "PyPI");
|
|
131
|
+
}
|
|
132
|
+
function fmtPep440(v) {
|
|
133
|
+
const pre = v.pre ? `${["a", "b", "rc"][v.pre[0]]}${v.pre[1]}` : "";
|
|
134
|
+
return `${v.epoch ? `${v.epoch}!` : ""}${v.release.join(".")}${pre}${v.post !== null ? `.post${v.post}` : ""}${v.dev !== null ? `.dev${v.dev}` : ""}`;
|
|
135
|
+
}
|
|
136
|
+
/** `==X.Y.*`: same epoch and the release starts with X.Y (missing parts read as 0). */
|
|
137
|
+
function matchesPrefix(v, prefix) {
|
|
138
|
+
if (v.epoch !== prefix.epoch)
|
|
139
|
+
return false;
|
|
140
|
+
return prefix.release.every((n, i) => (v.release[i] ?? 0) === n);
|
|
141
|
+
}
|
|
142
|
+
/** Order two versions of one ecosystem. */
|
|
143
|
+
export function compareIn(ecosystem, a, b) {
|
|
144
|
+
return compareVersions(a, b, ecosystem);
|
|
145
|
+
}
|
|
146
|
+
export function isPrereleaseIn(ecosystem, version) {
|
|
147
|
+
if (ecosystem === "Go" && /\d{14}-[0-9a-f]{12}/.test(version))
|
|
148
|
+
return true; // pseudo-version
|
|
149
|
+
return isPrereleaseVersion(version, ecosystem);
|
|
150
|
+
}
|
|
151
|
+
/** MAJOR.MINOR.PATCH of a version in any of the four ecosystems (PEP 440 release parts; missing ones 0). */
|
|
152
|
+
export function versionParts(ecosystem, version) {
|
|
153
|
+
if (ecosystem === "PyPI") {
|
|
154
|
+
const p = parsePep440(version);
|
|
155
|
+
return p ? { major: p.release[0] ?? 0, minor: p.release[1] ?? 0, patch: p.release[2] ?? 0 } : null;
|
|
156
|
+
}
|
|
157
|
+
const p = parseSemverPrecedence(version.replace(/\+incompatible$/, ""));
|
|
158
|
+
return p ? { major: p.major, minor: p.minor, patch: p.patch } : null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The release line a version belongs to, as a constraint: the same major (the
|
|
162
|
+
* same 0.minor below 1.0) on npm and crates.io; the same module major on Go,
|
|
163
|
+
* where v0 is one line (`go get -u` moves freely within a module path); the
|
|
164
|
+
* same X.Y series on PyPI, where many projects do not follow semver (Django
|
|
165
|
+
* 4.2 LTS, Jinja2 3.1).
|
|
166
|
+
*/
|
|
167
|
+
export function lineOf(ecosystem, version) {
|
|
168
|
+
const p = versionParts(ecosystem, version);
|
|
169
|
+
if (!p)
|
|
170
|
+
return null;
|
|
171
|
+
if (ecosystem === "PyPI")
|
|
172
|
+
return `==${p.major}.${p.minor}.*`;
|
|
173
|
+
if (ecosystem === "Go")
|
|
174
|
+
return `v${p.major}`;
|
|
175
|
+
return p.major === 0 ? `0.${p.minor}.x` : `${p.major}.x`;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The releases a constraint admits, judged against every advisory OSV.dev
|
|
179
|
+
* lists for the package. Withdrawn (yanked / unpublished / retracted) and
|
|
180
|
+
* deprecated releases are not candidates; neither are prereleases, unless
|
|
181
|
+
* the constraint admits nothing else. Maintenance notices do not make a
|
|
182
|
+
* release unsafe.
|
|
183
|
+
*/
|
|
184
|
+
export function checkRange(input) {
|
|
185
|
+
const { ecosystem, index, range, basis } = input;
|
|
186
|
+
const installable = Object.values(index.releases).filter((r) => !r.withdrawn && !r.deprecated).map((r) => r.version);
|
|
187
|
+
const admitted = installable.filter((v) => rangeAdmits(ecosystem, range, v) === true);
|
|
188
|
+
const stable = admitted.filter((v) => !isPrereleaseIn(ecosystem, v));
|
|
189
|
+
const candidates = (stable.length > 0 ? stable : admitted).sort((a, b) => compareIn(ecosystem, a, b));
|
|
190
|
+
const real = input.advisories.filter((a) => !a.maintenance);
|
|
191
|
+
const affecting = (v) => real.filter((a) => advisoryAffects(a, v, ecosystem)).map((a) => a.id);
|
|
192
|
+
const safe = candidates.filter((v) => affecting(v).length === 0);
|
|
193
|
+
const out = {
|
|
194
|
+
range,
|
|
195
|
+
basis,
|
|
196
|
+
releases_in_range: candidates.length,
|
|
197
|
+
vulnerable_in_range: candidates.length - safe.length,
|
|
198
|
+
smallest_safe_in_range: safe[0] ?? null,
|
|
199
|
+
newest_safe_in_range: safe[safe.length - 1] ?? null,
|
|
200
|
+
safe_versions: safe.slice(-8),
|
|
201
|
+
};
|
|
202
|
+
if (candidates.length > 0 && safe.length === 0) {
|
|
203
|
+
out.affecting_all = [...new Set(candidates.flatMap(affecting))].filter((id) => candidates.every((v) => affecting(v).includes(id))).slice(0, 10);
|
|
204
|
+
const outside = installable
|
|
205
|
+
.filter((v) => !isPrereleaseIn(ecosystem, v) && affecting(v).length === 0)
|
|
206
|
+
.sort((a, b) => compareIn(ecosystem, a, b));
|
|
207
|
+
out.newest_safe_outside_range = outside[outside.length - 1] ?? null;
|
|
208
|
+
}
|
|
209
|
+
if (input.from) {
|
|
210
|
+
out.smallest_safe_upgrade = safe.find((v) => compareIn(ecosystem, v, input.from) > 0) ?? null;
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
@@ -2,28 +2,43 @@
|
|
|
2
2
|
* dependency_check tool
|
|
3
3
|
*
|
|
4
4
|
* A pre-flight verdict for ONE proposed change per item: adding a dependency
|
|
5
|
-
* at a version, or
|
|
6
|
-
*
|
|
7
|
-
* one-line reason, and the
|
|
5
|
+
* at a version, pinning one, or moving an installed one to another version.
|
|
6
|
+
* An agent calls this before it writes a version into a manifest and gets
|
|
7
|
+
* `proceed | wait | review | avoid | unknown`, a one-line reason, and the
|
|
8
|
+
* signals behind it, each with evidence. npm, crates.io, PyPI and Go.
|
|
9
|
+
*
|
|
10
|
+
* `to` is an exact version, "latest", or a constraint ("7.x", "^1.20",
|
|
11
|
+
* "~=3.1", "v1"). For a constraint the item also returns `range_check`: every
|
|
12
|
+
* release it admits judged against every advisory OSV.dev lists for the
|
|
13
|
+
* package, with the smallest and newest safe one, and the verdict is given
|
|
14
|
+
* for the version it recommends (the smallest safe upgrade above `from`, or
|
|
15
|
+
* for a new dependency the newest safe release). An exact `to` that an
|
|
16
|
+
* advisory affects gets the same `range_check` over its own release line, so
|
|
17
|
+
* the answer to "is 7.5.10 safe?" names 7.5.11.
|
|
8
18
|
*
|
|
9
19
|
* Signals (see dependency-check-signals.ts): known advisories on the target
|
|
10
20
|
* (OSV), release age (hold releases < 3 days unless they fix an advisory on
|
|
11
|
-
* the installed version), publish-trust drop
|
|
12
|
-
* (npm), newly introduced runtime dependencies that are
|
|
13
|
-
* old, yanked/deprecated/unpublished
|
|
21
|
+
* the installed version), publish-trust drop and newly added install scripts
|
|
22
|
+
* (npm; trust also crates.io), newly introduced runtime dependencies that are
|
|
23
|
+
* themselves < 30 days old (npm, crates.io), yanked / deprecated / unpublished
|
|
24
|
+
* / retracted targets, and the upgrade type. A signal a registry cannot
|
|
25
|
+
* supply says "not applicable", never a made-up value.
|
|
14
26
|
*
|
|
15
27
|
* Honesty contract: a registry or OSV that cannot be reached makes the item
|
|
16
28
|
* `unknown`, never `proceed`. A package missing from the public registry is
|
|
17
29
|
* `unknown` with that said (private package, or a misspelled/hallucinated
|
|
18
30
|
* name), never treated as clean.
|
|
19
31
|
*
|
|
20
|
-
* Privacy:
|
|
21
|
-
* target versions are picked out of the full release list
|
|
22
|
-
*
|
|
32
|
+
* Privacy: npm, crates.io and PyPI requests carry the package name only — the
|
|
33
|
+
* installed and target versions are picked out of the full release list
|
|
34
|
+
* locally. The Go proxy lists versions without dates, so the judged versions'
|
|
35
|
+
* `.info` is read from it. OSV receives name + version, as vulnerability_scan
|
|
36
|
+
* already does, and the package name for its advisory list.
|
|
23
37
|
*/
|
|
24
38
|
import type { FourDADatabase } from "../db.js";
|
|
25
39
|
import type { LiveIntelligence } from "../live/index.js";
|
|
26
40
|
import { type Signal, type Verdict } from "./dependency-check-signals.js";
|
|
41
|
+
import { type RangeCheck } from "./dependency-check-versions.js";
|
|
27
42
|
export interface DependencyCheckParams {
|
|
28
43
|
items?: unknown;
|
|
29
44
|
}
|
|
@@ -71,11 +86,12 @@ interface ItemResult {
|
|
|
71
86
|
package: string;
|
|
72
87
|
from: string | null;
|
|
73
88
|
to: string;
|
|
74
|
-
/** "latest" when the caller asked for
|
|
89
|
+
/** "latest" or the constraint, when the caller asked for one and `to` is what it resolved to. */
|
|
75
90
|
to_requested?: string;
|
|
76
91
|
verdict: Verdict;
|
|
77
92
|
reason: string;
|
|
78
93
|
signals: Signal[];
|
|
94
|
+
range_check?: RangeCheck;
|
|
79
95
|
}
|
|
80
96
|
export declare function executeDependencyCheck(_db: FourDADatabase | null, params: DependencyCheckParams, liveIntel: LiveIntelligence | null, opts?: {
|
|
81
97
|
now?: Date;
|