prism-mcp-server 20.2.6 → 20.2.8
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 +119 -113
- package/dist/lifecycle.js +3 -0
- package/dist/mcpTransportHealth.js +62 -0
- package/dist/server.js +10 -1
- package/dist/session/sessionContext.js +113 -15
- package/dist/storage/configStorage.js +100 -0
- package/dist/tools/graphHandlers.js +5 -4
- package/dist/tools/handlers.js +2 -2
- package/dist/tools/ledgerHandlers.js +12 -16
- package/dist/tools/prismInferHandler.js +274 -14
- package/dist/tools/queryMemoryNaturalHandler.js +514 -0
- package/dist/tools/sessionMemoryDefinitions.js +23 -5
- package/dist/tools/skillRouting.js +1 -0
- package/dist/tools/v12Handlers.js +2 -46
- package/dist/utils/braveApi.js +31 -48
- package/dist/utils/codingQualityPolicy.js +400 -0
- package/dist/utils/entitlements.js +1 -0
- package/dist/utils/layer1.js +63 -4
- package/dist/utils/nlQuery.js +6 -33
- package/dist/utils/qualityGate.js +32 -15
- package/dist/utils/routeContract.js +319 -0
- package/dist/utils/synaluxSearch.js +12 -8
- package/package.json +3 -2
- package/dist/boundaries/__tests__/boundaries.test.js +0 -46
- package/dist/session/__tests__/sessionContext.test.js +0 -134
- package/dist/tools/__tests__/ingestHandler.test.js +0 -323
- package/dist/tools/__tests__/layer1Integration.test.js +0 -590
- package/dist/tools/__tests__/ledgerHandlers.test.js +0 -1281
package/dist/utils/nlQuery.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* This closes the gap where users have to learn tool syntax to query memories.
|
|
8
8
|
* Instead, they ask plain English questions and get structured results.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Parsing pipeline:
|
|
11
11
|
* 1. Intent classification (decision, todo, file, general)
|
|
12
12
|
* 2. Key phrase extraction
|
|
13
|
-
* 3.
|
|
14
|
-
*
|
|
13
|
+
* 3. Structured memory-query construction
|
|
14
|
+
*
|
|
15
|
+
* Execution, web fallback, and grounded prism_infer synthesis live in
|
|
16
|
+
* queryMemoryNaturalHandler.ts. Keeping network/model work out of this parser
|
|
17
|
+
* makes intent handling deterministic and independently testable.
|
|
15
18
|
*/
|
|
16
19
|
import { debugLog } from "./logger.js";
|
|
17
20
|
// ─── Intent Classification ───────────────────────────────────
|
|
@@ -151,34 +154,4 @@ export function parseNLQuery(query, project) {
|
|
|
151
154
|
suggestedArgs: args,
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
|
-
/**
|
|
155
|
-
* Execute a natural language query end-to-end.
|
|
156
|
-
*
|
|
157
|
-
* Parses the query, executes the appropriate tool, and optionally
|
|
158
|
-
* synthesizes an answer using the local LLM.
|
|
159
|
-
*/
|
|
160
|
-
export async function executeNLQuery(query, project, synthesize = false) {
|
|
161
|
-
const parsed = parseNLQuery(query, project);
|
|
162
|
-
// Execute the search
|
|
163
|
-
let sources = [];
|
|
164
|
-
let answer = "";
|
|
165
|
-
try {
|
|
166
|
-
// Return the parsed query structure — the caller (handler) will
|
|
167
|
-
// execute the actual tool call using the suggestedTool and suggestedArgs.
|
|
168
|
-
answer =
|
|
169
|
-
`Intent: ${parsed.intent} (confidence: ${(parsed.confidence * 100).toFixed(0)}%)\n` +
|
|
170
|
-
`Keywords: ${parsed.extractedKeywords.join(", ")}\n` +
|
|
171
|
-
`Suggested tool: ${parsed.suggestedTool}\n` +
|
|
172
|
-
`Search query: "${parsed.searchQuery}"`;
|
|
173
|
-
}
|
|
174
|
-
catch (err) {
|
|
175
|
-
debugLog(`NL query execution failed: ${err}`);
|
|
176
|
-
answer = `Unable to parse query: ${err}`;
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
...parsed,
|
|
180
|
-
answer: answer || "No relevant memories found.",
|
|
181
|
-
sources,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
157
|
debugLog("v12.2: Natural language query module loaded");
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Quality Gate — deterministic check for obvious inference failures.
|
|
3
|
-
*
|
|
4
|
-
* NARROW by design: only high-precision signals that rarely false-positive.
|
|
5
|
-
* Does NOT judge correctness — that's the grounding verifier's job.
|
|
6
|
-
* Does NOT use refusal regex (too many false positives on legitimate output).
|
|
7
|
-
*
|
|
8
|
-
* Returns: { pass: boolean, reason?: string }
|
|
9
|
-
*/
|
|
1
|
+
import { parseRouteOutput } from "./routeContract.js";
|
|
10
2
|
/**
|
|
11
3
|
* Signal 5 — Tool-call bleed: pipe-delimited format leaking into non-tool turns.
|
|
12
4
|
* Matches <|tool_call|> and <|tool_call_end|> only — NOT angle-bracket <tool_call> variants
|
|
@@ -37,11 +29,22 @@ export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
|
37
29
|
if (finishReason === "length") {
|
|
38
30
|
return { pass: false, reason: "hard_truncation" };
|
|
39
31
|
}
|
|
40
|
-
// Signal 5: Tool-call bleed
|
|
41
|
-
//
|
|
42
|
-
//
|
|
32
|
+
// Signal 5: Tool-call bleed. The pipe envelope is invalid in chat/code,
|
|
33
|
+
// but it is the canonical trained output in route mode. Route mode parses
|
|
34
|
+
// the whole envelope and fails only when the contract is malformed.
|
|
43
35
|
if (TOOL_CALL_BLEED_RE.test(stripped)) {
|
|
44
|
-
|
|
36
|
+
if (mode === "route") {
|
|
37
|
+
const parsedRoute = parseRouteOutput(stripped);
|
|
38
|
+
if (parsedRoute.kind === "tool_call") {
|
|
39
|
+
// Continue through the remaining generic loop checks.
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return { pass: false, reason: "route_tool_call_malformed" };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return { pass: false, reason: "tool_call_bleed" };
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
// Signal 4: Exact-loop detection (two passes).
|
|
47
50
|
//
|
|
@@ -54,7 +57,11 @@ export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
|
54
57
|
.replace(/^#{1,6}\s+.*$/gm, "")
|
|
55
58
|
.replace(/^[\s*-]*\*{1,2}[^*]+\*{1,2}:?\s*$/gm, "");
|
|
56
59
|
const proseSentences = proseOnly.split(/[.!?\n]+/).map(s => s.trim()).filter(s => s.length > 10);
|
|
57
|
-
|
|
60
|
+
// Plain source code often repeats structural lines across methods. In code
|
|
61
|
+
// mode those are not prose loops, and splitting on "." corrupts member
|
|
62
|
+
// access (`node = self.root` becomes `node = self`). Pass B below still
|
|
63
|
+
// catches egregious repetition at the higher threshold.
|
|
64
|
+
if (mode !== "code" && proseSentences.length >= 6) {
|
|
58
65
|
const counts = new Map();
|
|
59
66
|
for (const s of proseSentences) {
|
|
60
67
|
const key = s.toLowerCase();
|
|
@@ -68,7 +75,17 @@ export function passesQualityGate(stripped, thinkOnly, finishReason, mode) {
|
|
|
68
75
|
// inside fake code blocks or other structural elements. Higher
|
|
69
76
|
// threshold avoids false positives on legitimate code patterns
|
|
70
77
|
// (e.g. `node = self.root` × 4 is fine, × 5 is suspicious).
|
|
71
|
-
|
|
78
|
+
// In code mode, periods are member-access operators, not sentence
|
|
79
|
+
// boundaries. Splitting `input.length` at "." made several different
|
|
80
|
+
// loops share the fragment `while (i < input` and falsely counted it as
|
|
81
|
+
// repeated output. Full-line matching still catches genuinely duplicated
|
|
82
|
+
// source while preserving valid repeated control-flow shapes.
|
|
83
|
+
const fullTextUnits = mode === "code"
|
|
84
|
+
? stripped.split(/\n+/)
|
|
85
|
+
: stripped.split(/[.!?\n]+/);
|
|
86
|
+
const allSentences = fullTextUnits
|
|
87
|
+
.map(s => s.trim())
|
|
88
|
+
.filter(s => s.length > 10);
|
|
72
89
|
if (allSentences.length >= 10) {
|
|
73
90
|
const counts = new Map();
|
|
74
91
|
for (const s of allSentences) {
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic route-output contract for prism_infer.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately contains no intent taxonomy or benchmark-derived
|
|
5
|
+
* routing heuristics. It only parses the public Prism tool-call envelope and
|
|
6
|
+
* enforces the caller-advertised registry before a host can act on a route.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_PRISM_ROUTE_TOOLS = new Set([
|
|
9
|
+
"session_load_context",
|
|
10
|
+
"session_save_ledger",
|
|
11
|
+
"session_save_handoff",
|
|
12
|
+
"session_compact_ledger",
|
|
13
|
+
"session_search_memory",
|
|
14
|
+
"knowledge_search",
|
|
15
|
+
"brave_web_search",
|
|
16
|
+
]);
|
|
17
|
+
const PIPE_START = "<|tool_call|>";
|
|
18
|
+
const PIPE_END = "<|tool_call_end|>";
|
|
19
|
+
const ANGLE_START = "<tool_call>";
|
|
20
|
+
const ANGLE_END = "</tool_call>";
|
|
21
|
+
const TOOL_NAME_RE = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/;
|
|
22
|
+
const MAX_ROUTE_OUTPUT_CHARS = 32_000;
|
|
23
|
+
const MAX_ROUTE_REASON_CHARS = 256;
|
|
24
|
+
const MAX_ARGUMENT_DEPTH = 32;
|
|
25
|
+
const MAX_ARGUMENT_NODES = 2_048;
|
|
26
|
+
const TOOL_CALL_KEYS = new Set(["name", "arguments", "args"]);
|
|
27
|
+
export function isRouteToolName(value) {
|
|
28
|
+
return typeof value === "string" && TOOL_NAME_RE.test(value);
|
|
29
|
+
}
|
|
30
|
+
function isBoundedJsonValue(root) {
|
|
31
|
+
const stack = [{ value: root, depth: 0 }];
|
|
32
|
+
let nodes = 0;
|
|
33
|
+
while (stack.length > 0) {
|
|
34
|
+
const current = stack.pop();
|
|
35
|
+
nodes += 1;
|
|
36
|
+
if (nodes > MAX_ARGUMENT_NODES || current.depth > MAX_ARGUMENT_DEPTH) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (current.value === null)
|
|
40
|
+
continue;
|
|
41
|
+
if (typeof current.value === "number") {
|
|
42
|
+
if (!Number.isFinite(current.value))
|
|
43
|
+
return false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof current.value === "string" ||
|
|
47
|
+
typeof current.value === "boolean") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (typeof current.value !== "object")
|
|
51
|
+
return false;
|
|
52
|
+
const values = Array.isArray(current.value)
|
|
53
|
+
? current.value
|
|
54
|
+
: Object.values(current.value);
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
stack.push({ value, depth: current.depth + 1 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function primitiveLeaves(value) {
|
|
62
|
+
const leaves = [];
|
|
63
|
+
const stack = [value];
|
|
64
|
+
while (stack.length > 0) {
|
|
65
|
+
const current = stack.pop();
|
|
66
|
+
if (current !== null && typeof current === "object") {
|
|
67
|
+
stack.push(...(Array.isArray(current)
|
|
68
|
+
? current
|
|
69
|
+
: Object.values(current)));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
leaves.push(current);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return leaves;
|
|
76
|
+
}
|
|
77
|
+
function promptContainsPrimitive(prompt, value) {
|
|
78
|
+
const rendered = String(value).toLowerCase()
|
|
79
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
80
|
+
return new RegExp(`(^|[^A-Za-z0-9_.-])${rendered}($|[^A-Za-z0-9_.-])`)
|
|
81
|
+
.test(prompt.toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
function promptContainsString(prompt, value) {
|
|
84
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
85
|
+
if (!normalizedValue)
|
|
86
|
+
return false;
|
|
87
|
+
if (/^[A-Za-z0-9_.:-]+$/.test(normalizedValue)) {
|
|
88
|
+
const escaped = normalizedValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
|
+
return new RegExp(`(^|[^A-Za-z0-9_.:-])${escaped}($|[^A-Za-z0-9_.:-])`)
|
|
90
|
+
.test(prompt.toLowerCase());
|
|
91
|
+
}
|
|
92
|
+
return prompt.toLowerCase().includes(normalizedValue);
|
|
93
|
+
}
|
|
94
|
+
function sameJsonValue(left, right) {
|
|
95
|
+
if (Object.is(left, right))
|
|
96
|
+
return true;
|
|
97
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
98
|
+
return Array.isArray(left) &&
|
|
99
|
+
Array.isArray(right) &&
|
|
100
|
+
left.length === right.length &&
|
|
101
|
+
left.every((entry, index) => sameJsonValue(entry, right[index]));
|
|
102
|
+
}
|
|
103
|
+
if (left === null ||
|
|
104
|
+
right === null ||
|
|
105
|
+
typeof left !== "object" ||
|
|
106
|
+
typeof right !== "object") {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const leftRecord = left;
|
|
110
|
+
const rightRecord = right;
|
|
111
|
+
const leftKeys = Object.keys(leftRecord).sort();
|
|
112
|
+
const rightKeys = Object.keys(rightRecord).sort();
|
|
113
|
+
return leftKeys.length === rightKeys.length &&
|
|
114
|
+
leftKeys.every((key, index) => key === rightKeys[index] &&
|
|
115
|
+
sameJsonValue(leftRecord[key], rightRecord[key]));
|
|
116
|
+
}
|
|
117
|
+
function hasDerivedArguments(correctedArgs, originalArgs, originalPrompt) {
|
|
118
|
+
const originalLeaves = primitiveLeaves(originalArgs);
|
|
119
|
+
return primitiveLeaves(correctedArgs).every((leaf) => {
|
|
120
|
+
if (originalLeaves.some((candidate) => Object.is(candidate, leaf))) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (typeof leaf === "string") {
|
|
124
|
+
return promptContainsString(originalPrompt, leaf);
|
|
125
|
+
}
|
|
126
|
+
if (typeof leaf === "number" || typeof leaf === "boolean") {
|
|
127
|
+
return promptContainsPrimitive(originalPrompt, leaf);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function parseToolJson(raw) {
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(raw);
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
136
|
+
return { kind: "malformed" };
|
|
137
|
+
}
|
|
138
|
+
const record = value;
|
|
139
|
+
if (!isRouteToolName(record.name)) {
|
|
140
|
+
return { kind: "malformed" };
|
|
141
|
+
}
|
|
142
|
+
if (Object.keys(record).some((key) => !TOOL_CALL_KEYS.has(key))) {
|
|
143
|
+
return { kind: "malformed" };
|
|
144
|
+
}
|
|
145
|
+
const hasArguments = Object.prototype.hasOwnProperty.call(record, "arguments");
|
|
146
|
+
const hasArgs = Object.prototype.hasOwnProperty.call(record, "args");
|
|
147
|
+
if (hasArguments && hasArgs)
|
|
148
|
+
return { kind: "malformed" };
|
|
149
|
+
const rawArgs = hasArguments
|
|
150
|
+
? record.arguments
|
|
151
|
+
: hasArgs
|
|
152
|
+
? record.args
|
|
153
|
+
: {};
|
|
154
|
+
if (!rawArgs || typeof rawArgs !== "object" || Array.isArray(rawArgs)) {
|
|
155
|
+
return { kind: "malformed" };
|
|
156
|
+
}
|
|
157
|
+
if (!isBoundedJsonValue(rawArgs))
|
|
158
|
+
return { kind: "malformed" };
|
|
159
|
+
return {
|
|
160
|
+
kind: "tool_call",
|
|
161
|
+
name: record.name,
|
|
162
|
+
args: rawArgs,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { kind: "malformed" };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function parseEnvelope(output, startToken, endToken) {
|
|
170
|
+
const start = output.indexOf(startToken);
|
|
171
|
+
const end = output.indexOf(endToken, start + startToken.length);
|
|
172
|
+
if (start < 0 ||
|
|
173
|
+
end < 0 ||
|
|
174
|
+
output.indexOf(startToken, start + startToken.length) >= 0 ||
|
|
175
|
+
output.indexOf(endToken, end + endToken.length) >= 0) {
|
|
176
|
+
return { kind: "malformed" };
|
|
177
|
+
}
|
|
178
|
+
const before = output.slice(0, start).trim();
|
|
179
|
+
const after = output.slice(end + endToken.length).trim();
|
|
180
|
+
if (before || after)
|
|
181
|
+
return { kind: "malformed" };
|
|
182
|
+
return parseToolJson(output.slice(start + startToken.length, end).trim());
|
|
183
|
+
}
|
|
184
|
+
export function parseRouteOutput(output) {
|
|
185
|
+
if (output.length > MAX_ROUTE_OUTPUT_CHARS)
|
|
186
|
+
return { kind: "malformed" };
|
|
187
|
+
const trimmed = output.trim();
|
|
188
|
+
const hasPipeMarker = trimmed.includes(PIPE_START) || trimmed.includes(PIPE_END);
|
|
189
|
+
if (hasPipeMarker)
|
|
190
|
+
return parseEnvelope(trimmed, PIPE_START, PIPE_END);
|
|
191
|
+
const hasAngleMarker = trimmed.includes(ANGLE_START) || trimmed.includes(ANGLE_END);
|
|
192
|
+
if (hasAngleMarker)
|
|
193
|
+
return parseEnvelope(trimmed, ANGLE_START, ANGLE_END);
|
|
194
|
+
// A route answer that starts like raw tool JSON is an attempted contract
|
|
195
|
+
// response. Treat invalid JSON/shape as malformed rather than returning it
|
|
196
|
+
// as ordinary prose that a host might accidentally interpret.
|
|
197
|
+
if (trimmed.startsWith("{"))
|
|
198
|
+
return parseToolJson(trimmed);
|
|
199
|
+
return { kind: "plain_text" };
|
|
200
|
+
}
|
|
201
|
+
export function applyLocalRouteContract(draft, allowedTools = DEFAULT_PRISM_ROUTE_TOOLS) {
|
|
202
|
+
const parsed = parseRouteOutput(draft);
|
|
203
|
+
if (parsed.kind === "plain_text") {
|
|
204
|
+
return { output: draft, action: "plain_text", source: "local" };
|
|
205
|
+
}
|
|
206
|
+
if (parsed.kind === "malformed") {
|
|
207
|
+
return {
|
|
208
|
+
output: "NO_TOOL",
|
|
209
|
+
action: "suppressed",
|
|
210
|
+
source: "local",
|
|
211
|
+
reason: "malformed_tool_call",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (parsed.name === "NO_TOOL") {
|
|
215
|
+
return { output: "NO_TOOL", action: "plain_text", source: "local" };
|
|
216
|
+
}
|
|
217
|
+
if (!allowedTools.has(parsed.name)) {
|
|
218
|
+
return {
|
|
219
|
+
output: "NO_TOOL",
|
|
220
|
+
action: "suppressed",
|
|
221
|
+
source: "local",
|
|
222
|
+
original_tool: parsed.name,
|
|
223
|
+
reason: "unadvertised_tool",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
output: draft,
|
|
228
|
+
action: "preserved",
|
|
229
|
+
source: "local",
|
|
230
|
+
original_tool: parsed.name,
|
|
231
|
+
final_tool: parsed.name,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Validate a private portal correction before trusting it.
|
|
236
|
+
*
|
|
237
|
+
* The portal is an authenticated service, but the public client still treats
|
|
238
|
+
* its response as untrusted network input. The declared action must agree with
|
|
239
|
+
* the output shape and with the original draft.
|
|
240
|
+
*/
|
|
241
|
+
export function validatePortalRouteGuardOutcome(value, originalDraft, allowedTools, originalPrompt = "") {
|
|
242
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
243
|
+
return null;
|
|
244
|
+
const record = value;
|
|
245
|
+
if (typeof record.output !== "string" ||
|
|
246
|
+
record.output.length === 0 ||
|
|
247
|
+
record.output.length > MAX_ROUTE_OUTPUT_CHARS ||
|
|
248
|
+
record.source !== "portal" ||
|
|
249
|
+
!["plain_text", "preserved", "remapped", "suppressed"].includes(typeof record.action === "string" ? record.action : "")) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
if (record.original_tool !== undefined &&
|
|
253
|
+
!isRouteToolName(record.original_tool)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (record.final_tool !== undefined &&
|
|
257
|
+
!isRouteToolName(record.final_tool)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
if (record.reason !== undefined &&
|
|
261
|
+
(typeof record.reason !== "string" ||
|
|
262
|
+
record.reason.length > MAX_ROUTE_REASON_CHARS)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const action = record.action;
|
|
266
|
+
const original = parseRouteOutput(originalDraft);
|
|
267
|
+
const corrected = parseRouteOutput(record.output);
|
|
268
|
+
if (action === "plain_text") {
|
|
269
|
+
if (original.kind !== "plain_text" || corrected.kind !== "plain_text") {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (action === "suppressed") {
|
|
274
|
+
if (original.kind !== "tool_call" ||
|
|
275
|
+
record.output.trim() !== "NO_TOOL" ||
|
|
276
|
+
(record.original_tool !== undefined &&
|
|
277
|
+
record.original_tool !== original.name) ||
|
|
278
|
+
record.final_tool !== undefined) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
if (original.kind !== "tool_call" ||
|
|
284
|
+
corrected.kind !== "tool_call" ||
|
|
285
|
+
!allowedTools.has(corrected.name) ||
|
|
286
|
+
record.original_tool !== original.name ||
|
|
287
|
+
record.final_tool !== corrected.name) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (action === "preserved" && corrected.name !== original.name) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (action === "remapped" && corrected.name === original.name) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (action === "preserved" &&
|
|
297
|
+
!sameJsonValue(corrected.args, original.args)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (action === "remapped" &&
|
|
301
|
+
!hasDerivedArguments(corrected.args, original.args, originalPrompt)) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
output: record.output,
|
|
307
|
+
action,
|
|
308
|
+
source: "portal",
|
|
309
|
+
...(record.original_tool !== undefined
|
|
310
|
+
? { original_tool: record.original_tool }
|
|
311
|
+
: {}),
|
|
312
|
+
...(record.final_tool !== undefined
|
|
313
|
+
? { final_tool: record.final_tool }
|
|
314
|
+
: {}),
|
|
315
|
+
...(record.reason !== undefined
|
|
316
|
+
? { reason: record.reason }
|
|
317
|
+
: {}),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -69,7 +69,7 @@ async function portalPost(path, body, timeoutMs = 15_000) {
|
|
|
69
69
|
* the shape of performWebSearch() in braveApi.ts.
|
|
70
70
|
*/
|
|
71
71
|
export async function synaluxWebSearch(query, count = 10) {
|
|
72
|
-
debugLog(`[synaluxSearch] web search:
|
|
72
|
+
debugLog(`[synaluxSearch] web search: query_chars=${query.length}, limit=${count}`);
|
|
73
73
|
const data = await portalPost("/api/v1/prism/search", {
|
|
74
74
|
query,
|
|
75
75
|
limit: Math.min(count, 20),
|
|
@@ -92,7 +92,7 @@ export async function synaluxWebSearch(query, count = 10) {
|
|
|
92
92
|
* Used by code-mode handlers that pass raw data to the QuickJS sandbox.
|
|
93
93
|
*/
|
|
94
94
|
export async function synaluxWebSearchRaw(query, count = 10) {
|
|
95
|
-
debugLog(`[synaluxSearch] web search raw:
|
|
95
|
+
debugLog(`[synaluxSearch] web search raw: query_chars=${query.length}, limit=${count}`);
|
|
96
96
|
const data = await portalPost("/api/v1/prism/search", {
|
|
97
97
|
query,
|
|
98
98
|
limit: Math.min(count, 20),
|
|
@@ -118,7 +118,7 @@ export async function synaluxWebSearchRaw(query, count = 10) {
|
|
|
118
118
|
* Returns formatted text matching performLocalSearch() shape.
|
|
119
119
|
*/
|
|
120
120
|
export async function synaluxLocalSearch(query, count = 5) {
|
|
121
|
-
debugLog(`[synaluxSearch] local search:
|
|
121
|
+
debugLog(`[synaluxSearch] local search: query_chars=${query.length}, count=${count}`);
|
|
122
122
|
const data = await portalPost("/api/v1/prism/local-search", {
|
|
123
123
|
query,
|
|
124
124
|
count: Math.min(count, 20),
|
|
@@ -136,7 +136,7 @@ export async function synaluxLocalSearch(query, count = 5) {
|
|
|
136
136
|
* Local/POI search raw — returns JSON string for code-mode sandbox.
|
|
137
137
|
*/
|
|
138
138
|
export async function synaluxLocalSearchRaw(query, count = 5) {
|
|
139
|
-
debugLog(`[synaluxSearch] local search raw:
|
|
139
|
+
debugLog(`[synaluxSearch] local search raw: query_chars=${query.length}, count=${count}`);
|
|
140
140
|
const data = await portalPost("/api/v1/prism/local-search", {
|
|
141
141
|
query,
|
|
142
142
|
count: Math.min(count, 20),
|
|
@@ -162,7 +162,7 @@ export async function synaluxLocalSearchRaw(query, count = 5) {
|
|
|
162
162
|
* AI-grounded answers via Synalux portal.
|
|
163
163
|
*/
|
|
164
164
|
export async function synaluxBraveAnswers(query, model) {
|
|
165
|
-
debugLog(`[synaluxSearch] answers:
|
|
165
|
+
debugLog(`[synaluxSearch] answers: query_chars=${query.length}, model=${model || "default"}`);
|
|
166
166
|
const body = { query };
|
|
167
167
|
if (model)
|
|
168
168
|
body.model = model;
|
|
@@ -179,7 +179,7 @@ export async function synaluxBraveAnswers(query, model) {
|
|
|
179
179
|
* Scrape a URL via Synalux portal. Returns the extracted content string.
|
|
180
180
|
*/
|
|
181
181
|
export async function synaluxScrape(url, options) {
|
|
182
|
-
debugLog(`[synaluxSearch] scrape:
|
|
182
|
+
debugLog(`[synaluxSearch] scrape: url_chars=${url.length}`);
|
|
183
183
|
const body = { url };
|
|
184
184
|
if (options?.formats)
|
|
185
185
|
body.formats = options.formats;
|
|
@@ -187,9 +187,13 @@ export async function synaluxScrape(url, options) {
|
|
|
187
187
|
body.onlyMainContent = options.onlyMainContent;
|
|
188
188
|
if (options?.waitFor !== undefined)
|
|
189
189
|
body.waitFor = options.waitFor;
|
|
190
|
-
const data = await portalPost("/api/v1/prism/scrape", body);
|
|
190
|
+
const data = await portalPost("/api/v1/prism/scrape", body, options?.timeoutMs);
|
|
191
191
|
if (data.status === "error") {
|
|
192
192
|
throw new Error(`[synaluxSearch] scrape error: ${data.error || "unknown"}`);
|
|
193
193
|
}
|
|
194
|
-
return data.content ||
|
|
194
|
+
return data.content ||
|
|
195
|
+
data.data?.markdown ||
|
|
196
|
+
data.data?.html ||
|
|
197
|
+
data.data?.rawHtml ||
|
|
198
|
+
"";
|
|
195
199
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.2.
|
|
3
|
+
"version": "20.2.8",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
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, 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–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -154,10 +154,11 @@
|
|
|
154
154
|
"jsdom": "^29.0.1",
|
|
155
155
|
"openai": "^6.32.0",
|
|
156
156
|
"p-limit": "^7.3.0",
|
|
157
|
-
"postcss": "8.5.
|
|
157
|
+
"postcss": "8.5.23",
|
|
158
158
|
"quickjs-emscripten": "^0.32.0",
|
|
159
159
|
"smol-toml": "^1.7.0",
|
|
160
160
|
"stream-json": "^2.0.0",
|
|
161
|
+
"tldts": "^7.0.27",
|
|
161
162
|
"turndown": "^7.2.2",
|
|
162
163
|
"zod": "^4.3.6"
|
|
163
164
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* boundaries.ts unit tests
|
|
3
|
-
*
|
|
4
|
-
* Verifies structural invariants of the safety declaration:
|
|
5
|
-
* - BOUNDARIES_VERSION is present and non-empty
|
|
6
|
-
* - BOUNDARIES_TEXT covers safety-critical concepts
|
|
7
|
-
* - Architecture/routing docs moved to server instructions (not per-call)
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect } from "vitest";
|
|
10
|
-
import { BOUNDARIES_VERSION, BOUNDARIES_TEXT } from "../../boundaries/boundaries.js";
|
|
11
|
-
describe("BOUNDARIES_VERSION", () => {
|
|
12
|
-
it("is a non-empty string", () => {
|
|
13
|
-
expect(typeof BOUNDARIES_VERSION).toBe("string");
|
|
14
|
-
expect(BOUNDARIES_VERSION.length).toBeGreaterThan(0);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
describe("BOUNDARIES_TEXT structure", () => {
|
|
18
|
-
it("is a non-empty string under 500 chars (compressed from ~2000)", () => {
|
|
19
|
-
expect(typeof BOUNDARIES_TEXT).toBe("string");
|
|
20
|
-
expect(BOUNDARIES_TEXT.length).toBeGreaterThan(50);
|
|
21
|
-
expect(BOUNDARIES_TEXT.length).toBeLessThan(600);
|
|
22
|
-
});
|
|
23
|
-
it("has no leading or trailing whitespace (trim() was applied)", () => {
|
|
24
|
-
expect(BOUNDARIES_TEXT).toBe(BOUNDARIES_TEXT.trim());
|
|
25
|
-
});
|
|
26
|
-
it("references BCBA reserved categories", () => {
|
|
27
|
-
expect(BOUNDARIES_TEXT).toMatch(/BCBA reserved/i);
|
|
28
|
-
});
|
|
29
|
-
it("references fail-closed refusal for reserved content", () => {
|
|
30
|
-
expect(BOUNDARIES_TEXT).toMatch(/refused/i);
|
|
31
|
-
});
|
|
32
|
-
it("states AAC access is never restricted as a consequence", () => {
|
|
33
|
-
expect(BOUNDARIES_TEXT).toMatch(/AAC access is never restricted/i);
|
|
34
|
-
});
|
|
35
|
-
it("references restraint as reserved", () => {
|
|
36
|
-
expect(BOUNDARIES_TEXT).toMatch(/restraint/i);
|
|
37
|
-
});
|
|
38
|
-
it("references crisis/self-harm interception", () => {
|
|
39
|
-
expect(BOUNDARIES_TEXT).toMatch(/crisis|self-harm/i);
|
|
40
|
-
});
|
|
41
|
-
it("does NOT contain architecture/routing docs (those moved to server instructions)", () => {
|
|
42
|
-
expect(BOUNDARIES_TEXT).not.toContain("session_save_ledger");
|
|
43
|
-
expect(BOUNDARIES_TEXT).not.toContain("inference routing");
|
|
44
|
-
expect(BOUNDARIES_TEXT).not.toMatch(/host note/i);
|
|
45
|
-
});
|
|
46
|
-
});
|