@4da/mcp-server 5.0.4 → 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.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +233 -37
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +12 -1
  51. package/dist/live/osv-scanner.js +50 -15
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -0,0 +1,274 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Python and Go lockfile parsers (split out of lockfile-parsers.ts).
4
+ *
5
+ * Measured 2026-10-02 against osv-scanner and pip-audit, every miss below was
6
+ * a parsing loss before OSV was ever asked:
7
+ * - requirements.txt: `urllib3[secure]==1.25.0` and dotted names were skipped,
8
+ * and an inline comment rode along as part of the version — OSV was asked
9
+ * about `"8.0.0 # imaging"` and answered with 63 advisories for the real 45.
10
+ * - poetry.lock: the reader required `name` and `version` on consecutive
11
+ * lines. Poetry before 1.0 wrote its keys alphabetically (`category`,
12
+ * `description`, `name`, ..., `version`), so a whole lockfile read as empty.
13
+ * - go.sum: `(v[\d.]+)` cut `v0.0.0-20200622213623-75b288015ac9` to `v0.0.0`
14
+ * and dropped `+incompatible`; `v0.0.0` then sorted AFTER the real
15
+ * pseudo-version, so older advisories were missed and later ones invented.
16
+ * go.sum also lists versions the build never selected.
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { emptySource, found, InstanceSet } from "./lockfile-types.js";
21
+ // =============================================================================
22
+ // Python: poetry.lock > uv.lock > Pipfile.lock > requirements.txt
23
+ // =============================================================================
24
+ /** PEP 503 normalised name: lowercase, runs of `-`, `_`, `.` become one `-`. */
25
+ export function normalizePythonName(name) {
26
+ return name.trim().toLowerCase().replace(/[-_.]+/g, "-");
27
+ }
28
+ export function resolvePython(cwd) {
29
+ // poetry.lock and uv.lock are TOML arrays of [[package]] tables.
30
+ for (const file of ["poetry.lock", "uv.lock"]) {
31
+ const lockPath = path.join(cwd, file);
32
+ if (!fs.existsSync(lockPath))
33
+ continue;
34
+ try {
35
+ const read = readTomlPackageLock(fs.readFileSync(lockPath, "utf-8"));
36
+ if (read.instances.size > 0)
37
+ return found(read.versions, read.instances, lockPath, "lockfile");
38
+ }
39
+ catch { /* fall through */ }
40
+ }
41
+ const pipfileLock = path.join(cwd, "Pipfile.lock");
42
+ if (fs.existsSync(pipfileLock)) {
43
+ try {
44
+ const lock = JSON.parse(fs.readFileSync(pipfileLock, "utf-8"));
45
+ const versions = new Map();
46
+ const instances = new InstanceSet();
47
+ for (const [section, dev] of [["default", false], ["develop", true]]) {
48
+ for (const [name, info] of Object.entries(lock[section] ?? {})) {
49
+ const version = info.version?.replace(/^===?/, "");
50
+ if (!version)
51
+ continue;
52
+ const normalized = normalizePythonName(name);
53
+ instances.add(normalized, version, dev);
54
+ if (!versions.has(normalized))
55
+ versions.set(normalized, version);
56
+ }
57
+ }
58
+ if (instances.size > 0)
59
+ return found(versions, instances, pipfileLock, "lockfile");
60
+ }
61
+ catch { /* fall through */ }
62
+ }
63
+ const reqPath = path.join(cwd, "requirements.txt");
64
+ if (fs.existsSync(reqPath)) {
65
+ const versions = new Map();
66
+ const instances = new InstanceSet();
67
+ try {
68
+ for (const { name, version } of parseRequirements(fs.readFileSync(reqPath, "utf-8"))) {
69
+ instances.add(name, version);
70
+ if (!versions.has(name))
71
+ versions.set(name, version);
72
+ }
73
+ }
74
+ catch { /* skip */ }
75
+ return found(versions, instances, reqPath, "manifest");
76
+ }
77
+ return emptySource();
78
+ }
79
+ /**
80
+ * [[package]] tables from poetry.lock (any version) or uv.lock. A pre-1.2
81
+ * poetry.lock records `category = "dev"` for dev-only packages; newer files
82
+ * and uv.lock do not record scope here, so it stays unknown.
83
+ */
84
+ export function readTomlPackageLock(content) {
85
+ const versions = new Map();
86
+ const instances = new InstanceSet();
87
+ for (const block of content.split(/^\[\[package\]\]\s*$/m).slice(1)) {
88
+ // Stop at the next table header (e.g. [package.dependencies], [metadata]).
89
+ const body = block.split(/^\[/m)[0];
90
+ const field = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(body)?.[1] ?? null;
91
+ const name = field("name");
92
+ const version = field("version");
93
+ if (!name || !version)
94
+ continue;
95
+ // uv.lock lists the project itself (source = { editable/virtual = "." }); it is not a release.
96
+ if (/^source\s*=\s*\{\s*(?:editable|virtual)\s*=/m.test(body))
97
+ continue;
98
+ const category = field("category");
99
+ const normalized = normalizePythonName(name);
100
+ instances.add(normalized, version, category === null ? undefined : category === "dev");
101
+ if (!versions.has(normalized))
102
+ versions.set(normalized, version);
103
+ }
104
+ return { versions, instances };
105
+ }
106
+ /**
107
+ * Exact pins from a requirements file: `name==1.2`, `name===1.2`,
108
+ * `name[extra1,extra2]==1.2`, with environment markers and inline comments.
109
+ * Ranges (`>=`, `~=`) do not name an installed version and are skipped, as are
110
+ * options (`-r`, `-e`, `--hash`) and URL requirements.
111
+ */
112
+ export function parseRequirements(content) {
113
+ const out = [];
114
+ // Backslash continuations join physical lines into one requirement.
115
+ const logical = content.replace(/\\\r?\n/g, " ").split(/\r?\n/);
116
+ for (const raw of logical) {
117
+ // A `#` starts a comment at line start or after whitespace (PEP 508 URLs may contain `#`).
118
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
119
+ if (!line || line.startsWith("-"))
120
+ continue;
121
+ // Per-requirement options (pip-compile writes `--hash=sha256:...` on
122
+ // continuation lines) follow the specifier; they are not part of it.
123
+ const spec = line.split(";")[0].replace(/\s--?[A-Za-z][\w-]*(?:[=\s]\S+)?/g, "").trim();
124
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*===?\s*([^\s,;]+)\s*$/.exec(spec);
125
+ if (!match)
126
+ continue;
127
+ out.push({ name: normalizePythonName(match[1]), version: match[2] });
128
+ }
129
+ return out;
130
+ }
131
+ // =============================================================================
132
+ // Go: go.mod (the selected module set) completed from go.sum for go < 1.17
133
+ // =============================================================================
134
+ /** A full Go module version: release, prerelease, pseudo-version, `+incompatible`. */
135
+ const GO_VERSION = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
136
+ export function resolveGo(cwd) {
137
+ const goModPath = path.join(cwd, "go.mod");
138
+ if (!fs.existsSync(goModPath))
139
+ return emptySource();
140
+ let mod;
141
+ try {
142
+ mod = parseGoMod(fs.readFileSync(goModPath, "utf-8"));
143
+ }
144
+ catch {
145
+ return emptySource();
146
+ }
147
+ const versions = new Map();
148
+ const instances = new InstanceSet();
149
+ for (const { module, version } of mod.requires) {
150
+ versions.set(module, version);
151
+ instances.add(module, version);
152
+ }
153
+ // From go 1.17 the go.mod `require` list names every module the build needs
154
+ // (module-graph pruning), so it IS the selected set and go.sum (which keeps
155
+ // hashes for versions the build never selects) would only over-report.
156
+ // Before 1.17 go.mod lists direct requirements only; transitives come from
157
+ // go.sum, at the highest version whose module zip is hashed there — what
158
+ // minimal version selection picked.
159
+ if (!mod.goVersionAtLeast117) {
160
+ const goSumPath = path.join(cwd, "go.sum");
161
+ if (fs.existsSync(goSumPath)) {
162
+ try {
163
+ for (const [module, version] of selectedFromGoSum(fs.readFileSync(goSumPath, "utf-8"))) {
164
+ if (versions.has(module))
165
+ continue;
166
+ versions.set(module, version);
167
+ instances.add(module, version);
168
+ }
169
+ }
170
+ catch { /* skip */ }
171
+ }
172
+ }
173
+ return found(versions, instances, goModPath, "lockfile");
174
+ }
175
+ /** `require` lines (block and single form) with `replace` applied, and whether the go directive is >= 1.17. */
176
+ export function parseGoMod(content) {
177
+ const requires = new Map();
178
+ const replaces = new Map();
179
+ let block = null;
180
+ for (const rawLine of content.split(/\r?\n/)) {
181
+ const indirect = /\/\/\s*indirect\b/.test(rawLine);
182
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
183
+ if (!line)
184
+ continue;
185
+ if (block && line === ")") {
186
+ block = null;
187
+ continue;
188
+ }
189
+ const opener = /^(require|replace)\s*\($/.exec(line);
190
+ if (opener) {
191
+ block = opener[1];
192
+ continue;
193
+ }
194
+ const single = /^(require|replace)\s+(.+)$/.exec(line);
195
+ const kind = single ? single[1] : block;
196
+ const rest = single ? single[2] : line;
197
+ if (kind === "require") {
198
+ const [module, version] = rest.split(/\s+/);
199
+ if (module && version && GO_VERSION.test(version))
200
+ requires.set(module, { module, version, indirect });
201
+ }
202
+ else if (kind === "replace") {
203
+ // `old [v] => new v` replaces with another module version; `=> ./path` is local code.
204
+ const [left, right] = rest.split("=>").map((s) => s.trim().split(/\s+/));
205
+ if (!left?.[0] || !right?.[0])
206
+ continue;
207
+ replaces.set(left[0], right[1] && GO_VERSION.test(right[1]) ? { module: right[0], version: right[1] } : null);
208
+ }
209
+ }
210
+ const result = [];
211
+ for (const req of requires.values()) {
212
+ if (!replaces.has(req.module)) {
213
+ result.push(req);
214
+ continue;
215
+ }
216
+ const target = replaces.get(req.module);
217
+ if (target)
218
+ result.push({ module: target.module, version: target.version, indirect: req.indirect });
219
+ }
220
+ const go = /^go\s+(\d+)\.(\d+)/m.exec(content);
221
+ const goVersionAtLeast117 = go ? Number(go[1]) > 1 || Number(go[2]) >= 17 : false;
222
+ return { requires: result, goVersionAtLeast117 };
223
+ }
224
+ /**
225
+ * Per module, the highest version go.sum records — minimal version selection
226
+ * picks the highest required version, and go.sum records every version the
227
+ * module graph required. A module listed only by its `/go.mod` hash is still
228
+ * in Go's build list (`go list -m all`): measured 2026-10-02 on gh cli v1.0
229
+ * (go 1.13), skipping those lines missed 14 advisories (grpc 1.21.0,
230
+ * jwt-go, gorilla/websocket, ...) the build list carries.
231
+ */
232
+ export function selectedFromGoSum(content) {
233
+ const selected = new Map();
234
+ for (const line of content.split(/\r?\n/)) {
235
+ const [module, rawVersion] = line.trim().split(/\s+/);
236
+ const version = rawVersion?.replace(/\/go\.mod$/, "");
237
+ if (!module || !version || !GO_VERSION.test(version))
238
+ continue;
239
+ const current = selected.get(module);
240
+ if (!current || compareGoVersions(version, current) > 0)
241
+ selected.set(module, version);
242
+ }
243
+ return selected;
244
+ }
245
+ /** Semver precedence over Go versions; build metadata (`+incompatible`) is ignored. */
246
+ export function compareGoVersions(a, b) {
247
+ const parse = (v) => {
248
+ const m = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(v);
249
+ return m ? { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split(".") : [] } : null;
250
+ };
251
+ const pa = parse(a);
252
+ const pb = parse(b);
253
+ if (!pa || !pb)
254
+ return 0;
255
+ for (let i = 0; i < 3; i++)
256
+ if (pa.nums[i] !== pb.nums[i])
257
+ return pa.nums[i] > pb.nums[i] ? 1 : -1;
258
+ if (pa.pre.length === 0 || pb.pre.length === 0)
259
+ return Math.sign(pb.pre.length - pa.pre.length);
260
+ for (let i = 0; i < Math.min(pa.pre.length, pb.pre.length); i++) {
261
+ const x = pa.pre[i];
262
+ const y = pb.pre[i];
263
+ if (x === y)
264
+ continue;
265
+ const xn = /^\d+$/.test(x);
266
+ const yn = /^\d+$/.test(y);
267
+ if (xn && yn)
268
+ return BigInt(x) > BigInt(y) ? 1 : -1;
269
+ if (xn !== yn)
270
+ return xn ? -1 : 1;
271
+ return x > y ? 1 : -1;
272
+ }
273
+ return Math.sign(pa.pre.length - pb.pre.length);
274
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Lockfile parsers, one per ecosystem, each reporting WHICH file it resolved
3
+ * from as well as the versions it read.
4
+ *
5
+ * Priority per ecosystem: lock file (exact) > manifest (range/specifier).
6
+ * Uses only Node.js built-ins — no external parsers. The source path is what
7
+ * lets the live layer notice when that file changes and re-resolve (measured
8
+ * 2026-09-10: a server started before a lockfile bump kept reporting hono
9
+ * 4.13.3 after the lockfile said 4.13.5). The pnpm and yarn readers live in
10
+ * js-lockfile-readers.ts; Python and Go in lockfile-parsers-pygo.ts.
11
+ *
12
+ * Every parser reports two things (2026-10-02):
13
+ * - `instances`: EVERY installed copy, name@version, with the lockfile's own
14
+ * dev flag when it records one. A lockfile routinely holds several versions
15
+ * of one package (nested npm copies, two semver-incompatible crates). The
16
+ * reader used to keep one version per name, last one wins: measured against
17
+ * osv-scanner on 12 projects, nodegoat lost 287 of 1,091 installed copies
18
+ * and 4DA's src-tauri lost 143 of 934 — including its own `rsa 0.9.10`
19
+ * (RUSTSEC-2023-0071), overwritten by `rsa 0.10.0-rc.18` — and the direct
20
+ * `minimist@1.2.0` was reported as its nested `0.0.8`.
21
+ * - `versions`: the ONE version per name a direct dependency resolves to (the
22
+ * top-level npm copy, the crate version a workspace member depends on),
23
+ * which dependency health and upgrade planning read.
24
+ */
25
+ import { InstanceSet, type VersionSource } from "./lockfile-types.js";
26
+ import type { OsvEcosystem } from "./types.js";
27
+ export type { PackageInstance, VersionSource } from "./lockfile-types.js";
28
+ /** Every lock file the resolver may read for this ecosystem, present or not, in priority order. */
29
+ export declare function lockfileCandidates(cwd: string, ecosystem: OsvEcosystem): string[];
30
+ /** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
31
+ export declare function manifestCandidate(cwd: string, ecosystem: OsvEcosystem): string | null;
32
+ export declare function resolveVersionSource(cwd: string, ecosystem: OsvEcosystem): VersionSource;
33
+ /** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
34
+ export declare function packageNameFromNodeModulesPath(lockPath: string): string;
35
+ interface LockEntry {
36
+ version?: string;
37
+ dev?: boolean;
38
+ link?: boolean;
39
+ dependencies?: Record<string, LockEntry>;
40
+ }
41
+ /**
42
+ * package-lock.json / npm-shrinkwrap.json, lockfile v1, v2 and v3.
43
+ *
44
+ * v2/v3 `packages` keys are install paths: `node_modules/a` is the hoisted
45
+ * copy a direct dependency of the root resolves to, `node_modules/a/node_modules/b`
46
+ * a nested one. npm writes `dev: true` on every entry only reachable from
47
+ * devDependencies (`devOptional` = also reachable as an optional runtime dep,
48
+ * so runtime), which makes the dev scope of a TRANSITIVE known. v1 nests the
49
+ * same information under `dependencies`.
50
+ */
51
+ export declare function readPackageLock(lock: {
52
+ packages?: Record<string, LockEntry>;
53
+ dependencies?: Record<string, LockEntry>;
54
+ }): {
55
+ versions: Map<string, string>;
56
+ instances: InstanceSet;
57
+ };
58
+ interface CargoPackage {
59
+ name: string;
60
+ version: string;
61
+ source: string | null;
62
+ dependencies: string[];
63
+ }
64
+ /**
65
+ * Every `[[package]]` table in a Cargo.lock, read field by field. The old
66
+ * reader required `name` and `version` on consecutive lines; that holds for
67
+ * cargo's own writer, but the table is TOML and nothing else guarantees it.
68
+ */
69
+ export declare function parseCargoLockPackages(content: string): CargoPackage[];
@@ -0,0 +1,311 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Lockfile parsers, one per ecosystem, each reporting WHICH file it resolved
4
+ * from as well as the versions it read.
5
+ *
6
+ * Priority per ecosystem: lock file (exact) > manifest (range/specifier).
7
+ * Uses only Node.js built-ins — no external parsers. The source path is what
8
+ * lets the live layer notice when that file changes and re-resolve (measured
9
+ * 2026-09-10: a server started before a lockfile bump kept reporting hono
10
+ * 4.13.3 after the lockfile said 4.13.5). The pnpm and yarn readers live in
11
+ * js-lockfile-readers.ts; Python and Go in lockfile-parsers-pygo.ts.
12
+ *
13
+ * Every parser reports two things (2026-10-02):
14
+ * - `instances`: EVERY installed copy, name@version, with the lockfile's own
15
+ * dev flag when it records one. A lockfile routinely holds several versions
16
+ * of one package (nested npm copies, two semver-incompatible crates). The
17
+ * reader used to keep one version per name, last one wins: measured against
18
+ * osv-scanner on 12 projects, nodegoat lost 287 of 1,091 installed copies
19
+ * and 4DA's src-tauri lost 143 of 934 — including its own `rsa 0.9.10`
20
+ * (RUSTSEC-2023-0071), overwritten by `rsa 0.10.0-rc.18` — and the direct
21
+ * `minimist@1.2.0` was reported as its nested `0.0.8`.
22
+ * - `versions`: the ONE version per name a direct dependency resolves to (the
23
+ * top-level npm copy, the crate version a workspace member depends on),
24
+ * which dependency health and upgrade planning read.
25
+ */
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
28
+ import { readBunLock } from "./bun-lockfile.js";
29
+ import { readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
30
+ import { resolveGo, resolvePython } from "./lockfile-parsers-pygo.js";
31
+ import { emptySource, found, InstanceSet } from "./lockfile-types.js";
32
+ const LOCKFILES = {
33
+ npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
34
+ "crates.io": ["Cargo.lock"],
35
+ PyPI: ["poetry.lock", "uv.lock", "Pipfile.lock"],
36
+ Go: ["go.mod", "go.sum"],
37
+ };
38
+ const MANIFESTS = {
39
+ npm: "package.json",
40
+ "crates.io": "Cargo.toml",
41
+ PyPI: "requirements.txt",
42
+ };
43
+ /** Every lock file the resolver may read for this ecosystem, present or not, in priority order. */
44
+ export function lockfileCandidates(cwd, ecosystem) {
45
+ return (LOCKFILES[ecosystem] ?? []).map((name) => path.join(cwd, name));
46
+ }
47
+ /** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
48
+ export function manifestCandidate(cwd, ecosystem) {
49
+ const name = MANIFESTS[ecosystem];
50
+ return name ? path.join(cwd, name) : null;
51
+ }
52
+ export function resolveVersionSource(cwd, ecosystem) {
53
+ switch (ecosystem) {
54
+ case "npm":
55
+ return resolveNpm(cwd);
56
+ case "crates.io":
57
+ return resolveRust(cwd);
58
+ case "PyPI":
59
+ return resolvePython(cwd);
60
+ case "Go":
61
+ return resolveGo(cwd);
62
+ default:
63
+ return emptySource();
64
+ }
65
+ }
66
+ /** Instances for a manifest-only read: one per name, scope unknown. */
67
+ function manifestInstances(versions) {
68
+ return [...versions].map(([name, version]) => ({ name, version }));
69
+ }
70
+ // =============================================================================
71
+ // npm: package-lock.json > npm-shrinkwrap.json > pnpm-lock.yaml > yarn.lock > bun.lock > package.json ranges
72
+ // =============================================================================
73
+ function resolveNpm(cwd) {
74
+ for (const name of ["package-lock.json", "npm-shrinkwrap.json"]) {
75
+ const lockPath = path.join(cwd, name);
76
+ if (!fs.existsSync(lockPath))
77
+ continue;
78
+ try {
79
+ const read = readPackageLock(JSON.parse(fs.readFileSync(lockPath, "utf-8")));
80
+ if (read.instances.size > 0)
81
+ return found(read.versions, read.instances, lockPath, "lockfile");
82
+ }
83
+ catch { /* fall through */ }
84
+ }
85
+ // pnpm-lock.yaml (v5.x, v6 and v9, read by column; see js-lockfile-readers.ts)
86
+ const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
87
+ if (fs.existsSync(pnpmLockPath)) {
88
+ try {
89
+ const versions = new Map();
90
+ const instances = new InstanceSet();
91
+ readPnpmLock(fs.readFileSync(pnpmLockPath, "utf-8"), versions, instances);
92
+ if (versions.size > 0)
93
+ return found(versions, instances, pnpmLockPath, "lockfile");
94
+ }
95
+ catch { /* fall through */ }
96
+ }
97
+ // yarn.lock (v1 and berry)
98
+ const yarnLockPath = path.join(cwd, "yarn.lock");
99
+ if (fs.existsSync(yarnLockPath)) {
100
+ try {
101
+ const versions = new Map();
102
+ const instances = new InstanceSet();
103
+ const specs = new Map();
104
+ readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
105
+ // The copy a direct dependency resolves to is the one its package.json range names.
106
+ for (const [name, range] of manifestRanges(cwd)) {
107
+ const version = specs.get(`${name}@${range}`);
108
+ if (version)
109
+ versions.set(name, version);
110
+ }
111
+ if (versions.size > 0)
112
+ return found(versions, instances, yarnLockPath, "lockfile");
113
+ }
114
+ catch { /* fall through */ }
115
+ }
116
+ // bun.lock (Bun 1.2+ text lockfile; the binary bun.lockb is not readable)
117
+ const bunLockPath = path.join(cwd, "bun.lock");
118
+ if (fs.existsSync(bunLockPath)) {
119
+ try {
120
+ const versions = new Map();
121
+ const instances = new InstanceSet();
122
+ readBunLock(fs.readFileSync(bunLockPath, "utf-8"), versions, instances);
123
+ if (versions.size > 0)
124
+ return found(versions, instances, bunLockPath, "lockfile");
125
+ }
126
+ catch { /* fall through */ }
127
+ }
128
+ // Fallback: extract version floors from package.json ranges
129
+ const pkgPath = path.join(cwd, "package.json");
130
+ if (fs.existsSync(pkgPath)) {
131
+ const versions = new Map();
132
+ try {
133
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
134
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
135
+ for (const [name, spec] of Object.entries(allDeps)) {
136
+ const version = extractVersionFromSpec(spec);
137
+ if (version)
138
+ versions.set(name, version);
139
+ }
140
+ }
141
+ catch { /* skip */ }
142
+ return found(versions, manifestInstances(versions), pkgPath, "manifest");
143
+ }
144
+ return emptySource();
145
+ }
146
+ /** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
147
+ export function packageNameFromNodeModulesPath(lockPath) {
148
+ const marker = "node_modules/";
149
+ const index = lockPath.lastIndexOf(marker);
150
+ return index === -1 ? "" : lockPath.slice(index + marker.length);
151
+ }
152
+ /**
153
+ * package-lock.json / npm-shrinkwrap.json, lockfile v1, v2 and v3.
154
+ *
155
+ * v2/v3 `packages` keys are install paths: `node_modules/a` is the hoisted
156
+ * copy a direct dependency of the root resolves to, `node_modules/a/node_modules/b`
157
+ * a nested one. npm writes `dev: true` on every entry only reachable from
158
+ * devDependencies (`devOptional` = also reachable as an optional runtime dep,
159
+ * so runtime), which makes the dev scope of a TRANSITIVE known. v1 nests the
160
+ * same information under `dependencies`.
161
+ */
162
+ export function readPackageLock(lock) {
163
+ const versions = new Map();
164
+ const instances = new InstanceSet();
165
+ if (lock.packages) {
166
+ for (const [key, value] of Object.entries(lock.packages)) {
167
+ const name = packageNameFromNodeModulesPath(key);
168
+ if (!name || !value || value.link || !value.version)
169
+ continue;
170
+ instances.add(name, value.version, value.dev === true);
171
+ // The hoisted root copy is what `require(name)` from the project resolves to.
172
+ if (key === `node_modules/${name}`)
173
+ versions.set(name, value.version);
174
+ }
175
+ // A workspace package's own nested copy (packages/x/node_modules/a) when nothing is hoisted.
176
+ for (const instance of instances.toArray()) {
177
+ if (!versions.has(instance.name))
178
+ versions.set(instance.name, instance.version);
179
+ }
180
+ }
181
+ if (instances.size === 0 && lock.dependencies) {
182
+ const walk = (deps, topLevel) => {
183
+ for (const [name, value] of Object.entries(deps)) {
184
+ if (value?.version && /^\d/.test(value.version)) {
185
+ instances.add(name, value.version, value.dev === true);
186
+ if (topLevel)
187
+ versions.set(name, value.version);
188
+ }
189
+ if (value?.dependencies)
190
+ walk(value.dependencies, false);
191
+ }
192
+ };
193
+ walk(lock.dependencies, true);
194
+ }
195
+ return { versions, instances };
196
+ }
197
+ /**
198
+ * Every `[[package]]` table in a Cargo.lock, read field by field. The old
199
+ * reader required `name` and `version` on consecutive lines; that holds for
200
+ * cargo's own writer, but the table is TOML and nothing else guarantees it.
201
+ */
202
+ export function parseCargoLockPackages(content) {
203
+ const packages = [];
204
+ for (const block of content.split(/^\[\[package\]\]\s*$/m).slice(1)) {
205
+ const body = block.split(/^\[(?!\[package\]\])/m)[0];
206
+ const field = (key) => new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, "m").exec(body)?.[1] ?? null;
207
+ const name = field("name");
208
+ const version = field("version");
209
+ if (!name || !version)
210
+ continue;
211
+ const depsMatch = /^dependencies\s*=\s*\[([\s\S]*?)\]/m.exec(body);
212
+ const dependencies = depsMatch ? [...depsMatch[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]) : [];
213
+ packages.push({ name, version, source: field("source"), dependencies });
214
+ }
215
+ return packages;
216
+ }
217
+ function resolveRust(cwd) {
218
+ const lockPath = path.join(cwd, "Cargo.lock");
219
+ if (!fs.existsSync(lockPath)) {
220
+ return resolveRustFromManifest(cwd);
221
+ }
222
+ const versions = new Map();
223
+ const instances = new InstanceSet();
224
+ try {
225
+ const packages = parseCargoLockPackages(fs.readFileSync(lockPath, "utf-8"));
226
+ const versionsByName = new Map();
227
+ // Cargo writes `source` on every registry or git package. In a file it
228
+ // wrote, a package with none is a workspace member or path dependency:
229
+ // local code, not a release, so no advisory can be about it. A lockfile
230
+ // with no `source` anywhere was not written by cargo; keep everything.
231
+ const cargoWritten = packages.some((pkg) => pkg.source !== null);
232
+ const isLocal = (pkg) => cargoWritten && pkg.source === null;
233
+ for (const pkg of packages) {
234
+ if (isLocal(pkg))
235
+ continue;
236
+ instances.add(pkg.name, pkg.version);
237
+ versionsByName.set(pkg.name, [...(versionsByName.get(pkg.name) ?? []), pkg.version]);
238
+ }
239
+ // The version a direct dependency resolves to: the one a workspace member
240
+ // names. Cargo writes "name version" when two versions coexist, else "name".
241
+ for (const member of packages.filter(isLocal)) {
242
+ for (const dep of member.dependencies) {
243
+ const [name, version] = dep.split(" ");
244
+ const candidates = versionsByName.get(name);
245
+ if (!candidates || versions.has(name))
246
+ continue;
247
+ if (version && candidates.includes(version))
248
+ versions.set(name, version);
249
+ else if (!version && candidates.length === 1)
250
+ versions.set(name, candidates[0]);
251
+ }
252
+ }
253
+ for (const [name, list] of versionsByName) {
254
+ if (!versions.has(name))
255
+ versions.set(name, list[0]);
256
+ }
257
+ }
258
+ catch { /* skip */ }
259
+ return found(versions, instances, lockPath, "lockfile");
260
+ }
261
+ function resolveRustFromManifest(cwd) {
262
+ const versions = new Map();
263
+ const cargoPath = path.join(cwd, "Cargo.toml");
264
+ if (!fs.existsSync(cargoPath))
265
+ return emptySource();
266
+ try {
267
+ const content = fs.readFileSync(cargoPath, "utf-8");
268
+ // name = "1.0" or name = { version = "1.0", ... }
269
+ const depRegex = /^([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]+)"|.*?version\s*=\s*"([^"]+)")/gm;
270
+ let match;
271
+ while ((match = depRegex.exec(content)) !== null) {
272
+ const name = match[1];
273
+ const version = match[2] || match[3];
274
+ if (version && /^\d/.test(version)) {
275
+ versions.set(name, version);
276
+ }
277
+ }
278
+ }
279
+ catch { /* skip */ }
280
+ return found(versions, manifestInstances(versions), cargoPath, "manifest");
281
+ }
282
+ // =============================================================================
283
+ // Helpers
284
+ // =============================================================================
285
+ /** package.json dependency ranges (all dependency fields), or none when unreadable. */
286
+ function manifestRanges(cwd) {
287
+ try {
288
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
289
+ const out = [];
290
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
291
+ for (const [name, range] of Object.entries(pkg[field] ?? {})) {
292
+ if (typeof range === "string")
293
+ out.push([name, range]);
294
+ }
295
+ }
296
+ return out;
297
+ }
298
+ catch {
299
+ return [];
300
+ }
301
+ }
302
+ /**
303
+ * Extract a usable version from an npm version specifier.
304
+ * "^4.18.2" -> "4.18.2", "~1.0.0" -> "1.0.0", ">=2.0.0" -> "2.0.0"
305
+ */
306
+ function extractVersionFromSpec(spec) {
307
+ if (!spec)
308
+ return null;
309
+ const match = spec.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
310
+ return match ? match[1] : null;
311
+ }