prism-mcp-server 20.21.20 → 20.21.21
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 +7 -0
- package/dist/dashboard/ledgerReader.js +4 -2
- package/dist/dashboard/readMessages.js +48 -0
- package/dist/dashboard/server.js +13 -10
- package/dist/dashboard/ui.js +231 -386
- package/dist/storage/portalContracts.js +23 -0
- package/dist/storage/sqlite.js +7 -4
- package/dist/storage/supabase.js +32 -24
- package/dist/storage/synalux.js +14 -1
- package/dist/tools/hygieneHandlers.js +7 -2
- package/package.json +1 -1
|
@@ -17,6 +17,29 @@
|
|
|
17
17
|
* 3. Add a schema-contract test in synalux-portal-contract.test.ts.
|
|
18
18
|
*/
|
|
19
19
|
import { z } from "zod";
|
|
20
|
+
import { ANALYTICS_DATE_PATTERN } from "../dashboard/readMessages.js";
|
|
21
|
+
const AnalyticsCountSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
22
|
+
const UTC_DAY_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
export const ProjectAnalyticsSchema = z.object({
|
|
24
|
+
totalEntries: AnalyticsCountSchema,
|
|
25
|
+
totalRollups: AnalyticsCountSchema,
|
|
26
|
+
rollupSavings: AnalyticsCountSchema,
|
|
27
|
+
avgSummaryLength: z.number().nonnegative().finite(),
|
|
28
|
+
sessionsByDay: z.array(z.object({
|
|
29
|
+
date: z.string().regex(ANALYTICS_DATE_PATTERN), count: AnalyticsCountSchema,
|
|
30
|
+
})).length(14),
|
|
31
|
+
}).refine(value => value.totalRollups <= value.totalEntries
|
|
32
|
+
&& value.sessionsByDay.reduce((sum, day) => sum + day.count, 0) <= value.totalEntries - value.totalRollups
|
|
33
|
+
&& value.sessionsByDay.every((day, index, days) => {
|
|
34
|
+
const time = Date.parse(day.date);
|
|
35
|
+
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === day.date
|
|
36
|
+
&& (index === 0 || time - Date.parse(days[index - 1].date) === UTC_DAY_MS);
|
|
37
|
+
}));
|
|
38
|
+
export const ProjectAnalyticsResponseSchema = z.object({
|
|
39
|
+
status: z.literal('success'),
|
|
40
|
+
project: z.string().min(1).max(100),
|
|
41
|
+
analytics: ProjectAnalyticsSchema,
|
|
42
|
+
});
|
|
20
43
|
// ─── knowledge_search ────────────────────────────────────────────
|
|
21
44
|
export const KnowledgeSearchRequestSchema = z.object({
|
|
22
45
|
action: z.literal("knowledge_search"),
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1824,6 +1824,7 @@ export class SqliteStorage {
|
|
|
1824
1824
|
FROM session_ledger
|
|
1825
1825
|
WHERE user_id = ?
|
|
1826
1826
|
AND archived_at IS NULL
|
|
1827
|
+
AND deleted_at IS NULL
|
|
1827
1828
|
AND embedding IS NULL
|
|
1828
1829
|
`,
|
|
1829
1830
|
args: [userId], // bind user_id to the ? placeholder
|
|
@@ -1883,11 +1884,13 @@ export class SqliteStorage {
|
|
|
1883
1884
|
FROM session_ledger r
|
|
1884
1885
|
LEFT JOIN session_ledger a
|
|
1885
1886
|
ON a.archived_at IS NOT NULL
|
|
1887
|
+
AND a.deleted_at IS NULL
|
|
1886
1888
|
AND a.project = r.project
|
|
1887
1889
|
AND a.user_id = r.user_id
|
|
1888
1890
|
WHERE r.user_id = ?
|
|
1889
1891
|
AND r.is_rollup = 1
|
|
1890
1892
|
AND r.archived_at IS NULL
|
|
1893
|
+
AND r.deleted_at IS NULL
|
|
1891
1894
|
GROUP BY r.id
|
|
1892
1895
|
HAVING COUNT(a.id) = 0
|
|
1893
1896
|
`,
|
|
@@ -1901,12 +1904,12 @@ export class SqliteStorage {
|
|
|
1901
1904
|
sql: `
|
|
1902
1905
|
SELECT
|
|
1903
1906
|
(SELECT COUNT(*) FROM session_ledger
|
|
1904
|
-
WHERE user_id = ? AND archived_at IS NULL) as active,
|
|
1907
|
+
WHERE user_id = ? AND archived_at IS NULL AND deleted_at IS NULL) as active,
|
|
1905
1908
|
(SELECT COUNT(*) FROM session_handoffs
|
|
1906
1909
|
WHERE user_id = ?) as handoffs,
|
|
1907
1910
|
(SELECT COUNT(*) FROM session_ledger
|
|
1908
1911
|
WHERE user_id = ? AND is_rollup = 1
|
|
1909
|
-
AND archived_at IS NULL) as rollups
|
|
1912
|
+
AND archived_at IS NULL AND deleted_at IS NULL) as rollups
|
|
1910
1913
|
`,
|
|
1911
1914
|
args: [userId, userId, userId], // bind user_id 3x (one per subquery)
|
|
1912
1915
|
});
|
|
@@ -2177,7 +2180,7 @@ export class SqliteStorage {
|
|
|
2177
2180
|
COUNT(*) AS total_entries,
|
|
2178
2181
|
SUM(CASE WHEN is_rollup = 1 THEN 1 ELSE 0 END) AS total_rollups,
|
|
2179
2182
|
-- rollup_count tracks how many raw entries each rollup replaced,
|
|
2180
|
-
-- so
|
|
2183
|
+
-- so the dashboard reports how many originals were consolidated.
|
|
2181
2184
|
SUM(CASE WHEN is_rollup = 1 THEN COALESCE(rollup_count, 0) ELSE 0 END) AS rollup_savings,
|
|
2182
2185
|
COALESCE(AVG(LENGTH(summary)), 0) AS avg_summary_length
|
|
2183
2186
|
FROM session_ledger
|
|
@@ -2213,7 +2216,7 @@ export class SqliteStorage {
|
|
|
2213
2216
|
const sessionsByDay = [];
|
|
2214
2217
|
for (let i = 13; i >= 0; i--) {
|
|
2215
2218
|
const date = new Date();
|
|
2216
|
-
date.
|
|
2219
|
+
date.setUTCDate(date.getUTCDate() - i);
|
|
2217
2220
|
const dateStr = date.toISOString().slice(0, 10);
|
|
2218
2221
|
sessionsByDay.push({ date: dateStr, count: sparkMap.get(dateStr) || 0 });
|
|
2219
2222
|
}
|
package/dist/storage/supabase.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
|
|
16
16
|
import { gzipSync, gunzipSync } from "node:zlib";
|
|
17
17
|
import { debugLog } from "../utils/logger.js";
|
|
18
|
+
import { ProjectAnalyticsSchema } from './portalContracts.js';
|
|
19
|
+
import { DASHBOARD_READ_MESSAGES } from "../dashboard/readMessages.js";
|
|
18
20
|
import { PRISM_USER_ID } from "../config.js";
|
|
19
21
|
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
20
22
|
import { runAutoMigrations } from "./supabaseMigrations.js";
|
|
@@ -441,12 +443,14 @@ export class SupabaseStorage {
|
|
|
441
443
|
user_id: `eq.${userId}`,
|
|
442
444
|
archived_at: "is.null",
|
|
443
445
|
embedding: "is.null",
|
|
446
|
+
deleted_at: "is.null",
|
|
444
447
|
});
|
|
445
448
|
const missingEmbeddings = Array.isArray(missingData) ? missingData.length : 0;
|
|
446
449
|
const summData = await supabaseGet("session_ledger", {
|
|
447
450
|
select: "id,project,summary",
|
|
448
451
|
user_id: `eq.${userId}`,
|
|
449
452
|
archived_at: "is.null",
|
|
453
|
+
deleted_at: "is.null",
|
|
450
454
|
});
|
|
451
455
|
const activeLedgerSummaries = (Array.isArray(summData) ? summData : []).map((r) => ({
|
|
452
456
|
id: r.id,
|
|
@@ -463,6 +467,7 @@ export class SupabaseStorage {
|
|
|
463
467
|
select: "project",
|
|
464
468
|
user_id: `eq.${userId}`,
|
|
465
469
|
archived_at: "is.null",
|
|
470
|
+
deleted_at: "is.null",
|
|
466
471
|
});
|
|
467
472
|
const ledgerProjects = new Set((Array.isArray(ledgerData) ? ledgerData : [])
|
|
468
473
|
.map((r) => r.project));
|
|
@@ -474,11 +479,13 @@ export class SupabaseStorage {
|
|
|
474
479
|
user_id: `eq.${userId}`,
|
|
475
480
|
is_rollup: "eq.true",
|
|
476
481
|
archived_at: "is.null",
|
|
482
|
+
deleted_at: "is.null",
|
|
477
483
|
});
|
|
478
484
|
const archivedData = await supabaseGet("session_ledger", {
|
|
479
485
|
select: "project",
|
|
480
486
|
user_id: `eq.${userId}`,
|
|
481
487
|
"archived_at": "not.is.null",
|
|
488
|
+
deleted_at: "is.null",
|
|
482
489
|
});
|
|
483
490
|
const archivedProjects = new Set((Array.isArray(archivedData) ? archivedData : [])
|
|
484
491
|
.map((r) => r.project));
|
|
@@ -633,32 +640,33 @@ export class SupabaseStorage {
|
|
|
633
640
|
}
|
|
634
641
|
// ─── v3.1: Memory Analytics ──────────────────────────────────
|
|
635
642
|
async getAnalytics(project, userId) {
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (data) {
|
|
645
|
-
return {
|
|
646
|
-
totalEntries: data.total_entries || 0,
|
|
647
|
-
totalRollups: data.total_rollups || 0,
|
|
648
|
-
rollupSavings: data.rollup_savings || 0,
|
|
649
|
-
avgSummaryLength: data.avg_summary_length || 0,
|
|
650
|
-
sessionsByDay: data.sessions_by_day || [],
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
catch {
|
|
655
|
-
debugLog("[SupabaseStorage] getAnalytics RPC unavailable — returning zeroed struct");
|
|
643
|
+
// An unavailable read must not look like a genuinely empty project.
|
|
644
|
+
const result = await supabaseRpc("get_project_analytics", {
|
|
645
|
+
p_project: project,
|
|
646
|
+
p_user_id: userId,
|
|
647
|
+
});
|
|
648
|
+
const data = Array.isArray(result) ? result[0] : result;
|
|
649
|
+
if (!data) {
|
|
650
|
+
throw new Error(DASHBOARD_READ_MESSAGES.analyticsUnavailable);
|
|
656
651
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
652
|
+
const analytics = {
|
|
653
|
+
totalEntries: data.total_entries, totalRollups: data.total_rollups,
|
|
654
|
+
rollupSavings: data.rollup_savings, avgSummaryLength: data.avg_summary_length,
|
|
655
|
+
sessionsByDay: data.sessions_by_day,
|
|
661
656
|
};
|
|
657
|
+
if (analytics.totalEntries === 0 && analytics.totalRollups === 0 && analytics.rollupSavings === 0
|
|
658
|
+
&& analytics.avgSummaryLength === 0 && Array.isArray(analytics.sessionsByDay) && analytics.sessionsByDay.length === 0) {
|
|
659
|
+
const now = new Date();
|
|
660
|
+
analytics.sessionsByDay = Array.from({ length: 14 }, (_, index) => {
|
|
661
|
+
const date = new Date(now);
|
|
662
|
+
date.setUTCDate(now.getUTCDate() - (13 - index));
|
|
663
|
+
return { date: date.toISOString().slice(0, 10), count: 0 };
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const parsed = ProjectAnalyticsSchema.safeParse(analytics);
|
|
667
|
+
if (!parsed.success)
|
|
668
|
+
throw new Error(DASHBOARD_READ_MESSAGES.analyticsUnavailable);
|
|
669
|
+
return parsed.data;
|
|
662
670
|
}
|
|
663
671
|
// ─── v3.1: TTL / Automated Data Retention ────────────────────
|
|
664
672
|
async expireByTTL(project, ttlDays, userId) {
|
package/dist/storage/synalux.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* - patchLedger → POST /api/v1/prism/memory action=save_embedding
|
|
31
31
|
* - getEntriesMissingEmbeddings → POST /api/v1/prism/memory action=list_missing_embeddings
|
|
32
32
|
* - listProjects → POST /api/v1/prism/memory action=list_projects
|
|
33
|
+
* - getAnalytics → GET /api/v1/prism/analytics view=project
|
|
33
34
|
* - exportLedger → POST /api/v1/prism/memory action=export_memory (paginated)
|
|
34
35
|
*
|
|
35
36
|
* Methods still falling through to SupabaseStorage (Phase 3 Tier B+):
|
|
@@ -51,7 +52,7 @@ import { SupabaseStorage } from "./supabase.js";
|
|
|
51
52
|
import { debugLog } from "../utils/logger.js";
|
|
52
53
|
import { PRISM_SYNALUX_BASE_URL, PRISM_SYNALUX_API_KEY } from "../config.js";
|
|
53
54
|
import { isSynaluxSignedOut } from "../utils/synaluxCredentialState.js";
|
|
54
|
-
import { KnowledgeSearchRequestSchema, KnowledgeSearchResponseSchema } from "./portalContracts.js";
|
|
55
|
+
import { KnowledgeSearchRequestSchema, KnowledgeSearchResponseSchema, ProjectAnalyticsResponseSchema } from "./portalContracts.js";
|
|
55
56
|
function resolveKnowledgeScope(callerScope) {
|
|
56
57
|
if (callerScope === "user" || callerScope === "workspace") {
|
|
57
58
|
return callerScope;
|
|
@@ -758,6 +759,14 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
758
759
|
}));
|
|
759
760
|
}
|
|
760
761
|
// ─── Health Check ─────────────────────────────────────────────
|
|
762
|
+
async getAnalytics(project, _userId) {
|
|
763
|
+
const query = new URLSearchParams({ view: 'project', project });
|
|
764
|
+
const result = ProjectAnalyticsResponseSchema.safeParse(await this.portalGet('/api/v1/prism/analytics?' + query));
|
|
765
|
+
if (!result.success || result.data.project !== project) {
|
|
766
|
+
throw new Error('Project analytics unavailable');
|
|
767
|
+
}
|
|
768
|
+
return result.data.analytics;
|
|
769
|
+
}
|
|
761
770
|
// Phase 3 Tier B: route health_check through portal. The portal
|
|
762
771
|
// returns summary counts only (no per-entry duplicate scan), so
|
|
763
772
|
// activeLedgerSummaries is returned empty — the hygiene handler
|
|
@@ -769,6 +778,10 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
769
778
|
project: "prism-mcp",
|
|
770
779
|
});
|
|
771
780
|
const inventory = result.inventory;
|
|
781
|
+
if (!inventory || !['ledger_entries', 'active_projects', 'ledger_missing_embeddings'].every(field => Number.isSafeInteger(inventory[field]) && inventory[field] >= 0)
|
|
782
|
+
|| inventory.ledger_missing_embeddings > inventory.ledger_entries) {
|
|
783
|
+
throw new Error('Health inventory unavailable');
|
|
784
|
+
}
|
|
772
785
|
const totalActiveEntries = typeof inventory?.ledger_entries === "number" ? inventory.ledger_entries : 0;
|
|
773
786
|
const totalHandoffs = typeof inventory?.active_projects === "number" ? inventory.active_projects : 0;
|
|
774
787
|
// Hardcoding 0 here certified a 100%-missing-embeddings outage as
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* ═══════════════════════════════════════════════════════════════════
|
|
17
17
|
*/
|
|
18
18
|
import { debugLog } from "../utils/logger.js";
|
|
19
|
+
const BACKFILL_SCOPE_MESSAGES = {
|
|
20
|
+
empty: '✅ No eligible entries with missing embeddings found in the requested scope.',
|
|
21
|
+
complete: 'All scanned entries now have embeddings for semantic search.',
|
|
22
|
+
};
|
|
19
23
|
import { activeStorageBackend, getStorage } from "../storage/index.js";
|
|
20
24
|
import { getEmbeddingProvider } from "../utils/llm/factory.js";
|
|
21
25
|
import { PRISM_USER_ID } from "../config.js";
|
|
@@ -78,6 +82,7 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
78
82
|
const params = {
|
|
79
83
|
"embedding": "is.null",
|
|
80
84
|
"archived_at": "is.null",
|
|
85
|
+
"deleted_at": "is.null",
|
|
81
86
|
user_id: `eq.${PRISM_USER_ID}`,
|
|
82
87
|
order: "id.asc",
|
|
83
88
|
limit: String(safeLimit),
|
|
@@ -95,7 +100,7 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
95
100
|
return {
|
|
96
101
|
content: [{
|
|
97
102
|
type: "text",
|
|
98
|
-
text:
|
|
103
|
+
text: BACKFILL_SCOPE_MESSAGES.empty,
|
|
99
104
|
}],
|
|
100
105
|
isError: false,
|
|
101
106
|
};
|
|
@@ -165,7 +170,7 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
165
170
|
`• Total scanned: ${entries.length}\n\n` +
|
|
166
171
|
(failed > 0
|
|
167
172
|
? `⚠️ ${failed} entries could not be repaired. Check server logs for details.`
|
|
168
|
-
:
|
|
173
|
+
: BACKFILL_SCOPE_MESSAGES.complete),
|
|
169
174
|
}],
|
|
170
175
|
isError: false,
|
|
171
176
|
_stats: { repaired, failed, last_id: entries[entries.length - 1]?.id },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.21.
|
|
3
|
+
"version": "20.21.21",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — 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",
|