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
|
@@ -8,6 +8,55 @@ import { getStorage } from "../storage/index.js";
|
|
|
8
8
|
import { PRISM_USER_ID } from "../config.js";
|
|
9
9
|
import { getRoleIcon } from "./agentRegistryDefinitions.js";
|
|
10
10
|
import { getSetting } from "../storage/configStorage.js";
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Escape markdown metacharacters in user-controlled strings.
|
|
14
|
+
*
|
|
15
|
+
* Fields like `current_task`, `agent_name`, and `status` are user-provided
|
|
16
|
+
* and may contain characters (* _ ` [ etc.) that would corrupt the markdown
|
|
17
|
+
* formatting of agent_list_team output. We escape the most common ones to
|
|
18
|
+
* keep the display predictable without being overly aggressive.
|
|
19
|
+
*
|
|
20
|
+
* This is a display-integrity fix, not a security fix — MCP response text
|
|
21
|
+
* is rendered in the LLM's context where injection risk is low, but broken
|
|
22
|
+
* formatting reduces readability and can misguide the model.
|
|
23
|
+
*/
|
|
24
|
+
function escapeMd(str) {
|
|
25
|
+
if (!str)
|
|
26
|
+
return "";
|
|
27
|
+
return str
|
|
28
|
+
.replace(/\\/g, "\\\\")
|
|
29
|
+
.replace(/\*/g, "\\*")
|
|
30
|
+
.replace(/_/g, "\\_")
|
|
31
|
+
.replace(/`/g, "\\`")
|
|
32
|
+
.replace(/\[/g, "\\[")
|
|
33
|
+
.replace(/\]/g, "\\]")
|
|
34
|
+
// HTML angle-bracket escaping: prevents raw tags (e.g. <script> in agent
|
|
35
|
+
// names) from bleeding into LLM context as markup.
|
|
36
|
+
.replace(/</g, "<")
|
|
37
|
+
.replace(/>/g, ">");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns a human-readable "time ago" string for a heartbeat timestamp.
|
|
41
|
+
*
|
|
42
|
+
* Clamped to prevent negative or NaN outputs due to:
|
|
43
|
+
* - Clock skew between agents (heartbeat appears in the future)
|
|
44
|
+
* - Malformed ISO strings that produce NaN from Date.parse()
|
|
45
|
+
* Fallback returns "just now" to avoid exposing confusing negative values.
|
|
46
|
+
*/
|
|
47
|
+
function getTimeAgo(isoString) {
|
|
48
|
+
const parsed = new Date(isoString).getTime();
|
|
49
|
+
if (isNaN(parsed))
|
|
50
|
+
return "unknown time"; // malformed timestamp
|
|
51
|
+
const diffMs = Date.now() - parsed;
|
|
52
|
+
const mins = Math.max(0, Math.floor(diffMs / 60000)); // clamp at 0 (no "future")
|
|
53
|
+
if (mins < 1)
|
|
54
|
+
return "just now";
|
|
55
|
+
if (mins < 60)
|
|
56
|
+
return `${mins}m ago`;
|
|
57
|
+
const hours = Math.floor(mins / 60);
|
|
58
|
+
return `${hours}h ago`;
|
|
59
|
+
}
|
|
11
60
|
// ─── Type Guards ─────────────────────────────────────────────
|
|
12
61
|
function isAgentRegisterArgs(args) {
|
|
13
62
|
return typeof args.project === "string";
|
|
@@ -30,7 +79,10 @@ export async function agentRegisterHandler(args) {
|
|
|
30
79
|
const effectiveRole = args.role || await getSetting("default_role", "global");
|
|
31
80
|
const effectiveName = args.agent_name || await getSetting("agent_name", "") || null;
|
|
32
81
|
const storage = await getStorage();
|
|
33
|
-
|
|
82
|
+
// Register the agent. We don't use the return value here — the storage
|
|
83
|
+
// layer handles upsert internally and there are no caller-visible fields
|
|
84
|
+
// (e.g. server-assigned IDs) that we need to surface in the response.
|
|
85
|
+
await storage.registerAgent({
|
|
34
86
|
project: args.project,
|
|
35
87
|
user_id: PRISM_USER_ID,
|
|
36
88
|
role: effectiveRole,
|
|
@@ -43,10 +95,10 @@ export async function agentRegisterHandler(args) {
|
|
|
43
95
|
content: [{
|
|
44
96
|
type: "text",
|
|
45
97
|
text: `${icon} **Agent Registered**\n\n` +
|
|
46
|
-
`- **Project:** ${args.project}\n` +
|
|
47
|
-
`- **Role:** ${effectiveRole}\n` +
|
|
48
|
-
(effectiveName ? `- **Name:** ${effectiveName}\n` : "") +
|
|
49
|
-
(args.current_task ? `- **Task:** ${args.current_task}\n` : "") +
|
|
98
|
+
`- **Project:** ${escapeMd(args.project)}\n` +
|
|
99
|
+
`- **Role:** ${escapeMd(effectiveRole)}\n` +
|
|
100
|
+
(effectiveName ? `- **Name:** ${escapeMd(effectiveName)}\n` : "") +
|
|
101
|
+
(args.current_task ? `- **Task:** ${escapeMd(args.current_task)}\n` : "") +
|
|
50
102
|
`\nOther agents will see you when they call \`agent_list_team\` or \`session_load_context\`.`,
|
|
51
103
|
}],
|
|
52
104
|
};
|
|
@@ -64,8 +116,8 @@ export async function agentHeartbeatHandler(args) {
|
|
|
64
116
|
return {
|
|
65
117
|
content: [{
|
|
66
118
|
type: "text",
|
|
67
|
-
text: `💓 Heartbeat updated for **${effectiveRole}** on \`${args.project}\`.` +
|
|
68
|
-
(args.current_task ? ` Task: ${args.current_task}` : ""),
|
|
119
|
+
text: `💓 Heartbeat updated for **${escapeMd(effectiveRole)}** on \`${escapeMd(args.project)}\`.` +
|
|
120
|
+
(args.current_task ? ` Task: ${escapeMd(args.current_task)}` : ""),
|
|
69
121
|
}],
|
|
70
122
|
};
|
|
71
123
|
}
|
|
@@ -82,7 +134,7 @@ export async function agentListTeamHandler(args) {
|
|
|
82
134
|
return {
|
|
83
135
|
content: [{
|
|
84
136
|
type: "text",
|
|
85
|
-
text: `No active agents on \`${args.project}\`. Use \`agent_register\` to join the team.`,
|
|
137
|
+
text: `No active agents on \`${escapeMd(args.project)}\`. Use \`agent_register\` to join the team.`,
|
|
86
138
|
}],
|
|
87
139
|
};
|
|
88
140
|
}
|
|
@@ -91,29 +143,20 @@ export async function agentListTeamHandler(args) {
|
|
|
91
143
|
const ago = agent.last_heartbeat
|
|
92
144
|
? getTimeAgo(agent.last_heartbeat)
|
|
93
145
|
: "unknown";
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(agent.
|
|
146
|
+
// escapeMd() applied to all user-controlled fields to prevent
|
|
147
|
+
// markdown metacharacters in task descriptions from corrupting output
|
|
148
|
+
return (`${icon} **${escapeMd(agent.role)}**` +
|
|
149
|
+
(agent.agent_name ? ` (${escapeMd(agent.agent_name)})` : "") +
|
|
150
|
+
` — ${escapeMd(agent.status)}` +
|
|
151
|
+
(agent.current_task ? ` | Task: ${escapeMd(agent.current_task)}` : "") +
|
|
98
152
|
` | Last seen: ${ago}`);
|
|
99
153
|
});
|
|
100
154
|
return {
|
|
101
155
|
content: [{
|
|
102
156
|
type: "text",
|
|
103
|
-
text: `## 🐝 Active Hivemind Team — \`${args.project}\`\n\n` +
|
|
157
|
+
text: `## 🐝 Active Hivemind Team — \`${escapeMd(args.project)}\`\n\n` +
|
|
104
158
|
lines.join("\n") +
|
|
105
159
|
`\n\n_${team.length} agent(s) active. Stale agents (>30min) auto-pruned._`,
|
|
106
160
|
}],
|
|
107
161
|
};
|
|
108
162
|
}
|
|
109
|
-
// ─── Helpers ─────────────────────────────────────────────────
|
|
110
|
-
function getTimeAgo(isoString) {
|
|
111
|
-
const diff = Date.now() - new Date(isoString).getTime();
|
|
112
|
-
const mins = Math.floor(diff / 60000);
|
|
113
|
-
if (mins < 1)
|
|
114
|
-
return "just now";
|
|
115
|
-
if (mins < 60)
|
|
116
|
-
return `${mins}m ago`;
|
|
117
|
-
const hours = Math.floor(mins / 60);
|
|
118
|
-
return `${hours}h ago`;
|
|
119
|
-
}
|
package/dist/tools/index.js
CHANGED
|
@@ -26,8 +26,8 @@ export { webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, br
|
|
|
26
26
|
// This file always exports them — server.ts decides whether to include them in the tool list.
|
|
27
27
|
//
|
|
28
28
|
// v0.4.0: Added SESSION_COMPACT_LEDGER_TOOL and SESSION_SEARCH_MEMORY_TOOL
|
|
29
|
-
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler } from "./sessionMemoryHandlers.js";
|
|
29
|
+
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler } from "./sessionMemoryHandlers.js";
|
|
31
31
|
// ── Compaction Handler (v0.4.0 — Enhancement #2) ──
|
|
32
32
|
// The compaction handler is in a separate file because it's significantly
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
@@ -600,3 +600,38 @@ export function isSessionForgetMemoryArgs(args) {
|
|
|
600
600
|
"memory_id" in args &&
|
|
601
601
|
typeof args.memory_id === "string");
|
|
602
602
|
}
|
|
603
|
+
// ─── v3.1: Knowledge Set Retention (TTL) ─────────────────────
|
|
604
|
+
export const KNOWLEDGE_SET_RETENTION_TOOL = {
|
|
605
|
+
name: "knowledge_set_retention",
|
|
606
|
+
description: "Set an automatic data retention policy (TTL) for a project's memory. " +
|
|
607
|
+
"Entries older than ttl_days will be soft-deleted (archived) automatically " +
|
|
608
|
+
"on every server startup and every 12 hours while running.\n\n" +
|
|
609
|
+
"**Use cases:**\n" +
|
|
610
|
+
"- Set `ttl_days: 90` to auto-expire sessions older than 3 months\n" +
|
|
611
|
+
"- Set `ttl_days: 0` to disable auto-expiry (default)\n\n" +
|
|
612
|
+
"**Note:** Rollup/compaction entries are never expired — only raw sessions.",
|
|
613
|
+
inputSchema: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
project: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "Project to set retention policy for.",
|
|
619
|
+
},
|
|
620
|
+
ttl_days: {
|
|
621
|
+
type: "integer",
|
|
622
|
+
description: "Entries older than this many days are auto-expired. " +
|
|
623
|
+
"Set to 0 to disable. Minimum: 7 days when enabled.",
|
|
624
|
+
minimum: 0,
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
required: ["project", "ttl_days"],
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
export function isKnowledgeSetRetentionArgs(args) {
|
|
631
|
+
return (typeof args === "object" &&
|
|
632
|
+
args !== null &&
|
|
633
|
+
"project" in args &&
|
|
634
|
+
typeof args.project === "string" &&
|
|
635
|
+
"ttl_days" in args &&
|
|
636
|
+
typeof args.ttl_days === "number");
|
|
637
|
+
}
|
|
@@ -32,7 +32,12 @@ import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS
|
|
|
32
32
|
import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
33
33
|
import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isKnowledgeSearchArgs, isKnowledgeForgetArgs, isSessionSearchMemoryArgs, isBackfillEmbeddingsArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, isSessionHealthCheckArgs, // v2.2.0: health check type guard
|
|
34
34
|
isSessionForgetMemoryArgs, // Phase 2: GDPR-compliant memory deletion type guard
|
|
35
|
+
isKnowledgeSetRetentionArgs, // v3.1: TTL retention policy type guard
|
|
35
36
|
} from "./sessionMemoryDefinitions.js";
|
|
37
|
+
// v3.1: In-memory debounce lock for auto-compaction.
|
|
38
|
+
// Prevents multiple concurrent Gemini compaction tasks for the same project
|
|
39
|
+
// when many agents call session_save_ledger at the same time.
|
|
40
|
+
const activeCompactions = new Set();
|
|
36
41
|
import { notifyResourceUpdate } from "../server.js";
|
|
37
42
|
// ─── Save Ledger Handler ──────────────────────────────────────
|
|
38
43
|
/**
|
|
@@ -85,6 +90,36 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
}
|
|
93
|
+
// ─── Fire-and-forget auto-compact ────────────────────────────
|
|
94
|
+
// If the user has opted into auto-compact (via dashboard Settings → Boot),
|
|
95
|
+
// run a health check after saving and compact if brain is degraded/unhealthy.
|
|
96
|
+
// Uses debounce Set to prevent concurrent Gemini calls for same project.
|
|
97
|
+
getSetting("compaction_auto", "false").then(async (autoCompact) => {
|
|
98
|
+
if (autoCompact !== "true")
|
|
99
|
+
return;
|
|
100
|
+
if (activeCompactions.has(project)) {
|
|
101
|
+
debugLog(`[auto-compact] Skipped for "${project}" — compaction already in progress`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
activeCompactions.add(project);
|
|
105
|
+
try {
|
|
106
|
+
const { runHealthCheck } = await import("../utils/healthCheck.js");
|
|
107
|
+
const { compactLedgerHandler } = await import("./compactionHandler.js");
|
|
108
|
+
const healthStats = await storage.getHealthStats(PRISM_USER_ID);
|
|
109
|
+
const report = runHealthCheck(healthStats);
|
|
110
|
+
if (report.status === "degraded" || report.status === "unhealthy") {
|
|
111
|
+
debugLog(`[auto-compact] Brain "${project}" is ${report.status} — triggering compaction`);
|
|
112
|
+
await compactLedgerHandler({ project });
|
|
113
|
+
debugLog(`[auto-compact] Compaction complete for "${project}"`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error(`[auto-compact] Non-fatal error for "${project}": ${err}`);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
activeCompactions.delete(project);
|
|
121
|
+
}
|
|
122
|
+
}).catch(() => { });
|
|
88
123
|
return {
|
|
89
124
|
content: [{
|
|
90
125
|
type: "text",
|
|
@@ -1467,3 +1502,54 @@ export async function sessionForgetMemoryHandler(args) {
|
|
|
1467
1502
|
};
|
|
1468
1503
|
}
|
|
1469
1504
|
}
|
|
1505
|
+
// ─── v3.1: Knowledge Set Retention Handler ────────────────
|
|
1506
|
+
/**
|
|
1507
|
+
* Set a TTL (data retention policy) for a project.
|
|
1508
|
+
* Saves the policy to configStorage, then immediately runs one sweep
|
|
1509
|
+
* to expire any entries that are already over the TTL.
|
|
1510
|
+
*/
|
|
1511
|
+
export async function knowledgeSetRetentionHandler(args) {
|
|
1512
|
+
if (!isKnowledgeSetRetentionArgs(args)) {
|
|
1513
|
+
throw new Error("Invalid arguments for knowledge_set_retention");
|
|
1514
|
+
}
|
|
1515
|
+
const { project, ttl_days } = args;
|
|
1516
|
+
if (ttl_days < 0) {
|
|
1517
|
+
return {
|
|
1518
|
+
content: [{ type: "text", text: "Error: ttl_days must be 0 (disabled) or a positive integer." }],
|
|
1519
|
+
isError: true,
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
if (ttl_days > 0 && ttl_days < 7) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [{ type: "text", text: "Error: Minimum TTL is 7 days to prevent accidental data loss." }],
|
|
1525
|
+
isError: true,
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
const storage = await getStorage();
|
|
1529
|
+
// Save policy to configStorage so server.ts sweep can read it
|
|
1530
|
+
await storage.setSetting(`ttl:${project}`, String(ttl_days));
|
|
1531
|
+
if (ttl_days === 0) {
|
|
1532
|
+
return {
|
|
1533
|
+
content: [{
|
|
1534
|
+
type: "text",
|
|
1535
|
+
text: `✅ Data retention **disabled** for project \"${project}\".\n\nEntries will be kept indefinitely.`,
|
|
1536
|
+
}],
|
|
1537
|
+
isError: false,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
// Run an immediate sweep for entries already past TTL
|
|
1541
|
+
const result = await storage.expireByTTL(project, ttl_days, PRISM_USER_ID);
|
|
1542
|
+
return {
|
|
1543
|
+
content: [{
|
|
1544
|
+
type: "text",
|
|
1545
|
+
text: `⏱️ **Retention policy set** for project \"${project}\":\n\n` +
|
|
1546
|
+
`- Auto-expire entries older than: **${ttl_days} days**\n` +
|
|
1547
|
+
`- Sweep runs on: server startup + every 12 hours\n` +
|
|
1548
|
+
`- Rollup/compaction entries: **never expired**\n\n` +
|
|
1549
|
+
(result.expired > 0
|
|
1550
|
+
? `🗑️ Immediately expired **${result.expired}** entries already past the ${ttl_days}-day threshold.`
|
|
1551
|
+
: `✅ No existing entries exceeded the ${ttl_days}-day threshold.`),
|
|
1552
|
+
}],
|
|
1553
|
+
isError: false,
|
|
1554
|
+
};
|
|
1555
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-mcp",
|
|
5
5
|
"description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
88
88
|
"@supabase/supabase-js": "^2.99.3",
|
|
89
89
|
"dotenv": "^16.5.0",
|
|
90
|
+
"fflate": "^0.8.2",
|
|
90
91
|
"quickjs-emscripten": "^0.32.0"
|
|
91
92
|
}
|
|
92
93
|
}
|