@4da/mcp-server 4.6.1 → 4.6.3
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/README.md +3 -1
- package/dist/db.d.ts +21 -2
- package/dist/db.js +66 -16
- package/dist/doctor.js +5 -3
- package/dist/embeddings.d.ts +88 -0
- package/dist/embeddings.js +292 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +21 -15
- package/dist/live/cvss.d.ts +16 -0
- package/dist/live/cvss.js +62 -0
- package/dist/live/index.d.ts +9 -1
- package/dist/live/index.js +40 -3
- package/dist/live/npm-registry.js +29 -6
- package/dist/live/osv-scanner.d.ts +8 -0
- package/dist/live/osv-scanner.js +115 -30
- package/dist/live/platform.d.ts +32 -0
- package/dist/live/platform.js +156 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +24 -0
- package/dist/live/types.d.ts +31 -9
- package/dist/live/version-resolver.d.ts +12 -2
- package/dist/live/version-resolver.js +63 -20
- package/dist/llm.js +1 -1
- package/dist/project-scanner.d.ts +2 -0
- package/dist/project-scanner.js +34 -0
- package/dist/scheduler/core.d.ts +25 -0
- package/dist/scheduler/core.js +61 -0
- package/dist/scheduler/freshness.d.ts +17 -0
- package/dist/scheduler/freshness.js +41 -0
- package/dist/scheduler/index.d.ts +21 -0
- package/dist/scheduler/index.js +32 -0
- package/dist/scheduler/refreshers.d.ts +21 -0
- package/dist/scheduler/refreshers.js +21 -0
- package/dist/schemas/get-relevant-content.json +1 -1
- package/dist/schemas/upgrade-planner.json +1 -1
- package/dist/schemas/what-should-i-know.json +1 -1
- package/dist/tools/agent-memory.d.ts +11 -1
- package/dist/tools/agent-memory.js +120 -14
- package/dist/tools/decision-enforcement.d.ts +17 -1
- package/dist/tools/decision-enforcement.js +109 -81
- package/dist/tools/decision-memory.d.ts +1 -1
- package/dist/tools/decision-memory.js +58 -28
- package/dist/tools/decision-recall.d.ts +59 -0
- package/dist/tools/decision-recall.js +87 -0
- package/dist/tools/dependency-health.js +6 -4
- package/dist/tools/developer-dna.js +4 -2
- package/dist/tools/knowledge-gaps.js +16 -4
- package/dist/tools/recall.d.ts +33 -0
- package/dist/tools/recall.js +134 -0
- package/dist/tools/upgrade-planner.d.ts +25 -1
- package/dist/tools/upgrade-planner.js +111 -30
- package/dist/tools/vulnerability-scan.d.ts +46 -0
- package/dist/tools/vulnerability-scan.js +64 -24
- package/dist/tools/what-should-i-know.d.ts +3 -1
- package/dist/tools/what-should-i-know.js +210 -106
- package/dist/types.d.ts +9 -0
- package/package.json +8 -3
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/http-transport.d.ts.map +0 -1
- package/dist/http-transport.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/live/cache.d.ts.map +0 -1
- package/dist/live/cache.js.map +0 -1
- package/dist/live/crates-registry.d.ts.map +0 -1
- package/dist/live/crates-registry.js.map +0 -1
- package/dist/live/go-registry.d.ts.map +0 -1
- package/dist/live/go-registry.js.map +0 -1
- package/dist/live/hn-fetcher.d.ts.map +0 -1
- package/dist/live/hn-fetcher.js.map +0 -1
- package/dist/live/http-utils.d.ts.map +0 -1
- package/dist/live/http-utils.js.map +0 -1
- package/dist/live/index.d.ts.map +0 -1
- package/dist/live/index.js.map +0 -1
- package/dist/live/npm-registry.d.ts.map +0 -1
- package/dist/live/npm-registry.js.map +0 -1
- package/dist/live/osv-scanner.d.ts.map +0 -1
- package/dist/live/osv-scanner.js.map +0 -1
- package/dist/live/pypi-registry.d.ts.map +0 -1
- package/dist/live/pypi-registry.js.map +0 -1
- package/dist/live/rate-limiter.d.ts.map +0 -1
- package/dist/live/rate-limiter.js.map +0 -1
- package/dist/live/semver-utils.d.ts.map +0 -1
- package/dist/live/semver-utils.js.map +0 -1
- package/dist/live/types.d.ts.map +0 -1
- package/dist/live/types.js.map +0 -1
- package/dist/live/version-resolver.d.ts.map +0 -1
- package/dist/live/version-resolver.js.map +0 -1
- package/dist/live-singleton.d.ts.map +0 -1
- package/dist/live-singleton.js.map +0 -1
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js.map +0 -1
- package/dist/output-manager.d.ts.map +0 -1
- package/dist/output-manager.js.map +0 -1
- package/dist/project-scanner.d.ts.map +0 -1
- package/dist/project-scanner.js.map +0 -1
- package/dist/schema-registry.d.ts.map +0 -1
- package/dist/schema-registry.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/tool-dispatch.d.ts.map +0 -1
- package/dist/tool-dispatch.js.map +0 -1
- package/dist/tools/agent-memory.d.ts.map +0 -1
- package/dist/tools/agent-memory.js.map +0 -1
- package/dist/tools/decision-enforcement.d.ts.map +0 -1
- package/dist/tools/decision-enforcement.js.map +0 -1
- package/dist/tools/decision-memory.d.ts.map +0 -1
- package/dist/tools/decision-memory.js.map +0 -1
- package/dist/tools/dependency-health.d.ts.map +0 -1
- package/dist/tools/dependency-health.js.map +0 -1
- package/dist/tools/developer-dna.d.ts.map +0 -1
- package/dist/tools/developer-dna.js.map +0 -1
- package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
- package/dist/tools/ecosystem-pulse.js.map +0 -1
- package/dist/tools/get-actionable-signals.d.ts.map +0 -1
- package/dist/tools/get-actionable-signals.js.map +0 -1
- package/dist/tools/get-context.d.ts.map +0 -1
- package/dist/tools/get-context.js.map +0 -1
- package/dist/tools/get-relevant-content.d.ts.map +0 -1
- package/dist/tools/get-relevant-content.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/knowledge-gaps.d.ts.map +0 -1
- package/dist/tools/knowledge-gaps.js.map +0 -1
- package/dist/tools/record-feedback.d.ts.map +0 -1
- package/dist/tools/record-feedback.js.map +0 -1
- package/dist/tools/upgrade-planner.d.ts.map +0 -1
- package/dist/tools/upgrade-planner.js.map +0 -1
- package/dist/tools/vulnerability-scan.d.ts.map +0 -1
- package/dist/tools/vulnerability-scan.js.map +0 -1
- package/dist/tools/what-should-i-know.d.ts.map +0 -1
- package/dist/tools/what-should-i-know.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Target/platform awareness for dependency relevance.
|
|
4
|
+
*
|
|
5
|
+
* Cargo (and other ecosystems) gate dependencies behind a target: e.g.
|
|
6
|
+
* `[target.'cfg(windows)'.dependencies]` or `[target.x86_64-pc-windows-msvc.dependencies]`.
|
|
7
|
+
* A dependency that is only active on a platform the user does not build is NOT
|
|
8
|
+
* relevant to them - surfacing its advisory would be noise (the classic
|
|
9
|
+
* "GTK/webkit vuln shown to a Windows user" problem).
|
|
10
|
+
*
|
|
11
|
+
* This module evaluates a target spec against the HOST platform so the scanner
|
|
12
|
+
* can tag each dependency `platformActive` and let consumers suppress or label
|
|
13
|
+
* the irrelevant ones. It is deliberately conservative: anything it cannot
|
|
14
|
+
* confidently evaluate is treated as ACTIVE, so we never hide a real advisory.
|
|
15
|
+
*/
|
|
16
|
+
export function hostTarget() {
|
|
17
|
+
const os = process.platform === "win32"
|
|
18
|
+
? "windows"
|
|
19
|
+
: process.platform === "darwin"
|
|
20
|
+
? "macos"
|
|
21
|
+
: process.platform === "linux"
|
|
22
|
+
? "linux"
|
|
23
|
+
: String(process.platform);
|
|
24
|
+
const family = os === "windows" ? "windows" : "unix";
|
|
25
|
+
const arch = process.arch === "x64"
|
|
26
|
+
? "x86_64"
|
|
27
|
+
: process.arch === "arm64"
|
|
28
|
+
? "aarch64"
|
|
29
|
+
: process.arch === "ia32"
|
|
30
|
+
? "x86"
|
|
31
|
+
: String(process.arch);
|
|
32
|
+
return { os, family, arch };
|
|
33
|
+
}
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Target evaluation
|
|
36
|
+
// =============================================================================
|
|
37
|
+
/**
|
|
38
|
+
* Is a dependency's target spec active on the host?
|
|
39
|
+
* `target` is null/empty for unconditional deps (always active).
|
|
40
|
+
* Returns true when active OR when the spec can't be confidently evaluated.
|
|
41
|
+
*/
|
|
42
|
+
export function targetActiveOnHost(target, host = hostTarget()) {
|
|
43
|
+
if (!target)
|
|
44
|
+
return true;
|
|
45
|
+
const spec = target.trim();
|
|
46
|
+
// Explicit target triple, e.g. "x86_64-pc-windows-msvc" or "aarch64-apple-darwin".
|
|
47
|
+
if (!spec.startsWith("cfg(")) {
|
|
48
|
+
return tripleActiveOnHost(spec, host);
|
|
49
|
+
}
|
|
50
|
+
// cfg(...) predicate. Strip the outer cfg( ).
|
|
51
|
+
const inner = spec.slice(4, spec.lastIndexOf(")"));
|
|
52
|
+
return evalCfg(inner, host);
|
|
53
|
+
}
|
|
54
|
+
/** Evaluate a cfg predicate body (already unwrapped from `cfg( ... )`). */
|
|
55
|
+
export function evalCfg(expr, host = hostTarget()) {
|
|
56
|
+
const e = expr.trim();
|
|
57
|
+
if (!e)
|
|
58
|
+
return true;
|
|
59
|
+
// not( ... )
|
|
60
|
+
const not = matchCall(e, "not");
|
|
61
|
+
if (not !== null)
|
|
62
|
+
return !evalCfg(not, host);
|
|
63
|
+
// all( a, b, ... )
|
|
64
|
+
const all = matchCall(e, "all");
|
|
65
|
+
if (all !== null)
|
|
66
|
+
return splitArgs(all).every((a) => evalCfg(a, host));
|
|
67
|
+
// any( a, b, ... )
|
|
68
|
+
const any = matchCall(e, "any");
|
|
69
|
+
if (any !== null)
|
|
70
|
+
return splitArgs(any).some((a) => evalCfg(a, host));
|
|
71
|
+
// key = "value"
|
|
72
|
+
const kv = e.match(/^([a-z_]+)\s*=\s*"([^"]*)"$/);
|
|
73
|
+
if (kv)
|
|
74
|
+
return evalPredicate(kv[1], kv[2], host);
|
|
75
|
+
// bare identifier: windows | unix
|
|
76
|
+
if (e === "windows")
|
|
77
|
+
return host.family === "windows";
|
|
78
|
+
if (e === "unix")
|
|
79
|
+
return host.family === "unix";
|
|
80
|
+
// Unknown predicate -> conservatively active (never hide a real advisory).
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function evalPredicate(key, value, host) {
|
|
84
|
+
switch (key) {
|
|
85
|
+
case "target_os":
|
|
86
|
+
return value === host.os;
|
|
87
|
+
case "target_family":
|
|
88
|
+
return value === host.family;
|
|
89
|
+
case "target_arch":
|
|
90
|
+
return value === host.arch;
|
|
91
|
+
case "windows":
|
|
92
|
+
return host.family === "windows";
|
|
93
|
+
case "unix":
|
|
94
|
+
return host.family === "unix";
|
|
95
|
+
default:
|
|
96
|
+
return true; // unknown key -> conservatively active
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Match `name( ...body... )` and return the body, or null if not that call. */
|
|
100
|
+
function matchCall(expr, name) {
|
|
101
|
+
if (!expr.startsWith(name))
|
|
102
|
+
return null;
|
|
103
|
+
const rest = expr.slice(name.length).trimStart();
|
|
104
|
+
if (!rest.startsWith("(") || !rest.endsWith(")"))
|
|
105
|
+
return null;
|
|
106
|
+
return rest.slice(1, -1);
|
|
107
|
+
}
|
|
108
|
+
/** Split top-level comma-separated args, respecting nested parentheses. */
|
|
109
|
+
function splitArgs(body) {
|
|
110
|
+
const args = [];
|
|
111
|
+
let depth = 0;
|
|
112
|
+
let current = "";
|
|
113
|
+
for (const ch of body) {
|
|
114
|
+
if (ch === "(")
|
|
115
|
+
depth++;
|
|
116
|
+
if (ch === ")")
|
|
117
|
+
depth--;
|
|
118
|
+
if (ch === "," && depth === 0) {
|
|
119
|
+
if (current.trim())
|
|
120
|
+
args.push(current.trim());
|
|
121
|
+
current = "";
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
current += ch;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (current.trim())
|
|
128
|
+
args.push(current.trim());
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
/** Evaluate an explicit target triple (e.g. x86_64-pc-windows-msvc) against the host. */
|
|
132
|
+
function tripleActiveOnHost(triple, host) {
|
|
133
|
+
const t = triple.toLowerCase();
|
|
134
|
+
// OS component
|
|
135
|
+
const tripleOs = t.includes("windows")
|
|
136
|
+
? "windows"
|
|
137
|
+
: t.includes("darwin") || t.includes("apple")
|
|
138
|
+
? "macos"
|
|
139
|
+
: t.includes("linux")
|
|
140
|
+
? "linux"
|
|
141
|
+
: null;
|
|
142
|
+
if (tripleOs && tripleOs !== host.os)
|
|
143
|
+
return false;
|
|
144
|
+
// Arch component (best-effort)
|
|
145
|
+
const tripleArch = t.startsWith("x86_64")
|
|
146
|
+
? "x86_64"
|
|
147
|
+
: t.startsWith("aarch64")
|
|
148
|
+
? "aarch64"
|
|
149
|
+
: t.startsWith("i686") || t.startsWith("i586")
|
|
150
|
+
? "x86"
|
|
151
|
+
: null;
|
|
152
|
+
if (tripleArch && tripleArch !== host.arch)
|
|
153
|
+
return false;
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -2,4 +2,12 @@ import type { SemverDistance } from "./types.js";
|
|
|
2
2
|
export declare function parseSemver(version: string): [number, number, number] | null;
|
|
3
3
|
export declare function computeSemverDistance(current: string, latest: string): SemverDistance | null;
|
|
4
4
|
export declare function isPreRelease(version: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Compare two versions by MAJOR.MINOR.PATCH. Returns 1 if a > b, -1 if a < b,
|
|
7
|
+
* 0 if equal or either is unparseable. Prerelease/build suffixes are ignored
|
|
8
|
+
* (sufficient for choosing the highest fix version among advisories).
|
|
9
|
+
*/
|
|
10
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
11
|
+
/** Highest version (by MAJOR.MINOR.PATCH) from a list, or null if empty. */
|
|
12
|
+
export declare function maxSemver(versions: string[]): string | null;
|
|
5
13
|
//# sourceMappingURL=semver-utils.d.ts.map
|
|
@@ -25,4 +25,28 @@ export function computeSemverDistance(current, latest) {
|
|
|
25
25
|
export function isPreRelease(version) {
|
|
26
26
|
return /[-+]/.test(version.replace(/^v/, "").replace(/^\d+\.\d+\.\d+/, "").slice(0, 1));
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Compare two versions by MAJOR.MINOR.PATCH. Returns 1 if a > b, -1 if a < b,
|
|
30
|
+
* 0 if equal or either is unparseable. Prerelease/build suffixes are ignored
|
|
31
|
+
* (sufficient for choosing the highest fix version among advisories).
|
|
32
|
+
*/
|
|
33
|
+
export function compareSemver(a, b) {
|
|
34
|
+
const pa = parseSemver(a);
|
|
35
|
+
const pb = parseSemver(b);
|
|
36
|
+
if (!pa || !pb)
|
|
37
|
+
return 0;
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
if (pa[i] > pb[i])
|
|
40
|
+
return 1;
|
|
41
|
+
if (pa[i] < pb[i])
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
/** Highest version (by MAJOR.MINOR.PATCH) from a list, or null if empty. */
|
|
47
|
+
export function maxSemver(versions) {
|
|
48
|
+
if (versions.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
return versions.reduce((max, v) => (compareSemver(v, max) > 0 ? v : max), versions[0]);
|
|
51
|
+
}
|
|
28
52
|
//# sourceMappingURL=semver-utils.js.map
|
package/dist/live/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist";
|
|
1
|
+
export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist" | "Pub";
|
|
2
2
|
export interface ResolvedDependency {
|
|
3
3
|
name: string;
|
|
4
4
|
version: string | null;
|
|
@@ -6,17 +6,27 @@ export interface ResolvedDependency {
|
|
|
6
6
|
isDev: boolean;
|
|
7
7
|
isDirect: boolean;
|
|
8
8
|
devScopeKnown: boolean;
|
|
9
|
+
/** Target spec gating this dep (e.g. `cfg(windows)`), or null if unconditional. */
|
|
10
|
+
target: string | null;
|
|
11
|
+
/** False when `target` is not active on the host platform (advisory is not relevant). */
|
|
12
|
+
platformActive: boolean;
|
|
9
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* An OSV record. The `/v1/querybatch` endpoint returns only `{ id, modified }`
|
|
16
|
+
* per vulnerability (a lightweight index); the rich fields are populated only
|
|
17
|
+
* after hydrating each advisory via `/v1/vulns/{id}`. Hence every field beyond
|
|
18
|
+
* `id` is optional — consumers must tolerate the un-hydrated shape.
|
|
19
|
+
*/
|
|
10
20
|
export interface OsvVulnerability {
|
|
11
21
|
id: string;
|
|
12
|
-
summary
|
|
13
|
-
details
|
|
14
|
-
aliases
|
|
15
|
-
severity
|
|
22
|
+
summary?: string;
|
|
23
|
+
details?: string;
|
|
24
|
+
aliases?: string[];
|
|
25
|
+
severity?: Array<{
|
|
16
26
|
type: string;
|
|
17
27
|
score: string;
|
|
18
28
|
}>;
|
|
19
|
-
affected
|
|
29
|
+
affected?: Array<{
|
|
20
30
|
package: {
|
|
21
31
|
name: string;
|
|
22
32
|
ecosystem: string;
|
|
@@ -29,12 +39,18 @@ export interface OsvVulnerability {
|
|
|
29
39
|
}>;
|
|
30
40
|
}>;
|
|
31
41
|
}>;
|
|
32
|
-
references
|
|
42
|
+
references?: Array<{
|
|
33
43
|
type: string;
|
|
34
44
|
url: string;
|
|
35
45
|
}>;
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
/** GitHub-advisory severity label: LOW | MODERATE | HIGH | CRITICAL. */
|
|
47
|
+
database_specific?: {
|
|
48
|
+
severity?: string;
|
|
49
|
+
};
|
|
50
|
+
/** Set (to an RFC3339 timestamp) when the advisory has been withdrawn/retracted. */
|
|
51
|
+
withdrawn?: string;
|
|
52
|
+
published?: string;
|
|
53
|
+
modified?: string;
|
|
38
54
|
}
|
|
39
55
|
export interface VulnerabilityEntry {
|
|
40
56
|
package: string;
|
|
@@ -51,6 +67,10 @@ export interface VulnerabilityEntry {
|
|
|
51
67
|
fixedVersion: string | null;
|
|
52
68
|
published: string;
|
|
53
69
|
references: string[];
|
|
70
|
+
/** Target spec gating the affected dep, or null if unconditional. */
|
|
71
|
+
target: string | null;
|
|
72
|
+
/** False when the affected dep is not active on the host platform. */
|
|
73
|
+
platformActive: boolean;
|
|
54
74
|
}
|
|
55
75
|
export interface VulnerabilityScanResult {
|
|
56
76
|
scannedAt: string;
|
|
@@ -58,6 +78,8 @@ export interface VulnerabilityScanResult {
|
|
|
58
78
|
ecosystemsScanned: string[];
|
|
59
79
|
totalScanned: number;
|
|
60
80
|
totalVulnerable: number;
|
|
81
|
+
/** Vulnerable packages that are NOT active on the host platform (relevance noise). */
|
|
82
|
+
platformInactiveVulnerable: number;
|
|
61
83
|
bySeverity: {
|
|
62
84
|
critical: number;
|
|
63
85
|
high: number;
|
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
* Uses only Node.js built-ins — no external parsers.
|
|
7
7
|
*/
|
|
8
8
|
import type { OsvEcosystem, ResolvedDependency } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
|
|
11
|
+
*
|
|
12
|
+
* Falls back to "npm" ONLY for genuinely unrecognized input — a last resort, not
|
|
13
|
+
* a real default. OSV-unindexed languages (Swift, C/C++) and unknowns produce no
|
|
14
|
+
* OSV matches anyway (their package names don't collide with the npm namespace),
|
|
15
|
+
* so this never fabricates a vulnerability. Ecosystems that lack a version
|
|
16
|
+
* resolver here (NuGet/Maven/Packagist/RubyGems/Pub) still get the CORRECT label,
|
|
17
|
+
* so the moment a resolver is added the scan is right by construction.
|
|
18
|
+
*/
|
|
9
19
|
export declare function mapEcosystem(language: string): OsvEcosystem;
|
|
10
|
-
export declare function resolveVersions(cwd: string, deps: string[], devDeps: string[], language: string): ResolvedDependency[];
|
|
20
|
+
export declare function resolveVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
|
|
11
21
|
/**
|
|
12
22
|
* Resolve the complete lockfile package set for vulnerability scanning.
|
|
13
23
|
* Direct dependency health and upgrade planning continue to use resolveVersions.
|
|
14
24
|
*/
|
|
15
|
-
export declare function resolveAuditVersions(cwd: string, deps: string[], devDeps: string[], language: string): ResolvedDependency[];
|
|
25
|
+
export declare function resolveAuditVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
|
|
16
26
|
//# sourceMappingURL=version-resolver.d.ts.map
|
|
@@ -8,52 +8,90 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { targetActiveOnHost } from "./platform.js";
|
|
12
|
+
// Alias -> OSV ecosystem. Mirrors the desktop app's canonical
|
|
13
|
+
// `Ecosystem::parse` (src-tauri/src/ecosystem.rs) so both sides recognize the
|
|
14
|
+
// SAME aliases — they previously drifted (the Rust side knew csharp/dart/php
|
|
15
|
+
// while this map keyed C# as "dotnet" and omitted Dart entirely, mislabeling
|
|
16
|
+
// those deps). Keep this in sync with that enum.
|
|
11
17
|
const ECOSYSTEM_MAP = {
|
|
12
18
|
npm: "npm",
|
|
19
|
+
javascript: "npm",
|
|
20
|
+
typescript: "npm",
|
|
21
|
+
node: "npm",
|
|
22
|
+
js: "npm",
|
|
23
|
+
ts: "npm",
|
|
13
24
|
rust: "crates.io",
|
|
25
|
+
cargo: "crates.io",
|
|
26
|
+
"crates.io": "crates.io",
|
|
27
|
+
crates: "crates.io",
|
|
14
28
|
python: "PyPI",
|
|
29
|
+
pypi: "PyPI",
|
|
30
|
+
pip: "PyPI",
|
|
31
|
+
py: "PyPI",
|
|
15
32
|
go: "Go",
|
|
33
|
+
golang: "Go",
|
|
16
34
|
java: "Maven",
|
|
35
|
+
maven: "Maven",
|
|
36
|
+
kotlin: "Maven",
|
|
37
|
+
gradle: "Maven",
|
|
38
|
+
csharp: "NuGet",
|
|
39
|
+
"c#": "NuGet",
|
|
17
40
|
dotnet: "NuGet",
|
|
18
|
-
|
|
41
|
+
nuget: "NuGet",
|
|
19
42
|
php: "Packagist",
|
|
43
|
+
composer: "Packagist",
|
|
44
|
+
packagist: "Packagist",
|
|
45
|
+
ruby: "RubyGems",
|
|
46
|
+
rubygems: "RubyGems",
|
|
47
|
+
gem: "RubyGems",
|
|
48
|
+
dart: "Pub",
|
|
49
|
+
flutter: "Pub",
|
|
50
|
+
pub: "Pub",
|
|
20
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
|
|
54
|
+
*
|
|
55
|
+
* Falls back to "npm" ONLY for genuinely unrecognized input — a last resort, not
|
|
56
|
+
* a real default. OSV-unindexed languages (Swift, C/C++) and unknowns produce no
|
|
57
|
+
* OSV matches anyway (their package names don't collide with the npm namespace),
|
|
58
|
+
* so this never fabricates a vulnerability. Ecosystems that lack a version
|
|
59
|
+
* resolver here (NuGet/Maven/Packagist/RubyGems/Pub) still get the CORRECT label,
|
|
60
|
+
* so the moment a resolver is added the scan is right by construction.
|
|
61
|
+
*/
|
|
21
62
|
export function mapEcosystem(language) {
|
|
22
|
-
return ECOSYSTEM_MAP[language] || "npm";
|
|
63
|
+
return ECOSYSTEM_MAP[language.trim().toLowerCase()] || "npm";
|
|
23
64
|
}
|
|
24
|
-
export function resolveVersions(cwd, deps, devDeps, language) {
|
|
65
|
+
export function resolveVersions(cwd, deps, devDeps, language, targets = {}) {
|
|
25
66
|
const ecosystem = mapEcosystem(language);
|
|
26
67
|
const results = [];
|
|
27
68
|
const versionMap = resolveVersionMap(cwd, ecosystem);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
version: versionMap.get(normalizePackageName(name, ecosystem)) || null,
|
|
32
|
-
ecosystem,
|
|
33
|
-
isDev: false,
|
|
34
|
-
isDirect: true,
|
|
35
|
-
devScopeKnown: true,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
for (const name of devDeps) {
|
|
39
|
-
results.push({
|
|
69
|
+
const build = (name, isDev) => {
|
|
70
|
+
const target = targets[name] ?? null;
|
|
71
|
+
return {
|
|
40
72
|
name,
|
|
41
73
|
version: versionMap.get(normalizePackageName(name, ecosystem)) || null,
|
|
42
74
|
ecosystem,
|
|
43
|
-
isDev
|
|
75
|
+
isDev,
|
|
44
76
|
isDirect: true,
|
|
45
77
|
devScopeKnown: true,
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
target,
|
|
79
|
+
platformActive: targetActiveOnHost(target),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
for (const name of deps)
|
|
83
|
+
results.push(build(name, false));
|
|
84
|
+
for (const name of devDeps)
|
|
85
|
+
results.push(build(name, true));
|
|
48
86
|
return results;
|
|
49
87
|
}
|
|
50
88
|
/**
|
|
51
89
|
* Resolve the complete lockfile package set for vulnerability scanning.
|
|
52
90
|
* Direct dependency health and upgrade planning continue to use resolveVersions.
|
|
53
91
|
*/
|
|
54
|
-
export function resolveAuditVersions(cwd, deps, devDeps, language) {
|
|
92
|
+
export function resolveAuditVersions(cwd, deps, devDeps, language, targets = {}) {
|
|
55
93
|
const ecosystem = mapEcosystem(language);
|
|
56
|
-
const direct = resolveVersions(cwd, deps, devDeps, language);
|
|
94
|
+
const direct = resolveVersions(cwd, deps, devDeps, language, targets);
|
|
57
95
|
const versionMap = resolveVersionMap(cwd, ecosystem);
|
|
58
96
|
const directNames = new Set(deps.map((name) => normalizePackageName(name, ecosystem)));
|
|
59
97
|
const devNames = new Set(devDeps.map((name) => normalizePackageName(name, ecosystem)));
|
|
@@ -62,6 +100,9 @@ export function resolveAuditVersions(cwd, deps, devDeps, language) {
|
|
|
62
100
|
for (const [name, version] of versionMap) {
|
|
63
101
|
const normalized = normalizePackageName(name, ecosystem);
|
|
64
102
|
const isDirect = directNames.has(normalized) || devNames.has(normalized);
|
|
103
|
+
// Transitive deps from the lockfile carry no per-dep target info (Cargo.lock
|
|
104
|
+
// doesn't encode it), so they are treated as unconditionally active.
|
|
105
|
+
const target = targets[name] ?? null;
|
|
65
106
|
const candidate = {
|
|
66
107
|
name,
|
|
67
108
|
version,
|
|
@@ -69,6 +110,8 @@ export function resolveAuditVersions(cwd, deps, devDeps, language) {
|
|
|
69
110
|
isDev: devNames.has(normalized),
|
|
70
111
|
isDirect,
|
|
71
112
|
devScopeKnown: isDirect,
|
|
113
|
+
target,
|
|
114
|
+
platformActive: targetActiveOnHost(target),
|
|
72
115
|
};
|
|
73
116
|
const key = dependencyKey(candidate);
|
|
74
117
|
if (!seen.has(key)) {
|
package/dist/llm.js
CHANGED
|
@@ -107,7 +107,7 @@ export function getLLMConfig(_db) {
|
|
|
107
107
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
108
108
|
const llm = settings.llm || {};
|
|
109
109
|
const config = {
|
|
110
|
-
provider: llm.provider,
|
|
110
|
+
provider: (llm.provider ?? null),
|
|
111
111
|
anthropic_api_key: llm.api_key || llm.anthropic_api_key,
|
|
112
112
|
openai_api_key: llm.openai_api_key,
|
|
113
113
|
ollama_url: llm.base_url || llm.ollama_url || "http://localhost:11434",
|
|
@@ -23,6 +23,8 @@ export interface ProjectScanResult {
|
|
|
23
23
|
deps: string[];
|
|
24
24
|
devDeps: string[];
|
|
25
25
|
}>;
|
|
26
|
+
/** dependency name -> target spec (e.g. "cfg(windows)") for platform-gated deps. */
|
|
27
|
+
depTargets: Record<string, string>;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Scan a directory for project context.
|
package/dist/project-scanner.js
CHANGED
|
@@ -111,6 +111,7 @@ export function scanCurrentProject(cwd) {
|
|
|
111
111
|
projectName: path.basename(cwd),
|
|
112
112
|
projectPath: cwd,
|
|
113
113
|
depsByEcosystem: {},
|
|
114
|
+
depTargets: {},
|
|
114
115
|
};
|
|
115
116
|
const langSet = new Set();
|
|
116
117
|
const fwSet = new Set();
|
|
@@ -189,6 +190,9 @@ export function scanCurrentProject(cwd) {
|
|
|
189
190
|
parseTOMLDependencies(content, "[dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
190
191
|
parseTOMLDependencies(content, "[dev-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
191
192
|
parseTOMLDependencies(content, "[build-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
|
|
193
|
+
// Platform-gated deps: [target.'cfg(...)'.dependencies] / [target.<triple>.dependencies].
|
|
194
|
+
// Previously skipped entirely (e.g. windows-sys, winreg, libc were invisible).
|
|
195
|
+
parseTargetDependencies(content, rustDeps, rustDevDeps, result.depTargets, fwSet, RUST_FRAMEWORK_MAP);
|
|
192
196
|
// Add to flat arrays for backward compat
|
|
193
197
|
result.dependencies.push(...rustDeps);
|
|
194
198
|
result.devDependencies.push(...rustDevDeps);
|
|
@@ -360,6 +364,36 @@ function parseTOMLDependencies(content, sectionHeader, target, frameworkSet, fra
|
|
|
360
364
|
}
|
|
361
365
|
}
|
|
362
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Parse `[target.<spec>.dependencies]` (and dev/build variants) sections, which the
|
|
369
|
+
* plain section parser skips. Records each dep's target spec so the scanner can flag
|
|
370
|
+
* platform relevance. `<spec>` is a cfg() predicate or an explicit target triple.
|
|
371
|
+
*/
|
|
372
|
+
function parseTargetDependencies(content, deps, devDeps, depTargets, frameworkSet, frameworkMap) {
|
|
373
|
+
const headerRegex = /^\[target\.(.+?)\.(dependencies|dev-dependencies|build-dependencies)\]\s*$/gm;
|
|
374
|
+
let m;
|
|
375
|
+
while ((m = headerRegex.exec(content)) !== null) {
|
|
376
|
+
const spec = m[1].trim().replace(/^['"]|['"]$/g, ""); // strip surrounding quotes
|
|
377
|
+
const isDev = m[2] !== "dependencies";
|
|
378
|
+
// Body runs from the end of this header line to the next section header.
|
|
379
|
+
const rest = content.slice(m.index + m[0].length);
|
|
380
|
+
const nextHeader = rest.search(/^\[/m);
|
|
381
|
+
const body = nextHeader === -1 ? rest : rest.slice(0, nextHeader);
|
|
382
|
+
for (const line of body.split("\n")) {
|
|
383
|
+
const trimmed = line.trim();
|
|
384
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
385
|
+
continue;
|
|
386
|
+
const depMatch = trimmed.match(/^([a-zA-Z_][\w-]*)\s*=/);
|
|
387
|
+
if (depMatch) {
|
|
388
|
+
const name = depMatch[1];
|
|
389
|
+
(isDev ? devDeps : deps).push(name);
|
|
390
|
+
depTargets[name] = spec;
|
|
391
|
+
if (name in frameworkMap)
|
|
392
|
+
frameworkSet.add(frameworkMap[name]);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
363
397
|
/** Detect Python framework from package name */
|
|
364
398
|
function detectPythonFramework(name, fwSet) {
|
|
365
399
|
const pythonFrameworks = {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler core — the cadence engine (S1).
|
|
3
|
+
*
|
|
4
|
+
* A minimal, dependency-free job scheduler: register named jobs with an interval, tick them
|
|
5
|
+
* manually (tests, one-shots) or let start()/stop() drive them on timers. The one hard rule:
|
|
6
|
+
* the SAME job never overlaps itself — a tick that arrives while the job's fn is still
|
|
7
|
+
* running resolves as a no-op instead of double-running the pull.
|
|
8
|
+
*/
|
|
9
|
+
type JobFn = () => Promise<void>;
|
|
10
|
+
export interface SchedulerJobView {
|
|
11
|
+
name: string;
|
|
12
|
+
intervalMs: number;
|
|
13
|
+
lastRun: number | null;
|
|
14
|
+
}
|
|
15
|
+
export interface Scheduler {
|
|
16
|
+
register(name: string, intervalMs: number, fn: JobFn): void;
|
|
17
|
+
start(): void;
|
|
18
|
+
stop(): void;
|
|
19
|
+
tick(name: string): Promise<void>;
|
|
20
|
+
jobs(): SchedulerJobView[];
|
|
21
|
+
jobCount(): number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createScheduler(): Scheduler;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler core — the cadence engine (S1).
|
|
3
|
+
*
|
|
4
|
+
* A minimal, dependency-free job scheduler: register named jobs with an interval, tick them
|
|
5
|
+
* manually (tests, one-shots) or let start()/stop() drive them on timers. The one hard rule:
|
|
6
|
+
* the SAME job never overlaps itself — a tick that arrives while the job's fn is still
|
|
7
|
+
* running resolves as a no-op instead of double-running the pull.
|
|
8
|
+
*/
|
|
9
|
+
export function createScheduler() {
|
|
10
|
+
const jobs = new Map();
|
|
11
|
+
async function tick(name) {
|
|
12
|
+
const job = jobs.get(name);
|
|
13
|
+
if (!job) {
|
|
14
|
+
throw new Error(`scheduler: no job registered under "${name}"`);
|
|
15
|
+
}
|
|
16
|
+
if (job.running) {
|
|
17
|
+
return; // the same job never overlaps — a concurrent tick is a no-op
|
|
18
|
+
}
|
|
19
|
+
job.running = true;
|
|
20
|
+
try {
|
|
21
|
+
await job.fn();
|
|
22
|
+
job.lastRun = Date.now();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
job.running = false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
register(name, intervalMs, fn) {
|
|
30
|
+
jobs.set(name, { name, intervalMs, fn, lastRun: null, running: false, timer: null });
|
|
31
|
+
},
|
|
32
|
+
start() {
|
|
33
|
+
for (const job of jobs.values()) {
|
|
34
|
+
if (job.timer)
|
|
35
|
+
continue;
|
|
36
|
+
job.timer = setInterval(() => {
|
|
37
|
+
void tick(job.name).catch(() => {
|
|
38
|
+
// a failing scheduled run must never take the process down; the job's
|
|
39
|
+
// freshness simply doesn't advance, which is the observable signal
|
|
40
|
+
});
|
|
41
|
+
}, job.intervalMs);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
stop() {
|
|
45
|
+
for (const job of jobs.values()) {
|
|
46
|
+
if (job.timer) {
|
|
47
|
+
clearInterval(job.timer);
|
|
48
|
+
job.timer = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
tick,
|
|
53
|
+
jobs() {
|
|
54
|
+
return [...jobs.values()].map(({ name, intervalMs, lastRun }) => ({ name, intervalMs, lastRun }));
|
|
55
|
+
},
|
|
56
|
+
jobCount() {
|
|
57
|
+
return jobs.size;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness watermarks (S2) — when was each named feed last refreshed, and is it stale?
|
|
3
|
+
*
|
|
4
|
+
* A tiny persistent map: mark a name fresh at a moment, ask how stale it is against a budget.
|
|
5
|
+
* Persistence is an explicit save()/load() to a JSON file so the scheduler can survive a
|
|
6
|
+
* restart without inventing freshness it does not have — a missing file simply means
|
|
7
|
+
* "nothing is known", which reads as stale (the honest default).
|
|
8
|
+
*/
|
|
9
|
+
export interface Freshness {
|
|
10
|
+
markFresh(name: string, at?: number): void;
|
|
11
|
+
lastFresh(name: string): number | null;
|
|
12
|
+
isStale(name: string, maxAgeMs: number, now?: number): boolean;
|
|
13
|
+
save(): void;
|
|
14
|
+
load(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createFreshness(filePath?: string): Freshness;
|
|
17
|
+
//# sourceMappingURL=freshness.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness watermarks (S2) — when was each named feed last refreshed, and is it stale?
|
|
3
|
+
*
|
|
4
|
+
* A tiny persistent map: mark a name fresh at a moment, ask how stale it is against a budget.
|
|
5
|
+
* Persistence is an explicit save()/load() to a JSON file so the scheduler can survive a
|
|
6
|
+
* restart without inventing freshness it does not have — a missing file simply means
|
|
7
|
+
* "nothing is known", which reads as stale (the honest default).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
export function createFreshness(filePath) {
|
|
12
|
+
let marks = new Map();
|
|
13
|
+
return {
|
|
14
|
+
markFresh(name, at) {
|
|
15
|
+
marks.set(name, at ?? Date.now());
|
|
16
|
+
},
|
|
17
|
+
lastFresh(name) {
|
|
18
|
+
return marks.get(name) ?? null;
|
|
19
|
+
},
|
|
20
|
+
isStale(name, maxAgeMs, now) {
|
|
21
|
+
const last = marks.get(name);
|
|
22
|
+
if (last === undefined) {
|
|
23
|
+
return true; // never refreshed is the stalest state there is
|
|
24
|
+
}
|
|
25
|
+
return (now ?? Date.now()) - last > maxAgeMs;
|
|
26
|
+
},
|
|
27
|
+
save() {
|
|
28
|
+
if (!filePath)
|
|
29
|
+
return;
|
|
30
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
31
|
+
writeFileSync(filePath, JSON.stringify(Object.fromEntries(marks)), "utf8");
|
|
32
|
+
},
|
|
33
|
+
load() {
|
|
34
|
+
if (!filePath || !existsSync(filePath))
|
|
35
|
+
return;
|
|
36
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
37
|
+
marks = new Map(Object.entries(raw));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=freshness.js.map
|