@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,130 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Version ordering per ecosystem, for advisory ranges and fix selection.
4
+ *
5
+ * `compareSemver` reads MAJOR.MINOR.PATCH and calls anything else "equal".
6
+ * That is wrong outside npm and crates.io, and it was wrong in practice
7
+ * (measured 2026-10-02 against pip-audit and osv-scanner):
8
+ * - PyPI versions are PEP 440, not semver. `5.1` and `4.2` are ordinary
9
+ * releases; read as "equal" to everything, django 3.2.0 was sent to the 5.1
10
+ * line's fix (5.1.1) instead of its own line's (3.2.25 / 4.2.16).
11
+ * - Go versions carry a `v`, OSV's Go ranges do not, and pseudo-versions
12
+ * (`v0.0.0-20200622213623-75b288015ac9`) order as semver prereleases.
13
+ *
14
+ * `0` is OSV's "since the beginning" bound and orders below every version.
15
+ */
16
+ import { compareSemver, parseSemver } from "./semver-utils.js";
17
+ import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
18
+ const PEP440 = /^v?(?:(\d+)!)?(\d+(?:\.\d+)*)(?:[-_.]?(a|alpha|b|beta|c|rc|pre|preview)[-_.]?(\d*))?(?:(?:[-_.]?(?:post|rev|r)[-_.]?(\d*))|-(\d+))?(?:[-_.]?dev[-_.]?(\d*))?(?:\+[a-z0-9]+(?:[-_.][a-z0-9]+)*)?$/i;
19
+ export function parsePep440(version) {
20
+ const m = PEP440.exec(version.trim());
21
+ if (!m)
22
+ return null;
23
+ const preRank = { a: 0, alpha: 0, b: 1, beta: 1, c: 2, rc: 2, pre: 2, preview: 2 };
24
+ return {
25
+ epoch: m[1] ? Number(m[1]) : 0,
26
+ release: m[2].split(".").map(Number),
27
+ pre: m[3] ? [preRank[m[3].toLowerCase()], m[4] ? Number(m[4]) : 0] : null,
28
+ post: m[5] !== undefined ? (m[5] ? Number(m[5]) : 0) : m[6] !== undefined ? Number(m[6]) : null,
29
+ dev: m[7] !== undefined ? (m[7] ? Number(m[7]) : 0) : null,
30
+ };
31
+ }
32
+ export function comparePep440(a, b) {
33
+ if (a.epoch !== b.epoch)
34
+ return a.epoch > b.epoch ? 1 : -1;
35
+ const len = Math.max(a.release.length, b.release.length);
36
+ for (let i = 0; i < len; i++) {
37
+ const x = a.release[i] ?? 0;
38
+ const y = b.release[i] ?? 0;
39
+ if (x !== y)
40
+ return x > y ? 1 : -1;
41
+ }
42
+ // PEP 440's sort key within one release:
43
+ // X.devN < X.aN.devM < X.aN < X.aN.postM < X.bN < X.rcN < X < X.postN.devM < X.postN
44
+ // pre: a/b/rc rank, a dev-only release below every pre-release, a final above;
45
+ // then post (none lowest); then dev (none highest).
46
+ const key = (v) => [
47
+ v.pre ? v.pre[0] : v.post === null && v.dev !== null ? -1 : 3,
48
+ v.pre ? v.pre[1] : 0,
49
+ v.post === null ? -1 : v.post,
50
+ v.dev === null ? Number.POSITIVE_INFINITY : v.dev,
51
+ ];
52
+ const ka = key(a);
53
+ const kb = key(b);
54
+ for (let i = 0; i < ka.length; i++) {
55
+ if (ka[i] !== kb[i])
56
+ return ka[i] > kb[i] ? 1 : -1;
57
+ }
58
+ return 0;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Dispatch
62
+ // ---------------------------------------------------------------------------
63
+ const stripGoV = (v) => v.replace(/^v(?=\d)/, "");
64
+ /** Whether `version` is readable for this ecosystem's ordering. */
65
+ export function isComparable(version, ecosystem) {
66
+ if (version === "0")
67
+ return true;
68
+ switch (ecosystem) {
69
+ case "PyPI":
70
+ return parsePep440(version) !== null;
71
+ case "Go":
72
+ return parseSemverPrecedence(stripGoV(version)) !== null;
73
+ case "npm":
74
+ case "crates.io":
75
+ return parseSemverPrecedence(version) !== null;
76
+ default:
77
+ return parseSemver(version) !== null;
78
+ }
79
+ }
80
+ /**
81
+ * Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
82
+ * (the same contract as compareSemver, so an unreadable bound never decides).
83
+ */
84
+ export function compareVersions(a, b, ecosystem) {
85
+ if (a === b)
86
+ return 0;
87
+ if (a === "0")
88
+ return isComparable(b, ecosystem) ? -1 : 0;
89
+ if (b === "0")
90
+ return isComparable(a, ecosystem) ? 1 : 0;
91
+ switch (ecosystem) {
92
+ case "PyPI": {
93
+ const pa = parsePep440(a);
94
+ const pb = parsePep440(b);
95
+ return pa && pb ? comparePep440(pa, pb) : 0;
96
+ }
97
+ case "Go":
98
+ return compareVersionPrecedence(stripGoV(a), stripGoV(b)) ?? 0;
99
+ case "npm":
100
+ case "crates.io":
101
+ return compareVersionPrecedence(a, b) ?? compareSemver(a, b);
102
+ default:
103
+ return compareSemver(a, b);
104
+ }
105
+ }
106
+ /** Highest of `versions` in this ecosystem's order, or null when empty. */
107
+ export function maxVersion(versions, ecosystem) {
108
+ if (versions.length === 0)
109
+ return null;
110
+ return versions.reduce((max, v) => (compareVersions(v, max, ecosystem) > 0 ? v : max), versions[0]);
111
+ }
112
+ /** Package-name equality as the ecosystem's registry defines it. */
113
+ export function samePackageName(a, b, ecosystem) {
114
+ if (a === b)
115
+ return true;
116
+ if (ecosystem === "PyPI") {
117
+ const n = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
118
+ return n(a) === n(b);
119
+ }
120
+ if (ecosystem === "crates.io") {
121
+ const n = (s) => s.toLowerCase().replace(/_/g, "-");
122
+ return n(a) === n(b);
123
+ }
124
+ return false;
125
+ }
126
+ /** Whether a user-typed package name (`package` parameters) names this dependency: registry rules, then case-insensitive. */
127
+ export function namesPackage(query, name, ecosystem) {
128
+ const q = query.trim();
129
+ return samePackageName(q, name, ecosystem) || q.toLowerCase() === name.toLowerCase();
130
+ }
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Extracts exact dependency versions from lock files.
5
5
  * Priority: lock file (exact) > manifest (range/specifier).
6
- * Uses only Node.js built-ins — no external parsers.
6
+ * Uses only Node.js built-ins — no external parsers. The per-ecosystem
7
+ * parsers live in lockfile-parsers.ts, which also reports which file each
8
+ * resolution read from.
7
9
  */
8
10
  import type { OsvEcosystem, ResolvedDependency } from "./types.js";
11
+ import { type PackageInstance } from "./lockfile-parsers.js";
9
12
  /**
10
13
  * Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
11
14
  *
@@ -18,8 +21,18 @@ import type { OsvEcosystem, ResolvedDependency } from "./types.js";
18
21
  */
19
22
  export declare function mapEcosystem(language: string): OsvEcosystem;
20
23
  export declare function resolveVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
24
+ /** `resolveVersions` over a version map the caller already read from `cwd`. */
25
+ export declare function resolveVersionsFrom(cwd: string, deps: string[], devDeps: string[], ecosystem: OsvEcosystem, versionMap: Map<string, string>, targets?: Record<string, string>): ResolvedDependency[];
21
26
  /**
22
27
  * Resolve the complete lockfile package set for vulnerability scanning.
23
28
  * Direct dependency health and upgrade planning continue to use resolveVersions.
24
29
  */
25
30
  export declare function resolveAuditVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
31
+ /**
32
+ * `resolveAuditVersions` over a version map the caller already read. `direct`
33
+ * lets the caller pass direct entries it has already enriched (install state)
34
+ * so the audit set carries the same objects' context. `instances` is every
35
+ * installed copy the lockfile holds; without it the version map (one copy per
36
+ * name) is all there is.
37
+ */
38
+ export declare function resolveAuditVersionsFrom(cwd: string, deps: string[], devDeps: string[], ecosystem: OsvEcosystem, versionMap: Map<string, string>, targets?: Record<string, string>, direct?: ResolvedDependency[], instances?: PackageInstance[]): ResolvedDependency[];
@@ -4,11 +4,14 @@
4
4
  *
5
5
  * Extracts exact dependency versions from lock files.
6
6
  * Priority: lock file (exact) > manifest (range/specifier).
7
- * Uses only Node.js built-ins — no external parsers.
7
+ * Uses only Node.js built-ins — no external parsers. The per-ecosystem
8
+ * parsers live in lockfile-parsers.ts, which also reports which file each
9
+ * resolution read from.
8
10
  */
9
- import * as fs from "node:fs";
10
- import * as path from "node:path";
11
11
  import { targetActiveOnHost } from "./platform.js";
12
+ import { activeCratesForHost, hostTriple } from "./cargo-platform.js";
13
+ import { resolveVersionSource } from "./lockfile-parsers.js";
14
+ import { normalizePythonName } from "./lockfile-parsers-pygo.js";
12
15
  // Alias -> OSV ecosystem. Mirrors the desktop app's canonical
13
16
  // `Ecosystem::parse` (src-tauri/src/ecosystem.rs) so both sides recognize the
14
17
  // SAME aliases — they previously drifted (the Rust side knew csharp/dart/php
@@ -62,12 +65,35 @@ const ECOSYSTEM_MAP = {
62
65
  export function mapEcosystem(language) {
63
66
  return ECOSYSTEM_MAP[language.trim().toLowerCase()] || "npm";
64
67
  }
68
+ /**
69
+ * crates.io treats `-` and `_` as one namespace (you cannot publish both
70
+ * `http-body-util` and `http_body_util`), but the two spellings reach this
71
+ * resolver from different sources: Cargo.lock records the publisher's canonical
72
+ * name while ACE's import scraper reports the IMPORT identifier, which is
73
+ * always underscored. An exact-match lookup then fails for the import spelling
74
+ * and the same crate surfaces twice — once versioned, once `version: null`
75
+ * (observed live 2026-08-30: http_body_util, async_trait, tower_http,
76
+ * ed25519_dalek, ts_rs, proc_macro2, victauri_test, all null beside their
77
+ * hyphenated versioned twins). Resolve via the spelling the lock file actually
78
+ * uses and RETURN that spelling, so downstream dedupe merges the variants.
79
+ */
80
+ function canonicalCrateName(name, versionMap) {
81
+ if (versionMap.has(name))
82
+ return name;
83
+ const swapped = name.includes("_") ? name.replace(/_/g, "-") : name.replace(/-/g, "_");
84
+ return versionMap.has(swapped) ? swapped : name;
85
+ }
65
86
  export function resolveVersions(cwd, deps, devDeps, language, targets = {}) {
66
87
  const ecosystem = mapEcosystem(language);
88
+ const versionMap = resolveVersionSource(cwd, ecosystem).versions;
89
+ return resolveVersionsFrom(cwd, deps, devDeps, ecosystem, versionMap, targets);
90
+ }
91
+ /** `resolveVersions` over a version map the caller already read from `cwd`. */
92
+ export function resolveVersionsFrom(cwd, deps, devDeps, ecosystem, versionMap, targets = {}) {
67
93
  const results = [];
68
- const versionMap = resolveVersionMap(cwd, ecosystem);
69
- const build = (name, isDev) => {
70
- const target = targets[name] ?? null;
94
+ const build = (rawName, isDev) => {
95
+ const name = ecosystem === "crates.io" ? canonicalCrateName(rawName, versionMap) : rawName;
96
+ const target = targets[rawName] ?? targets[name] ?? null;
71
97
  return {
72
98
  name,
73
99
  version: versionMap.get(normalizePackageName(name, ecosystem)) || null,
@@ -77,6 +103,9 @@ export function resolveVersions(cwd, deps, devDeps, language, targets = {}) {
77
103
  devScopeKnown: true,
78
104
  target,
79
105
  platformActive: targetActiveOnHost(target),
106
+ // Provenance recorded at the point of resolution: this version came from
107
+ // THIS manifest directory's lock file, not from "the project" generally.
108
+ sourceDirs: [cwd],
80
109
  };
81
110
  };
82
111
  for (const name of deps)
@@ -91,27 +120,71 @@ export function resolveVersions(cwd, deps, devDeps, language, targets = {}) {
91
120
  */
92
121
  export function resolveAuditVersions(cwd, deps, devDeps, language, targets = {}) {
93
122
  const ecosystem = mapEcosystem(language);
94
- const direct = resolveVersions(cwd, deps, devDeps, language, targets);
95
- const versionMap = resolveVersionMap(cwd, ecosystem);
96
- const directNames = new Set(deps.map((name) => normalizePackageName(name, ecosystem)));
97
- const devNames = new Set(devDeps.map((name) => normalizePackageName(name, ecosystem)));
123
+ const read = resolveVersionSource(cwd, ecosystem);
124
+ return resolveAuditVersionsFrom(cwd, deps, devDeps, ecosystem, read.versions, targets, undefined, read.instances);
125
+ }
126
+ /**
127
+ * `resolveAuditVersions` over a version map the caller already read. `direct`
128
+ * lets the caller pass direct entries it has already enriched (install state)
129
+ * so the audit set carries the same objects' context. `instances` is every
130
+ * installed copy the lockfile holds; without it the version map (one copy per
131
+ * name) is all there is.
132
+ */
133
+ export function resolveAuditVersionsFrom(cwd, deps, devDeps, ecosystem, versionMap, targets = {}, direct = resolveVersionsFrom(cwd, deps, devDeps, ecosystem, versionMap, targets), instances = [...versionMap].map(([name, version]) => ({ name, version }))) {
134
+ // Canonicalize the same way resolveVersions does, so a dep declared under its
135
+ // import spelling still marks the lock file's canonical entry as direct.
136
+ const canonical = (name) => normalizePackageName(ecosystem === "crates.io" ? canonicalCrateName(name, versionMap) : name, ecosystem);
137
+ const directNames = new Set(deps.map(canonical));
138
+ const devNames = new Set(devDeps.map(canonical));
98
139
  const seen = new Set(direct.map((dep) => dependencyKey(dep)));
99
140
  const results = [...direct];
100
- for (const [name, version] of versionMap) {
141
+ // Cargo.lock is target-agnostic, so a Windows lockfile still lists the whole
142
+ // Linux GTK3 stack Tauri pulls in. `[target.'cfg(...)']` parsing only ever
143
+ // covered DIRECT gated deps, and that cluster is entirely transitive — which
144
+ // is why nine unreachable crates were reported while the scan claimed to be
145
+ // platform-filtered. Ask cargo to resolve the graph for this host instead.
146
+ // `null` means cargo could not answer; every crate then stays active, because
147
+ // "unknown" must never silently hide a real advisory.
148
+ //
149
+ // THE SHARED PREDICATE — keep in step with the app's
150
+ // `src-tauri/src/platform_filter.rs`, which documents it in full:
151
+ //
152
+ // a crate that is not RESOLVED FOR THE HOST is platform-inactive
153
+ //
154
+ // Two independent facts can make that true, and both are recorded because
155
+ // the copy differs: (1) the manifest gates it behind a `cfg(...)` for a
156
+ // target this machine is not — `targetActiveOnHost(declaredTarget)`, the
157
+ // app's `project_dependencies.target_cfg`; (2) cargo builds it for no
158
+ // target/feature combination here — `builtOnHost`, the app's
159
+ // `user_dependencies.target_cfg = 'lockfile-only'` (schema 122). Neither
160
+ // fact alone is the whole picture: (1) covers only DIRECT deps, and (2) is
161
+ // the only one that can see a transitive.
162
+ const hostCrates = ecosystem === "crates.io" ? activeCratesForHost(cwd) : null;
163
+ const triple = hostCrates ? hostTriple() : null;
164
+ for (const { name, version, dev } of instances) {
101
165
  const normalized = normalizePackageName(name, ecosystem);
102
- const isDirect = directNames.has(normalized) || devNames.has(normalized);
103
- // Transitive deps from the lockfile carry no per-dep target info (Cargo.lock
104
- // doesn't encode it), so they are treated as unconditionally active.
105
- const target = targets[name] ?? null;
166
+ // Direct only for the copy a manifest dependency resolves to; another
167
+ // installed version of the same name is somebody else's transitive.
168
+ const isDirect = (directNames.has(normalized) || devNames.has(normalized)) && versionMap.get(name) === version;
169
+ const declaredTarget = targets[name] ?? null;
170
+ const builtOnHost = hostCrates ? hostCrates.has(name) : true;
171
+ // Prefer the manifest's own cfg() spec when it has one — it is the more
172
+ // precise, human-readable explanation. Fall back to naming the triple the
173
+ // crate is absent from.
174
+ const target = declaredTarget ?? (builtOnHost || !triple ? null : `not built for ${triple}`);
106
175
  const candidate = {
107
176
  name,
108
177
  version,
109
178
  ecosystem,
110
- isDev: devNames.has(normalized),
179
+ // A direct dependency's scope is its manifest section; a transitive's is
180
+ // whatever the lockfile recorded for that copy (npm `dev`, pnpm 5/6
181
+ // `dev`, Pipfile `develop`, poetry `category`), else unknown.
182
+ isDev: isDirect ? devNames.has(normalized) : dev === true,
111
183
  isDirect,
112
- devScopeKnown: isDirect,
184
+ devScopeKnown: isDirect || dev !== undefined,
113
185
  target,
114
- platformActive: targetActiveOnHost(target),
186
+ platformActive: targetActiveOnHost(declaredTarget) && builtOnHost,
187
+ sourceDirs: [cwd],
115
188
  };
116
189
  const key = dependencyKey(candidate);
117
190
  if (!seen.has(key)) {
@@ -121,293 +194,10 @@ export function resolveAuditVersions(cwd, deps, devDeps, language, targets = {})
121
194
  }
122
195
  return results;
123
196
  }
124
- function resolveVersionMap(cwd, ecosystem) {
125
- const resolvers = {
126
- npm: () => resolveNpm(cwd),
127
- "crates.io": () => resolveRust(cwd),
128
- PyPI: () => resolvePython(cwd),
129
- Go: () => resolveGo(cwd),
130
- };
131
- return resolvers[ecosystem]?.() || new Map();
132
- }
133
197
  function normalizePackageName(name, ecosystem) {
134
- return ecosystem === "PyPI" ? name.toLowerCase() : name;
198
+ // PEP 503: the lockfile readers store Python names normalised the same way.
199
+ return ecosystem === "PyPI" ? normalizePythonName(name) : name;
135
200
  }
136
201
  function dependencyKey(dep) {
137
202
  return `${dep.ecosystem}\0${normalizePackageName(dep.name, dep.ecosystem)}\0${dep.version ?? ""}`;
138
203
  }
139
- // =============================================================================
140
- // npm: package-lock.json > pnpm-lock.yaml > yarn.lock > package.json ranges
141
- // =============================================================================
142
- function resolveNpm(cwd) {
143
- const versions = new Map();
144
- // Try package-lock.json first (npm)
145
- const lockPath = path.join(cwd, "package-lock.json");
146
- if (fs.existsSync(lockPath)) {
147
- try {
148
- const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
149
- // v2/v3 format: packages["node_modules/name"].version
150
- if (lock.packages) {
151
- for (const [key, value] of Object.entries(lock.packages)) {
152
- const name = packageNameFromNodeModulesPath(key);
153
- if (name && value.version) {
154
- versions.set(name, value.version);
155
- }
156
- }
157
- }
158
- // v1 format fallback: dependencies.name.version
159
- if (versions.size === 0 && lock.dependencies) {
160
- for (const [name, value] of Object.entries(lock.dependencies)) {
161
- if (value.version) {
162
- versions.set(name, value.version);
163
- }
164
- }
165
- }
166
- if (versions.size > 0)
167
- return versions;
168
- }
169
- catch { /* fall through */ }
170
- }
171
- // Try pnpm-lock.yaml (line-by-line, no YAML parser)
172
- const pnpmLockPath = path.join(cwd, "pnpm-lock.yaml");
173
- if (fs.existsSync(pnpmLockPath)) {
174
- try {
175
- const content = fs.readFileSync(pnpmLockPath, "utf-8");
176
- // pnpm v9 importers format: dependency name on one line, version on the next
177
- // Example:
178
- // '@modelcontextprotocol/sdk':
179
- // specifier: ^1.10.0
180
- // version: 1.26.0(zod@4.3.6)
181
- const lines = content.split("\n");
182
- let currentPkg = null;
183
- for (const line of lines) {
184
- // Match dependency name (with or without quotes, scoped packages)
185
- const pkgMatch = line.match(/^\s{4,8}'?(@?[^':]+)'?:\s*$/);
186
- if (pkgMatch) {
187
- currentPkg = pkgMatch[1].trim();
188
- continue;
189
- }
190
- // Match version line under a dependency
191
- if (currentPkg) {
192
- const versionMatch = line.match(/^\s+version:\s+['"]?(\d+\.\d+[^('"\s]*)['"]?/);
193
- if (versionMatch) {
194
- versions.set(currentPkg, versionMatch[1]);
195
- currentPkg = null;
196
- continue;
197
- }
198
- // specifier line — skip, wait for version
199
- if (line.match(/^\s+specifier:/))
200
- continue;
201
- // Any other line resets current package context
202
- if (!line.match(/^\s/))
203
- currentPkg = null;
204
- }
205
- }
206
- // Also parse package/snapshot keys. Requiring a numeric version after
207
- // the separator handles scoped packages without splitting at their
208
- // leading `@` (for example '@scope/pkg@1.2.3').
209
- const pkgRegex = /^\s{2}'?(.+?)@(\d+[^:('"]*)[^:]*'?:\s*$/gm;
210
- let match;
211
- while ((match = pkgRegex.exec(content)) !== null) {
212
- const name = match[1].replace(/^\//, "").trim();
213
- const version = match[2].trim().replace(/['":]/g, "");
214
- if (name && version && /^\d/.test(version) && !versions.has(name)) {
215
- versions.set(name, version);
216
- }
217
- }
218
- if (versions.size > 0)
219
- return versions;
220
- }
221
- catch { /* fall through */ }
222
- }
223
- // Try yarn.lock
224
- const yarnLockPath = path.join(cwd, "yarn.lock");
225
- if (fs.existsSync(yarnLockPath)) {
226
- try {
227
- const content = fs.readFileSync(yarnLockPath, "utf-8");
228
- // Format: "name@range":\n version "x.y.z"
229
- const blocks = content.split(/\n(?=\S)/);
230
- for (const block of blocks) {
231
- const headerMatch = block.match(/^"?([^@\s"]+)@/);
232
- const versionMatch = block.match(/^\s+version\s+"([^"]+)"/m);
233
- if (headerMatch && versionMatch) {
234
- versions.set(headerMatch[1], versionMatch[1]);
235
- }
236
- }
237
- if (versions.size > 0)
238
- return versions;
239
- }
240
- catch { /* fall through */ }
241
- }
242
- // Fallback: extract version ranges from package.json
243
- const pkgPath = path.join(cwd, "package.json");
244
- if (fs.existsSync(pkgPath)) {
245
- try {
246
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
247
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
248
- for (const [name, spec] of Object.entries(allDeps)) {
249
- const version = extractVersionFromSpec(spec);
250
- if (version)
251
- versions.set(name, version);
252
- }
253
- }
254
- catch { /* skip */ }
255
- }
256
- return versions;
257
- }
258
- function packageNameFromNodeModulesPath(lockPath) {
259
- const marker = "node_modules/";
260
- const index = lockPath.lastIndexOf(marker);
261
- return index === -1 ? "" : lockPath.slice(index + marker.length);
262
- }
263
- // =============================================================================
264
- // Rust: Cargo.lock
265
- // =============================================================================
266
- function resolveRust(cwd) {
267
- const versions = new Map();
268
- const lockPath = path.join(cwd, "Cargo.lock");
269
- if (!fs.existsSync(lockPath)) {
270
- // Fallback: parse Cargo.toml version strings
271
- return resolveRustFromManifest(cwd);
272
- }
273
- try {
274
- const content = fs.readFileSync(lockPath, "utf-8");
275
- // [[package]]\nname = "serde"\nversion = "1.0.193"
276
- const packageRegex = /\[\[package\]\]\s*\nname\s*=\s*"([^"]+)"\s*\nversion\s*=\s*"([^"]+)"/g;
277
- let match;
278
- while ((match = packageRegex.exec(content)) !== null) {
279
- versions.set(match[1], match[2]);
280
- }
281
- }
282
- catch { /* skip */ }
283
- return versions;
284
- }
285
- function resolveRustFromManifest(cwd) {
286
- const versions = new Map();
287
- const cargoPath = path.join(cwd, "Cargo.toml");
288
- if (!fs.existsSync(cargoPath))
289
- return versions;
290
- try {
291
- const content = fs.readFileSync(cargoPath, "utf-8");
292
- // name = "1.0" or name = { version = "1.0", ... }
293
- const depRegex = /^([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]+)"|.*?version\s*=\s*"([^"]+)")/gm;
294
- let match;
295
- while ((match = depRegex.exec(content)) !== null) {
296
- const name = match[1];
297
- const version = match[2] || match[3];
298
- if (version && /^\d/.test(version)) {
299
- versions.set(name, version);
300
- }
301
- }
302
- }
303
- catch { /* skip */ }
304
- return versions;
305
- }
306
- // =============================================================================
307
- // Python: poetry.lock > Pipfile.lock > requirements.txt
308
- // =============================================================================
309
- function resolvePython(cwd) {
310
- const versions = new Map();
311
- // poetry.lock
312
- const poetryLock = path.join(cwd, "poetry.lock");
313
- if (fs.existsSync(poetryLock)) {
314
- try {
315
- const content = fs.readFileSync(poetryLock, "utf-8");
316
- const packageRegex = /\[\[package\]\]\s*\nname\s*=\s*"([^"]+)"\s*\nversion\s*=\s*"([^"]+)"/g;
317
- let match;
318
- while ((match = packageRegex.exec(content)) !== null) {
319
- versions.set(match[1].toLowerCase(), match[2]);
320
- }
321
- if (versions.size > 0)
322
- return versions;
323
- }
324
- catch { /* fall through */ }
325
- }
326
- // Pipfile.lock
327
- const pipfileLock = path.join(cwd, "Pipfile.lock");
328
- if (fs.existsSync(pipfileLock)) {
329
- try {
330
- const lock = JSON.parse(fs.readFileSync(pipfileLock, "utf-8"));
331
- const sections = [lock.default, lock.develop].filter(Boolean);
332
- for (const section of sections) {
333
- for (const [name, info] of Object.entries(section)) {
334
- const version = info.version;
335
- if (version) {
336
- versions.set(name.toLowerCase(), version.replace(/^==/, ""));
337
- }
338
- }
339
- }
340
- if (versions.size > 0)
341
- return versions;
342
- }
343
- catch { /* fall through */ }
344
- }
345
- // requirements.txt pinned versions
346
- const reqPath = path.join(cwd, "requirements.txt");
347
- if (fs.existsSync(reqPath)) {
348
- try {
349
- const content = fs.readFileSync(reqPath, "utf-8");
350
- for (const line of content.split("\n")) {
351
- const match = line.trim().match(/^([a-zA-Z0-9_-]+)\s*==\s*(.+)/);
352
- if (match) {
353
- versions.set(match[1].toLowerCase(), match[2].trim());
354
- }
355
- }
356
- }
357
- catch { /* skip */ }
358
- }
359
- return versions;
360
- }
361
- // =============================================================================
362
- // Go: go.sum > go.mod
363
- // =============================================================================
364
- function resolveGo(cwd) {
365
- const versions = new Map();
366
- // go.sum has exact versions
367
- const goSumPath = path.join(cwd, "go.sum");
368
- if (fs.existsSync(goSumPath)) {
369
- try {
370
- const content = fs.readFileSync(goSumPath, "utf-8");
371
- for (const line of content.split("\n")) {
372
- const match = line.match(/^(\S+)\s+(v[\d.]+)/);
373
- if (match) {
374
- versions.set(match[1], match[2]);
375
- }
376
- }
377
- if (versions.size > 0)
378
- return versions;
379
- }
380
- catch { /* fall through */ }
381
- }
382
- // Fallback: go.mod require block
383
- const goModPath = path.join(cwd, "go.mod");
384
- if (fs.existsSync(goModPath)) {
385
- try {
386
- const content = fs.readFileSync(goModPath, "utf-8");
387
- const requireMatch = content.match(/require\s*\(([\s\S]*?)\)/);
388
- if (requireMatch) {
389
- for (const line of requireMatch[1].split("\n")) {
390
- const match = line.trim().match(/^(\S+)\s+(v[\d.]+)/);
391
- if (match) {
392
- versions.set(match[1], match[2]);
393
- }
394
- }
395
- }
396
- }
397
- catch { /* skip */ }
398
- }
399
- return versions;
400
- }
401
- // =============================================================================
402
- // Helpers
403
- // =============================================================================
404
- /**
405
- * Extract a usable version from an npm version specifier.
406
- * "^4.18.2" -> "4.18.2", "~1.0.0" -> "1.0.0", ">=2.0.0" -> "2.0.0"
407
- */
408
- function extractVersionFromSpec(spec) {
409
- if (!spec)
410
- return null;
411
- const match = spec.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/);
412
- return match ? match[1] : null;
413
- }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * better-sqlite3's native module: a real check and an actionable fix.
3
+ *
4
+ * `import("better-sqlite3")` succeeds even when the compiled module is
5
+ * missing; the failure comes at the first `new Database()`. Measured
6
+ * 2026-10-03 on node:24-bookworm-slim with npm 12.2.0, which blocks
7
+ * dependency install scripts not covered by `allowScripts`: `npm install`
8
+ * (local or -g) left no better_sqlite3.node, `--doctor` still reported the
9
+ * bindings as loaded, and every tool failed with "Could not locate the
10
+ * bindings file" and no hint why. `npx @4da/mcp-server` was unaffected.
11
+ */
12
+ import type BetterSqlite3 from "better-sqlite3";
13
+ export declare const NATIVE_BINDING_FIX: string;
14
+ /** Errors that mean the compiled module is absent or built for another Node/platform. */
15
+ export declare function isNativeBindingError(err: unknown): boolean;
16
+ /** The first line of a native-binding error, with the fix appended. */
17
+ export declare function nativeBindingMessage(err: unknown): string;
18
+ /** Null when an in-memory database opens; otherwise why not, with the fix. */
19
+ export declare function checkNativeBindings(Database: typeof BetterSqlite3): string | null;
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * better-sqlite3's native module: a real check and an actionable fix.
4
+ *
5
+ * `import("better-sqlite3")` succeeds even when the compiled module is
6
+ * missing; the failure comes at the first `new Database()`. Measured
7
+ * 2026-10-03 on node:24-bookworm-slim with npm 12.2.0, which blocks
8
+ * dependency install scripts not covered by `allowScripts`: `npm install`
9
+ * (local or -g) left no better_sqlite3.node, `--doctor` still reported the
10
+ * bindings as loaded, and every tool failed with "Could not locate the
11
+ * bindings file" and no hint why. `npx @4da/mcp-server` was unaffected.
12
+ */
13
+ // Each remedy measured on npm 12.2.0 (node:24-bookworm-slim), 2026-10-03.
14
+ export const NATIVE_BINDING_FIX = "npm 12+ blocks dependency install scripts unless allowed. With npx: allow it once with " +
15
+ "`npm config set allow-scripts=better-sqlite3 --location=user` (or run `npx --allow-scripts=better-sqlite3 @4da/mcp-server`), " +
16
+ "then clear the npx cache (`npx clear-npx-cache`, or delete the _npx folder in your npm cache). " +
17
+ "In an npm install: `npm install-scripts approve better-sqlite3`, then `npm rebuild better-sqlite3`. " +
18
+ "Without a prebuilt binary for your platform the rebuild needs a C++ toolchain " +
19
+ "(macOS: xcode-select --install; Debian/Ubuntu: apt install build-essential python3; Windows: Visual Studio Build Tools).";
20
+ /** Errors that mean the compiled module is absent or built for another Node/platform. */
21
+ export function isNativeBindingError(err) {
22
+ const msg = err instanceof Error ? err.message : String(err);
23
+ return /bindings file|better_sqlite3\.node|NODE_MODULE_VERSION|compiled against a different Node\.js version|invalid ELF header|not a valid Win32 application/i.test(msg);
24
+ }
25
+ /** The first line of a native-binding error, with the fix appended. */
26
+ export function nativeBindingMessage(err) {
27
+ const first = (err instanceof Error ? err.message : String(err)).split("\n")[0].replace(/\s*Tried:\s*$/, "").trim();
28
+ return `better-sqlite3's native module is not usable (${first}). ${NATIVE_BINDING_FIX}`;
29
+ }
30
+ /** Null when an in-memory database opens; otherwise why not, with the fix. */
31
+ export function checkNativeBindings(Database) {
32
+ try {
33
+ new Database(":memory:").close();
34
+ return null;
35
+ }
36
+ catch (err) {
37
+ return nativeBindingMessage(err);
38
+ }
39
+ }