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 +159 -5
- package/dist/dashboard/server.js +318 -11
- package/dist/dashboard/ui.js +502 -3
- package/dist/server.js +163 -38
- package/dist/storage/configStorage.js +41 -1
- package/dist/storage/sqlite.js +177 -8
- package/dist/storage/supabase.js +64 -0
- package/dist/tools/agentRegistryHandlers.js +80 -32
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +35 -0
- package/dist/tools/sessionMemoryHandlers.js +117 -4
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## Table of Contents
|
|
16
16
|
|
|
17
|
-
- [What's New (v3.
|
|
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.
|
|
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
|

|
|
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 |
|
package/dist/dashboard/server.js
CHANGED
|
@@ -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
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
112
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|