prism-mcp-server 2.1.2 β†’ 2.3.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 CHANGED
@@ -14,9 +14,26 @@
14
14
 
15
15
  ---
16
16
 
17
- ## What's New in v2.0 "Mind Palace" 🧠
17
+ ## What's New in v2.3.0 β€” AI Reasoning Engine 🧠
18
18
 
19
- Prism MCP has been completely rebuilt from the ground up to support **local-first workflows**, **visual agent memory**, and **multi-client synchronization**.
19
+ | Feature | Description |
20
+ |---|---|
21
+ | πŸ•ΈοΈ **Neural Graph** | Interactive knowledge graph on the Mind Palace Dashboard β€” visualize how projects connect through shared keywords and categories using Vis.js force-directed layout. |
22
+ | πŸ›‘οΈ **Prompt Injection Shield** | Gemini-powered security scan in `session_health_check` β€” detects system override attempts, jailbreaks, and data exfiltration hidden in agent memory. Tuned to avoid false positives on normal dev commands. |
23
+ | 🧬 **Fact Merger** | Async LLM contradiction resolution on every handoff save β€” if old context says "Postgres" and new says "MySQL", Gemini silently merges the facts in the background. Zero latency impact (fire-and-forget). |
24
+
25
+ <details>
26
+ <summary><strong>What's in v2.2.0</strong></summary>
27
+
28
+ | Feature | Description |
29
+ |---|---|
30
+ | 🩺 **Brain Health Check** | `session_health_check` β€” like Unix `fsck` for your agent's memory. Detects missing embeddings, duplicate entries, orphaned handoffs, and stale rollups. Use `auto_fix: true` to repair automatically. |
31
+ | πŸ“Š **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard β€” see your memory integrity at a glance. |
32
+
33
+ </details>
34
+
35
+ <details>
36
+ <summary><strong>What's in v2.0 "Mind Palace"</strong></summary>
20
37
 
21
38
  | Feature | Description |
22
39
  |---|---|
@@ -29,6 +46,8 @@ Prism MCP has been completely rebuilt from the ground up to support **local-firs
29
46
  | πŸ“ **Code Mode Templates** | 8 pre-built QuickJS extraction templates for GitHub, Jira, OpenAPI, Slack, CSV, and DOM parsing β€” zero reasoning tokens. |
30
47
  | πŸ” **Reality Drift Detection** | Prism captures Git state on save and warns if files changed outside the agent's view. |
31
48
 
49
+ </details>
50
+
32
51
  ---
33
52
 
34
53
  ## Quick Start (Zero Config β€” Local Mode)
@@ -301,7 +320,7 @@ graph TB
301
320
  | `knowledge_search` | Semantic search across accumulated knowledge |
302
321
  | `knowledge_forget` | Prune outdated or incorrect memories (4 modes + dry_run) |
303
322
  | `session_search_memory` | Vector similarity search across all sessions |
304
- | `backfill_embeddings` | Retroactively generate embeddings for existing entries |
323
+ | `session_compact_ledger` | Auto-compact old ledger entries via Gemini-powered summarization |
305
324
 
306
325
  ### v2.0 Advanced Memory Tools
307
326
 
@@ -312,6 +331,12 @@ graph TB
312
331
  | `session_save_image` | Save a screenshot/image to the visual memory vault |
313
332
  | `session_view_image` | Retrieve and display a saved image from the vault |
314
333
 
334
+ ### v2.2 Brain Health Tools
335
+
336
+ | Tool | Purpose | Key Args | Returns |
337
+ |------|---------|----------|---------|
338
+ | `session_health_check` | Scan brain for integrity issues (`fsck`) | `auto_fix` (boolean) | Health report & auto-repairs |
339
+
315
340
  ### Code Mode Templates (v2.1)
316
341
 
317
342
  Instead of writing custom JavaScript, pass a `template` name for instant extraction:
@@ -560,6 +585,8 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
560
585
  β”‚ β”œβ”€β”€ googleAi.ts # Gemini SDK wrapper
561
586
  β”‚ β”œβ”€β”€ executor.ts # QuickJS sandbox executor
562
587
  β”‚ β”œβ”€β”€ autoCapture.ts # Dev server HTML snapshot utility
