@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 +2 -2
- package/dist/index.js +0 -0
- package/dist/setup.js +0 -0
- package/package.json +11 -26
- package/dist/llm.d.ts +0 -162
- package/dist/llm.js +0 -502
- package/dist/output-manager.d.ts +0 -156
- package/dist/output-manager.js +0 -157
- package/dist/scheduler/core.d.ts +0 -25
- package/dist/scheduler/core.js +0 -61
- package/dist/scheduler/freshness.d.ts +0 -17
- package/dist/scheduler/freshness.js +0 -41
- package/dist/scheduler/index.d.ts +0 -21
- package/dist/scheduler/index.js +0 -32
- package/dist/scheduler/refreshers.d.ts +0 -21
- package/dist/scheduler/refreshers.js +0 -21
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
|
-
|
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
package/dist/output-manager.d.ts
DELETED
|
@@ -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
|
package/dist/output-manager.js
DELETED
|
@@ -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
|
package/dist/scheduler/core.d.ts
DELETED
|
@@ -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
|
package/dist/scheduler/core.js
DELETED
|
@@ -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
|
package/dist/scheduler/index.js
DELETED
|
@@ -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
|