@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,40 @@
1
+ /**
2
+ * Direct-dependency names from Python, Go and Cargo-workspace manifests, for
3
+ * the project scanner (project-scanner.ts keeps npm and single-crate Cargo).
4
+ *
5
+ * Measured 2026-10-02, each gap below hid a whole project from the scan:
6
+ * - Poetry projects declare dependencies under `[tool.poetry.dependencies]`;
7
+ * the scanner only looked for `dependencies = [`, found nothing, and never
8
+ * created a Python group — poetry 1.0's own lockfile (131 advisories per
9
+ * osv-scanner) scanned as zero dependencies.
10
+ * - A Cargo virtual workspace root (`[workspace]`, no `[package]`) declares
11
+ * its dependencies in `[workspace.dependencies]` and in member manifests;
12
+ * victauri's whole Cargo.lock (10 advisories per cargo audit) was ignored.
13
+ * - go.mod `// indirect` requirements are transitive, not direct.
14
+ */
15
+ /** The body of a TOML table (`[name]`) up to the next table header, or null. */
16
+ export declare function tomlTable(content: string, name: string): string | null;
17
+ /**
18
+ * Direct Python dependencies from pyproject.toml: PEP 621 `[project]`
19
+ * `dependencies`, `[project.optional-dependencies]` and PEP 735
20
+ * `[dependency-groups]` (dev), Poetry's `[tool.poetry.dependencies]` and its
21
+ * dev groups, and uv's `[tool.uv] dev-dependencies`. Names are PEP 503 normalised.
22
+ */
23
+ export declare function parsePyprojectDependencies(content: string): {
24
+ deps: string[];
25
+ devDeps: string[];
26
+ };
27
+ /** Requirement names (pins and ranges alike) from a requirements file, PEP 503 normalised. */
28
+ export declare function parseRequirementNames(content: string): string[];
29
+ /** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
30
+ export declare function parseGoModDirectDeps(content: string): string[];
31
+ /**
32
+ * Dependencies of a Cargo workspace root: `[workspace.dependencies]` plus the
33
+ * `[dependencies]`/`[dev-dependencies]`/`[build-dependencies]` of every member
34
+ * (`members = ["crates/*", "app"]`, one-level `*` globs). Member crates
35
+ * themselves are excluded: they are this project's own code.
36
+ */
37
+ export declare function cargoWorkspaceDeps(root: string, content: string): {
38
+ deps: string[];
39
+ devDeps: string[];
40
+ };
@@ -0,0 +1,193 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Direct-dependency names from Python, Go and Cargo-workspace manifests, for
4
+ * the project scanner (project-scanner.ts keeps npm and single-crate Cargo).
5
+ *
6
+ * Measured 2026-10-02, each gap below hid a whole project from the scan:
7
+ * - Poetry projects declare dependencies under `[tool.poetry.dependencies]`;
8
+ * the scanner only looked for `dependencies = [`, found nothing, and never
9
+ * created a Python group — poetry 1.0's own lockfile (131 advisories per
10
+ * osv-scanner) scanned as zero dependencies.
11
+ * - A Cargo virtual workspace root (`[workspace]`, no `[package]`) declares
12
+ * its dependencies in `[workspace.dependencies]` and in member manifests;
13
+ * victauri's whole Cargo.lock (10 advisories per cargo audit) was ignored.
14
+ * - go.mod `// indirect` requirements are transitive, not direct.
15
+ */
16
+ import * as fs from "node:fs";
17
+ import * as path from "node:path";
18
+ import { normalizePythonName, parseGoMod } from "./live/lockfile-parsers-pygo.js";
19
+ /** The body of a TOML table (`[name]`) up to the next table header, or null. */
20
+ export function tomlTable(content, name) {
21
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22
+ const header = new RegExp(`^\\[${escaped}\\]\\s*$`, "m").exec(content);
23
+ if (!header)
24
+ return null;
25
+ const rest = content.slice(header.index + header[0].length);
26
+ const next = rest.search(/^\[/m);
27
+ return next === -1 ? rest : rest.slice(0, next);
28
+ }
29
+ /** Keys of a TOML table body: `name = ...` lines, skipping comments. */
30
+ function tomlKeys(body) {
31
+ if (!body)
32
+ return [];
33
+ const keys = [];
34
+ for (const line of body.split(/\r?\n/)) {
35
+ const m = /^\s*"?([A-Za-z0-9_.-]+)"?\s*=/.exec(line);
36
+ if (m && !line.trim().startsWith("#"))
37
+ keys.push(m[1]);
38
+ }
39
+ return keys;
40
+ }
41
+ /**
42
+ * PEP 508 requirement strings inside a TOML array literal:
43
+ * `"requests[socks]>=2; python_version>'3'"` -> requests. String literals are
44
+ * read whole, so a quote INSIDE one (an environment marker) is not a new entry.
45
+ */
46
+ function requirementNames(arrayBody) {
47
+ const names = [];
48
+ for (const m of arrayBody.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)) {
49
+ const name = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(m[1] ?? m[2] ?? "");
50
+ if (name)
51
+ names.push(name[1]);
52
+ }
53
+ return names;
54
+ }
55
+ /**
56
+ * The array literal assigned to `key` inside `body` (multi-line), or "".
57
+ * Brackets inside quoted strings do not close it: `"requests[socks]>=2"` is
58
+ * an ordinary PEP 508 entry, and a non-greedy `\[...\]` stopped at its `]`
59
+ * and dropped every dependency after it.
60
+ */
61
+ function tomlArray(body, key) {
62
+ if (!body)
63
+ return "";
64
+ const start = new RegExp(`^\\s*${key}\\s*=\\s*\\[`, "m").exec(body);
65
+ if (!start)
66
+ return "";
67
+ let depth = 1;
68
+ let quote = null;
69
+ const from = start.index + start[0].length;
70
+ for (let i = from; i < body.length; i++) {
71
+ const ch = body[i];
72
+ if (quote) {
73
+ if (ch === "\\")
74
+ i++;
75
+ else if (ch === quote)
76
+ quote = null;
77
+ }
78
+ else if (ch === '"' || ch === "'")
79
+ quote = ch;
80
+ else if (ch === "[")
81
+ depth++;
82
+ else if (ch === "]" && --depth === 0)
83
+ return body.slice(from, i);
84
+ }
85
+ return body.slice(from);
86
+ }
87
+ /**
88
+ * Direct Python dependencies from pyproject.toml: PEP 621 `[project]`
89
+ * `dependencies`, `[project.optional-dependencies]` and PEP 735
90
+ * `[dependency-groups]` (dev), Poetry's `[tool.poetry.dependencies]` and its
91
+ * dev groups, and uv's `[tool.uv] dev-dependencies`. Names are PEP 503 normalised.
92
+ */
93
+ export function parsePyprojectDependencies(content) {
94
+ const deps = new Set();
95
+ const devDeps = new Set();
96
+ const add = (set, names) => {
97
+ for (const name of names) {
98
+ const normalized = normalizePythonName(name);
99
+ if (normalized !== "python")
100
+ set.add(normalized);
101
+ }
102
+ };
103
+ add(deps, requirementNames(tomlArray(tomlTable(content, "project"), "dependencies")));
104
+ const optional = tomlTable(content, "project.optional-dependencies");
105
+ if (optional)
106
+ add(deps, requirementNames(optional.replace(/^\s*[A-Za-z0-9_.-]+\s*=/gm, "")));
107
+ add(deps, tomlKeys(tomlTable(content, "tool.poetry.dependencies")));
108
+ add(devDeps, tomlKeys(tomlTable(content, "tool.poetry.dev-dependencies")));
109
+ for (const m of content.matchAll(/^\[tool\.poetry\.group\.([A-Za-z0-9_-]+)\.dependencies\]\s*$/gm)) {
110
+ add(devDeps, tomlKeys(tomlTable(content, `tool.poetry.group.${m[1]}.dependencies`)));
111
+ }
112
+ add(devDeps, requirementNames(tomlArray(tomlTable(content, "tool.uv"), "dev-dependencies")));
113
+ const groups = tomlTable(content, "dependency-groups");
114
+ if (groups)
115
+ add(devDeps, requirementNames(groups.replace(/^\s*[A-Za-z0-9_.-]+\s*=/gm, "")));
116
+ for (const name of deps)
117
+ devDeps.delete(name);
118
+ return { deps: [...deps], devDeps: [...devDeps] };
119
+ }
120
+ /** Requirement names (pins and ranges alike) from a requirements file, PEP 503 normalised. */
121
+ export function parseRequirementNames(content) {
122
+ const names = [];
123
+ for (const raw of content.replace(/\\\r?\n/g, " ").split(/\r?\n/)) {
124
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
125
+ if (!line || line.startsWith("-"))
126
+ continue;
127
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line);
128
+ if (m)
129
+ names.push(normalizePythonName(m[1]));
130
+ }
131
+ return [...new Set(names)];
132
+ }
133
+ /** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
134
+ export function parseGoModDirectDeps(content) {
135
+ return parseGoMod(content)
136
+ .requires.filter((r) => !r.indirect)
137
+ .map((r) => r.module);
138
+ }
139
+ /**
140
+ * Dependencies of a Cargo workspace root: `[workspace.dependencies]` plus the
141
+ * `[dependencies]`/`[dev-dependencies]`/`[build-dependencies]` of every member
142
+ * (`members = ["crates/*", "app"]`, one-level `*` globs). Member crates
143
+ * themselves are excluded: they are this project's own code.
144
+ */
145
+ export function cargoWorkspaceDeps(root, content) {
146
+ const workspace = tomlTable(content, "workspace");
147
+ if (workspace === null)
148
+ return { deps: [], devDeps: [] };
149
+ const deps = new Set(tomlKeys(tomlTable(content, "workspace.dependencies")));
150
+ const devDeps = new Set();
151
+ const memberNames = new Set();
152
+ const members = [...tomlArray(workspace, "members").matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
153
+ for (const pattern of members) {
154
+ for (const dir of expandMemberGlob(root, pattern)) {
155
+ let manifest;
156
+ try {
157
+ manifest = fs.readFileSync(path.join(dir, "Cargo.toml"), "utf-8");
158
+ }
159
+ catch {
160
+ continue;
161
+ }
162
+ const name = /^name\s*=\s*"([^"]+)"/m.exec(tomlTable(manifest, "package") ?? "")?.[1];
163
+ if (name)
164
+ memberNames.add(name);
165
+ for (const dep of tomlKeys(tomlTable(manifest, "dependencies")))
166
+ deps.add(dep);
167
+ for (const section of ["dev-dependencies", "build-dependencies"]) {
168
+ for (const dep of tomlKeys(tomlTable(manifest, section)))
169
+ devDeps.add(dep);
170
+ }
171
+ }
172
+ }
173
+ const strip = (set) => [...set].filter((d) => !memberNames.has(d));
174
+ const runtime = strip(deps);
175
+ return { deps: runtime, devDeps: strip(devDeps).filter((d) => !runtime.includes(d)) };
176
+ }
177
+ /** Member directories for one `members` entry: a path, or a path whose last segment is `*`. */
178
+ function expandMemberGlob(root, pattern) {
179
+ if (!pattern.includes("*"))
180
+ return [path.join(root, pattern)];
181
+ const star = pattern.indexOf("*");
182
+ const parent = path.join(root, pattern.slice(0, star));
183
+ try {
184
+ return fs
185
+ .readdirSync(parent, { withFileTypes: true })
186
+ .filter((e) => e.isDirectory())
187
+ .map((e) => path.join(parent, e.name))
188
+ .filter((dir) => fs.existsSync(path.join(dir, "Cargo.toml")));
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ }
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
+ import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, parseRequirementNames, } from "./project-manifests.js";
16
17
  // =============================================================================