588
+ β”‚ β”œβ”€β”€ healthCheck.ts # Brain integrity engine (v2.2.0) + security scanner (v2.3.0)
589
+ β”‚ β”œβ”€β”€ factMerger.ts # Async LLM contradiction resolution (v2.3.0)
563
590
  β”‚ β”œβ”€β”€ git.ts # Git state capture + drift detection
564
591
  β”‚ β”œβ”€β”€ embeddingApi.ts # Embedding generation (Gemini)
565
592
  β”‚ └── keywordExtractor.ts # Zero-dependency NLP keyword extraction
@@ -68,6 +68,90 @@ export async function startDashboardServer() {
68
68
  res.writeHead(200, { "Content-Type": "application/json" });
69
69
  return res.end(JSON.stringify({ context, ledger, history }));
70
70
  }
71
+ // ─── API: Brain Health Check (v2.2.0) ───
72
+ if (url.pathname === "/api/health") {
73
+ try {
74
+ const { runHealthCheck } = await import("../utils/healthCheck.js");
75
+ const stats = await storage.getHealthStats(PRISM_USER_ID);
76
+ const report = runHealthCheck(stats);
77
+ res.writeHead(200, { "Content-Type": "application/json" });
78
+ return res.end(JSON.stringify(report));
79
+ }
80
+ catch (err) {
81
+ console.error("[Dashboard] Health check error:", err);
82
+ res.writeHead(200, { "Content-Type": "application/json" });
83
+ return res.end(JSON.stringify({
84
+ status: "unknown",
85
+ summary: "Health check unavailable",
86
+ issues: [],
87
+ counts: { errors: 0, warnings: 0, infos: 0 },
88
+ totals: { activeEntries: 0, handoffs: 0, rollups: 0 },
89
+ timestamp: new Date().toISOString(),
90
+ }));
91
+ }
92
+ }
93
+ // ─── API: Knowledge Graph Data (v2.3.0) ───
94
+ if (url.pathname === "/api/graph") {
95
+ // Fetch recent ledger entries to build the graph
96
+ // We look at the last 100 entries to keep the graph relevant but performant
97
+ const entries = await storage.getLedgerEntries({
98
+ limit: "100",
99
+ order: "created_at.desc",
100
+ select: "project,keywords",
101
+ });
102
+ // Deduplication sets for nodes and edges
103
+ const nodes = [];
104
+ const edges = [];
105
+ const nodeIds = new Set(); // track unique node IDs
106
+ const edgeIds = new Set(); // track unique edges
107
+ // Helper: add a node only if it doesn't already exist
108
+ const addNode = (id, group, label) => {
109
+ if (!nodeIds.has(id)) {
110
+ nodes.push({ id, label: label || id, group });
111
+ nodeIds.add(id);
112
+ }
113
+ };
114
+ // Helper: add an edge only if it doesn't already exist
115
+ const addEdge = (from, to) => {
116
+ const id = `${from}-${to}`; // deterministic edge ID
117
+ if (!edgeIds.has(id)) {
118
+ edges.push({ from, to });
119
+ edgeIds.add(id);
120
+ }
121
+ };
122
+ // Transform relational data into graph nodes & edges
123
+ entries.forEach(row => {
124
+ if (!row.project)
125
+ return; // skip rows without project
126
+ // 1. Project node (hub β€” large purple dot)
127
+ addNode(row.project, "project");
128
+ // 2. Keyword nodes (spokes β€” small dots)
129
+ let keywords = [];
130
+ // Handle SQLite (JSON string) vs Supabase (native array)
131
+ if (Array.isArray(row.keywords)) {
132
+ keywords = row.keywords;
133
+ }
134
+ else if (typeof row.keywords === "string") {
135
+ try {
136
+ keywords = JSON.parse(row.keywords);
137
+ }
138
+ catch { /* skip malformed */ }
139
+ }
140
+ // Create nodes + edges for each keyword
141
+ keywords.forEach((kw) => {
142
+ if (kw.length < 3)
143
+ return; // skip noise like "a", "is"
144
+ // Handle categories (cat:debugging) vs raw keywords
145
+ const isCat = kw.startsWith("cat:");
146
+ const group = isCat ? "category" : "keyword";
147
+ const label = isCat ? kw.replace("cat:", "") : kw;
148
+ addNode(kw, group, label); // keyword/category node
149
+ addEdge(row.project, kw); // edge: project β†’ keyword
150
+ });
151
+ });
152
+ res.writeHead(200, { "Content-Type": "application/json" });
153
+ return res.end(JSON.stringify({ nodes, edges }));
154
+ }
71
155
  // ─── 404 ───
