prism-mcp-server 4.0.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -78
- package/dist/config.js +8 -0
- package/dist/dashboard/ui.js +120 -0
- package/dist/lifecycle.js +164 -0
- package/dist/server.js +108 -49
- package/dist/storage/configStorage.js +44 -11
- package/dist/storage/sqlite.js +31 -0
- package/dist/storage/supabase.js +33 -0
- package/dist/storage/supabaseMigrations.js +110 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +40 -0
- package/dist/tools/sessionMemoryHandlers.js +151 -2
- package/package.json +1 -1
package/dist/dashboard/ui.js
CHANGED
|
@@ -696,6 +696,19 @@ export function renderDashboardHTML(version) {
|
|
|
696
696
|
</select>
|
|
697
697
|
</div>
|
|
698
698
|
|
|
699
|
+
<div class="setting-row">
|
|
700
|
+
<div>
|
|
701
|
+
<div class="setting-label">Token Budget</div>
|
|
702
|
+
<div class="setting-desc">Max tokens for session_load_context (0 = unlimited)</div>
|
|
703
|
+
</div>
|
|
704
|
+
<input type="number" id="input-max-tokens"
|
|
705
|
+
placeholder="0"
|
|
706
|
+
min="0" max="100000" step="500"
|
|
707
|
+
style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 90px; text-align: right;"
|
|
708
|
+
onchange="saveSetting('max_tokens', this.value)"
|
|
709
|
+
oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('max_tokens',this.value),800)" />
|
|
710
|
+
</div>
|
|
711
|
+
|
|
699
712
|
<div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
|
|
700
713
|
|
|
701
714
|
<div class="setting-row">
|
|
@@ -716,6 +729,26 @@ export function renderDashboardHTML(version) {
|
|
|
716
729
|
</select>
|
|
717
730
|
</div>
|
|
718
731
|
|
|
732
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
733
|
+
<div>
|
|
734
|
+
<div class="setting-label">Auto-Load Projects</div>
|
|
735
|
+
<div class="setting-desc">Select projects to auto-push context on startup</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div id="autoload-checkboxes" style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;font-family:var(--font-mono);max-height:120px;overflow-y:auto;">
|
|
738
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
<div class="setting-row" style="align-items:flex-start">
|
|
743
|
+
<div>
|
|
744
|
+
<div class="setting-label">Project Repo Paths</div>
|
|
745
|
+
<div class="setting-desc">Map each project to its repo directory for save validation</div>
|
|
746
|
+
</div>
|
|
747
|
+
<div id="repopath-inputs" style="display:flex;flex-direction:column;gap:6px;font-size:0.85rem;max-height:160px;overflow-y:auto;">
|
|
748
|
+
<span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
719
752
|
<div class="setting-section">Agent Identity</div>
|
|
720
753
|
|
|
721
754
|
<div class="setting-row">
|
|
@@ -1278,6 +1311,88 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
1278
1311
|
}
|
|
1279
1312
|
|
|
1280
1313
|
|
|
1314
|
+
// ─── Auto-Load Checkboxes (v4.1) ─────────────────────────────────
|
|
1315
|
+
async function loadAutoloadCheckboxes() {
|
|
1316
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
1317
|
+
if (!container) return;
|
|
1318
|
+
try {
|
|
1319
|
+
var projRes = await fetch('/api/projects');
|
|
1320
|
+
var projData = await projRes.json();
|
|
1321
|
+
var projects = projData.projects || [];
|
|
1322
|
+
|
|
1323
|
+
var settRes = await fetch('/api/settings');
|
|
1324
|
+
var settData = await settRes.json();
|
|
1325
|
+
var saved = (settData.settings || {}).autoload_projects || '';
|
|
1326
|
+
var selected = saved.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
|
1327
|
+
|
|
1328
|
+
if (projects.length === 0) {
|
|
1329
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
container.innerHTML = projects.map(function(p) {
|
|
1334
|
+
var checked = selected.indexOf(p) !== -1 ? ' checked' : '';
|
|
1335
|
+
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-primary)">' +
|
|
1336
|
+
'<input type="checkbox" value="' + escapeHtml(p) + '"' + checked +
|
|
1337
|
+
' onchange="onAutoloadToggle()"' +
|
|
1338
|
+
' style="accent-color:var(--accent-purple);cursor:pointer" />' +
|
|
1339
|
+
escapeHtml(p) + '</label>';
|
|
1340
|
+
}).join('');
|
|
1341
|
+
} catch(e) {
|
|
1342
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function onAutoloadToggle() {
|
|
1347
|
+
var container = document.getElementById('autoload-checkboxes');
|
|
1348
|
+
if (!container) return;
|
|
1349
|
+
var boxes = container.querySelectorAll('input[type=checkbox]');
|
|
1350
|
+
var selected = [];
|
|
1351
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
1352
|
+
if (boxes[i].checked) selected.push(boxes[i].value);
|
|
1353
|
+
}
|
|
1354
|
+
saveBootSetting('autoload_projects', selected.join(','));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ─── Project Repo Paths (v4.2) ─────────────────────────────────
|
|
1358
|
+
async function loadRepoPathInputs() {
|
|
1359
|
+
var container = document.getElementById('repopath-inputs');
|
|
1360
|
+
if (!container) return;
|
|
1361
|
+
try {
|
|
1362
|
+
var projRes = await fetch('/api/projects');
|
|
1363
|
+
var projData = await projRes.json();
|
|
1364
|
+
var projects = projData.projects || [];
|
|
1365
|
+
|
|
1366
|
+
var settRes = await fetch('/api/settings');
|
|
1367
|
+
var settData = await settRes.json();
|
|
1368
|
+
var settings = settData.settings || {};
|
|
1369
|
+
|
|
1370
|
+
if (projects.length === 0) {
|
|
1371
|
+
container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
container.innerHTML = projects.map(function(p) {
|
|
1376
|
+
var savedPath = settings['repo_path:' + p] || '';
|
|
1377
|
+
return '<div style="display:flex;align-items:center;gap:6px">' +
|
|
1378
|
+
'<span style="min-width:100px;color:var(--text-secondary);font-family:var(--font-mono);font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(p) + '">' + escapeHtml(p) + '</span>' +
|
|
1379
|
+
'<input type="text" value="' + escapeHtml(savedPath) + '"' +
|
|
1380
|
+
' placeholder="/path/to/repo"' +
|
|
1381
|
+
' data-project="' + escapeHtml(p) + '"' +
|
|
1382
|
+
' style="flex:1;min-width:140px;padding:0.2rem 0.4rem;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-glass);border-radius:4px;font-size:0.8rem;font-family:var(--font-mono)"' +
|
|
1383
|
+
' onchange="saveRepoPath(this.dataset.project, this.value)"' +
|
|
1384
|
+
' oninput="clearTimeout(this._t); var self=this; this._t=setTimeout(function(){saveRepoPath(self.dataset.project, self.value)},1200)" />' +
|
|
1385
|
+
'</div>';
|
|
1386
|
+
}).join('');
|
|
1387
|
+
} catch(e) {
|
|
1388
|
+
container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async function saveRepoPath(project, path) {
|
|
1393
|
+
await saveSetting('repo_path:' + project, path.trim());
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1281
1396
|
async function loadSettings() {
|
|
1282
1397
|
try {
|
|
1283
1398
|
var res = await fetch('/api/settings');
|
|
@@ -1304,6 +1419,11 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
|
|
|
1304
1419
|
// Agent Identity
|
|
1305
1420
|
if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
|
|
1306
1421
|
if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
|
|
1422
|
+
if (s.max_tokens) document.getElementById('input-max-tokens').value = s.max_tokens;
|
|
1423
|
+
// Autoload checkboxes are loaded dynamically
|
|
1424
|
+
loadAutoloadCheckboxes();
|
|
1425
|
+
// Repo path inputs are loaded dynamically
|
|
1426
|
+
loadRepoPathInputs();
|
|
1307
1427
|
} catch(e) { console.warn('Settings load failed:', e); }
|
|
1308
1428
|
}
|
|
1309
1429
|
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Lifecycle Management
|
|
3
|
+
* Handles singleton PID locking, graceful shutdown, and SQLite handle cleanup.
|
|
4
|
+
*
|
|
5
|
+
* CRITICAL: All logging MUST use console.error() (stderr).
|
|
6
|
+
* Using console.log() (stdout) will corrupt the MCP JSON-RPC stream.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
import { closeConfigStorage } from "./storage/configStorage.js";
|
|
13
|
+
import { getStorage } from "./storage/index.js";
|
|
14
|
+
const PRISM_DIR = path.join(os.homedir(), ".prism-mcp");
|
|
15
|
+
/**
|
|
16
|
+
* Instance-aware PID file.
|
|
17
|
+
* Set PRISM_INSTANCE env var to run multiple Prism MCP servers
|
|
18
|
+
* side-by-side (e.g. "athena-public" and "prism-mcp").
|
|
19
|
+
* Each instance gets its own PID file to prevent lock conflicts.
|
|
20
|
+
*/
|
|
21
|
+
const INSTANCE_NAME = process.env.PRISM_INSTANCE || "default";
|
|
22
|
+
const PID_FILE = path.join(PRISM_DIR, `server-${INSTANCE_NAME}.pid`);
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.error(`[Prism Lifecycle] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a process is an orphan (adopted by init/launchd, PPID=1).
|
|
28
|
+
* Returns false on Windows (PID logic is different there).
|
|
29
|
+
*/
|
|
30
|
+
function isOrphanProcess(pid) {
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
// Windows doesn't have reliable PPID checks via 'ps'.
|
|
33
|
+
// Safer to assume it's NOT an orphan to avoid killing valid instances.
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
// 'ps -o ppid= -p PID' returns just the parent PID
|
|
38
|
+
const ppid = execSync(`ps -o ppid= -p ${pid}`, { encoding: "utf8" }).trim();
|
|
39
|
+
return ppid === "1";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// If ps fails (e.g. process gone), assume it's safe to claim
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ensures valid server execution state.
|
|
48
|
+
*
|
|
49
|
+
* LOGIC:
|
|
50
|
+
* 1. If --no-lock is passed, skip everything (testing mode).
|
|
51
|
+
* 2. If PID file exists:
|
|
52
|
+
* - If process is dead: Overwrite lock.
|
|
53
|
+
* - If process is alive AND is an orphan (PPID=1): Kill it (Zombie), then overwrite.
|
|
54
|
+
* - If process is alive AND has a parent: Log warning, allow coexistence (don't kill).
|
|
55
|
+
*/
|
|
56
|
+
export function acquireLock() {
|
|
57
|
+
if (process.argv.includes("--no-lock")) {
|
|
58
|
+
log("Lock acquisition skipped (--no-lock flag)");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(PRISM_DIR)) {
|
|
62
|
+
fs.mkdirSync(PRISM_DIR, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
if (fs.existsSync(PID_FILE)) {
|
|
65
|
+
try {
|
|
66
|
+
const oldPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
67
|
+
if (oldPid && oldPid !== process.pid) {
|
|
68
|
+
let isAlive = false;
|
|
69
|
+
try {
|
|
70
|
+
process.kill(oldPid, 0); // 0 signal checks for existence
|
|
71
|
+
isAlive = true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
isAlive = false;
|
|
75
|
+
}
|
|
76
|
+
if (isAlive) {
|
|
77
|
+
// Process exists. Is it a zombie?
|
|
78
|
+
if (isOrphanProcess(oldPid)) {
|
|
79
|
+
log(`Found zombie process (PID ${oldPid}, PPID=1). Terminating...`);
|
|
80
|
+
try {
|
|
81
|
+
process.kill(oldPid, "SIGTERM");
|
|
82
|
+
// Give it 100ms to die, then force kill if needed
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(oldPid, "SIGKILL");
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}, 100);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
log(`Failed to kill zombie: ${e}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// It has a parent (e.g., another VS Code window or Claude Desktop)
|
|
96
|
+
log(`Existing server (PID ${oldPid}) is active and managed. Coexisting...`);
|
|
97
|
+
// We do NOT overwrite the PID file here.
|
|
98
|
+
// If we overwrite it, the *active* server will fail to clean up
|
|
99
|
+
// the PID file when it eventually shuts down.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
log(`Warning: Failed to process existing PID file: ${err}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Claim the lock for this process
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(PID_FILE, process.pid.toString(), "utf8");
|
|
112
|
+
log(`Acquired singleton lock (PID ${process.pid})`);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
log(`Warning: Failed to write PID file: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Registers handlers to close SQLite file handles cleanly when the server stops.
|
|
120
|
+
*/
|
|
121
|
+
export function registerShutdownHandlers() {
|
|
122
|
+
let shuttingDown = false;
|
|
123
|
+
const shutdown = async (reason) => {
|
|
124
|
+
if (shuttingDown)
|
|
125
|
+
return;
|
|
126
|
+
shuttingDown = true;
|
|
127
|
+
log(`Shutting down gracefully (${reason})...`);
|
|
128
|
+
try {
|
|
129
|
+
// 1. Close system settings DB
|
|
130
|
+
closeConfigStorage();
|
|
131
|
+
// 2. Close main ledger DB
|
|
132
|
+
const storage = await getStorage();
|
|
133
|
+
if (storage && typeof storage.close === "function") {
|
|
134
|
+
await storage.close();
|
|
135
|
+
}
|
|
136
|
+
// 3. Remove PID lockfile (only if WE own it)
|
|
137
|
+
if (fs.existsSync(PID_FILE)) {
|
|
138
|
+
try {
|
|
139
|
+
const storedPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
|
|
140
|
+
if (storedPid === process.pid) {
|
|
141
|
+
fs.unlinkSync(PID_FILE);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore read errors during shutdown
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
log(`Error during shutdown cleanup: ${err}`);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// OS Signals
|
|
157
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
158
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
159
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
160
|
+
// MCP Client Disconnect (CRITICAL)
|
|
161
|
+
process.stdin.on("close", () => {
|
|
162
|
+
shutdown("CLIENT_DISCONNECTED_STDIN_CLOSED");
|
|
163
|
+
});
|
|
164
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -61,6 +61,7 @@ SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/
|
|
|
61
61
|
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND } from "./config.js";
|
|
62
62
|
import { getSyncBus } from "./sync/factory.js";
|
|
63
63
|
import { startDashboardServer } from "./dashboard/server.js";
|
|
64
|
+
import { acquireLock, registerShutdownHandlers } from "./lifecycle.js";
|
|
64
65
|
// ─── v2.3.6 FIX: Use Storage Abstraction for Prompts/Resources ───
|
|
65
66
|
// CRITICAL FIX: Previously imported supabaseRpc/supabaseGet directly,
|
|
66
67
|
// which bypassed the storage abstraction layer and caused the server
|
|
@@ -101,6 +102,8 @@ sessionForgetMemoryHandler,
|
|
|
101
102
|
knowledgeSetRetentionHandler,
|
|
102
103
|
// v4.0: Active Behavioral Memory handlers
|
|
103
104
|
sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler,
|
|
105
|
+
// v4.2: Knowledge Sync Rules
|
|
106
|
+
KNOWLEDGE_SYNC_RULES_TOOL, knowledgeSyncRulesHandler,
|
|
104
107
|
// ─── v3.0: Agent Hivemind tools ───
|
|
105
108
|
AGENT_REGISTRY_TOOLS, agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler, } from "./tools/index.js";
|
|
106
109
|
// ─── Dynamic Tool Registration ───────────────────────────────────
|
|
@@ -114,43 +117,54 @@ const BASE_TOOLS = [
|
|
|
114
117
|
BRAVE_ANSWERS_TOOL, // brave_answers — AI-grounded answers
|
|
115
118
|
RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
|
|
116
119
|
];
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
120
|
+
// ─── v4.1 FIX: Build Session Memory Tools dynamically ────────
|
|
121
|
+
// The session_load_context tool description is dynamically modified
|
|
122
|
+
// to include auto-load project instructions. Tool descriptions are
|
|
123
|
+
// the ONLY mechanism guaranteed by ALL MCP clients (Antigravity,
|
|
124
|
+
// Claude Code, Claude CLI). The `instructions` field and
|
|
125
|
+
// sendLoggingMessage are NOT reliably surfaced by any client.
|
|
126
|
+
//
|
|
127
|
+
// buildSessionMemoryTools() is called inside createServer() AFTER
|
|
128
|
+
// initConfigStorage() has run, so getSettingSync() returns real
|
|
129
|
+
// dashboard config values.
|
|
130
|
+
function buildSessionMemoryTools(autoloadList) {
|
|
131
|
+
// Dynamically inject auto-load instruction into session_load_context description
|
|
132
|
+
let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
|
|
133
|
+
if (autoloadList.length > 0) {
|
|
134
|
+
const projectList = autoloadList.join(", ");
|
|
135
|
+
loadContextTool = {
|
|
136
|
+
...SESSION_LOAD_CONTEXT_TOOL,
|
|
137
|
+
description: SESSION_LOAD_CONTEXT_TOOL.description +
|
|
138
|
+
`\n\nIMPORTANT: At the start of every conversation, you MUST call this tool for each of these projects: ${projectList}. Do this before any other work. Load level should be "standard" unless the user specifies otherwise.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return [
|
|
142
|
+
SESSION_SAVE_LEDGER_TOOL, // session_save_ledger — append immutable session log
|
|
143
|
+
SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
|
|
144
|
+
loadContextTool, // session_load_context — progressive context loading (+ auto-load instruction)
|
|
145
|
+
KNOWLEDGE_SEARCH_TOOL, // knowledge_search — search accumulated knowledge
|
|
146
|
+
KNOWLEDGE_FORGET_TOOL, // knowledge_forget — prune bad/old memories
|
|
147
|
+
SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
|
|
148
|
+
SESSION_SEARCH_MEMORY_TOOL, // session_search_memory — semantic search via embeddings (v0.4.0)
|
|
149
|
+
MEMORY_HISTORY_TOOL, // memory_history — view version timeline (v2.0)
|
|
150
|
+
MEMORY_CHECKOUT_TOOL, // memory_checkout — revert to past version (v2.0)
|
|
151
|
+
// ─── v2.0: Visual Memory tools ───
|
|
152
|
+
SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
|
|
153
|
+
SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
|
|
154
|
+
// ─── v2.2.0: Health Check tool ───
|
|
155
|
+
SESSION_HEALTH_CHECK_TOOL, // session_health_check — brain integrity checker (v2.2.0)
|
|
156
|
+
// ─── Phase 2: GDPR Memory Deletion tool ───
|
|
157
|
+
SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
|
|
158
|
+
// ─── v3.1: TTL Retention tool ───
|
|
159
|
+
KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retention — set auto-expiry TTL for a project
|
|
160
|
+
// ─── v4.0: Active Behavioral Memory tools ───
|
|
161
|
+
SESSION_SAVE_EXPERIENCE_TOOL, // session_save_experience — record typed experience events
|
|
162
|
+
KNOWLEDGE_UPVOTE_TOOL, // knowledge_upvote — increase entry importance
|
|
163
|
+
KNOWLEDGE_DOWNVOTE_TOOL, // knowledge_downvote — decrease entry importance
|
|
164
|
+
// ─── v4.2: Knowledge Sync Rules tool ───
|
|
165
|
+
KNOWLEDGE_SYNC_RULES_TOOL, // knowledge_sync_rules — sync graduated insights to IDE rules files
|
|
166
|
+
];
|
|
167
|
+
}
|
|
154
168
|
// ─── v0.4.0: Resource Subscription Tracking ──────────────────────
|
|
155
169
|
// REVIEWER NOTE: We track which project URIs clients have subscribed
|
|
156
170
|
// to. When sessionSaveHandoffHandler successfully updates a project,
|
|
@@ -194,6 +208,35 @@ export function notifyResourceUpdate(project, server) {
|
|
|
194
208
|
* with subscribe support for live refresh
|
|
195
209
|
*/
|
|
196
210
|
export function createServer() {
|
|
211
|
+
// ─── v4.1 FIX: Auto-Load via Dynamic Tool Descriptions ────────
|
|
212
|
+
// Read auto-load projects EXCLUSIVELY from dashboard config
|
|
213
|
+
// (available after initConfigStorage() in startServer).
|
|
214
|
+
//
|
|
215
|
+
// ARCHITECTURE DECISION: We inject the auto-load instruction into
|
|
216
|
+
// the session_load_context TOOL DESCRIPTION, not into `instructions`
|
|
217
|
+
// or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
|
|
218
|
+
// guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
|
|
219
|
+
//
|
|
220
|
+
// The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
|
|
221
|
+
// is the single source of truth. This prevents mismatches between
|
|
222
|
+
// env var and dashboard settings causing duplicate project loads.
|
|
223
|
+
const dashboardAutoload = getSettingSync("autoload_projects", "");
|
|
224
|
+
const autoloadList = dashboardAutoload
|
|
225
|
+
.split(",").map(p => p.trim()).filter(Boolean);
|
|
226
|
+
if (autoloadList.length > 0) {
|
|
227
|
+
console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
// Build the dynamic tool list with auto-load instruction injected
|
|
230
|
+
const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(autoloadList);
|
|
231
|
+
// Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
|
|
232
|
+
// can enumerate the full capability set. Runtime guards in the CallTool handler
|
|
233
|
+
// still prevent execution without valid Supabase credentials.
|
|
234
|
+
const ALL_TOOLS = [
|
|
235
|
+
...BASE_TOOLS,
|
|
236
|
+
...SESSION_MEMORY_TOOLS,
|
|
237
|
+
// v3.0: Agent Hivemind tools — only when PRISM_ENABLE_HIVEMIND=true
|
|
238
|
+
...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
|
|
239
|
+
];
|
|
197
240
|
const server = new Server({
|
|
198
241
|
name: SERVER_CONFIG.name,
|
|
199
242
|
version: SERVER_CONFIG.version,
|
|
@@ -201,21 +244,13 @@ export function createServer() {
|
|
|
201
244
|
capabilities: {
|
|
202
245
|
tools: {},
|
|
203
246
|
// ─── v0.4.0: Prompt capability (Enhancement #1) ───
|
|
204
|
-
// REVIEWER NOTE: Declaring `prompts: {}` tells Claude Desktop
|
|
205
|
-
// that we support the prompts/list and prompts/get methods.
|
|
206
|
-
// This enables the /resume_session slash command in the UI.
|
|
207
|
-
// Only enabled when Supabase is configured (prompts need
|
|
208
|
-
// session data to be useful).
|
|
209
247
|
...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
|
|
210
248
|
// ─── v0.4.0: Resource capability (Enhancement #3) ───
|
|
211
|
-
// REVIEWER NOTE: Setting subscribe: true tells Claude Desktop
|
|
212
|
-
// that we support resource subscriptions. When a user attaches
|
|
213
|
-
// memory://project/handoff and the LLM later updates it,
|
|
214
|
-
// we push an update notification so the attached context
|
|
215
|
-
// is silently refreshed. Without subscribe:true, the
|
|
216
|
-
// paperclipped context would become stale.
|
|
217
249
|
...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
|
|
218
250
|
},
|
|
251
|
+
// Supplementary signal — not all clients support this field.
|
|
252
|
+
// Primary mechanism is the dynamic tool description above.
|
|
253
|
+
instructions: `Prism MCP — The Mind Palace for AI Agents. This server provides persistent session memory, knowledge search, and context management tools. Use session_load_context to recover previous work state, session_save_ledger to log completed work, and session_save_handoff to preserve state for the next session.`,
|
|
219
254
|
});
|
|
220
255
|
// ── Handler: Initialize ──
|
|
221
256
|
// NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
|
|
@@ -607,6 +642,11 @@ export function createServer() {
|
|
|
607
642
|
if (!SESSION_MEMORY_ENABLED)
|
|
608
643
|
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
609
644
|
return await knowledgeDownvoteHandler(args);
|
|
645
|
+
// ─── v4.2: Knowledge Sync Rules Tool ───
|
|
646
|
+
case "knowledge_sync_rules":
|
|
647
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
648
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
649
|
+
return await knowledgeSyncRulesHandler(args);
|
|
610
650
|
// ─── v3.0: Agent Hivemind Tools ───
|
|
611
651
|
case "agent_register":
|
|
612
652
|
if (!SESSION_MEMORY_ENABLED)
|
|
@@ -666,7 +706,7 @@ export function createSandboxServer() {
|
|
|
666
706
|
});
|
|
667
707
|
// Register all tool listings unconditionally
|
|
668
708
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
669
|
-
tools: [...BASE_TOOLS, ...
|
|
709
|
+
tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS],
|
|
670
710
|
}));
|
|
671
711
|
// Register prompts listing so scanners see resume_session
|
|
672
712
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
@@ -715,6 +755,9 @@ export function createSandboxServer() {
|
|
|
715
755
|
* responses to stdout. Log messages go to stderr.
|
|
716
756
|
*/
|
|
717
757
|
export async function startServer() {
|
|
758
|
+
// MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
|
|
759
|
+
// before touching SQLite. This prevents lock contention on prism-config.db.
|
|
760
|
+
acquireLock();
|
|
718
761
|
// Pre-warm the config settings cache BEFORE connecting the MCP transport.
|
|
719
762
|
// This ensures getSettingSync() returns real values (agent_name, default_role)
|
|
720
763
|
// during the Initialize handshake — zero extra latency for resource reads.
|
|
@@ -723,6 +766,10 @@ export async function startServer() {
|
|
|
723
766
|
const server = createServer();
|
|
724
767
|
const transport = new StdioServerTransport();
|
|
725
768
|
await server.connect(transport);
|
|
769
|
+
// Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
|
|
770
|
+
// The stdin close handler is critical — when MCP clients disconnect, they
|
|
771
|
+
// often just close the pipe without sending a signal, leaving zombie processes.
|
|
772
|
+
registerShutdownHandlers();
|
|
726
773
|
// Pre-warm storage AFTER connecting — fired async so we never block the
|
|
727
774
|
// stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
|
|
728
775
|
// on it before server.connect() was the root cause of the 1m 56s CLI delay.
|
|
@@ -740,6 +787,18 @@ export async function startServer() {
|
|
|
740
787
|
]).catch(err => {
|
|
741
788
|
console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err}`);
|
|
742
789
|
});
|
|
790
|
+
// ─── v4.1: Auto-Load is handled via dynamic tool descriptions ──
|
|
791
|
+
// The session_load_context tool description is dynamically modified
|
|
792
|
+
// in createServer() → buildSessionMemoryTools() to include the
|
|
793
|
+
// auto-load projects list. This is the ONLY universally reliable
|
|
794
|
+
// mechanism — tool descriptions are surfaced by ALL MCP clients.
|
|
795
|
+
//
|
|
796
|
+
// Previous approaches that FAILED:
|
|
797
|
+
// - sendLoggingMessage: goes to debug logs, not AI conversation
|
|
798
|
+
// - instructions field: not supported by Claude Code or Claude CLI
|
|
799
|
+
//
|
|
800
|
+
// No runtime code needed here — the instruction is baked into the
|
|
801
|
+
// tool schema returned by ListTools.
|
|
743
802
|
}
|
|
744
803
|
// ─── v2.0 Step 6: Initialize SyncBus (Telepathy) ───
|
|
745
804
|
// Fire-and-forget — SyncBus is non-critical for startup.
|
|
@@ -102,17 +102,36 @@ export async function getSetting(key, defaultValue = "") {
|
|
|
102
102
|
export async function setSetting(key, value) {
|
|
103
103
|
await initConfigStorage();
|
|
104
104
|
const client = getClient();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
// Retry with exponential backoff for SQLITE_BUSY (concurrent writes).
|
|
106
|
+
// The dashboard and load tests can fire many parallel setting saves.
|
|
107
|
+
const MAX_RETRIES = 5;
|
|
108
|
+
const BASE_DELAY_MS = 20;
|
|
109
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
await client.execute({
|
|
112
|
+
sql: `
|
|
113
|
+
INSERT INTO system_settings (key, value, updated_at)
|
|
114
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
115
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
|
116
|
+
`,
|
|
117
|
+
args: [key, value],
|
|
118
|
+
});
|
|
119
|
+
// Keep the cache in sync so getSettingSync() reflects the new value immediately.
|
|
120
|
+
if (settingsCache) {
|
|
121
|
+
settingsCache[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return; // Success — exit
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const isBusy = err?.code === "SQLITE_BUSY" || err?.rawCode === 5;
|
|
127
|
+
if (isBusy && attempt < MAX_RETRIES) {
|
|
128
|
+
// Exponential backoff + jitter
|
|
129
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 10;
|
|
130
|
+
await new Promise(r => setTimeout(r, delay));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw err; // Not SQLITE_BUSY or retries exhausted
|
|
134
|
+
}
|
|
116
135
|
}
|
|
117
136
|
}
|
|
118
137
|
export async function getAllSettings() {
|
|
@@ -129,3 +148,17 @@ export async function getAllSettings() {
|
|
|
129
148
|
}
|
|
130
149
|
return settings;
|
|
131
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Closes the config SQLite client to release the file handle on prism-config.db.
|
|
153
|
+
* Called by the lifecycle module during graceful shutdown.
|
|
154
|
+
*/
|
|
155
|
+
export function closeConfigStorage() {
|
|
156
|
+
if (configClient) {
|
|
157
|
+
try {
|
|
158
|
+
configClient.close();
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.error(`[ConfigStorage] Error closing db:`, e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1391,4 +1391,35 @@ export class SqliteStorage {
|
|
|
1391
1391
|
});
|
|
1392
1392
|
debugLog(`[SqliteStorage] Adjusted importance for ${id} by ${delta > 0 ? "+" : ""}${delta}`);
|
|
1393
1393
|
}
|
|
1394
|
+
// ─── v4.2: Graduated Insights Query ──────────────────────────
|
|
1395
|
+
//
|
|
1396
|
+
// Returns ledger entries that have "graduated" — i.e., their
|
|
1397
|
+
// importance score has reached the threshold (default 7).
|
|
1398
|
+
// Used by knowledge_sync_rules to physically write insights
|
|
1399
|
+
// into .cursorrules / .clauderules files at the project repo path.
|
|
1400
|
+
async getGraduatedInsights(project, userId, minImportance = 7) {
|
|
1401
|
+
const result = await this.db.execute({
|
|
1402
|
+
sql: `SELECT id, project, user_id, role, summary, importance,
|
|
1403
|
+
event_type, decisions, created_at
|
|
1404
|
+
FROM session_ledger
|
|
1405
|
+
WHERE project = ? AND user_id = ?
|
|
1406
|
+
AND importance >= ?
|
|
1407
|
+
AND deleted_at IS NULL
|
|
1408
|
+
AND archived_at IS NULL
|
|
1409
|
+
ORDER BY importance DESC, created_at DESC`,
|
|
1410
|
+
args: [project, userId, minImportance],
|
|
1411
|
+
});
|
|
1412
|
+
return result.rows.map(row => ({
|
|
1413
|
+
id: row.id,
|
|
1414
|
+
project: row.project,
|
|
1415
|
+
user_id: row.user_id,
|
|
1416
|
+
role: row.role || "global",
|
|
1417
|
+
summary: row.summary,
|
|
1418
|
+
importance: Number(row.importance),
|
|
1419
|
+
event_type: row.event_type || "session",
|
|
1420
|
+
decisions: this.parseJsonColumn(row.decisions),
|
|
1421
|
+
created_at: row.created_at,
|
|
1422
|
+
conversation_id: "",
|
|
1423
|
+
}));
|
|
1424
|
+
}
|
|
1394
1425
|
}
|