@4da/mcp-server 5.0.0 → 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.
@@ -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