17
18
  // Framework Detection Maps
18
19
  // =============================================================================
@@ -193,6 +194,19 @@ export function scanCurrentProject(cwd) {
193
194
  // Platform-gated deps: [target.'cfg(...)'.dependencies] / [target.<triple>.dependencies].
194
195
  // Previously skipped entirely (e.g. windows-sys, winreg, libc were invisible).
195
196
  parseTargetDependencies(content, rustDeps, rustDevDeps, result.depTargets, fwSet, RUST_FRAMEWORK_MAP);
197
+ // A workspace root declares dependencies for its members; a virtual one
198
+ // (no [package]) declares nothing else, and was scanned as empty.
199
+ const workspace = cargoWorkspaceDeps(cwd, content);
200
+ for (const dep of workspace.deps) {
201
+ if (!rustDeps.includes(dep))
202
+ rustDeps.push(dep);
203
+ if (dep in RUST_FRAMEWORK_MAP)
204
+ fwSet.add(RUST_FRAMEWORK_MAP[dep]);
205
+ }
206
+ for (const dep of workspace.devDeps) {
207
+ if (!rustDeps.includes(dep) && !rustDevDeps.includes(dep))
208
+ rustDevDeps.push(dep);
209
+ }
196
210
  // Add to flat arrays for backward compat
197
211
  result.dependencies.push(...rustDeps);
198
212
  result.devDependencies.push(...rustDevDeps);
@@ -211,59 +225,42 @@ export function scanCurrentProject(cwd) {
211
225
  const pyprojectPath = path.join(cwd, "pyproject.toml");
212
226
  const requirementsPath = path.join(cwd, "requirements.txt");
213
227
  const setupPyPath = path.join(cwd, "setup.py");
228
+ // pyproject.toml (PEP 621, Poetry, uv) and requirements.txt are read
229
+ // together: a project often has both, and the old else-if dropped the second.
230
+ const pyDeps = new Set();
231
+ const pyDevDeps = new Set();
214
232
  if (fs.existsSync(pyprojectPath)) {
215
233
  langSet.add("python");
216
- const pyDeps = [];
217
234
  try {
218
- const content = fs.readFileSync(pyprojectPath, "utf-8");
219
- // Extract dependencies from pyproject.toml
220
- const depsMatch = content.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
221
- if (depsMatch) {
222
- const deps = depsMatch[1].match(/"([^">=<!\s]+)/g);
223
- if (deps) {
224
- for (const dep of deps) {
225
- const name = dep.replace(/^"/, "");
226
- result.dependencies.push(name);
227
- pyDeps.push(name);
228
- detectPythonFramework(name, fwSet);
229
- }
230
- }
231
- }
235
+ const parsed = parsePyprojectDependencies(fs.readFileSync(pyprojectPath, "utf-8"));
236
+ parsed.deps.forEach((d) => pyDeps.add(d));
237
+ parsed.devDeps.forEach((d) => pyDevDeps.add(d));
232
238
  }
233
239
  catch {
234
240
  // Skip
235
241
  }
236
- if (pyDeps.length > 0) {
237
- result.depsByEcosystem["python"] = { deps: pyDeps, devDeps: [] };
238
- }
239
242
  }
240
- else if (fs.existsSync(requirementsPath)) {
243
+ if (fs.existsSync(requirementsPath)) {
241
244
  langSet.add("python");
242
- const pyDeps = [];
243
245
  try {
244
- const content = fs.readFileSync(requirementsPath, "utf-8");
245
- for (const line of content.split("\n")) {
246
- const trimmed = line.trim();
247
- if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("-")) {
248
- const name = trimmed.split(/[>=<!\[]/)[0].trim();
249
- if (name) {
250
- result.dependencies.push(name);
251
- pyDeps.push(name);
252
- detectPythonFramework(name, fwSet);
253
- }
254
- }
255
- }
246
+ parseRequirementNames(fs.readFileSync(requirementsPath, "utf-8")).forEach((d) => pyDeps.add(d));
256
247
  }
257
248
  catch {
258
249
  // Skip
259
250
  }
260
- if (pyDeps.length > 0) {
261
- result.depsByEcosystem["python"] = { deps: pyDeps, devDeps: [] };
262
- }
263
251
  }
264
- else if (fs.existsSync(setupPyPath)) {
252
+ if (fs.existsSync(setupPyPath)) {
265
253
  langSet.add("python");
266
254
  }
255
+ for (const name of pyDeps) {
256
+ pyDevDeps.delete(name);
257
+ result.dependencies.push(name);
258
+ detectPythonFramework(name, fwSet);
259
+ }
260
+ result.devDependencies.push(...pyDevDeps);
261
+ if (pyDeps.size > 0 || pyDevDeps.size > 0) {
262
+ result.depsByEcosystem["python"] = { deps: [...pyDeps], devDeps: [...pyDevDeps] };
263
+ }
267
264
  // -------------------------------------------------------------------------
268
265
  // 4. go.mod (Go ecosystem)
269
266
  // -------------------------------------------------------------------------
@@ -279,18 +276,11 @@ export function scanCurrentProject(cwd) {
279
276
  const parts = moduleMatch[1].split("/");
280
277
  result.projectName = parts[parts.length - 1];
281
278
  }
282
- // Extract require block dependencies
283
- const requireMatch = content.match(/require\s*\(([\s\S]*?)\)/);
284
- if (requireMatch) {
285
- for (const line of requireMatch[1].split("\n")) {
286
- const depMatch = line.trim().match(/^(\S+)\s+/);
287
- if (depMatch && !depMatch[1].startsWith("//")) {
288
- const dep = depMatch[1];
289
- result.dependencies.push(dep);
290
- goDeps.push(dep);
291
- detectGoFramework(dep, fwSet);
292
- }
293
- }
279
+ // Direct requirements only: `// indirect` lines are transitive.
280
+ for (const dep of parseGoModDirectDeps(content)) {
281
+ result.dependencies.push(dep);
282
+ goDeps.push(dep);
283
+ detectGoFramework(dep, fwSet);
294
284
  }
295
285
  }
296
286
  catch {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Every independently-locked project under a root, for standalone scanning.
3
+ *
4
+ * A repository root is often not where its lockfiles live: 4DA keeps
5
+ * `src-tauri/Cargo.lock`, `relay/Cargo.lock`, `site/pnpm-lock.yaml` beside a
6
+ * root `pnpm-lock.yaml`. Standalone mode used to read the root directory only,
7
+ * so `npx @4da/mcp-server` started at the repo root never scanned the Rust
8
+ * code at all (measured 2026-10-02: 0 of src-tauri's 791 crates). The desktop
9
+ * app does not have this problem — it records each dependency's own manifest
10
+ * directory — so this is the standalone counterpart of that inventory.
11
+ *
12
+ * A directory joins when it holds a lockfile (or go.mod / requirements.txt,
13
+ * which carry exact versions themselves). Workspace members that share their
14
+ * root's lockfile are covered by the root. Bounded: depth 2 below the root,
15
+ * at most 64 projects, and directories that hold installed code, build
16
+ * output, test fixtures or tool worktrees are skipped.
17
+ */
18
+ import { type ProjectScanResult } from "./project-scanner.js";
19
+ export interface ProjectTreeEntry {
20
+ dir: string;
21
+ scan: ProjectScanResult;
22
+ }
23
+ /**
24
+ * The root (always) and every locked project below it, each with its own
25
+ * scan. Directories and lockfiles the repository's .gitignore excludes are not
26
+ * part of the project and are skipped (gitignore.ts).
27
+ */
28
+ export declare function scanProjectTree(root: string): ProjectTreeEntry[];
29
+ /**
30
+ * Resolution groups for the live layer: one per (project directory, ecosystem).
31
+ * A lockfile whose manifest declared nothing the scanner could read still gets
32
+ * a group, so its installed packages are scanned (as transitives) rather than
33
+ * silently dropped.
34
+ */
35
+ export declare function treeResolutionGroups(entries: ProjectTreeEntry[]): Array<{
36
+ dir: string;
37
+ language: string;
38
+ deps: string[];
39
+ devDeps: string[];
40
+ targets: Record<string, string>;
41
+ }>;
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Every independently-locked project under a root, for standalone scanning.
4
+ *
5
+ * A repository root is often not where its lockfiles live: 4DA keeps
6
+ * `src-tauri/Cargo.lock`, `relay/Cargo.lock`, `site/pnpm-lock.yaml` beside a
7
+ * root `pnpm-lock.yaml`. Standalone mode used to read the root directory only,
8
+ * so `npx @4da/mcp-server` started at the repo root never scanned the Rust
9
+ * code at all (measured 2026-10-02: 0 of src-tauri's 791 crates). The desktop
10
+ * app does not have this problem — it records each dependency's own manifest
11
+ * directory — so this is the standalone counterpart of that inventory.
12
+ *
13
+ * A directory joins when it holds a lockfile (or go.mod / requirements.txt,
14
+ * which carry exact versions themselves). Workspace members that share their
15
+ * root's lockfile are covered by the root. Bounded: depth 2 below the root,
16
+ * at most 64 projects, and directories that hold installed code, build
17
+ * output, test fixtures or tool worktrees are skipped.
18
+ */
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { IgnoreRules } from "./gitignore.js";
22
+ import { scanCurrentProject } from "./project-scanner.js";
23
+ const LOCK_MARKERS = [
24
+ "package-lock.json",
25
+ "npm-shrinkwrap.json",
26
+ "pnpm-lock.yaml",
27
+ "yarn.lock",
28
+ "bun.lock",
29
+ "Cargo.lock",
30
+ "poetry.lock",
31
+ "uv.lock",
32
+ "Pipfile.lock",
33
+ "go.mod",
34
+ "requirements.txt",
35
+ ];
36
+ const SKIP_DIRS = new Set([
37
+ "node_modules", "target", ".git", "dist", "build", "out", "vendor", ".venv", "venv",
38
+ "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor", "fixtures",
39
+ "__fixtures__", "testdata", "test-fixtures", "__tests__", ".turbo", ".cache", ".pnpm-store",
40
+ ]);
41
+ const MAX_DEPTH = 2;
42
+ const MAX_PROJECTS = 64;
43
+ /**
44
+ * The root (always) and every locked project below it, each with its own
45
+ * scan. Directories and lockfiles the repository's .gitignore excludes are not
46
+ * part of the project and are skipped (gitignore.ts).
47
+ */
48
+ export function scanProjectTree(root) {
49
+ const entries = [{ dir: root, scan: scanCurrentProject(root) }];
50
+ const ignore = new IgnoreRules(root);
51
+ const walk = (dir, rel, depth) => {
52
+ if (depth > MAX_DEPTH || entries.length >= MAX_PROJECTS)
53
+ return;
54
+ ignore.load(rel);
55
+ let children;
56
+ try {
57
+ children = fs.readdirSync(dir, { withFileTypes: true });
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ for (const child of children) {
63
+ if (entries.length >= MAX_PROJECTS)
64
+ return;
65
+ if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
66
+ continue;
67
+ const childRel = rel ? `${rel}/${child.name}` : child.name;
68
+ if (ignore.ignores(childRel, true))
69
+ continue;
70
+ const sub = path.join(dir, child.name);
71
+ ignore.load(childRel);
72
+ const locked = LOCK_MARKERS.some((marker) => fs.existsSync(path.join(sub, marker)) && !ignore.ignores(`${childRel}/${marker}`, false));
73
+ if (locked)
74
+ entries.push({ dir: sub, scan: scanCurrentProject(sub) });
75
+ walk(sub, childRel, depth + 1);
76
+ }
77
+ };
78
+ walk(root, "", 1);
79
+ return entries;
80
+ }
81
+ /** Lockfiles that make an ecosystem scannable in a directory even when no direct dependency was parsed. */
82
+ const LOCKS_BY_LANGUAGE = {
83
+ npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
84
+ rust: ["Cargo.lock"],
85
+ python: ["poetry.lock", "uv.lock", "Pipfile.lock", "requirements.txt"],
86
+ go: ["go.mod"],
87
+ };
88
+ /**
89
+ * Resolution groups for the live layer: one per (project directory, ecosystem).
90
+ * A lockfile whose manifest declared nothing the scanner could read still gets
91
+ * a group, so its installed packages are scanned (as transitives) rather than
92
+ * silently dropped.
93
+ */
94
+ export function treeResolutionGroups(entries) {
95
+ return entries.flatMap(({ dir, scan }) => {
96
+ const byLanguage = { ...scan.depsByEcosystem };
97
+ for (const [language, locks] of Object.entries(LOCKS_BY_LANGUAGE)) {
98
+ if (!byLanguage[language] && locks.some((lock) => fs.existsSync(path.join(dir, lock)))) {
99
+ byLanguage[language] = { deps: [], devDeps: [] };
100
+ }
101
+ }
102
+ return Object.entries(byLanguage).map(([language, { deps, devDeps }]) => ({
103
+ dir,
104
+ language,
105
+ deps,
106
+ devDeps,
107
+ targets: scan.depTargets,
108
+ }));
109
+ });
110
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP prompts — user-invoked workflows (e.g. `/deps` in hosts that surface
3
+ * prompts as slash commands). A prompt is text handed to the agent; it does
4
+ * nothing on its own and never runs without the user choosing it.
5
+ */
6
+ export interface PromptDefinition {
7
+ name: string;
8
+ title: string;
9
+ description: string;
10
+ arguments: Array<{
11
+ name: string;
12
+ description: string;
13
+ required: boolean;
14
+ }>;
15
+ }
16
+ export declare const PROMPTS: PromptDefinition[];
17
+ export declare function listPrompts(): PromptDefinition[];
18
+ /** The prompt's messages, or null for an unknown prompt name. */
19
+ export declare function getPrompt(name: string, args: Record<string, string> | undefined): {
20
+ description: string;
21
+ messages: Array<{
22
+ role: "user";
23
+ content: {
24
+ type: "text";
25
+ text: string;
26
+ };
27
+ }>;
28
+ } | null;
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * MCP prompts — user-invoked workflows (e.g. `/deps` in hosts that surface
4
+ * prompts as slash commands). A prompt is text handed to the agent; it does
5
+ * nothing on its own and never runs without the user choosing it.
6
+ */
7
+ const DEPS_PROMPT = {
8
+ name: "deps",
9
+ title: "Safe dependency updates",
10
+ description: "Plan dependency upgrades, vet every bump with dependency_check, apply only the safe ones in small tested batches, and report the rest with evidence.",
11
+ arguments: [
12
+ {
13
+ name: "scope",
14
+ description: "Optional focus, e.g. a package name, \"security only\", or \"patch and minor only\".",
15
+ required: false,
16
+ },
17
+ ],
18
+ };
19
+ export const PROMPTS = [DEPS_PROMPT];
20
+ export function listPrompts() {
21
+ return PROMPTS;
22
+ }
23
+ /** The prompt's messages, or null for an unknown prompt name. */
24
+ export function getPrompt(name, args) {
25
+ if (name !== DEPS_PROMPT.name)
26
+ return null;
27
+ const scope = args?.scope?.trim();
28
+ return {
29
+ description: DEPS_PROMPT.description,
30
+ messages: [{ role: "user", content: { type: "text", text: depsWorkflow(scope) } }],
31
+ };
32
+ }
33
+ function depsWorkflow(scope) {
34
+ return [
35
+ "Update this project's dependencies safely, using the 4DA tools in this order.",
36
+ scope ? `Scope for this run: ${scope}. Leave everything outside it alone.` : "",
37
+ "",
38
+ "1. Call `upgrade_planner` to get the proposed upgrades (it runs the vulnerability scan itself; if it says it is CVE-blind, call it again once the scan completes). When its provenance is `app_plan`, each step is a work order: for every installed line it gives `installed`, `target`, `upgrade_type` and the project sites, plus a `mechanism`. Only touch steps whose sites include this project.",
39
+ "2. Call `dependency_check` on every proposed bump, passing `from` (the installed version) and `to` (the proposed version). Batch up to 25 items per call.",
40
+ "3. For each `proceed` item that is a major upgrade (a 0.x minor counts), call `upgrade_impact` with the package and target version first: it lists the breaking changes and the files here that import the package. Make the code changes it calls for in the same batch, and give each major its own batch.",
41
+ "4. Apply ONLY items whose verdict is `proceed`. Apply them in small batches (a few packages at a time, never majors mixed with patches), using the project's own package manager so the lockfile is updated. A `target` is a minimum: ask the package manager for at least that version and let it resolve the exact one. Follow the step's `mechanism`:",
42
+ " - `manifest_bump`: raise the declared version range in this project's manifest, then install.",
43
+ " - `lockfile_or_parent_update`: the package is not declared here. Update it within the existing ranges with the package manager's update command for that one package; if the ranges do not allow the target, update the direct dependency that pulls it in instead. Never add the transitive package as a new direct dependency.",
44
+ " - `mixed`: handle each site by whether it is direct or transitive, as above.",
45
+ " - `no_fix`: no fixed version exists yet. Do not change anything; report it.",
46
+ "5. After each batch, run the project's own tests (and its build or type-check if it has one). If anything fails, revert that batch, note which package caused it, and continue with the next batch.",
47
+ "6. Do NOT apply `review`, `avoid`, `wait` or `unknown` items. Stop on them and report each one to me with its verdict, reason, and the evidence from its signals, so I can decide. For `wait`, say when the release becomes old enough. If the package manager refuses a version because of its own policy (for example a minimum release age or a trust check), report that too; do not change the policy.",
48
+ "7. When done, call `vulnerability_scan` again and report: what was applied, what was skipped and why, test results per batch, and any advisories still open. If the app plan was used, say that a 4DA rescan will drop the finished steps.",
49
+ "",
50
+ "Never bypass a failing test, never edit a lockfile by hand, and never apply an item whose verdict you did not get from dependency_check.",
51
+ ]
52
+ .filter((line, i, all) => line !== "" || all[i - 1] !== "")
53
+ .join("\n");
54
+ }