@4da/mcp-server 5.0.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* deprecation status, and known vulnerabilities across npm, crates.io,
|
|
7
7
|
* PyPI, and Go ecosystems. Zero config — auto-detects from lock files.
|
|
8
8
|
*/
|
|
9
|
+
import { isActionableVulnerability } from "../live/maintenance.js";
|
|
10
|
+
import { driftFor } from "./install-drift-notes.js";
|
|
11
|
+
/** How long a health check waits for its own OSV scan before answering without CVE data (the scan keeps running). */
|
|
12
|
+
const HEALTH_SCAN_TIMEOUT_MS = 60_000;
|
|
9
13
|
export const dependencyHealthTool = {
|
|
10
14
|
name: "dependency_health",
|
|
11
|
-
description: "Assess health of project dependencies — version freshness, deprecation status, known CVEs. Auto-detects stack from lock files. Covers npm, Rust, Python, and Go.",
|
|
15
|
+
description: "Assess health of project dependencies — version freshness, deprecation status, known CVEs. Auto-detects stack from lock files. Covers npm, Rust, Python, and Go. For npm, `installedVersion` appears beside `currentVersion` when node_modules holds a different version than the lockfile.",
|
|
12
16
|
inputSchema: {
|
|
13
17
|
type: "object",
|
|
14
18
|
properties: {
|
|
@@ -33,6 +37,19 @@ export const dependencyHealthTool = {
|
|
|
33
37
|
},
|
|
34
38
|
},
|
|
35
39
|
};
|
|
40
|
+
/** Stamp each npm row with the version node_modules holds, where it differs from the lockfile's. */
|
|
41
|
+
function withInstalledVersions(rows, drift) {
|
|
42
|
+
if (drift.length === 0)
|
|
43
|
+
return rows;
|
|
44
|
+
return rows.map((row) => {
|
|
45
|
+
if (row.ecosystem !== "npm")
|
|
46
|
+
return row;
|
|
47
|
+
const records = driftFor(drift, row.name, row.currentVersion);
|
|
48
|
+
if (records.length === 0)
|
|
49
|
+
return row;
|
|
50
|
+
return { ...row, installedVersion: [...new Set(records.map((r) => r.installedVersion))].join(", ") };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
36
53
|
export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
37
54
|
const start = Date.now();
|
|
38
55
|
if (!liveIntel || !liveIntel.isInitialized()) {
|
|
@@ -44,6 +61,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
44
61
|
outdatedCount: 0,
|
|
45
62
|
deprecatedCount: 0,
|
|
46
63
|
vulnerableCount: 0,
|
|
64
|
+
advisoryCount: 0,
|
|
47
65
|
healthScore: 0,
|
|
48
66
|
dependencies: [],
|
|
49
67
|
vulnerabilitySummary: null,
|
|
@@ -52,6 +70,9 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
52
70
|
cached: false,
|
|
53
71
|
};
|
|
54
72
|
}
|
|
73
|
+
// A long-lived server must not answer from the versions it read at startup:
|
|
74
|
+
// re-resolve if a lockfile (or node_modules) changed. Stat calls only.
|
|
75
|
+
liveIntel.refreshIfLockfilesChanged();
|
|
55
76
|
// Get resolved deps
|
|
56
77
|
const includeDev = params.include_dev ?? false;
|
|
57
78
|
let deps = liveIntel.getResolvedDeps();
|
|
@@ -63,15 +84,29 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
63
84
|
}
|
|
64
85
|
const ecosystems = [...new Set(deps.map((d) => d.ecosystem))];
|
|
65
86
|
// Fetch registry health for all deps
|
|
66
|
-
const registryData = await liveIntel.fetchRegistryHealth(deps);
|
|
67
|
-
// Merge vulnerability data
|
|
68
|
-
|
|
87
|
+
const registryData = withInstalledVersions(await liveIntel.fetchRegistryHealth(deps), liveIntel.getInstallDrift());
|
|
88
|
+
// Merge vulnerability data. "Vulnerable" means the actionable set — built
|
|
89
|
+
// on this host and not a maintenance notice — which is exactly what
|
|
90
|
+
// vulnerability_scan reports as `total_vulnerable_packages`. Counting every
|
|
91
|
+
// row here made the two tools disagree over one scan (this one said 11
|
|
92
|
+
// vulnerable packages where the scan listed 5 and filed the rest under
|
|
93
|
+
// platform-inactive and maintenance notices). `advisoryCount` keeps the raw
|
|
94
|
+
// row count visible so the filter hides nothing.
|
|
95
|
+
// Loaded here (bounded) when no scan has run: telling the agent to "run
|
|
96
|
+
// vulnerability_scan first" left a session's first health check CVE-blind.
|
|
97
|
+
const root = liveIntel.getProjectRoot() ?? process.cwd();
|
|
98
|
+
const vulnResult = (await liveIntel.ensureVulnerabilities(root, HEALTH_SCAN_TIMEOUT_MS, { includeDev })) ?? liveIntel.getVulnerabilities();
|
|
99
|
+
const allVulns = vulnResult?.vulnerabilities ?? [];
|
|
100
|
+
const activeVulns = allVulns.filter(isActionableVulnerability);
|
|
101
|
+
// Keyed by (ecosystem, name): a bare name let an npm advisory mark a
|
|
102
|
+
// same-named crate as vulnerable, and counted the two as one package.
|
|
103
|
+
const vulnKey = (ecosystem, name) => `${ecosystem}\u0000${name}`;
|
|
69
104
|
const vulnMap = new Map();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
105
|
+
for (const v of activeVulns) {
|
|
106
|
+
const key = vulnKey(v.ecosystem, v.package);
|
|
107
|
+
vulnMap.set(key, (vulnMap.get(key) || 0) + 1);
|
|
74
108
|
}
|
|
109
|
+
const advisoryCount = allVulns.length;
|
|
75
110
|
// Compute stats
|
|
76
111
|
let outdated = 0;
|
|
77
112
|
let deprecated = 0;
|
|
@@ -86,6 +121,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
86
121
|
// breakdown shown beside it. registryData is direct deps only — counting there
|
|
87
122
|
// undercounts CVEs that live in transitive dependencies.
|
|
88
123
|
const vulnerable = vulnMap.size;
|
|
124
|
+
const drifted = registryData.filter((d) => d.installedVersion).length;
|
|
89
125
|
// Sort
|
|
90
126
|
const sortBy = params.sort_by ?? "risk";
|
|
91
127
|
const sorted = [...registryData].sort((a, b) => {
|
|
@@ -99,7 +135,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
99
135
|
// risk: vulnerable > deprecated > major outdated > minor > patch > current
|
|
100
136
|
const riskScore = (dep) => {
|
|
101
137
|
let score = 0;
|
|
102
|
-
if (vulnMap.has(dep.name))
|
|
138
|
+
if (vulnMap.has(vulnKey(dep.ecosystem, dep.name)))
|
|
103
139
|
score += 1000;
|
|
104
140
|
if (dep.deprecated)
|
|
105
141
|
score += 500;
|
|
@@ -121,13 +157,13 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
121
157
|
// Vulnerabilities: severity-weighted; transitive findings at one-third
|
|
122
158
|
// weight (their fix arrives via a parent bump — the direct surface is what
|
|
123
159
|
// the user controls directly).
|
|
160
|
+
// Penalised over the same actionable set the count reports, so a score
|
|
161
|
+
// below 100 always has a vulnerable package beside it.
|
|
124
162
|
const sevWeight = { critical: 20, high: 10, medium: 4, low: 1 };
|
|
125
163
|
let vulnPenaltyRaw = 0;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
vulnPenaltyRaw += v.isDirect ? w : w / 3;
|
|
130
|
-
}
|
|
164
|
+
for (const v of activeVulns) {
|
|
165
|
+
const w = sevWeight[v.severity] ?? 2; // "unknown" severity with a real (non-maintenance) advisory
|
|
166
|
+
vulnPenaltyRaw += v.isDirect ? w : w / 3;
|
|
131
167
|
}
|
|
132
168
|
const vulnPenalty = Math.min(Math.round(vulnPenaltyRaw), 55);
|
|
133
169
|
// Staleness: degree-weighted and proportional to the tracked set — a major
|
|
@@ -145,12 +181,14 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
145
181
|
const outdatedPenalty = Math.min(30, Math.round((stalenessWeight / total) * 60));
|
|
146
182
|
const deprecatedPenalty = Math.min(deprecated * 8, 15);
|
|
147
183
|
const healthScore = Math.max(0, 100 - vulnPenalty - deprecatedPenalty - outdatedPenalty);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
// Severity breakdown over the actionable set, matching the count above.
|
|
185
|
+
const vulnSummary = vulnResult ? { critical: 0, high: 0, medium: 0, low: 0 } : null;
|
|
186
|
+
if (vulnSummary) {
|
|
187
|
+
for (const v of activeVulns) {
|
|
188
|
+
if (v.severity in vulnSummary)
|
|
189
|
+
vulnSummary[v.severity]++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
154
192
|
// Summary
|
|
155
193
|
const parts = [];
|
|
156
194
|
parts.push(`${total} dependenc${total !== 1 ? "ies" : "y"} scanned`);
|
|
@@ -160,8 +198,22 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
160
198
|
parts.push(`${deprecated} deprecated`);
|
|
161
199
|
if (outdated > 0)
|
|
162
200
|
parts.push(`${outdated} outdated`);
|
|
163
|
-
if (
|
|
201
|
+
if (drifted > 0) {
|
|
202
|
+
parts.push(`${drifted} installed at a different version than the lockfile pins (see installedVersion; vulnerability_scan lists install_drift)`);
|
|
203
|
+
}
|
|
204
|
+
// "All healthy" is a claim about CVEs too; it needs a scan behind it.
|
|
205
|
+
if (vulnResult && vulnerable === 0 && deprecated === 0 && outdated === 0 && drifted === 0) {
|
|
164
206
|
parts.push("all healthy");
|
|
207
|
+
}
|
|
208
|
+
const hidden = advisoryCount - activeVulns.length;
|
|
209
|
+
if (hidden > 0) {
|
|
210
|
+
parts.push(`${hidden} advisor${hidden !== 1 ? "ies" : "y"} not counted (platform-inactive or maintenance notices)`);
|
|
211
|
+
}
|
|
212
|
+
if (!vulnResult) {
|
|
213
|
+
parts.push(liveIntel.isEnabled()
|
|
214
|
+
? `CVE data not loaded: the OSV scan did not finish within ${HEALTH_SCAN_TIMEOUT_MS / 1000}s or OSV was unreachable. Call again to use it once it completes`
|
|
215
|
+
: "CVE data not loaded: offline mode (FOURDA_OFFLINE)");
|
|
216
|
+
}
|
|
165
217
|
return {
|
|
166
218
|
scannedAt: new Date().toISOString(),
|
|
167
219
|
projectPath: liveIntel.getProjectRoot() ?? process.cwd(),
|
|
@@ -170,6 +222,7 @@ export async function executeDependencyHealth(_db, params, liveIntel) {
|
|
|
170
222
|
outdatedCount: outdated,
|
|
171
223
|
deprecatedCount: deprecated,
|
|
172
224
|
vulnerableCount: vulnerable,
|
|
225
|
+
advisoryCount,
|
|
173
226
|
healthScore,
|
|
174
227
|
dependencies: limited,
|
|
175
228
|
vulnerabilitySummary: vulnSummary,
|
|
@@ -36,7 +36,10 @@ export declare const developerDnaTool: {
|
|
|
36
36
|
};
|
|
37
37
|
interface DependencyEntry {
|
|
38
38
|
name: string;
|
|
39
|
+
/** One project that declares it. */
|
|
39
40
|
project_path: string;
|
|
41
|
+
/** How many of the user's projects declare it. */
|
|
42
|
+
projects: number;
|
|
40
43
|
}
|
|
41
44
|
interface EngagedTopic {
|
|
42
45
|
topic: string;
|
|
@@ -77,13 +77,20 @@ export function executeDeveloperDna(db, params) {
|
|
|
77
77
|
// 3. Top Dependencies — from project_dependencies table
|
|
78
78
|
// -------------------------------------------------------------------------
|
|
79
79
|
let topDependencies = [];
|
|
80
|
+
/** How many of the user's projects declare each dependency (lowercased name). */
|
|
81
|
+
const projectCounts = new Map();
|
|
80
82
|
try {
|
|
83
|
+
// "Top" means most used: declared by the most projects. It meant the
|
|
84
|
+
// first N alphabetically ("@11ty/eleventy..." led the live profile).
|
|
81
85
|
const depRows = rawDb
|
|
82
|
-
.prepare(`SELECT package_name, project_path
|
|
86
|
+
.prepare(`SELECT package_name, MIN(project_path) AS project_path, COUNT(DISTINCT project_path) AS projects
|
|
83
87
|
FROM project_dependencies
|
|
84
|
-
|
|
88
|
+
GROUP BY LOWER(package_name)
|
|
89
|
+
ORDER BY projects DESC, package_name
|
|
85
90
|
LIMIT ?`)
|
|
86
|
-
.all(maxDependencies);
|
|
91
|
+
.all(maxDependencies * 3);
|
|
92
|
+
for (const row of depRows)
|
|
93
|
+
projectCounts.set(row.package_name.toLowerCase(), row.projects);
|
|
87
94
|
// Filter out invalid package names (template literals, path aliases, non-packages)
|
|
88
95
|
const isValidPackageName = (name) => {
|
|
89
96
|
if (!name)
|
|
@@ -104,19 +111,12 @@ export function executeDeveloperDna(db, params) {
|
|
|
104
111
|
};
|
|
105
112
|
topDependencies = depRows
|
|
106
113
|
.filter((r) => isValidPackageName(r.package_name))
|
|
114
|
+
.slice(0, maxDependencies)
|
|
107
115
|
.map((r) => ({
|
|
108
116
|
name: r.package_name,
|
|
109
117
|
project_path: r.project_path,
|
|
118
|
+
projects: r.projects,
|
|
110
119
|
}));
|
|
111
|
-
// Deduplicate (case-insensitive)
|
|
112
|
-
const seen = new Set();
|
|
113
|
-
topDependencies = topDependencies.filter(d => {
|
|
114
|
-
const key = d.name.toLowerCase();
|
|
115
|
-
if (seen.has(key))
|
|
116
|
-
return false;
|
|
117
|
-
seen.add(key);
|
|
118
|
-
return true;
|
|
119
|
-
});
|
|
120
120
|
}
|
|
121
121
|
catch {
|
|
122
122
|
// table may not exist
|
|
@@ -217,9 +217,10 @@ export function executeDeveloperDna(db, params) {
|
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
|
-
else {
|
|
221
|
-
// Never
|
|
222
|
-
//
|
|
220
|
+
else if ((projectCounts.get(depLower) ?? 0) >= 3) {
|
|
221
|
+
// Never read about a dependency three or more projects rely on.
|
|
222
|
+
// Every never-read dependency used to be listed, so the section
|
|
223
|
+
// was the whole dependency list and said nothing.
|
|
223
224
|
blindSpots.push({
|
|
224
225
|
dependency: dep.name,
|
|
225
226
|
severity: "low",
|
|
@@ -44,8 +44,12 @@ export declare const ecosystemPulseTool: {
|
|
|
44
44
|
};
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* The terms HN headlines are searched and matched on: the frameworks the
|
|
48
|
+
* project uses and its most-used direct dependencies — names specific enough
|
|
49
|
+
* that a headline naming one is about something this project runs. Language
|
|
50
|
+
* names are the fallback only: matched on "rust", the pulse was "What Zig felt
|
|
51
|
+
* like, coming from Rust" and an Emacs fork (live 2026-10-01). Package names
|
|
52
|
+
* that are everyday words are left out (they match unrelated titles).
|
|
49
53
|
* Works on both the desktop DB and the standalone minimal schema.
|
|
50
54
|
*/
|
|
51
55
|
export declare function deriveTechStackForHeadlines(db: FourDADatabase): string[];
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Data is already fetched on server startup from Hacker News via Algolia API.
|
|
7
7
|
* This tool makes it queryable.
|
|
8
8
|
*/
|
|
9
|
+
import { COMMON_WORDS } from "./briefing-task-scope.js";
|
|
9
10
|
export const ecosystemPulseTool = {
|
|
10
11
|
name: "ecosystem_pulse",
|
|
11
12
|
description: "Live ecosystem news relevant to your tech stack. Surfaces trending Hacker News discussions filtered by your detected technologies. Updated on server startup.",
|
|
@@ -23,34 +24,72 @@ export const ecosystemPulseTool = {
|
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
};
|
|
27
|
+
/** Languages: matching a headline on one is matching the whole language's news. */
|
|
28
|
+
const LANGUAGES = new Set(["rust", "javascript", "typescript", "python", "go", "golang", "java", "kotlin", "ruby", "php", "c", "c++", "c#", "swift", "dart", "npm"]);
|
|
29
|
+
const MAX_TERMS = 8;
|
|
26
30
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
* The terms HN headlines are searched and matched on: the frameworks the
|
|
32
|
+
* project uses and its most-used direct dependencies — names specific enough
|
|
33
|
+
* that a headline naming one is about something this project runs. Language
|
|
34
|
+
* names are the fallback only: matched on "rust", the pulse was "What Zig felt
|
|
35
|
+
* like, coming from Rust" and an Emacs fork (live 2026-10-01). Package names
|
|
36
|
+
* that are everyday words are left out (they match unrelated titles).
|
|
29
37
|
* Works on both the desktop DB and the standalone minimal schema.
|
|
30
38
|
*/
|
|
31
39
|
export function deriveTechStackForHeadlines(db) {
|
|
32
|
-
const
|
|
40
|
+
const terms = [];
|
|
41
|
+
const add = (term) => {
|
|
42
|
+
const t = (term ?? "").toLowerCase().trim();
|
|
43
|
+
if (t.length >= 3 && !LANGUAGES.has(t) && !COMMON_WORDS.has(t) && !terms.includes(t) && !t.includes("/"))
|
|
44
|
+
terms.push(t);
|
|
45
|
+
};
|
|
33
46
|
const rawDb = db.getRawDb();
|
|
34
47
|
try {
|
|
35
|
-
for (const row of rawDb
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
for (const row of rawDb
|
|
49
|
+
.prepare("SELECT name FROM detected_tech WHERE category IN ('framework', 'library') ORDER BY confidence DESC LIMIT 20")
|
|
50
|
+
.all()) {
|
|
51
|
+
add(row.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// detected_tech may not exist on exotic DBs.
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const direct = db.hasColumn("project_dependencies", "is_direct") ? "WHERE is_direct = 1" : "";
|
|
59
|
+
for (const row of rawDb
|
|
60
|
+
.prepare(`SELECT package_name FROM project_dependencies ${direct}
|
|
61
|
+
GROUP BY LOWER(package_name) ORDER BY COUNT(DISTINCT project_path) DESC, package_name LIMIT 40`)
|
|
62
|
+
.all()) {
|
|
63
|
+
if (terms.length >= MAX_TERMS)
|
|
64
|
+
break;
|
|
65
|
+
add(row.package_name);
|
|
38
66
|
}
|
|
39
67
|
}
|
|
40
68
|
catch {
|
|
41
|
-
//
|
|
69
|
+
// project_dependencies may not exist.
|
|
42
70
|
}
|
|
71
|
+
// Then the project's languages, as language-level news (labelled so by the
|
|
72
|
+
// fetcher): HN titles rarely name a library in a two-week window, and an
|
|
73
|
+
// asked-for ecosystem pulse should still say what moved in the language.
|
|
74
|
+
const specific = terms.slice(0, MAX_TERMS - 2);
|
|
75
|
+
const languages = [];
|
|
43
76
|
try {
|
|
44
77
|
for (const row of rawDb.prepare("SELECT DISTINCT language FROM project_dependencies").all()) {
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
const term = (row.language ?? "").toLowerCase();
|
|
79
|
+
if (term && PULSE_LANGUAGES.has(term) && !languages.includes(term))
|
|
80
|
+
languages.push(term);
|
|
47
81
|
}
|
|
48
82
|
}
|
|
49
83
|
catch {
|
|
50
|
-
//
|
|
84
|
+
// An empty stack yields an honest empty result.
|
|
51
85
|
}
|
|
52
|
-
return [...
|
|
86
|
+
return [...specific, ...languages.slice(0, MAX_TERMS - specific.length)];
|
|
53
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Languages with distinctive enough names to search HN for: not
|
|
90
|
+
* "javascript"/"typescript" (they flood), not "go" (an everyday word in titles).
|
|
91
|
+
*/
|
|
92
|
+
const PULSE_LANGUAGES = new Set(["rust", "python", "zig", "kotlin", "swift", "ruby", "elixir"]);
|
|
54
93
|
export async function executeEcosystemPulse(db, params, liveIntel) {
|
|
55
94
|
if (!liveIntel) {
|
|
56
95
|
return {
|
|
@@ -60,9 +99,9 @@ export async function executeEcosystemPulse(db, params, liveIntel) {
|
|
|
60
99
|
note: "Live intelligence not available. Set FOURDA_OFFLINE=false and restart.",
|
|
61
100
|
};
|
|
62
101
|
}
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// the cache
|
|
102
|
+
// Fetched when asked for, never at startup: the search terms are names from
|
|
103
|
+
// the user's projects, and HN should see them only when the user asks for
|
|
104
|
+
// the pulse. The cache serves warm repeat calls.
|
|
66
105
|
let headlines = liveIntel.getHeadlines();
|
|
67
106
|
if (headlines.length === 0 && liveIntel.isEnabled()) {
|
|
68
107
|
const techStack = deriveTechStackForHeadlines(db);
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* Cross-references against the user's ACE-detected tech stack.
|
|
7
7
|
*/
|
|
8
8
|
import type { FourDADatabase } from "../db.js";
|
|
9
|
-
type
|
|
10
|
-
type
|
|
9
|
+
import type { LiveIntelligence } from "../live/index.js";
|
|
10
|
+
import type { VulnerabilityEntry } from "../live/types.js";
|
|
11
|
+
import { type SignalPriority, type SignalType } from "./signal-classifier.js";
|
|
12
|
+
export { normalizeStoredPriority, type SignalPriority, type SignalType } from "./signal-classifier.js";
|
|
11
13
|
interface ClassifiedSignal {
|
|
12
14
|
id: number;
|
|
13
15
|
title: string;
|
|
@@ -56,9 +58,29 @@ export interface GetActionableSignalsParams {
|
|
|
56
58
|
limit?: number;
|
|
57
59
|
since_hours?: number;
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
/** The slice of the live layer this tool reads; a stub suffices in tests. */
|
|
62
|
+
export type SignalsLiveIntel = Pick<LiveIntelligence, "getVulnerabilities">;
|
|
63
|
+
/** One signal's worth of live findings: the entry to report and every id it is known by. */
|
|
64
|
+
export interface VulnerabilityCluster {
|
|
65
|
+
representative: VulnerabilityEntry;
|
|
66
|
+
ids: string[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Group scan entries that are the same vulnerability under different ids.
|
|
70
|
+
*
|
|
71
|
+
* OSV returns a GHSA record and a RUSTSEC (or CVE) record for one bug, each
|
|
72
|
+
* naming the other in `aliases`; injected one-per-record they read as two
|
|
73
|
+
* problems and doubled the advisory count in every briefing. Union-find over
|
|
74
|
+
* `[vulnId, ...aliases]` connects the records transitively (A knows B, B knows
|
|
75
|
+
* C), scoped to the affected (ecosystem, package, version) so one advisory
|
|
76
|
+
* against two packages — or two pinned versions of one package — stays two
|
|
77
|
+
* things to fix. The representative is the GHSA record when the cluster has
|
|
78
|
+
* one (the richest and most stable id), else the first seen; `ids` lists
|
|
79
|
+
* every identifier in first-seen order.
|
|
80
|
+
*/
|
|
81
|
+
export declare function clusterVulnerabilities(vulns: VulnerabilityEntry[]): VulnerabilityCluster[];
|
|
82
|
+
export declare function executeGetActionableSignals(db: FourDADatabase, params: GetActionableSignalsParams, liveIntel?: SignalsLiveIntel | null): {
|
|
60
83
|
signals: ClassifiedSignal[];
|
|
61
84
|
total: number;
|
|
62
85
|
summary: Record<string, number>;
|
|
63
86
|
};
|
|
64
|
-
export {};
|