@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.
Files changed (96) hide show
  1. package/README.md +3 -3
  2. package/dist/auth-context.d.ts +0 -1
  3. package/dist/auth-context.js +0 -1
  4. package/dist/auth.d.ts +0 -1
  5. package/dist/auth.js +0 -1
  6. package/dist/db.d.ts +17 -2
  7. package/dist/db.js +104 -67
  8. package/dist/doctor.d.ts +0 -1
  9. package/dist/doctor.js +17 -15
  10. package/dist/embeddings.d.ts +0 -1
  11. package/dist/embeddings.js +0 -1
  12. package/dist/http-transport.d.ts +0 -1
  13. package/dist/http-transport.js +0 -1
  14. package/dist/index.d.ts +0 -1
  15. package/dist/index.js +17 -5
  16. package/dist/live/cache.d.ts +0 -1
  17. package/dist/live/cache.js +0 -1
  18. package/dist/live/crates-registry.d.ts +0 -1
  19. package/dist/live/crates-registry.js +5 -8
  20. package/dist/live/cvss.d.ts +0 -1
  21. package/dist/live/cvss.js +0 -1
  22. package/dist/live/go-registry.d.ts +0 -1
  23. package/dist/live/go-registry.js +4 -9
  24. package/dist/live/hn-fetcher.d.ts +0 -1
  25. package/dist/live/hn-fetcher.js +23 -11
  26. package/dist/live/http-utils.d.ts +0 -1
  27. package/dist/live/http-utils.js +0 -1
  28. package/dist/live/index.d.ts +0 -1
  29. package/dist/live/index.js +0 -1
  30. package/dist/live/npm-registry.d.ts +0 -1
  31. package/dist/live/npm-registry.js +4 -8
  32. package/dist/live/osv-scanner.d.ts +20 -2
  33. package/dist/live/osv-scanner.js +82 -9
  34. package/dist/live/platform.d.ts +0 -1
  35. package/dist/live/platform.js +0 -1
  36. package/dist/live/pypi-registry.d.ts +0 -1
  37. package/dist/live/pypi-registry.js +7 -10
  38. package/dist/live/rate-limiter.d.ts +0 -1
  39. package/dist/live/rate-limiter.js +0 -1
  40. package/dist/live/semver-utils.d.ts +11 -1
  41. package/dist/live/semver-utils.js +14 -1
  42. package/dist/live/types.d.ts +0 -1
  43. package/dist/live/types.js +0 -1
  44. package/dist/live/version-resolver.d.ts +0 -1
  45. package/dist/live/version-resolver.js +0 -1
  46. package/dist/live-singleton.d.ts +0 -1
  47. package/dist/live-singleton.js +0 -1
  48. package/dist/project-scanner.d.ts +0 -1
  49. package/dist/project-scanner.js +0 -1
  50. package/dist/schema-registry.d.ts +17 -8
  51. package/dist/schema-registry.js +54 -9
  52. package/dist/schemas/developer-dna.json +1 -1
  53. package/dist/schemas/get-context.json +2 -2
  54. package/dist/schemas/get-relevant-content.json +1 -1
  55. package/dist/schemas/record-feedback.json +1 -1
  56. package/dist/setup.d.ts +0 -1
  57. package/dist/setup.js +0 -1
  58. package/dist/tool-dispatch.d.ts +0 -1
  59. package/dist/tool-dispatch.js +21 -2
  60. package/dist/tools/agent-memory.d.ts +0 -1
  61. package/dist/tools/agent-memory.js +0 -1
  62. package/dist/tools/decision-enforcement.d.ts +0 -1
  63. package/dist/tools/decision-enforcement.js +0 -1
  64. package/dist/tools/decision-memory.d.ts +0 -1
  65. package/dist/tools/decision-memory.js +0 -1
  66. package/dist/tools/decision-recall.d.ts +0 -1
  67. package/dist/tools/decision-recall.js +0 -1
  68. package/dist/tools/dependency-health.d.ts +0 -1
  69. package/dist/tools/dependency-health.js +31 -5
  70. package/dist/tools/developer-dna.d.ts +3 -3
  71. package/dist/tools/developer-dna.js +63 -47
  72. package/dist/tools/ecosystem-pulse.d.ts +7 -2
  73. package/dist/tools/ecosystem-pulse.js +39 -3
  74. package/dist/tools/get-actionable-signals.d.ts +0 -1
  75. package/dist/tools/get-actionable-signals.js +0 -1
  76. package/dist/tools/get-context.d.ts +0 -1
  77. package/dist/tools/get-context.js +2 -3
  78. package/dist/tools/get-relevant-content.d.ts +0 -1
  79. package/dist/tools/get-relevant-content.js +1 -3
  80. package/dist/tools/index.d.ts +0 -1
  81. package/dist/tools/index.js +0 -1
  82. package/dist/tools/knowledge-gaps.d.ts +0 -1
  83. package/dist/tools/knowledge-gaps.js +79 -17
  84. package/dist/tools/recall.d.ts +0 -1
  85. package/dist/tools/recall.js +0 -1
  86. package/dist/tools/record-feedback.d.ts +1 -2
  87. package/dist/tools/record-feedback.js +7 -8
  88. package/dist/tools/upgrade-planner.d.ts +0 -1
  89. package/dist/tools/upgrade-planner.js +15 -3
  90. package/dist/tools/vulnerability-scan.d.ts +0 -1
  91. package/dist/tools/vulnerability-scan.js +0 -1
  92. package/dist/tools/what-should-i-know.d.ts +0 -1
  93. package/dist/tools/what-should-i-know.js +6 -3
  94. package/dist/types.d.ts +2 -3
  95. package/dist/types.js +0 -1
  96. 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, affinities. |
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 to teach 4DA what matters. Sharpens future scoring. |
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, engagement patterns, blind spots. |
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
 
