prism-mcp-server 20.2.8 → 20.2.9
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 +10 -7
- package/dist/cli.js +45 -0
- package/dist/connect.js +7 -0
- package/dist/evidenceWorkflowPolicy.js +19 -0
- package/dist/mcpTransportHealth.js +10 -2
- package/dist/server.js +2 -0
- package/dist/skillManifestSync.js +9 -6
- package/dist/storage/synalux.js +41 -1
- package/dist/tools/behavioralVerifierHandler.js +20 -4
- package/dist/tools/ledgerHandlers.js +125 -80
- package/dist/tools/sessionMemoryDefinitions.js +2 -1
- package/dist/tools/skillRouting.js +3 -2
- package/dist/utils/codingQualityPolicy.js +9 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -148,13 +148,16 @@ that would require a host lifecycle hook, launcher, extension, or Prism-owned
|
|
|
148
148
|
panel. Context loading itself remains complete even when a host shortens the
|
|
149
149
|
visible reply.
|
|
150
150
|
|
|
151
|
-
Free accounts receive the
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
Free accounts receive only the public hook-free `prism-startup` package; the MCP
|
|
152
|
+
server still supplies a compact, non-proprietary safety and evidence contract.
|
|
153
|
+
Authenticated paid accounts receive the protected behavioral and engineering
|
|
154
|
+
packages plus the current subscribed routing set. The paid
|
|
155
|
+
`evidence-first-protocol` keeps ordinary coding lightweight: one correlated
|
|
156
|
+
reproduction is enough to begin an edit, while strict acceptance starts only
|
|
157
|
+
before a completion claim, push, or release and inspects only the exact artifacts
|
|
158
|
+
used as proof. Upgrades install newly entitled packages; verified downgrades
|
|
159
|
+
remove only Prism-owned packages while preserving local skills and locally
|
|
160
|
+
modified conflicts.
|
|
158
161
|
|
|
159
162
|
When upgrading an older Claude Code installation, `prism connect` removes only
|
|
160
163
|
the exact Prism-owned startup, skill-sync, handoff, and drift hook actions from
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { configureClaudeAgentPolicy, configureClaudeNativeStartup, configureCode
|
|
|
12
12
|
import { runBrowserCli } from './browserCli.js';
|
|
13
13
|
import { filterPrismMemoryContext } from './utils/memoryQuality.js';
|
|
14
14
|
import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
|
|
15
|
+
import { verifyBehaviorHandler } from './tools/behavioralVerifierHandler.js';
|
|
15
16
|
const program = new Command();
|
|
16
17
|
/** Build the stable `prism load --json` envelope from depth-specific context. */
|
|
17
18
|
export function buildLoadJsonOutput(project, data, level, metadata) {
|
|
@@ -111,6 +112,50 @@ program
|
|
|
111
112
|
.command('bootstrap')
|
|
112
113
|
.description('Print the canonical dashboard-configured first-turn Prism greeting')
|
|
113
114
|
.action(runBootstrapCommand);
|
|
115
|
+
/**
|
|
116
|
+
* One-shot fallback for hosts whose long-lived MCP transport has closed.
|
|
117
|
+
* This calls the canonical handler, including its authenticated portal path
|
|
118
|
+
* and fail-closed offline scenario; it never asks the host to invent one.
|
|
119
|
+
*/
|
|
120
|
+
export async function runVerifyBehaviorCommand(options) {
|
|
121
|
+
try {
|
|
122
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
|
|
123
|
+
process.env.SYNALUX_BASE_URL?.trim() ||
|
|
124
|
+
(await getSetting('PRISM_SYNALUX_BASE_URL', '')).trim() ||
|
|
125
|
+
(await getSetting('SYNALUX_BASE_URL', '')).trim() ||
|
|
126
|
+
'https://synalux.ai';
|
|
127
|
+
const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim() ||
|
|
128
|
+
(await getSetting('PRISM_SYNALUX_API_KEY', '')).trim();
|
|
129
|
+
process.env.PRISM_SYNALUX_BASE_URL = baseUrl.replace(/\/+$/, '');
|
|
130
|
+
if (apiKey)
|
|
131
|
+
process.env.PRISM_SYNALUX_API_KEY = apiKey;
|
|
132
|
+
const result = await verifyBehaviorHandler({
|
|
133
|
+
file_path: options.file,
|
|
134
|
+
change_summary: options.summary,
|
|
135
|
+
...(options.project ? { project: options.project } : {}),
|
|
136
|
+
...(options.workspaceId ? { workspace_id: options.workspaceId } : {}),
|
|
137
|
+
});
|
|
138
|
+
const output = result.content
|
|
139
|
+
?.map((part) => part?.text)
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n') || '';
|
|
142
|
+
if (!output)
|
|
143
|
+
throw new Error('verify_behavior returned no scenario');
|
|
144
|
+
console.log(output);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
console.error(`Behavioral verification failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
program
|
|
152
|
+
.command('verify-behavior')
|
|
153
|
+
.description('Get the behavioral edit scenario when an MCP transport is unavailable')
|
|
154
|
+
.requiredOption('--file <path>', 'Path of the file about to be edited')
|
|
155
|
+
.requiredOption('--summary <text>', 'Brief description of the intended change')
|
|
156
|
+
.option('--project <project>', 'Project identifier for workspace-scoped scenarios')
|
|
157
|
+
.option('--workspace-id <id>', 'Workspace ID for custom scenarios')
|
|
158
|
+
.action(runVerifyBehaviorCommand);
|
|
114
159
|
// Parsed by the direct dispatch at the bottom so all Python CLI flags pass
|
|
115
160
|
// through unchanged. Registering it here keeps the command visible in help.
|
|
116
161
|
program
|
package/dist/connect.js
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep, win32 as w
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
|
+
import { EVIDENCE_WORKFLOW_POLICY_LINES } from "./evidenceWorkflowPolicy.js";
|
|
7
8
|
import { LOCAL_FIRST_POLICY_ID, LOCAL_FIRST_POLICY_LINES } from "./localFirstPolicy.js";
|
|
8
9
|
export const CONNECT_HOSTS = [
|
|
9
10
|
"claude-code",
|
|
@@ -46,6 +47,8 @@ const CODEX_STARTUP_BODY = [
|
|
|
46
47
|
"block is managed by `prism connect`; do not edit it manually.",
|
|
47
48
|
"",
|
|
48
49
|
...LOCAL_FIRST_POLICY_LINES,
|
|
50
|
+
"",
|
|
51
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
49
52
|
];
|
|
50
53
|
const CONNECT_STORAGE_BACKENDS = ["auto", "local", "synalux", "supabase"];
|
|
51
54
|
const LEGACY_CLAUDE_PROJECT_PRISM_ENTRY = {
|
|
@@ -542,6 +545,8 @@ function serializeClaudeStartupBlock(newline) {
|
|
|
542
545
|
"managed by `prism connect`; do not edit it manually.",
|
|
543
546
|
"",
|
|
544
547
|
...LOCAL_FIRST_POLICY_LINES,
|
|
548
|
+
"",
|
|
549
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
545
550
|
CLAUDE_STARTUP_MANAGED_END,
|
|
546
551
|
"",
|
|
547
552
|
].join(newline);
|
|
@@ -617,6 +622,8 @@ function serializeGeminiStartupBlock(newline) {
|
|
|
617
622
|
"session_detect_drift calls. This block is managed by `prism connect`; do not edit it manually.",
|
|
618
623
|
"",
|
|
619
624
|
...LOCAL_FIRST_POLICY_LINES,
|
|
625
|
+
"",
|
|
626
|
+
...EVIDENCE_WORKFLOW_POLICY_LINES,
|
|
620
627
|
GEMINI_STARTUP_MANAGED_END,
|
|
621
628
|
"",
|
|
622
629
|
].join(newline);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimum evidence contract shared by every MCP host.
|
|
3
|
+
*
|
|
4
|
+
* Native-skill hosts also receive the full evidence-first protocol. Keep this
|
|
5
|
+
* compact copy in MCP initialize instructions so hosts without a filesystem
|
|
6
|
+
* skill surface, including Claude Desktop, still follow the same workflow.
|
|
7
|
+
*/
|
|
8
|
+
export const EVIDENCE_WORKFLOW_POLICY_LINES = [
|
|
9
|
+
"## Prism evidence workflow",
|
|
10
|
+
"During diagnosis and editing, one trustworthy correlated reproduction is enough; do not block coding on",
|
|
11
|
+
"inspecting unrelated diagnostic screenshots, trace frames, or abandoned attempts.",
|
|
12
|
+
"Before a completion claim, push, or release, exercise the corrected path with fresh evidence from the current",
|
|
13
|
+
"build and bind stateful proof to the exact run and entity. Inspect every artifact used to support the claim.",
|
|
14
|
+
"When a screenshot is requested or used as proof, the active agent must open it and compare its visible state",
|
|
15
|
+
"with the issue's expected and forbidden states. Complete that review yourself; do not ask the user to verify it.",
|
|
16
|
+
"A screenshot is an observation, not absolute truth. Reject stale, wrong-run, wrong-entity, or visibly failing",
|
|
17
|
+
"evidence even when its metadata says passed.",
|
|
18
|
+
];
|
|
19
|
+
export const EVIDENCE_WORKFLOW_POLICY_TEXT = EVIDENCE_WORKFLOW_POLICY_LINES.join(" ");
|
|
@@ -28,8 +28,16 @@ export function monitorMcpTransport(server, options) {
|
|
|
28
28
|
options.onFailure(reason, error);
|
|
29
29
|
};
|
|
30
30
|
const handleClose = () => {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
let closeError;
|
|
32
|
+
try {
|
|
33
|
+
previousOnClose?.();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
closeError = error instanceof Error ? error : new Error(String(error));
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
fail("MCP_TRANSPORT_CLOSED", closeError);
|
|
40
|
+
}
|
|
33
41
|
};
|
|
34
42
|
server.onclose = handleClose;
|
|
35
43
|
const check = async () => {
|
package/dist/server.js
CHANGED
|
@@ -85,6 +85,7 @@ import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
|
|
|
85
85
|
import { recordInvocation } from "./utils/analytics.js";
|
|
86
86
|
import { BOUNDARIES_TEXT } from "./boundaries/boundaries.js";
|
|
87
87
|
import { triggerSkillManifestSync } from "./skillManifestSync.js";
|
|
88
|
+
import { EVIDENCE_WORKFLOW_POLICY_TEXT } from "./evidenceWorkflowPolicy.js";
|
|
88
89
|
import { LOCAL_FIRST_POLICY_TEXT } from "./localFirstPolicy.js";
|
|
89
90
|
// ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
|
|
90
91
|
import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL, CODE_MODE_TRANSFORM_TOOL, BRAVE_ANSWERS_TOOL, RESEARCH_PAPER_ANALYSIS_TOOL, webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, braveLocalSearchCodeModeHandler, codeModeTransformHandler, braveAnswersHandler, researchPaperAnalysisHandler, } from "./tools/index.js";
|
|
@@ -328,6 +329,7 @@ export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI A
|
|
|
328
329
|
`Reuse the conversation_id returned by session_bootstrap in structuredContent for those saves and for ` +
|
|
329
330
|
`session_detect_drift, the 60-minute goal-alignment drift check. Do not add the id to the visible greeting.\n\n` +
|
|
330
331
|
`${LOCAL_FIRST_POLICY_TEXT}\n\n` +
|
|
332
|
+
`${EVIDENCE_WORKFLOW_POLICY_TEXT}\n\n` +
|
|
331
333
|
`Architecture: session_save_ledger and session_save_handoff require context loaded by session_bootstrap ` +
|
|
332
334
|
`or session_load_context when a conversation_id is supplied. ${BOUNDARIES_TEXT} ` +
|
|
333
335
|
`All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`;
|
|
@@ -4,7 +4,7 @@ import { access, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realp
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { applyManagedSkillManifest, getSetting, refreshConfigStorageCache, } from "./storage/configStorage.js";
|
|
7
|
-
import { REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
|
|
7
|
+
import { FREE_NATIVE_SKILL_NAMES, REQUIRED_NATIVE_SKILL_NAMES } from "./tools/skillRouting.js";
|
|
8
8
|
import { getSynaluxJwt, invalidateSynaluxJwt } from "./utils/synaluxJwt.js";
|
|
9
9
|
const OWNER = "prism-skill-sync-v1";
|
|
10
10
|
const MARKER = ".prism-managed.json";
|
|
@@ -181,17 +181,20 @@ export function validateSkillManifest(payload) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
const
|
|
185
|
-
|
|
184
|
+
const requiredForTier = value.tier === "free"
|
|
185
|
+
? FREE_NATIVE_SKILL_NAMES
|
|
186
|
+
: REQUIRED_NATIVE_SKILL_NAMES;
|
|
187
|
+
const requiredNames = new Set(requiredForTier);
|
|
188
|
+
for (const required of requiredForTier) {
|
|
186
189
|
const requiredSkill = skills.find((skill) => skill.name === required);
|
|
187
190
|
if (!requiredSkill)
|
|
188
|
-
throw new Error(`manifest is missing required
|
|
191
|
+
throw new Error(`manifest is missing required native skill: ${required}`);
|
|
189
192
|
if (!requiredSkill.metadata.protected || !requiredSkill.metadata.categories.includes("universal")) {
|
|
190
|
-
throw new Error(`required skill is not protected universal: ${required}`);
|
|
193
|
+
throw new Error(`required native skill is not protected universal: ${required}`);
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
if (value.tier === "free" && (skills.length !== requiredNames.size || skills.some((skill) => !requiredNames.has(skill.name)))) {
|
|
194
|
-
throw new Error("free manifest must contain exactly the
|
|
197
|
+
throw new Error("free manifest must contain exactly the public startup package");
|
|
195
198
|
}
|
|
196
199
|
const normalized = {
|
|
197
200
|
schema_version: 1,
|
package/dist/storage/synalux.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* Methods migrated to portal:
|
|
21
21
|
* - saveLedger → POST /api/v1/prism/memory action=save_ledger
|
|
22
22
|
* - saveHandoff → POST /api/v1/prism/memory action=save_handoff
|
|
23
|
+
* - saveHistorySnapshot → POST /api/v1/prism/memory action=save_history_snapshot
|
|
23
24
|
* - loadContext → POST /api/v1/prism/memory action=load_context
|
|
24
25
|
* - searchKnowledge → POST /api/v1/prism/memory action=search
|
|
25
26
|
* - softDeleteLedger → POST /api/v1/prism/memory action=forget_memory (Phase 3 Tier A)
|
|
@@ -203,7 +204,46 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
203
204
|
role: handoff.role,
|
|
204
205
|
expected_version: expectedVersion ?? undefined,
|
|
205
206
|
});
|
|
206
|
-
|
|
207
|
+
const candidate = Object.prototype.hasOwnProperty.call(result, "result")
|
|
208
|
+
? result.result
|
|
209
|
+
: Object.prototype.hasOwnProperty.call(result, "handoff")
|
|
210
|
+
? result.handoff
|
|
211
|
+
: result;
|
|
212
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
213
|
+
throw new Error("[SynaluxStorage] Invalid save_handoff response: missing result");
|
|
214
|
+
}
|
|
215
|
+
const value = candidate;
|
|
216
|
+
if (value.status === "conflict" && Number.isSafeInteger(value.current_version)) {
|
|
217
|
+
return {
|
|
218
|
+
status: "conflict",
|
|
219
|
+
current_version: value.current_version,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if ((value.status === "created" || value.status === "updated")
|
|
223
|
+
&& Number.isSafeInteger(value.version)) {
|
|
224
|
+
return {
|
|
225
|
+
status: value.status,
|
|
226
|
+
version: value.version,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Older portal RPC wrappers returned only the new version. Preserve that
|
|
230
|
+
// rolling-upgrade contract without accepting an unversioned success.
|
|
231
|
+
if (value.status === undefined && Number.isSafeInteger(value.version)) {
|
|
232
|
+
return {
|
|
233
|
+
status: "updated",
|
|
234
|
+
version: value.version,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
throw new Error("[SynaluxStorage] Invalid save_handoff response: malformed OCC result");
|
|
238
|
+
}
|
|
239
|
+
async saveHistorySnapshot(handoff, branch = "main") {
|
|
240
|
+
await this.portalPost("/api/v1/prism/memory", {
|
|
241
|
+
action: "save_history_snapshot",
|
|
242
|
+
project: handoff.project,
|
|
243
|
+
version: handoff.version,
|
|
244
|
+
snapshot: handoff,
|
|
245
|
+
branch,
|
|
246
|
+
});
|
|
207
247
|
}
|
|
208
248
|
// ─── Context ─────────────────────────────────────────────────
|
|
209
249
|
async loadContext(project, level, userId, role) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* FAIL-CLOSED: if the portal is unreachable, returns a generic
|
|
9
9
|
* verification challenge rather than skipping verification.
|
|
10
10
|
*/
|
|
11
|
-
import { PRISM_SYNALUX_BASE_URL
|
|
11
|
+
import { PRISM_SYNALUX_BASE_URL } from "../config.js";
|
|
12
12
|
import { getSynaluxJwt } from "../utils/synaluxJwt.js";
|
|
13
13
|
const FALLBACK_SCENARIO = [
|
|
14
14
|
"⚠️ BEHAVIORAL VERIFICATION (OFFLINE MODE)",
|
|
@@ -33,7 +33,13 @@ export async function verifyBehaviorHandler(args) {
|
|
|
33
33
|
return { content: [{ type: "text", text: await buildScenarioText(args) }] };
|
|
34
34
|
}
|
|
35
35
|
async function buildScenarioText(args) {
|
|
36
|
-
|
|
36
|
+
const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
|
|
37
|
+
process.env.SYNALUX_BASE_URL?.trim() ||
|
|
38
|
+
PRISM_SYNALUX_BASE_URL;
|
|
39
|
+
// OAuth/JWT-backed installs intentionally do not copy a long-lived API key
|
|
40
|
+
// into host configuration. A valid portal URL is enough to attempt the
|
|
41
|
+
// short-lived JWT flow; getSynaluxJwt() remains the fail-closed auth gate.
|
|
42
|
+
if (!baseUrl) {
|
|
37
43
|
return FALLBACK_SCENARIO;
|
|
38
44
|
}
|
|
39
45
|
const jwt = await getSynaluxJwt();
|
|
@@ -42,7 +48,7 @@ async function buildScenarioText(args) {
|
|
|
42
48
|
return FALLBACK_SCENARIO;
|
|
43
49
|
}
|
|
44
50
|
try {
|
|
45
|
-
const url = `${
|
|
51
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/prism/verify-behavior`;
|
|
46
52
|
const res = await fetch(url, {
|
|
47
53
|
method: "POST",
|
|
48
54
|
headers: {
|
|
@@ -60,7 +66,11 @@ async function buildScenarioText(args) {
|
|
|
60
66
|
console.error(`[verify-behavior] ⚠️ portal returned ${res.status} — fail-closed. URL: ${url}`);
|
|
61
67
|
return FALLBACK_SCENARIO;
|
|
62
68
|
}
|
|
63
|
-
const data =
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!isVerifyBehaviorResult(data)) {
|
|
71
|
+
console.error("[verify-behavior] ⚠️ portal returned a malformed response — fail-closed");
|
|
72
|
+
return FALLBACK_SCENARIO;
|
|
73
|
+
}
|
|
64
74
|
return formatResult(data);
|
|
65
75
|
}
|
|
66
76
|
catch (err) {
|
|
@@ -68,6 +78,12 @@ async function buildScenarioText(args) {
|
|
|
68
78
|
return FALLBACK_SCENARIO;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
81
|
+
function isVerifyBehaviorResult(value) {
|
|
82
|
+
return typeof value === "object" &&
|
|
83
|
+
value !== null &&
|
|
84
|
+
!Array.isArray(value) &&
|
|
85
|
+
typeof value.requires_verification === "boolean";
|
|
86
|
+
}
|
|
71
87
|
function formatResult(data) {
|
|
72
88
|
if (!data.requires_verification) {
|
|
73
89
|
return JSON.stringify({ requires_verification: false, reason: data.reason || "non-behavioral file" });
|
|
@@ -142,29 +142,42 @@ function parseNativeSkillNames(value) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
async function resolveNativeSkillManifestSnapshot(syncResult) {
|
|
145
|
-
const { REQUIRED_NATIVE_SKILL_NAMES } = await import("./skillRouting.js");
|
|
145
|
+
const { FREE_NATIVE_SKILL_NAMES, REQUIRED_NATIVE_SKILL_NAMES } = await import("./skillRouting.js");
|
|
146
146
|
const [storedNamesValue, storedTierValue] = await Promise.all([
|
|
147
147
|
getSetting("skill_manifest:names", "[]"),
|
|
148
148
|
getSetting("skill_manifest:tier", ""),
|
|
149
149
|
]);
|
|
150
|
-
const
|
|
151
|
-
? syncResult.entitledNames
|
|
150
|
+
const partialNamesInput = syncResult.status === "partial" && Array.isArray(syncResult.entitledNames)
|
|
151
|
+
? syncResult.entitledNames
|
|
152
|
+
: null;
|
|
153
|
+
const partialProvided = partialNamesInput !== null;
|
|
154
|
+
const partialNames = partialNamesInput
|
|
155
|
+
? partialNamesInput.filter((name) => typeof name === "string" && NATIVE_SKILL_NAME.test(name))
|
|
152
156
|
: [];
|
|
153
157
|
const storedNames = parseNativeSkillNames(storedNamesValue);
|
|
154
|
-
const names = partialNames.length > 0
|
|
155
|
-
? [...new Set(partialNames)]
|
|
156
|
-
: storedNames.length > 0
|
|
157
|
-
? storedNames
|
|
158
|
-
: [...REQUIRED_NATIVE_SKILL_NAMES];
|
|
159
|
-
const source = partialNames.length > 0
|
|
160
|
-
? "validated-partial"
|
|
161
|
-
: storedNames.length > 0
|
|
162
|
-
? "committed"
|
|
163
|
-
: "protected-fallback";
|
|
164
158
|
const candidateTier = syncResult.status === "partial" ? syncResult.tier : storedTierValue || syncResult.tier;
|
|
165
159
|
const tier = typeof candidateTier === "string" && SYNALUX_TIERS.has(candidateTier)
|
|
166
160
|
? candidateTier
|
|
167
161
|
: "free";
|
|
162
|
+
const tierFallbackNames = tier === "free"
|
|
163
|
+
? FREE_NATIVE_SKILL_NAMES
|
|
164
|
+
: REQUIRED_NATIVE_SKILL_NAMES;
|
|
165
|
+
const freeNames = new Set(FREE_NATIVE_SKILL_NAMES);
|
|
166
|
+
const namesForTier = (names) => tier === "free"
|
|
167
|
+
? names.filter((name) => freeNames.has(name))
|
|
168
|
+
: names;
|
|
169
|
+
const entitledPartialNames = namesForTier([...new Set(partialNames)]);
|
|
170
|
+
const entitledStoredNames = namesForTier(storedNames);
|
|
171
|
+
const names = partialProvided
|
|
172
|
+
? entitledPartialNames.length > 0 ? entitledPartialNames : [...tierFallbackNames]
|
|
173
|
+
: entitledStoredNames.length > 0
|
|
174
|
+
? entitledStoredNames
|
|
175
|
+
: [...tierFallbackNames];
|
|
176
|
+
const source = partialProvided
|
|
177
|
+
? "validated-partial"
|
|
178
|
+
: entitledStoredNames.length > 0
|
|
179
|
+
? "committed"
|
|
180
|
+
: "tier-fallback";
|
|
168
181
|
return {
|
|
169
182
|
names,
|
|
170
183
|
tier,
|
|
@@ -216,10 +229,10 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
216
229
|
`> - 🧠 **Context depth:** ${depth}\n` +
|
|
217
230
|
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · native materialization incomplete${conflictSuffix}`;
|
|
218
231
|
}
|
|
219
|
-
if (snapshot.source === "
|
|
232
|
+
if (snapshot.source === "tier-fallback") {
|
|
220
233
|
return `> **Prism System Ready**\n>\n` +
|
|
221
234
|
`> - 🪪 **Subscription tier:** ${snapshot.tier}\n` +
|
|
222
|
-
`> - 🛡️ **
|
|
235
|
+
`> - 🛡️ **Fallback skill names:** ${formatBoundedSkillNames(snapshot.names, "fallback")}\n` +
|
|
223
236
|
`> - 🧠 **Context depth:** ${depth}\n` +
|
|
224
237
|
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · no committed manifest${conflictSuffix}`;
|
|
225
238
|
}
|
|
@@ -341,38 +354,46 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
341
354
|
role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
|
|
342
355
|
});
|
|
343
356
|
// ─── Fire-and-forget embedding generation ───
|
|
357
|
+
let embeddingQueued = false;
|
|
344
358
|
if (result) {
|
|
345
359
|
const embeddingText = [summary, ...(decisions || [])].join("\n");
|
|
346
360
|
const savedEntry = Array.isArray(result) ? result[0] : result;
|
|
347
361
|
const entryId = savedEntry?.id;
|
|
348
362
|
if (entryId) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
363
|
+
try {
|
|
364
|
+
const embeddingPromise = getLLMProvider().generateEmbedding(embeddingText);
|
|
365
|
+
embeddingQueued = true;
|
|
366
|
+
embeddingPromise
|
|
367
|
+
.then(async (embedding) => {
|
|
368
|
+
// Build atomic patch — float32 + TurboQuant in ONE DB update
|
|
369
|
+
const patchData = {
|
|
370
|
+
embedding: JSON.stringify(embedding),
|
|
371
|
+
};
|
|
372
|
+
// TurboQuant: compress alongside float32 (non-fatal)
|
|
373
|
+
try {
|
|
374
|
+
const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
|
|
375
|
+
const compressor = getDefaultCompressor();
|
|
376
|
+
const compressed = compressor.compress(embedding);
|
|
377
|
+
const buf = serialize(compressed);
|
|
378
|
+
patchData.embedding_compressed = buf.toString("base64");
|
|
379
|
+
patchData.embedding_format = `turbo${compressor.bits}`;
|
|
380
|
+
patchData.embedding_turbo_radius = compressed.radius;
|
|
381
|
+
debugLog(`[session_save_ledger] TurboQuant compressed: ${buf.length} bytes (${(3072 / buf.length).toFixed(1)}× ratio)`);
|
|
382
|
+
}
|
|
383
|
+
catch (turboErr) {
|
|
384
|
+
console.error(`[session_save_ledger] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
|
|
385
|
+
}
|
|
386
|
+
// Single atomic DB update for all embedding data
|
|
387
|
+
await storage.patchLedger(entryId, patchData);
|
|
388
|
+
debugLog(`[session_save_ledger] Embedding saved for entry ${entryId}`);
|
|
389
|
+
})
|
|
390
|
+
.catch((err) => {
|
|
391
|
+
console.error(`[session_save_ledger] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
console.error(`[session_save_ledger] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
396
|
+
}
|
|
376
397
|
}
|
|
377
398
|
}
|
|
378
399
|
// ─── v6.0 Phase 3: Fire-and-forget auto-linking ────────────
|
|
@@ -439,7 +460,9 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
439
460
|
(todos?.length ? `TODOs: ${todos.length} items\n` : "") +
|
|
440
461
|
(files_changed?.length ? `Files changed: ${files_changed.length}\n` : "") +
|
|
441
462
|
(decisions?.length ? `Decisions: ${decisions.length}\n` : "") +
|
|
442
|
-
|
|
463
|
+
(embeddingQueued
|
|
464
|
+
? `📊 Embedding generation queued for semantic search.`
|
|
465
|
+
: `📊 Primary history saved; optional semantic indexing was not queued.`) +
|
|
443
466
|
metricsBlock +
|
|
444
467
|
resolverNote,
|
|
445
468
|
}],
|
|
@@ -599,9 +622,11 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
599
622
|
}
|
|
600
623
|
// ─── Success: handoff created or updated ───
|
|
601
624
|
const newVersion = data.version;
|
|
602
|
-
// ─── TIME MACHINE: Auto-snapshot for time travel
|
|
625
|
+
// ─── TIME MACHINE: Auto-snapshot for time travel ───
|
|
603
626
|
// Every successful save creates a snapshot so the user can revert later.
|
|
604
|
-
//
|
|
627
|
+
// Await the attempt so short-lived CLI/MCP transports cannot exit before the
|
|
628
|
+
// snapshot reaches storage. Failure stays non-fatal because the primary
|
|
629
|
+
// handoff has already been durably saved.
|
|
605
630
|
if (data.status === "created" || data.status === "updated") {
|
|
606
631
|
const snapshotEntry = {
|
|
607
632
|
project,
|
|
@@ -614,9 +639,15 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
614
639
|
active_branch: active_branch ?? null,
|
|
615
640
|
version: newVersion,
|
|
616
641
|
};
|
|
617
|
-
|
|
642
|
+
try {
|
|
643
|
+
await storage.saveHistorySnapshot(snapshotEntry);
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
647
|
+
}
|
|
618
648
|
}
|
|
619
649
|
// ─── Fire-and-forget embedding generation (enables semantic search on handoffs) ───
|
|
650
|
+
let embeddingQueued = false;
|
|
620
651
|
if (data.status === "created" || data.status === "updated") {
|
|
621
652
|
const embeddingText = [
|
|
622
653
|
last_summary || "",
|
|
@@ -624,30 +655,37 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
624
655
|
...(open_todos || []),
|
|
625
656
|
].filter(Boolean).join("\n");
|
|
626
657
|
if (embeddingText.trim()) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
658
|
+
try {
|
|
659
|
+
const embeddingPromise = getLLMProvider().generateEmbedding(embeddingText);
|
|
660
|
+
embeddingQueued = true;
|
|
661
|
+
embeddingPromise
|
|
662
|
+
.then(async (embedding) => {
|
|
663
|
+
const patchData = {
|
|
664
|
+
embedding: JSON.stringify(embedding),
|
|
665
|
+
};
|
|
666
|
+
try {
|
|
667
|
+
const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
|
|
668
|
+
const compressor = getDefaultCompressor();
|
|
669
|
+
const compressed = compressor.compress(embedding);
|
|
670
|
+
const buf = serialize(compressed);
|
|
671
|
+
patchData.embedding_compressed = buf.toString("base64");
|
|
672
|
+
patchData.embedding_format = `turbo${compressor.bits}`;
|
|
673
|
+
patchData.embedding_turbo_radius = compressed.radius;
|
|
674
|
+
debugLog(`[session_save_handoff] TurboQuant compressed: ${buf.length} bytes`);
|
|
675
|
+
}
|
|
676
|
+
catch (turboErr) {
|
|
677
|
+
console.error(`[session_save_handoff] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
|
|
678
|
+
}
|
|
679
|
+
await storage.patchHandoff(project, PRISM_USER_ID, patchData);
|
|
680
|
+
debugLog(`[session_save_handoff] Embedding saved for project "${project}"`);
|
|
681
|
+
})
|
|
682
|
+
.catch((err) => {
|
|
683
|
+
console.error(`[session_save_handoff] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
console.error(`[session_save_handoff] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
688
|
+
}
|
|
651
689
|
}
|
|
652
690
|
}
|
|
653
691
|
// ─── Trigger resource subscription notification ───
|
|
@@ -776,7 +814,9 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
776
814
|
(last_summary ? `Last summary: ${last_summary}\n` : "") +
|
|
777
815
|
(open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
|
|
778
816
|
(active_branch ? `Active branch: ${active_branch}\n` : "") +
|
|
779
|
-
|
|
817
|
+
(embeddingQueued
|
|
818
|
+
? `📊 Embedding generation queued for semantic search.\n`
|
|
819
|
+
: `📊 Primary history saved; optional semantic indexing was not queued.\n`) +
|
|
780
820
|
metricsBlock +
|
|
781
821
|
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
782
822
|
`to maintain concurrency control.`);
|
|
@@ -1961,16 +2001,21 @@ export async function sessionSaveExperienceHandler(args) {
|
|
|
1961
2001
|
const savedEntry = Array.isArray(result) ? result[0] : result;
|
|
1962
2002
|
const entryId = savedEntry?.id;
|
|
1963
2003
|
if (entryId) {
|
|
1964
|
-
|
|
1965
|
-
.
|
|
1966
|
-
|
|
1967
|
-
|
|
2004
|
+
try {
|
|
2005
|
+
getLLMProvider().generateEmbedding(embeddingText)
|
|
2006
|
+
.then(async (embedding) => {
|
|
2007
|
+
await storage.patchLedger(entryId, {
|
|
2008
|
+
embedding: JSON.stringify(embedding),
|
|
2009
|
+
});
|
|
2010
|
+
debugLog(`[session_save_experience] Embedding saved for entry ${entryId}`);
|
|
2011
|
+
})
|
|
2012
|
+
.catch((err) => {
|
|
2013
|
+
console.error(`[session_save_experience] Embedding failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1968
2014
|
});
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
.
|
|
1972
|
-
|
|
1973
|
-
});
|
|
2015
|
+
}
|
|
2016
|
+
catch (err) {
|
|
2017
|
+
console.error(`[session_save_experience] Embedding provider initialization failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2018
|
+
}
|
|
1974
2019
|
}
|
|
1975
2020
|
}
|
|
1976
2021
|
return {
|
|
@@ -1839,7 +1839,8 @@ export const VERIFY_BEHAVIOR_TOOL = {
|
|
|
1839
1839
|
description: "Call BEFORE editing behavioral source files (API routes, ordering logic, billing, auth, migrations). " +
|
|
1840
1840
|
"Returns a domain-specific scenario you must answer to demonstrate understanding of the end-user impact. " +
|
|
1841
1841
|
"Example: editing a KDS route returns 'A cook has a 3-item ticket. One item is voided. What should the cook see?' " +
|
|
1842
|
-
"Answer the scenario concretely before proceeding with the edit."
|
|
1842
|
+
"Answer the scenario concretely before proceeding with the edit. If the MCP transport is unavailable, use the " +
|
|
1843
|
+
"packaged `prism verify-behavior` CLI fallback; never fabricate a replacement scenario.",
|
|
1843
1844
|
inputSchema: {
|
|
1844
1845
|
type: "object",
|
|
1845
1846
|
properties: {
|
|
@@ -22,11 +22,10 @@ export const REQUIRED_PROTECTED_SKILL_NAMES = [
|
|
|
22
22
|
'pre-commit-protocol',
|
|
23
23
|
'pre-push-audit',
|
|
24
24
|
'implementation-integrity-audit',
|
|
25
|
-
'current-staging-acceptance',
|
|
26
25
|
'local-inference-first',
|
|
27
26
|
];
|
|
28
27
|
/**
|
|
29
|
-
* Native skills that
|
|
28
|
+
* Native skills that paid subscription tiers receive through `prism connect`.
|
|
30
29
|
*
|
|
31
30
|
* `prism-startup` is deliberately not part of OFFLINE_FALLBACK: it tells the
|
|
32
31
|
* host to call session_load_context, so injecting it back into that tool's
|
|
@@ -37,6 +36,8 @@ export const REQUIRED_NATIVE_SKILL_NAMES = [
|
|
|
37
36
|
...REQUIRED_PROTECTED_SKILL_NAMES,
|
|
38
37
|
'prism-startup',
|
|
39
38
|
];
|
|
39
|
+
/** Public hook-free bootstrap package available without a paid entitlement. */
|
|
40
|
+
export const FREE_NATIVE_SKILL_NAMES = ['prism-startup'];
|
|
40
41
|
export const OFFLINE_FALLBACK = {
|
|
41
42
|
version: 1,
|
|
42
43
|
universal: REQUIRED_PROTECTED_SKILL_NAMES.map((name, priority) => ({ name, priority, protected: true })),
|
|
@@ -41,7 +41,9 @@ const UNFENCED_PYTHON_START_RE = /^\s*(?:from\s+\S+\s+import|import\s+\S+|(?:asy
|
|
|
41
41
|
const TRAILING_PROSE_LINE_RE = /^(?!(?:async\s+def|def|class|from|import|return|raise|yield|assert|del|global|nonlocal|if|elif|else|for|while|try|except|finally|with|match|case)\b)(?![A-Za-z_]\w*\s*=)[A-Za-z][A-Za-z'’-]*:?(?:\s+(?:[A-Za-z][A-Za-z'’+/-]*|`[^`\r\n]+`|https?:\/\/\S+|\d[\w.,%()+\-]*|[+*=<>-])){2,}[.!?]$/;
|
|
42
42
|
const TRAILING_URL_LINE_RE = /^https?:\/\/\S+$/i;
|
|
43
43
|
const PYTHON_INVALID_DEF_RE = /(?:^|\n)\s*(?:async\s+)?def\s+[A-Za-z_]\w*\s*:/m;
|
|
44
|
-
const
|
|
44
|
+
const PYTHON_READY_SENTINEL = "PRISM_PYTHON_READY";
|
|
45
|
+
const PYTHON_AST_SCRIPT = `import ast,sys; print('${PYTHON_READY_SENTINEL}', flush=True); ` +
|
|
46
|
+
"tree=ast.parse(sys.stdin.read()); compile(tree, '<prism-coding-gate>', 'exec')";
|
|
45
47
|
const PYTHON_COMMANDS = ["python3", "python"];
|
|
46
48
|
const PYTHON_CHILDREN_KEYS_UNPACK_RE = /\bfor\s+[A-Za-z_]\w*\s*,\s*child(?:_node)?\s+in\s+(?:sorted\(\s*)?[A-Za-z_][\w.]*\.children\.keys\(\)\s*\)?\s*:/;
|
|
47
49
|
function extractUnfencedPythonCode(output) {
|
|
@@ -279,6 +281,12 @@ function pythonSyntaxFailure(code) {
|
|
|
279
281
|
if (parsed.error && parsed.error.code === "ENOENT") {
|
|
280
282
|
continue;
|
|
281
283
|
}
|
|
284
|
+
const parserStarted = parsed.stdout
|
|
285
|
+
?.split(/\r?\n/)
|
|
286
|
+
.includes(PYTHON_READY_SENTINEL) === true;
|
|
287
|
+
if (!parserStarted) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
282
290
|
return parsed.status === 0 ? undefined : "python_syntax_error";
|
|
283
291
|
}
|
|
284
292
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.2.
|
|
3
|
+
"version": "20.2.9",
|
|
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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prebuild": "npm run clean",
|
|
23
23
|
"build": "tsc && npm run chmod-bins",
|
|
24
24
|
"chmod-bins": "node -e \"['dist/cli.js','dist/server.js','dist/utils/universalImporter.js'].forEach(f => { try { require('fs').chmodSync(f, 0o755); } catch (e) { console.warn('chmod skipped', f, e.message); } })\"",
|
|
25
|
-
"prepublishOnly": "npm run build",
|
|
25
|
+
"prepublishOnly": "node scripts/check-publish-clean.mjs && npm run build",
|
|
26
26
|
"lint:dashboard": "node scripts/lint-dashboard-es5.cjs",
|
|
27
27
|
"start": "node dist/server.js",
|
|
28
28
|
"test": "vitest run",
|