prism-mcp-server 20.7.0 → 20.8.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 CHANGED
@@ -56,6 +56,30 @@ Prism works locally without an account, API key, or cloud subscription. Add a
56
56
  Synalux subscription when you want cloud memory, paid-tier skills, or team
57
57
  features.
58
58
 
59
+ ### Install as a plugin
60
+
61
+ Prism also ships as a plugin, which registers the MCP server and the startup
62
+ skill for you.
63
+
64
+ **Claude Code** — from the community marketplace:
65
+
66
+ ```bash
67
+ /plugin marketplace add anthropics/claude-plugins-community
68
+ /plugin install synalux-prism@claude-community
69
+ ```
70
+
71
+ **Codex** — this repository is itself a plugin marketplace:
72
+
73
+ ```bash
74
+ codex plugin marketplace add dcostenco/prism-coder
75
+ codex plugin add synalux-prism@prism
76
+ ```
77
+
78
+ The plugin registers `prism-mcp` via `npx -y prism-mcp-server`. If you already
79
+ configured Prism by hand — `prism connect` writes an `mcp_servers.prism-mcp`
80
+ entry — you have that server twice under one key. Install the plugin **or**
81
+ run `prism connect`, not both.
82
+
59
83
  ### What `prism connect` changes about host subagents
60
84
 
61
85
  `connect` steers bounded work to `prism_infer` on your machine rather than to
package/dist/connect.js CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
1
+ import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as win32Path } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -1287,6 +1287,29 @@ function registerCodexTomlHost(definition, entry, dryRun, refresh, beforeCommit)
1287
1287
  return result(definition, "error", `could not parse config: ${error instanceof Error ? error.message : String(error)}`);
1288
1288
  }
1289
1289
  }
1290
+ // A Codex plugin may already register prism-mcp. Writing our own entry would
1291
+ // configure the same server twice under one key, so every Prism tool appears
1292
+ // in duplicate and load order decides which wins. Skip instead, and say why —
1293
+ // an unexplained no-op is worse than the duplicate it prevents.
1294
+ // Build the set of plugins Codex will actually load: present in [plugins]
1295
+ // and not explicitly disabled. Detection must not fire on a stale cache
1296
+ // entry for a plugin that is disabled or gone.
1297
+ const enabledPluginKeys = new Set();
1298
+ const pluginsTable = isJsonObject(config) ? config.plugins : undefined;
1299
+ if (isJsonObject(pluginsTable)) {
1300
+ for (const [key, entry] of Object.entries(pluginsTable)) {
1301
+ if (isJsonObject(entry) && entry.enabled !== false)
1302
+ enabledPluginKeys.add(key);
1303
+ }
1304
+ }
1305
+ const providingPlugin = codexPluginProvidesPrismMcp(dirname(configPath), enabledPluginKeys);
1306
+ if (providingPlugin && !locateCodexManagedBlock(originalText ?? "")) {
1307
+ // "existing" rather than a new status: the server IS already registered,
1308
+ // just by the plugin rather than by us. Nothing to do is the truth.
1309
+ return result(definition, "existing", `the Codex plugin ${providingPlugin} already registers prism-mcp; ` +
1310
+ "not adding a second registration under the same key. Remove the plugin " +
1311
+ "if you would rather prism connect manage this server.");
1312
+ }
1290
1313
  let managedBlock;
