@4da/mcp-server 5.0.1 → 5.0.2

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 CHANGED
@@ -172,7 +172,7 @@ The MCP server works without the desktop app. On first run it creates a local da
172
172
  | Agent memory (cross-session) | Yes | Yes |
173
173
  | Scored content feed (20+ sources) | -- | Yes |
174
174
  | Actionable signals + knowledge gaps | -- | Yes |
175
- | Compound intelligence (learns over time) | -- | Yes |
175
+ | The analysis layer (Signal Chains, Knowledge Gaps, temporal analysis) | -- | Yes |
176
176
 
177
177
  > **[Download 4DA](https://github.com/4DA-Systems/4DA/releases/latest)** for the full experience.
178
178
 
@@ -229,7 +229,7 @@ npx @4da/mcp-server --version # Print version
229
229
  No. The server sends package names and versions to public APIs ([OSV.dev](https://osv.dev), npm registry, crates.io, PyPI, Go proxy) and generic tech keywords to [HN Algolia](https://hn.algolia.com/api). The same public data visible in your `package.json`. No source code, no file paths, no personal data. Set `FOURDA_OFFLINE=true` to disable all network calls. (The sole exception is opt-in OpenAI embeddings — see the network note above.)
230
230
 
231
231
  **Do I need the 4DA desktop app?**
232
- No. 9 tools work standalone: vulnerability scanning, dependency health, upgrade planning, ecosystem news, pre-task briefings, project context, decision memory, alignment checking, and agent memory. The desktop app adds scored content from 20+ sources that compounds over time.
232
+ No. 9 tools work standalone: vulnerability scanning, dependency health, upgrade planning, ecosystem news, pre-task briefings, project context, decision memory, alignment checking, and agent memory. The desktop app adds a scored content feed from 20+ sources, graded against your actual stack.
233
233
 
234
234
  **Which AI tools does this work with?**
235
235
  Any tool that supports [MCP](https://modelcontextprotocol.io): Claude Code, Claude Desktop, Cursor, Windsurf, VS Code (Copilot), and any custom MCP client.
package/dist/index.js CHANGED
File without changes
package/dist/setup.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4da/mcp-server",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
4
4
  "mcpName": "io.github.4DA-Systems/4da-mcp-server",
5
5
  "description": "Dependency intelligence for AI coding agents. Live CVE scanning, dependency health, upgrade planning, ecosystem news, decision memory — zero config, privacy-first. 9 tools standalone, 14 total with desktop app.",
6
6
  "type": "module",
@@ -18,17 +18,6 @@
18
18
  "README.md",
19
19
  "LICENSE"
20
20
  ],
21
- "scripts": {
22
- "build": "tsc && pnpm run copy-schemas",
23
- "copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
24
- "dev": "tsc --watch",
25
- "start": "node dist/index.js",
26
- "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
27
- "setup": "node dist/setup.js",
28
- "test": "vitest run",
29
- "test:watch": "vitest",
30
- "prepublishOnly": "pnpm run build"
31
- },
32
21
  "keywords": [
33
22
  "mcp",
34
23
  "mcp-server",
@@ -95,18 +84,14 @@
95
84
  "engines": {
96
85
  "node": ">=20.0.0"
97
86
  },
98
- "pnpm": {
99
- "onlyBuiltDependencies": [
100
- "better-sqlite3",
101
- "esbuild"
102
- ],
103
- "overrides": {
104
- "fast-uri": ">=3.1.2",
105
- "ip-address": "^10.1.1",
106
- "vite": ">=7.3.5 <8",
107
- "esbuild": ">=0.28.1",
108
- "hono": ">=4.12.34 <5",
109
- "@hono/node-server": ">=2.0.5 <3"
110
- }
87
+ "scripts": {
88
+ "build": "tsc && pnpm run copy-schemas",
89
+ "copy-schemas": "node -e \"const fs=require('fs');const path=require('path');const src='src/schemas';const dst='dist/schemas';if(!fs.existsSync(dst))fs.mkdirSync(dst,{recursive:true});fs.readdirSync(src).filter(f=>f.endsWith('.json')).forEach(f=>fs.copyFileSync(path.join(src,f),path.join(dst,f)))\"",
90
+ "dev": "tsc --watch",
91
+ "start": "node dist/index.js",
92
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
93
+ "setup": "node dist/setup.js",
94
+ "test": "vitest run",
95
+ "test:watch": "vitest"
111
96
  }
112
- }
97
+ }
package/dist/llm.d.ts DELETED
@@ -1,162 +0,0 @@
1
- /**
2
- * LLM Client for 4DA MCP Server
3
- *
4
- * Provides intelligence synthesis over raw tool data.
5
- * Supports Anthropic Claude, OpenAI, and Ollama (local).
6
- *
7
- * Features:
8
- * - Tiered model selection (use smaller models for simpler tasks)
9
- * - Optimized prompts for local models
10
- * - Automatic fallback
11
- */
12
- export interface LLMConfig {
13
- provider: "anthropic" | "openai" | "ollama" | null;
14
- anthropic_api_key?: string;
15
- openai_api_key?: string;
16
- ollama_url?: string;
17
- model?: string;
18
- model_light?: string;
19
- model_heavy?: string;
20
- }
21
- export type TaskComplexity = "light" | "medium" | "heavy";
22
- export interface SynthesisRequest {
23
- system: string;
24
- prompt: string;
25
- max_tokens?: number;
26
- complexity?: TaskComplexity;
27
- }
28
- export interface SynthesisResult {
29
- synthesis: string;
30
- model_used: string;
31
- tokens_used?: number;
32
- latency_ms?: number;
33
- }
34
- /**
35
- * Default models for different task complexities
36
- * Optimized for 16GB VRAM
37
- */
38
- export declare const DEFAULT_OLLAMA_MODELS: {
39
- light: string;
40
- medium: string;
41
- heavy: string;
42
- };
43
- /**
44
- * Task complexity by tool
45
- */
46
- export declare const TOOL_COMPLEXITY: Record<string, TaskComplexity>;
47
- /**
48
- * Get LLM configuration from settings.json file.
49
- *
50
- * Uses a cached approach: the file is only re-read if its mtime has changed,
51
- * avoiding unnecessary filesystem reads on every tool invocation.
52
- */
53
- export declare function getLLMConfig(_db?: {
54
- prepare: (sql: string) => {
55
- get: (...args: unknown[]) => unknown;
56
- };
57
- }): LLMConfig;
58
- /**
59
- * Check if LLM synthesis is available
60
- */
61
- export declare function canSynthesize(config: LLMConfig): boolean;
62
- /**
63
- * Synthesize insights using LLM
64
- *
65
- * Accepts an optional AbortSignal to allow timeout/cancellation of the
66
- * underlying fetch calls.
67
- */
68
- export declare function synthesize(config: LLMConfig, request: SynthesisRequest, signal?: AbortSignal): Promise<SynthesisResult>;
69
- /**
70
- * Synthesize with a timeout wrapper.
71
- *
72
- * Wraps the LLM API call with an AbortController + configurable timeout.
73
- * On timeout, throws a descriptive error so callers can fall back to
74
- * data-only results.
75
- *
76
- * @param config - LLM provider configuration
77
- * @param request - Synthesis request (system prompt, user prompt, etc.)
78
- * @param timeoutMs - Timeout in milliseconds (default: 30000)
79
- */
80
- export declare function synthesizeWithTimeout(config: LLMConfig, request: SynthesisRequest, timeoutMs?: number): Promise<SynthesisResult>;
81
- /**
82
- * Prompts are optimized for:
83
- * 1. Clear structure (local models need explicit formatting)
84
- * 2. Concise output (fewer tokens = faster inference)
85
- * 3. Specific instructions (reduce ambiguity)
86
- */
87
- export declare const SYNTHESIS_PROMPTS: {
88
- /**
89
- * Daily Briefing - Executive synthesis
90
- * Complexity: MEDIUM
91
- */
92
- dailyBriefing: {
93
- complexity: TaskComplexity;
94
- system: string;
95
- buildPrompt: (data: unknown, context: unknown) => string;
96
- };
97
- /**
98
- * Score Autopsy - Explain the reasoning
99
- * Complexity: HEAVY (needs to reason about correctness)
100
- */
101
- scoreAutopsy: {
102
- complexity: TaskComplexity;
103
- system: string;
104
- buildPrompt: (data: unknown, context: unknown) => string;
105
- };
106
- /**
107
- * Trend Analysis - Predict and explain
108
- * Complexity: HEAVY (needs pattern recognition + prediction)
109
- */
110
- trendAnalysis: {
111
- complexity: TaskComplexity;
112
- system: string;
113
- buildPrompt: (data: unknown, context: unknown) => string;
114
- };
115
- /**
116
- * Context Analysis - Personalized optimization advice
117
- * Complexity: MEDIUM (structured recommendations)
118
- */
119
- contextAnalysis: {
120
- complexity: TaskComplexity;
121
- system: string;
122
- buildPrompt: (data: unknown) => string;
123
- };
124
- /**
125
- * Topic Connections - Explain the knowledge graph
126
- * Complexity: HEAVY (needs graph reasoning)
127
- */
128
- topicConnections: {
129
- complexity: TaskComplexity;
130
- system: string;
131
- buildPrompt: (data: unknown, context: unknown) => string;
132
- };
133
- };
134
- /**
135
- * Check if Ollama is running and has required models
136
- */
137
- export declare function checkOllamaStatus(baseUrl?: string): Promise<{
138
- available: boolean;
139
- models: string[];
140
- error?: string;
141
- }>;
142
- /**
143
- * Recommended models for 4DA synthesis tasks
144
- */
145
- export declare const RECOMMENDED_MODELS: {
146
- "16GB_VRAM": {
147
- primary: string;
148
- fallback: string;
149
- install: string;
150
- };
151
- "12GB_VRAM": {
152
- primary: string;
153
- fallback: string;
154
- install: string;
155
- };
156
- "8GB_VRAM": {
157
- primary: string;
158
- fallback: string;
159
- install: string;
160
- };
161
- };
162
- //# sourceMappingURL=llm.d.ts.map
package/dist/llm.js DELETED
@@ -1,502 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * LLM Client for 4DA MCP Server
4
- *
5
- * Provides intelligence synthesis over raw tool data.
6
- * Supports Anthropic Claude, OpenAI, and Ollama (local).
7
- *
8
- * Features:
9
- * - Tiered model selection (use smaller models for simpler tasks)
10
- * - Optimized prompts for local models
11
- * - Automatic fallback
12
- */
13
- // =============================================================================
14
- // Model Tiers - Optimized for 16GB VRAM (RTX 4080 Super)
15
- // =============================================================================
16
- /**
17
- * Default models for different task complexities
18
- * Optimized for 16GB VRAM
19
- */
20
- export const DEFAULT_OLLAMA_MODELS = {
21
- light: "gemma3:4b",
22
- medium: "qwen3:8b",
23
- heavy: "qwen3:14b",
24
- };
25
- /**
26
- * Task complexity by tool
27
- */
28
- export const TOOL_COMPLEXITY = {
29
- daily_briefing: "medium", // Needs to synthesize multiple themes
30
- score_autopsy: "heavy", // Needs to reason about score correctness
31
- trend_analysis: "heavy", // Needs pattern recognition + prediction
32
- context_analysis: "medium", // Structured advice
33
- topic_connections: "heavy", // Graph reasoning
34
- };
35
- // =============================================================================
36
- // Configuration
37
- // =============================================================================
38
- import { readFileSync, existsSync, statSync } from "fs";
39
- import { join, dirname } from "path";
40
- import { fileURLToPath } from "node:url";
41
- const __filename = fileURLToPath(import.meta.url);
42
- const __dirname = dirname(__filename);
43
- /**
44
- * Find settings.json by looking in common locations
45
- */
46
- function findSettingsFile() {
47
- // 1. Environment variable (highest priority)
48
- const envPath = process.env.FOURDA_SETTINGS_PATH;
49
- if (envPath && existsSync(envPath)) {
50
- return envPath;
51
- }
52
- // 2. Sibling to database path
53
- const dbPath = process.env.FOURDA_DB_PATH;
54
- if (dbPath) {
55
- const settingsPath = join(dirname(dbPath), "settings.json");
56
- if (existsSync(settingsPath)) {
57
- return settingsPath;
58
- }
59
- }
60
- // 3. Common relative locations (portable — no hardcoded dev paths)
61
- const searchPaths = [
62
- join(process.cwd(), "data", "settings.json"), // CWD/data/
63
- join(__dirname, "..", "..", "data", "settings.json"), // Project root (mcp-4da-server/../data/)
64
- join(process.cwd(), "..", "data", "settings.json"), // Parent/data/
65
- ];
66
- for (const candidate of searchPaths) {
67
- if (existsSync(candidate)) {
68
- return candidate;
69
- }
70
- }
71
- return null;
72
- }
73
- // =============================================================================
74
- // Settings Cache — re-read only when file mtime changes
75
- // =============================================================================
76
- let _cachedConfig = null;
77
- let _cachedSettingsPath = null;
78
- let _cachedMtimeMs = 0;
79
- /**
80
- * Get LLM configuration from settings.json file.
81
- *
82
- * Uses a cached approach: the file is only re-read if its mtime has changed,
83
- * avoiding unnecessary filesystem reads on every tool invocation.
84
- */
85
- export function getLLMConfig(_db) {
86
- const settingsPath = findSettingsFile();
87
- if (!settingsPath) {
88
- // Return environment-based config as fallback
89
- return {
90
- provider: process.env.LLM_PROVIDER || null,
91
- anthropic_api_key: process.env.ANTHROPIC_API_KEY,
92
- openai_api_key: process.env.OPENAI_API_KEY,
93
- ollama_url: process.env.OLLAMA_URL || "http://localhost:11434",
94
- model: process.env.LLM_MODEL,
95
- model_light: process.env.LLM_MODEL_LIGHT,
96
- model_heavy: process.env.LLM_MODEL_HEAVY,
97
- };
98
- }
99
- // Check if we can use the cached config
100
- try {
101
- const currentMtimeMs = statSync(settingsPath).mtimeMs;
102
- if (_cachedConfig &&
103
- _cachedSettingsPath === settingsPath &&
104
- _cachedMtimeMs === currentMtimeMs) {
105
- return _cachedConfig;
106
- }
107
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
108
- const llm = settings.llm || {};
109
- const config = {
110
- provider: (llm.provider ?? null),
111
- anthropic_api_key: llm.api_key || llm.anthropic_api_key,
112
- openai_api_key: llm.openai_api_key,
113
- ollama_url: llm.base_url || llm.ollama_url || "http://localhost:11434",
114
- model: llm.model,
115
- model_light: llm.model_light,
116
- model_heavy: llm.model_heavy,
117
- };
118
- // Update cache
119
- _cachedConfig = config;
120
- _cachedSettingsPath = settingsPath;
121
- _cachedMtimeMs = currentMtimeMs;
122
- return config;
123
- }
124
- catch (error) {
125
- console.error("Failed to read settings.json:", error);
126
- return { provider: null };
127
- }
128
- }
129
- /**
130
- * Check if LLM synthesis is available
131
- */
132
- export function canSynthesize(config) {
133
- if (!config.provider)
134
- return false;
135
- switch (config.provider) {
136
- case "anthropic":
137
- return !!config.anthropic_api_key;
138
- case "openai":
139
- return !!config.openai_api_key;
140
- case "ollama":
141
- return true; // Ollama doesn't need a key
142
- default:
143
- return false;
144
- }
145
- }
146
- /**
147
- * Get the appropriate model for task complexity
148
- */
149
- function getModelForComplexity(config, complexity) {
150
- if (config.provider !== "ollama") {
151
- // Cloud providers use the configured model
152
- return config.model || (config.provider === "anthropic" ? "claude-sonnet-4-6" : "gpt-4o");
153
- }
154
- // Ollama: use tiered models
155
- switch (complexity) {
156
- case "light":
157
- return config.model_light || config.model || DEFAULT_OLLAMA_MODELS.light;
158
- case "medium":
159
- return config.model || DEFAULT_OLLAMA_MODELS.medium;
160
- case "heavy":
161
- return config.model_heavy || config.model || DEFAULT_OLLAMA_MODELS.heavy;
162
- }
163
- }
164
- // =============================================================================
165
- // Synthesis
166
- // =============================================================================
167
- /**
168
- * Synthesize insights using LLM
169
- *
170
- * Accepts an optional AbortSignal to allow timeout/cancellation of the
171
- * underlying fetch calls.
172
- */
173
- export async function synthesize(config, request, signal) {
174
- if (!canSynthesize(config)) {
175
- throw new Error("LLM not configured - cannot synthesize");
176
- }
177
- const startTime = Date.now();
178
- const complexity = request.complexity || "medium";
179
- let result;
180
- switch (config.provider) {
181
- case "anthropic":
182
- result = await synthesizeAnthropic(config, request, signal);
183
- break;
184
- case "openai":
185
- result = await synthesizeOpenAI(config, request, signal);
186
- break;
187
- case "ollama":
188
- result = await synthesizeOllama(config, request, complexity, signal);
189
- break;
190
- default:
191
- throw new Error(`Unknown LLM provider: ${config.provider}`);
192
- }
193
- result.latency_ms = Date.now() - startTime;
194
- return result;
195
- }
196
- /**
197
- * Synthesize with a timeout wrapper.
198
- *
199
- * Wraps the LLM API call with an AbortController + configurable timeout.
200
- * On timeout, throws a descriptive error so callers can fall back to
201
- * data-only results.
202
- *
203
- * @param config - LLM provider configuration
204
- * @param request - Synthesis request (system prompt, user prompt, etc.)
205
- * @param timeoutMs - Timeout in milliseconds (default: 30000)
206
- */
207
- export async function synthesizeWithTimeout(config, request, timeoutMs = 30000) {
208
- const controller = new AbortController();
209
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
210
- try {
211
- return await synthesize(config, request, controller.signal);
212
- }
213
- catch (error) {
214
- if (error instanceof Error && error.name === "AbortError") {
215
- throw new Error(`LLM timeout after ${timeoutMs / 1000}s`);
216
- }
217
- throw error;
218
- }
219
- finally {
220
- clearTimeout(timeout);
221
- }
222
- }
223
- /**
224
- * Anthropic Claude synthesis
225
- */
226
- async function synthesizeAnthropic(config, request, signal) {
227
- const model = config.model || "claude-sonnet-4-6";
228
- const maxTokens = request.max_tokens || 1024;
229
- const response = await fetch("https://api.anthropic.com/v1/messages", {
230
- method: "POST",
231
- headers: {
232
- "Content-Type": "application/json",
233
- "x-api-key": config.anthropic_api_key,
234
- "anthropic-version": "2023-06-01",
235
- },
236
- body: JSON.stringify({
237
- model,
238
- max_tokens: maxTokens,
239
- system: request.system,
240
- messages: [{ role: "user", content: request.prompt }],
241
- }),
242
- signal,
243
- });
244
- if (!response.ok) {
245
- const error = await response.text();
246
- throw new Error(`Anthropic API error: ${error}`);
247
- }
248
- const data = (await response.json());
249
- return {
250
- synthesis: data.content[0]?.text || "",
251
- model_used: model,
252
- tokens_used: data.usage?.output_tokens,
253
- };
254
- }
255
- /**
256
- * OpenAI synthesis
257
- */
258
- async function synthesizeOpenAI(config, request, signal) {
259
- const model = config.model || "gpt-4o";
260
- const maxTokens = request.max_tokens || 1024;
261
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
262
- method: "POST",
263
- headers: {
264
- "Content-Type": "application/json",
265
- Authorization: `Bearer ${config.openai_api_key}`,
266
- },
267
- body: JSON.stringify({
268
- model,
269
- max_tokens: maxTokens,
270
- messages: [
271
- { role: "system", content: request.system },
272
- { role: "user", content: request.prompt },
273
- ],
274
- }),
275
- signal,
276
- });
277
- if (!response.ok) {
278
- const error = await response.text();
279
- throw new Error(`OpenAI API error: ${error}`);
280
- }
281
- const data = (await response.json());
282
- return {
283
- synthesis: data.choices[0]?.message?.content || "",
284
- model_used: model,
285
- tokens_used: data.usage?.completion_tokens,
286
- };
287
- }
288
- /**
289
- * Ollama synthesis (local) - optimized for local models
290
- */
291
- async function synthesizeOllama(config, request, complexity, signal) {
292
- const model = getModelForComplexity(config, complexity);
293
- const baseUrl = config.ollama_url || "http://localhost:11434";
294
- // Use chat endpoint for better instruction following
295
- const response = await fetch(`${baseUrl}/api/chat`, {
296
- method: "POST",
297
- headers: {
298
- "Content-Type": "application/json",
299
- },
300
- body: JSON.stringify({
301
- model,
302
- messages: [
303
- { role: "system", content: request.system },
304
- { role: "user", content: request.prompt },
305
- ],
306
- stream: false,
307
- options: {
308
- num_predict: request.max_tokens || 512,
309
- temperature: 0.7,
310
- top_p: 0.9,
311
- },
312
- }),
313
- signal,
314
- });
315
- if (!response.ok) {
316
- const error = await response.text();
317
- throw new Error(`Ollama API error: ${error}`);
318
- }
319
- const data = (await response.json());
320
- return {
321
- synthesis: data.message?.content || "",
322
- model_used: model,
323
- tokens_used: data.eval_count,
324
- };
325
- }
326
- // =============================================================================
327
- // Synthesis Prompts - Optimized for Local Models
328
- // =============================================================================
329
- /**
330
- * Prompts are optimized for:
331
- * 1. Clear structure (local models need explicit formatting)
332
- * 2. Concise output (fewer tokens = faster inference)
333
- * 3. Specific instructions (reduce ambiguity)
334
- */
335
- export const SYNTHESIS_PROMPTS = {
336
- /**
337
- * Daily Briefing - Executive synthesis
338
- * Complexity: MEDIUM
339
- */
340
- dailyBriefing: {
341
- complexity: "medium",
342
- system: `You are an intelligence analyst. Synthesize data into actionable briefings.
343
-
344
- OUTPUT FORMAT:
345
- 1. One-sentence summary of the day
346
- 2. The single most important thing to review (with reason)
347
- 3. One recommended action
348
-
349
- Be direct. No filler. Every word must add value.`,
350
- buildPrompt: (data, context) => `
351
- DATA:
352
- ${JSON.stringify(data, null, 2)}
353
-
354
- USER CONTEXT:
355
- ${JSON.stringify(context, null, 2)}
356
-
357
- Generate briefing with:
358
- 1. ONE-LINE SUMMARY
359
- 2. TOP PRIORITY (title + why it matters to this user)
360
- 3. RECOMMENDED ACTION`,
361
- },
362
- /**
363
- * Score Autopsy - Explain the reasoning
364
- * Complexity: HEAVY (needs to reason about correctness)
365
- */
366
- scoreAutopsy: {
367
- complexity: "heavy",
368
- system: `You are a scoring forensics expert. Analyze if relevance scores are correct.
369
-
370
- OUTPUT FORMAT:
371
- 1. VERDICT: [ACCURATE / TOO HIGH / TOO LOW]
372
- 2. REASONING: Why you reached this verdict (cite specific data)
373
- 3. KEY FACTOR: The main thing driving this score
374
- 4. SUGGESTION: How to get more/fewer items like this
375
-
376
- Be analytical. Cite numbers from the data.`,
377
- buildPrompt: (data, context) => `
378
- SCORE DATA:
379
- ${JSON.stringify(data, null, 2)}
380
-
381
- USER INTERESTS:
382
- ${JSON.stringify(context, null, 2)}
383
-
384
- Analyze: Is this score correct given the user's interests?`,
385
- },
386
- /**
387
- * Trend Analysis - Predict and explain
388
- * Complexity: HEAVY (needs pattern recognition + prediction)
389
- */
390
- trendAnalysis: {
391
- complexity: "heavy",
392
- system: `You are a tech trend analyst. Interpret patterns and predict trajectories.
393
-
394
- OUTPUT FORMAT:
395
- 1. KEY SIGNAL: The most important trend (one sentence)
396
- 2. WHY IT'S HAPPENING: Brief explanation
397
- 3. PREDICTION: What to expect in the next 1-2 weeks
398
- 4. ACTION: Should the user adjust their focus?
399
-
400
- Ground predictions in the data. Don't speculate wildly.`,
401
- buildPrompt: (data, context) => `
402
- TREND DATA:
403
- ${JSON.stringify(data, null, 2)}
404
-
405
- USER CONTEXT:
406
- ${JSON.stringify(context, null, 2)}
407
-
408
- Interpret: What do these trends mean for this specific user?`,
409
- },
410
- /**
411
- * Context Analysis - Personalized optimization advice
412
- * Complexity: MEDIUM (structured recommendations)
413
- */
414
- contextAnalysis: {
415
- complexity: "medium",
416
- system: `You are a personalization expert. Help users optimize their content filtering.
417
-
418
- OUTPUT FORMAT:
419
- 1. BIGGEST GAP: What's missing from their context?
420
- 2. HIGHEST IMPACT CHANGE: One specific thing to add/remove
421
- 3. QUICK WIN: Something they can do in 30 seconds
422
-
423
- Be specific. Give exact examples they can copy-paste.`,
424
- buildPrompt: (data) => `
425
- CONTEXT DATA:
426
- ${JSON.stringify(data, null, 2)}
427
-
428
- Provide optimization recommendations. Be specific and actionable.`,
429
- },
430
- /**
431
- * Topic Connections - Explain the knowledge graph
432
- * Complexity: HEAVY (needs graph reasoning)
433
- */
434
- topicConnections: {
435
- complexity: "heavy",
436
- system: `You are a knowledge graph analyst. Find hidden patterns in topic relationships.
437
-
438
- OUTPUT FORMAT:
439
- 1. SURPRISING CONNECTION: A non-obvious relationship and its significance
440
- 2. MISSING LINK: Topics that should connect but don't
441
- 3. EXPLORATION PATH: A topic worth investigating based on the graph
442
- 4. PATTERN: What the graph structure reveals about their interests
443
-
444
- Look for insights the user wouldn't notice themselves.`,
445
- buildPrompt: (data, context) => `
446
- GRAPH DATA:
447
- ${JSON.stringify(data, null, 2)}
448
-
449
- USER CONTEXT:
450
- ${JSON.stringify(context, null, 2)}
451
-
452
- Interpret: What hidden patterns does this graph reveal?`,
453
- },
454
- };
455
- // =============================================================================
456
- // Utility: Check Ollama availability
457
- // =============================================================================
458
- /**
459
- * Check if Ollama is running and has required models
460
- */
461
- export async function checkOllamaStatus(baseUrl = "http://localhost:11434") {
462
- try {
463
- const response = await fetch(`${baseUrl}/api/tags`, {
464
- method: "GET",
465
- signal: AbortSignal.timeout(5000),
466
- });
467
- if (!response.ok) {
468
- return { available: false, models: [], error: "Ollama not responding" };
469
- }
470
- const data = (await response.json());
471
- const models = data.models?.map(m => m.name) || [];
472
- return { available: true, models };
473
- }
474
- catch (error) {
475
- return {
476
- available: false,
477
- models: [],
478
- error: error instanceof Error ? error.message : "Connection failed",
479
- };
480
- }
481
- }
482
- /**
483
- * Recommended models for 4DA synthesis tasks
484
- */
485
- export const RECOMMENDED_MODELS = {
486
- "16GB_VRAM": {
487
- primary: "qwen3:14b",
488
- fallback: "gemma3:12b",
489
- install: "ollama pull qwen3:14b",
490
- },
491
- "12GB_VRAM": {
492
- primary: "gemma3:12b",
493
- fallback: "qwen3:8b",
494
- install: "ollama pull gemma3:12b",
495
- },
496
- "8GB_VRAM": {
497
- primary: "qwen3:8b",
498
- fallback: "gemma3:4b",
499
- install: "ollama pull qwen3:8b",
500
- },
501
- };
502
- //# sourceMappingURL=llm.js.map
@@ -1,156 +0,0 @@
1
- /**
2
- * Output Manager for Dynamic Context Discovery
3
- *
4
- * Reduces token usage by ~80% through:
5
- * - Writing full results to files (referenced by path)
6
- * - Returning compact summaries inline
7
- * - Providing retrieval hints for common queries
8
- *
9
- * Full results stored at: ~/.local/share/4da/context/results/
10
- */
11
- /**
12
- * Compact result format - what gets returned inline to Claude
13
- */
14
- export interface CompactResult<T> {
15
- /** 2-3 sentence human-readable summary */
16
- summary: string;
17
- /** Essential 20% of data inline for immediate use */
18
- key_data: T;
19
- /** Path to complete JSON file */
20
- full_result_path: string;
21
- /** Hints for retrieving specific data (jq patterns, grep patterns) */
22
- retrieval_hints: string[];
23
- }
24
- /**
25
- * Get the results directory path (for documentation)
26
- */
27
- export declare function getResultsDir(): string;
28
- /**
29
- * Daily Briefing compact output
30
- */
31
- export interface DailyBriefingKeyData {
32
- total_items: number;
33
- high_priority: number;
34
- top_theme: string;
35
- tldr: string;
36
- executive_brief?: string;
37
- }
38
- export declare function createDailyBriefingCompact(result: {
39
- summary: {
40
- total_items_analyzed: number;
41
- high_relevance_count: number;
42
- tldr: string;
43
- };
44
- themes: {
45
- name: string;
46
- item_count: number;
47
- }[];
48
- ai_synthesis?: {
49
- executive_brief: string;
50
- };
51
- }): CompactResult<DailyBriefingKeyData>;
52
- /**
53
- * Trend Analysis compact output
54
- */
55
- export interface TrendAnalysisKeyData {
56
- total_items: number;
57
- volume_trend: string;
58
- top_rising: string | null;
59
- top_falling: string | null;
60
- anomaly_count: number;
61
- narrative?: string;
62
- }
63
- export declare function createTrendAnalysisCompact(result: {
64
- summary: {
65
- total_items: number;
66
- volume_trend: string;
67
- top_rising_topic: string | null;
68
- top_falling_topic: string | null;
69
- };
70
- anomalies: unknown[];
71
- ai_interpretation?: {
72
- narrative: string;
73
- };
74
- }): CompactResult<TrendAnalysisKeyData>;
75
- /**
76
- * Score Autopsy compact output
77
- */
78
- export interface ScoreAutopsyKeyData {
79
- item_title: string;
80
- final_score: number;
81
- score_assessment?: "accurate" | "too_high" | "too_low" | "uncertain";
82
- top_contributor: string;
83
- verdict?: string;
84
- }
85
- export declare function createScoreAutopsyCompact(result: {
86
- item: {
87
- title: string;
88
- };
89
- final_score: number;
90
- components: {
91
- name: string;
92
- contribution: number;
93
- }[];
94
- ai_analysis?: {
95
- score_assessment: "accurate" | "too_high" | "too_low" | "uncertain";
96
- verdict: string;
97
- };
98
- }): CompactResult<ScoreAutopsyKeyData>;
99
- /**
100
- * Context Analysis compact output
101
- */
102
- export interface ContextAnalysisKeyData {
103
- quality_score: number;
104
- interests_count: number;
105
- tech_count: number;
106
- gap_count: number;
107
- top_recommendation: string;
108
- personalized_advice?: string;
109
- }
110
- export declare function createContextAnalysisCompact(result: {
111
- context_summary: {
112
- interests_count: number;
113
- tech_stack_count: number;
114
- };
115
- quality_score: {
116
- overall: number;
117
- };
118
- coverage_gaps: unknown[];
119
- recommendations: {
120
- action: string;
121
- }[];
122
- ai_advice?: {
123
- personalized_recommendations: string;
124
- };
125
- }): CompactResult<ContextAnalysisKeyData>;
126
- /**
127
- * Topic Connections compact output
128
- */
129
- export interface TopicConnectionsKeyData {
130
- items_analyzed: number;
131
- node_count: number;
132
- edge_count: number;
133
- central_topics: string[];
134
- top_connection: {
135
- from: string;
136
- to: string;
137
- strength: number;
138
- } | null;
139
- graph_narrative?: string;
140
- }
141
- export declare function createTopicConnectionsCompact(result: {
142
- analysis_period: {
143
- items_analyzed: number;
144
- };
145
- nodes: unknown[];
146
- edges: {
147
- from: string;
148
- to: string;
149
- co_occurrences: number;
150
- }[];
151
- central_topics: string[];
152
- ai_insights?: {
153
- graph_narrative: string;
154
- };
155
- }): CompactResult<TopicConnectionsKeyData>;
156
- //# sourceMappingURL=output-manager.d.ts.map
@@ -1,157 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- /**
3
- * Output Manager for Dynamic Context Discovery
4
- *
5
- * Reduces token usage by ~80% through:
6
- * - Writing full results to files (referenced by path)
7
- * - Returning compact summaries inline
8
- * - Providing retrieval hints for common queries
9
- *
10
- * Full results stored at: ~/.local/share/4da/context/results/
11
- */
12
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
- import { homedir } from "node:os";
14
- import { join } from "node:path";
15
- // Base directory for 4DA context files
16
- const CONTEXT_DIR = join(homedir(), ".local", "share", "4da", "context");
17
- const RESULTS_DIR = join(CONTEXT_DIR, "results");
18
- /**
19
- * Ensure the context directories exist
20
- */
21
- function ensureDirectories() {
22
- if (!existsSync(CONTEXT_DIR)) {
23
- mkdirSync(CONTEXT_DIR, { recursive: true });
24
- }
25
- if (!existsSync(RESULTS_DIR)) {
26
- mkdirSync(RESULTS_DIR, { recursive: true });
27
- }
28
- }
29
- /**
30
- * Write full result to file and return the file path
31
- */
32
- function writeResultFile(toolName, fullResult) {
33
- ensureDirectories();
34
- // Generate timestamped filename
35
- const timestamp = Math.floor(Date.now() / 1000);
36
- const filename = `${toolName}_${timestamp}.json`;
37
- const filePath = join(RESULTS_DIR, filename);
38
- // Write full result to file
39
- writeFileSync(filePath, JSON.stringify(fullResult, null, 2), "utf-8");
40
- return filePath;
41
- }
42
- /**
43
- * Get the results directory path (for documentation)
44
- */
45
- export function getResultsDir() {
46
- return RESULTS_DIR;
47
- }
48
- export function createDailyBriefingCompact(result) {
49
- const topTheme = result.themes[0]?.name || "general";
50
- return {
51
- summary: `${result.summary.total_items_analyzed} items analyzed, ${result.summary.high_relevance_count} high-priority. Top theme: ${topTheme}. ${result.ai_synthesis ? "AI synthesis included." : ""}`,
52
- key_data: {
53
- total_items: result.summary.total_items_analyzed,
54
- high_priority: result.summary.high_relevance_count,
55
- top_theme: result.themes[0]?.name || "none",
56
- tldr: result.summary.tldr,
57
- executive_brief: result.ai_synthesis?.executive_brief,
58
- },
59
- full_result_path: writeResultFile("daily_briefing", result),
60
- retrieval_hints: [
61
- "jq '.notable_items' - get high-priority items",
62
- "jq '.themes' - get topic breakdown",
63
- "jq '.recommendations' - get action items",
64
- "jq '.ai_synthesis.executive_brief' - get AI summary",
65
- ],
66
- };
67
- }
68
- export function createTrendAnalysisCompact(result) {
69
- const anomalyNote = result.anomalies.length > 0 ? ` ${result.anomalies.length} anomalies detected.` : "";
70
- return {
71
- summary: `Volume ${result.summary.volume_trend}. Rising: ${result.summary.top_rising_topic || "none"}. Falling: ${result.summary.top_falling_topic || "none"}.${anomalyNote}`,
72
- key_data: {
73
- total_items: result.summary.total_items,
74
- volume_trend: result.summary.volume_trend,
75
- top_rising: result.summary.top_rising_topic,
76
- top_falling: result.summary.top_falling_topic,
77
- anomaly_count: result.anomalies.length,
78
- narrative: result.ai_interpretation?.narrative,
79
- },
80
- full_result_path: writeResultFile("trend_analysis", result),
81
- retrieval_hints: [
82
- "jq '.topic_trends' - get all topic trends",
83
- "jq '.volume_by_day' - get daily volume data",
84
- "jq '.anomalies' - get detected anomalies",
85
- "jq '.ai_interpretation.forecast' - get predictions",
86
- ],
87
- };
88
- }
89
- export function createScoreAutopsyCompact(result) {
90
- const topComponent = result.components.sort((a, b) => b.contribution - a.contribution)[0];
91
- const assessment = result.ai_analysis?.score_assessment || "unanalyzed";
92
- return {
93
- summary: `"${result.item.title.substring(0, 40)}..." scored ${Math.round(result.final_score * 100)}%. Assessment: ${assessment}. Top contributor: ${topComponent?.name || "unknown"}.`,
94
- key_data: {
95
- item_title: result.item.title,
96
- final_score: result.final_score,
97
- score_assessment: result.ai_analysis?.score_assessment,
98
- top_contributor: topComponent?.name || "unknown",
99
- verdict: result.ai_analysis?.verdict,
100
- },
101
- full_result_path: writeResultFile("score_autopsy", result),
102
- retrieval_hints: [
103
- "jq '.components' - get score breakdown",
104
- "jq '.matching_context' - get what matched",
105
- "jq '.similar_items' - get comparison items",
106
- "jq '.recommendations' - get improvement suggestions",
107
- ],
108
- };
109
- }
110
- export function createContextAnalysisCompact(result) {
111
- const quality = Math.round(result.quality_score.overall * 100);
112
- return {
113
- summary: `Context quality: ${quality}%. ${result.context_summary.interests_count} interests, ${result.context_summary.tech_stack_count} tech. ${result.coverage_gaps.length} gaps found.`,
114
- key_data: {
115
- quality_score: result.quality_score.overall,
116
- interests_count: result.context_summary.interests_count,
117
- tech_count: result.context_summary.tech_stack_count,
118
- gap_count: result.coverage_gaps.length,
119
- top_recommendation: result.recommendations[0]?.action || "No recommendations",
120
- personalized_advice: result.ai_advice?.personalized_recommendations,
121
- },
122
- full_result_path: writeResultFile("context_analysis", result),
123
- retrieval_hints: [
124
- "jq '.quality_score.dimensions' - get quality breakdown",
125
- "jq '.coverage_gaps' - get what's missing",
126
- "jq '.recommendations' - get all suggestions",
127
- "jq '.top_affinities' - get learned preferences",
128
- ],
129
- };
130
- }
131
- export function createTopicConnectionsCompact(result) {
132
- const topEdge = result.edges[0];
133
- const connectionNote = topEdge
134
- ? `Strongest: ${topEdge.from} ↔ ${topEdge.to}.`
135
- : "No strong connections.";
136
- return {
137
- summary: `${result.nodes.length} topics, ${result.edges.length} connections. Central: ${result.central_topics.slice(0, 3).join(", ")}. ${connectionNote}`,
138
- key_data: {
139
- items_analyzed: result.analysis_period.items_analyzed,
140
- node_count: result.nodes.length,
141
- edge_count: result.edges.length,
142
- central_topics: result.central_topics.slice(0, 5),
143
- top_connection: topEdge
144
- ? { from: topEdge.from, to: topEdge.to, strength: topEdge.co_occurrences }
145
- : null,
146
- graph_narrative: result.ai_insights?.graph_narrative,
147
- },
148
- full_result_path: writeResultFile("topic_connections", result),
149
- retrieval_hints: [
150
- "jq '.nodes' - get all topic nodes",
151
- "jq '.edges' - get all connections",
152
- "jq '.clusters' - get topic clusters",
153
- "jq '.path' - get path between topics (if requested)",
154
- ],
155
- };
156
- }
157
- //# sourceMappingURL=output-manager.js.map
@@ -1,25 +0,0 @@
1
- /**
2
- * Scheduler core — the cadence engine (S1).
3
- *
4
- * A minimal, dependency-free job scheduler: register named jobs with an interval, tick them
5
- * manually (tests, one-shots) or let start()/stop() drive them on timers. The one hard rule:
6
- * the SAME job never overlaps itself — a tick that arrives while the job's fn is still
7
- * running resolves as a no-op instead of double-running the pull.
8
- */
9
- type JobFn = () => Promise<void>;
10
- export interface SchedulerJobView {
11
- name: string;
12
- intervalMs: number;
13
- lastRun: number | null;
14
- }
15
- export interface Scheduler {
16
- register(name: string, intervalMs: number, fn: JobFn): void;
17
- start(): void;
18
- stop(): void;
19
- tick(name: string): Promise<void>;
20
- jobs(): SchedulerJobView[];
21
- jobCount(): number;
22
- }
23
- export declare function createScheduler(): Scheduler;
24
- export {};
25
- //# sourceMappingURL=core.d.ts.map
@@ -1,61 +0,0 @@
1
- /**
2
- * Scheduler core — the cadence engine (S1).
3
- *
4
- * A minimal, dependency-free job scheduler: register named jobs with an interval, tick them
5
- * manually (tests, one-shots) or let start()/stop() drive them on timers. The one hard rule:
6
- * the SAME job never overlaps itself — a tick that arrives while the job's fn is still
7
- * running resolves as a no-op instead of double-running the pull.
8
- */
9
- export function createScheduler() {
10
- const jobs = new Map();
11
- async function tick(name) {
12
- const job = jobs.get(name);
13
- if (!job) {
14
- throw new Error(`scheduler: no job registered under "${name}"`);
15
- }
16
- if (job.running) {
17
- return; // the same job never overlaps — a concurrent tick is a no-op
18
- }
19
- job.running = true;
20
- try {
21
- await job.fn();
22
- job.lastRun = Date.now();
23
- }
24
- finally {
25
- job.running = false;
26
- }
27
- }
28
- return {
29
- register(name, intervalMs, fn) {
30
- jobs.set(name, { name, intervalMs, fn, lastRun: null, running: false, timer: null });
31
- },
32
- start() {
33
- for (const job of jobs.values()) {
34
- if (job.timer)
35
- continue;
36
- job.timer = setInterval(() => {
37
- void tick(job.name).catch(() => {
38
- // a failing scheduled run must never take the process down; the job's
39
- // freshness simply doesn't advance, which is the observable signal
40
- });
41
- }, job.intervalMs);
42
- }
43
- },
44
- stop() {
45
- for (const job of jobs.values()) {
46
- if (job.timer) {
47
- clearInterval(job.timer);
48
- job.timer = null;
49
- }
50
- }
51
- },
52
- tick,
53
- jobs() {
54
- return [...jobs.values()].map(({ name, intervalMs, lastRun }) => ({ name, intervalMs, lastRun }));
55
- },
56
- jobCount() {
57
- return jobs.size;
58
- },
59
- };
60
- }
61
- //# sourceMappingURL=core.js.map
@@ -1,17 +0,0 @@
1
- /**
2
- * Freshness watermarks (S2) — when was each named feed last refreshed, and is it stale?
3
- *
4
- * A tiny persistent map: mark a name fresh at a moment, ask how stale it is against a budget.
5
- * Persistence is an explicit save()/load() to a JSON file so the scheduler can survive a
6
- * restart without inventing freshness it does not have — a missing file simply means
7
- * "nothing is known", which reads as stale (the honest default).
8
- */
9
- export interface Freshness {
10
- markFresh(name: string, at?: number): void;
11
- lastFresh(name: string): number | null;
12
- isStale(name: string, maxAgeMs: number, now?: number): boolean;
13
- save(): void;
14
- load(): void;
15
- }
16
- export declare function createFreshness(filePath?: string): Freshness;
17
- //# sourceMappingURL=freshness.d.ts.map
@@ -1,41 +0,0 @@
1
- /**
2
- * Freshness watermarks (S2) — when was each named feed last refreshed, and is it stale?
3
- *
4
- * A tiny persistent map: mark a name fresh at a moment, ask how stale it is against a budget.
5
- * Persistence is an explicit save()/load() to a JSON file so the scheduler can survive a
6
- * restart without inventing freshness it does not have — a missing file simply means
7
- * "nothing is known", which reads as stale (the honest default).
8
- */
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
- import { dirname } from "node:path";
11
- export function createFreshness(filePath) {
12
- let marks = new Map();
13
- return {
14
- markFresh(name, at) {
15
- marks.set(name, at ?? Date.now());
16
- },
17
- lastFresh(name) {
18
- return marks.get(name) ?? null;
19
- },
20
- isStale(name, maxAgeMs, now) {
21
- const last = marks.get(name);
22
- if (last === undefined) {
23
- return true; // never refreshed is the stalest state there is
24
- }
25
- return (now ?? Date.now()) - last > maxAgeMs;
26
- },
27
- save() {
28
- if (!filePath)
29
- return;
30
- mkdirSync(dirname(filePath), { recursive: true });
31
- writeFileSync(filePath, JSON.stringify(Object.fromEntries(marks)), "utf8");
32
- },
33
- load() {
34
- if (!filePath || !existsSync(filePath))
35
- return;
36
- const raw = JSON.parse(readFileSync(filePath, "utf8"));
37
- marks = new Map(Object.entries(raw));
38
- },
39
- };
40
- }
41
- //# sourceMappingURL=freshness.js.map
@@ -1,21 +0,0 @@
1
- /**
2
- * Scheduler composition (S4) — wire core + freshness + refreshers into one switchable unit.
3
- *
4
- * The whole scheduler is OFF unless explicitly enabled (fail-safe: an MCP server must never
5
- * grow background network activity by surprise). Enabled, it registers every refresh unit on
6
- * the cadence engine, starts the timers, and exposes a status view the server (or a future
7
- * `scheduler_status` tool) can render: every unit, its cadence, and when it last ran.
8
- */
9
- import { type SchedulerJobView } from "./core.js";
10
- export interface InitSchedulerOpts {
11
- enabled: boolean;
12
- pull: Record<string, () => Promise<void>>;
13
- freshnessPath?: string;
14
- }
15
- export interface RunningScheduler {
16
- started: boolean;
17
- status(): SchedulerJobView[];
18
- stop(): void;
19
- }
20
- export declare function initScheduler(opts: InitSchedulerOpts): RunningScheduler;
21
- //# sourceMappingURL=index.d.ts.map
@@ -1,32 +0,0 @@
1
- /**
2
- * Scheduler composition (S4) — wire core + freshness + refreshers into one switchable unit.
3
- *
4
- * The whole scheduler is OFF unless explicitly enabled (fail-safe: an MCP server must never
5
- * grow background network activity by surprise). Enabled, it registers every refresh unit on
6
- * the cadence engine, starts the timers, and exposes a status view the server (or a future
7
- * `scheduler_status` tool) can render: every unit, its cadence, and when it last ran.
8
- */
9
- import { createScheduler } from "./core.js";
10
- import { createFreshness } from "./freshness.js";
11
- import { buildRefreshers } from "./refreshers.js";
12
- export function initScheduler(opts) {
13
- if (!opts.enabled) {
14
- return { started: false, status: () => [], stop: () => { } };
15
- }
16
- const freshness = createFreshness(opts.freshnessPath);
17
- freshness.load();
18
- const scheduler = createScheduler();
19
- for (const unit of buildRefreshers({ pull: opts.pull, freshness })) {
20
- scheduler.register(unit.name, unit.intervalMs, () => unit.run());
21
- }
22
- scheduler.start();
23
- return {
24
- started: true,
25
- status: () => scheduler.jobs(),
26
- stop: () => {
27
- scheduler.stop();
28
- freshness.save();
29
- },
30
- };
31
- }
32
- //# sourceMappingURL=index.js.map
@@ -1,21 +0,0 @@
1
- /**
2
- * Refresh adapters (S3) — wrap each named data pull into a schedulable refresh unit.
3
- *
4
- * Pure dependency injection: the pulls and the freshness sink are handed in, nothing is
5
- * imported from the fetcher layer and nothing touches the network here. A refresh unit runs
6
- * its pull and only THEN marks freshness — a failing pull rethrows and leaves the watermark
7
- * untouched, so staleness stays honest.
8
- */
9
- export interface RefreshDeps {
10
- pull: Record<string, () => Promise<void>>;
11
- freshness: {
12
- markFresh(name: string, at?: number): void;
13
- };
14
- }
15
- export interface RefreshUnit {
16
- name: string;
17
- intervalMs: number;
18
- run(): Promise<void>;
19
- }
20
- export declare function buildRefreshers(deps: RefreshDeps): RefreshUnit[];
21
- //# sourceMappingURL=refreshers.d.ts.map
@@ -1,21 +0,0 @@
1
- /**
2
- * Refresh adapters (S3) — wrap each named data pull into a schedulable refresh unit.
3
- *
4
- * Pure dependency injection: the pulls and the freshness sink are handed in, nothing is
5
- * imported from the fetcher layer and nothing touches the network here. A refresh unit runs
6
- * its pull and only THEN marks freshness — a failing pull rethrows and leaves the watermark
7
- * untouched, so staleness stays honest.
8
- */
9
- /** Default cadence per refresh unit; callers can re-register with their own interval. */
10
- const DEFAULT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes — fresh enough for feed data, kind to upstreams
11
- export function buildRefreshers(deps) {
12
- return Object.entries(deps.pull).map(([name, pull]) => ({
13
- name,
14
- intervalMs: DEFAULT_INTERVAL_MS,
15
- async run() {
16
- await pull(); // a throw propagates — freshness must NOT advance on a failed pull
17
- deps.freshness.markFresh(name);
18
- },
19
- }));
20
- }
21
- //# sourceMappingURL=refreshers.js.map