prism-mcp-server 17.0.0 → 17.1.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 +94 -11
- package/dist/tools/ledgerHandlers.js +37 -7
- package/dist/tools/prismInferHandler.js +46 -6
- package/dist/tools/skillRouting.js +39 -0
- package/dist/utils/braveApi.js +52 -0
- package/dist/utils/entitlements.js +136 -0
- package/dist/utils/phiGuard.js +88 -0
- package/dist/utils/synaluxSearch.js +195 -0
- package/package.json +2 -2
- package/dist/agent/agentTools.js +0 -453
- package/dist/agent/mcpBridge.js +0 -234
- package/dist/agent/platformUtils.js +0 -470
- package/dist/agent/terminalUI.js +0 -198
- package/dist/auth.js +0 -218
- package/dist/darkfactory/cloudDelegate.js +0 -173
- package/dist/env-preload.cjs +0 -2
- package/dist/plugins/pluginManager.js +0 -199
- package/dist/prism-cloud.js +0 -110
- package/dist/scm/ciPipeline.js +0 -220
- package/dist/start-with-env.sh +0 -5
- package/dist/sync/encryptedSync.js +0 -172
- package/dist/sync/synaluxProxy.js +0 -177
- package/dist/tools/adaptiveDefinitions.js +0 -148
- package/dist/tools/projects.js +0 -214
- package/dist/utils/changelogGenerator.js +0 -158
- package/dist/utils/fallbackClient.js +0 -52
- package/dist/utils/memoryAttestation.js +0 -163
- package/dist/utils/rbac.js +0 -321
- package/dist/utils/tavilyApi.js +0 -70
- package/dist/vm/quotaEnforcer.js +0 -192
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PHI Guard — detect and redact Protected Health Information before storage/logging.
|
|
3
|
+
*
|
|
4
|
+
* HIPAA §164.502: PHI must not be disclosed except as permitted.
|
|
5
|
+
* This module scans text for common PHI patterns (SSN, DOB, MRN, phone,
|
|
6
|
+
* email, patient names in clinical context) and redacts them.
|
|
7
|
+
*
|
|
8
|
+
* Detection events are logged to stderr (picked up by DD agent) with
|
|
9
|
+
* the pattern type and character position — never the actual PHI value.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { scanAndRedactPHI, hasPHI } from './phiGuard.js';
|
|
13
|
+
* const { redacted, detections } = scanAndRedactPHI(userText);
|
|
14
|
+
* // `redacted` is safe to store/log; `detections` lists what was found
|
|
15
|
+
*/
|
|
16
|
+
// Patterns ordered by specificity (most specific first)
|
|
17
|
+
const PHI_PATTERNS = [
|
|
18
|
+
// SSN: 123-45-6789 or 123456789
|
|
19
|
+
{ name: 'SSN', regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: '[SSN-REDACTED]' },
|
|
20
|
+
{ name: 'SSN', regex: /\b\d{9}\b(?=\s|$|[,.])/g, replacement: '[SSN-REDACTED]' },
|
|
21
|
+
// Date of birth patterns: DOB: 01/15/1990, born 1990-01-15, birthday 01/15/90
|
|
22
|
+
{ name: 'DOB', regex: /\b(?:dob|date\s*of\s*birth|born|birthday)\s*[:=]?\s*\d{1,2}[/\-]\d{1,2}[/\-]\d{2,4}\b/gi, replacement: '[DOB-REDACTED]' },
|
|
23
|
+
{ name: 'DOB', regex: /\b(?:dob|date\s*of\s*birth|born|birthday)\s*[:=]?\s*\d{4}[/\-]\d{1,2}[/\-]\d{1,2}\b/gi, replacement: '[DOB-REDACTED]' },
|
|
24
|
+
// Medical Record Number: MRN: 12345678, MRN#12345
|
|
25
|
+
{ name: 'MRN', regex: /\b(?:mrn|medical\s*record)\s*[#:=]?\s*\d{4,12}\b/gi, replacement: '[MRN-REDACTED]' },
|
|
26
|
+
// US Phone: (301) 433-1943, 301-433-1943, +1-301-433-1943
|
|
27
|
+
{ name: 'PHONE', regex: /\b(?:\+?1[-.]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replacement: '[PHONE-REDACTED]' },
|
|
28
|
+
// Email in clinical context: patient email, client email
|
|
29
|
+
{ name: 'EMAIL', regex: /\b(?:patient|client|parent|caregiver)\s*(?:email|e-mail)\s*[:=]?\s*[\w.+-]+@[\w.-]+\.\w{2,}\b/gi, replacement: '[EMAIL-REDACTED]' },
|
|
30
|
+
// Patient/client name patterns: "Patient: John Doe", "Client Name: Jane Smith"
|
|
31
|
+
{ name: 'PATIENT_NAME', regex: /\b(?:patient|client)\s*(?:name)?\s*[:=]\s*[A-Z][a-z]+\s+[A-Z][a-z]+/gi, replacement: '[NAME-REDACTED]' },
|
|
32
|
+
// Insurance ID: Ins#, Policy#, Member ID
|
|
33
|
+
{ name: 'INSURANCE_ID', regex: /\b(?:ins(?:urance)?|policy|member)\s*(?:id|#|number)\s*[:=]?\s*[A-Z0-9]{6,20}\b/gi, replacement: '[INSURANCE-REDACTED]' },
|
|
34
|
+
// Diagnosis codes in patient context: "diagnosed with F84.0", "ICD: F32.1"
|
|
35
|
+
{ name: 'DIAGNOSIS', regex: /\b(?:diagnos\w*|icd|dx)\s*(?:[:=]|with)?\s*[A-Z]\d{2}(?:\.\d{1,2})?\b/gi, replacement: '[DX-REDACTED]' },
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Scan text for PHI patterns and return redacted version + detection list.
|
|
39
|
+
* Never logs or stores the actual PHI values — only type + position.
|
|
40
|
+
*/
|
|
41
|
+
export function scanAndRedactPHI(text) {
|
|
42
|
+
if (typeof text !== 'string' || !text) {
|
|
43
|
+
return { redacted: text || '', detections: [], hasPHI: false };
|
|
44
|
+
}
|
|
45
|
+
const detections = [];
|
|
46
|
+
let redacted = text;
|
|
47
|
+
for (const { name, regex, replacement } of PHI_PATTERNS) {
|
|
48
|
+
// Reset regex state for global patterns
|
|
49
|
+
regex.lastIndex = 0;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = regex.exec(text)) !== null) {
|
|
52
|
+
detections.push({
|
|
53
|
+
type: name,
|
|
54
|
+
position: match.index,
|
|
55
|
+
length: match[0].length,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
redacted = redacted.replace(regex, replacement);
|
|
59
|
+
}
|
|
60
|
+
if (detections.length > 0) {
|
|
61
|
+
// Log detection event — type + count only, NEVER the actual value
|
|
62
|
+
const summary = detections.reduce((acc, d) => {
|
|
63
|
+
acc[d.type] = (acc[d.type] || 0) + 1;
|
|
64
|
+
return acc;
|
|
65
|
+
}, {});
|
|
66
|
+
const summaryStr = Object.entries(summary).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
67
|
+
console.error(`[PHI-GUARD] Detected and redacted PHI: ${summaryStr}`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
redacted,
|
|
71
|
+
detections,
|
|
72
|
+
hasPHI: detections.length > 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Quick check — does the text contain PHI patterns?
|
|
77
|
+
* Faster than full redaction when you only need a boolean.
|
|
78
|
+
*/
|
|
79
|
+
export function hasPHI(text) {
|
|
80
|
+
if (typeof text !== 'string' || !text)
|
|
81
|
+
return false;
|
|
82
|
+
for (const { regex } of PHI_PATTERNS) {
|
|
83
|
+
regex.lastIndex = 0;
|
|
84
|
+
if (regex.test(text))
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synalux Portal Search & Scrape Client
|
|
3
|
+
* ─────────────────────────────────────────────────────────────
|
|
4
|
+
* Routes web search and scrape calls through the Synalux portal
|
|
5
|
+
* so API keys (Brave, Firecrawl, etc.) live server-side. The
|
|
6
|
+
* portal endpoints:
|
|
7
|
+
*
|
|
8
|
+
* POST /api/v1/prism/search — { query, limit? }
|
|
9
|
+
* returns { status, results: [{title, url, description}], source }
|
|
10
|
+
*
|
|
11
|
+
* POST /api/v1/prism/scrape — { url, formats?, onlyMainContent?, waitFor? }
|
|
12
|
+
* returns { status, content }
|
|
13
|
+
*
|
|
14
|
+
* Auth uses the shared JWT exchange from synaluxJwt.ts (same
|
|
15
|
+
* refresh-token dance as SynaluxStorage, but without requiring
|
|
16
|
+
* the full storage class). Falls back gracefully: callers check
|
|
17
|
+
* SYNALUX_SEARCH_AVAILABLE before calling.
|
|
18
|
+
*/
|
|
19
|
+
import { debugLog } from "./logger.js";
|
|
20
|
+
import { getSynaluxJwt, invalidateSynaluxJwt } from "./synaluxJwt.js";
|
|
21
|
+
import { PRISM_SYNALUX_BASE_URL, SYNALUX_CONFIGURED, } from "../config.js";
|
|
22
|
+
// ─── Public availability flag ────────────────────────────────
|
|
23
|
+
/** True when Synalux portal credentials are configured. */
|
|
24
|
+
export const SYNALUX_SEARCH_AVAILABLE = SYNALUX_CONFIGURED;
|
|
25
|
+
// ─── Internal helpers ────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* POST to a portal endpoint with JWT auth. Retries once on 401
|
|
28
|
+
* (JWT may have just expired). Throws on network or HTTP errors.
|
|
29
|
+
*/
|
|
30
|
+
async function portalPost(path, body, timeoutMs = 15_000) {
|
|
31
|
+
const baseUrl = PRISM_SYNALUX_BASE_URL.replace(/\/+$/, "");
|
|
32
|
+
const url = `${baseUrl}${path}`;
|
|
33
|
+
const send = async (jwt) => {
|
|
34
|
+
return fetch(url, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Authorization": `Bearer ${jwt}`,
|
|
39
|
+
"X-Prism-Client": "prism-mcp-search",
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
let jwt = await getSynaluxJwt();
|
|
46
|
+
if (!jwt) {
|
|
47
|
+
throw new Error("[synaluxSearch] JWT exchange failed — no token available");
|
|
48
|
+
}
|
|
49
|
+
let res = await send(jwt);
|
|
50
|
+
// Retry once on 401 (stale JWT)
|
|
51
|
+
if (res.status === 401) {
|
|
52
|
+
debugLog("[synaluxSearch] 401 on first attempt, re-exchanging JWT");
|
|
53
|
+
invalidateSynaluxJwt();
|
|
54
|
+
jwt = await getSynaluxJwt();
|
|
55
|
+
if (!jwt) {
|
|
56
|
+
throw new Error("[synaluxSearch] JWT re-exchange failed after 401");
|
|
57
|
+
}
|
|
58
|
+
res = await send(jwt);
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const text = await res.text().catch(() => "(no body)");
|
|
62
|
+
throw new Error(`[synaluxSearch] ${path} HTTP ${res.status}: ${text}`);
|
|
63
|
+
}
|
|
64
|
+
return (await res.json());
|
|
65
|
+
}
|
|
66
|
+
// ─── Public API ──────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Web search via Synalux portal. Returns formatted text matching
|
|
69
|
+
* the shape of performWebSearch() in braveApi.ts.
|
|
70
|
+
*/
|
|
71
|
+
export async function synaluxWebSearch(query, count = 10) {
|
|
72
|
+
debugLog(`[synaluxSearch] web search: q="${query}", limit=${count}`);
|
|
73
|
+
const data = await portalPost("/api/v1/prism/search", {
|
|
74
|
+
query,
|
|
75
|
+
limit: Math.min(count, 20),
|
|
76
|
+
});
|
|
77
|
+
if (data.status === "error") {
|
|
78
|
+
throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
|
|
79
|
+
}
|
|
80
|
+
const results = (data.results || []).map((r) => ({
|
|
81
|
+
title: r.title || "",
|
|
82
|
+
description: r.description || "",
|
|
83
|
+
url: r.url || "",
|
|
84
|
+
}));
|
|
85
|
+
debugLog(`[synaluxSearch] got ${results.length} results (source=${data.source || "portal"})`);
|
|
86
|
+
return results
|
|
87
|
+
.map((r) => `Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`)
|
|
88
|
+
.join("\n\n");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Web search via Synalux portal — returns raw JSON string.
|
|
92
|
+
* Used by code-mode handlers that pass raw data to the QuickJS sandbox.
|
|
93
|
+
*/
|
|
94
|
+
export async function synaluxWebSearchRaw(query, count = 10) {
|
|
95
|
+
debugLog(`[synaluxSearch] web search raw: q="${query}", limit=${count}`);
|
|
96
|
+
const data = await portalPost("/api/v1/prism/search", {
|
|
97
|
+
query,
|
|
98
|
+
limit: Math.min(count, 20),
|
|
99
|
+
});
|
|
100
|
+
if (data.status === "error") {
|
|
101
|
+
throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
|
|
102
|
+
}
|
|
103
|
+
// Re-shape into the Brave-compatible format that code-mode handlers expect
|
|
104
|
+
const braveCompatible = {
|
|
105
|
+
web: {
|
|
106
|
+
results: (data.results || []).map((r) => ({
|
|
107
|
+
title: r.title || "",
|
|
108
|
+
description: r.description || "",
|
|
109
|
+
url: r.url || "",
|
|
110
|
+
})),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
debugLog(`[synaluxSearch] raw: ${braveCompatible.web.results.length} results`);
|
|
114
|
+
return JSON.stringify(braveCompatible);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Local/POI search via Synalux portal.
|
|
118
|
+
* Returns formatted text matching performLocalSearch() shape.
|
|
119
|
+
*/
|
|
120
|
+
export async function synaluxLocalSearch(query, count = 5) {
|
|
121
|
+
debugLog(`[synaluxSearch] local search: q="${query}", count=${count}`);
|
|
122
|
+
const data = await portalPost("/api/v1/prism/local-search", {
|
|
123
|
+
query,
|
|
124
|
+
count: Math.min(count, 20),
|
|
125
|
+
});
|
|
126
|
+
if (data.status === "error") {
|
|
127
|
+
throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
|
|
128
|
+
}
|
|
129
|
+
const results = data.results || [];
|
|
130
|
+
debugLog(`[synaluxSearch] local: got ${results.length} results`);
|
|
131
|
+
return results
|
|
132
|
+
.map((r) => `Name: ${r.name || "N/A"}\nAddress: ${r.address || "N/A"}\nPhone: ${r.phone || "N/A"}\nRating: ${r.rating || "N/A"}\nHours: ${r.hours || "N/A"}\nDescription: ${r.description || "No description available"}`)
|
|
133
|
+
.join("\n---\n");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Local/POI search raw — returns JSON string for code-mode sandbox.
|
|
137
|
+
*/
|
|
138
|
+
export async function synaluxLocalSearchRaw(query, count = 5) {
|
|
139
|
+
debugLog(`[synaluxSearch] local search raw: q="${query}", count=${count}`);
|
|
140
|
+
const data = await portalPost("/api/v1/prism/local-search", {
|
|
141
|
+
query,
|
|
142
|
+
count: Math.min(count, 20),
|
|
143
|
+
});
|
|
144
|
+
if (data.status === "error") {
|
|
145
|
+
throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
|
|
146
|
+
}
|
|
147
|
+
const results = data.results || [];
|
|
148
|
+
debugLog(`[synaluxSearch] local raw: ${results.length} results`);
|
|
149
|
+
// Build envelope compatible with code-mode sandbox expectations
|
|
150
|
+
const envelope = {
|
|
151
|
+
source: "local",
|
|
152
|
+
query,
|
|
153
|
+
count,
|
|
154
|
+
poisData: { results },
|
|
155
|
+
descriptionsData: {
|
|
156
|
+
descriptions: Object.fromEntries(results.map((r, i) => [String(i), r.description || ""])),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
return JSON.stringify(envelope);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* AI-grounded answers via Synalux portal.
|
|
163
|
+
*/
|
|
164
|
+
export async function synaluxBraveAnswers(query, model) {
|
|
165
|
+
debugLog(`[synaluxSearch] answers: q="${query}", model=${model || "default"}`);
|
|
166
|
+
const body = { query };
|
|
167
|
+
if (model)
|
|
168
|
+
body.model = model;
|
|
169
|
+
const data = await portalPost("/api/v1/prism/answers", body);
|
|
170
|
+
if (data.status === "error") {
|
|
171
|
+
throw new Error(`[synaluxSearch] portal error: ${data.error || "unknown"}`);
|
|
172
|
+
}
|
|
173
|
+
if (!data.answer) {
|
|
174
|
+
throw new Error("[synaluxSearch] answers endpoint returned empty answer");
|
|
175
|
+
}
|
|
176
|
+
return data.answer;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Scrape a URL via Synalux portal. Returns the extracted content string.
|
|
180
|
+
*/
|
|
181
|
+
export async function synaluxScrape(url, options) {
|
|
182
|
+
debugLog(`[synaluxSearch] scrape: url="${url}"`);
|
|
183
|
+
const body = { url };
|
|
184
|
+
if (options?.formats)
|
|
185
|
+
body.formats = options.formats;
|
|
186
|
+
if (options?.onlyMainContent !== undefined)
|
|
187
|
+
body.onlyMainContent = options.onlyMainContent;
|
|
188
|
+
if (options?.waitFor !== undefined)
|
|
189
|
+
body.waitFor = options.waitFor;
|
|
190
|
+
const data = await portalPost("/api/v1/prism/scrape", body);
|
|
191
|
+
if (data.status === "error") {
|
|
192
|
+
throw new Error(`[synaluxSearch] scrape error: ${data.error || "unknown"}`);
|
|
193
|
+
}
|
|
194
|
+
return data.content || "";
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.1.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
|
-
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy,
|
|
5
|
+
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder:7b / 14b open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "dist/server.js",
|