@@ -46,4 +46,3 @@ export declare function requiredOperation(toolName: string): "read" | "write";
46
46
  * Throws `AuthorizationError` when the role is insufficient.
47
47
  */
48
48
  export declare function assertToolPermission(toolName: string): void;
49
- //# sourceMappingURL=auth-context.d.ts.map
@@ -69,4 +69,3 @@ export function assertToolPermission(toolName) {
69
69
  throw new AuthorizationError(`Role '${ctx.claims.role}' is not permitted to perform '${operation}' operations (tool: ${toolName})`);
70
70
  }
71
71
  }
72
- //# sourceMappingURL=auth-context.js.map
package/dist/auth.d.ts CHANGED
@@ -60,4 +60,3 @@ export declare function extractAuthClaims(req: IncomingMessage): TeamClaims | nu
60
60
  * `readOnlyHint` annotation.
61
61
  */
62
62
  export declare function hasPermission(role: string, operation: "read" | "write" | "admin"): boolean;
63
- //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js CHANGED
@@ -190,4 +190,3 @@ export function hasPermission(role, operation) {
190
190
  return false;
191
191
  }
192
192
  }
193
- //# sourceMappingURL=auth.js.map
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
- private hasColumn;
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
- -- Topic affinities (learned) — queried by get_context, developer_dna, attention_report
311
- CREATE TABLE IF NOT EXISTS topic_affinities (
312
- id INTEGER PRIMARY KEY AUTOINCREMENT,
313
- topic TEXT NOT NULL UNIQUE,
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
- query += ` ORDER BY relevance_score DESC LIMIT ?`;
792
- params.push(limit);
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 items = stmt.all(...params);
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
- try {
986
- const affinitiesStmt = this.db.prepare(`
987
- SELECT topic, affinity_score, confidence, positive_signals, negative_signals, total_exposures
988
- FROM topic_affinities
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
@@ -12,4 +12,3 @@
12
12
  * 4da-mcp --doctor
13
13
  */
14
14
  export declare function runDoctor(): void;
15
- //# sourceMappingURL=doctor.d.ts.map
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 >= 18 ? "pass" : "fail",
26
- detail: major >= 18
27
- ? `${nodeVersion} (>= 18 required)`
28
- : `${nodeVersion} — Node.js 18+ required`,
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. LLM providers (optional)
73
- const llmProvider = process.env.LLM_PROVIDER;
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
- if (llmProvider || hasAnthropic || hasOpenAI) {
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: "LLM provider",
81
+ name: "Semantic recall provider",
79
82
  status: "pass",
80
- detail: llmProvider
81
- ? `${llmProvider} configured`
82
- : hasAnthropic ? "Anthropic API key set" : "OpenAI API key set",
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: "LLM provider",
90
+ name: "Semantic recall provider",
88
91
  status: "warn",
89
- detail: "No API keys set — AI synthesis tools will be unavailable",
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
@@ -85,4 +85,3 @@ export declare function semanticScores(db: FourDADatabase, table: string, query:
85
85
  }>, config: EmbeddingConfig, opts?: {
86
86
  maxNewEmbeds?: number;
87
87
  }): Promise<SemanticScoreResult>;
88
- //# sourceMappingURL=embeddings.d.ts.map
@@ -289,4 +289,3 @@ function loadStoredEmbeddings(rawDb, table, ids, modelTagValue) {
289
289
  }
290
290
  return out;
291
291
  }
292
- //# sourceMappingURL=embeddings.js.map
@@ -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
@@ -220,4 +220,3 @@ export async function startHttpServer(factory, options = { port: DEFAULT_PORT, h
220
220
  process.once("SIGTERM", shutdown);
221
221
  return { server: httpServer, close };
222
222
  }
223
- //# sourceMappingURL=http-transport.js.map
package/dist/index.d.ts CHANGED
@@ -9,4 +9,3 @@ import { Server } from "@modelcontextprotocol/server";
9
9
  * shares the same lazily-initialized database and live-intelligence layer.
10
10
  */
11
11
  export declare function buildServer(): Server;
12
- //# sourceMappingURL=index.d.ts.map
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("SELECT DISTINCT package_name, language, project_path, is_dev, is_direct FROM project_dependencies").all();
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
- console.error(`4DA MCP Server v${SERVER_VERSION} started — ${toolCount} tools, stdio transport`);
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
@@ -13,4 +13,3 @@ export declare class LiveCache {
13
13
  invalidateSource(source: string): void;
14
14
  invalidateAll(): void;
15
15
  }
16
- //# sourceMappingURL=cache.d.ts.map
@@ -58,4 +58,3 @@ export class LiveCache {
58
58
  this.db.prepare("DELETE FROM live_cache").run();
59
59
  }
60
60
  }
61
- //# sourceMappingURL=cache.js.map
@@ -15,4 +15,3 @@ export declare class CratesRegistry {
15
15
  constructor(cache: LiveCache, rateLimiter: RateLimiter);
16
16
  getPackageInfo(name: string, currentVersion: string | null, isDev: boolean): Promise<RegistryPackageInfo>;
17
17
  }
18
- //# sourceMappingURL=crates-registry.d.ts.map
@@ -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, isPreRelease } from "./semver-utils.js";
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
- for (let i = entries.length - 1; i >= 0; i--) {
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
@@ -13,4 +13,3 @@
13
13
  * v2/v4, or malformed).
14
14
  */
15
15
  export declare function cvssBaseScore(vector: string): number | null;
16
- //# sourceMappingURL=cvss.d.ts.map
package/dist/live/cvss.js CHANGED
@@ -59,4 +59,3 @@ export function cvssBaseScore(vector) {
59
59
  const raw = scope === "C" ? 1.08 * (impact + exploitability) : impact + exploitability;
60
60
  return roundUp(Math.min(raw, 10));
61
61
  }
62
- //# sourceMappingURL=cvss.js.map
@@ -16,4 +16,3 @@ export declare class GoRegistry {
16
16
  constructor(cache: LiveCache, rateLimiter: RateLimiter);
17
17
  getPackageInfo(name: string, currentVersion: string | null, isDev: boolean): Promise<RegistryPackageInfo>;
18
18
  }
19
- //# sourceMappingURL=go-registry.d.ts.map
@@ -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
- for (let i = versions.length - 1; i >= 0; i--) {
103
- const v = versions[i];
104
- if (!isPreRelease(v.replace(/^v/, ""))) {
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
@@ -16,4 +16,3 @@ export declare class HNFetcher {
16
16
  fetch(techStack: string[]): Promise<LiveHeadline[]>;
17
17
  private searchHN;
18
18
  }
19
- //# sourceMappingURL=hn-fetcher.d.ts.map
@@ -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
- const idx = text.indexOf(term);
86
- if (idx === -1)
87
- return false;
88
- const before = idx === 0 || !/[a-zA-Z0-9]/.test(text[idx - 1]);
89
- const after = idx + term.length >= text.length || !/[a-zA-Z0-9]/.test(text[idx + term.length]);
90
- return before && after;
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.length > 0
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
@@ -1,3 +1,2 @@
1
1
  export declare function fetchWithTimeout(url: string, options?: RequestInit, timeoutMs?: number): Promise<Response>;
2
2
  export declare function fetchJson<T>(url: string, options?: RequestInit, timeoutMs?: number): Promise<T>;
3
- //# sourceMappingURL=http-utils.d.ts.map
@@ -17,4 +17,3 @@ export async function fetchJson(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_M
17
17
  }
18
18
  return (await response.json());
19
19
  }
20
- //# sourceMappingURL=http-utils.js.map