prism-mcp-server 2.3.12 → 2.5.1
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 +274 -18
- package/dist/config.js +20 -3
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +2 -2
- package/dist/server.js +13 -2
- package/dist/storage/sqlite.js +70 -3
- package/dist/storage/supabase.js +35 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +88 -0
- package/dist/tools/sessionMemoryHandlers.js +285 -42
- package/dist/utils/embeddingApi.js +11 -4
- package/dist/utils/tracing.js +139 -0
- package/package.json +1 -1
package/dist/storage/sqlite.js
CHANGED
|
@@ -152,6 +152,36 @@ export class SqliteStorage {
|
|
|
152
152
|
CREATE INDEX IF NOT EXISTS idx_history_version
|
|
153
153
|
ON session_handoffs_history(project, version);
|
|
154
154
|
`);
|
|
155
|
+
// ─── Phase 2 Migration: GDPR Soft Delete Columns ──────────
|
|
156
|
+
//
|
|
157
|
+
// SQLITE GOTCHA: Unlike CREATE TABLE IF NOT EXISTS, ALTER TABLE
|
|
158
|
+
// throws a fatal error if the column already exists. We MUST
|
|
159
|
+
// wrap each ALTER TABLE in a try/catch and only ignore
|
|
160
|
+
// "duplicate column name" errors.
|
|
161
|
+
//
|
|
162
|
+
// This migration runs on every boot but is idempotent — the
|
|
163
|
+
// try/catch ensures it's safe to run repeatedly.
|
|
164
|
+
try {
|
|
165
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN deleted_at TEXT DEFAULT NULL`);
|
|
166
|
+
debugLog("[SqliteStorage] Phase 2 migration: added deleted_at column");
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
// "duplicate column name" = column already exists from prior boot.
|
|
170
|
+
// Any other error is a real problem — rethrow it.
|
|
171
|
+
if (!e.message?.includes("duplicate column name"))
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN deleted_reason TEXT DEFAULT NULL`);
|
|
176
|
+
debugLog("[SqliteStorage] Phase 2 migration: added deleted_reason column");
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
if (!e.message?.includes("duplicate column name"))
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
// Index for fast WHERE deleted_at IS NULL queries.
|
|
183
|
+
// CREATE INDEX IF NOT EXISTS is safe to run repeatedly (no try/catch needed).
|
|
184
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_deleted ON session_ledger(deleted_at)`);
|
|
155
185
|
}
|
|
156
186
|
// ─── PostgREST Filter Parser ───────────────────────────────
|
|
157
187
|
//
|
|
@@ -341,6 +371,37 @@ export class SqliteStorage {
|
|
|
341
371
|
});
|
|
342
372
|
return entries;
|
|
343
373
|
}
|
|
374
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion ──────────────
|
|
375
|
+
//
|
|
376
|
+
// These methods are SURGICAL — they operate on a single entry by ID.
|
|
377
|
+
// They MUST verify user_id ownership to prevent cross-user deletion.
|
|
378
|
+
//
|
|
379
|
+
// softDeleteLedger: Sets deleted_at + deleted_reason. Entry stays in
|
|
380
|
+
// DB for audit trail. All search queries filter it out via
|
|
381
|
+
// "AND deleted_at IS NULL". Reversible.
|
|
382
|
+
//
|
|
383
|
+
// hardDeleteLedger: Physical DELETE. Irreversible. FTS5 triggers
|
|
384
|
+
// automatically clean up the full-text index.
|
|
385
|
+
async softDeleteLedger(id, userId, reason) {
|
|
386
|
+
// UPDATE (not DELETE): sets tombstone fields while preserving the row.
|
|
387
|
+
// The JS-side datetime('now') matches SQLite's native format.
|
|
388
|
+
await this.db.execute({
|
|
389
|
+
sql: `UPDATE session_ledger
|
|
390
|
+
SET deleted_at = datetime('now'), deleted_reason = ?
|
|
391
|
+
WHERE id = ? AND user_id = ?`,
|
|
392
|
+
args: [reason || null, id, userId],
|
|
393
|
+
});
|
|
394
|
+
debugLog(`[SqliteStorage] Soft-deleted ledger entry ${id} (reason: ${reason || "none"})`);
|
|
395
|
+
}
|
|
396
|
+
async hardDeleteLedger(id, userId) {
|
|
397
|
+
// Physical DELETE — row is permanently removed.
|
|
398
|
+
// FTS5 trigger (ledger_fts_delete) automatically cleans up the index.
|
|
399
|
+
await this.db.execute({
|
|
400
|
+
sql: `DELETE FROM session_ledger WHERE id = ? AND user_id = ?`,
|
|
401
|
+
args: [id, userId],
|
|
402
|
+
});
|
|
403
|
+
debugLog(`[SqliteStorage] Hard-deleted ledger entry ${id}`);
|
|
404
|
+
}
|
|
344
405
|
// ─── Handoff Operations (OCC) ──────────────────────────────
|
|
345
406
|
async saveHandoff(handoff, expectedVersion) {
|
|
346
407
|
// CASE 1: No expectedVersion → UPSERT (create or force-update)
|
|
@@ -471,10 +532,11 @@ export class SqliteStorage {
|
|
|
471
532
|
context.key_context = handoff.key_context;
|
|
472
533
|
if (level === "standard") {
|
|
473
534
|
// Add recent ledger entries as summaries
|
|
535
|
+
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
474
536
|
const recentLedger = await this.db.execute({
|
|
475
537
|
sql: `SELECT summary, decisions, session_date, created_at
|
|
476
538
|
FROM session_ledger
|
|
477
|
-
WHERE project = ? AND user_id = ? AND archived_at IS NULL
|
|
539
|
+
WHERE project = ? AND user_id = ? AND archived_at IS NULL AND deleted_at IS NULL
|
|
478
540
|
ORDER BY created_at DESC
|
|
479
541
|
LIMIT 5`,
|
|
480
542
|
args: [project, userId],
|
|
@@ -487,10 +549,11 @@ export class SqliteStorage {
|
|
|
487
549
|
return context;
|
|
488
550
|
}
|
|
489
551
|
// Deep: add full session history
|
|
552
|
+
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
490
553
|
const fullLedger = await this.db.execute({
|
|
491
554
|
sql: `SELECT summary, decisions, files_changed, todos, session_date, created_at
|
|
492
555
|
FROM session_ledger
|
|
493
|
-
WHERE project = ? AND user_id = ? AND archived_at IS NULL
|
|
556
|
+
WHERE project = ? AND user_id = ? AND archived_at IS NULL AND deleted_at IS NULL
|
|
494
557
|
ORDER BY created_at DESC
|
|
495
558
|
LIMIT 50`,
|
|
496
559
|
args: [project, userId],
|
|
@@ -529,6 +592,7 @@ export class SqliteStorage {
|
|
|
529
592
|
AND l.project = ?
|
|
530
593
|
AND l.user_id = ?
|
|
531
594
|
AND l.archived_at IS NULL
|
|
595
|
+
AND l.deleted_at IS NULL
|
|
532
596
|
ORDER BY rank
|
|
533
597
|
LIMIT ?
|
|
534
598
|
`;
|
|
@@ -544,6 +608,7 @@ export class SqliteStorage {
|
|
|
544
608
|
WHERE ledger_fts MATCH ?
|
|
545
609
|
AND l.user_id = ?
|
|
546
610
|
AND l.archived_at IS NULL
|
|
611
|
+
AND l.deleted_at IS NULL
|
|
547
612
|
ORDER BY rank
|
|
548
613
|
LIMIT ?
|
|
549
614
|
`;
|
|
@@ -573,7 +638,7 @@ export class SqliteStorage {
|
|
|
573
638
|
}
|
|
574
639
|
/** Fallback search using LIKE when FTS5 query syntax fails */
|
|
575
640
|
async searchKnowledgeFallback(params) {
|
|
576
|
-
const conditions = ["user_id = ?", "archived_at IS NULL"];
|
|
641
|
+
const conditions = ["user_id = ?", "archived_at IS NULL", "deleted_at IS NULL"];
|
|
577
642
|
const args = [params.userId];
|
|
578
643
|
if (params.project) {
|
|
579
644
|
conditions.push("project = ?");
|
|
@@ -626,6 +691,7 @@ export class SqliteStorage {
|
|
|
626
691
|
AND l.user_id = ?
|
|
627
692
|
AND l.project = ?
|
|
628
693
|
AND l.archived_at IS NULL
|
|
694
|
+
AND l.deleted_at IS NULL
|
|
629
695
|
ORDER BY similarity DESC
|
|
630
696
|
LIMIT ?
|
|
631
697
|
`;
|
|
@@ -640,6 +706,7 @@ export class SqliteStorage {
|
|
|
640
706
|
WHERE l.embedding IS NOT NULL
|
|
641
707
|
AND l.user_id = ?
|
|
642
708
|
AND l.archived_at IS NULL
|
|
709
|
+
AND l.deleted_at IS NULL
|
|
643
710
|
ORDER BY similarity DESC
|
|
644
711
|
LIMIT ?
|
|
645
712
|
`;
|
package/dist/storage/supabase.js
CHANGED
|
@@ -67,6 +67,41 @@ export class SupabaseStorage {
|
|
|
67
67
|
const result = await supabaseDelete("session_ledger", params);
|
|
68
68
|
return Array.isArray(result) ? result : [];
|
|
69
69
|
}
|
|
70
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion ──────────────
|
|
71
|
+
//
|
|
72
|
+
// These methods are SURGICAL — they operate on a single entry by ID.
|
|
73
|
+
// They MUST verify user_id ownership to prevent cross-user deletion.
|
|
74
|
+
//
|
|
75
|
+
// softDeleteLedger: Sets deleted_at + deleted_reason. Entry stays in
|
|
76
|
+
// DB for audit trail. Supabase RPCs and TypeScript queries filter
|
|
77
|
+
// it out via "WHERE deleted_at IS NULL". Reversible.
|
|
78
|
+
//
|
|
79
|
+
// hardDeleteLedger: Physical DELETE. Irreversible. For GDPR Article 17
|
|
80
|
+
// "right to erasure" when the audit trail must also be removed.
|
|
81
|
+
async softDeleteLedger(id, userId, reason) {
|
|
82
|
+
// PATCH (not DELETE): sets tombstone fields while preserving the row.
|
|
83
|
+
// The deleted_at timestamp is set server-side for consistency.
|
|
84
|
+
// deleted_reason captures the GDPR justification (e.g., "User requested",
|
|
85
|
+
// "Data retention policy", "GDPR Article 17 request").
|
|
86
|
+
await supabasePatch("session_ledger", {
|
|
87
|
+
deleted_at: new Date().toISOString(),
|
|
88
|
+
deleted_reason: reason || null,
|
|
89
|
+
}, {
|
|
90
|
+
id: `eq.${id}`,
|
|
91
|
+
user_id: `eq.${userId}`, // Ownership guard — prevents cross-user deletion
|
|
92
|
+
});
|
|
93
|
+
debugLog(`[SupabaseStorage] Soft-deleted ledger entry ${id} (reason: ${reason || "none"})`);
|
|
94
|
+
}
|
|
95
|
+
async hardDeleteLedger(id, userId) {
|
|
96
|
+
// Physical DELETE — row is permanently removed from the database.
|
|
97
|
+
// This is irreversible. The FTS5 index (if any) is cleaned up by
|
|
98
|
+
// Supabase's built-in trigger handling.
|
|
99
|
+
await supabaseDelete("session_ledger", {
|
|
100
|
+
id: `eq.${id}`,
|
|
101
|
+
user_id: `eq.${userId}`, // Ownership guard
|
|
102
|
+
});
|
|
103
|
+
debugLog(`[SupabaseStorage] Hard-deleted ledger entry ${id}`);
|
|
104
|
+
}
|
|
70
105
|
// ─── Handoff Operations ────────────────────────────────────
|
|
71
106
|
async saveHandoff(handoff, expectedVersion) {
|
|
72
107
|
// Direct mapping from sessionSaveHandoffHandler line 214
|
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 } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler } 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 } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler } 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
|
|
@@ -114,6 +114,10 @@ export const SESSION_LOAD_CONTEXT_TOOL = {
|
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
116
|
// ─── Knowledge Search ─────────────────────────────────────────
|
|
117
|
+
// Phase 1 Change: Added `enable_trace` optional boolean.
|
|
118
|
+
// When true, the handler returns a separate content[1] block with a
|
|
119
|
+
// MemoryTrace object (strategy="keyword", latency, result metadata).
|
|
120
|
+
// Default: false — output is identical to pre-Phase 1 behavior.
|
|
117
121
|
export const KNOWLEDGE_SEARCH_TOOL = {
|
|
118
122
|
name: "knowledge_search",
|
|
119
123
|
description: "Search accumulated knowledge across all sessions by keywords, category, or free text. " +
|
|
@@ -144,6 +148,14 @@ export const KNOWLEDGE_SEARCH_TOOL = {
|
|
|
144
148
|
description: "Maximum results to return (default: 10, max: 50).",
|
|
145
149
|
default: 10,
|
|
146
150
|
},
|
|
151
|
+
// Phase 1: Explainability — when true, appends a MemoryTrace JSON
|
|
152
|
+
// object as content[1] in the response array.
|
|
153
|
+
// MCP clients can parse content[1] programmatically for debugging.
|
|
154
|
+
enable_trace: {
|
|
155
|
+
type: "boolean",
|
|
156
|
+
description: "If true, returns a separate MEMORY TRACE content block with search strategy, " +
|
|
157
|
+
"latency breakdown, and scoring metadata for explainability. Default: false.",
|
|
158
|
+
},
|
|
147
159
|
},
|
|
148
160
|
},
|
|
149
161
|
};
|
|
@@ -265,6 +277,15 @@ export const SESSION_SEARCH_MEMORY_TOOL = {
|
|
|
265
277
|
description: "Minimum similarity score 0-1 (default: 0.7). Higher = more relevant, fewer results.",
|
|
266
278
|
default: 0.7,
|
|
267
279
|
},
|
|
280
|
+
// Phase 1: Explainability — when true, appends a MemoryTrace JSON
|
|
281
|
+
// object as content[1] in the response array. For semantic search,
|
|
282
|
+
// the trace includes embedding_ms (Gemini API time) vs storage_ms
|
|
283
|
+
// (pgvector query time) to pinpoint performance bottlenecks.
|
|
284
|
+
enable_trace: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "If true, returns a separate MEMORY TRACE content block with search strategy, " +
|
|
287
|
+
"latency breakdown (embedding vs storage), and scoring metadata. Default: false.",
|
|
288
|
+
},
|
|
268
289
|
},
|
|
269
290
|
required: ["query"],
|
|
270
291
|
},
|
|
@@ -306,6 +327,9 @@ export const SESSION_BACKFILL_EMBEDDINGS_TOOL = {
|
|
|
306
327
|
export function isKnowledgeForgetArgs(args) {
|
|
307
328
|
return typeof args === "object" && args !== null;
|
|
308
329
|
}
|
|
330
|
+
// Phase 1: Added enable_trace to the type guard.
|
|
331
|
+
// Optional boolean — when true, the handler returns a MemoryTrace content block.
|
|
332
|
+
// Default: false, so existing callers see no change in behavior.
|
|
309
333
|
export function isKnowledgeSearchArgs(args) {
|
|
310
334
|
return typeof args === "object" && args !== null;
|
|
311
335
|
}
|
|
@@ -328,6 +352,9 @@ export function isSessionSaveHandoffArgs(args) {
|
|
|
328
352
|
typeof args.project === "string");
|
|
329
353
|
}
|
|
330
354
|
// ─── v0.4.0: Type guard for semantic search ──────────────────
|
|
355
|
+
// Phase 1: Added enable_trace to the type guard.
|
|
356
|
+
// Optional boolean — when true, a MemoryTrace block (with embedding_ms,
|
|
357
|
+
// storage_ms, top_score, etc.) is appended as content[1] in the response.
|
|
331
358
|
export function isSessionSearchMemoryArgs(args) {
|
|
332
359
|
return (typeof args === "object" &&
|
|
333
360
|
args !== null &&
|
|
@@ -500,3 +527,64 @@ export const SESSION_HEALTH_CHECK_TOOL = {
|
|
|
500
527
|
export function isSessionHealthCheckArgs(args) {
|
|
501
528
|
return typeof args === "object" && args !== null; // any object is valid
|
|
502
529
|
}
|
|
530
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion Tool ────────────
|
|
531
|
+
//
|
|
532
|
+
// This tool enables SURGICAL deletion of individual memory entries by ID.
|
|
533
|
+
// It supports two modes:
|
|
534
|
+
// 1. Soft Delete (default): Sets deleted_at = NOW(). The entry remains
|
|
535
|
+
// in the database for audit trails but is excluded from ALL search
|
|
536
|
+
// queries (both FTS5 and vector). This prevents the Top-K Hole
|
|
537
|
+
// problem where LIMIT N queries return fewer results than expected.
|
|
538
|
+
// 2. Hard Delete: Physical removal from the database. Irreversible.
|
|
539
|
+
// Use only when GDPR Article 17 requires complete erasure.
|
|
540
|
+
//
|
|
541
|
+
// DESIGN DECISION: This is intentionally separate from knowledge_forget,
|
|
542
|
+
// which operates on bulk filter criteria (project, category, age).
|
|
543
|
+
// session_forget_memory is surgical — one entry at a time — for
|
|
544
|
+
// precise GDPR compliance.
|
|
545
|
+
export const SESSION_FORGET_MEMORY_TOOL = {
|
|
546
|
+
name: "session_forget_memory",
|
|
547
|
+
description: "Forget (delete) a specific memory entry by its ID. " +
|
|
548
|
+
"Supports two modes:\n\n" +
|
|
549
|
+
"- **Soft delete** (default): Tombstones the entry — it stays in the database " +
|
|
550
|
+
"for audit trails but is excluded from all search results. Reversible.\n" +
|
|
551
|
+
"- **Hard delete**: Permanently removes the entry from the database. Irreversible. " +
|
|
552
|
+
"Use only when GDPR Article 17 requires complete erasure.\n\n" +
|
|
553
|
+
"⚠️ Soft delete is recommended for most use cases. The entry can be " +
|
|
554
|
+
"restored in the future if needed.",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
memory_id: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "The UUID of the memory (ledger) entry to forget. " +
|
|
561
|
+
"You can find this ID in search results returned by " +
|
|
562
|
+
"session_search_memory or knowledge_search.",
|
|
563
|
+
},
|
|
564
|
+
hard_delete: {
|
|
565
|
+
type: "boolean",
|
|
566
|
+
description: "If true, permanently removes the entry (irreversible). " +
|
|
567
|
+
"If false (default), soft-deletes by setting deleted_at timestamp. " +
|
|
568
|
+
"Soft-deleted entries are excluded from searches but remain in the database.",
|
|
569
|
+
},
|
|
570
|
+
reason: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Optional GDPR Article 17 justification for the deletion. " +
|
|
573
|
+
"Examples: 'User requested', 'Data retention policy', 'Outdated information'. " +
|
|
574
|
+
"Stored alongside the tombstone for audit trail purposes.",
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
required: ["memory_id"],
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* Type guard for session_forget_memory arguments.
|
|
582
|
+
* Validates that memory_id (required) is present and is a string.
|
|
583
|
+
* hard_delete and reason are optional.
|
|
584
|
+
*/
|
|
585
|
+
export function isSessionForgetMemoryArgs(args) {
|
|
586
|
+
return (typeof args === "object" &&
|
|
587
|
+
args !== null &&
|
|
588
|
+
"memory_id" in args &&
|
|
589
|
+
typeof args.memory_id === "string");
|
|
590
|
+
}
|