prism-mcp-server 5.2.0 → 5.5.0
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 +308 -218
- package/dist/backgroundScheduler.js +327 -0
- package/dist/config.js +29 -0
- package/dist/dashboard/server.js +246 -0
- package/dist/dashboard/ui.js +216 -6
- package/dist/hivemindWatchdog.js +206 -0
- package/dist/lifecycle.js +59 -4
- package/dist/scholar/freeSearch.js +78 -0
- package/dist/scholar/webScholar.js +258 -0
- package/dist/sdm/sdmDecoder.js +75 -0
- package/dist/sdm/sdmEngine.js +158 -0
- package/dist/server.js +173 -11
- package/dist/storage/sqlite.js +298 -47
- package/dist/storage/supabase.js +114 -1
- package/dist/tools/agentRegistryDefinitions.js +11 -4
- package/dist/tools/agentRegistryHandlers.js +23 -5
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +46 -1
- package/dist/tools/sessionMemoryHandlers.js +210 -38
- package/dist/utils/briefing.js +1 -1
- package/dist/utils/crdtMerge.js +152 -0
- package/dist/utils/healthCheck.js +15 -0
- package/dist/utils/llm/adapters/gemini.js +3 -3
- package/package.json +9 -2
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Purge Scheduler (v5.4) — Unified Maintenance Automation
|
|
3
|
+
*
|
|
4
|
+
* Automates all storage maintenance tasks that were previously manual-only:
|
|
5
|
+
* 1. TTL Sweep — expireByTTL() for all projects with configured TTL
|
|
6
|
+
* 2. Importance Decay — decayImportance() across all projects
|
|
7
|
+
* 3. Compaction — auto-compact projects exceeding entry threshold
|
|
8
|
+
* 4. Deep Purge — purge float32 embeddings for old compressed entries
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Single `setInterval` loop (default: 12 hours)
|
|
12
|
+
* - Independent from Hivemind Watchdog (60s loop) — different cadence, different concerns
|
|
13
|
+
* - Non-blocking: all errors caught and logged, sweep never crashes the server
|
|
14
|
+
* - Configurable via env vars: PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS
|
|
15
|
+
*
|
|
16
|
+
* Each sweep runs tasks sequentially (not parallel) to avoid overloading
|
|
17
|
+
* storage backends during maintenance windows.
|
|
18
|
+
*/
|
|
19
|
+
import { getStorage } from "./storage/index.js";
|
|
20
|
+
import { PRISM_USER_ID, PRISM_SCHOLAR_ENABLED, PRISM_SCHOLAR_INTERVAL_MS } from "./config.js";
|
|
21
|
+
import { debugLog } from "./utils/logger.js";
|
|
22
|
+
import { runWebScholar } from "./scholar/webScholar.js";
|
|
23
|
+
import { getAllActiveSdmProjects, getSdmEngine } from "./sdm/sdmEngine.js";
|
|
24
|
+
export const DEFAULT_SCHEDULER_CONFIG = {
|
|
25
|
+
intervalMs: 43_200_000, // 12 hours
|
|
26
|
+
enableTTLSweep: true,
|
|
27
|
+
enableDecay: true,
|
|
28
|
+
enableCompaction: true,
|
|
29
|
+
enableDeepPurge: true,
|
|
30
|
+
enableSdmFlush: true,
|
|
31
|
+
purgeOlderThanDays: 30,
|
|
32
|
+
compactionThreshold: 50,
|
|
33
|
+
compactionKeepRecent: 10,
|
|
34
|
+
decayDays: 30,
|
|
35
|
+
};
|
|
36
|
+
// ─── Scheduler State ─────────────────────────────────────────
|
|
37
|
+
let schedulerInterval = null;
|
|
38
|
+
/** Tracks the last completed sweep for dashboard status */
|
|
39
|
+
let lastSweepResult = null;
|
|
40
|
+
/** When the scheduler was started */
|
|
41
|
+
let schedulerStartedAt = null;
|
|
42
|
+
// ─── Public API ──────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Start the background scheduler.
|
|
45
|
+
* Returns a cleanup function that stops the interval.
|
|
46
|
+
*
|
|
47
|
+
* @param config - Override defaults for testing or production tuning
|
|
48
|
+
*/
|
|
49
|
+
export function startScheduler(config) {
|
|
50
|
+
const cfg = { ...DEFAULT_SCHEDULER_CONFIG, ...config };
|
|
51
|
+
if (schedulerInterval) {
|
|
52
|
+
clearTimeout(schedulerInterval);
|
|
53
|
+
}
|
|
54
|
+
schedulerStartedAt = new Date().toISOString();
|
|
55
|
+
const runLoop = () => {
|
|
56
|
+
runSchedulerSweep(cfg)
|
|
57
|
+
.catch(err => {
|
|
58
|
+
console.error(`[Scheduler] Sweep error (non-fatal): ${err}`);
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
schedulerInterval = setTimeout(runLoop, cfg.intervalMs);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
// Run an immediate first sweep (after a short delay to let storage fully warm up)
|
|
65
|
+
schedulerInterval = setTimeout(runLoop, 5_000);
|
|
66
|
+
const enabledTasks = [
|
|
67
|
+
cfg.enableTTLSweep && "TTL",
|
|
68
|
+
cfg.enableDecay && "Decay",
|
|
69
|
+
cfg.enableCompaction && "Compaction",
|
|
70
|
+
cfg.enableDeepPurge && "DeepPurge",
|
|
71
|
+
cfg.enableSdmFlush && "SdmFlush",
|
|
72
|
+
].filter(Boolean).join(", ");
|
|
73
|
+
console.error(`[Scheduler] ⏰ Started (interval=${formatDuration(cfg.intervalMs)}, tasks=[${enabledTasks}])`);
|
|
74
|
+
return () => {
|
|
75
|
+
if (schedulerInterval) {
|
|
76
|
+
clearTimeout(schedulerInterval);
|
|
77
|
+
schedulerInterval = null;
|
|
78
|
+
console.error("[Scheduler] Stopped");
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get scheduler status for the dashboard.
|
|
84
|
+
*/
|
|
85
|
+
export function getSchedulerStatus() {
|
|
86
|
+
return {
|
|
87
|
+
running: schedulerInterval !== null,
|
|
88
|
+
startedAt: schedulerStartedAt,
|
|
89
|
+
intervalMs: DEFAULT_SCHEDULER_CONFIG.intervalMs,
|
|
90
|
+
lastSweep: lastSweepResult,
|
|
91
|
+
scholarRunning: scholarInterval !== null,
|
|
92
|
+
scholarIntervalMs: PRISM_SCHOLAR_INTERVAL_MS,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ─── Scholar State ───────────────────────────────────────────
|
|
96
|
+
let scholarInterval = null;
|
|
97
|
+
export function startScholarScheduler() {
|
|
98
|
+
if (scholarInterval) {
|
|
99
|
+
clearTimeout(scholarInterval);
|
|
100
|
+
}
|
|
101
|
+
if (!PRISM_SCHOLAR_ENABLED || PRISM_SCHOLAR_INTERVAL_MS <= 0) {
|
|
102
|
+
debugLog("[WebScholar] 🕒 Scheduler disabled (PRISM_SCHOLAR_ENABLED=false or PRISM_SCHOLAR_INTERVAL_MS=0)");
|
|
103
|
+
return () => { };
|
|
104
|
+
}
|
|
105
|
+
const runLoop = () => {
|
|
106
|
+
runWebScholar()
|
|
107
|
+
.catch(err => {
|
|
108
|
+
console.error(`[WebScholar] Sweep error: ${err}`);
|
|
109
|
+
})
|
|
110
|
+
.finally(() => {
|
|
111
|
+
scholarInterval = setTimeout(runLoop, PRISM_SCHOLAR_INTERVAL_MS);
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
// Initial trigger after 30s to avoid thundering herd on boot
|
|
115
|
+
scholarInterval = setTimeout(runLoop, 30_000);
|
|
116
|
+
console.error(`[WebScholar] ⏰ Started (interval=${formatDuration(PRISM_SCHOLAR_INTERVAL_MS)})`);
|
|
117
|
+
return () => {
|
|
118
|
+
if (scholarInterval) {
|
|
119
|
+
clearTimeout(scholarInterval);
|
|
120
|
+
scholarInterval = null;
|
|
121
|
+
console.error("[WebScholar] Stopped");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ─── Core Sweep Logic ────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Single scheduler sweep — orchestrates all maintenance tasks.
|
|
128
|
+
* Exported for testing.
|
|
129
|
+
*
|
|
130
|
+
* Execution order:
|
|
131
|
+
* 1. TTL Sweep — lightweight SQL UPDATEs (fast)
|
|
132
|
+
* 2. Importance Decay — lightweight SQL UPDATEs (fast)
|
|
133
|
+
* 3. Compaction — LLM-powered summarization (slow, expensive)
|
|
134
|
+
* 4. Deep Purge — SQL UPDATEs to NULL embeddings (moderate)
|
|
135
|
+
*/
|
|
136
|
+
export async function runSchedulerSweep(cfg = DEFAULT_SCHEDULER_CONFIG) {
|
|
137
|
+
const sweepStart = Date.now();
|
|
138
|
+
const startedAt = new Date().toISOString();
|
|
139
|
+
const result = {
|
|
140
|
+
startedAt,
|
|
141
|
+
completedAt: "",
|
|
142
|
+
durationMs: 0,
|
|
143
|
+
tasks: {
|
|
144
|
+
ttlSweep: { ran: false, projectsSwept: 0, totalExpired: 0 },
|
|
145
|
+
importanceDecay: { ran: false, projectsDecayed: 0 },
|
|
146
|
+
compaction: { ran: false, projectsCompacted: 0 },
|
|
147
|
+
deepPurge: { ran: false, purged: 0, reclaimedBytes: 0 },
|
|
148
|
+
sdmFlush: { ran: false, projectsFlushed: 0 },
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
debugLog("[Scheduler] 🔄 Sweep starting...");
|
|
152
|
+
const storage = await getStorage();
|
|
153
|
+
// ── Task 1: TTL Sweep ──────────────────────────────────────
|
|
154
|
+
if (cfg.enableTTLSweep) {
|
|
155
|
+
try {
|
|
156
|
+
result.tasks.ttlSweep.ran = true;
|
|
157
|
+
const projects = await storage.listProjects();
|
|
158
|
+
const settings = await storage.getAllSettings();
|
|
159
|
+
for (const project of projects) {
|
|
160
|
+
const ttlKey = `ttl:${project}`;
|
|
161
|
+
const ttlValue = settings[ttlKey];
|
|
162
|
+
if (!ttlValue)
|
|
163
|
+
continue;
|
|
164
|
+
const ttlDays = parseInt(ttlValue, 10);
|
|
165
|
+
if (isNaN(ttlDays) || ttlDays <= 0)
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
const { expired } = await storage.expireByTTL(project, ttlDays, PRISM_USER_ID);
|
|
169
|
+
result.tasks.ttlSweep.projectsSwept++;
|
|
170
|
+
result.tasks.ttlSweep.totalExpired += expired;
|
|
171
|
+
if (expired > 0) {
|
|
172
|
+
debugLog(`[Scheduler] TTL: expired ${expired} entries for "${project}" (>${ttlDays}d)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
debugLog(`[Scheduler] TTL sweep failed for "${project}": ${err}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
result.tasks.ttlSweep.error = err instanceof Error ? err.message : String(err);
|
|
182
|
+
console.error(`[Scheduler] TTL sweep error: ${err}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ── Task 2: Importance Decay ───────────────────────────────
|
|
186
|
+
if (cfg.enableDecay) {
|
|
187
|
+
try {
|
|
188
|
+
result.tasks.importanceDecay.ran = true;
|
|
189
|
+
const projects = await storage.listProjects();
|
|
190
|
+
for (const project of projects) {
|
|
191
|
+
try {
|
|
192
|
+
await storage.decayImportance(project, PRISM_USER_ID, cfg.decayDays);
|
|
193
|
+
result.tasks.importanceDecay.projectsDecayed++;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
debugLog(`[Scheduler] Decay failed for "${project}": ${err}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
result.tasks.importanceDecay.error = err instanceof Error ? err.message : String(err);
|
|
202
|
+
console.error(`[Scheduler] Importance decay error: ${err}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// ── Task 3: Compaction ─────────────────────────────────────
|
|
206
|
+
// NOTE: Compaction uses LLM summarization which is expensive.
|
|
207
|
+
// We only trigger the candidate detection here — actual compaction
|
|
208
|
+
// is deferred to avoid blocking the sweep with long LLM calls.
|
|
209
|
+
// Instead, we log which projects need compaction for dashboard visibility.
|
|
210
|
+
if (cfg.enableCompaction) {
|
|
211
|
+
try {
|
|
212
|
+
result.tasks.compaction.ran = true;
|
|
213
|
+
const candidates = await storage.getCompactionCandidates(cfg.compactionThreshold, cfg.compactionKeepRecent, PRISM_USER_ID);
|
|
214
|
+
if (candidates.length > 0) {
|
|
215
|
+
// Import compaction handler dynamically to avoid circular deps
|
|
216
|
+
const { compactLedgerHandler } = await import("./tools/compactionHandler.js");
|
|
217
|
+
for (const candidate of candidates) {
|
|
218
|
+
try {
|
|
219
|
+
debugLog(`[Scheduler] Compacting "${candidate.project}": ` +
|
|
220
|
+
`${candidate.total_entries} entries (${candidate.to_compact} to compact)`);
|
|
221
|
+
await compactLedgerHandler({
|
|
222
|
+
project: candidate.project,
|
|
223
|
+
threshold: cfg.compactionThreshold,
|
|
224
|
+
keep_recent: cfg.compactionKeepRecent,
|
|
225
|
+
dry_run: false,
|
|
226
|
+
});
|
|
227
|
+
result.tasks.compaction.projectsCompacted++;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
debugLog(`[Scheduler] Compaction failed for "${candidate.project}": ${err}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
result.tasks.compaction.error = err instanceof Error ? err.message : String(err);
|
|
237
|
+
console.error(`[Scheduler] Compaction error: ${err}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// ── Task 4: Deep Purge ─────────────────────────────────────
|
|
241
|
+
if (cfg.enableDeepPurge) {
|
|
242
|
+
try {
|
|
243
|
+
result.tasks.deepPurge.ran = true;
|
|
244
|
+
const purgeResult = await storage.purgeHighPrecisionEmbeddings({
|
|
245
|
+
olderThanDays: cfg.purgeOlderThanDays,
|
|
246
|
+
dryRun: false,
|
|
247
|
+
userId: PRISM_USER_ID,
|
|
248
|
+
});
|
|
249
|
+
result.tasks.deepPurge.purged = purgeResult.purged;
|
|
250
|
+
result.tasks.deepPurge.reclaimedBytes = purgeResult.reclaimedBytes;
|
|
251
|
+
if (purgeResult.purged > 0) {
|
|
252
|
+
debugLog(`[Scheduler] Deep purge: freed ${formatBytes(purgeResult.reclaimedBytes)} ` +
|
|
253
|
+
`(${purgeResult.purged} entries purged)`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
result.tasks.deepPurge.error = err instanceof Error ? err.message : String(err);
|
|
258
|
+
console.error(`[Scheduler] Deep purge error: ${err}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ── Task 5: SDM Flush ──────────────────────────────────────
|
|
262
|
+
if (cfg.enableSdmFlush) {
|
|
263
|
+
try {
|
|
264
|
+
result.tasks.sdmFlush.ran = true;
|
|
265
|
+
const activeProjects = getAllActiveSdmProjects();
|
|
266
|
+
for (const project of activeProjects) {
|
|
267
|
+
try {
|
|
268
|
+
const sdm = getSdmEngine(project);
|
|
269
|
+
const state = sdm.exportState();
|
|
270
|
+
await storage.saveSdmState(project, state);
|
|
271
|
+
result.tasks.sdmFlush.projectsFlushed++;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
debugLog(`[Scheduler] SDM flush failed for "${project}": ${err}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (result.tasks.sdmFlush.projectsFlushed > 0) {
|
|
278
|
+
debugLog(`[Scheduler] SDM flush: saved matrices for ${result.tasks.sdmFlush.projectsFlushed} projects`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
result.tasks.sdmFlush.error = err instanceof Error ? err.message : String(err);
|
|
283
|
+
console.error(`[Scheduler] SDM flush error: ${err}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ── Finalize ───────────────────────────────────────────────
|
|
287
|
+
result.completedAt = new Date().toISOString();
|
|
288
|
+
result.durationMs = Date.now() - sweepStart;
|
|
289
|
+
lastSweepResult = result;
|
|
290
|
+
// Build summary line
|
|
291
|
+
const parts = [];
|
|
292
|
+
if (result.tasks.ttlSweep.ran && result.tasks.ttlSweep.totalExpired > 0) {
|
|
293
|
+
parts.push(`TTL:${result.tasks.ttlSweep.totalExpired} expired`);
|
|
294
|
+
}
|
|
295
|
+
if (result.tasks.importanceDecay.ran) {
|
|
296
|
+
parts.push(`Decay:${result.tasks.importanceDecay.projectsDecayed} projects`);
|
|
297
|
+
}
|
|
298
|
+
if (result.tasks.compaction.ran && result.tasks.compaction.projectsCompacted > 0) {
|
|
299
|
+
parts.push(`Compact:${result.tasks.compaction.projectsCompacted} projects`);
|
|
300
|
+
}
|
|
301
|
+
if (result.tasks.deepPurge.ran && result.tasks.deepPurge.purged > 0) {
|
|
302
|
+
parts.push(`Purge:${result.tasks.deepPurge.purged} entries (${formatBytes(result.tasks.deepPurge.reclaimedBytes)})`);
|
|
303
|
+
}
|
|
304
|
+
if (result.tasks.sdmFlush.ran && result.tasks.sdmFlush.projectsFlushed > 0) {
|
|
305
|
+
parts.push(`SDM:${result.tasks.sdmFlush.projectsFlushed} projects`);
|
|
306
|
+
}
|
|
307
|
+
const summaryLine = parts.length > 0
|
|
308
|
+
? parts.join(" | ")
|
|
309
|
+
: "no maintenance actions needed";
|
|
310
|
+
debugLog(`[Scheduler] ✅ Sweep completed in ${result.durationMs}ms — ${summaryLine}`);
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
314
|
+
function formatDuration(ms) {
|
|
315
|
+
if (ms < 60_000)
|
|
316
|
+
return `${ms}ms`;
|
|
317
|
+
if (ms < 3_600_000)
|
|
318
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
319
|
+
return `${Math.round(ms / 3_600_000)}h`;
|
|
320
|
+
}
|
|
321
|
+
function formatBytes(bytes) {
|
|
322
|
+
if (bytes < 1024)
|
|
323
|
+
return `${bytes}B`;
|
|
324
|
+
if (bytes < 1_048_576)
|
|
325
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
326
|
+
return `${(bytes / 1_048_576).toFixed(1)}MB`;
|
|
327
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -128,3 +128,32 @@ if (PRISM_AUTO_CAPTURE) {
|
|
|
128
128
|
console.error(`[AutoCapture] Enabled for ports: ${PRISM_CAPTURE_PORTS.join(", ")}`);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
// ─── v5.3: Hivemind Watchdog Thresholds ──────────────────────
|
|
132
|
+
// All values have sane defaults. Override via env vars only for
|
|
133
|
+
// testing or production tuning. Dashboard UI exposure deferred to v5.4.
|
|
134
|
+
export const WATCHDOG_INTERVAL_MS = parseInt(process.env.PRISM_WATCHDOG_INTERVAL_MS || "60000", 10);
|
|
135
|
+
export const WATCHDOG_STALE_MIN = parseInt(process.env.PRISM_WATCHDOG_STALE_MIN || "5", 10);
|
|
136
|
+
export const WATCHDOG_FROZEN_MIN = parseInt(process.env.PRISM_WATCHDOG_FROZEN_MIN || "15", 10);
|
|
137
|
+
export const WATCHDOG_OFFLINE_MIN = parseInt(process.env.PRISM_WATCHDOG_OFFLINE_MIN || "30", 10);
|
|
138
|
+
export const WATCHDOG_LOOP_THRESHOLD = parseInt(process.env.PRISM_WATCHDOG_LOOP_THRESHOLD || "5", 10);
|
|
139
|
+
// ─── v5.4: Background Purge Scheduler ────────────────────────
|
|
140
|
+
// Automated background maintenance: TTL sweep, importance decay,
|
|
141
|
+
// compaction, and deep storage purge. Runs independently from
|
|
142
|
+
// the Watchdog (different cadence: 12h vs 60s).
|
|
143
|
+
export const PRISM_SCHEDULER_ENABLED = process.env.PRISM_SCHEDULER_ENABLED !== "false"; // Default: true
|
|
144
|
+
export const PRISM_SCHEDULER_INTERVAL_MS = parseInt(process.env.PRISM_SCHEDULER_INTERVAL_MS || "43200000", 10 // 12 hours
|
|
145
|
+
);
|
|
146
|
+
// ─── v5.4: Autonomous Web Scholar ─────────────────────────────
|
|
147
|
+
// Background LLM research pipeline powered by Brave Search + Firecrawl.
|
|
148
|
+
// Defaults are conservative to prevent runaway API costs.
|
|
149
|
+
export const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;
|
|
150
|
+
export const PRISM_SCHOLAR_ENABLED = process.env.PRISM_SCHOLAR_ENABLED === "true"; // Opt-in
|
|
151
|
+
if (PRISM_SCHOLAR_ENABLED && !FIRECRAWL_API_KEY) {
|
|
152
|
+
console.error("Warning: FIRECRAWL_API_KEY environment variable is missing. Web Scholar will be unavailable.");
|
|
153
|
+
}
|
|
154
|
+
export const PRISM_SCHOLAR_INTERVAL_MS = parseInt(process.env.PRISM_SCHOLAR_INTERVAL_MS || "0", 10 // Default manual-only
|
|
155
|
+
);
|
|
156
|
+
export const PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN = parseInt(process.env.PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN || "3", 10);
|
|
157
|
+
export const PRISM_SCHOLAR_TOPICS = (process.env.PRISM_SCHOLAR_TOPICS || "ai,agents")
|
|
158
|
+
.split(",")
|
|
159
|
+
.map(t => t.trim());
|
package/dist/dashboard/server.js
CHANGED
|
@@ -864,6 +864,249 @@ return false;}
|
|
|
864
864
|
return res.end(JSON.stringify({ error: err.message || "Import failed" }));
|
|
865
865
|
}
|
|
866
866
|
}
|
|
867
|
+
// ─── API: Background Scheduler Status (v5.4) ────────────
|
|
868
|
+
if (url.pathname === "/api/scheduler" && req.method === "GET") {
|
|
869
|
+
try {
|
|
870
|
+
const { getSchedulerStatus } = await import("../backgroundScheduler.js");
|
|
871
|
+
const status = getSchedulerStatus();
|
|
872
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
873
|
+
return res.end(JSON.stringify(status));
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
console.error("[Dashboard] Scheduler status error:", err);
|
|
877
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
878
|
+
return res.end(JSON.stringify({
|
|
879
|
+
running: false, startedAt: null, intervalMs: 0, lastSweep: null,
|
|
880
|
+
}));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// ─── API: Autonomous Web Scholar Trigger (v5.4) ─────────
|
|
884
|
+
if (url.pathname === "/api/scholar/trigger" && req.method === "POST") {
|
|
885
|
+
try {
|
|
886
|
+
const { runWebScholar } = await import("../scholar/webScholar.js");
|
|
887
|
+
// Fire and forget, don't block the request
|
|
888
|
+
runWebScholar().catch(err => {
|
|
889
|
+
console.error("[Dashboard] Web Scholar async trigger failed:", err);
|
|
890
|
+
});
|
|
891
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
892
|
+
return res.end(JSON.stringify({ ok: true, message: "Autonomous research started in background" }));
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
console.error("[Dashboard] Web Scholar trigger error:", err);
|
|
896
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
897
|
+
return res.end(JSON.stringify({ error: err.message || "Failed to trigger Web Scholar" }));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (url.pathname === "/manifest.json" && req.method === "GET") {
|
|
901
|
+
const manifest = {
|
|
902
|
+
name: "Prism Mind Palace",
|
|
903
|
+
short_name: "Prism",
|
|
904
|
+
description: "Prism MCP Mobile Dashboard",
|
|
905
|
+
start_url: "/",
|
|
906
|
+
display: "standalone",
|
|
907
|
+
background_color: "#0a0e1a",
|
|
908
|
+
theme_color: "#0a0e1a",
|
|
909
|
+
icons: [
|
|
910
|
+
{ src: "/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any" },
|
|
911
|
+
{ src: "/icon-192-maskable.svg", sizes: "192x192", type: "image/svg+xml", purpose: "maskable" },
|
|
912
|
+
{ src: "/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any" },
|
|
913
|
+
{ src: "/icon-512-maskable.svg", sizes: "512x512", type: "image/svg+xml", purpose: "maskable" }
|
|
914
|
+
]
|
|
915
|
+
};
|
|
916
|
+
res.writeHead(200, {
|
|
917
|
+
"Content-Type": "application/json",
|
|
918
|
+
"Cache-Control": "public, max-age=86400"
|
|
919
|
+
});
|
|
920
|
+
return res.end(JSON.stringify(manifest));
|
|
921
|
+
}
|
|
922
|
+
// ─── PWA: Service Worker (v5.4) ───
|
|
923
|
+
if (url.pathname === "/sw.js" && req.method === "GET") {
|
|
924
|
+
const swContent = `
|
|
925
|
+
const CACHE_NAME = 'prism-pwa-v2.1';
|
|
926
|
+
const ASSETS = [
|
|
927
|
+
'/',
|
|
928
|
+
'/manifest.json',
|
|
929
|
+
'/icon-192.svg',
|
|
930
|
+
'/icon-192-maskable.svg',
|
|
931
|
+
'/icon-512.svg',
|
|
932
|
+
'/icon-512-maskable.svg',
|
|
933
|
+
'/apple-touch-icon.png',
|
|
934
|
+
'/offline.html'
|
|
935
|
+
];
|
|
936
|
+
|
|
937
|
+
self.addEventListener('install', (e) => {
|
|
938
|
+
e.waitUntil(caches.open(CACHE_NAME).then((c) => c.addAll(ASSETS)));
|
|
939
|
+
self.skipWaiting();
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
self.addEventListener('activate', (e) => {
|
|
943
|
+
e.waitUntil(caches.keys().then((keys) => {
|
|
944
|
+
return Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)));
|
|
945
|
+
}));
|
|
946
|
+
self.clients.claim();
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
self.addEventListener('fetch', (e) => {
|
|
950
|
+
if (e.request.method !== 'GET') return;
|
|
951
|
+
// Serve offline page for HTML navigation
|
|
952
|
+
if (e.request.mode === 'navigate') {
|
|
953
|
+
e.respondWith(fetch(e.request).catch(() => caches.match('/offline.html')));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
// Network-first for API requests, Cache-first for Assets
|
|
957
|
+
if (e.request.url.includes('/api/')) {
|
|
958
|
+
e.respondWith(fetch(e.request).catch(() => new Response(JSON.stringify({ error: "Offline" }), { headers: { "Content-Type": "application/json" }, status: 503 })));
|
|
959
|
+
} else {
|
|
960
|
+
e.respondWith(caches.match(e.request).then((res) => res || fetch(e.request).then((fres) => {
|
|
961
|
+
// Cache dynamically fetched non-API assets
|
|
962
|
+
return caches.open(CACHE_NAME).then(c => { c.put(e.request, fres.clone()); return fres; });
|
|
963
|
+
}).catch(() => null)));
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
self.addEventListener('message', (e) => {
|
|
968
|
+
if (e.data && e.data.action === 'skipWaiting') {
|
|
969
|
+
self.skipWaiting();
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
`.trim();
|
|
973
|
+
res.writeHead(200, {
|
|
974
|
+
"Content-Type": "application/javascript",
|
|
975
|
+
"Cache-Control": "no-cache"
|
|
976
|
+
});
|
|
977
|
+
return res.end(swContent);
|
|
978
|
+
}
|
|
979
|
+
// ─── PWA: Offline Fallback HTML (v5.4) ───
|
|
980
|
+
if (url.pathname === "/offline.html" && req.method === "GET") {
|
|
981
|
+
const offlineHtml = `<!DOCTYPE html>
|
|
982
|
+
<html lang="en">
|
|
983
|
+
<head>
|
|
984
|
+
<meta charset="UTF-8">
|
|
985
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
986
|
+
<title>Prism MCP — Offline</title>
|
|
987
|
+
<link rel="manifest" href="/manifest.json">
|
|
988
|
+
<meta name="theme-color" content="#0a0e1a">
|
|
989
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
990
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
|
991
|
+
<style>
|
|
992
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
993
|
+
body {
|
|
994
|
+
background: #0a0e1a;
|
|
995
|
+
color: #f1f5f9;
|
|
996
|
+
font-family: 'Inter', sans-serif;
|
|
997
|
+
min-height: 100vh;
|
|
998
|
+
display: flex;
|
|
999
|
+
flex-direction: column;
|
|
1000
|
+
align-items: center;
|
|
1001
|
+
justify-content: center;
|
|
1002
|
+
text-align: center;
|
|
1003
|
+
padding: 2rem;
|
|
1004
|
+
}
|
|
1005
|
+
.bg {
|
|
1006
|
+
position: fixed;
|
|
1007
|
+
inset: 0;
|
|
1008
|
+
background-image:
|
|
1009
|
+
radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.08) 0%, transparent 50%),
|
|
1010
|
+
radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.06) 0%, transparent 50%);
|
|
1011
|
+
z-index: 0;
|
|
1012
|
+
}
|
|
1013
|
+
.card {
|
|
1014
|
+
position: relative;
|
|
1015
|
+
z-index: 1;
|
|
1016
|
+
background: rgba(17, 24, 39, 0.6);
|
|
1017
|
+
backdrop-filter: blur(16px);
|
|
1018
|
+
border: 1px solid rgba(139, 92, 246, 0.15);
|
|
1019
|
+
border-radius: 16px;
|
|
1020
|
+
padding: 3rem 2rem;
|
|
1021
|
+
max-width: 400px;
|
|
1022
|
+
width: 100%;
|
|
1023
|
+
}
|
|
1024
|
+
.icon {
|
|
1025
|
+
font-size: 3rem;
|
|
1026
|
+
margin-bottom: 1.5rem;
|
|
1027
|
+
opacity: 0.8;
|
|
1028
|
+
filter: drop-shadow(0 0 10px rgba(139, 92, 246, 0.5));
|
|
1029
|
+
}
|
|
1030
|
+
h1 {
|
|
1031
|
+
font-size: 1.5rem;
|
|
1032
|
+
font-weight: 600;
|
|
1033
|
+
margin-bottom: 1rem;
|
|
1034
|
+
background: linear-gradient(135deg, #8b5cf6, #3b82f6);
|
|
1035
|
+
-webkit-background-clip: text;
|
|
1036
|
+
-webkit-text-fill-color: transparent;
|
|
1037
|
+
}
|
|
1038
|
+
p {
|
|
1039
|
+
color: #94a3b8;
|
|
1040
|
+
font-size: 0.95rem;
|
|
1041
|
+
line-height: 1.5;
|
|
1042
|
+
margin-bottom: 2rem;
|
|
1043
|
+
}
|
|
1044
|
+
button {
|
|
1045
|
+
background: linear-gradient(135deg, #8b5cf6, #3b82f6);
|
|
1046
|
+
color: white;
|
|
1047
|
+
border: none;
|
|
1048
|
+
padding: 0.75rem 1.5rem;
|
|
1049
|
+
border-radius: 8px;
|
|
1050
|
+
font-size: 0.95rem;
|
|
1051
|
+
font-weight: 600;
|
|
1052
|
+
cursor: pointer;
|
|
1053
|
+
font-family: 'Inter', sans-serif;
|
|
1054
|
+
transition: opacity 0.2s;
|
|
1055
|
+
}
|
|
1056
|
+
button:hover { opacity: 0.9; }
|
|
1057
|
+
</style>
|
|
1058
|
+
</head>
|
|
1059
|
+
<body>
|
|
1060
|
+
<div class="bg"></div>
|
|
1061
|
+
<div class="card">
|
|
1062
|
+
<div class="icon">🔌</div>
|
|
1063
|
+
<h1>You are currently offline.</h1>
|
|
1064
|
+
<p>The Prism Dashboard cannot reach the MCP server. Please check your internet connection to resume.</p>
|
|
1065
|
+
<button onclick="window.location.reload()">Try Again</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
</body>
|
|
1068
|
+
</html>`;
|
|
1069
|
+
res.writeHead(200, {
|
|
1070
|
+
"Content-Type": "text/html",
|
|
1071
|
+
"Cache-Control": "public, max-age=86400"
|
|
1072
|
+
});
|
|
1073
|
+
return res.end(offlineHtml);
|
|
1074
|
+
}
|
|
1075
|
+
// ─── PWA: Dynamic SVG Icons (v5.4) ───
|
|
1076
|
+
if ((url.pathname === "/icon-192.svg" || url.pathname === "/icon-512.svg" || url.pathname === "/icon-192-maskable.svg" || url.pathname === "/icon-512-maskable.svg") && req.method === "GET") {
|
|
1077
|
+
const size = url.pathname.includes("192") ? 192 : 512;
|
|
1078
|
+
const isMaskable = url.pathname.includes("maskable");
|
|
1079
|
+
// For standard "any" icons, we might want rounded corners or a specific size ratio if needed,
|
|
1080
|
+
// but since we separate purposes, we can keep the SVG identical as adaptive scaling is handled by OS
|
|
1081
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}">
|
|
1082
|
+
<defs>
|
|
1083
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
1084
|
+
<stop offset="0%" stop-color="#8b5cf6" />
|
|
1085
|
+
<stop offset="50%" stop-color="#3b82f6" />
|
|
1086
|
+
<stop offset="100%" stop-color="#06b6d4" />
|
|
1087
|
+
</linearGradient>
|
|
1088
|
+
</defs>
|
|
1089
|
+
<rect width="${size}" height="${size}" rx="${isMaskable ? 0 : Math.floor(size * 0.2)}" fill="#0a0e1a"/>
|
|
1090
|
+
<path d="M${size * 0.5} ${size * 0.25} L${size * 0.75} ${size * 0.75} L${size * 0.25} ${size * 0.75} Z" fill="url(#grad)" opacity="0.9"/>
|
|
1091
|
+
<circle cx="${size * 0.5}" cy="${size * 0.55}" r="${size * 0.15}" fill="#ffffff" opacity="0.1" />
|
|
1092
|
+
</svg>`;
|
|
1093
|
+
res.writeHead(200, {
|
|
1094
|
+
"Content-Type": "image/svg+xml",
|
|
1095
|
+
"Cache-Control": "public, max-age=86400"
|
|
1096
|
+
});
|
|
1097
|
+
return res.end(svg);
|
|
1098
|
+
}
|
|
1099
|
+
// ─── PWA: iOS Apple Touch Icon (v5.5) ───
|
|
1100
|
+
if (url.pathname === "/apple-touch-icon.png" && req.method === "GET") {
|
|
1101
|
+
// iOS Safari does not support SVG for apple-touch-icon; requires PNG.
|
|
1102
|
+
// We serve a robust Base64-encoded 180x180 opaque PNG to prevent the "black box" issue.
|
|
1103
|
+
const pngBase64 = "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";
|
|
1104
|
+
res.writeHead(200, {
|
|
1105
|
+
"Content-Type": "image/png",
|
|
1106
|
+
"Cache-Control": "public, max-age=86400"
|
|
1107
|
+
});
|
|
1108
|
+
return res.end(Buffer.from(pngBase64, "base64"));
|
|
1109
|
+
}
|
|
867
1110
|
// ─── 404 ───
|
|
868
1111
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
869
1112
|
res.end("Not found");
|
|
@@ -930,6 +1173,9 @@ return false;}
|
|
|
930
1173
|
}
|
|
931
1174
|
console.error(`[Prism] 🧠 Mind Palace Dashboard → http://localhost:${boundPort}`);
|
|
932
1175
|
// ─── v3.1: TTL Sweep — runs at startup + every 12 hours ───────────
|
|
1176
|
+
// NOTE (v5.4): The Background Scheduler in server.ts now also handles
|
|
1177
|
+
// TTL sweeps. This dashboard sweep is kept as a legacy fallback for
|
|
1178
|
+
// deployments where the scheduler is disabled. Both are idempotent.
|
|
933
1179
|
async function runTtlSweep() {
|
|
934
1180
|
try {
|
|
935
1181
|
const allSettings = await getAllSettings();
|