prism-mcp-server 3.0.1 โ 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 +21 -2
- package/dist/dashboard/server.js +247 -13
- package/dist/dashboard/ui.js +211 -1
- package/dist/server.js +123 -29
- package/dist/storage/configStorage.js +41 -1
- package/dist/storage/sqlite.js +165 -8
- package/dist/storage/supabase.js +52 -0
- package/dist/tools/agentRegistryHandlers.js +67 -24
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +35 -0
- package/dist/tools/sessionMemoryHandlers.js +86 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## Table of Contents
|
|
16
16
|
|
|
17
|
-
- [What's New (v3.0
|
|
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)
|
|
@@ -39,7 +39,18 @@
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
## What's New in v3.0
|
|
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>
|
|
43
54
|
|
|
44
55
|
| Feature | Description |
|
|
45
56
|
|---|---|
|
|
@@ -48,6 +59,8 @@
|
|
|
48
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. |
|
|
49
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. |
|
|
50
61
|
|
|
62
|
+
</details>
|
|
63
|
+
|
|
51
64
|
<details>
|
|
52
65
|
<summary><strong>What's in v3.0.0 โ Agent Hivemind ๐</strong></summary>
|
|
53
66
|
|
|
@@ -468,6 +481,12 @@ graph TB
|
|
|
468
481
|
| `session_search_memory` | Vector similarity search across all sessions |
|
|
469
482
|
| `session_compact_ledger` | Auto-compact old ledger entries via Gemini-powered summarization |
|
|
470
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
|
+
|
|
471
490
|
### v2.0 Advanced Memory Tools
|
|
472
491
|
|
|
473
492
|
| Tool | Purpose |
|
package/dist/dashboard/server.js
CHANGED
|
@@ -21,7 +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 } from "../storage/configStorage.js";
|
|
24
|
+
import { getAllSettings, setSetting, getSetting } from "../storage/configStorage.js";
|
|
25
|
+
import { compactLedgerHandler } from "../tools/compactionHandler.js";
|
|
25
26
|
const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
|
|
26
27
|
/** Read HTTP request body as string */
|
|
27
28
|
function readBody(req) {
|
|
@@ -74,9 +75,24 @@ async function killPortHolder(port) {
|
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
export async function startDashboardServer() {
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
};
|
|
80
96
|
const httpServer = http.createServer(async (req, res) => {
|
|
81
97
|
// CORS headers for local dev
|
|
82
98
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -98,7 +114,12 @@ export async function startDashboardServer() {
|
|
|
98
114
|
}
|
|
99
115
|
// โโโ API: List all projects โโโ
|
|
100
116
|
if (url.pathname === "/api/projects") {
|
|
101
|
-
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();
|
|
102
123
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
103
124
|
return res.end(JSON.stringify({ projects }));
|
|
104
125
|
}
|
|
@@ -109,15 +130,20 @@ export async function startDashboardServer() {
|
|
|
109
130
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
110
131
|
return res.end(JSON.stringify({ error: "Missing ?name= parameter" }));
|
|
111
132
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
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({
|
|
114
140
|
project: `eq.${projectName}`,
|
|
115
141
|
order: "created_at.desc",
|
|
116
142
|
limit: "20",
|
|
117
143
|
});
|
|
118
144
|
let history = [];
|
|
119
145
|
try {
|
|
120
|
-
history = await
|
|
146
|
+
history = await s.getHistory(projectName, PRISM_USER_ID, 10);
|
|
121
147
|
}
|
|
122
148
|
catch {
|
|
123
149
|
// History may not exist for all projects
|
|
@@ -129,7 +155,12 @@ export async function startDashboardServer() {
|
|
|
129
155
|
if (url.pathname === "/api/health" && req.method === "GET") {
|
|
130
156
|
try {
|
|
131
157
|
const { runHealthCheck } = await import("../utils/healthCheck.js");
|
|
132
|
-
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);
|
|
133
164
|
const report = runHealthCheck(stats);
|
|
134
165
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
135
166
|
return res.end(JSON.stringify(report));
|
|
@@ -152,14 +183,19 @@ export async function startDashboardServer() {
|
|
|
152
183
|
if (url.pathname === "/api/health/cleanup" && req.method === "POST") {
|
|
153
184
|
try {
|
|
154
185
|
const { runHealthCheck } = await import("../utils/healthCheck.js");
|
|
155
|
-
const
|
|
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);
|
|
156
192
|
const report = runHealthCheck(stats);
|
|
157
193
|
// Collect orphaned handoff projects from the health issues
|
|
158
194
|
const orphaned = stats.orphanedHandoffs || [];
|
|
159
195
|
const cleaned = [];
|
|
160
196
|
for (const { project } of orphaned) {
|
|
161
197
|
try {
|
|
162
|
-
await
|
|
198
|
+
await s.deleteHandoff(project, PRISM_USER_ID);
|
|
163
199
|
cleaned.push(project);
|
|
164
200
|
console.error(`[Dashboard] Cleaned up orphaned handoff: ${project}`);
|
|
165
201
|
}
|
|
@@ -223,7 +259,12 @@ export async function startDashboardServer() {
|
|
|
223
259
|
if (url.pathname === "/api/graph") {
|
|
224
260
|
// Fetch recent ledger entries to build the graph
|
|
225
261
|
// We look at the last 100 entries to keep the graph relevant but performant
|
|
226
|
-
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({
|
|
227
268
|
limit: "100",
|
|
228
269
|
order: "created_at.desc",
|
|
229
270
|
select: "project,keywords",
|
|
@@ -289,7 +330,12 @@ export async function startDashboardServer() {
|
|
|
289
330
|
return res.end(JSON.stringify({ error: "Missing ?project= parameter" }));
|
|
290
331
|
}
|
|
291
332
|
try {
|
|
292
|
-
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);
|
|
293
339
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
294
340
|
return res.end(JSON.stringify({ team }));
|
|
295
341
|
}
|
|
@@ -330,6 +376,167 @@ export async function startDashboardServer() {
|
|
|
330
376
|
return res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
331
377
|
}
|
|
332
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
|
+
}
|
|
333
540
|
// โโโ 404 โโโ
|
|
334
541
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
335
542
|
res.end("Not found");
|
|
@@ -353,4 +560,31 @@ export async function startDashboardServer() {
|
|
|
353
560
|
httpServer.listen(PORT, () => {
|
|
354
561
|
console.error(`[Prism] ๐ง Mind Palace Dashboard โ http://localhost:${PORT}`);
|
|
355
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);
|
|
356
590
|
}
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -444,6 +444,54 @@ export function renderDashboardHTML(version) {
|
|
|
444
444
|
flex-shrink: 0; animation: pulseDot 2s ease-in-out infinite;
|
|
445
445
|
}
|
|
446
446
|
@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
447
|
+
|
|
448
|
+
/* โโโ Memory Analytics (v3.1) โโโ */
|
|
449
|
+
.sparkline {
|
|
450
|
+
display: flex; align-items: flex-end; gap: 3px;
|
|
451
|
+
height: 48px; margin: 0.75rem 0 0.25rem;
|
|
452
|
+
}
|
|
453
|
+
.spark-bar {
|
|
454
|
+
flex: 1; background: rgba(139,92,246,0.35);
|
|
455
|
+
border-radius: 3px 3px 0 0; min-height: 3px;
|
|
456
|
+
transition: background 0.2s;
|
|
457
|
+
}
|
|
458
|
+
.spark-bar:hover { background: var(--accent-purple); }
|
|
459
|
+
.analytics-stats {
|
|
460
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
|
|
461
|
+
margin-top: 0.75rem;
|
|
462
|
+
}
|
|
463
|
+
.astat {
|
|
464
|
+
background: rgba(15,23,42,0.5); border-radius: var(--radius-sm);
|
|
465
|
+
padding: 0.6rem 0.75rem; display: flex; flex-direction: column; gap: 0.15rem;
|
|
466
|
+
}
|
|
467
|
+
.astat-val { font-size: 1.1rem; font-weight: 700; color: var(--accent-purple); font-family: var(--font-mono); }
|
|
468
|
+
.astat-label { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
469
|
+
|
|
470
|
+
/* โโโ Lifecycle Controls (v3.1) โโโ */
|
|
471
|
+
.lc-row { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; align-items: center; }
|
|
472
|
+
.lc-btn {
|
|
473
|
+
flex: 1; padding: 0.5rem 0.6rem; font-size: 0.8rem; font-weight: 600;
|
|
474
|
+
border-radius: var(--radius-sm); border: none; cursor: pointer;
|
|
475
|
+
transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 0.4rem;
|
|
476
|
+
}
|
|
477
|
+
.lc-btn.compact { background: rgba(139,92,246,0.15); color: var(--accent-purple); border: 1px solid rgba(139,92,246,0.3); }
|
|
478
|
+
.lc-btn.compact:hover { background: rgba(139,92,246,0.3); }
|
|
479
|
+
.lc-btn.export { background: rgba(16,185,129,0.12); color: var(--accent-green); border: 1px solid rgba(16,185,129,0.3); }
|
|
480
|
+
.lc-btn.export:hover { background: rgba(16,185,129,0.25); }
|
|
481
|
+
.lc-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
482
|
+
.ttl-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; }
|
|
483
|
+
.ttl-input {
|
|
484
|
+
width: 70px; background: var(--bg-secondary); border: 1px solid var(--border-glass);
|
|
485
|
+
color: var(--text-primary); border-radius: 6px; padding: 0.3rem 0.5rem;
|
|
486
|
+
font-size: 0.82rem; font-family: var(--font-mono); text-align: center;
|
|
487
|
+
}
|
|
488
|
+
.ttl-label { font-size: 0.8rem; color: var(--text-secondary); }
|
|
489
|
+
.ttl-save-btn {
|
|
490
|
+
margin-left: auto; padding: 0.3rem 0.75rem; font-size: 0.78rem; font-weight: 600;
|
|
491
|
+
background: rgba(245,158,11,0.15); color: var(--accent-amber);
|
|
492
|
+
border: 1px solid rgba(245,158,11,0.3); border-radius: 6px; cursor: pointer; transition: all 0.2s;
|
|
493
|
+
}
|
|
494
|
+
.ttl-save-btn:hover { background: rgba(245,158,11,0.3); }
|
|
447
495
|
</style>
|
|
448
496
|
</head>
|
|
449
497
|
<body>
|
|
@@ -509,7 +557,42 @@ export function renderDashboardHTML(version) {
|
|
|
509
557
|
<div class="health-issues" id="healthIssues"></div>
|
|
510
558
|
</div>
|
|
511
559
|
|
|
512
|
-
<!--
|
|
560
|
+
<!-- Memory Analytics (v3.1) -->
|
|
561
|
+
<div class="card" id="analyticsCard" style="display:none">
|
|
562
|
+
<div class="card-title">
|
|
563
|
+
<span class="dot" style="background:var(--accent-purple)"></span>
|
|
564
|
+
Memory Analytics ๐
|
|
565
|
+
</div>
|
|
566
|
+
<div class="sparkline" id="sparkline" title="Sessions per day (last 14 days)"></div>
|
|
567
|
+
<div style="font-size:0.68rem;color:var(--text-muted);text-align:right">Sessions / day (14d)</div>
|
|
568
|
+
<div class="analytics-stats">
|
|
569
|
+
<div class="astat"><div class="astat-val" id="astat-entries">โ</div><div class="astat-label">Active sessions</div></div>
|
|
570
|
+
<div class="astat"><div class="astat-val" id="astat-rollups">โ</div><div class="astat-label">Rollups</div></div>
|
|
571
|
+
<div class="astat"><div class="astat-val" id="astat-savings">โ</div><div class="astat-label">Entries saved</div></div>
|
|
572
|
+
<div class="astat"><div class="astat-val" id="astat-avglen">โ</div><div class="astat-label">Avg summary chars</div></div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<!-- Lifecycle Controls (v3.1) -->
|
|
577
|
+
<div class="card" id="lifecycleCard" style="display:none">
|
|
578
|
+
<div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Lifecycle Controls โ๏ธ</div>
|
|
579
|
+
<div class="lc-row">
|
|
580
|
+
<button class="lc-btn compact" id="compactBtn" onclick="compactNow()">
|
|
581
|
+
๐๏ธ Compact Now
|
|
582
|
+
</button>
|
|
583
|
+
<button class="lc-btn export" id="exportBtn" onclick="exportPKM()">
|
|
584
|
+
๐ฆ Export ZIP
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="ttl-row">
|
|
588
|
+
<span class="ttl-label">Auto-expire after</span>
|
|
589
|
+
<input type="number" class="ttl-input" id="ttlInput" min="0" max="3650" placeholder="0" title="Days. 0 = disabled">
|
|
590
|
+
<span class="ttl-label">days</span>
|
|
591
|
+
<button class="ttl-save-btn" onclick="saveTTL()">Save TTL</button>
|
|
592
|
+
</div>
|
|
593
|
+
<div style="font-size:0.7rem;color:var(--text-muted);margin-top:0.4rem">0 = disabled. Min 7 days. Rollups are never expired.</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
513
596
|
<div class="card" id="briefingCard" style="display:none">
|
|
514
597
|
<div class="card-title"><span class="dot" style="background:var(--accent-amber)"></span> Morning Briefing ๐
</div>
|
|
515
598
|
<div class="briefing-text" id="briefingText"></div>
|
|
@@ -894,6 +977,13 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
894
977
|
|
|
895
978
|
document.getElementById('content').className = 'grid grid-main fade-in';
|
|
896
979
|
document.getElementById('content').style.display = 'grid';
|
|
980
|
+
|
|
981
|
+
// v3.1: Analytics + Lifecycle Controls
|
|
982
|
+
document.getElementById('analyticsCard').style.display = 'block';
|
|
983
|
+
document.getElementById('lifecycleCard').style.display = 'block';
|
|
984
|
+
loadAnalytics(project);
|
|
985
|
+
loadRetention(project);
|
|
986
|
+
|
|
897
987
|
loadTeam(); // v3.0: auto-load Hivemind team
|
|
898
988
|
} catch(e) {
|
|
899
989
|
alert('Failed to load project data: ' + e.message);
|
|
@@ -902,6 +992,126 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
902
992
|
}
|
|
903
993
|
}
|
|
904
994
|
|
|
995
|
+
// โโโ v3.1: Memory Analytics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
996
|
+
async function loadAnalytics(project) {
|
|
997
|
+
try {
|
|
998
|
+
var res = await fetch('/api/analytics?project=' + encodeURIComponent(project));
|
|
999
|
+
var d = await res.json();
|
|
1000
|
+
|
|
1001
|
+
document.getElementById('astat-entries').textContent = (d.totalEntries || 0);
|
|
1002
|
+
document.getElementById('astat-rollups').textContent = (d.totalRollups || 0);
|
|
1003
|
+
document.getElementById('astat-savings').textContent = (d.rollupSavings || 0);
|
|
1004
|
+
document.getElementById('astat-avglen').textContent = Math.round(d.avgSummaryLength || 0);
|
|
1005
|
+
|
|
1006
|
+
// Sparkline
|
|
1007
|
+
var sparkEl = document.getElementById('sparkline');
|
|
1008
|
+
var days = d.sessionsByDay || [];
|
|
1009
|
+
if (days.length === 0) {
|
|
1010
|
+
// Pad with 14 zero days
|
|
1011
|
+
days = Array.from({length:14}, function(_, i) {
|
|
1012
|
+
var dt = new Date(); dt.setDate(dt.getDate() - (13 - i));
|
|
1013
|
+
return { date: dt.toISOString().slice(0,10), count: 0 };
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
var maxCount = Math.max.apply(null, days.map(function(x){return x.count || 0;})) || 1;
|
|
1017
|
+
sparkEl.innerHTML = days.slice(-14).map(function(d) {
|
|
1018
|
+
var pct = Math.max(4, Math.round(((d.count || 0) / maxCount) * 100));
|
|
1019
|
+
return '<div class="spark-bar" style="height:' + pct + '%" title="' + d.date + ': ' + d.count + '"></div>';
|
|
1020
|
+
}).join('');
|
|
1021
|
+
} catch(e) {
|
|
1022
|
+
console.warn('Analytics load failed:', e);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// โโโ v3.1: TTL Retention โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1027
|
+
async function loadRetention(project) {
|
|
1028
|
+
try {
|
|
1029
|
+
var res = await fetch('/api/retention?project=' + encodeURIComponent(project));
|
|
1030
|
+
var d = await res.json();
|
|
1031
|
+
var inp = document.getElementById('ttlInput');
|
|
1032
|
+
if (inp) inp.value = d.ttl_days || 0;
|
|
1033
|
+
} catch(e) {}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function saveTTL() {
|
|
1037
|
+
var project = document.getElementById('projectSelect').value;
|
|
1038
|
+
if (!project) return;
|
|
1039
|
+
var days = parseInt(document.getElementById('ttlInput').value, 10) || 0;
|
|
1040
|
+
try {
|
|
1041
|
+
var res = await fetch('/api/retention', {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: {'Content-Type':'application/json'},
|
|
1044
|
+
body: JSON.stringify({ project, ttl_days: days })
|
|
1045
|
+
});
|
|
1046
|
+
var d = await res.json();
|
|
1047
|
+
if (d.ok) {
|
|
1048
|
+
showToast(days > 0 ? 'โ TTL saved: ' + days + 'd (expired ' + (d.expired || 0) + ')' : 'โ TTL disabled');
|
|
1049
|
+
} else {
|
|
1050
|
+
showToast('โ ' + (d.error || 'Save failed'), true);
|
|
1051
|
+
}
|
|
1052
|
+
} catch(e) { showToast('โ Cannot save TTL', true); }
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// โโโ v3.1: Compact Now โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1056
|
+
async function compactNow() {
|
|
1057
|
+
var project = document.getElementById('projectSelect').value;
|
|
1058
|
+
if (!project) return;
|
|
1059
|
+
var btn = document.getElementById('compactBtn');
|
|
1060
|
+
btn.disabled = true;
|
|
1061
|
+
btn.textContent = '๐๏ธ Compacting...';
|
|
1062
|
+
try {
|
|
1063
|
+
var res = await fetch('/api/compact', {
|
|
1064
|
+
method: 'POST',
|
|
1065
|
+
headers: {'Content-Type':'application/json'},
|
|
1066
|
+
body: JSON.stringify({ project })
|
|
1067
|
+
});
|
|
1068
|
+
var d = await res.json();
|
|
1069
|
+
if (d.ok) {
|
|
1070
|
+
showToast('โ Compaction done');
|
|
1071
|
+
loadAnalytics(project); // refresh stats
|
|
1072
|
+
} else {
|
|
1073
|
+
showToast('โ Compaction failed', true);
|
|
1074
|
+
}
|
|
1075
|
+
} catch(e) { showToast('โ ' + e.message, true); }
|
|
1076
|
+
finally {
|
|
1077
|
+
btn.disabled = false;
|
|
1078
|
+
btn.textContent = '๐๏ธ Compact Now';
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// โโโ v3.1: PKM Export (Obsidian / Logseq ZIP) โโโโโโโโโโโโโโโโโโโโโโโ
|
|
1083
|
+
async function exportPKM() {
|
|
1084
|
+
var project = document.getElementById('projectSelect').value;
|
|
1085
|
+
if (!project) return;
|
|
1086
|
+
var btn = document.getElementById('exportBtn');
|
|
1087
|
+
btn.disabled = true;
|
|
1088
|
+
btn.textContent = '๐ฆ Exporting...';
|
|
1089
|
+
try {
|
|
1090
|
+
var a = document.createElement('a');
|
|
1091
|
+
a.href = '/api/export?project=' + encodeURIComponent(project);
|
|
1092
|
+
a.download = 'prism-export-' + project + '.zip';
|
|
1093
|
+
document.body.appendChild(a);
|
|
1094
|
+
a.click();
|
|
1095
|
+
document.body.removeChild(a);
|
|
1096
|
+
showToast('โ Download started');
|
|
1097
|
+
} catch(e) { showToast('โ Export failed', true); }
|
|
1098
|
+
finally {
|
|
1099
|
+
btn.disabled = false;
|
|
1100
|
+
btn.textContent = '๐ฆ Export ZIP';
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function showToast(msg, isErr) {
|
|
1105
|
+
var el = document.getElementById('fixedToast');
|
|
1106
|
+
if (!el) return;
|
|
1107
|
+
el.textContent = msg;
|
|
1108
|
+
el.style.borderColor = isErr ? 'rgba(244,63,94,0.4)' : 'var(--border-glow)';
|
|
1109
|
+
el.style.color = isErr ? 'var(--accent-rose)' : 'var(--text-primary)';
|
|
1110
|
+
el.classList.add('show');
|
|
1111
|
+
clearTimeout(el._t);
|
|
1112
|
+
el._t = setTimeout(function(){ el.classList.remove('show'); }, 3000);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
905
1115
|
function escapeHtml(str) {
|
|
906
1116
|
if (!str) return '';
|
|
907
1117
|
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|