@4da/mcp-server 5.0.2 → 5.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/auth-context.d.ts +0 -1
- package/dist/auth-context.js +0 -1
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -1
- package/dist/db.d.ts +17 -2
- package/dist/db.js +104 -67
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +17 -15
- package/dist/embeddings.d.ts +0 -1
- package/dist/embeddings.js +0 -1
- package/dist/http-transport.d.ts +0 -1
- package/dist/http-transport.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +17 -5
- package/dist/live/cache.d.ts +0 -1
- package/dist/live/cache.js +0 -1
- package/dist/live/crates-registry.d.ts +0 -1
- package/dist/live/crates-registry.js +5 -8
- package/dist/live/cvss.d.ts +0 -1
- package/dist/live/cvss.js +0 -1
- package/dist/live/go-registry.d.ts +0 -1
- package/dist/live/go-registry.js +4 -9
- package/dist/live/hn-fetcher.d.ts +0 -1
- package/dist/live/hn-fetcher.js +23 -11
- package/dist/live/http-utils.d.ts +0 -1
- package/dist/live/http-utils.js +0 -1
- package/dist/live/index.d.ts +0 -1
- package/dist/live/index.js +0 -1
- package/dist/live/npm-registry.d.ts +0 -1
- package/dist/live/npm-registry.js +4 -8
- package/dist/live/osv-scanner.d.ts +20 -2
- package/dist/live/osv-scanner.js +82 -9
- package/dist/live/platform.d.ts +0 -1
- package/dist/live/platform.js +0 -1
- package/dist/live/pypi-registry.d.ts +0 -1
- package/dist/live/pypi-registry.js +7 -10
- package/dist/live/rate-limiter.d.ts +0 -1
- package/dist/live/rate-limiter.js +0 -1
- package/dist/live/semver-utils.d.ts +11 -1
- package/dist/live/semver-utils.js +14 -1
- package/dist/live/types.d.ts +0 -1
- package/dist/live/types.js +0 -1
- package/dist/live/version-resolver.d.ts +0 -1
- package/dist/live/version-resolver.js +0 -1
- package/dist/live-singleton.d.ts +0 -1
- package/dist/live-singleton.js +0 -1
- package/dist/project-scanner.d.ts +0 -1
- package/dist/project-scanner.js +0 -1
- package/dist/schema-registry.d.ts +17 -8
- package/dist/schema-registry.js +54 -9
- package/dist/schemas/developer-dna.json +1 -1
- package/dist/schemas/get-context.json +2 -2
- package/dist/schemas/get-relevant-content.json +1 -1
- package/dist/schemas/record-feedback.json +1 -1
- package/dist/setup.d.ts +0 -1
- package/dist/setup.js +0 -1
- package/dist/tool-dispatch.d.ts +0 -1
- package/dist/tool-dispatch.js +21 -2
- package/dist/tools/agent-memory.d.ts +0 -1
- package/dist/tools/agent-memory.js +0 -1
- package/dist/tools/decision-enforcement.d.ts +0 -1
- package/dist/tools/decision-enforcement.js +0 -1
- package/dist/tools/decision-memory.d.ts +0 -1
- package/dist/tools/decision-memory.js +0 -1
- package/dist/tools/decision-recall.d.ts +0 -1
- package/dist/tools/decision-recall.js +0 -1
- package/dist/tools/dependency-health.d.ts +0 -1
- package/dist/tools/dependency-health.js +31 -5
- package/dist/tools/developer-dna.d.ts +3 -3
- package/dist/tools/developer-dna.js +63 -47
- package/dist/tools/ecosystem-pulse.d.ts +7 -2
- package/dist/tools/ecosystem-pulse.js +39 -3
- package/dist/tools/get-actionable-signals.d.ts +0 -1
- package/dist/tools/get-actionable-signals.js +0 -1
- package/dist/tools/get-context.d.ts +0 -1
- package/dist/tools/get-context.js +2 -3
- package/dist/tools/get-relevant-content.d.ts +0 -1
- package/dist/tools/get-relevant-content.js +1 -3
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/tools/knowledge-gaps.d.ts +0 -1
- package/dist/tools/knowledge-gaps.js +79 -17
- package/dist/tools/recall.d.ts +0 -1
- package/dist/tools/recall.js +0 -1
- package/dist/tools/record-feedback.d.ts +1 -2
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/upgrade-planner.d.ts +0 -1
- package/dist/tools/upgrade-planner.js +15 -3
- package/dist/tools/vulnerability-scan.d.ts +0 -1
- package/dist/tools/vulnerability-scan.js +0 -1
- package/dist/tools/what-should-i-know.d.ts +0 -1
- package/dist/tools/what-should-i-know.js +6 -3
- package/dist/types.d.ts +2 -3
- package/dist/types.js +0 -1
- package/package.json +37 -11
package/README.md
CHANGED
|
@@ -134,11 +134,11 @@ Results are cached (24h for registry data, 1h for vulnerabilities, 30min for new
|
|
|
134
134
|
|------|-------------|
|
|
135
135
|
| `what_should_i_know` | Pre-task intelligence briefing: vulns, decisions, signals, ecosystem updates. |
|
|
136
136
|
| `ecosystem_pulse` | Live ecosystem news from Hacker News, filtered by your detected tech stack. |
|
|
137
|
-
| `get_context` | Your tech stack, resolved dependency versions, interests,
|
|
137
|
+
| `get_context` | Your tech stack, resolved dependency versions, interests, detected topics. |
|
|
138
138
|
| `get_relevant_content`* | Scored content feed — articles, advisories, releases ranked by relevance. |
|
|
139
139
|
| `get_actionable_signals`* | Classified alerts: security advisories, breaking changes, trending repos. |
|
|
140
140
|
| `knowledge_gaps`* | Dependencies you use daily but never read about. Surfaces missed CVEs and updates. |
|
|
141
|
-
| `record_feedback`* | Save or dismiss items
|
|
141
|
+
| `record_feedback`* | Save or dismiss items so 4DA can record explicit interaction history. |
|
|
142
142
|
|
|
143
143
|
### Decisions & Memory
|
|
144
144
|
|
|
@@ -152,7 +152,7 @@ Results are cached (24h for registry data, 1h for vulnerabilities, 30min for new
|
|
|
152
152
|
|
|
153
153
|
| Tool | What it does |
|
|
154
154
|
|------|-------------|
|
|
155
|
-
| `developer_dna`* | Your tech identity: primary stack,
|
|
155
|
+
| `developer_dna`* | Your tech identity: primary stack, top dependencies, blind spots. |
|
|
156
156
|
|
|
157
157
|
*\* Requires the [4DA desktop app](https://4da.ai) for full data.*
|
|
158
158
|
|
package/dist/auth-context.d.ts
CHANGED
package/dist/auth-context.js
CHANGED
package/dist/auth.d.ts
CHANGED
package/dist/auth.js
CHANGED
package/dist/db.d.ts
CHANGED
|
@@ -40,6 +40,22 @@ export interface DataFreshness {
|
|
|
40
40
|
/** Human-readable summary with the remedy when stale. */
|
|
41
41
|
note: string;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Collapse near-duplicate stories (same news via reddit + rss + mastodon…) by
|
|
45
|
+
* token-set Jaccard similarity over normalized titles. Input is score-ordered,
|
|
46
|
+
* so the highest-scored copy of a story survives. Exported for tests.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dedupeByTitle<T extends {
|
|
49
|
+
title: string | null;
|
|
50
|
+
}>(items: T[], threshold?: number): T[];
|
|
51
|
+
/**
|
|
52
|
+
* The dependency-group query the server runs at init against a pre-existing
|
|
53
|
+
* database (index.ts full-DB branch). Exported as a single source of truth so
|
|
54
|
+
* the schema-compatibility regression test exercises the EXACT production SQL —
|
|
55
|
+
* the `is_direct` standalone-schema gap slipped through precisely because the
|
|
56
|
+
* two modes were only ever tested separately.
|
|
57
|
+
*/
|
|
58
|
+
export declare const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, language, project_path, is_dev, is_direct FROM project_dependencies";
|
|
43
59
|
/**
|
|
44
60
|
* 4DA Database accessor
|
|
45
61
|
*/
|
|
@@ -116,7 +132,7 @@ export declare class FourDADatabase {
|
|
|
116
132
|
getFreshness(): DataFreshness;
|
|
117
133
|
/** `MAX(sources.last_fetch)`, tolerant of a missing `sources` table (returns null). */
|
|
118
134
|
private safeMaxLastFetch;
|
|
119
|
-
|
|
135
|
+
hasColumn(table: string, column: string): boolean;
|
|
120
136
|
/**
|
|
121
137
|
* Idempotently add a column to a table if it does not already exist.
|
|
122
138
|
* Used to add the optional `embedding` / `embedding_model` columns to
|
|
@@ -223,4 +239,3 @@ export declare class FourDADatabase {
|
|
|
223
239
|
* Create a database instance
|
|
224
240
|
*/
|
|
225
241
|
export declare function createDatabase(dbPath?: string): FourDADatabase;
|
|
226
|
-
//# sourceMappingURL=db.d.ts.map
|
package/dist/db.js
CHANGED
|
@@ -80,6 +80,60 @@ function minutesSince(ts) {
|
|
|
80
80
|
return null;
|
|
81
81
|
return Math.max(0, Math.round((Date.now() - ms) / 60000));
|
|
82
82
|
}
|
|
83
|
+
/** Filler words ignored when comparing titles for near-duplicate collapsing. */
|
|
84
|
+
const TITLE_STOPWORDS = new Set([
|
|
85
|
+
"the", "a", "an", "is", "are", "was", "out", "in", "on", "of", "for", "to",
|
|
86
|
+
"and", "announcing", "announced", "new", "blog", "via", "with",
|
|
87
|
+
]);
|
|
88
|
+
function titleTokens(title) {
|
|
89
|
+
const tokens = title
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9.]+/g, " ")
|
|
92
|
+
.split(/\s+/)
|
|
93
|
+
.map((t) => t.replace(/^\.+|\.+$/g, ""))
|
|
94
|
+
.filter((t) => t.length > 0 && !TITLE_STOPWORDS.has(t));
|
|
95
|
+
return new Set(tokens);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Collapse near-duplicate stories (same news via reddit + rss + mastodon…) by
|
|
99
|
+
* token-set Jaccard similarity over normalized titles. Input is score-ordered,
|
|
100
|
+
* so the highest-scored copy of a story survives. Exported for tests.
|
|
101
|
+
*/
|
|
102
|
+
export function dedupeByTitle(items, threshold = 0.6) {
|
|
103
|
+
const kept = [];
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const tokens = titleTokens(item.title ?? "");
|
|
106
|
+
if (tokens.size === 0) {
|
|
107
|
+
kept.push({ item, tokens });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
let isDup = false;
|
|
111
|
+
for (const prev of kept) {
|
|
112
|
+
if (prev.tokens.size === 0)
|
|
113
|
+
continue;
|
|
114
|
+
let overlap = 0;
|
|
115
|
+
for (const t of tokens)
|
|
116
|
+
if (prev.tokens.has(t))
|
|
117
|
+
overlap++;
|
|
118
|
+
const union = tokens.size + prev.tokens.size - overlap;
|
|
119
|
+
if (union > 0 && overlap / union >= threshold) {
|
|
120
|
+
isDup = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!isDup)
|
|
125
|
+
kept.push({ item, tokens });
|
|
126
|
+
}
|
|
127
|
+
return kept.map((k) => k.item);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The dependency-group query the server runs at init against a pre-existing
|
|
131
|
+
* database (index.ts full-DB branch). Exported as a single source of truth so
|
|
132
|
+
* the schema-compatibility regression test exercises the EXACT production SQL —
|
|
133
|
+
* the `is_direct` standalone-schema gap slipped through precisely because the
|
|
134
|
+
* two modes were only ever tested separately.
|
|
135
|
+
*/
|
|
136
|
+
export const DEPENDENCY_GROUP_QUERY = "SELECT DISTINCT package_name, language, project_path, is_dev, is_direct FROM project_dependencies";
|
|
83
137
|
/**
|
|
84
138
|
* 4DA Database accessor
|
|
85
139
|
*/
|
|
@@ -112,6 +166,24 @@ export class FourDADatabase {
|
|
|
112
166
|
this.createMinimalSchema();
|
|
113
167
|
this._isStandalone = true;
|
|
114
168
|
}
|
|
169
|
+
// Schema upgrade for standalone databases created before `is_direct` was
|
|
170
|
+
// added to the minimal schema: the full-DB init branch queries that column
|
|
171
|
+
// (DEPENDENCY_GROUP_QUERY), and without it session 2+ of a standalone
|
|
172
|
+
// install throws, gets caught, and silently disables vulnerability_scan /
|
|
173
|
+
// dependency_health / upgrade_planner. Scanner-inserted manifest deps are
|
|
174
|
+
// direct by definition, so DEFAULT 1 backfills correctly. The desktop
|
|
175
|
+
// app's database already has the column — no-op there.
|
|
176
|
+
try {
|
|
177
|
+
const hasDepsTable = this.db
|
|
178
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='project_dependencies'")
|
|
179
|
+
.get();
|
|
180
|
+
if (hasDepsTable) {
|
|
181
|
+
this.ensureColumn("project_dependencies", "is_direct", "INTEGER DEFAULT 1");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
console.error(`[4da] project_dependencies is_direct upgrade failed (dependency tools may be degraded): ${err instanceof Error ? err.message : String(err)}`);
|
|
186
|
+
}
|
|
115
187
|
}
|
|
116
188
|
/**
|
|
117
189
|
* Whether this database was freshly created in standalone mode
|
|
@@ -307,35 +379,10 @@ export class FourDADatabase {
|
|
|
307
379
|
);
|
|
308
380
|
CREATE INDEX IF NOT EXISTS idx_active_topics_topic ON active_topics(topic);
|
|
309
381
|
|
|
310
|
-
--
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
embedding BLOB,
|
|
315
|
-
positive_signals INTEGER DEFAULT 0,
|
|
316
|
-
negative_signals INTEGER DEFAULT 0,
|
|
317
|
-
total_exposures INTEGER DEFAULT 0,
|
|
318
|
-
affinity_score REAL DEFAULT 0.0,
|
|
319
|
-
confidence REAL DEFAULT 0.0,
|
|
320
|
-
last_interaction TEXT DEFAULT (datetime('now')),
|
|
321
|
-
decay_applied INTEGER DEFAULT 0,
|
|
322
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
323
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
-- Anti-topics (learned exclusions) — queried by get_context
|
|
327
|
-
CREATE TABLE IF NOT EXISTS anti_topics (
|
|
328
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
329
|
-
topic TEXT NOT NULL UNIQUE,
|
|
330
|
-
rejection_count INTEGER DEFAULT 0,
|
|
331
|
-
confidence REAL DEFAULT 0.0,
|
|
332
|
-
auto_detected INTEGER DEFAULT 1,
|
|
333
|
-
user_confirmed INTEGER DEFAULT 0,
|
|
334
|
-
first_rejection TEXT DEFAULT (datetime('now')),
|
|
335
|
-
last_rejection TEXT DEFAULT (datetime('now')),
|
|
336
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
337
|
-
updated_at TEXT DEFAULT (datetime('now'))
|
|
338
|
-
);
|
|
382
|
+
-- topic_affinities / anti_topics are deliberately NOT created: the
|
|
383
|
+
-- implicit-capture learning system was removed product-wide (AD-029 →
|
|
384
|
+
-- v20b) and desktop schema 105 dropped both tables. Baking them into
|
|
385
|
+
-- fresh standalone installs would recreate dead tables no code reads.
|
|
339
386
|
|
|
340
387
|
-- Interactions — queried by record_feedback, developer_dna, knowledge_gaps
|
|
341
388
|
CREATE TABLE IF NOT EXISTS interactions (
|
|
@@ -361,6 +408,7 @@ export class FourDADatabase {
|
|
|
361
408
|
package_name TEXT NOT NULL,
|
|
362
409
|
version TEXT,
|
|
363
410
|
is_dev INTEGER DEFAULT 0,
|
|
411
|
+
is_direct INTEGER DEFAULT 1,
|
|
364
412
|
language TEXT NOT NULL,
|
|
365
413
|
last_scanned TEXT NOT NULL DEFAULT (datetime('now')),
|
|
366
414
|
UNIQUE(project_path, package_name)
|
|
@@ -788,10 +836,24 @@ export class FourDADatabase {
|
|
|
788
836
|
query += ` AND source_type = ?`;
|
|
789
837
|
params.push(sourceType);
|
|
790
838
|
}
|
|
791
|
-
|
|
792
|
-
|
|
839
|
+
// Ranked read (desktop audit items 12+26, schema 110): order by the
|
|
840
|
+
// batch-relative rank when the analysis cycle has ranked the item, falling
|
|
841
|
+
// back to the evidence score. This mirrors the Rust source of truth —
|
|
842
|
+
// `RANKED_ORDER_EXPR` in `src-tauri/src/db/scoring_queries.rs` — and must
|
|
843
|
+
// stay textually in sync with it. The `relevance_score >= ?` membership
|
|
844
|
+
// filter above deliberately stays on evidence: evidence decides membership,
|
|
845
|
+
// rank decides order. Guarded on column existence for pre-110 DBs.
|
|
846
|
+
const rankedOrder = this.hasColumn("source_items", "rank_score")
|
|
847
|
+
? `COALESCE(rank_score, relevance_score) DESC`
|
|
848
|
+
: `relevance_score DESC`;
|
|
849
|
+
query += ` ORDER BY ${rankedOrder} LIMIT ?`;
|
|
850
|
+
// Over-fetch so near-duplicate collapsing below can still fill `limit`
|
|
851
|
+
// (the same story arrives via reddit + rss + mastodon: live feeds showed
|
|
852
|
+
// three copies of "Rust 1.98.0" in one 20-item response).
|
|
853
|
+
params.push(limit * 2);
|
|
793
854
|
const stmt = this.db.prepare(query);
|
|
794
|
-
const
|
|
855
|
+
const fetched = stmt.all(...params);
|
|
856
|
+
const items = dedupeByTitle(fetched).slice(0, limit);
|
|
795
857
|
const now = Date.now();
|
|
796
858
|
return items.map((item) => {
|
|
797
859
|
const createdAt = new Date(item.created_at.replace(" ", "T") + "Z");
|
|
@@ -980,41 +1042,17 @@ export class FourDADatabase {
|
|
|
980
1042
|
};
|
|
981
1043
|
}
|
|
982
1044
|
}
|
|
983
|
-
// Learned preferences
|
|
1045
|
+
// Learned preferences: permanently empty by design. The implicit-capture
|
|
1046
|
+
// learning system was removed product-wide (AD-029 quarantine, then v20b
|
|
1047
|
+
// #488 deleted it) and desktop schema 105 DROPPED topic_affinities /
|
|
1048
|
+
// anti_topics. Even a pre-105 database's rows are quarantined data the
|
|
1049
|
+
// product no longer honors, so they are not read — the fields remain in
|
|
1050
|
+
// the shape for API stability.
|
|
984
1051
|
if (includeLearned) {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
WHERE confidence > 0.1
|
|
990
|
-
ORDER BY affinity_score DESC
|
|
991
|
-
LIMIT 50
|
|
992
|
-
`);
|
|
993
|
-
const affinities = affinitiesStmt.all();
|
|
994
|
-
const antiTopicsStmt = this.db.prepare(`
|
|
995
|
-
SELECT topic, rejection_count, confidence, auto_detected, user_confirmed
|
|
996
|
-
FROM anti_topics
|
|
997
|
-
WHERE confidence > 0.3
|
|
998
|
-
ORDER BY confidence DESC
|
|
999
|
-
LIMIT 50
|
|
1000
|
-
`);
|
|
1001
|
-
const antiTopics = antiTopicsStmt.all();
|
|
1002
|
-
context.learned = {
|
|
1003
|
-
topic_affinities: affinities,
|
|
1004
|
-
anti_topics: antiTopics.map((at) => ({
|
|
1005
|
-
...at,
|
|
1006
|
-
auto_detected: Boolean(at.auto_detected),
|
|
1007
|
-
user_confirmed: Boolean(at.user_confirmed),
|
|
1008
|
-
})),
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
catch {
|
|
1012
|
-
// Learned tables might not exist
|
|
1013
|
-
context.learned = {
|
|
1014
|
-
topic_affinities: [],
|
|
1015
|
-
anti_topics: [],
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1052
|
+
context.learned = {
|
|
1053
|
+
topic_affinities: [],
|
|
1054
|
+
anti_topics: [],
|
|
1055
|
+
};
|
|
1018
1056
|
}
|
|
1019
1057
|
return context;
|
|
1020
1058
|
}
|
|
@@ -1486,4 +1524,3 @@ export class FourDADatabase {
|
|
|
1486
1524
|
export function createDatabase(dbPath) {
|
|
1487
1525
|
return new FourDADatabase(dbPath);
|
|
1488
1526
|
}
|
|
1489
|
-
//# sourceMappingURL=db.js.map
|
package/dist/doctor.d.ts
CHANGED
package/dist/doctor.js
CHANGED
|
@@ -20,12 +20,13 @@ export function runDoctor() {
|
|
|
20
20
|
// 1. Node.js version
|
|
21
21
|
const nodeVersion = process.version;
|
|
22
22
|
const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
23
|
+
const nodeRequired = 20;
|
|
23
24
|
checks.push({
|
|
24
25
|
name: "Node.js version",
|
|
25
|
-
status: major >=
|
|
26
|
-
detail: major >=
|
|
27
|
-
? `${nodeVersion} (>=
|
|
28
|
-
: `${nodeVersion} — Node.js
|
|
26
|
+
status: major >= nodeRequired ? "pass" : "fail",
|
|
27
|
+
detail: major >= nodeRequired
|
|
28
|
+
? `${nodeVersion} (>= ${nodeRequired} required)`
|
|
29
|
+
: `${nodeVersion} — Node.js ${nodeRequired}+ required`,
|
|
29
30
|
});
|
|
30
31
|
// 2. Native bindings — if we reached here, the dynamic import in db.ts succeeded
|
|
31
32
|
// (it calls process.exit(1) with a diagnostic message on failure)
|
|
@@ -69,24 +70,26 @@ export function runDoctor() {
|
|
|
69
70
|
detail: envExists ? envDb : `Set to ${envDb} but file does not exist`,
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
|
-
// 5.
|
|
73
|
-
const
|
|
74
|
-
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
73
|
+
// 5. Semantic recall provider (optional)
|
|
74
|
+
const embedProvider = process.env.FOURDA_EMBED_PROVIDER;
|
|
75
75
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
76
|
-
|
|
76
|
+
const hasOllama = process.env.FOURDA_EMBED_PROVIDER === "ollama" ||
|
|
77
|
+
process.env.OLLAMA_HOST ||
|
|
78
|
+
process.env.OLLAMA_BASE_URL;
|
|
79
|
+
if (embedProvider || hasOpenAI || hasOllama) {
|
|
77
80
|
checks.push({
|
|
78
|
-
name: "
|
|
81
|
+
name: "Semantic recall provider",
|
|
79
82
|
status: "pass",
|
|
80
|
-
detail:
|
|
81
|
-
? `${
|
|
82
|
-
:
|
|
83
|
+
detail: embedProvider
|
|
84
|
+
? `${embedProvider} configured`
|
|
85
|
+
: hasOllama ? "Ollama endpoint configured" : "OpenAI API key set",
|
|
83
86
|
});
|
|
84
87
|
}
|
|
85
88
|
else {
|
|
86
89
|
checks.push({
|
|
87
|
-
name: "
|
|
90
|
+
name: "Semantic recall provider",
|
|
88
91
|
status: "warn",
|
|
89
|
-
detail: "
|
|
92
|
+
detail: "Not configured — keyword recall remains available",
|
|
90
93
|
});
|
|
91
94
|
}
|
|
92
95
|
// Print results
|
|
@@ -123,4 +126,3 @@ export function runDoctor() {
|
|
|
123
126
|
console.log(" All checks passed.\n");
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
|
-
//# sourceMappingURL=doctor.js.map
|
package/dist/embeddings.d.ts
CHANGED
package/dist/embeddings.js
CHANGED
package/dist/http-transport.d.ts
CHANGED
|
@@ -84,4 +84,3 @@ export declare function enforceBindPolicy(host: string, policy: HttpSecurityPoli
|
|
|
84
84
|
* Resolves once the server is accepting connections.
|
|
85
85
|
*/
|
|
86
86
|
export declare function startHttpServer(factory: () => Server, options?: HttpServerOptions): Promise<HttpServerHandle>;
|
|
87
|
-
//# sourceMappingURL=http-transport.d.ts.map
|
package/dist/http-transport.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ import { runSetup } from "./setup.js";
|
|
|
42
42
|
import { runDoctor } from "./doctor.js";
|
|
43
43
|
import { scanCurrentProject } from "./project-scanner.js";
|
|
44
44
|
import { LiveIntelligence } from "./live/index.js";
|
|
45
|
+
import { deriveTechStackForHeadlines } from "./tools/ecosystem-pulse.js";
|
|
45
46
|
import { setLiveIntelligence } from "./live-singleton.js";
|
|
46
47
|
// Schema registry for slim tool listing + category metadata
|
|
47
48
|
import { getSlimToolList, getSchemaResources, hasToolSchema, getSchemaFilename, getCategoryManifest } from "./schema-registry.js";
|
|
@@ -61,7 +62,7 @@ const SERVER_VERSION = (() => {
|
|
|
61
62
|
return "0.0.0";
|
|
62
63
|
}
|
|
63
64
|
})();
|
|
64
|
-
import { createDatabase, FourDADatabase } from "./db.js";
|
|
65
|
+
import { createDatabase, DEPENDENCY_GROUP_QUERY, FourDADatabase } from "./db.js";
|
|
65
66
|
// =============================================================================
|
|
66
67
|
// Server Setup
|
|
67
68
|
// =============================================================================
|
|
@@ -137,7 +138,7 @@ function getDatabase() {
|
|
|
137
138
|
// npm deps living in sub-packages.
|
|
138
139
|
try {
|
|
139
140
|
const rawDb = db.getRawDb();
|
|
140
|
-
const rows = rawDb.prepare(
|
|
141
|
+
const rows = rawDb.prepare(DEPENDENCY_GROUP_QUERY).all();
|
|
141
142
|
// Scope to the active project root. Sibling projects tracked in the same
|
|
142
143
|
// database (the ACE engine indexes every local project) must not bleed
|
|
143
144
|
// into this project's vulnerability scan.
|
|
@@ -165,6 +166,17 @@ function getDatabase() {
|
|
|
165
166
|
if (groups.size > 0) {
|
|
166
167
|
liveIntel.initFromDependencyGroups([...groups.values()]);
|
|
167
168
|
}
|
|
169
|
+
// Warm the headline cache for ecosystem_pulse — previously only the
|
|
170
|
+
// standalone branch prefetched, so full-DB servers served an empty
|
|
171
|
+
// cache forever. Non-blocking; the tool also fetches on demand now.
|
|
172
|
+
if (liveIntel.isEnabled()) {
|
|
173
|
+
const techStack = deriveTechStackForHeadlines(db);
|
|
174
|
+
if (techStack.length > 0) {
|
|
175
|
+
liveIntel.fetchHeadlines(techStack).catch((err) => {
|
|
176
|
+
console.error(`[4DA] Headline prefetch failed: ${err instanceof Error ? err.message : String(err)}.`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
168
180
|
}
|
|
169
181
|
catch (err) {
|
|
170
182
|
// Non-fatal — live intel just won't have version data — but log to stderr
|
|
@@ -451,12 +463,12 @@ async function main() {
|
|
|
451
463
|
db.close();
|
|
452
464
|
process.exit(0);
|
|
453
465
|
});
|
|
454
|
-
const toolCount = getSlimToolList().length;
|
|
455
|
-
|
|
466
|
+
const toolCount = getSlimToolList(validation.standalone ? true : undefined).length;
|
|
467
|
+
const toolLabel = validation.standalone ? "standalone tools" : "tools";
|
|
468
|
+
console.error(`4DA MCP Server v${SERVER_VERSION} started — ${toolCount} ${toolLabel}, stdio transport`);
|
|
456
469
|
console.error(" Use --http for Streamable HTTP, --setup to configure editors, --doctor to check health");
|
|
457
470
|
}
|
|
458
471
|
main().catch((error) => {
|
|
459
472
|
console.error("Fatal error:", error);
|
|
460
473
|
process.exit(1);
|
|
461
474
|
});
|
|
462
|
-
//# sourceMappingURL=index.js.map
|
package/dist/live/cache.d.ts
CHANGED
package/dist/live/cache.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Privacy: sends only crate names — public manifest data.
|
|
9
9
|
*/
|
|
10
10
|
import { fetchWithTimeout } from "./http-utils.js";
|
|
11
|
-
import { computeSemverDistance,
|
|
11
|
+
import { computeSemverDistance, maxStableSemver } from "./semver-utils.js";
|
|
12
12
|
const CRATES_TIMEOUT_MS = 8_000;
|
|
13
13
|
const CRATES_CACHE_TTL = 86_400; // 24 hours
|
|
14
14
|
export class CratesRegistry {
|
|
@@ -105,13 +105,11 @@ function parseIndexLines(text) {
|
|
|
105
105
|
}
|
|
106
106
|
return entries;
|
|
107
107
|
}
|
|
108
|
+
// Semver-max among non-yanked entries, not last-published: rsa published
|
|
109
|
+
// 0.9.10 after 0.10.0-rc.*, so last-entry scanning reported the older line
|
|
110
|
+
// as latest stable (see maxStableSemver).
|
|
108
111
|
function findLatestStable(entries) {
|
|
109
|
-
|
|
110
|
-
if (!entries[i].yanked && !isPreRelease(entries[i].vers)) {
|
|
111
|
-
return entries[i].vers;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
112
|
+
return maxStableSemver(entries.filter((e) => !e.yanked).map((e) => e.vers));
|
|
115
113
|
}
|
|
116
114
|
function errorResult(name, currentVersion, isDev, fetchError) {
|
|
117
115
|
return {
|
|
@@ -130,4 +128,3 @@ function errorResult(name, currentVersion, isDev, fetchError) {
|
|
|
130
128
|
fetchError,
|
|
131
129
|
};
|
|
132
130
|
}
|
|
133
|
-
//# sourceMappingURL=crates-registry.js.map
|
package/dist/live/cvss.d.ts
CHANGED
package/dist/live/cvss.js
CHANGED
package/dist/live/go-registry.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Privacy: sends only module paths -- public manifest data.
|
|
10
10
|
*/
|
|
11
11
|
import { fetchJson, fetchWithTimeout } from "./http-utils.js";
|
|
12
|
-
import { computeSemverDistance, isPreRelease } from "./semver-utils.js";
|
|
12
|
+
import { computeSemverDistance, isPreRelease, maxStableSemver } from "./semver-utils.js";
|
|
13
13
|
const GO_PROXY_URL = "https://proxy.golang.org";
|
|
14
14
|
const GO_TIMEOUT_MS = 8_000;
|
|
15
15
|
const GO_CACHE_TTL = 86_400; // 24 hours
|
|
@@ -99,13 +99,9 @@ function escapeModulePath(mod) {
|
|
|
99
99
|
* Versions use "v" prefix (v1.2.3) -- strip for prerelease check.
|
|
100
100
|
*/
|
|
101
101
|
function findLatestStable(versions) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return v;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
102
|
+
// Semver-max, not last-listed (see maxStableSemver). parseSemver strips the
|
|
103
|
+
// "v" prefix; isPreRelease needs it stripped here.
|
|
104
|
+
return maxStableSemver(versions.filter((v) => !isPreRelease(v.replace(/^v/, ""))));
|
|
109
105
|
}
|
|
110
106
|
function errorResult(name, currentVersion, isDev, fetchError) {
|
|
111
107
|
return {
|
|
@@ -124,4 +120,3 @@ function errorResult(name, currentVersion, isDev, fetchError) {
|
|
|
124
120
|
fetchError,
|
|
125
121
|
};
|
|
126
122
|
}
|
|
127
|
-
//# sourceMappingURL=go-registry.js.map
|
package/dist/live/hn-fetcher.js
CHANGED
|
@@ -53,10 +53,14 @@ export class HNFetcher {
|
|
|
53
53
|
return headlines;
|
|
54
54
|
}
|
|
55
55
|
async searchHN(query) {
|
|
56
|
+
// "Pulse" means current: without a recency floor Algolia's point-sorted
|
|
57
|
+
// results are dominated by multi-year-old all-time hits (e.g. the Tauri
|
|
58
|
+
// 1.0 launch), which is archaeology, not ecosystem news.
|
|
59
|
+
const fourteenDaysAgo = Math.floor(Date.now() / 1000) - 14 * 24 * 60 * 60;
|
|
56
60
|
const params = new URLSearchParams({
|
|
57
61
|
query,
|
|
58
62
|
tags: "story",
|
|
59
|
-
numericFilters: `points>${MIN_POINTS}`,
|
|
63
|
+
numericFilters: `points>${MIN_POINTS},created_at_i>${fourteenDaysAgo}`,
|
|
60
64
|
hitsPerPage: String(RESULTS_PER_QUERY),
|
|
61
65
|
});
|
|
62
66
|
const url = `${HN_ALGOLIA_URL}?${params}`;
|
|
@@ -82,12 +86,16 @@ function buildQueries(techStack) {
|
|
|
82
86
|
return prioritized;
|
|
83
87
|
}
|
|
84
88
|
function wordBoundaryMatch(text, term) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Check EVERY occurrence — indexOf-only missed "react" in a title whose
|
|
90
|
+
// first hit was inside "reactor".
|
|
91
|
+
let idx = -1;
|
|
92
|
+
while ((idx = text.indexOf(term, idx + 1)) !== -1) {
|
|
93
|
+
const before = idx === 0 || !/[a-zA-Z0-9]/.test(text[idx - 1]);
|
|
94
|
+
const after = idx + term.length >= text.length || !/[a-zA-Z0-9]/.test(text[idx + term.length]);
|
|
95
|
+
if (before && after)
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
91
99
|
}
|
|
92
100
|
function scoreAndMap(hits, techStack) {
|
|
93
101
|
const techLower = techStack.map((t) => t.toLowerCase());
|
|
@@ -95,6 +103,13 @@ function scoreAndMap(hits, techStack) {
|
|
|
95
103
|
.map((hit) => {
|
|
96
104
|
const titleLower = hit.title.toLowerCase();
|
|
97
105
|
const matchedTech = techLower.filter((t) => wordBoundaryMatch(titleLower, t));
|
|
106
|
+
return { hit, matchedTech };
|
|
107
|
+
})
|
|
108
|
+
// The tool's contract is "filtered by detected technologies" — a hit whose
|
|
109
|
+
// title matches none of the user's stack is Algolia query noise (nuclear
|
|
110
|
+
// "reactors" and weather stories arrived this way), not ecosystem news.
|
|
111
|
+
.filter(({ matchedTech }) => matchedTech.length > 0)
|
|
112
|
+
.map(({ hit, matchedTech }) => {
|
|
98
113
|
const techBoost = Math.min(matchedTech.length * 0.2, 0.4);
|
|
99
114
|
const pointsBoost = Math.min(hit.points / 500, 0.3);
|
|
100
115
|
const commentsBoost = Math.min(hit.num_comments / 200, 0.2);
|
|
@@ -108,9 +123,7 @@ function scoreAndMap(hits, techStack) {
|
|
|
108
123
|
comments: hit.num_comments,
|
|
109
124
|
published: hit.created_at,
|
|
110
125
|
relevanceScore: Math.round(score * 100) / 100,
|
|
111
|
-
relevanceReason: matchedTech.
|
|
112
|
-
? `Matches your stack: ${matchedTech.join(", ")}`
|
|
113
|
-
: `Trending (${hit.points} points)`,
|
|
126
|
+
relevanceReason: `Matches your stack: ${matchedTech.join(", ")}`,
|
|
114
127
|
};
|
|
115
128
|
})
|
|
116
129
|
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
@@ -120,4 +133,3 @@ const SKIP_TERMS = new Set([
|
|
|
120
133
|
"javascript", "typescript", // too generic, floods results
|
|
121
134
|
"docker", // too broad
|
|
122
135
|
]);
|
|
123
|
-
//# sourceMappingURL=hn-fetcher.js.map
|
package/dist/live/http-utils.js
CHANGED