prism-mcp-server 3.0.0 → 3.1.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,7 +14,7 @@
14
14
 
15
15
  ## Table of Contents
16
16
 
17
- - [What's New (v3.0.0)](#whats-new-in-v300---agent-hivemind-)
17
+ - [What's New (v3.1.0)](#whats-new-in-v310---memory-lifecycle-)
18
18
  - [How Prism Compares](#how-prism-compares)
19
19
  - [Quick Start](#quick-start-zero-config--local-mode)
20
20
  - [Mind Palace Dashboard](#-the-mind-palace-dashboard)
@@ -22,6 +22,7 @@
22
22
  - [Use Cases](#use-cases)
23
23
  - [Architecture](#architecture)
24
24
  - [Tool Reference](#tool-reference)
25
+ - [Agent Hivemind — Role Usage](#agent-hivemind--role-usage)
25
26
  - [LangChain / LangGraph Integration](#langchain--langgraph-integration)
26
27
  - [Environment Variables](#environment-variables)
27
28
  - [Boot Settings (Restart Required)](#-boot-settings-restart-required)
@@ -38,7 +39,30 @@
38
39
 
39
40
  ---
40
41
 
41
- ## What's New in v3.0.0 — Agent Hivemind 🐝
42
+ ## What's New in v3.1.0 — Memory Lifecycle 🔄
43
+
44
+ | Feature | Description |
45
+ |---|---|
46
+ | 📊 **Memory Analytics** | New **Memory Analytics** card in the dashboard — 14-day sparkline chart, active sessions count, rollup savings, and average context richness. Powered by `getAnalytics()` on both SQLite and Supabase backends. |
47
+ | ⏳ **Automated Data Retention (TTL)** | Set a per-project data retention policy via `knowledge_set_retention` MCP tool or the dashboard **Lifecycle Controls** card. Entries older than the TTL are soft-deleted (GDPR-compliant `archived_at` tombstone) every 12 hours automatically. Rollups are never expired. Minimum 7 days to prevent accidental mass-delete. |
48
+ | 🗜️ **Smart Auto-Compaction** | After every `session_save_ledger`, Prism runs a background health check and triggers compaction automatically if the brain is degraded or unhealthy — gated by `compaction_auto` setting and debounced per-project to prevent concurrent Gemini calls. **Compact Now** button also available in the dashboard. |
49
+ | 📦 **PKM Export (Obsidian / Logseq)** | Export any project's full memory as a ZIP archive of Markdown files — one file per session with YAML-like frontmatter, TODOs, decisions, files-changed, and `#hashtag` keywords. Includes an `_index.md` with `[[wikilink]]` references. Click **Export ZIP** in the dashboard Lifecycle Controls card. |
50
+ | 🧪 **Expanded Test Suite** | 37 new Vitest tests (95 total) — covers analytics queries, TTL soft-delete idempotency, rollup preservation, `activeCompactions` Set memory-leak prevention, type guards, export Markdown structure, and TTL sweep scheduler contracts. |
51
+
52
+ <details>
53
+ <summary><strong>What's in v3.0.1 — Agent Identity & Brain Clean-up 🧹</strong></summary>
54
+
55
+ | Feature | Description |
56
+ |---|---|
57
+ | 🧹 **Brain Health Clean-up** | New **Fix Issues** button in the Mind Palace Dashboard's Brain Health card — detects orphaned handoffs, missing embeddings, and stale rollups, then cleans them up in one click without needing the MCP tool. |
58
+ | 👤 **Agent Identity Settings** | Dashboard Settings → Agent Identity panel lets you set a **Default Role** (`dev`, `qa`, `pm`…) and **Agent Name** (e.g. `Dmitri`). Both values auto-apply as fallbacks in all memory and Hivemind tools — no need to pass them per call. |
59
+ | 📜 **Role-Scoped Skills** | Each agent role can have its own persistent skill/rules document stored in the dashboard (⚙️ Settings → Skills). It is automatically injected into every `session_load_context` response so the agent boots with its rules pre-loaded. |
60
+ | 🔤 **Resource Formatting Fix** | `memory://{project}/handoff` resources now render as formatted plain text (Last Summary, TODOs, Keywords) instead of a raw JSON blob — readable in Claude Desktop's paperclip attach panel. |
61
+
62
+ </details>
63
+
64
+ <details>
65
+ <summary><strong>What's in v3.0.0 — Agent Hivemind 🐝</strong></summary>
42
66
 
43
67
  | Feature | Description |
44
68
  |---|---|
@@ -50,6 +74,9 @@
50
74
  | 🔒 **Conditional Tool Registration** | `PRISM_ENABLE_HIVEMIND` env var gates Hivemind tools — users who don't need multi-agent features keep the same lean tool count as v2.x. |
51
75
  | ✅ **Test Suite** | 58 tests across 4 suites (storage, tools, dashboard, load) with Vitest — includes concurrent write stress tests, role isolation verification, and 0.2ms/write performance benchmarks. |
52
76
 
77
+ </details>
78
+
79
+
53
80
  <details>
54
81
  <summary><strong>What's in v2.5.0 — Enterprise Memory 🏗️</strong></summary>
55
82
 
@@ -102,7 +129,7 @@
102
129
  | Feature | Description |
103
130
  |---|---|
104
131
  | 🩺 **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. |
105
- | 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. |
132
+ | 📊 **Mind Palace Health** | Brain health indicator on the Mind Palace Dashboard — see your memory integrity at a glance. **🧹 Fix Issues** button auto-deletes orphaned handoffs in one click. |
106
133
 
107
134
  </details>
108
135
 
@@ -237,11 +264,15 @@ Open **`http://localhost:3000`** in your browser to see exactly what your AI age
237
264
  ![Mind Palace Dashboard](docs/mind-palace-dashboard.png)
238
265
 
239
266
  - **Current State & TODOs** — See the exact context injected into the LLM's prompt
267
+ - **Agent Identity Chip** — Header shows your active role + name (e.g. `🛠️ dev · Antigravity`); click to open Settings
268
+ - **Brain Health 🩺** — Memory integrity status at a glance; **🧹 Fix Issues** button auto-cleans orphaned handoffs in one click
240
269
  - **Git Drift Detection** — Alerts you if you've modified code outside the agent's view
241
270
  - **Morning Briefing** — AI-synthesized action plan from your last sessions
242
271
  - **Time Travel Timeline** — Browse historical handoff states and revert any version
243
272
  - **Visual Memory Vault** — Browse UI screenshots and auto-captured HTML states
244
273
  - **Session Ledger** — Full audit trail of every decision your agent has made
274
+ - **Neural Graph** — Force-directed visualization of project ↔ keyword associations
275
+ - **Hivemind Radar** — Real-time active agent roster with role, task, and heartbeat
245
276
 
246
277
  The dashboard auto-discovers all your projects and updates in real time.
247
278
 
@@ -450,6 +481,12 @@ graph TB
450
481
  | `session_search_memory` | Vector similarity search across all sessions |
451
482
  | `session_compact_ledger` | Auto-compact old ledger entries via Gemini-powered summarization |
452
483
 
484
+ ### v3.1 Lifecycle Tools
485
+
486
+ | Tool | Purpose |
487
+ |------|---------|
488
+ | `knowledge_set_retention` | Set a per-project TTL retention policy (0 = disabled, min 7 days). Immediately expires overdue entries. |
489
+
453
490
  ### v2.0 Advanced Memory Tools
454
491
 
455
492
  | Tool | Purpose |
@@ -463,7 +500,15 @@ graph TB
463
500
 
464
501
  | Tool | Purpose | Key Args | Returns |
465
502
  |------|---------|----------|---------|
466
- | `session_health_check` | Scan brain for integrity issues (`fsck`) | `auto_fix` (boolean) | Health report & auto-repairs |
503
+ | `session_health_check` | Scan brain for integrity issues (`fsck`) | `project`, `auto_fix` (boolean) | Health report & auto-repairs |
504
+
505
+ The **Mind Palace Dashboard** also shows a live **Brain Health 🩺** card for every project:
506
+
507
+ - **Status indicator** — `✅ Healthy` or `⚠️ Issues detected` with entry/handoff/rollup counts
508
+ - **🧹 Fix Issues button** — appears automatically when issues are detected; click to clean up orphaned handoffs and stale rollups in one click, no MCP tool call required
509
+ - **No issues found** — shown in green when memory integrity is confirmed
510
+
511
+ The tool and dashboard button both call the same repair logic — the dashboard button is simply a zero-friction shortcut for common maintenance.
467
512
 
468
513
  ### v2.5 Enterprise Memory Tools
469
514
 
@@ -492,6 +537,110 @@ Instead of writing custom JavaScript, pass a `template` name for instant extract
492
537
 
493
538
  ---
494
539
 
540
+ ## Agent Hivemind — Role Usage
541
+
542
+ Role-scoped memory lets multiple agents work on the same project without stepping on each other's memory. Each role gets its own isolated memory lane. Defaults to `global` for full backward compatibility.
543
+
544
+ ### Available Roles
545
+
546
+ | Role | Use for |
547
+ |------|---------|
548
+ | `dev` | Development agent |
549
+ | `qa` | Testing / QA agent |
550
+ | `pm` | Product management |
551
+ | `lead` | Tech lead / orchestrator |
552
+ | `security` | Security review |
553
+ | `ux` | Design / UX |
554
+ | `global` | Default — shared, no isolation |
555
+
556
+ Custom role strings are also supported (e.g. `"docs"`, `"ml"`).
557
+
558
+ ### Using Roles with Memory Tools
559
+
560
+ Just add `"role"` to any of the core memory tools:
561
+
562
+ ```json
563
+ // Save a ledger entry as the "dev" agent
564
+ { "name": "session_save_ledger", "arguments": {
565
+ "project": "my-app",
566
+ "role": "dev",
567
+ "conversation_id": "abc123",
568
+ "summary": "Fixed the auth race condition"
569
+ }}
570
+
571
+ // Load context scoped to your role
572
+ // Also injects a Team Roster showing active teammates
573
+ { "name": "session_load_context", "arguments": {
574
+ "project": "my-app",
575
+ "role": "dev",
576
+ "level": "standard"
577
+ }}
578
+
579
+ // Save handoff as the "qa" agent
580
+ { "name": "session_save_handoff", "arguments": {
581
+ "project": "my-app",
582
+ "role": "qa",
583
+ "last_summary": "Ran regression suite — 2 failures in auth module"
584
+ }}
585
+ ```
586
+
587
+ ### Hivemind Coordination Tools
588
+
589
+ > **Requires:** `PRISM_ENABLE_HIVEMIND=true` (Boot Setting — restart required)
590
+
591
+ ```json
592
+ // Announce yourself to the team at session start
593
+ { "name": "agent_register", "arguments": {
594
+ "project": "my-app",
595
+ "role": "dev",
596
+ "agent_name": "Dev Agent #1",
597
+ "current_task": "Refactoring auth module"
598
+ }}
599
+
600
+ // Pulse every ~5 min to stay visible (agents pruned after 30 min)
601
+ { "name": "agent_heartbeat", "arguments": {
602
+ "project": "my-app",
603
+ "role": "dev",
604
+ "current_task": "Now writing tests"
605
+ }}
606
+
607
+ // See everyone on the team
608
+ { "name": "agent_list_team", "arguments": {
609
+ "project": "my-app"
610
+ }}
611
+ ```
612
+
613
+ ### How Role Isolation Works
614
+
615
+ - `session_load_context` with `role: "dev"` only sees entries saved with `role: "dev"`
616
+ - The `global` role is a shared pool — anything saved without a role goes here
617
+ - When loading *with* a role, Prism auto-injects a **Team Roster** block listing active teammates, roles, and tasks — no extra tool call needed
618
+ - The Hivemind Radar widget in the Mind Palace dashboard shows agent activity in real time
619
+
620
+ ### Setting Your Agent Identity
621
+
622
+ The easiest way to configure your role and name is via the **Mind Palace Dashboard ⚙️ Settings → Agent Identity**:
623
+
624
+ - **Default Role** — dropdown to select `dev`, `qa`, `pm`, `lead`, `security`, `ux`, or `global`
625
+ - **Agent Name** — free text for your display name (e.g. `Dmitri`, `Dev Alex`, `QA Bot`)
626
+
627
+ Once set, **all memory and Hivemind tools automatically use these values** as fallbacks — no need to pass `role` or `agent_name` in every tool call.
628
+
629
+ > **Priority order:** explicit tool arg → dashboard setting → `"global"` (default)
630
+
631
+ **Alternative — hardcode in your startup rules** (if you prefer prompt-level config):
632
+
633
+ ```markdown
634
+ ## Prism MCP Memory Auto-Load (CRITICAL)
635
+ At the start of every new session, call session_load_context with:
636
+ - project: "my-app", role: "dev"
637
+ - project: "my-other-project", role: "dev"
638
+ ```
639
+
640
+ > **Tip:** For true multi-agent setups, each AI instance has its own Mind Palace dashboard — set a different identity per agent there rather than managing it in prompts.
641
+
642
+ ---
643
+
495
644
  ## LangChain / LangGraph Integration
496
645
 
497
646
  Prism MCP includes first-class Python adapters for the LangChain ecosystem, located in `examples/langgraph-agent/`:
@@ -979,14 +1128,19 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
979
1128
 
980
1129
  > **[View the full project board →](https://github.com/users/dcostenco/projects/1/views/1)**
981
1130
 
1131
+ ### ✅ v3.0.1 — Agent Identity & Brain Clean-up (Shipped!)
1132
+
1133
+ See [What's New in v3.0.1](#whats-new-in-v301---agent-identity--brain-clean-up-) above.
1134
+
982
1135
  ### ✅ v3.0 — Agent Hivemind (Shipped!)
983
1136
 
984
- See [What's New in v3.0.0](#whats-new-in-v300---agent-hivemind-) above.
1137
+ See [What's New in v3.0.0 — Agent Hivemind](#whats-new-in-v300---agent-hivemind-) above.
985
1138
 
986
1139
  ### 🚀 Future Ideas
987
1140
 
988
1141
  | Feature | Issue | Description |
989
1142
  |---------|-------|-------------|
1143
+ | **Role-Scoped Skills & Rules** | — | Each agent role (`dev`, `qa`, `pm`, etc.) gets its own persistent skill/rules document. Preloaded automatically at session start via `session_load_context`. Skills editable and uploadable from the Mind Palace Dashboard (⚙️ → Skills tab per role). Stored in `configStorage` per-role key — backend already exists. |
990
1144
  | OpenTelemetry SDK Integration | [#6](https://github.com/dcostenco/prism-mcp/issues/6) | W3C-compliant tracing with Jaeger/Zipkin export |
991
1145
  | GDPR Right to Portability | [#7](https://github.com/dcostenco/prism-mcp/issues/7) | `session_export_memory` tool for Art. 20 compliance |
992
1146
  | Multi-agent CRDT Conflict Resolution | [#9](https://github.com/dcostenco/prism-mcp/issues/9) | Conflict-free replicated data types for concurrent agent edits |
@@ -21,6 +21,8 @@ import { exec } from "child_process";
21
21
  import { getStorage } from "../storage/index.js";
22
22
  import { PRISM_USER_ID, SERVER_CONFIG } from "../config.js";
23
23
  import { renderDashboardHTML } from "./ui.js";
24
+ import { getAllSettings, setSetting, getSetting } from "../storage/configStorage.js";
25
+ import { compactLedgerHandler } from "../tools/compactionHandler.js";
24
26
  const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
25
27
  /** Read HTTP request body as string */
26
28
  function readBody(req) {
@@ -73,9 +75,24 @@ async function killPortHolder(port) {
73
75
  });
74
76
  }
75
77
  export async function startDashboardServer() {
76
- // Clean up any zombie dashboard process from a previous session
77
- await killPortHolder(PORT);
78
- const storage = await getStorage();
78
+ // Fire-and-forget port cleanup don't block server start.
79
+ // Previously awaiting this added 300ms+ delay from lsof + setTimeout,
80
+ // starving the MCP stdio transport during the init handshake.
81
+ killPortHolder(PORT).catch(() => { });
82
+ // Lazy storage accessor — returns null if storage isn't ready yet.
83
+ // API routes gracefully degrade with 503 instead of blocking startup.
84
+ let _storage = null;
85
+ const getStorageSafe = async () => {
86
+ if (_storage)
87
+ return _storage;
88
+ try {
89
+ _storage = await getStorage();
90
+ return _storage;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ };
79
96
  const httpServer = http.createServer(async (req, res) => {
80
97
  // CORS headers for local dev
81
98
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -97,7 +114,12 @@ export async function startDashboardServer() {
97
114
  }
98
115
  // ─── API: List all projects ───
99
116
  if (url.pathname === "/api/projects") {
100
- const projects = await storage.listProjects();
117
+ const s = await getStorageSafe();
118
+ if (!s) {
119
+ res.writeHead(503, { "Content-Type": "application/json" });
120
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
121
+ }
122
+ const projects = await s.listProjects();
101
123
  res.writeHead(200, { "Content-Type": "application/json" });
102
124
  return res.end(JSON.stringify({ projects }));
103
125
  }
@@ -108,15 +130,20 @@ export async function startDashboardServer() {
108
130
  res.writeHead(400, { "Content-Type": "application/json" });
109
131
  return res.end(JSON.stringify({ error: "Missing ?name= parameter" }));
110
132
  }
111
- const context = await storage.loadContext(projectName, "deep", PRISM_USER_ID);
112
- const ledger = await storage.getLedgerEntries({
133
+ const s = await getStorageSafe();
134
+ if (!s) {
135
+ res.writeHead(503, { "Content-Type": "application/json" });
136
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
137
+ }
138
+ const context = await s.loadContext(projectName, "deep", PRISM_USER_ID);
139
+ const ledger = await s.getLedgerEntries({
113
140
  project: `eq.${projectName}`,
114
141
  order: "created_at.desc",
115
142
  limit: "20",
116
143
  });
117
144
  let history = [];
118
145
  try {
119
- history = await storage.getHistory(projectName, PRISM_USER_ID, 10);
146
+ history = await s.getHistory(projectName, PRISM_USER_ID, 10);
120
147
  }
121
148
  catch {
122
149
  // History may not exist for all projects
@@ -125,10 +152,15 @@ export async function startDashboardServer() {
125
152
  return res.end(JSON.stringify({ context, ledger, history }));
126
153
  }
127
154
  // ─── API: Brain Health Check (v2.2.0) ───
128
- if (url.pathname === "/api/health") {
155
+ if (url.pathname === "/api/health" && req.method === "GET") {
129
156
  try {
130
157
  const { runHealthCheck } = await import("../utils/healthCheck.js");
131
- const stats = await storage.getHealthStats(PRISM_USER_ID);
158
+ const s = await getStorageSafe();
159
+ if (!s) {
160
+ res.writeHead(503, { "Content-Type": "application/json" });
161
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
162
+ }
163
+ const stats = await s.getHealthStats(PRISM_USER_ID);
132
164
  const report = runHealthCheck(stats);
133
165
  res.writeHead(200, { "Content-Type": "application/json" });
134
166
  return res.end(JSON.stringify(report));
@@ -146,11 +178,93 @@ export async function startDashboardServer() {
146
178
  }));
147
179
  }
148
180
  }
181
+ // ─── API: Brain Health Cleanup (v3.1) ───
182
+ // Deletes orphaned handoffs (handoffs with no backing ledger entries).
183
+ if (url.pathname === "/api/health/cleanup" && req.method === "POST") {
184
+ try {
185
+ const { runHealthCheck } = await import("../utils/healthCheck.js");
186
+ const s = await getStorageSafe();
187
+ if (!s) {
188
+ res.writeHead(503, { "Content-Type": "application/json" });
189
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
190
+ }
191
+ const stats = await s.getHealthStats(PRISM_USER_ID);
192
+ const report = runHealthCheck(stats);
193
+ // Collect orphaned handoff projects from the health issues
194
+ const orphaned = stats.orphanedHandoffs || [];
195
+ const cleaned = [];
196
+ for (const { project } of orphaned) {
197
+ try {
198
+ await s.deleteHandoff(project, PRISM_USER_ID);
199
+ cleaned.push(project);
200
+ console.error(`[Dashboard] Cleaned up orphaned handoff: ${project}`);
201
+ }
202
+ catch (delErr) {
203
+ console.error(`[Dashboard] Failed to delete handoff for ${project}:`, delErr);
204
+ }
205
+ }
206
+ res.writeHead(200, { "Content-Type": "application/json" });
207
+ return res.end(JSON.stringify({
208
+ ok: true,
209
+ cleaned,
210
+ count: cleaned.length,
211
+ message: cleaned.length > 0
212
+ ? `Cleaned up ${cleaned.length} orphaned handoff(s): ${cleaned.join(", ")}`
213
+ : "No orphaned handoffs to clean up.",
214
+ }));
215
+ }
216
+ catch (err) {
217
+ console.error("[Dashboard] Health cleanup error:", err);
218
+ res.writeHead(500, { "Content-Type": "application/json" });
219
+ return res.end(JSON.stringify({ ok: false, error: "Cleanup failed" }));
220
+ }
221
+ }
222
+ // ─── API: Role-Scoped Skills (v3.1) ───
223
+ // GET /api/skills → { skills: { dev: "...", qa: "..." } }
224
+ if (url.pathname === "/api/skills" && req.method === "GET") {
225
+ const all = await getAllSettings();
226
+ const skills = {};
227
+ for (const [k, v] of Object.entries(all)) {
228
+ if (k.startsWith("skill:") && v) {
229
+ skills[k.replace("skill:", "")] = v;
230
+ }
231
+ }
232
+ res.writeHead(200, { "Content-Type": "application/json" });
233
+ return res.end(JSON.stringify({ skills }));
234
+ }
235
+ // POST /api/skills → { role, content } saves skill:<role>
236
+ if (url.pathname === "/api/skills" && req.method === "POST") {
237
+ const body = await new Promise(resolve => {
238
+ let data = "";
239
+ req.on("data", c => data += c);
240
+ req.on("end", () => resolve(data));
241
+ });
242
+ const { role, content } = JSON.parse(body || "{}");
243
+ if (!role) {
244
+ res.writeHead(400);
245
+ return res.end(JSON.stringify({ error: "role required" }));
246
+ }
247
+ await setSetting(`skill:${role}`, content || "");
248
+ res.writeHead(200, { "Content-Type": "application/json" });
249
+ return res.end(JSON.stringify({ ok: true, role }));
250
+ }
251
+ // DELETE /api/skills/:role → clears skill:<role>
252
+ if (url.pathname.startsWith("/api/skills/") && req.method === "DELETE") {
253
+ const role = url.pathname.replace("/api/skills/", "");
254
+ await setSetting(`skill:${role}`, "");
255
+ res.writeHead(200, { "Content-Type": "application/json" });
256
+ return res.end(JSON.stringify({ ok: true, role }));
257
+ }
149
258
  // ─── API: Knowledge Graph Data (v2.3.0) ───
150
259
  if (url.pathname === "/api/graph") {
151
260
  // Fetch recent ledger entries to build the graph
152
261
  // We look at the last 100 entries to keep the graph relevant but performant
153
- const entries = await storage.getLedgerEntries({
262
+ const s = await getStorageSafe();
263
+ if (!s) {
264
+ res.writeHead(503, { "Content-Type": "application/json" });
265
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
266
+ }
267
+ const entries = await s.getLedgerEntries({
154
268
  limit: "100",
155
269
  order: "created_at.desc",
156
270
  select: "project,keywords",
@@ -216,7 +330,12 @@ export async function startDashboardServer() {
216
330
  return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
217
331
  }
218
332
  try {
219
- const team = await storage.listTeam(projectName, PRISM_USER_ID);
333
+ const s = await getStorageSafe();
334
+ if (!s) {
335
+ res.writeHead(503, { "Content-Type": "application/json" });
336
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
337
+ }
338
+ const team = await s.listTeam(projectName, PRISM_USER_ID);
220
339
  res.writeHead(200, { "Content-Type": "application/json" });
221
340
  return res.end(JSON.stringify({ team }));
222
341
  }
@@ -257,6 +376,167 @@ export async function startDashboardServer() {
257
376
  return res.end(JSON.stringify({ error: "Invalid JSON body" }));
258
377
  }
259
378
  }
379
+ // ─── API: Memory Analytics (v3.1) ────────────────────
380
+ if (url.pathname === "/api/analytics" && req.method === "GET") {
381
+ const projectName = url.searchParams.get("project");
382
+ if (!projectName) {
383
+ res.writeHead(400, { "Content-Type": "application/json" });
384
+ return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
385
+ }
386
+ try {
387
+ const s = await getStorageSafe();
388
+ if (!s) {
389
+ res.writeHead(503, { "Content-Type": "application/json" });
390
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
391
+ }
392
+ const analytics = await s.getAnalytics(projectName, PRISM_USER_ID);
393
+ res.writeHead(200, { "Content-Type": "application/json" });
394
+ return res.end(JSON.stringify(analytics));
395
+ }
396
+ catch (err) {
397
+ console.error("[Dashboard] Analytics error:", err);
398
+ res.writeHead(200, { "Content-Type": "application/json" });
399
+ return res.end(JSON.stringify({
400
+ totalEntries: 0, totalRollups: 0, rollupSavings: 0,
401
+ avgSummaryLength: 0, sessionsByDay: [],
402
+ }));
403
+ }
404
+ }
405
+ // ─── API: Retention (TTL) Settings (v3.1) ──────────────
406
+ // GET /api/retention?project= → current TTL setting
407
+ // POST /api/retention → { project, ttl_days } → saves + runs sweep
408
+ if (url.pathname === "/api/retention") {
409
+ if (req.method === "GET") {
410
+ const projectName = url.searchParams.get("project");
411
+ if (!projectName) {
412
+ res.writeHead(400, { "Content-Type": "application/json" });
413
+ return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
414
+ }
415
+ const ttlRaw = await getSetting(`ttl:${projectName}`, "0");
416
+ res.writeHead(200, { "Content-Type": "application/json" });
417
+ return res.end(JSON.stringify({ project: projectName, ttl_days: parseInt(ttlRaw, 10) || 0 }));
418
+ }
419
+ if (req.method === "POST") {
420
+ const body = await readBody(req);
421
+ const { project, ttl_days } = JSON.parse(body || "{}");
422
+ if (!project || ttl_days === undefined) {
423
+ res.writeHead(400, { "Content-Type": "application/json" });
424
+ return res.end(JSON.stringify({ error: "project and ttl_days required" }));
425
+ }
426
+ if (ttl_days > 0 && ttl_days < 7) {
427
+ res.writeHead(400, { "Content-Type": "application/json" });
428
+ return res.end(JSON.stringify({ error: "Minimum TTL is 7 days" }));
429
+ }
430
+ await setSetting(`ttl:${project}`, String(ttl_days));
431
+ let expired = 0;
432
+ if (ttl_days > 0) {
433
+ const s = await getStorageSafe();
434
+ if (!s) {
435
+ res.writeHead(503, { "Content-Type": "application/json" });
436
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
437
+ }
438
+ const result = await s.expireByTTL(project, ttl_days, PRISM_USER_ID);
439
+ expired = result.expired;
440
+ }
441
+ res.writeHead(200, { "Content-Type": "application/json" });
442
+ return res.end(JSON.stringify({ ok: true, project, ttl_days, expired }));
443
+ }
444
+ }
445
+ // ─── API: Compact Now (v3.1 — Dashboard button) ──────────
446
+ if (url.pathname === "/api/compact" && req.method === "POST") {
447
+ const body = await readBody(req);
448
+ const { project } = JSON.parse(body || "{}");
449
+ if (!project) {
450
+ res.writeHead(400, { "Content-Type": "application/json" });
451
+ return res.end(JSON.stringify({ error: "project required" }));
452
+ }
453
+ try {
454
+ const result = await compactLedgerHandler({ project });
455
+ res.writeHead(200, { "Content-Type": "application/json" });
456
+ return res.end(JSON.stringify({ ok: true, result }));
457
+ }
458
+ catch (err) {
459
+ console.error("[Dashboard] Compact error:", err);
460
+ res.writeHead(500, { "Content-Type": "application/json" });
461
+ return res.end(JSON.stringify({ ok: false, error: "Compaction failed" }));
462
+ }
463
+ }
464
+ // ─── API: PKM Export — Obsidian/Logseq ZIP (v3.1) ──────
465
+ if (url.pathname === "/api/export" && req.method === "GET") {
466
+ const projectName = url.searchParams.get("project");
467
+ if (!projectName) {
468
+ res.writeHead(400, { "Content-Type": "application/json" });
469
+ return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
470
+ }
471
+ try {
472
+ // Lazy-import fflate to keep startup fast
473
+ const { strToU8, zipSync } = await import("fflate");
474
+ // Fetch all active ledger entries for this project
475
+ const s = await getStorageSafe();
476
+ if (!s) {
477
+ res.writeHead(503, { "Content-Type": "application/json" });
478
+ return res.end(JSON.stringify({ error: "Storage initializing..." }));
479
+ }
480
+ const entries = await s.getLedgerEntries({
481
+ project: `eq.${projectName}`,
482
+ order: "created_at.asc",
483
+ limit: "1000",
484
+ });
485
+ const files = {};
486
+ // One MD file per session
487
+ for (const entry of entries) {
488
+ const date = entry.created_at?.slice(0, 10) ?? "unknown";
489
+ const id = entry.id?.slice(0, 8) ?? "xxxxxxxx";
490
+ const filename = `${projectName}/${date}-${id}.md`;
491
+ const todos = Array.isArray(entry.todos) ? entry.todos : [];
492
+ const decisions = Array.isArray(entry.decisions) ? entry.decisions : [];
493
+ const files_changed = Array.isArray(entry.files_changed) ? entry.files_changed : [];
494
+ const tags = (Array.isArray(entry.keywords) ? entry.keywords : []).slice(0, 10);
495
+ const content = [
496
+ `# Session: ${date}`,
497
+ ``,
498
+ `**Project:** ${projectName}`,
499
+ `**Date:** ${date}`,
500
+ `**Role:** ${entry.role || "global"}`,
501
+ tags.length ? `**Tags:** ${tags.map(t => `#${t.replace(/\s+/g, "_")}`).join(" ")}` : "",
502
+ ``,
503
+ `## Summary`,
504
+ ``,
505
+ entry.summary,
506
+ ``,
507
+ todos.length ? `## TODOs\n\n${todos.map(t => `- [ ] ${t}`).join("\n")}` : "",
508
+ decisions.length ? `## Decisions\n\n${decisions.map(d => `- ${d}`).join("\n")}` : "",
509
+ files_changed.length ? `## Files Changed\n\n${files_changed.map(f => `- \`${f}\``).join("\n")}` : "",
510
+ ].filter(Boolean).join("\n");
511
+ files[filename] = strToU8(content);
512
+ }
513
+ // Index file linking all sessions
514
+ const indexLines = [
515
+ `# ${projectName} — Session Index`,
516
+ ``,
517
+ `> Exported from Prism MCP on ${new Date().toISOString().slice(0, 10)}`,
518
+ ``,
519
+ ...entries.map(e => {
520
+ const d = e.created_at?.slice(0, 10) ?? "unknown";
521
+ const i = e.id?.slice(0, 8) ?? "xxxxxxxx";
522
+ return `- [[${projectName}/${d}-${i}]]`;
523
+ }),
524
+ ];
525
+ files[`${projectName}/_index.md`] = strToU8(indexLines.join("\n"));
526
+ const zipped = zipSync(files, { level: 6 });
527
+ res.writeHead(200, {
528
+ "Content-Type": "application/zip",
529
+ "Content-Disposition": `attachment; filename="prism-export-${projectName}-${Date.now()}.zip"`,
530
+ "Content-Length": String(zipped.byteLength),
531
+ });
532
+ return res.end(Buffer.from(zipped));
533
+ }
534
+ catch (err) {
535
+ console.error("[Dashboard] PKM export error:", err);
536
+ res.writeHead(500, { "Content-Type": "application/json" });
537
+ return res.end(JSON.stringify({ error: "Export failed" }));
538
+ }
539
+ }
260
540
  // ─── 404 ───
261
541
  res.writeHead(404, { "Content-Type": "text/plain" });
262
542
  res.end("Not found");
@@ -280,4 +560,31 @@ export async function startDashboardServer() {
280
560
  httpServer.listen(PORT, () => {
281
561
  console.error(`[Prism] 🧠 Mind Palace Dashboard → http://localhost:${PORT}`);
282
562
  });
563
+ // ─── v3.1: TTL Sweep — runs at startup + every 12 hours ───────────
564
+ async function runTtlSweep() {
565
+ try {
566
+ const allSettings = await getAllSettings();
567
+ for (const [key, val] of Object.entries(allSettings)) {
568
+ if (!key.startsWith("ttl:"))
569
+ continue;
570
+ const project = key.replace("ttl:", "");
571
+ const ttlDays = parseInt(val, 10);
572
+ if (!ttlDays || ttlDays <= 0)
573
+ continue;
574
+ const s = await getStorageSafe();
575
+ if (!s)
576
+ continue;
577
+ const result = await s.expireByTTL(project, ttlDays, PRISM_USER_ID);
578
+ if (result.expired > 0) {
579
+ console.error(`[Dashboard] TTL sweep: expired ${result.expired} entries for "${project}" (ttl=${ttlDays}d)`);
580
+ }
581
+ }
582
+ }
583
+ catch (err) {
584
+ console.error("[Dashboard] TTL sweep error (non-fatal):", err);
585
+ }
586
+ }
587
+ // Run immediately on startup, then every 12 hours
588
+ runTtlSweep().catch(() => { });
589
+ setInterval(() => { runTtlSweep().catch(() => { }); }, 12 * 60 * 60 * 1000);
283
590
  }