1291
1314
  try {
1292
1315
  managedBlock = locateCodexManagedBlock(originalText ?? "");
@@ -1377,6 +1400,71 @@ function serializeCodexManagedBlock(entry, existingText) {
1377
1400
  const serialized = stringifyToml({ mcp_servers: { "prism-mcp": entry } }).trimEnd();
1378
1401
  return `${CODEX_MANAGED_START}\n${serialized}\n${CODEX_MANAGED_END}\n`.replaceAll("\n", newline);
1379
1402
  }
1403
+ /**
1404
+ * Is a Codex plugin already providing the prism-mcp server?
1405
+ *
1406
+ * Installing the plugin and running `prism connect` both register a server
1407
+ * under the key `prism-mcp`, so doing both leaves the same server configured
1408
+ * twice — every Prism tool appears in duplicate, and which one wins depends on
1409
+ * load order. Previously this was only documented ("install the plugin OR run
1410
+ * prism connect, not both"), which puts the burden on the user to remember.
1411
+ *
1412
+ * Detection reads the installed plugin's own manifest rather than matching a
1413
+ * plugin NAME, so it keeps working if the plugin is renamed or vendored:
1414
+ * <codexHome>/plugins/cache/<marketplace>/<plugin>/<version>/.mcp.json
1415
+ *
1416
+ * A cache manifest is necessary but NOT sufficient: a disabled or half-removed
1417
+ * plugin leaves its .mcp.json on disk (verified 2026-08-06 — flipping
1418
+ * `enabled = false` in config.toml does not clear the cache). Counting file
1419
+ * presence alone would make `prism connect` skip its own registration for a
1420
+ * plugin that is not actually providing the server, leaving the user with no
1421
+ * prism-mcp at all. So the plugin@marketplace key must ALSO be enabled in the
1422
+ * caller's parsed config for it to count.
1423
+ */
1424
+ export function codexPluginProvidesPrismMcp(codexHome, enabledPluginKeys) {
1425
+ const cacheRoot = join(codexHome, "plugins", "cache");
1426
+ let marketplaces;
1427
+ try {
1428
+ marketplaces = readdirSync(cacheRoot);
1429
+ }
1430
+ catch {
1431
+ return null; // no plugins installed
1432
+ }
1433
+ for (const marketplace of marketplaces) {
1434
+ let plugins;
1435
+ try {
1436
+ plugins = readdirSync(join(cacheRoot, marketplace));
1437
+ }
1438
+ catch {
1439
+ continue;
1440
+ }
1441
+ for (const plugin of plugins) {
1442
+ let versions;
1443
+ try {
1444
+ versions = readdirSync(join(cacheRoot, marketplace, plugin));
1445
+ }
1446
+ catch {
1447
+ continue;
1448
+ }
1449
+ for (const version of versions) {
1450
+ const manifest = join(cacheRoot, marketplace, plugin, version, ".mcp.json");
1451
+ try {
1452
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
1453
+ const key = `${plugin}@${marketplace}`;
1454
+ if (enabledPluginKeys.has(key) &&
1455
+ parsed?.mcpServers &&
1456
+ Object.prototype.hasOwnProperty.call(parsed.mcpServers, "prism-mcp")) {
1457
+ return key;
1458
+ }
1459
+ }
1460
+ catch {
1461
+ // absent or unreadable manifest — not a provider
1462
+ }
1463
+ }
1464
+ }
1465
+ }
1466
+ return null;
1467
+ }
1380
1468
  function validateCodexCandidate(text) {
1381
1469
  const parsed = parseToml(text);
1382
1470
  if (!isJsonObject(parsed) || !isJsonObject(parsed.mcp_servers)) {
@@ -5,12 +5,60 @@ import { getSetting } from "./configStorage.js";
5
5
  export function isValidHttpUrl(url) {
6
6
  try {
7
7
  const parsed = new URL(url);
8
- return parsed.protocol === "http:" || parsed.protocol === "https:";
8
+ if (parsed.protocol === "https:")
9
+ return true;
10
+ // Plain http is accepted ONLY for loopback, where the traffic never leaves
11
+ // the machine — the local Supabase stack runs on 127.0.0.1:54321.
12
+ //
13
+ // Every caller of this function is gating a CLOUD backend, so accepting
14
+ // http for a remote host meant session content — summaries, decisions,
15
+ // filenames — could be sent unencrypted. The privacy policy states this
16
+ // traffic travels over TLS; before this change that was true only because
17
+ // the default base URL happens to be https, not because anything enforced
18
+ // it. A published claim should be guaranteed by the code, not by a default
19
+ // the user can silently override.
20
+ if (parsed.protocol !== "http:")
21
+ return false;
22
+ const host = parsed.hostname;
23
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
9
24
  }
10
25
  catch {
11
26
  return false;
12
27
  }
13
28
  }
29
+ /**
30
+ * Upgrade a remote http:// cloud URL to https:// rather than rejecting it.
31
+ *
32
+ * Rejecting was the first fix for the plaintext gap, and it closed the hole
33
+ * but reported it badly: a user who set an http base URL got "credentials are
34
+ * missing or invalid", which says nothing about the protocol being the
35
+ * problem. Upgrading keeps the guarantee — session content never leaves over
36
+ * plaintext — while letting a working configuration keep working.
37
+ *
38
+ * Loopback is left alone: http on 127.0.0.1 never crosses a network, and the
39
+ * local Supabase stack serves plain http on 54321.
40
+ *
41
+ * The upgrade is announced, not silent. If the host genuinely has no TLS
42
+ * listener the connection fails afterwards, and the log line is what explains
43
+ * why.
44
+ */
45
+ export function upgradeInsecureCloudUrl(raw) {
46
+ try {
47
+ const parsed = new URL(raw);
48
+ if (parsed.protocol !== "http:")
49
+ return raw;
50
+ const host = parsed.hostname;
51
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]")
52
+ return raw;
53
+ parsed.protocol = "https:";
54
+ const upgraded = parsed.toString().replace(/\/+$/, "");
55
+ debugLog(`[Prism Storage] Upgraded ${raw} to ${upgraded}: session content is never sent over plaintext to a remote host.`);
56
+ return upgraded;
57
+ }
58
+ catch {
59
+ return raw;
60
+ }
61
+ }
14
62
  /**
15
63
  * Probe for synalux credentials: env vars first, then config DB.
16
64
  * Returns true if usable credentials are now in process.env.
@@ -21,12 +69,14 @@ export async function ensureSynaluxCredentials() {
21
69
  // Re-check process.env directly: SYNALUX_CONFIGURED is captured at module
22
70
  // load, so credentials injected later by another caller would be invisible
23
71
  // to it. Mirrors ensureSupabaseCredentials below.
24
- const envUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || process.env.SYNALUX_BASE_URL?.trim();
72
+ const rawEnvUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || process.env.SYNALUX_BASE_URL?.trim();
73
+ const envUrl = rawEnvUrl ? upgradeInsecureCloudUrl(rawEnvUrl) : rawEnvUrl;
25
74
  const envKey = process.env.PRISM_SYNALUX_API_KEY?.trim();
26
75
  if (envUrl && envKey && isValidHttpUrl(envUrl))
27
76
  return true;
28
- const url = (await getSetting("PRISM_SYNALUX_BASE_URL"))?.trim() ||
77
+ const rawUrl = (await getSetting("PRISM_SYNALUX_BASE_URL"))?.trim() ||
29
78
  (await getSetting("SYNALUX_BASE_URL"))?.trim();
79
+ const url = rawUrl ? upgradeInsecureCloudUrl(rawUrl) : rawUrl;
30
80
  const key = (await getSetting("PRISM_SYNALUX_API_KEY"))?.trim();
31
81
  if (url && key && isValidHttpUrl(url)) {
32
82
  process.env.PRISM_SYNALUX_BASE_URL = url;
@@ -43,11 +93,13 @@ export async function ensureSynaluxCredentials() {
43
93
  async function ensureSupabaseCredentials() {
44
94
  if (SUPABASE_CONFIGURED)
45
95
  return true;
46
- const envUrl = process.env.SUPABASE_URL?.trim();
96
+ const rawEnvUrl = process.env.SUPABASE_URL?.trim();
97
+ const envUrl = rawEnvUrl ? upgradeInsecureCloudUrl(rawEnvUrl) : rawEnvUrl;
47
98
  const envKey = process.env.SUPABASE_KEY?.trim();
48
99
  if (envUrl && envKey && isValidHttpUrl(envUrl))
49
100
  return true;
50
- const url = (await getSetting("SUPABASE_URL"))?.trim();
101
+ const rawUrl = (await getSetting("SUPABASE_URL"))?.trim();
102
+ const url = rawUrl ? upgradeInsecureCloudUrl(rawUrl) : rawUrl;
51
103
  const key = (await getSetting("SUPABASE_KEY"))?.trim();
52
104
  if (url && key && isValidHttpUrl(url)) {
53
105
  process.env.SUPABASE_URL = url;
@@ -158,12 +210,36 @@ export async function getStorage() {
158
210
  return storageInstance;
159
211
  }
160
212
  export async function closeStorage() {
161
- if (storageInstance) {
162
- await storageInstance.close();
213
+ if (!storageInstance)
214
+ return;
215
+ const closing = storageInstance;
216
+ // Clear the slot in `finally`: if close() throws, the previous code left a
217
+ // DEAD instance installed as the singleton, and every later getStorage()
218
+ // handed that broken connection to callers. An empty slot is strictly
219
+ // safer — the next getStorage() re-opens cleanly. The error still
220
+ // propagates, so a caller like restoreFromBackup can abort before swapping
221
+ // the database file.
222
+ try {
223
+ await closing.close();
224
+ }
225
+ finally {
163
226
  storageInstance = null;
164
227
  }
165
228
  }
166
- /** Test-only: inject a pre-initialized storage instance into the singleton slot. */
229
+ /**
230
+ * Test-only: inject a pre-initialized storage instance into the singleton slot.
231
+ *
232
+ * CONTRACT: the CALLER owns the lifecycle of what it injects. This function
233
+ * deliberately does NOT close the instance it replaces — callers pair the
234
+ * injection with their own cleanup (see createTestDb().cleanup), so closing
235
+ * here would double-close a storage the test still owns.
236
+ *
237
+ * Worth knowing when auditing handle leaks: an instance dropped without
238
+ * close() keeps its sqlite lock, and on Windows that lock blocks unlink of
239
+ * the database file until the process exits (libsql close() does not release
240
+ * it either — tursodatabase/libsql-js#228 — so closing is hygiene, not a
241
+ * guarantee).
242
+ */
167
243
  export function _setStorageForTesting(instance) {
168
244
  storageInstance = instance;
169
245
  }
@@ -1689,6 +1689,64 @@ async function createLocalStartupStorage() {
1689
1689
  await storage.initialize(true);
1690
1690
  return storage;
1691
1691
  }
1692
+ /** Project name for the first-run demo memory. Its own project so it can never
1693
+ * mix with real work, and trivially removable as a unit. */
1694
+ const DEMO_PROJECT = "prism-demo";
1695
+ /**
1696
+ * First-run seed-and-show: write one demo ledger entry, then READ IT BACK
1697
+ * through the storage layer and render from the read-back row.
1698
+ *
1699
+ * The read-back is the point. Rendering the local variable would prove
1700
+ * nothing — a broken storage backend would still print a convincing
1701
+ * "recalled" block. Rendering only what getLedgerEntries returned means the
1702
+ * block the user sees IS evidence the save→recall loop works on their
1703
+ * machine. If either half fails we return null and the greeting simply
1704
+ * omits the demo — a first run must never break on a demo.
1705
+ */
1706
+ export async function seedAndRecallDemoMemory(conversationId) {
1707
+ try {
1708
+ const storage = await getStorage();
1709
+ // Idempotence before insert: the first_bootstrap_at marker is
1710
+ // check-then-set, so two hosts bootstrapping a fresh machine at once BOTH
1711
+ // take the first-run branch (observed setups run several agents
1712
+ // concurrently). Seeding only when no demo row exists narrows that race
1713
+ // from "every concurrent first run inserts" to a near-simultaneous
1714
+ // read-read window, and the loser still renders the winner's row — the
1715
+ // user sees one demo either way.
1716
+ const existing = (await storage.getLedgerEntries({
1717
+ project: `eq.${DEMO_PROJECT}`,
1718
+ user_id: `eq.${PRISM_USER_ID}`,
1719
+ limit: "1",
1720
+ }));
1721
+ if (existing.length === 0) {
1722
+ await storage.saveLedger({
1723
+ project: DEMO_PROJECT,
1724
+ conversation_id: conversationId,
1725
+ user_id: PRISM_USER_ID,
1726
+ summary: "Prism saved this memory during your first session to demonstrate recall.",
1727
+ todos: ["Try it yourself: ask your agent to `session_save_ledger` at the end of this session"],
1728
+ decisions: ["Demo memory — delete anytime with session_forget_memory or from the dashboard"],
1729
+ keywords: ["demo", "first-run"],
1730
+ });
1731
+ }
1732
+ const rows = (await storage.getLedgerEntries({
1733
+ project: `eq.${DEMO_PROJECT}`,
1734
+ user_id: `eq.${PRISM_USER_ID}`,
1735
+ order: "created_at.desc",
1736
+ limit: "1",
1737
+ }));
1738
+ const recalled = rows[0];
1739
+ if (!recalled?.summary)
1740
+ return null;
1741
+ const todo = Array.isArray(recalled.todos) && recalled.todos[0] ? `\n - TODO it carried: ${recalled.todos[0]}` : "";
1742
+ return (`- 🧠 **Watch this — Prism just saved a memory and recalled it from disk:**\n` +
1743
+ ` - "${recalled.summary}"${todo}\n` +
1744
+ ` - This round-trip is what every future session gets: your decisions, TODOs, and changed files, back the moment you return. (Demo lives in the \`${DEMO_PROJECT}\` project — delete it anytime.)`);
1745
+ }
1746
+ catch {
1747
+ return null;
1748
+ }
1749
+ }
1692
1750
  export async function sessionBootstrapHandler(args = {}, options = {}) {
1693
1751
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
1694
1752
  throw new Error("Invalid arguments for session_bootstrap");
@@ -1759,7 +1817,17 @@ export async function sessionBootstrapHandler(args = {}, options = {}) {
1759
1817
  if (isFirstRun) {
1760
1818
  // Action-first instead of absence-first: every line is a capability or
1761
1819
  // a next step. The wizard exists and is well-built; route to it.
1820
+ //
1821
+ // Seed-and-show: a memory product's payoff is structurally deferred to
1822
+ // session 2 — "it remembered" can't be felt until you come back. So the
1823
+ // first run seeds one clearly-marked demo memory and shows it RECALLED,
1824
+ // read back through the real storage layer (not string theater), so the
1825
+ // save→recall loop is felt in session 1. The demo lives in its own
1826
+ // project so it never mixes with real work, and this branch only runs
1827
+ // once — the first_bootstrap_at marker above is durable.
1828
+ const demoRecall = await seedAndRecallDemoMemory(conversationId);
1762
1829
  const firstRunText = `${greeting}\n\n` +
1830
+ (demoRecall ? `${demoRecall}\n` : "") +
1763
1831
  `- 🚀 **Get started:** run the \`onboarding_wizard\` tool (guided setup, ~3 minutes)\n` +
1764
1832
  `${dashboardLine}\n` +
1765
1833
  `- 💾 **Already working?** \`session_save_ledger\` records this session; the next one resumes with full context\n\n` +
@@ -165,7 +165,24 @@ export async function restoreFromBackup(dbPath, backupPath) {
165
165
  // Create pre-restore backup
166
166
  const preRestoreResult = await createBackup(dbPath);
167
167
  debugLog(`Pre-restore backup: ${preRestoreResult.success ? "OK" : "FAILED"}`);
168
- // Copy backup over current database
168
+ // Release the live connection BEFORE swapping the file underneath it.
169
+ //
170
+ // NOT a platform workaround. Measured on windows-x64 (2026-08-05):
171
+ // overwriting the database while a connection is open SUCCEEDS on
172
+ // every platform — SQLite shares the file for reading and writing,
173
+ // and only unlink/rename are blocked. So restore was never broken.
174
+ //
175
+ // The reason is correctness: replacing a database file beneath a live
176
+ // connection leaves that connection pointing at bytes it did not
177
+ // read, with a stale page cache and a WAL that no longer describes
178
+ // the file. SQLite documents this as unsafe. closeStorage() makes the
179
+ // swap atomic from the connection's point of view — the next
180
+ // getStorage() opens the restored file cleanly.
181
+ const { closeStorage } = await import("../storage/index.js");
182
+ await closeStorage();
183
+ // Copy backup over current database. closeStorage() cleared the
184
+ // singleton, so the next getStorage() re-opens against the restored
185
+ // file and no caller retains a handle to the replaced one.
169
186
  copyFileSync(backupPath, dbPath);
170
187
  return {
171
188
  success: true,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.7.0",
3
+ "version": "20.8.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
- "description": "Prism Coder \u2014 Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B\u201332B open-weights LLM fleet.",
5
+ "description": "Persistent session memory for AI coding agents that never leaves your machine \u2014 including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
6
6
  "module": "index.ts",
7
7
  "type": "module",
8
8
  "main": "dist/server.js",
@@ -35,68 +35,18 @@
35
35
  "import": "node dist/utils/universalImporter.js"
36
36
  },
37
37
  "keywords": [
38
- "mcp",
39
38
  "mcp-server",
40
- "model-context-protocol",
41
- "ai-agent",
42
39
  "session-memory",
43
40
  "agent-memory",
44
- "mind-palace",
45
41
  "local-first",
46
- "sqlite",
47
- "time-travel",
48
- "visual-memory",
49
- "multi-agent-sync",
50
- "telepathy",
51
- "morning-briefing",
52
- "reality-drift",
53
- "code-mode-templates",
54
- "semantic-search",
55
- "vector-search",
56
- "pgvector",
57
- "f32-blob",
58
- "concurrency-control",
59
- "mcp-prompts",
60
- "mcp-resources",
61
- "google-gemini",
62
- "brave-search",
63
- "llm",
64
- "claude-desktop",
65
- "supabase",
66
- "progressive-context-loading",
67
- "knowledge-accumulation",
68
- "ai-memory",
69
- "ai-tools",
70
- "typescript",
71
- "rag",
72
- "embeddings",
73
- "cursor",
42
+ "local-inference",
43
+ "drift-detection",
44
+ "knowledge-graph",
45
+ "claude-code",
74
46
  "codex",
75
- "windsurf",
76
- "cline",
77
- "persistent-memory",
78
- "zero-config",
79
- "auto-capture",
80
- "dashboard",
81
- "actr",
82
- "cognitive-memory",
83
- "cognitive-architecture",
84
- "activation-memory",
85
- "spreading-activation",
86
- "hebbian-learning",
87
- "multi-hop-reasoning",
88
- "rejection-gate",
89
- "episodic-semantic",
90
- "dark-factory",
91
- "autonomous-pipeline",
92
- "fail-closed",
93
- "anti-sycophancy",
94
- "local-embeddings",
95
- "transformers-js",
96
- "nomic-embed",
97
- "business",
98
- "enterprise",
99
- "hipaa"
47
+ "cursor",
48
+ "offline",
49
+ "rag"
100
50
  ],
101
51
  "homepage": "https://github.com/dcostenco/prism-coder",
102
52
  "repository": {