@4da/mcp-server 5.0.3 → 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.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,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
|
+
}
|
package/dist/project-scanner.js
CHANGED
|
@@ -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
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
243
|
+
if (fs.existsSync(requirementsPath)) {
|
|
241
244
|
langSet.add("python");
|
|
242
|
-
const pyDeps = [];
|
|
243
245
|
try {
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
package/dist/prompts.js
ADDED
|
@@ -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
|
+
}
|