@4da/mcp-server 5.0.4 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +698 -0
- package/README.md +70 -39
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -58
- package/dist/db.js +226 -299
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -136
- package/package.json +35 -35
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -6,195 +6,16 @@
|
|
|
6
6
|
* breaking changes, tool discoveries, etc.) with priority levels.
|
|
7
7
|
* Cross-references against the user's ACE-detected tech stack.
|
|
8
8
|
*/
|
|
9
|
+
import { isMaintenanceNotice } from "../live/maintenance.js";
|
|
10
|
+
import { presentedSeverity, scopeAdjustmentReason } from "../live/severity-scope.js";
|
|
9
11
|
import { getLiveIntelligence } from "../live-singleton.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"zero-day", "zero day", "0-day", "patch", "ransomware",
|
|
18
|
-
"malware", "rce", "injection attack", "xss", "csrf",
|
|
19
|
-
"privilege escalation", "backdoor", "supply chain attack",
|
|
20
|
-
],
|
|
21
|
-
boostWords: ["critical", "urgent", "severe", "actively exploited", "emergency"],
|
|
22
|
-
weight: 1.0,
|
|
23
|
-
},
|
|
24
|
-
breaking_change: {
|
|
25
|
-
keywords: [
|
|
26
|
-
"breaking change", "deprecated", "end of life", "eol",
|
|
27
|
-
"migration guide", "major release", "drops support",
|
|
28
|
-
"removed in", "no longer supported", "sunset",
|
|
29
|
-
"backwards incompatible", "api change",
|
|
30
|
-
],
|
|
31
|
-
boostWords: ["v2", "v3", "v4", "v5", "major version", "upgrade required"],
|
|
32
|
-
weight: 0.9,
|
|
33
|
-
},
|
|
34
|
-
tool_discovery: {
|
|
35
|
-
keywords: [
|
|
36
|
-
"new release", "just released", "announcing", "launch",
|
|
37
|
-
"alternative to", "built with", "replacement for",
|
|
38
|
-
"open source", "open-source", "introducing",
|
|
39
|
-
"we built", "i built", "show hn",
|
|
40
|
-
],
|
|
41
|
-
boostWords: ["faster", "better", "simpler", "lightweight", "blazing"],
|
|
42
|
-
weight: 0.7,
|
|
43
|
-
},
|
|
44
|
-
tech_trend: {
|
|
45
|
-
keywords: [
|
|
46
|
-
"adoption", "growing", "trending", "benchmark",
|
|
47
|
-
"comparison", "state of", "survey", "report",
|
|
48
|
-
"market share", "ecosystem", "roadmap",
|
|
49
|
-
],
|
|
50
|
-
boostWords: ["2025", "2026", "accelerating", "mainstream", "industry"],
|
|
51
|
-
weight: 0.6,
|
|
52
|
-
},
|
|
53
|
-
learning: {
|
|
54
|
-
keywords: [
|
|
55
|
-
"tutorial", "how to", "guide", "deep dive",
|
|
56
|
-
"explained", "best practices", "patterns",
|
|
57
|
-
"architecture", "lessons learned", "walkthrough",
|
|
58
|
-
"step by step", "from scratch",
|
|
59
|
-
],
|
|
60
|
-
boostWords: ["advanced", "production", "real-world", "comprehensive"],
|
|
61
|
-
weight: 0.5,
|
|
62
|
-
},
|
|
63
|
-
competitive_intel: {
|
|
64
|
-
keywords: [
|
|
65
|
-
"acquired", "funding", "raised", "ipo",
|
|
66
|
-
"valuation", "market share", "competitor",
|
|
67
|
-
"pivots", "pivot", "layoffs", "shutdown",
|
|
68
|
-
"acqui-hire", "series a", "series b",
|
|
69
|
-
],
|
|
70
|
-
boostWords: ["million", "billion", "disrupts", "overtakes"],
|
|
71
|
-
weight: 0.6,
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
const BASE_WEIGHTS = {
|
|
75
|
-
security_alert: 2,
|
|
76
|
-
breaking_change: 2,
|
|
77
|
-
tool_discovery: 1,
|
|
78
|
-
tech_trend: 1,
|
|
79
|
-
learning: 1,
|
|
80
|
-
competitive_intel: 1,
|
|
81
|
-
};
|
|
82
|
-
// Word-boundary matching prevents "rce" matching inside "source", "xss" inside "success", etc.
|
|
83
|
-
function hasWordBoundary(text, term) {
|
|
84
|
-
let searchFrom = 0;
|
|
85
|
-
while (true) {
|
|
86
|
-
const pos = text.indexOf(term, searchFrom);
|
|
87
|
-
if (pos === -1)
|
|
88
|
-
return false;
|
|
89
|
-
const beforeOk = pos === 0 || !/[a-zA-Z0-9]/.test(text[pos - 1]);
|
|
90
|
-
const afterIdx = pos + term.length;
|
|
91
|
-
const afterOk = afterIdx >= text.length || !/[a-zA-Z0-9]/.test(text[afterIdx]);
|
|
92
|
-
if (beforeOk && afterOk)
|
|
93
|
-
return true;
|
|
94
|
-
searchFrom = pos + 1;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const PRIORITY_LABELS = {
|
|
98
|
-
security_alert: "Security Alert",
|
|
99
|
-
breaking_change: "Breaking Change",
|
|
100
|
-
tool_discovery: "Tool Discovery",
|
|
101
|
-
tech_trend: "Tech Trend",
|
|
102
|
-
learning: "Learning",
|
|
103
|
-
competitive_intel: "Competitive Intel",
|
|
104
|
-
};
|
|
105
|
-
const KEYWORD_MAP = new Map();
|
|
106
|
-
// Initialize keyword map
|
|
107
|
-
for (const [type, pattern] of Object.entries(SIGNAL_PATTERNS)) {
|
|
108
|
-
for (const kw of pattern.keywords) {
|
|
109
|
-
if (!KEYWORD_MAP.has(kw)) {
|
|
110
|
-
KEYWORD_MAP.set(kw, []);
|
|
111
|
-
}
|
|
112
|
-
KEYWORD_MAP.get(kw).push({ type, weight: pattern.weight, isBoost: false });
|
|
113
|
-
}
|
|
114
|
-
for (const bw of pattern.boostWords) {
|
|
115
|
-
if (!KEYWORD_MAP.has(bw)) {
|
|
116
|
-
KEYWORD_MAP.set(bw, []);
|
|
117
|
-
}
|
|
118
|
-
KEYWORD_MAP.get(bw).push({ type, weight: 0.2, isBoost: true });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function classify(title, content, relevanceScore, detectedTech) {
|
|
122
|
-
// Pre-compute lowercased text once
|
|
123
|
-
const textLower = `${title} ${content}`.toLowerCase();
|
|
124
|
-
const titleLower = title.toLowerCase();
|
|
125
|
-
// Track scores per signal type
|
|
126
|
-
const typeScores = {};
|
|
127
|
-
// Single pass through keyword map (word-boundary matched to prevent false positives)
|
|
128
|
-
for (const [keyword, entries] of KEYWORD_MAP.entries()) {
|
|
129
|
-
if (hasWordBoundary(textLower, keyword)) {
|
|
130
|
-
for (const entry of entries) {
|
|
131
|
-
if (!typeScores[entry.type]) {
|
|
132
|
-
typeScores[entry.type] = { score: 0, matched: [] };
|
|
133
|
-
}
|
|
134
|
-
typeScores[entry.type].score += entry.weight;
|
|
135
|
-
typeScores[entry.type].matched.push(keyword);
|
|
136
|
-
// Boost if keyword is in title (only for non-boost words)
|
|
137
|
-
if (!entry.isBoost && hasWordBoundary(titleLower, keyword)) {
|
|
138
|
-
typeScores[entry.type].score += entry.weight * 0.5;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Find best type
|
|
144
|
-
let bestType = null;
|
|
145
|
-
let bestConfidence = 0;
|
|
146
|
-
let bestTriggers = [];
|
|
147
|
-
for (const [type, data] of Object.entries(typeScores)) {
|
|
148
|
-
const confidence = Math.min(data.score / 3.0, 1.0);
|
|
149
|
-
if (confidence > bestConfidence) {
|
|
150
|
-
bestType = type;
|
|
151
|
-
bestConfidence = confidence;
|
|
152
|
-
bestTriggers = data.matched;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!bestType)
|
|
156
|
-
return null;
|
|
157
|
-
// Require at least 2 keyword matches — single keyword produces too many false positives
|
|
158
|
-
if (bestTriggers.length < 2)
|
|
159
|
-
return null;
|
|
160
|
-
// Compute priority
|
|
161
|
-
let priorityScore = BASE_WEIGHTS[bestType];
|
|
162
|
-
const techMatch = detectedTech.find((t) => hasWordBoundary(textLower, t.toLowerCase()));
|
|
163
|
-
// Security alerts MUST mention something in the user's stack to be relevant.
|
|
164
|
-
// A CVE for "Prometheus" or "PraisonAI" is noise if the user doesn't use them.
|
|
165
|
-
// Only keep security alerts that explicitly name a technology the user has.
|
|
166
|
-
if (bestType === "security_alert" && !techMatch) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
if (techMatch)
|
|
170
|
-
priorityScore += 1;
|
|
171
|
-
if (relevanceScore > 0.7)
|
|
172
|
-
priorityScore += 1;
|
|
173
|
-
priorityScore = Math.min(priorityScore, 4);
|
|
174
|
-
const priority = priorityScore >= 4 ? "critical" : priorityScore === 3 ? "high" : priorityScore === 2 ? "medium" : "low";
|
|
175
|
-
// Generate action
|
|
176
|
-
const shortTitle = title.length > 60 ? title.substring(0, 60) + "..." : title;
|
|
177
|
-
let action;
|
|
178
|
-
if (bestType === "security_alert" && techMatch) {
|
|
179
|
-
action = `Review ${shortTitle} - affects your ${techMatch} stack`;
|
|
180
|
-
}
|
|
181
|
-
else if (bestType === "breaking_change" && techMatch) {
|
|
182
|
-
action = `Check migration path - ${techMatch} breaking change`;
|
|
183
|
-
}
|
|
184
|
-
else if (bestType === "tool_discovery" && techMatch) {
|
|
185
|
-
action = `Evaluate for your ${techMatch} workflow: ${shortTitle}`;
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
action = `${PRIORITY_LABELS[bestType] || bestType}: ${shortTitle}`;
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
signalType: bestType,
|
|
192
|
-
priority,
|
|
193
|
-
confidence: Math.round(bestConfidence * 100) / 100,
|
|
194
|
-
action,
|
|
195
|
-
triggers: bestTriggers,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
12
|
+
import { driftAction } from "./install-drift-notes.js";
|
|
13
|
+
import { normalizeStoredPriority } from "./signal-classifier.js";
|
|
14
|
+
// The priority vocabulary lives in signal-classifier.ts; re-exported for existing importers.
|
|
15
|
+
export { normalizeStoredPriority } from "./signal-classifier.js";
|
|
16
|
+
/** Feed sources whose rows are structured facts (an advisory record, a registry release), not prose. */
|
|
17
|
+
const STRUCTURED_SOURCES = new Set(["cve", "osv", "crates_io", "npm_registry", "pypi", "go_modules", "github"]);
|
|
18
|
+
const ADVISORY_SOURCES = new Set(["cve", "osv"]);
|
|
198
19
|
// ============================================================================
|
|
199
20
|
// Tool Definition
|
|
200
21
|
// ============================================================================
|
|
@@ -231,68 +52,172 @@ Use this to get prioritized, actionable intelligence from 4DA's feed.`,
|
|
|
231
52
|
},
|
|
232
53
|
since_hours: {
|
|
233
54
|
type: "number",
|
|
234
|
-
description: "Only include items from the last N hours. Default: 48",
|
|
55
|
+
description: "Only include items from the last N hours. Default: 48, max: 720 (30 days)",
|
|
235
56
|
default: 48,
|
|
236
57
|
},
|
|
237
58
|
},
|
|
238
59
|
},
|
|
239
60
|
};
|
|
61
|
+
/** Widest window a caller may ask for: 30 days. */
|
|
62
|
+
const MAX_SINCE_HOURS = 720;
|
|
63
|
+
/** Windows past this reach rows the current scoring pipeline may not have re-scored. */
|
|
64
|
+
const DEEP_WINDOW_HOURS = 168;
|
|
65
|
+
/**
|
|
66
|
+
* Group scan entries that are the same vulnerability under different ids.
|
|
67
|
+
*
|
|
68
|
+
* OSV returns a GHSA record and a RUSTSEC (or CVE) record for one bug, each
|
|
69
|
+
* naming the other in `aliases`; injected one-per-record they read as two
|
|
70
|
+
* problems and doubled the advisory count in every briefing. Union-find over
|
|
71
|
+
* `[vulnId, ...aliases]` connects the records transitively (A knows B, B knows
|
|
72
|
+
* C), scoped to the affected (ecosystem, package, version) so one advisory
|
|
73
|
+
* against two packages — or two pinned versions of one package — stays two
|
|
74
|
+
* things to fix. The representative is the GHSA record when the cluster has
|
|
75
|
+
* one (the richest and most stable id), else the first seen; `ids` lists
|
|
76
|
+
* every identifier in first-seen order.
|
|
77
|
+
*/
|
|
78
|
+
export function clusterVulnerabilities(vulns) {
|
|
79
|
+
const parent = new Map();
|
|
80
|
+
const find = (key) => {
|
|
81
|
+
let root = key;
|
|
82
|
+
while (parent.get(root) !== root)
|
|
83
|
+
root = parent.get(root);
|
|
84
|
+
// Path compression keeps repeated lookups flat.
|
|
85
|
+
let cursor = key;
|
|
86
|
+
while (parent.get(cursor) !== root) {
|
|
87
|
+
const next = parent.get(cursor);
|
|
88
|
+
parent.set(cursor, root);
|
|
89
|
+
cursor = next;
|
|
90
|
+
}
|
|
91
|
+
return root;
|
|
92
|
+
};
|
|
93
|
+
const union = (a, b) => {
|
|
94
|
+
const ra = find(a);
|
|
95
|
+
const rb = find(b);
|
|
96
|
+
if (ra !== rb)
|
|
97
|
+
parent.set(ra, rb);
|
|
98
|
+
};
|
|
99
|
+
const scopeOf = (v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`;
|
|
100
|
+
const idsOf = (v) => [v.vulnId, ...v.aliases].filter(Boolean);
|
|
101
|
+
for (const v of vulns) {
|
|
102
|
+
const scope = scopeOf(v);
|
|
103
|
+
const keys = idsOf(v).map((id) => `${scope}\0${id}`);
|
|
104
|
+
for (const key of keys)
|
|
105
|
+
if (!parent.has(key))
|
|
106
|
+
parent.set(key, key);
|
|
107
|
+
for (let i = 1; i < keys.length; i++)
|
|
108
|
+
union(keys[0], keys[i]);
|
|
109
|
+
}
|
|
110
|
+
const groups = new Map();
|
|
111
|
+
for (const v of vulns) {
|
|
112
|
+
const root = find(`${scopeOf(v)}\0${v.vulnId}`);
|
|
113
|
+
const members = groups.get(root);
|
|
114
|
+
if (members)
|
|
115
|
+
members.push(v);
|
|
116
|
+
else
|
|
117
|
+
groups.set(root, [v]);
|
|
118
|
+
}
|
|
119
|
+
return [...groups.values()].map((members) => {
|
|
120
|
+
const representative = members.find((m) => m.vulnId.startsWith("GHSA-")) ?? members[0];
|
|
121
|
+
const ids = [...new Set(members.flatMap(idsOf))];
|
|
122
|
+
return { representative, ids };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** Latest relevance-judge verdict per item (`llm_judgments`), when the desktop app has judged it. */
|
|
126
|
+
function judgeVerdicts(db, ids) {
|
|
127
|
+
const out = new Map();
|
|
128
|
+
if (ids.length === 0)
|
|
129
|
+
return out;
|
|
130
|
+
try {
|
|
131
|
+
const raw = db.getRawDb();
|
|
132
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
133
|
+
const chunk = ids.slice(i, i + 500);
|
|
134
|
+
const rows = raw
|
|
135
|
+
.prepare(`SELECT source_item_id AS id, relevance_score AS v FROM llm_judgments
|
|
136
|
+
WHERE id IN (SELECT MAX(id) FROM llm_judgments WHERE source_item_id IN (${chunk.map(() => "?").join(",")}) GROUP BY source_item_id)`)
|
|
137
|
+
.all(...chunk);
|
|
138
|
+
for (const row of rows)
|
|
139
|
+
out.set(row.id, row.v);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// No llm_judgments table (standalone or an older app): no verdicts.
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/** What an agent can do with a signal of this type and source. */
|
|
148
|
+
function actionFor(type, source) {
|
|
149
|
+
if (ADVISORY_SOURCES.has(source))
|
|
150
|
+
return "Advisory record: run vulnerability_scan to see whether your installed versions are affected.";
|
|
151
|
+
if (source === "crates_io" || source === "npm_registry" || source === "pypi" || source === "go_modules") {
|
|
152
|
+
return "New release of a dependency: call upgrade_impact for what changed before bumping.";
|
|
153
|
+
}
|
|
154
|
+
if (type === "security_alert")
|
|
155
|
+
return "Security news (not an advisory against your versions): read for context.";
|
|
156
|
+
if (type === "breaking_change")
|
|
157
|
+
return "Possible breaking change: check whether it touches a dependency you use.";
|
|
158
|
+
return "Read for context.";
|
|
159
|
+
}
|
|
240
160
|
// ============================================================================
|
|
241
161
|
// Execution
|
|
242
162
|
// ============================================================================
|
|
243
|
-
export function executeGetActionableSignals(db, params) {
|
|
163
|
+
export function executeGetActionableSignals(db, params, liveIntel = getLiveIntelligence()) {
|
|
244
164
|
const limit = Math.max(1, Math.min(100, params.limit ?? 20));
|
|
245
|
-
const sinceHours = Math.max(1, Math.min(
|
|
246
|
-
// Get items from DB (
|
|
247
|
-
//
|
|
248
|
-
|
|
165
|
+
const sinceHours = Math.max(1, Math.min(MAX_SINCE_HOURS, params.since_hours ?? 48));
|
|
166
|
+
// Get items from DB (low min score to get more items). A signal_type filter
|
|
167
|
+
// is pushed into the read as well: the general read is capped at the top
|
|
168
|
+
// 200 ranked items across EVERY type, and on a real corpus that cap starved
|
|
169
|
+
// a type-specific pass — live 2026-09-07, the two in-window stored security
|
|
170
|
+
// alerts ranked 299th and 582nd, so a security-only read returned nothing.
|
|
171
|
+
// Stored matches are fetched by type and merged ahead of the general read,
|
|
172
|
+
// which still carries the unstamped rows for keyword classification.
|
|
173
|
+
const fetchItems = (minScore, hours, requireCurrentVersion) => {
|
|
174
|
+
const general = db.getRelevantContent(minScore, undefined, 200, hours, requireCurrentVersion);
|
|
175
|
+
if (!params.signal_type)
|
|
176
|
+
return general;
|
|
177
|
+
const stored = db.getRelevantContent(minScore, undefined, 200, hours, requireCurrentVersion, params.signal_type);
|
|
178
|
+
const seen = new Set(stored.map((item) => item.id));
|
|
179
|
+
return [...stored, ...general.filter((item) => !seen.has(item.id))];
|
|
180
|
+
};
|
|
181
|
+
// Try progressively wider time windows if no items found. A window wider
|
|
182
|
+
// than seven days reaches the stale-epoch tail, so it carries the same
|
|
183
|
+
// current-pipeline-version guard as the deep fallback below.
|
|
184
|
+
let items = fetchItems(0.1, sinceHours, sinceHours > DEEP_WINDOW_HOURS);
|
|
249
185
|
if (items.length === 0 && sinceHours < 168) {
|
|
250
|
-
items =
|
|
186
|
+
items = fetchItems(0.1, 168, false); // Try 7 days
|
|
251
187
|
}
|
|
252
188
|
if (items.length === 0) {
|
|
253
189
|
// Deep 30-day/any-score fallback reaches the stale-epoch tail after a
|
|
254
190
|
// pipeline-version bump. This path trusts stored signal_type/signal_priority
|
|
255
191
|
// verbatim (confidence 0.90 below), so it MUST only see current-version
|
|
256
192
|
// rows — stale persisted signals are claims the live brain no longer makes.
|
|
257
|
-
items =
|
|
193
|
+
items = fetchItems(0.0, 720, true);
|
|
258
194
|
}
|
|
259
|
-
// Get user's detected tech for cross-referencing
|
|
260
|
-
const context = db.getUserContext(true, false);
|
|
261
|
-
const detectedTech = (context.ace?.detected_tech || []).map((t) => t.name);
|
|
262
195
|
const signals = [];
|
|
196
|
+
const verdicts = judgeVerdicts(db, items.map((item) => item.id));
|
|
263
197
|
for (const item of items) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (params.signal_type && storedType !== params.signal_type)
|
|
272
|
-
continue;
|
|
273
|
-
signals.push({
|
|
274
|
-
id: item.id,
|
|
275
|
-
title: item.title,
|
|
276
|
-
url: item.url,
|
|
277
|
-
source_type: item.source_type,
|
|
278
|
-
relevance_score: item.relevance_score,
|
|
279
|
-
signal_type: storedType,
|
|
280
|
-
signal_priority: storedPriority,
|
|
281
|
-
action: `${storedType}: ${item.title.substring(0, 60)}`,
|
|
282
|
-
triggers: [],
|
|
283
|
-
confidence: 0.90,
|
|
284
|
-
discovered_ago: item.discovered_ago,
|
|
285
|
-
});
|
|
198
|
+
// Only the pipeline's own classifications, and only when they can be
|
|
199
|
+
// stood behind: a prose item (news, social, papers) appears when the
|
|
200
|
+
// relevance judge accepted it (latest verdict >= 0.5); a structured row
|
|
201
|
+
// (advisory record, registry release) may appear unjudged. Measured
|
|
202
|
+
// 2026-10-01: of 154 items stamped security_alert in 14 days the judge
|
|
203
|
+
// accepted 10, rejected 62 and never saw 82; 3 came from an advisory source.
|
|
204
|
+
if (!item.signal_type || !item.signal_priority)
|
|
286
205
|
continue;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (!result)
|
|
206
|
+
const verdict = verdicts.get(item.id);
|
|
207
|
+
const structured = STRUCTURED_SOURCES.has(item.source_type);
|
|
208
|
+
if (verdict !== undefined ? verdict < 0.5 : !structured)
|
|
291
209
|
continue;
|
|
210
|
+
const storedType = item.signal_type;
|
|
211
|
+
let priority = normalizeStoredPriority(item.signal_priority);
|
|
212
|
+
// A headline about security is not an advisory against your versions:
|
|
213
|
+
// never above medium unless it comes from an advisory database.
|
|
214
|
+
if (storedType === "security_alert" && !ADVISORY_SOURCES.has(item.source_type) && (priority === "critical" || priority === "high")) {
|
|
215
|
+
priority = "medium";
|
|
216
|
+
}
|
|
292
217
|
// Apply filters
|
|
293
|
-
if (params.priority_filter &&
|
|
218
|
+
if (params.priority_filter && priority !== params.priority_filter)
|
|
294
219
|
continue;
|
|
295
|
-
if (params.signal_type &&
|
|
220
|
+
if (params.signal_type && storedType !== params.signal_type)
|
|
296
221
|
continue;
|
|
297
222
|
signals.push({
|
|
298
223
|
id: item.id,
|
|
@@ -300,39 +225,77 @@ export function executeGetActionableSignals(db, params) {
|
|
|
300
225
|
url: item.url,
|
|
301
226
|
source_type: item.source_type,
|
|
302
227
|
relevance_score: item.relevance_score,
|
|
303
|
-
signal_type:
|
|
304
|
-
signal_priority:
|
|
305
|
-
action:
|
|
306
|
-
triggers:
|
|
307
|
-
|
|
228
|
+
signal_type: storedType,
|
|
229
|
+
signal_priority: priority,
|
|
230
|
+
action: actionFor(storedType, item.source_type),
|
|
231
|
+
triggers: [],
|
|
232
|
+
// The judge's verdict when there is one; a structured row carries none.
|
|
233
|
+
confidence: verdict ?? (structured ? 0.5 : 0),
|
|
308
234
|
discovered_ago: item.discovered_ago,
|
|
309
235
|
});
|
|
310
236
|
}
|
|
311
|
-
// Inject live vulnerability data as security_alert signals
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
for (const vuln of vulnResult.vulnerabilities) {
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
237
|
+
// Inject live vulnerability data as security_alert signals — one per
|
|
238
|
+
// vulnerability (alias clusters), not one per OSV record.
|
|
239
|
+
const vulnResult = liveIntel?.getVulnerabilities() ?? null;
|
|
240
|
+
if (vulnResult && vulnResult.vulnerabilities.length > 0) {
|
|
241
|
+
if (!params.signal_type || params.signal_type === "security_alert") {
|
|
242
|
+
for (const { representative: vuln, ids } of clusterVulnerabilities(vulnResult.vulnerabilities)) {
|
|
243
|
+
// The grade every surface shares (AD-046; twin of the app's
|
|
244
|
+
// `osv::identity::scope_adjusted_urgency`), not the raw advisory tier:
|
|
245
|
+
// a transitive of unknown scope read "CRITICAL: Sandbox Breakout" here
|
|
246
|
+
// while the app graded it High (sandbox@3.1.2, 2026-09-10).
|
|
247
|
+
const presented = presentedSeverity(vuln);
|
|
248
|
+
// Low stays low (a "LOW: ..." finding was listed at medium); an
|
|
249
|
+
// advisory with no severity at all is medium, not hidden.
|
|
250
|
+
let priority = presented === "critical" ? "critical" :
|
|
251
|
+
presented === "high" ? "high" :
|
|
252
|
+
presented === "low" ? "low" : "medium";
|
|
253
|
+
let relevance = presented === "critical" ? 1.0 : presented === "high" ? 0.9 : presented === "low" ? 0.5 : 0.7;
|
|
254
|
+
// An installed copy the lockfile does not pin is fixed by a reinstall,
|
|
255
|
+
// not an upgrade: the lockfile is already where it should be.
|
|
256
|
+
let action = vuln.installDriftOf
|
|
257
|
+
? driftAction(vuln)
|
|
258
|
+
: vuln.fixedVersion
|
|
259
|
+
? `Upgrade ${vuln.package} to ${vuln.fixedVersion}`
|
|
260
|
+
: `Review ${vuln.package} — no fix version published`;
|
|
261
|
+
const adjusted = scopeAdjustmentReason(vuln);
|
|
262
|
+
if (adjusted) {
|
|
263
|
+
action += ` (the advisory rates it ${vuln.severity}; graded ${presented} as a ${adjusted})`;
|
|
264
|
+
}
|
|
265
|
+
let label = presented.toUpperCase();
|
|
266
|
+
// "X is unmaintained" is information to plan around, not a fix to
|
|
267
|
+
// apply today; an advisory against a crate this host never builds is
|
|
268
|
+
// not this host's exposure. Both stay visible at LOW so they cannot
|
|
269
|
+
// outrank, or be mistaken for, a live CVE — and the briefing's
|
|
270
|
+
// delegation verdict does not read them at all.
|
|
271
|
+
const maintenance = isMaintenanceNotice(vuln);
|
|
272
|
+
if (maintenance) {
|
|
273
|
+
priority = "low";
|
|
274
|
+
relevance = 0.3;
|
|
275
|
+
action = "Maintenance notice — no fix to apply";
|
|
276
|
+
label = "MAINTENANCE";
|
|
277
|
+
}
|
|
278
|
+
if (vuln.platformActive === false) {
|
|
279
|
+
priority = "low";
|
|
280
|
+
relevance = 0.3;
|
|
281
|
+
action = `Not built on this host — ${action}`;
|
|
282
|
+
// The title must agree with the priority: "MEDIUM: ... (glib@0.18.5)" at
|
|
283
|
+
// priority low read as a medium issue on a Windows host that never builds glib.
|
|
284
|
+
if (!maintenance)
|
|
285
|
+
label = `NOT BUILT HERE (${presented})`;
|
|
286
|
+
}
|
|
287
|
+
if (params.priority_filter && priority !== params.priority_filter)
|
|
323
288
|
continue;
|
|
324
289
|
signals.push({
|
|
325
290
|
id: -1,
|
|
326
|
-
title: `${
|
|
291
|
+
title: `${label}: ${vuln.summary} (${vuln.package}@${vuln.currentVersion})`,
|
|
327
292
|
url: vuln.references[0] || null,
|
|
328
293
|
source_type: "osv_live",
|
|
329
|
-
relevance_score:
|
|
294
|
+
relevance_score: relevance,
|
|
330
295
|
signal_type: "security_alert",
|
|
331
|
-
signal_priority:
|
|
332
|
-
action
|
|
333
|
-
|
|
334
|
-
: `Review ${vuln.package} — no fix version published`,
|
|
335
|
-
triggers: [vuln.vulnId, ...vuln.aliases].filter(Boolean),
|
|
296
|
+
signal_priority: priority,
|
|
297
|
+
action,
|
|
298
|
+
triggers: ids,
|
|
336
299
|
confidence: 1.0,
|
|
337
300
|
discovered_ago: vuln.published ? `published ${vuln.published}` : "recently",
|
|
338
301
|
});
|
|
@@ -45,16 +45,24 @@ export function executeGetContext(db, params) {
|
|
|
45
45
|
if (liveIntel) {
|
|
46
46
|
const deps = liveIntel.getResolvedDeps();
|
|
47
47
|
if (deps.length > 0) {
|
|
48
|
+
// Bounded: the full direct-dependency list ran this answer to ~30 KB
|
|
49
|
+
// on a 150-dependency project. vulnerability_scan / dependency_health
|
|
50
|
+
// are the tools for the whole list.
|
|
48
51
|
return {
|
|
49
52
|
...context,
|
|
50
|
-
resolved_dependencies: deps.map((d) => ({
|
|
53
|
+
resolved_dependencies: deps.slice(0, MAX_LISTED_DEPENDENCIES).map((d) => ({
|
|
51
54
|
name: d.name,
|
|
52
55
|
version: d.version,
|
|
53
56
|
ecosystem: d.ecosystem,
|
|
54
57
|
isDev: d.isDev,
|
|
55
58
|
})),
|
|
59
|
+
resolved_dependencies_total: deps.length,
|
|
60
|
+
...(deps.length > MAX_LISTED_DEPENDENCIES
|
|
61
|
+
? { resolved_dependencies_note: `showing ${MAX_LISTED_DEPENDENCIES} of ${deps.length}; dependency_health lists them all` }
|
|
62
|
+
: {}),
|
|
56
63
|
};
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
return context;
|
|
60
67
|
}
|
|
68
|
+
const MAX_LISTED_DEPENDENCIES = 40;
|
|
@@ -40,4 +40,6 @@ export declare const getRelevantContentTool: {
|
|
|
40
40
|
/**
|
|
41
41
|
* Execute the get_relevant_content tool
|
|
42
42
|
*/
|
|
43
|
-
export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): RelevantItem
|
|
43
|
+
export declare function executeGetRelevantContent(db: FourDADatabase, params: GetRelevantContentParams): Array<RelevantItem & {
|
|
44
|
+
judge_relevance: number | null;
|
|
45
|
+
}>;
|
|
@@ -30,8 +30,13 @@ Use this to find content relevant to the user's current context.`,
|
|
|
30
30
|
},
|
|
31
31
|
source_type: {
|
|
32
32
|
type: "string",
|
|
33
|
-
description:
|
|
34
|
-
enum
|
|
33
|
+
description: "Only items from this source. Leave empty for all sources.",
|
|
34
|
+
// Every source the app fetches; the enum used to list three of them.
|
|
35
|
+
enum: [
|
|
36
|
+
"hackernews", "reddit", "lobsters", "devto", "arxiv", "huggingface", "papers_with_code",
|
|
37
|
+
"github", "crates_io", "npm_registry", "pypi", "go_modules", "cve", "osv", "stackoverflow",
|
|
38
|
+
"rss", "youtube", "mastodon", "lemmy", "bluesky", "twitter", "producthunt",
|
|
39
|
+
],
|
|
35
40
|
},
|
|
36
41
|
limit: {
|
|
37
42
|
type: "number",
|
|
@@ -64,5 +69,31 @@ export function executeGetRelevantContent(db, params) {
|
|
|
64
69
|
// numbers the live scoring brain doesn't stand behind.
|
|
65
70
|
items = db.getRelevantContent(0.0, params.source_type, limit, 720, true);
|
|
66
71
|
}
|
|
67
|
-
|
|
72
|
+
// Items the relevance judge rejected are not "relevant content", whatever
|
|
73
|
+
// their score: measured 2026-10-01, 151 of 950 feed-relevant items carried
|
|
74
|
+
// a latest judge verdict below 0.5. Unjudged items stay (scored, not yet
|
|
75
|
+
// judged) and carry `judge_relevance: null`.
|
|
76
|
+
const verdicts = latestJudgeVerdicts(db, items.map((item) => item.id));
|
|
77
|
+
return items
|
|
78
|
+
.filter((item) => (verdicts.get(item.id) ?? 1) >= 0.5)
|
|
79
|
+
.map((item) => ({ ...item, judge_relevance: verdicts.get(item.id) ?? null }));
|
|
80
|
+
}
|
|
81
|
+
/** Latest relevance-judge verdict per item, when the desktop app has judged it. */
|
|
82
|
+
function latestJudgeVerdicts(db, ids) {
|
|
83
|
+
const out = new Map();
|
|
84
|
+
if (ids.length === 0)
|
|
85
|
+
return out;
|
|
86
|
+
try {
|
|
87
|
+
const rows = db
|
|
88
|
+
.getRawDb()
|
|
89
|
+
.prepare(`SELECT source_item_id AS id, relevance_score AS v FROM llm_judgments
|
|
90
|
+
WHERE id IN (SELECT MAX(id) FROM llm_judgments WHERE source_item_id IN (${ids.map(() => "?").join(",")}) GROUP BY source_item_id)`)
|
|
91
|
+
.all(...ids);
|
|
92
|
+
for (const row of rows)
|
|
93
|
+
out.set(row.id, Math.round(row.v * 100) / 100);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// No llm_judgments table: nothing judged.
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
68
99
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool exports for 4DA MCP Server —
|
|
2
|
+
* Tool exports for 4DA MCP Server — 16 tools across 5 categories
|
|
3
3
|
*
|
|
4
|
-
* Security (
|
|
4
|
+
* Security (5) — vulnerability scanning, dependency health, upgrade planning, upgrade impact, dependency check
|
|
5
5
|
* Intelligence (6) — briefing, ecosystem news, context, content feed, signals, knowledge gaps, feedback
|
|
6
6
|
* Decisions (2) — decision memory, alignment checking
|
|
7
7
|
* Agent (1) — cross-session persistent memory
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
export { vulnerabilityScanTool, executeVulnerabilityScan, } from "./vulnerability-scan.js";
|
|
11
11
|
export { dependencyHealthTool, executeDependencyHealth, } from "./dependency-health.js";
|
|
12
12
|
export { upgradePlannerTool, executeUpgradePlanner, } from "./upgrade-planner.js";
|
|
13
|
+
export { dependencyCheckTool, executeDependencyCheck, } from "./dependency-check.js";
|
|
14
|
+
export { upgradeImpactTool, executeUpgradeImpact, } from "./upgrade-impact.js";
|
|
13
15
|
export { whatShouldIKnowTool, executeWhatShouldIKnow, } from "./what-should-i-know.js";
|
|
14
16
|
export { ecosystemPulseTool, executeEcosystemPulse, } from "./ecosystem-pulse.js";
|
|
15
17
|
export { getContextTool, executeGetContext } from "./get-context.js";
|
package/dist/tools/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
|
-
* Tool exports for 4DA MCP Server —
|
|
3
|
+
* Tool exports for 4DA MCP Server — 16 tools across 5 categories
|
|
4
4
|
*
|
|
5
|
-
* Security (
|
|
5
|
+
* Security (5) — vulnerability scanning, dependency health, upgrade planning, upgrade impact, dependency check
|
|
6
6
|
* Intelligence (6) — briefing, ecosystem news, context, content feed, signals, knowledge gaps, feedback
|
|
7
7
|
* Decisions (2) — decision memory, alignment checking
|
|
8
8
|
* Agent (1) — cross-session persistent memory
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
export { vulnerabilityScanTool, executeVulnerabilityScan, } from "./vulnerability-scan.js";
|
|
13
13
|
export { dependencyHealthTool, executeDependencyHealth, } from "./dependency-health.js";
|
|
14
14
|
export { upgradePlannerTool, executeUpgradePlanner, } from "./upgrade-planner.js";
|
|
15
|
+
export { dependencyCheckTool, executeDependencyCheck, } from "./dependency-check.js";
|
|
16
|
+
export { upgradeImpactTool, executeUpgradeImpact, } from "./upgrade-impact.js";
|
|
15
17
|
// Intelligence
|
|
16
18
|
export { whatShouldIKnowTool, executeWhatShouldIKnow, } from "./what-should-i-know.js";
|
|
17
19
|
export { ecosystemPulseTool, executeEcosystemPulse, } from "./ecosystem-pulse.js";
|