prism-mcp-server 20.21.6 → 20.21.7

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/dist/connect.js CHANGED
@@ -1250,6 +1250,7 @@ function getHostDefinitions(homeDir, platform, env, includeSystemPaths) {
1250
1250
  label: "Gemini CLI",
1251
1251
  format: "json",
1252
1252
  configPath: join(homeDir, ".gemini", "settings.json"),
1253
+ additionalConfigPaths: antigravityMcpConfigPaths(homeDir),
1253
1254
  detectionPaths: [join(homeDir, ".gemini")],
1254
1255
  executables: ["gemini"],
1255
1256
  }, {
@@ -1304,6 +1305,22 @@ function executableExists(name, platform, pathEnv) {
1304
1305
  }
1305
1306
  return false;
1306
1307
  }
1308
+ /**
1309
+ * Antigravity has used three user-level MCP locations across releases. Keep
1310
+ * the Gemini CLI settings registration as the primary result, and converge
1311
+ * only Antigravity paths whose parent directory already exists. That avoids
1312
+ * creating an unrelated host config on a Gemini-CLI-only installation while
1313
+ * repairing an existing Antigravity installation, including the legacy IDE
1314
+ * path that can otherwise continue launching an old server indefinitely.
1315
+ */
1316
+ function antigravityMcpConfigPaths(homeDir) {
1317
+ const geminiDir = join(homeDir, ".gemini");
1318
+ return [
1319
+ join(geminiDir, "config", "mcp_config.json"),
1320
+ join(geminiDir, "antigravity", "mcp_config.json"),
1321
+ join(geminiDir, "antigravity-ide", "mcp_config.json"),
1322
+ ].filter((configPath) => existsSync(configPath) || existsSync(dirname(configPath)));
1323
+ }
1307
1324
  function buildMcpEntry(nodePath, serverPath, env) {
1308
1325
  const storage = env.PRISM_STORAGE ?? "auto";
1309
1326
  if (!CONNECT_STORAGE_BACKENDS.includes(storage)) {
@@ -1331,10 +1348,42 @@ function registerHost(definition, entry, dryRun, refresh, beforeCommit) {
1331
1348
  if (definition.configurationError) {
1332
1349
  return result(definition, "error", definition.configurationError);
1333
1350
  }
1334
- if (definition.format === "codex-toml") {
1335
- return registerCodexTomlHost(definition, entry, dryRun, refresh, beforeCommit);
1351
+ const configDefinitions = [
1352
+ definition,
1353
+ ...(definition.additionalConfigPaths ?? []).map((configPath) => ({ ...definition, configPath })),
1354
+ ];
1355
+ const registerOne = (configDefinition) => {
1356
+ if (configDefinition.format === "codex-toml") {
1357
+ return registerCodexTomlHost(configDefinition, entry, dryRun, refresh, beforeCommit);
1358
+ }
1359
+ return registerJsonHost(configDefinition, entry, dryRun, refresh, beforeCommit);
1360
+ };
1361
+ const primary = registerOne(configDefinitions[0]);
1362
+ if (primary.status === "error")
1363
+ return primary;
1364
+ const registrations = [primary, ...configDefinitions.slice(1).map(registerOne)];
1365
+ const failures = registrations.slice(1).filter((registration) => registration.status === "error");
1366
+ if (failures.length > 0) {
1367
+ return {
1368
+ ...primary,
1369
+ status: "error",
1370
+ startupCompatible: false,
1371
+ message: `additional config registration failed: ${failures.map((failure) => `${failure.path}: ${failure.message || "unknown error"}`).join("; ")}`,
1372
+ };
1336
1373
  }
1337
- return registerJsonHost(definition, entry, dryRun, refresh, beforeCommit);
1374
+ const additionalChanges = registrations.slice(1).filter((registration) => registration.status !== "existing");
1375
+ const incompatible = registrations.filter((registration) => !registration.startupCompatible);
1376
+ const additionalMessage = additionalChanges.length > 0
1377
+ ? `also ${additionalChanges.map((registration) => `${registration.status} ${registration.path}`).join(", ")}`
1378
+ : undefined;
1379
+ const incompatibleMessage = incompatible.length > 0
1380
+ ? `retained incompatible or unmanaged registration(s): ${incompatible.map((registration) => registration.path).join(", ")}`
1381
+ : undefined;
1382
+ return {
1383
+ ...primary,
1384
+ startupCompatible: registrations.every((registration) => registration.startupCompatible),
1385
+ message: [primary.message, additionalMessage, incompatibleMessage].filter(Boolean).join("; ") || undefined,
1386
+ };
1338
1387
  }
1339
1388
  function registerJsonHost(definition, entry, dryRun, refresh, beforeCommit) {
1340
1389
  const { configPath } = definition;
@@ -20,24 +20,30 @@ export function resolveDashboardToken(cfg) {
20
20
  return randomBytes(32).toString("hex");
21
21
  }
22
22
  /** Extract the prism_dashboard_token cookie value, if present. */
23
- export function tokenFromCookie(cookieHeader) {
23
+ export function dashboardTokenCookieName(port) {
24
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
25
+ throw new Error('Invalid dashboard port');
26
+ return `prism_dashboard_token_${port}`;
27
+ }
28
+ export function tokenFromCookie(cookieHeader, cookieName = 'prism_dashboard_token') {
24
29
  // Capture the whole value (any run of non-";", non-space) so pinned tokens
25
30
  // with hyphens/underscores match; the name is anchored to start-or-"; " so a
26
31
  // look-alike cookie (evil_prism_dashboard_token=…) cannot match.
27
- const m = (cookieHeader || "").match(/(?:^|;\s*)prism_dashboard_token=([^;\s]+)/);
28
- return m ? m[1] : null;
32
+ const entry = (cookieHeader || '').split(';').map(c => c.trim()).find(c => c.startsWith(`${cookieName}=`));
33
+ const value = entry?.slice(cookieName.length + 1);
34
+ return value && !/\s/.test(value) ? value : null;
29
35
  }
30
36
  /**
31
37
  * True when the request presents the active token via the cookie, the
32
38
  * X-Prism-Dashboard-Token header, or a ?token= query param. Every comparison is
33
39
  * timing-safe and only runs against the single active token.
34
40
  */
35
- export function requestHasToken(headers, queryToken, activeToken) {
36
- const candidates = [tokenFromCookie(headers.cookie), headers.headerToken ?? null, queryToken];
41
+ export function requestHasToken(headers, queryToken, activeToken, cookieName = 'prism_dashboard_token') {
42
+ const candidates = [tokenFromCookie(headers.cookie, cookieName), tokenFromCookie(headers.cookie), headers.headerToken ?? null, queryToken];
37
43
  return candidates.some((c) => c !== null && safeCompare(c, activeToken));
38
44
  }
39
45
  /** Build the Set-Cookie value that stores the token for a browser session. */
40
- export function buildTokenCookie(token, maxAgeMs, secure) {
41
- return (`prism_dashboard_token=${token}; Path=/; HttpOnly; SameSite=Strict; ` +
46
+ export function buildTokenCookie(token, maxAgeMs, secure, cookieName = 'prism_dashboard_token') {
47
+ return (`${cookieName}=${token}; Path=/; HttpOnly; SameSite=Strict; ` +
42
48
  `Max-Age=${Math.floor(maxAgeMs / 1000)}${secure ? "; Secure" : ""}`);
43
49
  }
@@ -65,16 +65,26 @@ export async function handleGraphRoutes(url, req, res, getStorageSafe) {
65
65
  else {
66
66
  params.limit = "200"; // Bump limit when exploring specific filters
67
67
  }
68
+ let createdAfter;
68
69
  if (project)
69
70
  params.project = `eq.${project}`;
70
71
  if (days) {
71
72
  const past = new Date();
72
73
  past.setDate(past.getDate() - parseInt(days, 10));
73
- params.created_at = `gte.${past.toISOString()}`;
74
+ createdAfter = past.toISOString();
75
+ params.created_at = `gte.${createdAfter}`;
74
76
  }
75
77
  if (min_importance)
76
78
  params.importance = `gte.${parseInt(min_importance, 10)}`;
77
- const entries = await s.getLedgerEntries(params);
79
+ const graphLimit = !project && !days && !min_importance ? 30 : 200;
80
+ const entries = s.getDashboardGraphEntries
81
+ ? await s.getDashboardGraphEntries({
82
+ ...(project ? { project } : {}),
83
+ ...(createdAfter ? { createdAfter } : {}),
84
+ ...(min_importance ? { minImportance: parseInt(min_importance, 10) } : {}),
85
+ limit: graphLimit,
86
+ })
87
+ : await s.getLedgerEntries(params);
78
88
  const now = Date.now();
79
89
  // ── Decay tracking: aggregate per-keyword and per-project ──
80
90
  // For each keyword/project, track the *freshest* entry's decay info
@@ -0,0 +1,9 @@
1
+ /** Paid clients have Portal credentials, not direct Supabase credentials. */
2
+ export async function readDashboardLedger(storage, backend, project, order, limit) {
3
+ if (backend !== "synalux") {
4
+ return storage.getLedgerEntries({ project: `eq.${project}`, order, limit: String(limit) });
5
+ }
6
+ if (!storage.getDashboardLedger)
7
+ throw new Error("Cloud dashboard ledger reader unavailable");
8
+ return storage.getDashboardLedger(project, order, limit);
9
+ }
@@ -22,7 +22,8 @@ import * as os from "os";
22
22
  import * as fs from "fs";
23
23
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
24
24
  import { createServer, getAllPossibleTools } from "../server.js";
25
- import { getStorage } from "../storage/index.js";
25
+ import { getStorage, activeStorageBackend } from "../storage/index.js";
26
+ import { readDashboardLedger } from "./ledgerReader.js";
26
27
  import { PRISM_USER_ID, SERVER_CONFIG } from "../config.js";
27
28
  import { renderDashboardHTML } from "./ui.js";
28
29
  import { computeIntentHealth } from "./intentHealth.js";
@@ -34,7 +35,7 @@ import { redactSettings } from "../tools/commonHelpers.js";
34
35
  import { handleGraphRoutes } from "./graphRouter.js";
35
36
  import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
36
37
  import { isTrustedRequest, isRebindGuardedPath } from "./hostGuard.js";
37
- import { resolveDashboardToken, requestHasToken, buildTokenCookie, } from "./dashboardToken.js";
38
+ import { resolveDashboardToken, requestHasToken, buildTokenCookie, dashboardTokenCookieName, } from "./dashboardToken.js";
38
39
  import { safeCompare, generateToken, isAuthenticated, createRateLimiter, initJWKS, } from "./authUtils.js";
39
40
  const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
40
41
  /** Read HTTP request body as string (Buffer-based to avoid GC thrash on large imports) */
@@ -238,7 +239,7 @@ return false;}
238
239
  reqUrl.searchParams.delete("token");
239
240
  const cleanTarget = reqUrl.pathname + (reqUrl.search ? reqUrl.search : "");
240
241
  res.writeHead(302, {
241
- "Set-Cookie": buildTokenCookie(DASHBOARD_TOKEN, SESSION_TTL_MS, COOKIE_SECURE),
242
+ "Set-Cookie": buildTokenCookie(DASHBOARD_TOKEN, SESSION_TTL_MS, COOKIE_SECURE, dashboardTokenCookieName(req.socket.localPort)),
242
243
  Location: cleanTarget,
243
244
  });
244
245
  return res.end();
@@ -247,7 +248,7 @@ return false;}
247
248
  !requestHasToken({
248
249
  cookie: req.headers.cookie,
249
250
  headerToken: req.headers["x-prism-dashboard-token"] || null,
250
- }, qToken, DASHBOARD_TOKEN)) {
251
+ }, qToken, DASHBOARD_TOKEN, dashboardTokenCookieName(req.socket.localPort))) {
251
252
  res.writeHead(401, { "Content-Type": "application/json" });
252
253
  return res.end(JSON.stringify({
253
254
  error: "Dashboard token required — open the tokenized URL printed in the Prism startup log.",
@@ -482,11 +483,7 @@ return false;}
482
483
  return res.end(JSON.stringify({ error: "Storage initializing..." }));
483
484
  }
484
485
  const context = await s.loadContext(projectName, "deep", PRISM_USER_ID);
485
- const ledger = await s.getLedgerEntries({
486
- project: `eq.${projectName}`,
487
- order: "created_at.desc",
488
- limit: "20",
489
- });
486
+ const ledger = await readDashboardLedger(s, activeStorageBackend, projectName, "created_at.desc", 20);
490
487
  let history = [];
491
488
  try {
492
489
  history = await s.getHistory(projectName, PRISM_USER_ID, 10);
@@ -824,11 +821,7 @@ return false;}
824
821
  }
825
822
  // Gather data (mirrors sessionExportMemoryHandler)
826
823
  const ctx = await s.loadContext(projectName, "deep", PRISM_USER_ID);
827
- const rawLedger = await s.getLedgerEntries({
828
- project: `eq.${projectName}`,
829
- order: "created_at.asc",
830
- limit: "1000",
831
- });
824
+ const rawLedger = await readDashboardLedger(s, activeStorageBackend, projectName, "created_at.asc", 1000);
832
825
  // Strip binary embedding fields
833
826
  const cleanLedger = rawLedger.map(({ embedding: _e, embedding_compressed: _ec, ...rest }) => rest);
834
827
  const rawSettings = await getAllSettings();
@@ -712,6 +712,7 @@ export function renderDashboardHTML(version) {
712
712
  <button class="settings-btn" onclick="openSettings()" title="Settings">⚙️</button>
713
713
  </div>
714
714
  </header>
715
+ <p id="projectLoadError" role="alert" style="display:none; color:var(--accent-rose);"></p>
715
716
 
716
717
  <div class="main-tabs" style="display:flex; gap: 1rem; border-bottom: 1px solid var(--border-glass); margin-bottom: 1.5rem; padding-bottom: 0;">
717
718
  <button class="s-tab active" id="mtab-project" onclick="switchMainTab('project')" style="font-size: 1rem;">📁 Project View</button>
@@ -2049,6 +2050,7 @@ function loadIdentityChip() {
2049
2050
  return [4 /*yield*/, res.json()];
2050
2051
  case 2:
2051
2052
  data = _a.sent();
2053
+ if (!res.ok) throw new Error(data.error || ('Request failed (' + res.status + ')'));
2052
2054
  select = document.getElementById('projectSelect');
2053
2055
  if (data.projects && data.projects.length > 0) {
2054
2056
  select.innerHTML = '<option value="">— Select a project —</option>' +
@@ -2097,6 +2099,8 @@ function loadIdentityChip() {
2097
2099
  case 3:
2098
2100
  e_2 = _a.sent();
2099
2101
  document.getElementById('projectSelect').innerHTML = '<option value="">Error loading projects</option>';
2102
+ document.getElementById('projectLoadError').textContent = e_2.message || 'Error loading projects';
2103
+ document.getElementById('projectLoadError').style.display = 'block';
2100
2104
  return [3 /*break*/, 4];
2101
2105
  case 4:
2102
2106
  // Load identity chip once settings are available
@@ -2918,7 +2922,9 @@ function loadGraph() {
2918
2922
  }
2919
2923
  MAX_NODES = 200;
2920
2924
  if (data.nodes.length > MAX_NODES) {
2921
- priority = { project: 0, category: 1, keyword: 2 };
2925
+ // Keep the project/category/keyword ordering truthy because the
2926
+ // comparator falls back with a high priority for unknown groups.
2927
+ priority = { project: 1, category: 2, keyword: 3 };
2922
2928
  data.nodes.sort(function (a, b) { return (priority[a.group] || 9) - (priority[b.group] || 9); });
2923
2929
  kept = new Set(data.nodes.slice(0, MAX_NODES).map(function (n) { return n.id; }));
2924
2930
  data.nodes = data.nodes.slice(0, MAX_NODES);
@@ -451,6 +451,46 @@ export class SynaluxStorage extends SupabaseStorage {
451
451
  const entries = Array.isArray(result.entries) ? result.entries : [];
452
452
  return entries;
453
453
  }
454
+ /**
455
+ * Read the bounded, authenticated ledger projection used by the dashboard
456
+ * graph. This is deliberately separate from getLedgerEntries: the latter
457
+ * has many PostgREST callers that are not representable by the portal API.
458
+ */
459
+ async getDashboardGraphEntries(params) {
460
+ if (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 200) {
461
+ throw new Error("Invalid dashboard graph limit");
462
+ }
463
+ if (params.project !== undefined && (!params.project.trim() || params.project.length > 100)) {
464
+ throw new Error("Invalid dashboard graph project");
465
+ }
466
+ if (params.createdAfter !== undefined
467
+ && (Number.isNaN(new Date(params.createdAfter).getTime())
468
+ || new Date(params.createdAfter).toISOString() !== params.createdAfter)) {
469
+ throw new Error("Invalid dashboard graph timestamp");
470
+ }
471
+ if (params.minImportance !== undefined
472
+ && (!Number.isInteger(params.minImportance)
473
+ || params.minImportance < -2147483648 || params.minImportance > 2147483647)) {
474
+ throw new Error("Invalid dashboard graph importance");
475
+ }
476
+ if (params.keywords !== undefined
477
+ && (params.keywords.length === 0 || params.keywords.length > 50
478
+ || params.keywords.some((keyword) => !keyword || keyword.length > 200))) {
479
+ throw new Error("Invalid dashboard graph keywords");
480
+ }
481
+ const result = await this.portalPost("/api/v1/prism/memory", {
482
+ action: "dashboard_ledger",
483
+ ...(params.project !== undefined ? { project: params.project.trim() } : {}),
484
+ ...(params.createdAfter !== undefined ? { created_after: params.createdAfter } : {}),
485
+ ...(params.minImportance !== undefined ? { min_importance: params.minImportance } : {}),
486
+ ...(params.keywords !== undefined ? { keywords: params.keywords } : {}),
487
+ limit: params.limit,
488
+ });
489
+ if (!Array.isArray(result.ledger)) {
490
+ throw new Error("Dashboard graph contract drift: ledger[] is required");
491
+ }
492
+ return result.ledger;
493
+ }
454
494
  // ─── Project inventory + export ──────────────────────────────
455
495
  // Both portal actions shipped in Phase 3 but the client was never
456
496
  // wired: listProjects and the export path fell through to
@@ -503,6 +543,32 @@ export class SynaluxStorage extends SupabaseStorage {
503
543
  }
504
544
  return rows;
505
545
  }
546
+ /** Read the requested dashboard window without the backup's 10k-row cap. */
547
+ async getDashboardLedger(project, order, limit) {
548
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000)
549
+ throw new Error("Invalid dashboard ledger limit");
550
+ let offset = 0;
551
+ for (let attempt = 0; attempt < 4; attempt++) {
552
+ const result = await this.portalPost("/api/v1/prism/memory", {
553
+ action: "export_memory", project, offset, limit,
554
+ });
555
+ const page = result.page;
556
+ const total = page?.total;
557
+ if (!Array.isArray(result.ledger) || typeof total !== "number" || !Number.isSafeInteger(total) || total < 0) {
558
+ throw new Error("Dashboard export contract drift: ledger and total are required");
559
+ }
560
+ const target = order === "created_at.desc" ? Math.max(0, total - limit) : 0;
561
+ if (offset !== target) {
562
+ offset = target;
563
+ continue;
564
+ }
565
+ const rows = result.ledger;
566
+ const direction = order === "created_at.asc" ? 1 : -1;
567
+ return [...rows].sort((a, b) => direction * (String(a.created_at).localeCompare(String(b.created_at))
568
+ || String(a.id).localeCompare(String(b.id))));
569
+ }
570
+ throw new Error("Dashboard ledger changed during read; retry");
571
+ }
506
572
  // ─── Time Travel ─────────────────────────────────────────────
507
573
  // Phase 3 Tier B: route memory_history through portal instead of
508
574
  // falling through to SupabaseStorage (which requires a direct
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.21.6",
3
+ "version": "20.21.7",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
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",