@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.
Files changed (139) hide show
  1. package/README.md +3 -1
  2. package/dist/db.d.ts +21 -2
  3. package/dist/db.js +66 -16
  4. package/dist/doctor.js +5 -3
  5. package/dist/embeddings.d.ts +88 -0
  6. package/dist/embeddings.js +292 -0
  7. package/dist/index.d.ts +9 -9
  8. package/dist/index.js +21 -15
  9. package/dist/live/cvss.d.ts +16 -0
  10. package/dist/live/cvss.js +62 -0
  11. package/dist/live/index.d.ts +9 -1
  12. package/dist/live/index.js +40 -3
  13. package/dist/live/npm-registry.js +29 -6
  14. package/dist/live/osv-scanner.d.ts +8 -0
  15. package/dist/live/osv-scanner.js +115 -30
  16. package/dist/live/platform.d.ts +32 -0
  17. package/dist/live/platform.js +156 -0
  18. package/dist/live/semver-utils.d.ts +8 -0
  19. package/dist/live/semver-utils.js +24 -0
  20. package/dist/live/types.d.ts +31 -9
  21. package/dist/live/version-resolver.d.ts +12 -2
  22. package/dist/live/version-resolver.js +63 -20
  23. package/dist/llm.js +1 -1
  24. package/dist/project-scanner.d.ts +2 -0
  25. package/dist/project-scanner.js +34 -0
  26. package/dist/scheduler/core.d.ts +25 -0
  27. package/dist/scheduler/core.js +61 -0
  28. package/dist/scheduler/freshness.d.ts +17 -0
  29. package/dist/scheduler/freshness.js +41 -0
  30. package/dist/scheduler/index.d.ts +21 -0
  31. package/dist/scheduler/index.js +32 -0
  32. package/dist/scheduler/refreshers.d.ts +21 -0
  33. package/dist/scheduler/refreshers.js +21 -0
  34. package/dist/schemas/get-relevant-content.json +1 -1
  35. package/dist/schemas/upgrade-planner.json +1 -1
  36. package/dist/schemas/what-should-i-know.json +1 -1
  37. package/dist/tools/agent-memory.d.ts +11 -1
  38. package/dist/tools/agent-memory.js +120 -14
  39. package/dist/tools/decision-enforcement.d.ts +17 -1
  40. package/dist/tools/decision-enforcement.js +109 -81
  41. package/dist/tools/decision-memory.d.ts +1 -1
  42. package/dist/tools/decision-memory.js +58 -28
  43. package/dist/tools/decision-recall.d.ts +59 -0
  44. package/dist/tools/decision-recall.js +87 -0
  45. package/dist/tools/dependency-health.js +6 -4
  46. package/dist/tools/developer-dna.js +4 -2
  47. package/dist/tools/knowledge-gaps.js +16 -4
  48. package/dist/tools/recall.d.ts +33 -0
  49. package/dist/tools/recall.js +134 -0
  50. package/dist/tools/upgrade-planner.d.ts +25 -1
  51. package/dist/tools/upgrade-planner.js +111 -30
  52. package/dist/tools/vulnerability-scan.d.ts +46 -0
  53. package/dist/tools/vulnerability-scan.js +64 -24
  54. package/dist/tools/what-should-i-know.d.ts +3 -1
  55. package/dist/tools/what-should-i-know.js +210 -106
  56. package/dist/types.d.ts +9 -0
  57. package/package.json +8 -3
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js.map +0 -1
  60. package/dist/db.d.ts.map +0 -1
  61. package/dist/db.js.map +0 -1
  62. package/dist/doctor.d.ts.map +0 -1
  63. package/dist/doctor.js.map +0 -1
  64. package/dist/http-transport.d.ts.map +0 -1
  65. package/dist/http-transport.js.map +0 -1
  66. package/dist/index.d.ts.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/live/cache.d.ts.map +0 -1
  69. package/dist/live/cache.js.map +0 -1
  70. package/dist/live/crates-registry.d.ts.map +0 -1
  71. package/dist/live/crates-registry.js.map +0 -1
  72. package/dist/live/go-registry.d.ts.map +0 -1
  73. package/dist/live/go-registry.js.map +0 -1
  74. package/dist/live/hn-fetcher.d.ts.map +0 -1
  75. package/dist/live/hn-fetcher.js.map +0 -1
  76. package/dist/live/http-utils.d.ts.map +0 -1
  77. package/dist/live/http-utils.js.map +0 -1
  78. package/dist/live/index.d.ts.map +0 -1
  79. package/dist/live/index.js.map +0 -1
  80. package/dist/live/npm-registry.d.ts.map +0 -1
  81. package/dist/live/npm-registry.js.map +0 -1
  82. package/dist/live/osv-scanner.d.ts.map +0 -1
  83. package/dist/live/osv-scanner.js.map +0 -1
  84. package/dist/live/pypi-registry.d.ts.map +0 -1
  85. package/dist/live/pypi-registry.js.map +0 -1
  86. package/dist/live/rate-limiter.d.ts.map +0 -1
  87. package/dist/live/rate-limiter.js.map +0 -1
  88. package/dist/live/semver-utils.d.ts.map +0 -1
  89. package/dist/live/semver-utils.js.map +0 -1
  90. package/dist/live/types.d.ts.map +0 -1
  91. package/dist/live/types.js.map +0 -1
  92. package/dist/live/version-resolver.d.ts.map +0 -1
  93. package/dist/live/version-resolver.js.map +0 -1
  94. package/dist/live-singleton.d.ts.map +0 -1
  95. package/dist/live-singleton.js.map +0 -1
  96. package/dist/llm.d.ts.map +0 -1
  97. package/dist/llm.js.map +0 -1
  98. package/dist/output-manager.d.ts.map +0 -1
  99. package/dist/output-manager.js.map +0 -1
  100. package/dist/project-scanner.d.ts.map +0 -1
  101. package/dist/project-scanner.js.map +0 -1
  102. package/dist/schema-registry.d.ts.map +0 -1
  103. package/dist/schema-registry.js.map +0 -1
  104. package/dist/setup.d.ts.map +0 -1
  105. package/dist/setup.js.map +0 -1
  106. package/dist/tool-dispatch.d.ts.map +0 -1
  107. package/dist/tool-dispatch.js.map +0 -1
  108. package/dist/tools/agent-memory.d.ts.map +0 -1
  109. package/dist/tools/agent-memory.js.map +0 -1
  110. package/dist/tools/decision-enforcement.d.ts.map +0 -1
  111. package/dist/tools/decision-enforcement.js.map +0 -1
  112. package/dist/tools/decision-memory.d.ts.map +0 -1
  113. package/dist/tools/decision-memory.js.map +0 -1
  114. package/dist/tools/dependency-health.d.ts.map +0 -1
  115. package/dist/tools/dependency-health.js.map +0 -1
  116. package/dist/tools/developer-dna.d.ts.map +0 -1
  117. package/dist/tools/developer-dna.js.map +0 -1
  118. package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
  119. package/dist/tools/ecosystem-pulse.js.map +0 -1
  120. package/dist/tools/get-actionable-signals.d.ts.map +0 -1
  121. package/dist/tools/get-actionable-signals.js.map +0 -1
  122. package/dist/tools/get-context.d.ts.map +0 -1
  123. package/dist/tools/get-context.js.map +0 -1
  124. package/dist/tools/get-relevant-content.d.ts.map +0 -1
  125. package/dist/tools/get-relevant-content.js.map +0 -1
  126. package/dist/tools/index.d.ts.map +0 -1
  127. package/dist/tools/index.js.map +0 -1
  128. package/dist/tools/knowledge-gaps.d.ts.map +0 -1
  129. package/dist/tools/knowledge-gaps.js.map +0 -1
  130. package/dist/tools/record-feedback.d.ts.map +0 -1
  131. package/dist/tools/record-feedback.js.map +0 -1
  132. package/dist/tools/upgrade-planner.d.ts.map +0 -1
  133. package/dist/tools/upgrade-planner.js.map +0 -1
  134. package/dist/tools/vulnerability-scan.d.ts.map +0 -1
  135. package/dist/tools/vulnerability-scan.js.map +0 -1
  136. package/dist/tools/what-should-i-know.d.ts.map +0 -1
  137. package/dist/tools/what-should-i-know.js.map +0 -1
  138. package/dist/types.d.ts.map +0 -1
  139. 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
@@ -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: string;
13
- details: string;
14
- aliases: string[];
15
- severity: Array<{
22
+ summary?: string;
23
+ details?: string;
24
+ aliases?: string[];
25
+ severity?: Array<{
16
26
  type: string;
17
27
  score: string;
18
28
  }>;
19
- affected: Array<{
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: Array<{
42
+ references?: Array<{
33
43
  type: string;
34
44
  url: string;
35
45
  }>;
36
- published: string;
37
- modified: string;
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
- ruby: "RubyGems",
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
- for (const name of deps) {
29
- results.push({
30
- name,
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: true,
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.
@@ -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