@4da/mcp-server 5.0.4 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +698 -0
- package/README.md +70 -39
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -58
- package/dist/db.js +226 -299
- 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 +12 -1
- package/dist/live/osv-scanner.js +50 -15
- 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 -136
- package/package.json +35 -35
- 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,95 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Set operations over resolved dependencies, shared by the live layer.
|
|
4
|
+
* (Moved out of live/index.ts; behaviour unchanged except for the install
|
|
5
|
+
* fields, which did not exist before.)
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Deepest common ancestor of a set of directories (segment-wise, both slash
|
|
9
|
+
* styles). Null for an empty set; a single dir is its own root.
|
|
10
|
+
*/
|
|
11
|
+
export function commonPathRoot(dirs) {
|
|
12
|
+
if (dirs.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
const split = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").split("/");
|
|
15
|
+
let common = split(dirs[0]);
|
|
16
|
+
for (const dir of dirs.slice(1)) {
|
|
17
|
+
const parts = split(dir);
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < common.length && i < parts.length && common[i].toLowerCase() === parts[i].toLowerCase())
|
|
20
|
+
i++;
|
|
21
|
+
common = common.slice(0, i);
|
|
22
|
+
if (common.length === 0)
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
return common.length > 0 ? common.join("/") : null;
|
|
26
|
+
}
|
|
27
|
+
export function dedupeDependencies(deps) {
|
|
28
|
+
const unique = new Map();
|
|
29
|
+
for (const dep of deps) {
|
|
30
|
+
const key = `${dep.ecosystem}\0${dep.name}\0${dep.version ?? ""}`;
|
|
31
|
+
const existing = unique.get(key);
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.isDirect ||= dep.isDirect;
|
|
34
|
+
existing.devScopeKnown &&= dep.devScopeKnown;
|
|
35
|
+
existing.isDev = existing.devScopeKnown && existing.isDev && dep.isDev;
|
|
36
|
+
// A crate reachable via ANY active path is active, and its target label
|
|
37
|
+
// is then the active copy's: rsa 0.9.10, built for relay and not for
|
|
38
|
+
// src-tauri on Windows, read "not built for x86_64-pc-windows-msvc"
|
|
39
|
+
// beside platform_active: true (2026-10-02 agent eval).
|
|
40
|
+
if (dep.platformActive && !existing.platformActive)
|
|
41
|
+
existing.target = dep.target;
|
|
42
|
+
else if (existing.platformActive === dep.platformActive)
|
|
43
|
+
existing.target = existing.target ?? dep.target;
|
|
44
|
+
existing.platformActive ||= dep.platformActive;
|
|
45
|
+
// Union the provenance rather than discarding it — the same version can
|
|
46
|
+
// legitimately be pinned by several workspaces, and the reader needs all
|
|
47
|
+
// of them to know where to apply the fix.
|
|
48
|
+
for (const dir of dep.sourceDirs ?? []) {
|
|
49
|
+
if (!existing.sourceDirs.includes(dir))
|
|
50
|
+
existing.sourceDirs.push(dir);
|
|
51
|
+
}
|
|
52
|
+
mergeInstallContext(existing, dep);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
unique.set(key, { ...dep, sourceDirs: [...(dep.sourceDirs ?? [])] });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...unique.values()];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A drift entry exists only because node_modules holds a version the lockfile
|
|
62
|
+
* does not pin. When a lockfile really does pin that same version somewhere,
|
|
63
|
+
* the merged entry is a lockfile instance, not drift, and must not read as
|
|
64
|
+
* "only in node_modules". Two drift entries keep the first one's context.
|
|
65
|
+
* A drifted install reading wins over a matching one, so a merge never hides
|
|
66
|
+
* a disagreement.
|
|
67
|
+
*/
|
|
68
|
+
function mergeInstallContext(existing, dep) {
|
|
69
|
+
if (existing.installDriftOf !== undefined && dep.installDriftOf === undefined) {
|
|
70
|
+
existing.installDriftOf = undefined;
|
|
71
|
+
existing.installFix = undefined;
|
|
72
|
+
}
|
|
73
|
+
if (dep.installedVersion === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const drifted = (d) => d.installedVersion !== undefined && d.installedVersion !== null && d.installedVersion !== d.version;
|
|
76
|
+
if (existing.installedVersion === undefined || (drifted(dep) && !drifted(existing))) {
|
|
77
|
+
existing.installedVersion = dep.installedVersion;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function emptyVulnResult(projectPath, offline) {
|
|
81
|
+
return {
|
|
82
|
+
scannedAt: new Date().toISOString(),
|
|
83
|
+
projectPath,
|
|
84
|
+
ecosystemsScanned: [],
|
|
85
|
+
totalScanned: 0,
|
|
86
|
+
totalVulnerable: 0,
|
|
87
|
+
platformInactiveVulnerable: 0,
|
|
88
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 },
|
|
89
|
+
vulnerabilities: [],
|
|
90
|
+
cleanCount: 0,
|
|
91
|
+
scanDurationMs: 0,
|
|
92
|
+
cached: false,
|
|
93
|
+
offline,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transitive dev/runtime scope from the desktop app's dependency inventory.
|
|
3
|
+
*
|
|
4
|
+
* A lockfile read tells the MCP server whether a package is direct. For a
|
|
5
|
+
* transitive it cannot tell dev from runtime, so under the shared severity
|
|
6
|
+
* rule a transitive gets the clamp but never the dev discount. In
|
|
7
|
+
* full-database mode the app's `dependency_instances` table records every
|
|
8
|
+
* installed instance with `is_dev` and `scope`; this module reads it.
|
|
9
|
+
*
|
|
10
|
+
* Only a real determination counts: `is_dev = 1`, or a `scope` of runtime,
|
|
11
|
+
* dev or build. The app's current writers stamp every row `is_dev = 0,
|
|
12
|
+
* scope = 'unknown'` as a placeholder. Reading that as "known runtime" would
|
|
13
|
+
* claim a certainty the app itself does not have. The grade would not change
|
|
14
|
+
* (unknown and runtime both get no discount), but `dev_scope_known` would lie.
|
|
15
|
+
*/
|
|
16
|
+
import type Database from "better-sqlite3";
|
|
17
|
+
import type { ResolvedDependency } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* A project path in the app's storage form: forward slashes, lowercased on
|
|
20
|
+
* Windows only. Must match `project_inclusion::canonical_storage_path` in the
|
|
21
|
+
* app, or the lookup misses every row.
|
|
22
|
+
*/
|
|
23
|
+
export declare function canonicalStoragePath(p: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Stamp transitive dependencies with the dev scope the app determined for
|
|
26
|
+
* them. A dependency pinned in several workspaces is dev-only only when every
|
|
27
|
+
* one of them says dev, and known only when every one of them was determined.
|
|
28
|
+
* Anything short of that keeps the resolver's own (unknown) scope. Direct
|
|
29
|
+
* dependencies are untouched: their manifest already says. Returns new
|
|
30
|
+
* objects; never throws; a database without the table changes nothing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyInstanceDevScope(db: Database.Database | null, deps: ResolvedDependency[]): ResolvedDependency[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Transitive dev/runtime scope from the desktop app's dependency inventory.
|
|
4
|
+
*
|
|
5
|
+
* A lockfile read tells the MCP server whether a package is direct. For a
|
|
6
|
+
* transitive it cannot tell dev from runtime, so under the shared severity
|
|
7
|
+
* rule a transitive gets the clamp but never the dev discount. In
|
|
8
|
+
* full-database mode the app's `dependency_instances` table records every
|
|
9
|
+
* installed instance with `is_dev` and `scope`; this module reads it.
|
|
10
|
+
*
|
|
11
|
+
* Only a real determination counts: `is_dev = 1`, or a `scope` of runtime,
|
|
12
|
+
* dev or build. The app's current writers stamp every row `is_dev = 0,
|
|
13
|
+
* scope = 'unknown'` as a placeholder. Reading that as "known runtime" would
|
|
14
|
+
* claim a certainty the app itself does not have. The grade would not change
|
|
15
|
+
* (unknown and runtime both get no discount), but `dev_scope_known` would lie.
|
|
16
|
+
*/
|
|
17
|
+
const DETERMINED_SCOPES = new Set(["runtime", "dev", "build"]);
|
|
18
|
+
/**
|
|
19
|
+
* A project path in the app's storage form: forward slashes, lowercased on
|
|
20
|
+
* Windows only. Must match `project_inclusion::canonical_storage_path` in the
|
|
21
|
+
* app, or the lookup misses every row.
|
|
22
|
+
*/
|
|
23
|
+
export function canonicalStoragePath(p) {
|
|
24
|
+
const forward = p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
25
|
+
return process.platform === "win32" ? forward.toLowerCase() : forward;
|
|
26
|
+
}
|
|
27
|
+
function instanceKey(ecosystem, name, version) {
|
|
28
|
+
const normalized = ecosystem === "PyPI" ? name.toLowerCase() : name;
|
|
29
|
+
return `${normalized}\0${version}`;
|
|
30
|
+
}
|
|
31
|
+
function hasInstancesTable(db) {
|
|
32
|
+
try {
|
|
33
|
+
return (db
|
|
34
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'dependency_instances'")
|
|
35
|
+
.get() !== undefined);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Every instance row for one (project, ecosystem), keyed by name + version. Null when unreadable. */
|
|
42
|
+
function loadScopeIndex(db, dir, ecosystem) {
|
|
43
|
+
const project = canonicalStoragePath(dir);
|
|
44
|
+
let rows;
|
|
45
|
+
try {
|
|
46
|
+
rows = db
|
|
47
|
+
.prepare("SELECT package_name, version, is_dev, scope FROM dependency_instances WHERE project_path = ? AND ecosystem = ?")
|
|
48
|
+
.all(project, ecosystem);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
// A table without the `scope` column: only `is_dev = 1` can count.
|
|
53
|
+
rows = db
|
|
54
|
+
.prepare("SELECT package_name, version, is_dev FROM dependency_instances WHERE project_path = ? AND ecosystem = ?")
|
|
55
|
+
.all(project, ecosystem);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const index = new Map();
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
const scope = (row.scope ?? "").toLowerCase();
|
|
64
|
+
const isDev = row.is_dev === 1 || scope === "dev";
|
|
65
|
+
index.set(instanceKey(ecosystem, row.package_name, row.version), {
|
|
66
|
+
known: row.is_dev === 1 || DETERMINED_SCOPES.has(scope),
|
|
67
|
+
isDev,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return index;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Stamp transitive dependencies with the dev scope the app determined for
|
|
74
|
+
* them. A dependency pinned in several workspaces is dev-only only when every
|
|
75
|
+
* one of them says dev, and known only when every one of them was determined.
|
|
76
|
+
* Anything short of that keeps the resolver's own (unknown) scope. Direct
|
|
77
|
+
* dependencies are untouched: their manifest already says. Returns new
|
|
78
|
+
* objects; never throws; a database without the table changes nothing.
|
|
79
|
+
*/
|
|
80
|
+
export function applyInstanceDevScope(db, deps) {
|
|
81
|
+
if (!db || !hasInstancesTable(db))
|
|
82
|
+
return deps;
|
|
83
|
+
const indexes = new Map();
|
|
84
|
+
const indexFor = (dir, ecosystem) => {
|
|
85
|
+
const key = `${canonicalStoragePath(dir)}\0${ecosystem}`;
|
|
86
|
+
if (!indexes.has(key))
|
|
87
|
+
indexes.set(key, loadScopeIndex(db, dir, ecosystem));
|
|
88
|
+
return indexes.get(key) ?? null;
|
|
89
|
+
};
|
|
90
|
+
return deps.map((dep) => {
|
|
91
|
+
if (dep.isDirect || !dep.version || dep.sourceDirs.length === 0)
|
|
92
|
+
return dep;
|
|
93
|
+
let allDev = true;
|
|
94
|
+
for (const dir of dep.sourceDirs) {
|
|
95
|
+
const row = indexFor(dir, dep.ecosystem)?.get(instanceKey(dep.ecosystem, dep.name, dep.version));
|
|
96
|
+
if (!row || !row.known)
|
|
97
|
+
return dep;
|
|
98
|
+
allDev &&= row.isDev;
|
|
99
|
+
}
|
|
100
|
+
return { ...dep, devScopeKnown: true, isDev: allDev };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cheap identity for a file at one moment, so the live layer can notice a
|
|
3
|
+
* change with stat calls alone: "absent", or mtime + size + inode.
|
|
4
|
+
*
|
|
5
|
+
* The inode matters for node_modules. npm packs every tarball file with one
|
|
6
|
+
* fixed mtime, and pnpm swaps a package version by re-pointing a link at a
|
|
7
|
+
* different store file, so two versions can share an mtime. They cannot share
|
|
8
|
+
* an inode.
|
|
9
|
+
*/
|
|
10
|
+
export type FileSignature = string;
|
|
11
|
+
export declare const ABSENT: FileSignature;
|
|
12
|
+
export declare function fileSignature(filePath: string): FileSignature;
|
|
13
|
+
/** The modification time a signature recorded, or null for an absent file. */
|
|
14
|
+
export declare function signatureMtime(signature: FileSignature | undefined): number | null;
|
|
15
|
+
/** Signatures for a set of paths, taken now. */
|
|
16
|
+
export declare function signatureMap(paths: Iterable<string>): Map<string, FileSignature>;
|
|
17
|
+
/** True when any recorded file changed, appeared, or disappeared since its signature was taken. */
|
|
18
|
+
export declare function anySignatureChanged(snapshot: Map<string, FileSignature>): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* A cheap identity for a file at one moment, so the live layer can notice a
|
|
4
|
+
* change with stat calls alone: "absent", or mtime + size + inode.
|
|
5
|
+
*
|
|
6
|
+
* The inode matters for node_modules. npm packs every tarball file with one
|
|
7
|
+
* fixed mtime, and pnpm swaps a package version by re-pointing a link at a
|
|
8
|
+
* different store file, so two versions can share an mtime. They cannot share
|
|
9
|
+
* an inode.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
export const ABSENT = "absent";
|
|
13
|
+
export function fileSignature(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
const s = fs.statSync(filePath);
|
|
16
|
+
return `${s.mtimeMs}:${s.size}:${s.ino}`;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return ABSENT;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** The modification time a signature recorded, or null for an absent file. */
|
|
23
|
+
export function signatureMtime(signature) {
|
|
24
|
+
if (!signature || signature === ABSENT)
|
|
25
|
+
return null;
|
|
26
|
+
const mtime = Number(signature.split(":")[0]);
|
|
27
|
+
return Number.isFinite(mtime) ? mtime : null;
|
|
28
|
+
}
|
|
29
|
+
/** Signatures for a set of paths, taken now. */
|
|
30
|
+
export function signatureMap(paths) {
|
|
31
|
+
const out = new Map();
|
|
32
|
+
for (const p of paths)
|
|
33
|
+
out.set(p, fileSignature(p));
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
/** True when any recorded file changed, appeared, or disappeared since its signature was taken. */
|
|
37
|
+
export function anySignatureChanged(snapshot) {
|
|
38
|
+
for (const [p, recorded] of snapshot) {
|
|
39
|
+
if (fileSignature(p) !== recorded)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
package/dist/live/go-registry.js
CHANGED
|
@@ -21,7 +21,9 @@ export class GoRegistry {
|
|
|
21
21
|
this.rateLimiter = rateLimiter;
|
|
22
22
|
}
|
|
23
23
|
async getPackageInfo(name, currentVersion, isDev) {
|
|
24
|
-
|
|
24
|
+
// Keyed by the installed version too: the cached answer carries currentVersion
|
|
25
|
+
// and its distance, so a name-only key served one project's answer to another.
|
|
26
|
+
const cacheKey = `go-reg:${name}@${currentVersion ?? ""}:${isDev ? 1 : 0}`;
|
|
25
27
|
const cached = this.cache.get(cacheKey);
|
|
26
28
|
if (cached !== null)
|
|
27
29
|
return cached;
|
package/dist/live/hn-fetcher.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const HN_ALGOLIA_URL = "https://hn.algolia.com/api/v1/search";
|
|
11
11
|
const HN_TIMEOUT_MS = 8_000;
|
|
12
12
|
const HN_CACHE_TTL = 1800; // 30 minutes
|
|
13
|
-
const MAX_QUERIES =
|
|
13
|
+
const MAX_QUERIES = 8;
|
|
14
14
|
const RESULTS_PER_QUERY = 10;
|
|
15
15
|
const MIN_POINTS = 10;
|
|
16
16
|
export class HNFetcher {
|
|
@@ -110,7 +110,9 @@ function scoreAndMap(hits, techStack) {
|
|
|
110
110
|
// "reactors" and weather stories arrived this way), not ecosystem news.
|
|
111
111
|
.filter(({ matchedTech }) => matchedTech.length > 0)
|
|
112
112
|
.map(({ hit, matchedTech }) => {
|
|
113
|
-
|
|
113
|
+
// A headline naming a dependency outranks language-level news.
|
|
114
|
+
const named = matchedTech.filter((t) => !LANGUAGE_TERMS.has(t));
|
|
115
|
+
const techBoost = named.length > 0 ? Math.min(0.3 + named.length * 0.1, 0.5) : 0.1;
|
|
114
116
|
const pointsBoost = Math.min(hit.points / 500, 0.3);
|
|
115
117
|
const commentsBoost = Math.min(hit.num_comments / 200, 0.2);
|
|
116
118
|
const score = Math.min(0.1 + techBoost + pointsBoost + commentsBoost, 1.0);
|
|
@@ -123,12 +125,16 @@ function scoreAndMap(hits, techStack) {
|
|
|
123
125
|
comments: hit.num_comments,
|
|
124
126
|
published: hit.created_at,
|
|
125
127
|
relevanceScore: Math.round(score * 100) / 100,
|
|
126
|
-
relevanceReason:
|
|
128
|
+
relevanceReason: named.length > 0
|
|
129
|
+
? `Names ${named.join(", ")} (used in your projects)`
|
|
130
|
+
: `Language-level news: ${matchedTech.join(", ")}`,
|
|
127
131
|
};
|
|
128
132
|
})
|
|
129
133
|
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
130
134
|
.slice(0, 15);
|
|
131
135
|
}
|
|
136
|
+
/** Search terms that name a language rather than a dependency. */
|
|
137
|
+
const LANGUAGE_TERMS = new Set(["rust", "python", "zig", "kotlin", "swift", "ruby", "elixir"]);
|
|
132
138
|
const SKIP_TERMS = new Set([
|
|
133
139
|
"javascript", "typescript", // too generic, floods results
|
|
134
140
|
"docker", // too broad
|
package/dist/live/index.d.ts
CHANGED
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
* Set FOURDA_OFFLINE=true to disable all network calls.
|
|
9
9
|
*/
|
|
10
10
|
import type Database from "better-sqlite3";
|
|
11
|
-
import type {
|
|
12
|
-
|
|
11
|
+
import type { ReleaseMetadataSource } from "./release-metadata.js";
|
|
12
|
+
import { type ResolutionGroup } from "./resolution.js";
|
|
13
|
+
import type { InstallDriftRecord, ResolutionProvenance, ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
|
|
14
|
+
export type { VulnerabilityScanResult, VulnerabilityEntry, LiveHeadline, LiveIntelligenceStatus, RegistryPackageInfo, DependencyHealthResult, InstallDriftRecord, ResolutionProvenance, } from "./types.js";
|
|
13
15
|
export declare class LiveIntelligence {
|
|
16
|
+
private db;
|
|
14
17
|
private cache;
|
|
15
18
|
private rateLimiter;
|
|
16
19
|
private osvScanner;
|
|
@@ -19,13 +22,45 @@ export declare class LiveIntelligence {
|
|
|
19
22
|
private cratesRegistry;
|
|
20
23
|
private pypiRegistry;
|
|
21
24
|
private goRegistry;
|
|
25
|
+
private npmPackuments;
|
|
26
|
+
private cratesVersions;
|
|
22
27
|
private enabled;
|
|
23
28
|
private lastVulnScan;
|
|
29
|
+
/** Whether the stored scan included direct devDependencies. */
|
|
30
|
+
private lastVulnScanIncludesDev;
|
|
31
|
+
/**
|
|
32
|
+
* Order of scan REQUESTS. A scan's result is stored only if no later request
|
|
33
|
+
* has stored one already: the init warmup (runtime scope) used to finish
|
|
34
|
+
* after an agent's `include_dev` scan and replace it, so `upgrade_planner`
|
|
35
|
+
* read a scan without the dev dependencies the agent had just scanned and
|
|
36
|
+
* sent node-fetch 2.6.0 to the ESM-only 3.3.2 with no advisories (2 of 6
|
|
37
|
+
* runs, 2026-10-03 pre-publish verification).
|
|
38
|
+
*/
|
|
39
|
+
private scanRequests;
|
|
40
|
+
private storedScanRequest;
|
|
24
41
|
private lastHeadlines;
|
|
25
42
|
private resolvedDeps;
|
|
26
43
|
private auditDeps;
|
|
27
44
|
private initialized;
|
|
28
45
|
private projectRoot;
|
|
46
|
+
/** One entry per resolution group: its inputs, result, and the file signatures that say when it went stale. */
|
|
47
|
+
private groupResults;
|
|
48
|
+
/** When the dependency set in use was assembled: at init, or at the last re-resolution. */
|
|
49
|
+
private resolvedAt;
|
|
50
|
+
/**
|
|
51
|
+
* Bumped by every re-resolution. A scan started under an older generation
|
|
52
|
+
* describes a dependency set that no longer exists: it may still be handed
|
|
53
|
+
* to the call that asked for it, but it is never stored or served later.
|
|
54
|
+
*/
|
|
55
|
+
private generation;
|
|
56
|
+
/**
|
|
57
|
+
* The background vulnerability scan started at server init. Tools that need
|
|
58
|
+
* scan data before answering (`what_should_i_know`) await this through
|
|
59
|
+
* `ensureVulnerabilities` instead of reading the cache and finding it empty.
|
|
60
|
+
*/
|
|
61
|
+
private warmup;
|
|
62
|
+
/** When the last warmup came back offline; gates the retry so an offline host is not re-probed on every call. */
|
|
63
|
+
private warmupFailedAt;
|
|
29
64
|
constructor(db: Database.Database);
|
|
30
65
|
/**
|
|
31
66
|
* Initialize with project data. Call once after project scan.
|
|
@@ -41,6 +76,12 @@ export declare class LiveIntelligence {
|
|
|
41
76
|
deps: string[];
|
|
42
77
|
devDeps: string[];
|
|
43
78
|
}>, depTargets?: Record<string, string>): void;
|
|
79
|
+
/**
|
|
80
|
+
* Initialize from every independently-locked project under `root` (see
|
|
81
|
+
* project-tree.ts): each group resolves from its own directory and keeps
|
|
82
|
+
* its manifest's platform targets; `root` is the reported project root.
|
|
83
|
+
*/
|
|
84
|
+
initFromProjectTree(root: string, groups: ResolutionGroup[]): void;
|
|
44
85
|
/**
|
|
45
86
|
* Initialize from dependency groups that each carry their own resolution
|
|
46
87
|
* directory. Used in 4DA database mode, where dependencies span multiple
|
|
@@ -57,6 +98,22 @@ export declare class LiveIntelligence {
|
|
|
57
98
|
deps: string[];
|
|
58
99
|
devDeps: string[];
|
|
59
100
|
}>): void;
|
|
101
|
+
private applyGroups;
|
|
102
|
+
private rebuildDependencySet;
|
|
103
|
+
/**
|
|
104
|
+
* Re-resolve every group whose inputs changed since they were read (a
|
|
105
|
+
* lockfile rewritten, created or deleted; the fallback manifest; for npm,
|
|
106
|
+
* the node_modules install state), and drop every answer that described the
|
|
107
|
+
* old set: the stored scan and the warmup. Returns true when anything was
|
|
108
|
+
* re-resolved. Stat calls only, so it is cheap enough for the top of every
|
|
109
|
+
* tool call. A process that started before a `git pull` no longer answers
|
|
110
|
+
* for the dependency set it saw at startup.
|
|
111
|
+
*/
|
|
112
|
+
refreshIfLockfilesChanged(): boolean;
|
|
113
|
+
/** When the dependency set in use was resolved, and from which files. Null before init. */
|
|
114
|
+
getResolutionProvenance(): ResolutionProvenance | null;
|
|
115
|
+
/** npm direct dependencies whose installed copy differs from the lockfile, per manifest directory. */
|
|
116
|
+
getInstallDrift(): InstallDriftRecord[];
|
|
60
117
|
/**
|
|
61
118
|
* Root directory the dependency set was resolved from, or null before init.
|
|
62
119
|
* For multi-manifest (grouped) init this is the deepest common ancestor of
|
|
@@ -66,11 +123,43 @@ export declare class LiveIntelligence {
|
|
|
66
123
|
getProjectRoot(): string | null;
|
|
67
124
|
/**
|
|
68
125
|
* Run vulnerability scan (returns cached if fresh, fetches otherwise).
|
|
126
|
+
*
|
|
127
|
+
* Without `includeDev`, only DIRECT devDependencies are left out (the
|
|
128
|
+
* documented contract). A transitive stays in whatever its scope: an
|
|
129
|
+
* unknown scope must not be hidden, and a known dev-only one is graded
|
|
130
|
+
* down by the shared severity rule, not dropped.
|
|
69
131
|
*/
|
|
70
132
|
scanVulnerabilities(projectPath: string, options?: {
|
|
71
133
|
includeDev?: boolean;
|
|
72
134
|
forceRefresh?: boolean;
|
|
73
135
|
}): Promise<VulnerabilityScanResult>;
|
|
136
|
+
/**
|
|
137
|
+
* Start the vulnerability scan in the background without blocking startup.
|
|
138
|
+
* Never rejects: a failed scan resolves to an empty OFFLINE result so the
|
|
139
|
+
* warmup can be awaited by anyone. Idempotent while a warmup is in flight.
|
|
140
|
+
*
|
|
141
|
+
* Before this existed, full-database mode initialised the dependency set
|
|
142
|
+
* and warmed headlines but never scanned, so `lastVulnScan` stayed null
|
|
143
|
+
* until some tool happened to call `vulnerability_scan` — and the first
|
|
144
|
+
* `what_should_i_know` of a session read an empty cache and reported
|
|
145
|
+
* "safe_to_delegate" for a task naming a package with an open advisory.
|
|
146
|
+
*/
|
|
147
|
+
startVulnerabilityWarmup(projectPath: string): void;
|
|
148
|
+
/**
|
|
149
|
+
* A usable vulnerability scan, or null. Re-resolves first when a lockfile
|
|
150
|
+
* changed. Returns the stored scan when one exists, otherwise awaits the
|
|
151
|
+
* warmup (starting one if none is running) for at most `timeoutMs`. Null
|
|
152
|
+
* means the scan is not available: disabled, still running past the
|
|
153
|
+
* deadline, finished offline (an offline scan makes no clean-dependency
|
|
154
|
+
* claims, so it cannot support a "safe" verdict), or overtaken by a
|
|
155
|
+
* re-resolution while it ran. Never throws. A timed-out warmup keeps running
|
|
156
|
+
* so a later call can use it.
|
|
157
|
+
*/
|
|
158
|
+
ensureVulnerabilities(projectPath: string, timeoutMs: number, options?: {
|
|
159
|
+
includeDev?: boolean;
|
|
160
|
+
}): Promise<VulnerabilityScanResult | null>;
|
|
161
|
+
/** A scan's result within `timeoutMs`, or null when it times out, comes back offline, or is overtaken by a re-resolution. */
|
|
162
|
+
private awaitScan;
|
|
74
163
|
/**
|
|
75
164
|
* Fetch relevant headlines for the user's tech stack.
|
|
76
165
|
*/
|
|
@@ -79,6 +168,8 @@ export declare class LiveIntelligence {
|
|
|
79
168
|
* Get last vulnerability scan result (from cache/memory, no network).
|
|
80
169
|
*/
|
|
81
170
|
getVulnerabilities(): VulnerabilityScanResult | null;
|
|
171
|
+
/** Whether the stored scan covered direct devDependencies (false when there is none). */
|
|
172
|
+
lastScanIncludesDev(): boolean;
|
|
82
173
|
/**
|
|
83
174
|
* Get last headlines (from cache/memory, no network).
|
|
84
175
|
*/
|
|
@@ -91,5 +182,17 @@ export declare class LiveIntelligence {
|
|
|
91
182
|
isEnabled(): boolean;
|
|
92
183
|
isInitialized(): boolean;
|
|
93
184
|
fetchRegistryHealth(deps: ResolvedDependency[]): Promise<RegistryPackageInfo[]>;
|
|
185
|
+
/** Per-release registry readers for dependency_check (name-only requests). */
|
|
186
|
+
getReleaseSources(): {
|
|
187
|
+
npm: ReleaseMetadataSource;
|
|
188
|
+
"crates.io": ReleaseMetadataSource;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* OSV advisories for explicit (ecosystem, name, version) triples that need
|
|
192
|
+
* not be in the project, e.g. a version an agent is about to install. Same
|
|
193
|
+
* scanner, cache and rate limit as vulnerability_scan; never touches the
|
|
194
|
+
* stored project scan. Disabled or failed yields an `offline` result.
|
|
195
|
+
*/
|
|
196
|
+
queryAdvisories(deps: ResolvedDependency[]): Promise<VulnerabilityScanResult>;
|
|
94
197
|
getStatus(): LiveIntelligenceStatus;
|
|
95
198
|
}
|