72
156
  res.writeHead(404, { "Content-Type": "text/plain" });
73
157
  res.end("Not found");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Mind Palace Dashboard β€” UI Renderer (v2.0 β€” Step 8)
2
+ * Mind Palace Dashboard β€” UI Renderer (v2.2.0)
3
3
  *
4
4
  * Pure CSS + Vanilla JS single-page dashboard.
5
5
  * No build step, no Tailwind, no framework β€” served as a template literal.
@@ -22,6 +22,8 @@ export function renderDashboardHTML() {
22
22
  <title>Prism MCP β€” Mind Palace</title>
23
23
  <link rel="preconnect" href="https://fonts.googleapis.com">
24
24
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
25
+ <!-- Vis.js for Neural Graph (v2.3.0) -->
26
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
25
27
  <style>
26
28
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
29
 
@@ -224,6 +226,50 @@ export function renderDashboardHTML() {
224
226
  /* ─── Fade in animation ─── */
225
227
  @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
226
228
  .fade-in { animation: fadeIn 0.4s ease-out forwards; }
229
+
230
+ /* ─── Brain Health Indicator (v2.2.0) ─── */
231
+ .health-status {
232
+ display: flex; align-items: center; gap: 0.75rem;
233
+ padding: 0.75rem 1rem; border-radius: var(--radius-sm);
234
+ background: rgba(15,23,42,0.6); margin-bottom: 1rem;
235
+ }
236
+ .health-dot {
237
+ width: 12px; height: 12px; border-radius: 50%;
238
+ flex-shrink: 0; position: relative;
239
+ }
240
+ .health-dot::after {
241
+ content: ''; position: absolute; inset: -3px;
242
+ border-radius: 50%; animation: healthPulse 2s ease-in-out infinite;
243
+ }
244
+ .health-dot.healthy { background: var(--accent-green); }
245
+ .health-dot.healthy::after { border: 2px solid rgba(16,185,129,0.3); }
246
+ .health-dot.degraded { background: var(--accent-amber); }
247
+ .health-dot.degraded::after { border: 2px solid rgba(245,158,11,0.3); }
248
+ .health-dot.unhealthy { background: var(--accent-rose); }
249
+ .health-dot.unhealthy::after { border: 2px solid rgba(244,63,94,0.3); }
250
+ .health-dot.unknown { background: var(--text-muted); }
251
+ .health-dot.unknown::after { border: 2px solid rgba(100,116,139,0.3); }
252
+ @keyframes healthPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
253
+ .health-label { font-size: 0.8rem; font-weight: 500; }
254
+ .health-summary { font-size: 0.75rem; color: var(--text-muted); }
255
+ .health-issues { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; }
256
+ .health-issues .issue-row {
257
+ padding: 0.3rem 0; display: flex; gap: 0.5rem; align-items: flex-start;
258
+ }
259
+
260
+ /* ─── Neural Graph (v2.3.0) ─── */
261
+ #network-container {
262
+ width: 100%; height: 300px;
263
+ border-radius: var(--radius);
264
+ background: rgba(0,0,0,0.2);
265
+ border: 1px solid var(--border-glass);
266
+ }
267
+ .refresh-btn {
268
+ margin-left: auto; background: none; border: none;
269
+ color: var(--text-muted); cursor: pointer; font-size: 0.85rem;
270
+ transition: color 0.2s;
271
+ }
272
+ .refresh-btn:hover { color: var(--accent-purple); }
227
273
  </style>
228
274
  </head>
229
275
  <body>
@@ -233,7 +279,7 @@ export function renderDashboardHTML() {
233
279
  <div class="logo">
234
280
  <span class="logo-icon">🧠</span>
235
281
  Prism Mind Palace
236
- <span class="version-badge">v2.0</span>
282
+ <span class="version-badge">v2.2.0</span>
237
283
  </div>
238
284
  <div class="selector">
239
285
  <select id="projectSelect">
@@ -271,6 +317,19 @@ export function renderDashboardHTML() {
271
317
  <div class="git-row"><span class="git-label">Key Context</span><span class="git-value" id="keyContext" style="font-family:var(--font-sans);max-width:200px;text-align:right">β€”</span></div>
272
318
  </div>
273
319
 
320
+ <!-- Brain Health (v2.2.0) -->
321
+ <div class="card" id="healthCard" style="display:none">
322
+ <div class="card-title"><span class="dot" style="background:var(--accent-green)"></span> Brain Health 🩺</div>
323
+ <div class="health-status">
324
+ <div class="health-dot unknown" id="healthDot"></div>
325
+ <div>
326
+ <div class="health-label" id="healthLabel">Scanning...</div>
327
+ <div class="health-summary" id="healthSummary"></div>
328
+ </div>
329
+ </div>
330
+ <div class="health-issues" id="healthIssues"></div>
331
+ </div>
332
+
274
333
  <!-- Morning Briefing -->
275
334
  <div class="card" id="briefingCard" style="display:none">
276
335
  <div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Morning Briefing πŸŒ…</div>
@@ -286,6 +345,17 @@ export function renderDashboardHTML() {
286
345
 
287
346
  <!-- Right Column -->
288
347
  <div class="grid" style="align-content: start;">
348
+
349
+ <!-- Neural Graph (v2.3.0) -->
350
+ <div class="card">
351
+ <div class="card-title">
352
+ <span class="dot" style="background:var(--accent-blue)"></span>
353
+ Neural Graph πŸ•ΈοΈ
354
+ <button onclick="loadGraph()" class="refresh-btn">↻</button>
355
+ </div>
356
+ <div id="network-container">Loading nodes...</div>
357
+ </div>
358
+
289
359
  <!-- Time Travel -->
290
360
  <div class="card">
291
361
  <div class="card-title"><span class="dot" style="background:var(--accent-purple)"></span> Time Travel History πŸ•°οΈ</div>
@@ -413,6 +483,49 @@ export function renderDashboardHTML() {
413
483
  ledgerEl.innerHTML = '<div style="color:var(--text-muted);font-size:0.85rem;padding:1rem;text-align:center">No ledger entries yet.</div>';
414
484
  }
415
485
 
486
+ // ─── Brain Health (v2.2.0) ───
487
+ try {
488
+ var healthRes = await fetch('/api/health');
489
+ var healthData = await healthRes.json();
490
+ var healthCard = document.getElementById('healthCard');
491
+ var healthDot = document.getElementById('healthDot');
492
+ var healthLabel = document.getElementById('healthLabel');
493
+ var healthSummary = document.getElementById('healthSummary');
494
+ var healthIssues = document.getElementById('healthIssues');
495
+
496
+ // Set the dot color based on status
497
+ healthDot.className = 'health-dot ' + (healthData.status || 'unknown');
498
+
499
+ // Map status to emoji + label
500
+ var statusMap = { healthy: 'βœ… Healthy', degraded: '⚠️ Degraded', unhealthy: 'πŸ”΄ Unhealthy' };
501
+ healthLabel.textContent = statusMap[healthData.status] || '❓ Unknown';
502
+
503
+ // Stats summary line
504
+ var t = healthData.totals || {};
505
+ healthSummary.textContent = (t.activeEntries || 0) + ' entries Β· ' +
506
+ (t.handoffs || 0) + ' handoffs Β· ' +
507
+ (t.rollups || 0) + ' rollups';
508
+
509
+ // Issue rows
510
+ var issues = healthData.issues || [];
511
+ if (issues.length > 0) {
512
+ var sevIcons = { error: 'πŸ”΄', warning: '🟑', info: 'πŸ”΅' };
513
+ healthIssues.innerHTML = issues.map(function(i) {
514
+ return '<div class="issue-row">' +
515
+ '<span>' + (sevIcons[i.severity] || '❓') + '</span>' +
516
+ '<span>' + escapeHtml(i.message) + '</span>' +
517
+ '</div>';
518
+ }).join('');
519
+ } else {
520
+ healthIssues.innerHTML = '<div style="color:var(--accent-green);font-size:0.8rem">πŸŽ‰ No issues found</div>';
521
+ }
522
+
523
+ healthCard.style.display = 'block';
524
+ } catch(he) {
525
+ // Health check not available β€” silently skip
526
+ console.warn('Health check unavailable:', he);
527
+ }
528
+
416
529
  document.getElementById('content').className = 'grid grid-main fade-in';
417
530
  document.getElementById('content').style.display = 'grid';
418
531
  } catch(e) {
@@ -438,6 +551,73 @@ export function renderDashboardHTML() {
438
551
 
439
552
  // Allow Enter key in select to trigger load
440
553
  document.getElementById('projectSelect').addEventListener('change', loadProject);
554
+
555
+ // ─── Neural Graph (v2.3.0) ───
556
+ // Renders a force-directed graph of projects ↔ keywords ↔ categories
557
+ async function loadGraph() {
558
+ var container = document.getElementById('network-container');
559
+ if (!container) return;
560
+
561
+ try {
562
+ var res = await fetch('/api/graph');
563
+ var data = await res.json();
564
+
565
+ // Empty state β€” no ledger entries yet
566
+ if (data.nodes.length === 0) {
567
+ container.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:0.85rem">No knowledge associations found yet.</div>';
568
+ return;
569
+ }
570
+
571
+ // Vis.js dark-theme config matching the glassmorphism palette
572
+ var options = {
573
+ nodes: {
574
+ shape: 'dot', // all nodes are circles
575
+ borderWidth: 0, // no borders for clean look
576
+ font: { color: '#94a3b8', face: 'Inter', size: 12 }
577
+ },
578
+ edges: {
579
+ width: 1, // thin edges for subtlety
580
+ color: { color: 'rgba(139,92,246,0.15)', highlight: '#8b5cf6' },
581
+ smooth: { type: 'continuous' } // smooth curves
582
+ },
583
+ groups: {
584
+ project: { // Hub nodes β€” large purple
585
+ color: { background: '#8b5cf6', border: '#7c3aed' },
586
+ size: 20,
587
+ font: { size: 14, color: '#f1f5f9', face: 'Inter' }
588
+ },
589
+ category: { // Category nodes β€” cyan diamonds
590
+ color: { background: '#06b6d4', border: '#0891b2' },
591
+ size: 10,
592
+ shape: 'diamond'
593
+ },
594
+ keyword: { // Keyword nodes β€” small dark dots
595
+ color: { background: '#1e293b', border: '#334155' },
596
+ size: 6,
597
+ font: { size: 10, color: '#64748b' }
598
+ }
599
+ },
600
+ physics: {
601
+ stabilization: false, // animate on load for visual pop
602
+ barnesHut: {
603
+ gravitationalConstant: -3000, // spread nodes apart
604
+ springConstant: 0.04, // gentle spring force
605
+ springLength: 80 // default edge length
606
+ }
607
+ },
608
+ interaction: { hover: true } // highlight on hover
609
+ };
610
+
611
+ // Create the network visualization
612
+ new vis.Network(container, data, options);
613
+ } catch (e) {
614
+ console.error('Graph error', e);
615
+ container.innerHTML = '<div style="padding:1rem;color:var(--accent-rose)">Graph failed to load</div>';
616
+ }
617
+ }
618
+
619
+ // Initialize the graph on page load
620
+ loadGraph();
441
621
  </script>
442
622
  </body>
443
623
  </html>`;
package/dist/server.js CHANGED
@@ -72,17 +72,21 @@ import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BR
72
72
  // Session memory tools β€” only used if Supabase is configured
73
73
  import { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL,
74
74
  // ─── v0.4.0: New tool definitions (Enhancements #2 and #4) ───
75
- SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, SESSION_BACKFILL_EMBEDDINGS_TOOL,
75
+ SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL,
76
76
  // ─── v2.0: Time Travel tool definitions ───
77
77
  MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL,
78
78
  // ─── v2.0: Visual Memory tool definitions ───
79
- SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
79
+ SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL,
80
+ // ─── v2.2.0: Health Check tool definition ───
81
+ SESSION_HEALTH_CHECK_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
80
82
  // ─── v0.4.0: New tool handlers ───
81
- compactLedgerHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler,
83
+ compactLedgerHandler, sessionSearchMemoryHandler,
82
84
  // ─── v2.0: Time Travel handlers ───
83
85
  memoryHistoryHandler, memoryCheckoutHandler,
84
86
  // ─── v2.0: Visual Memory handlers ───
85
- sessionSaveImageHandler, sessionViewImageHandler, } from "./tools/index.js";
87
+ sessionSaveImageHandler, sessionViewImageHandler,
88
+ // ─── v2.2.0: Health Check handler ───
89
+ sessionHealthCheckHandler, } from "./tools/index.js";
86
90
  // ─── Dynamic Tool Registration ───────────────────────────────────
87
91
  // Base tools: always available regardless of configuration
88
92
  const BASE_TOOLS = [
@@ -106,12 +110,13 @@ const SESSION_MEMORY_TOOLS = [
106
110
  KNOWLEDGE_FORGET_TOOL, // knowledge_forget β€” prune bad/old memories
107
111
  SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger β€” auto-compact old ledger entries (v0.4.0)
108
112
  SESSION_SEARCH_MEMORY_TOOL, // session_search_memory β€” semantic search via embeddings (v0.4.0)
109
- SESSION_BACKFILL_EMBEDDINGS_TOOL, // session_backfill_embeddings β€” repair missing embeddings
110
113
  MEMORY_HISTORY_TOOL, // memory_history β€” view version timeline (v2.0)
111
114
  MEMORY_CHECKOUT_TOOL, // memory_checkout β€” revert to past version (v2.0)
112
115
  // ─── v2.0: Visual Memory tools ───
113
116
  SESSION_SAVE_IMAGE_TOOL, // session_save_image β€” save image to media vault (v2.0)
114
117
  SESSION_VIEW_IMAGE_TOOL, // session_view_image β€” retrieve image from vault (v2.0)
118
+ // ─── v2.2.0: Health Check tool ───
119
+ SESSION_HEALTH_CHECK_TOOL, // session_health_check β€” brain integrity checker (v2.2.0)
115
120
  ];
116
121
  // Combine: if session memory is enabled, add those tools too
117
122
  const ALL_TOOLS = [
@@ -477,10 +482,6 @@ export function createServer() {
477
482
  if (!SESSION_MEMORY_ENABLED)
478
483
  throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
479
484
  return await sessionSearchMemoryHandler(args);
480
- case "session_backfill_embeddings":
481
- if (!SESSION_MEMORY_ENABLED)
482
- throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
483
- return await backfillEmbeddingsHandler(args);
484
485
  // ─── v2.0: Time Travel Tools ───
485
486
  case "memory_history":
486
487
  if (!SESSION_MEMORY_ENABLED)
@@ -499,6 +500,11 @@ export function createServer() {
499
500
  if (!SESSION_MEMORY_ENABLED)
500
501
  throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
501
502
  return await sessionViewImageHandler(args);
503
+ // ─── v2.2.0: Health Check Tool ───
504
+ case "session_health_check":
505
+ if (!SESSION_MEMORY_ENABLED)
506
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
507
+ return await sessionHealthCheckHandler(args);
502
508
  default:
503
509
  return {
504
510
  content: [{ type: "text", text: `Unknown tool: ${name}` }],
@@ -726,4 +726,123 @@ export class SqliteStorage {
726
726
  const result = await this.db.execute("SELECT DISTINCT project FROM session_handoffs ORDER BY project ASC");
727
727
  return result.rows.map(row => row.project);
728
728
  }
729
+ // ─── v2.2.0 Health Check (fsck) ─────────────────────────────
730
+ /**
731
+ * Gather raw health statistics for the integrity checker.
732
+ *
733
+ * This method runs 5 lightweight SQL queries and returns raw data.
734
+ * The heavy analysis (duplicate detection via Jaccard similarity)
735
+ * happens in healthCheck.ts in pure JS β€” keeping SQLite free of
736
+ * C-extension dependencies like Levenshtein.
737
+ */
738
+ async getHealthStats(userId) {
739
+ // ── Check 1: Count entries with no embedding vector ──────────
740
+ // When Gemini API is down during save, the fire-and-forget
741
+ // embedding call fails silently. These rows need backfill.
742
+ const missingResult = await this.db.execute({
743
+ sql: `
744
+ SELECT COUNT(*) as cnt
745
+ FROM session_ledger
746
+ WHERE user_id = ?
747
+ AND archived_at IS NULL
748
+ AND embedding IS NULL
749
+ `,
750
+ args: [userId], // bind user_id to the ? placeholder
751
+ });
752
+ const missingEmbeddings = Number(// extract count, default 0
753
+ missingResult.rows[0]?.cnt ?? 0);
754
+ // ── Check 2: Fetch active summaries for JS duplicate detection ─
755
+ // We pull id + project + summary into memory so healthCheck.ts
756
+ // can run Jaccard similarity in pure JS (~5ms for typical sets).
757
+ // The Compactor keeps the active ledger small, so this is safe.
758
+ const summariesResult = await this.db.execute({
759
+ sql: `
760
+ SELECT id, project, summary
761
+ FROM session_ledger
762
+ WHERE user_id = ?
763
+ AND archived_at IS NULL
764
+ `,
765
+ args: [userId], // bind user_id to the ? placeholder
766
+ });
767
+ // Map raw DB rows to typed objects for the health engine
768
+ const activeLedgerSummaries = summariesResult.rows.map(row => ({
769
+ id: row.id, // unique entry identifier
770
+ project: row.project, // project this entry belongs to
771
+ summary: row.summary, // text we compare for duplicates
772
+ }));
773
+ // ── Check 3: Find orphaned handoffs ──────────────────────────
774
+ // An orphaned handoff = handoff state exists but zero active
775
+ // ledger entries back it. Usually from testing or bugs.
776
+ // LEFT JOIN + HAVING COUNT = 0 finds projects with no entries.
777
+ const orphanResult = await this.db.execute({
778
+ sql: `
779
+ SELECT h.project
780
+ FROM session_handoffs h
781
+ LEFT JOIN session_ledger l
782
+ ON h.project = l.project
783
+ AND h.user_id = l.user_id
784
+ AND l.archived_at IS NULL
785
+ WHERE h.user_id = ?
786
+ GROUP BY h.project
787
+ HAVING COUNT(l.id) = 0
788
+ `,
789
+ args: [userId], // bind user_id to the ? placeholder
790
+ });
791
+ // Map to simple project name objects
792
+ const orphanedHandoffs = orphanResult.rows.map(row => ({
793
+ project: row.project, // the orphaned project name
794
+ }));
795
+ // ── Check 4: Count stale rollups ─────────────────────────────
796
+ // A rollup entry should have archived originals backing it.
797
+ // If those originals were hard-deleted, the rollup is stale.
798
+ // Self-join: rollups (is_rollup=1) LEFT JOIN archived entries.
799
+ const staleResult = await this.db.execute({
800
+ sql: `
801
+ SELECT r.id
802
+ FROM session_ledger r
803
+ LEFT JOIN session_ledger a
804
+ ON a.archived_at IS NOT NULL
805
+ AND a.project = r.project
806
+ AND a.user_id = r.user_id
807
+ WHERE r.user_id = ?
808
+ AND r.is_rollup = 1
809
+ AND r.archived_at IS NULL
810
+ GROUP BY r.id
811
+ HAVING COUNT(a.id) = 0
812
+ `,
813
+ args: [userId], // bind user_id to the ? placeholder
814
+ });
815
+ // Count how many rollups have zero archived originals
816
+ const staleRollups = staleResult.rows.length;
817
+ // ── Totals: aggregate counts for health report summary ───────
818
+ // Three scalar subqueries in one shot for efficiency.
819
+ const totalsResult = await this.db.execute({
820
+ sql: `
821
+ SELECT
822
+ (SELECT COUNT(*) FROM session_ledger
823
+ WHERE user_id = ? AND archived_at IS NULL) as active,
824
+ (SELECT COUNT(*) FROM session_handoffs
825
+ WHERE user_id = ?) as handoffs,
826
+ (SELECT COUNT(*) FROM session_ledger
827
+ WHERE user_id = ? AND is_rollup = 1
828
+ AND archived_at IS NULL) as rollups
829
+ `,
830
+ args: [userId, userId, userId], // bind user_id 3x (one per subquery)
831
+ });
832
+ // Extract each total, fallback to 0 if undefined
833
+ const totalActiveEntries = Number(totalsResult.rows[0]?.active ?? 0);
834
+ const totalHandoffs = Number(totalsResult.rows[0]?.handoffs ?? 0);
835
+ const totalRollups = Number(totalsResult.rows[0]?.rollups ?? 0);
836
+ // ── Return the complete raw health stats ─────────────────────
837
+ // healthCheck.ts engine will analyze this + produce HealthReport
838
+ return {
839
+ missingEmbeddings, // entries needing embedding repair
840
+ activeLedgerSummaries, // raw summaries for JS dupe detection
841
+ orphanedHandoffs, // projects with handoff but no ledger
842
+ staleRollups, // rollups with no archived originals
843
+ totalActiveEntries, // grand total of active entries
844
+ totalHandoffs, // grand total of handoff records
845
+ totalRollups, // grand total of rollup entries
846
+ };
847
+ }
729
848
  }