prism-mcp-server 4.3.0 → 4.6.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 +259 -81
- package/dist/dashboard/ui.js +333 -2
- package/dist/lifecycle.js +6 -0
- package/dist/server.js +234 -147
- package/dist/storage/sqlite.js +21 -0
- package/dist/storage/supabase.js +49 -15
- package/dist/storage/supabaseMigrations.js +42 -1
- package/dist/tools/compactionHandler.js +7 -14
- package/dist/tools/handlers.js +26 -3
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +53 -0
- package/dist/tools/sessionMemoryHandlers.js +263 -30
- package/dist/utils/briefing.js +9 -10
- package/dist/utils/factMerger.js +11 -16
- package/dist/utils/healthCheck.js +19 -22
- package/dist/utils/imageCaptioner.js +240 -0
- package/dist/utils/llm/adapters/anthropic.js +128 -0
- package/dist/utils/llm/adapters/gemini.js +152 -0
- package/dist/utils/llm/adapters/openai.js +183 -0
- package/dist/utils/llm/adapters/traced.js +190 -0
- package/dist/utils/llm/factory.js +143 -0
- package/dist/utils/llm/provider.js +25 -0
- package/dist/utils/telemetry.js +174 -0
- package/package.json +9 -2
package/dist/tools/handlers.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* helps the AI understand how much token budget was saved.
|
|
21
21
|
*/
|
|
22
22
|
import { performWebSearch, performWebSearchRaw, performLocalSearch, performLocalSearchRaw, performBraveAnswers } from "../utils/braveApi.js";
|
|
23
|
-
import {
|
|
23
|
+
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
24
24
|
import { isBraveWebSearchArgs, isBraveLocalSearchArgs, isBraveAnswersArgs, isGeminiResearchPaperAnalysisArgs, isBraveWebSearchCodeModeArgs, isBraveLocalSearchCodeModeArgs, isCodeModeTransformArgs } from "./definitions.js";
|
|
25
25
|
import { runInSandbox } from "../utils/executor.js";
|
|
26
26
|
import { CODE_MODE_TEMPLATES, getTemplateNames } from "../templates/codeMode.js";
|
|
@@ -216,8 +216,31 @@ export async function researchPaperAnalysisHandler(args) {
|
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
218
|
try {
|
|
219
|
-
debugLog(`Analyzing research paper
|
|
220
|
-
|
|
219
|
+
debugLog(`Analyzing research paper (${analysisType} analysis)...`);
|
|
220
|
+
// Build the analysis prompt (mirrors the original analyzePaperWithGemini logic)
|
|
221
|
+
let prompt = `I need you to perform a detailed ${analysisType} analysis of the following research paper.\n\n`;
|
|
222
|
+
if (additionalContext) {
|
|
223
|
+
prompt += `Additional context: ${additionalContext}\n\n`;
|
|
224
|
+
}
|
|
225
|
+
prompt += `Research paper content:\n${paperContent}\n\n`;
|
|
226
|
+
switch (analysisType.toLowerCase()) {
|
|
227
|
+
case "summary":
|
|
228
|
+
prompt += "Provide a comprehensive summary including the research question, methodology, key findings, and conclusions.";
|
|
229
|
+
break;
|
|
230
|
+
case "critique":
|
|
231
|
+
prompt += "Provide a critical evaluation of the research methodology, validity of findings, limitations, and suggestions for improvement.";
|
|
232
|
+
break;
|
|
233
|
+
case "literature review":
|
|
234
|
+
prompt += "Analyze how this paper fits into the broader research landscape, identifying key related works and research gaps.";
|
|
235
|
+
break;
|
|
236
|
+
case "key findings":
|
|
237
|
+
prompt += "Extract and explain the most significant findings and their implications.";
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
prompt += "Perform a comprehensive analysis including summary, methodology assessment, key findings, limitations, and significance.";
|
|
241
|
+
}
|
|
242
|
+
const llm = getLLMProvider();
|
|
243
|
+
const analysis = await llm.generateText(prompt);
|
|
221
244
|
return {
|
|
222
245
|
content: [{ type: "text", text: analysis }],
|
|
223
246
|
isError: false,
|
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, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler } 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, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler, sessionExportMemoryHandler } 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
|
|
@@ -605,6 +605,59 @@ export function isSessionForgetMemoryArgs(args) {
|
|
|
605
605
|
"memory_id" in args &&
|
|
606
606
|
typeof args.memory_id === "string");
|
|
607
607
|
}
|
|
608
|
+
// ─── Phase 2: GDPR Export Tool ─────────────────────────────────────────
|
|
609
|
+
//
|
|
610
|
+
// Complements session_forget_memory (surgical deletion) with a full data
|
|
611
|
+
// portability export. Fulfills GDPR Article 20 (Right to Data Portability).
|
|
612
|
+
// API keys are always redacted from the exported settings object.
|
|
613
|
+
export const SESSION_EXPORT_MEMORY_TOOL = {
|
|
614
|
+
name: "session_export_memory",
|
|
615
|
+
description: "Export all of a project's memory to a local file. " +
|
|
616
|
+
"Fulfills GDPR Article 20 (Right to Data Portability) and the " +
|
|
617
|
+
"'local-first' portability promise.\n\n" +
|
|
618
|
+
"**What is exported:**\n" +
|
|
619
|
+
"- All session ledger entries (summaries, decisions, TODOs, file changes)\n" +
|
|
620
|
+
"- Current handoff state (live project context)\n" +
|
|
621
|
+
"- System settings (API keys are \"**REDACTED**\" for security)\n" +
|
|
622
|
+
"- Visual memory index (descriptions, captions, timestamps; not the raw files)\n\n" +
|
|
623
|
+
"**Formats:**\n" +
|
|
624
|
+
"- `json` — machine-readable, suitable for import into another Prism instance\n" +
|
|
625
|
+
"- `markdown` — human-readable, ideal for Obsidian, Notion, or archiving\n\n" +
|
|
626
|
+
"⚠️ Output directory must exist and be writable. " +
|
|
627
|
+
"Filenames are auto-generated: `prism-export-<project>-<date>.(json|md)`",
|
|
628
|
+
inputSchema: {
|
|
629
|
+
type: "object",
|
|
630
|
+
properties: {
|
|
631
|
+
project: {
|
|
632
|
+
type: "string",
|
|
633
|
+
description: "Project to export. If omitted, exports ALL projects into separate files.",
|
|
634
|
+
},
|
|
635
|
+
format: {
|
|
636
|
+
type: "string",
|
|
637
|
+
enum: ["json", "markdown"],
|
|
638
|
+
description: "Export format: 'json' (machine-readable) or 'markdown' (human-readable). Default: json.",
|
|
639
|
+
default: "json",
|
|
640
|
+
},
|
|
641
|
+
output_dir: {
|
|
642
|
+
type: "string",
|
|
643
|
+
description: "Absolute path to the directory where the export file(s) will be written. " +
|
|
644
|
+
"Must exist and be writable. Example: '/Users/admin/Desktop'.",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
required: ["output_dir"],
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* Type guard for session_export_memory arguments.
|
|
652
|
+
* output_dir is required (must be an absolute path).
|
|
653
|
+
* project and format are optional.
|
|
654
|
+
*/
|
|
655
|
+
export function isSessionExportMemoryArgs(args) {
|
|
656
|
+
return (typeof args === "object" &&
|
|
657
|
+
args !== null &&
|
|
658
|
+
"output_dir" in args &&
|
|
659
|
+
typeof args.output_dir === "string");
|
|
660
|
+
}
|
|
608
661
|
// ─── v3.1: Knowledge Set Retention (TTL) ─────────────────────
|
|
609
662
|
export const KNOWLEDGE_SET_RETENTION_TOOL = {
|
|
610
663
|
name: "knowledge_set_retention",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
import { debugLog } from "../utils/logger.js";
|
|
19
19
|
import { getStorage } from "../storage/index.js";
|
|
20
20
|
import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
21
|
-
import {
|
|
21
|
+
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
22
22
|
import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
23
|
-
import { getSetting } from "../storage/configStorage.js";
|
|
23
|
+
import { getSetting, getAllSettings } from "../storage/configStorage.js";
|
|
24
24
|
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
25
25
|
// These utilities provide structured tracing metadata for search operations.
|
|
26
26
|
// When `enable_trace: true` is passed to session_search_memory or knowledge_search,
|
|
@@ -30,6 +30,7 @@ import { getSetting } from "../storage/configStorage.js";
|
|
|
30
30
|
import { createMemoryTrace, traceToContentBlock } from "../utils/tracing.js";
|
|
31
31
|
import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
|
|
32
32
|
import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
33
|
+
import { fireCaptionAsync } from "../utils/imageCaptioner.js";
|
|
33
34
|
import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isKnowledgeSearchArgs, isKnowledgeForgetArgs, isSessionSearchMemoryArgs, isBackfillEmbeddingsArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, isSessionHealthCheckArgs, // v2.2.0: health check type guard
|
|
34
35
|
isSessionForgetMemoryArgs, // Phase 2: GDPR-compliant memory deletion type guard
|
|
35
36
|
isKnowledgeSetRetentionArgs, // v3.1: TTL retention policy type guard
|
|
@@ -40,7 +41,7 @@ isKnowledgeSyncRulesArgs, } from "./sessionMemoryDefinitions.js";
|
|
|
40
41
|
// v4.2: File system access for knowledge_sync_rules
|
|
41
42
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
42
43
|
import { existsSync } from "node:fs";
|
|
43
|
-
import { join, dirname } from "node:path";
|
|
44
|
+
import { join, dirname, resolve, isAbsolute, sep } from "node:path";
|
|
44
45
|
// v3.1: In-memory debounce lock for auto-compaction.
|
|
45
46
|
// Prevents multiple concurrent Gemini compaction tasks for the same project
|
|
46
47
|
// when many agents call session_save_ledger at the same time.
|
|
@@ -102,7 +103,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
102
103
|
const savedEntry = Array.isArray(result) ? result[0] : result;
|
|
103
104
|
const entryId = savedEntry?.id;
|
|
104
105
|
if (entryId) {
|
|
105
|
-
generateEmbedding(embeddingText)
|
|
106
|
+
getLLMProvider().generateEmbedding(embeddingText)
|
|
106
107
|
.then(async (embedding) => {
|
|
107
108
|
await storage.patchLedger(entryId, {
|
|
108
109
|
embedding: JSON.stringify(embedding),
|
|
@@ -144,6 +145,13 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
144
145
|
activeCompactions.delete(project);
|
|
145
146
|
}
|
|
146
147
|
}).catch(() => { });
|
|
148
|
+
// ─── Fire-and-forget importance decay (v4.3) ──────────────
|
|
149
|
+
// Decays stale behavioral insights (>30d old) by -1 importance.
|
|
150
|
+
// Matches SQLite's automatic decay behavior on every save.
|
|
151
|
+
// Non-fatal: errors are logged but never surfaced to the caller.
|
|
152
|
+
storage.decayImportance(project, PRISM_USER_ID, 30).catch((err) => {
|
|
153
|
+
debugLog(`[session_save_ledger] Background decay failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
154
|
+
});
|
|
147
155
|
return {
|
|
148
156
|
content: [{
|
|
149
157
|
type: "text",
|
|
@@ -824,7 +832,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
824
832
|
// This is the most variable component: 50ms on a good day, 2000ms under load.
|
|
825
833
|
const embeddingStart = performance.now();
|
|
826
834
|
try {
|
|
827
|
-
queryEmbedding = await generateEmbedding(query);
|
|
835
|
+
queryEmbedding = await getLLMProvider().generateEmbedding(query);
|
|
828
836
|
}
|
|
829
837
|
catch (err) {
|
|
830
838
|
return {
|
|
@@ -1013,7 +1021,7 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
1013
1021
|
failed++;
|
|
1014
1022
|
continue;
|
|
1015
1023
|
}
|
|
1016
|
-
const embedding = await generateEmbedding(textToEmbed);
|
|
1024
|
+
const embedding = await getLLMProvider().generateEmbedding(textToEmbed);
|
|
1017
1025
|
await storage.patchLedger(e.id, {
|
|
1018
1026
|
embedding: JSON.stringify(embedding),
|
|
1019
1027
|
});
|
|
@@ -1244,6 +1252,8 @@ export async function sessionSaveImageHandler(args) {
|
|
|
1244
1252
|
const fileSize = fs.statSync(vaultPath).size;
|
|
1245
1253
|
const sizeKB = (fileSize / 1024).toFixed(1);
|
|
1246
1254
|
debugLog(`[Visual Memory] Saved image [${imageId}] for "${project}" (${sizeKB}KB, ${ext})`);
|
|
1255
|
+
// Fire-and-forget VLM captioning (2-5s — don’t block the MCP response)
|
|
1256
|
+
fireCaptionAsync(project, imageId, vaultPath, description);
|
|
1247
1257
|
return {
|
|
1248
1258
|
content: [{
|
|
1249
1259
|
type: "text",
|
|
@@ -1251,7 +1261,8 @@ export async function sessionSaveImageHandler(args) {
|
|
|
1251
1261
|
`• ID: \`${imageId}\`\n` +
|
|
1252
1262
|
`• Description: ${description}\n` +
|
|
1253
1263
|
`• Format: ${ext} (${sizeKB}KB)\n` +
|
|
1254
|
-
`• Vault: ${vaultPath}\n
|
|
1264
|
+
`• Vault: ${vaultPath}\n` +
|
|
1265
|
+
`• Captioning: ⏳ queued (will be searchable in ~5s)\n\n` +
|
|
1255
1266
|
`Use \`session_view_image("${project}", "${imageId}")\` to retrieve it later.`,
|
|
1256
1267
|
}],
|
|
1257
1268
|
isError: false,
|
|
@@ -1321,7 +1332,8 @@ export async function sessionViewImageHandler(args) {
|
|
|
1321
1332
|
type: "text",
|
|
1322
1333
|
text: `🖼️ Visual Memory [${image_id}]: ${imgMeta.description}\n` +
|
|
1323
1334
|
`Saved: ${imgMeta.timestamp?.split("T")[0] || "unknown"}\n` +
|
|
1324
|
-
`Format: ${ext.replace(".", "").toUpperCase()} (${(fileSize / 1024).toFixed(1)}KB)
|
|
1335
|
+
`Format: ${ext.replace(".", "").toUpperCase()} (${(fileSize / 1024).toFixed(1)}KB)` +
|
|
1336
|
+
(imgMeta.caption ? `\n\n🤖 VLM Caption:\n${imgMeta.caption}` : "\n\n⏳ Caption: generating..."),
|
|
1325
1337
|
},
|
|
1326
1338
|
{
|
|
1327
1339
|
type: "image",
|
|
@@ -1645,7 +1657,7 @@ export async function sessionSaveExperienceHandler(args) {
|
|
|
1645
1657
|
const savedEntry = Array.isArray(result) ? result[0] : result;
|
|
1646
1658
|
const entryId = savedEntry?.id;
|
|
1647
1659
|
if (entryId) {
|
|
1648
|
-
generateEmbedding(embeddingText)
|
|
1660
|
+
getLLMProvider().generateEmbedding(embeddingText)
|
|
1649
1661
|
.then(async (embedding) => {
|
|
1650
1662
|
await storage.patchLedger(entryId, {
|
|
1651
1663
|
embedding: JSON.stringify(embedding),
|
|
@@ -1679,15 +1691,21 @@ export async function knowledgeUpvoteHandler(args) {
|
|
|
1679
1691
|
throw new Error("Invalid arguments for knowledge_upvote");
|
|
1680
1692
|
}
|
|
1681
1693
|
const storage = await getStorage();
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1694
|
+
try {
|
|
1695
|
+
await storage.adjustImportance(args.id, 1, PRISM_USER_ID);
|
|
1696
|
+
debugLog(`[knowledge_upvote] Upvoted entry ${args.id}`);
|
|
1697
|
+
return {
|
|
1698
|
+
content: [{ type: "text", text: `👍 Entry ${args.id} upvoted (+1 importance).` }],
|
|
1699
|
+
isError: false,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
catch (err) {
|
|
1703
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1704
|
+
return {
|
|
1705
|
+
content: [{ type: "text", text: `❌ Failed to upvote entry ${args.id}: ${msg}` }],
|
|
1706
|
+
isError: true,
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1691
1709
|
}
|
|
1692
1710
|
// ─── v4.0: Knowledge Downvote Handler ────────────────────────
|
|
1693
1711
|
/**
|
|
@@ -1699,15 +1717,21 @@ export async function knowledgeDownvoteHandler(args) {
|
|
|
1699
1717
|
throw new Error("Invalid arguments for knowledge_downvote");
|
|
1700
1718
|
}
|
|
1701
1719
|
const storage = await getStorage();
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1720
|
+
try {
|
|
1721
|
+
await storage.adjustImportance(args.id, -1, PRISM_USER_ID);
|
|
1722
|
+
debugLog(`[knowledge_downvote] Downvoted entry ${args.id}`);
|
|
1723
|
+
return {
|
|
1724
|
+
content: [{ type: "text", text: `👎 Entry ${args.id} downvoted (-1 importance).` }],
|
|
1725
|
+
isError: false,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
catch (err) {
|
|
1729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1730
|
+
return {
|
|
1731
|
+
content: [{ type: "text", text: `❌ Failed to downvote entry ${args.id}: ${msg}` }],
|
|
1732
|
+
isError: true,
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1711
1735
|
}
|
|
1712
1736
|
// ─── v4.2: Knowledge Sync Rules Handler ─────────────────────
|
|
1713
1737
|
//
|
|
@@ -1729,7 +1753,7 @@ const SENTINEL_END = "<!-- PRISM:AUTO-RULES:END -->";
|
|
|
1729
1753
|
*/
|
|
1730
1754
|
function formatRulesBlock(insights, project) {
|
|
1731
1755
|
const header = `## Prism Graduated Insights (auto-synced)\n\n` +
|
|
1732
|
-
`> These rules were automatically generated by [Prism MCP](https://github.com/
|
|
1756
|
+
`> These rules were automatically generated by [Prism MCP](https://github.com/dcostenco/prism-mcp) ` +
|
|
1733
1757
|
`from behavioral memory for project \"${project}\".\n` +
|
|
1734
1758
|
`> Last synced: ${new Date().toISOString().split("T")[0]}\n\n`;
|
|
1735
1759
|
const rules = insights.map(i => {
|
|
@@ -1804,8 +1828,32 @@ export async function knowledgeSyncRulesHandler(args) {
|
|
|
1804
1828
|
isError: false,
|
|
1805
1829
|
};
|
|
1806
1830
|
}
|
|
1807
|
-
// 5. Idempotent file write
|
|
1808
|
-
|
|
1831
|
+
// 5. Idempotent file write — with path traversal protection
|
|
1832
|
+
// Reject absolute paths (e.g. "/etc/hosts")
|
|
1833
|
+
if (isAbsolute(target_file)) {
|
|
1834
|
+
return {
|
|
1835
|
+
content: [{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: `❌ Security Error: target_file cannot be an absolute path. Got: "${target_file}"`,
|
|
1838
|
+
}],
|
|
1839
|
+
isError: true,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
// Resolve both paths to their canonical forms, then assert containment
|
|
1843
|
+
const resolvedRepo = resolve(normalizedRepoPath);
|
|
1844
|
+
const targetPath = resolve(resolvedRepo, target_file);
|
|
1845
|
+
// Ensure the resolved target is strictly inside the repo root
|
|
1846
|
+
// (handles "../../../etc/hosts" style traversal)
|
|
1847
|
+
if (!targetPath.startsWith(resolvedRepo + sep)) {
|
|
1848
|
+
return {
|
|
1849
|
+
content: [{
|
|
1850
|
+
type: "text",
|
|
1851
|
+
text: `❌ Security Error: Path traversal blocked.\n` +
|
|
1852
|
+
`"${target_file}" resolves outside the repo root "${resolvedRepo}".`,
|
|
1853
|
+
}],
|
|
1854
|
+
isError: true,
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1809
1857
|
// Ensure directory exists (handles nested target_file like ".config/rules.md")
|
|
1810
1858
|
const targetDir = dirname(targetPath);
|
|
1811
1859
|
if (!existsSync(targetDir)) {
|
|
@@ -1833,3 +1881,188 @@ export async function knowledgeSyncRulesHandler(args) {
|
|
|
1833
1881
|
isError: false,
|
|
1834
1882
|
};
|
|
1835
1883
|
}
|
|
1884
|
+
// ────────────────────────────────────────────────────────
|
|
1885
|
+
// GDPR Export Handler (v4.5.1)
|
|
1886
|
+
// Implements session_export_memory.
|
|
1887
|
+
// Article 20: Right to Data Portability — fully local, no network calls.
|
|
1888
|
+
// ────────────────────────────────────────────────────────
|
|
1889
|
+
import { isSessionExportMemoryArgs, } from "./sessionMemoryDefinitions.js";
|
|
1890
|
+
// Keys whose values must be redacted from the export.
|
|
1891
|
+
// Matches any setting key ending with "_api_key" or "_secret".
|
|
1892
|
+
const REDACT_PATTERNS = [/_api_key$/i, /_secret$/i, /^password$/i];
|
|
1893
|
+
function redactSettings(settings) {
|
|
1894
|
+
const redacted = {};
|
|
1895
|
+
for (const [k, v] of Object.entries(settings)) {
|
|
1896
|
+
redacted[k] = REDACT_PATTERNS.some(p => p.test(k)) ? "**REDACTED**" : v;
|
|
1897
|
+
}
|
|
1898
|
+
return redacted;
|
|
1899
|
+
}
|
|
1900
|
+
function toMarkdown(exportData) {
|
|
1901
|
+
const data = exportData;
|
|
1902
|
+
const d = data.prism_export;
|
|
1903
|
+
const lines = [];
|
|
1904
|
+
lines.push(`# Prism Memory Export: \`${d.project}\``);
|
|
1905
|
+
lines.push(``);
|
|
1906
|
+
lines.push(`> Exported: ${d.exported_at} | Version: ${d.version}`);
|
|
1907
|
+
lines.push(``);
|
|
1908
|
+
// ── Settings
|
|
1909
|
+
lines.push(`## ⚙️ Settings`);
|
|
1910
|
+
lines.push(``);
|
|
1911
|
+
lines.push(`| Key | Value |`);
|
|
1912
|
+
lines.push(`|-----|-------|`);
|
|
1913
|
+
for (const [k, v] of Object.entries(d.settings)) {
|
|
1914
|
+
lines.push(`| \`${k}\` | ${v} |`);
|
|
1915
|
+
}
|
|
1916
|
+
lines.push(``);
|
|
1917
|
+
// ── Handoff State
|
|
1918
|
+
lines.push(`## 🎯 Live Project State (Handoff)`);
|
|
1919
|
+
lines.push(``);
|
|
1920
|
+
lines.push(`\`\`\`json`);
|
|
1921
|
+
lines.push(JSON.stringify(d.handoff, null, 2));
|
|
1922
|
+
lines.push(`\`\`\``);
|
|
1923
|
+
lines.push(``);
|
|
1924
|
+
// ── Visual Memory
|
|
1925
|
+
if (Array.isArray(d.visual_memory) && d.visual_memory.length > 0) {
|
|
1926
|
+
lines.push(`## 🖼️ Visual Memory (${d.visual_memory.length} images)`);
|
|
1927
|
+
lines.push(``);
|
|
1928
|
+
for (const img of d.visual_memory) {
|
|
1929
|
+
lines.push(`### ${img.id ?? "??"}`);
|
|
1930
|
+
lines.push(`- **Description:** ${img.description ?? "-"}`);
|
|
1931
|
+
lines.push(`- **Saved:** ${String(img.timestamp ?? "-").split("T")[0]}`);
|
|
1932
|
+
if (img.caption)
|
|
1933
|
+
lines.push(`- **VLM Caption:** ${img.caption}`);
|
|
1934
|
+
}
|
|
1935
|
+
lines.push(``);
|
|
1936
|
+
}
|
|
1937
|
+
// ── Ledger
|
|
1938
|
+
lines.push(`## 📚 Session Ledger (${d.ledger.length} entries)`);
|
|
1939
|
+
lines.push(``);
|
|
1940
|
+
for (const entry of d.ledger) {
|
|
1941
|
+
const date = entry.created_at?.split("T")[0] ?? "unknown";
|
|
1942
|
+
const type = entry.event_type ?? "session";
|
|
1943
|
+
lines.push(`---`);
|
|
1944
|
+
lines.push(``);
|
|
1945
|
+
lines.push(`### ${date} \u00b7 \`${type}\` ${entry.id ? `\`${entry.id.slice(0, 8)}\`` : ""}`);
|
|
1946
|
+
lines.push(``);
|
|
1947
|
+
lines.push(entry.summary);
|
|
1948
|
+
if (entry.decisions?.length) {
|
|
1949
|
+
lines.push(``);
|
|
1950
|
+
lines.push(`**Decisions:**`);
|
|
1951
|
+
entry.decisions.forEach(d => lines.push(`- ${d}`));
|
|
1952
|
+
}
|
|
1953
|
+
if (entry.todos?.length) {
|
|
1954
|
+
lines.push(``);
|
|
1955
|
+
lines.push(`**TODOs:**`);
|
|
1956
|
+
entry.todos.forEach(t => lines.push(`- [ ] ${t}`));
|
|
1957
|
+
}
|
|
1958
|
+
if (entry.files_changed?.length) {
|
|
1959
|
+
lines.push(``);
|
|
1960
|
+
lines.push(`**Files:** ${entry.files_changed.join(", ")}`);
|
|
1961
|
+
}
|
|
1962
|
+
lines.push(``);
|
|
1963
|
+
}
|
|
1964
|
+
return lines.join("\n");
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Export a project's full memory (ledger + handoff + settings + visual memory)
|
|
1968
|
+
* to a local file. No network calls. API keys always redacted.
|
|
1969
|
+
*/
|
|
1970
|
+
export async function sessionExportMemoryHandler(args) {
|
|
1971
|
+
if (!isSessionExportMemoryArgs(args)) {
|
|
1972
|
+
return {
|
|
1973
|
+
content: [{ type: "text", text: "Error: output_dir (string) is required." }],
|
|
1974
|
+
isError: true,
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
const { output_dir, format = "json" } = args;
|
|
1978
|
+
const requestedProject = args.project;
|
|
1979
|
+
// Validate output directory
|
|
1980
|
+
if (!existsSync(output_dir)) {
|
|
1981
|
+
return {
|
|
1982
|
+
content: [{
|
|
1983
|
+
type: "text",
|
|
1984
|
+
text: `Error: output_dir does not exist: "${output_dir}". Please create it first.`,
|
|
1985
|
+
}],
|
|
1986
|
+
isError: true,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
const storage = await getStorage();
|
|
1990
|
+
const exportedFiles = [];
|
|
1991
|
+
try {
|
|
1992
|
+
// Determine which projects to export
|
|
1993
|
+
let projects;
|
|
1994
|
+
if (requestedProject) {
|
|
1995
|
+
projects = [requestedProject];
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
projects = await storage.listProjects();
|
|
1999
|
+
if (projects.length === 0) {
|
|
2000
|
+
return {
|
|
2001
|
+
content: [{ type: "text", text: "No projects found in memory — nothing to export." }],
|
|
2002
|
+
isError: false,
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
// Fetch settings once (shared across all projects)
|
|
2007
|
+
const rawSettings = await getAllSettings();
|
|
2008
|
+
const safeSettings = redactSettings(rawSettings);
|
|
2009
|
+
const exportedAt = new Date().toISOString();
|
|
2010
|
+
const dateSuffix = exportedAt.split("T")[0]; // YYYY-MM-DD
|
|
2011
|
+
for (const project of projects) {
|
|
2012
|
+
debugLog(`[session_export_memory] Exporting project "${project}" as ${format}`);
|
|
2013
|
+
// Fetch handoff (live context)
|
|
2014
|
+
const ctx = await storage.loadContext(project, "deep", PRISM_USER_ID);
|
|
2015
|
+
// Fetch full ledger (all non-deleted entries)
|
|
2016
|
+
const ledger = await storage.getLedgerEntries({ project });
|
|
2017
|
+
// Strip raw embedding vectors from the export (large binary data)
|
|
2018
|
+
const cleanLedger = ledger.map(({ embedding: _emb, ...rest }) => rest);
|
|
2019
|
+
const visualMemory = ctx?.metadata?.visual_memory ?? [];
|
|
2020
|
+
const exportPayload = {
|
|
2021
|
+
prism_export: {
|
|
2022
|
+
version: "4.5",
|
|
2023
|
+
exported_at: exportedAt,
|
|
2024
|
+
project,
|
|
2025
|
+
settings: safeSettings,
|
|
2026
|
+
handoff: ctx ?? null,
|
|
2027
|
+
visual_memory: visualMemory,
|
|
2028
|
+
ledger: cleanLedger,
|
|
2029
|
+
},
|
|
2030
|
+
};
|
|
2031
|
+
// Serialize
|
|
2032
|
+
const ext = format === "markdown" ? "md" : "json";
|
|
2033
|
+
const filename = `prism-export-${project}-${dateSuffix}.${ext}`;
|
|
2034
|
+
const outputPath = join(output_dir, filename);
|
|
2035
|
+
let content;
|
|
2036
|
+
if (format === "markdown") {
|
|
2037
|
+
content = toMarkdown(exportPayload);
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
content = JSON.stringify(exportPayload, null, 2);
|
|
2041
|
+
}
|
|
2042
|
+
await writeFile(outputPath, content, "utf-8");
|
|
2043
|
+
exportedFiles.push(outputPath);
|
|
2044
|
+
debugLog(`[session_export_memory] Wrote ${content.length} bytes to ${outputPath}`);
|
|
2045
|
+
}
|
|
2046
|
+
const plural = exportedFiles.length > 1 ? "files" : "file";
|
|
2047
|
+
return {
|
|
2048
|
+
content: [{
|
|
2049
|
+
type: "text",
|
|
2050
|
+
text: `✅ Memory exported successfully (${format.toUpperCase()})\n\n` +
|
|
2051
|
+
`**Project(s):** ${projects.join(", ")}\n` +
|
|
2052
|
+
`**${exportedFiles.length} ${plural} written:**\n` +
|
|
2053
|
+
exportedFiles.map(f => ` \u2022 \`${f}\``).join("\n") +
|
|
2054
|
+
`\n\n⚠️ API keys have been redacted. Vault image files are NOT included — ` +
|
|
2055
|
+
`only metadata and captions. Re-run \`session_save_image\` to re-attach images.`,
|
|
2056
|
+
}],
|
|
2057
|
+
isError: false,
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
catch (err) {
|
|
2061
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2062
|
+
console.error(`[session_export_memory] Error: ${msg}`);
|
|
2063
|
+
return {
|
|
2064
|
+
content: [{ type: "text", text: `Export failed: ${msg}` }],
|
|
2065
|
+
isError: true,
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
}
|
package/dist/utils/briefing.js
CHANGED
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* - Reuses GOOGLE_API_KEY from config.ts (same key as embeddings)
|
|
13
13
|
* ═══════════════════════════════════════════════════════════════════
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { GOOGLE_API_KEY } from "../config.js";
|
|
15
|
+
import { getLLMProvider } from "./llm/factory.js";
|
|
17
16
|
import { debugLog } from "./logger.js";
|
|
18
17
|
/**
|
|
19
18
|
* Generates a 3-bullet Morning Briefing using Gemini.
|
|
@@ -23,12 +22,13 @@ import { debugLog } from "./logger.js";
|
|
|
23
22
|
* @returns Formatted briefing text, or a graceful fallback string
|
|
24
23
|
*/
|
|
25
24
|
export async function generateMorningBriefing(context, recentEntries) {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
let llm;
|
|
26
|
+
try {
|
|
27
|
+
llm = getLLMProvider();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "☀️ Good morning! (Morning Briefing unavailable — LLM provider not configured)";
|
|
28
31
|
}
|
|
29
|
-
const genAI = new GoogleGenerativeAI(GOOGLE_API_KEY);
|
|
30
|
-
// 2.5-flash for speed — briefings should take ≤3s
|
|
31
|
-
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
|
|
32
32
|
const todosBlock = context.pendingTodos?.length
|
|
33
33
|
? `Pending TODOs:\n${context.pendingTodos.map(t => ` • ${t}`).join("\n")}`
|
|
34
34
|
: "No pending TODOs.";
|
|
@@ -57,13 +57,12 @@ Rules:
|
|
|
57
57
|
- No preamble, no closing remarks — just the 3 bullets.
|
|
58
58
|
- Each bullet starts with a relevant emoji.`;
|
|
59
59
|
try {
|
|
60
|
-
const
|
|
61
|
-
const text = result.response.text().trim();
|
|
60
|
+
const text = (await llm.generateText(prompt)).trim();
|
|
62
61
|
debugLog(`[Morning Briefing] Generated for "${context.project}" (${text.length} chars)`);
|
|
63
62
|
return text;
|
|
64
63
|
}
|
|
65
64
|
catch (error) {
|
|
66
|
-
console.error(`[Morning Briefing]
|
|
65
|
+
console.error(`[Morning Briefing] LLM call failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
66
|
return "☀️ Good morning! Ready to continue — check your TODOs above to pick up where you left off.";
|
|
68
67
|
}
|
|
69
68
|
}
|
package/dist/utils/factMerger.js
CHANGED
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
* - Uses gemini-2.5-flash for speed (~2-3s per merge)
|
|
34
34
|
* ═══════════════════════════════════════════════════════════════════
|
|
35
35
|
*/
|
|
36
|
-
import {
|
|
37
|
-
import { GOOGLE_API_KEY } from "../config.js"; // API key from environment
|
|
36
|
+
import { getLLMProvider } from "./llm/factory.js";
|
|
38
37
|
import { debugLog } from "./logger.js";
|
|
39
38
|
/**
|
|
40
39
|
* Merge old and new key_context using Gemini to resolve contradictions.
|
|
@@ -56,9 +55,13 @@ import { debugLog } from "./logger.js";
|
|
|
56
55
|
* // Result: "We use MySQL for the main DB"
|
|
57
56
|
*/
|
|
58
57
|
export async function consolidateFacts(oldContext, newContext) {
|
|
59
|
-
// Guard: need
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Guard: need LLM provider — factory throws if no API key
|
|
59
|
+
let llm;
|
|
60
|
+
try {
|
|
61
|
+
llm = getLLMProvider();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
debugLog("[FactMerger] Skipped — LLM provider unavailable (no API key configured)");
|
|
62
65
|
return newContext; // fallback: just use the new context as-is
|
|
63
66
|
}
|
|
64
67
|
// Guard: if either context is empty, no merging needed
|
|
@@ -73,13 +76,7 @@ export async function consolidateFacts(oldContext, newContext) {
|
|
|
73
76
|
debugLog("[FactMerger] Old and new context are identical — skipping merge");
|
|
74
77
|
return newContext; // no changes needed
|
|
75
78
|
}
|
|
76
|
-
//
|
|
77
|
-
const genAI = new GoogleGenerativeAI(GOOGLE_API_KEY);
|
|
78
|
-
// Use gemini-2.5-flash for speed — merges should complete in ~2-3s
|
|
79
|
-
const model = genAI.getGenerativeModel({
|
|
80
|
-
model: "gemini-2.5-flash",
|
|
81
|
-
});
|
|
82
|
-
// Build the merge prompt — instructs Gemini to resolve contradictions
|
|
79
|
+
// Build the merge prompt — instructs LLM to resolve contradictions
|
|
83
80
|
// and deduplicate while keeping the NEW UPDATE as source of truth
|
|
84
81
|
const prompt = "You are a memory consolidation engine for an AI agent.\n\n" +
|
|
85
82
|
"OLD MEMORY:\n" + oldContext + "\n\n" +
|
|
@@ -92,10 +89,8 @@ export async function consolidateFacts(oldContext, newContext) {
|
|
|
92
89
|
"4. Preserve unique facts from both old and new that don't conflict.\n" +
|
|
93
90
|
"5. Return ONLY the consolidated raw text. No markdown, no preamble, " +
|
|
94
91
|
"no explanation — just the merged facts.";
|
|
95
|
-
// Call
|
|
96
|
-
const
|
|
97
|
-
// Extract and trim the merged text from Gemini's response
|
|
98
|
-
const mergedText = result.response.text().trim();
|
|
92
|
+
// Call LLM to perform the intelligent merge
|
|
93
|
+
const mergedText = (await llm.generateText(prompt)).trim();
|
|
99
94
|
// Log the merge result for debugging (to stderr, not stdout)
|
|
100
95
|
debugLog("[FactMerger] Merged context (" +
|
|
101
96
|
oldContext.length + " chars old + " +
|