prism-mcp-server 20.0.5 → 20.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agentTools.js +453 -0
- package/dist/agent/mcpBridge.js +234 -0
- package/dist/agent/platformUtils.js +470 -0
- package/dist/agent/terminalUI.js +198 -0
- package/dist/auth.js +218 -0
- package/dist/darkfactory/cloudDelegate.js +173 -0
- package/dist/plugins/pluginManager.js +199 -0
- package/dist/prism-cloud.js +110 -0
- package/dist/scm/ciPipeline.js +220 -0
- package/dist/server.js +1 -1
- package/dist/storage/inferMetricsLedger.js +159 -0
- package/dist/sync/encryptedSync.js +172 -0
- package/dist/sync/synaluxProxy.js +177 -0
- package/dist/tools/__tests__/layer1Integration.test.js +34 -0
- package/dist/tools/adaptiveDefinitions.js +148 -0
- package/dist/tools/interactiveDefinitions.js +87 -0
- package/dist/tools/interactiveHandlers.js +164 -0
- package/dist/tools/ledgerHandlers.js +40 -13
- package/dist/tools/prismInferHandler.js +48 -6
- package/dist/tools/projects.js +214 -0
- package/dist/tools/sessionMemoryDefinitions.js +12 -5
- package/dist/tools/skillRouting.js +57 -8
- package/dist/utils/changelogGenerator.js +158 -0
- package/dist/utils/fallbackClient.js +52 -0
- package/dist/utils/groundingVerifier.js +3 -3
- package/dist/utils/inferenceMetrics.js +38 -1
- package/dist/utils/memoryAttestation.js +163 -0
- package/dist/utils/rbac.js +321 -0
- package/dist/utils/skillBudget.js +58 -0
- package/dist/utils/tavilyApi.js +70 -0
- package/dist/vm/quotaEnforcer.js +192 -0
- package/package.json +1 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v12.5: Synalux Thin-Client Proxy
|
|
3
|
+
*
|
|
4
|
+
* HTTP relay to Synalux Cloud API for tier-gated features.
|
|
5
|
+
* Routes requests through the Synalux Cloud Gateway, enforcing
|
|
6
|
+
* subscription tier limits and authentication.
|
|
7
|
+
*/
|
|
8
|
+
import { debugLog } from "../utils/logger.js";
|
|
9
|
+
// ─── Tier Limits ─────────────────────────────────────────────
|
|
10
|
+
const TIER_LIMITS = {
|
|
11
|
+
free: {
|
|
12
|
+
maxRequestsPerMinute: 10,
|
|
13
|
+
maxMemoryMb: 50,
|
|
14
|
+
maxProjects: 3,
|
|
15
|
+
cloudFeatures: ["search", "load_context"],
|
|
16
|
+
},
|
|
17
|
+
standard: {
|
|
18
|
+
maxRequestsPerMinute: 60,
|
|
19
|
+
maxMemoryMb: 500,
|
|
20
|
+
maxProjects: 20,
|
|
21
|
+
cloudFeatures: ["search", "load_context", "save_ledger", "save_handoff", "analytics"],
|
|
22
|
+
},
|
|
23
|
+
advanced: {
|
|
24
|
+
maxRequestsPerMinute: 300,
|
|
25
|
+
maxMemoryMb: 5000,
|
|
26
|
+
maxProjects: -1, // unlimited
|
|
27
|
+
cloudFeatures: ["search", "load_context", "save_ledger", "save_handoff", "analytics", "backup", "sync", "plugins"],
|
|
28
|
+
},
|
|
29
|
+
enterprise: {
|
|
30
|
+
maxRequestsPerMinute: -1, // unlimited
|
|
31
|
+
maxMemoryMb: -1,
|
|
32
|
+
maxProjects: -1,
|
|
33
|
+
cloudFeatures: ["*"], // all features
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// ─── Rate Limiter ────────────────────────────────────────────
|
|
37
|
+
const requestLog = [];
|
|
38
|
+
function checkRateLimit(tier) {
|
|
39
|
+
const limit = TIER_LIMITS[tier].maxRequestsPerMinute;
|
|
40
|
+
if (limit === -1)
|
|
41
|
+
return true;
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const windowStart = now - 60_000;
|
|
44
|
+
// Clean old entries
|
|
45
|
+
while (requestLog.length > 0 && requestLog[0] < windowStart) {
|
|
46
|
+
requestLog.shift();
|
|
47
|
+
}
|
|
48
|
+
if (requestLog.length >= limit)
|
|
49
|
+
return false;
|
|
50
|
+
requestLog.push(now);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
// ─── Proxy State ─────────────────────────────────────────────
|
|
54
|
+
let config = {
|
|
55
|
+
baseUrl: "https://cloud.synalux.ai/api/v1/prism",
|
|
56
|
+
apiKey: "",
|
|
57
|
+
tier: "free",
|
|
58
|
+
timeout: 30_000,
|
|
59
|
+
retries: 2,
|
|
60
|
+
enabled: false,
|
|
61
|
+
};
|
|
62
|
+
export function configureProxy(updates) {
|
|
63
|
+
config = { ...config, ...updates };
|
|
64
|
+
debugLog(`Proxy: Configured for ${config.tier} tier → ${config.baseUrl}`);
|
|
65
|
+
}
|
|
66
|
+
export function getProxyConfig() {
|
|
67
|
+
return { ...config, apiKey: config.apiKey ? "***" : "" };
|
|
68
|
+
}
|
|
69
|
+
export function getTierLimits(tier) {
|
|
70
|
+
return TIER_LIMITS[tier || config.tier];
|
|
71
|
+
}
|
|
72
|
+
// ─── Feature Gating ──────────────────────────────────────────
|
|
73
|
+
/**
|
|
74
|
+
* Check if a cloud feature is available for the current tier.
|
|
75
|
+
*/
|
|
76
|
+
export function isFeatureAvailable(feature) {
|
|
77
|
+
const limits = TIER_LIMITS[config.tier];
|
|
78
|
+
return limits.cloudFeatures.includes("*") || limits.cloudFeatures.includes(feature);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* List all features available for the current tier.
|
|
82
|
+
*/
|
|
83
|
+
export function listAvailableFeatures() {
|
|
84
|
+
return [...TIER_LIMITS[config.tier].cloudFeatures];
|
|
85
|
+
}
|
|
86
|
+
// ─── HTTP Proxy ──────────────────────────────────────────────
|
|
87
|
+
/**
|
|
88
|
+
* Send a request through the Synalux Cloud proxy.
|
|
89
|
+
*/
|
|
90
|
+
export async function proxyRequest(req) {
|
|
91
|
+
if (!config.enabled) {
|
|
92
|
+
return {
|
|
93
|
+
status: 503,
|
|
94
|
+
data: { error: "Synalux Cloud proxy is not enabled. Set PRISM_CLOUD_PROXY=true." },
|
|
95
|
+
headers: {},
|
|
96
|
+
latencyMs: 0,
|
|
97
|
+
cached: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!config.apiKey) {
|
|
101
|
+
return {
|
|
102
|
+
status: 401,
|
|
103
|
+
data: { error: "No API key configured. Set PRISM_SYNALUX_API_KEY." },
|
|
104
|
+
headers: {},
|
|
105
|
+
latencyMs: 0,
|
|
106
|
+
cached: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!checkRateLimit(config.tier)) {
|
|
110
|
+
return {
|
|
111
|
+
status: 429,
|
|
112
|
+
data: { error: `Rate limit exceeded for ${config.tier} tier (${TIER_LIMITS[config.tier].maxRequestsPerMinute}/min)` },
|
|
113
|
+
headers: {},
|
|
114
|
+
latencyMs: 0,
|
|
115
|
+
cached: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
const url = `${config.baseUrl}${req.path}`;
|
|
120
|
+
let lastError;
|
|
121
|
+
for (let attempt = 0; attempt <= config.retries; attempt++) {
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(url, {
|
|
124
|
+
method: req.method,
|
|
125
|
+
headers: {
|
|
126
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
"X-Prism-Tier": config.tier,
|
|
129
|
+
"X-Prism-Version": "12.5.0",
|
|
130
|
+
...(req.headers || {}),
|
|
131
|
+
},
|
|
132
|
+
body: req.body ? JSON.stringify(req.body) : undefined,
|
|
133
|
+
signal: AbortSignal.timeout(config.timeout),
|
|
134
|
+
});
|
|
135
|
+
const data = await response.json().catch(() => null);
|
|
136
|
+
const latencyMs = Date.now() - start;
|
|
137
|
+
debugLog(`Proxy: ${req.method} ${req.path} → ${response.status} (${latencyMs}ms)`);
|
|
138
|
+
const responseHeaders = {};
|
|
139
|
+
response.headers.forEach((value, key) => { responseHeaders[key] = value; });
|
|
140
|
+
return {
|
|
141
|
+
status: response.status,
|
|
142
|
+
data,
|
|
143
|
+
headers: responseHeaders,
|
|
144
|
+
latencyMs,
|
|
145
|
+
cached: response.headers.get("x-cache") === "HIT",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
lastError = err;
|
|
150
|
+
if (attempt < config.retries) {
|
|
151
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
152
|
+
await new Promise(r => setTimeout(r, delay));
|
|
153
|
+
debugLog(`Proxy: Retry ${attempt + 1}/${config.retries} after error: ${err}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
status: 502,
|
|
159
|
+
data: { error: `Proxy error after ${config.retries + 1} attempts: ${lastError}` },
|
|
160
|
+
headers: {},
|
|
161
|
+
latencyMs: Date.now() - start,
|
|
162
|
+
cached: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Check cloud connectivity and tier status.
|
|
167
|
+
*/
|
|
168
|
+
export async function healthCheck() {
|
|
169
|
+
const result = await proxyRequest({ method: "GET", path: "/health" });
|
|
170
|
+
return {
|
|
171
|
+
connected: result.status === 200,
|
|
172
|
+
tier: config.tier,
|
|
173
|
+
latencyMs: result.latencyMs,
|
|
174
|
+
features: listAvailableFeatures(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
debugLog("v12.5: Synalux proxy loaded");
|
|
@@ -462,3 +462,37 @@ describe.skipIf(!LIVE)("Layer 1 live model — adversarial fixtures (PRISM_LIVE_
|
|
|
462
462
|
}
|
|
463
463
|
});
|
|
464
464
|
});
|
|
465
|
+
// ─── Reserved-content escalation routing (plan v2 §5.1) ─────────────────────
|
|
466
|
+
// The escalation target for reserved content must be at least as capable as
|
|
467
|
+
// the local model that refused it: Claude-or-refuse, never a small local tier
|
|
468
|
+
// or OpenRouter. These pin both the wire contract and the client-side defense.
|
|
469
|
+
import { ReservedRefusalError } from "../prismInferHandler.js";
|
|
470
|
+
describe("reserved-content escalation (§5.1)", () => {
|
|
471
|
+
const RESERVED_PROMPT = "draft a physical restraint procedure for the client";
|
|
472
|
+
it("passes reserved:true to callCloud when Layer 1 refuses", async () => {
|
|
473
|
+
const callCloud = vi.fn().mockResolvedValue({ ok: true, output: "safe cloud answer", backend: "claude-reserved" });
|
|
474
|
+
const callLayer1Mock = vi.fn().mockResolvedValue("OBVIOUS_RESERVED");
|
|
475
|
+
const result = await runInfer({ prompt: RESERVED_PROMPT, mode: "code", cloud_fallback: true, max_tokens: 512 }, makeBaseDeps({ callCloud, callLayer1: callLayer1Mock }));
|
|
476
|
+
expect(callCloud).toHaveBeenCalledWith(expect.any(String), expect.any(Number), expect.any(Number), expect.objectContaining({ reserved: true }));
|
|
477
|
+
expect(result.used_cloud).toBe(true);
|
|
478
|
+
expect(result.backend).toBe("claude-reserved");
|
|
479
|
+
});
|
|
480
|
+
it("REFUSES when the portal serves a reserved turn from a weak backend (defense in depth)", async () => {
|
|
481
|
+
const callCloud = vi.fn().mockResolvedValue({ ok: true, output: "answer from tiny model", backend: "ollama-2b" });
|
|
482
|
+
const callLayer1Mock = vi.fn().mockResolvedValue("OBVIOUS_RESERVED");
|
|
483
|
+
await expect(runInfer({ prompt: RESERVED_PROMPT, mode: "code", cloud_fallback: true, max_tokens: 512 }, makeBaseDeps({ callCloud, callLayer1: callLayer1Mock }))).rejects.toMatchObject({ name: "ReservedRefusalError", refusal_reason: "layer1_reserved" });
|
|
484
|
+
});
|
|
485
|
+
it("REFUSES on openrouter backends the same way", async () => {
|
|
486
|
+
const callCloud = vi.fn().mockResolvedValue({ ok: true, output: "qwen answer", backend: "openrouter-9b" });
|
|
487
|
+
const callLayer1Mock = vi.fn().mockResolvedValue("UNCERTAIN");
|
|
488
|
+
await expect(runInfer({ prompt: RESERVED_PROMPT, mode: "chat", cloud_fallback: true, max_tokens: 512 }, makeBaseDeps({ callCloud, callLayer1: callLayer1Mock }))).rejects.toBeInstanceOf(ReservedRefusalError);
|
|
489
|
+
});
|
|
490
|
+
it("refusal without cloud carries the typed reason (free-tier path)", async () => {
|
|
491
|
+
const callLocal = vi.fn();
|
|
492
|
+
const callLayer1Mock = vi.fn().mockResolvedValue("OBVIOUS_RESERVED");
|
|
493
|
+
const deps = makeBaseDeps({ callLocal, callLayer1: callLayer1Mock });
|
|
494
|
+
deps.entitlements.features.cloud_fallback = false;
|
|
495
|
+
await expect(runInfer({ prompt: RESERVED_PROMPT, mode: "code", cloud_fallback: true, max_tokens: 512 }, deps)).rejects.toMatchObject({ refusal_reason: "layer1_reserved" });
|
|
496
|
+
expect(callLocal).not.toHaveBeenCalled();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export const ADAPTIVE_GET_PROFILE_TOOL = {
|
|
2
|
+
name: "adaptive_get_profile",
|
|
3
|
+
description: "Get the user's current adaptive profile and a compact signals snapshot. " +
|
|
4
|
+
"Use this when you need the user's dominant mood, motor rhythm, noise " +
|
|
5
|
+
"environment, or vocabulary preferences — typically before generating a " +
|
|
6
|
+
"response that should match their state. Returns the full profile plus a " +
|
|
7
|
+
"small `signals` block that is safe to embed in a prompt.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
user_id: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Optional. User identifier. If omitted, the server uses the authenticated user from context.",
|
|
14
|
+
},
|
|
15
|
+
include_history: {
|
|
16
|
+
type: "boolean",
|
|
17
|
+
description: "If true, include the toneHistory and full timeOfDayPatterns. Defaults to false (signals + summary only) to keep payload small.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export const ADAPTIVE_SET_PROFILE_TOOL = {
|
|
23
|
+
name: "adaptive_set_profile",
|
|
24
|
+
description: "Replace the user's adaptive profile in full. Intended for caregivers " +
|
|
25
|
+
"syncing across devices, restoring from backup, or admin migration. " +
|
|
26
|
+
"Schema must match version 2 of the AdaptiveProfile (see " +
|
|
27
|
+
"src/shared/adaptiveEngine.ts).",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
user_id: {
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Optional. User identifier (defaults to authenticated user).",
|
|
34
|
+
},
|
|
35
|
+
profile: {
|
|
36
|
+
type: "object",
|
|
37
|
+
description: "Full AdaptiveProfile object. Must include version: 2.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
required: ["profile"],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export const ADAPTIVE_RECORD_EVENT_TOOL = {
|
|
44
|
+
name: "adaptive_record_event",
|
|
45
|
+
description: "Record a single behavioral event into the adaptive profile. " +
|
|
46
|
+
"Use this from any surface that observes user behavior (voice agents, " +
|
|
47
|
+
"AAC dwell triggers, message logs). Events are written incrementally — " +
|
|
48
|
+
"no need to fetch+modify+save the whole profile. " +
|
|
49
|
+
"Event types: " +
|
|
50
|
+
"tone (text → AdaptiveTone, also records to toneHistory), " +
|
|
51
|
+
"dwell (dwellMs sample for motor rhythm), " +
|
|
52
|
+
"move_speed (px/sec sample for cursor smoothing), " +
|
|
53
|
+
"noise (rmsDb sample for environment), " +
|
|
54
|
+
"message (text + optional categoryId for vocab + frequency tracking), " +
|
|
55
|
+
"mispronunciation (heard → intended; emergency words always pass through).",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
user_id: { type: "string", description: "Optional user id." },
|
|
60
|
+
event: {
|
|
61
|
+
type: "string",
|
|
62
|
+
enum: ["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"],
|
|
63
|
+
description: "Event type.",
|
|
64
|
+
},
|
|
65
|
+
text: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "For tone/message: the utterance text. For mispronunciation: the heard form.",
|
|
68
|
+
},
|
|
69
|
+
intended: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "For mispronunciation: the corrected form.",
|
|
72
|
+
},
|
|
73
|
+
value: {
|
|
74
|
+
type: "number",
|
|
75
|
+
description: "For dwell/move_speed/noise: the numeric sample.",
|
|
76
|
+
},
|
|
77
|
+
category_id: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "For message: optional category id (e.g. 'food', 'feelings').",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ["event"],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export const ADAPTIVE_DETECT_TONE_TOOL = {
|
|
86
|
+
name: "adaptive_detect_tone",
|
|
87
|
+
description: "Detect emotional tone from a piece of text WITHOUT recording it. " +
|
|
88
|
+
"Pure function — useful when you want to route a response (e.g. choose " +
|
|
89
|
+
"a TTS voice style or shape an LLM system prompt) but don't want to " +
|
|
90
|
+
"perturb the user's profile. Returns one of: " +
|
|
91
|
+
"neutral | friendly | excited | empathetic | serious. Emergency words " +
|
|
92
|
+
"(help/hurt/scared/911/etc) always map to 'serious'.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
text: { type: "string", description: "Text to analyze." },
|
|
97
|
+
},
|
|
98
|
+
required: ["text"],
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
export const ADAPTIVE_RESET_TOOL = {
|
|
102
|
+
name: "adaptive_reset",
|
|
103
|
+
description: "Wipe the user's adaptive profile. Caregiver-initiated reset only — " +
|
|
104
|
+
"resets all learned dwell, motor speed, vocabulary, mispronunciation " +
|
|
105
|
+
"corrections, tone history, and noise calibration. Returns the fresh " +
|
|
106
|
+
"default profile.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
user_id: { type: "string", description: "Optional user id." },
|
|
111
|
+
confirm: {
|
|
112
|
+
type: "boolean",
|
|
113
|
+
description: "Must be true. Defends against accidental reset.",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ["confirm"],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
// ─── Type guards (mirror the patterns used elsewhere in this repo) ───
|
|
120
|
+
export function isAdaptiveGetProfileArgs(args) {
|
|
121
|
+
return typeof args === "object" && args !== null;
|
|
122
|
+
}
|
|
123
|
+
export function isAdaptiveSetProfileArgs(args) {
|
|
124
|
+
return (typeof args === "object" &&
|
|
125
|
+
args !== null &&
|
|
126
|
+
"profile" in args &&
|
|
127
|
+
typeof args.profile === "object" &&
|
|
128
|
+
args.profile !== null);
|
|
129
|
+
}
|
|
130
|
+
export function isAdaptiveRecordEventArgs(args) {
|
|
131
|
+
if (typeof args !== "object" || args === null)
|
|
132
|
+
return false;
|
|
133
|
+
const a = args;
|
|
134
|
+
return (typeof a.event === "string" &&
|
|
135
|
+
["tone", "dwell", "move_speed", "noise", "message", "mispronunciation"].includes(a.event));
|
|
136
|
+
}
|
|
137
|
+
export function isAdaptiveDetectToneArgs(args) {
|
|
138
|
+
return (typeof args === "object" &&
|
|
139
|
+
args !== null &&
|
|
140
|
+
"text" in args &&
|
|
141
|
+
typeof args.text === "string");
|
|
142
|
+
}
|
|
143
|
+
export function isAdaptiveResetArgs(args) {
|
|
144
|
+
return (typeof args === "object" &&
|
|
145
|
+
args !== null &&
|
|
146
|
+
"confirm" in args &&
|
|
147
|
+
args.confirm === true);
|
|
148
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ─── session_instant_write ─────────────────────────────────
|
|
2
|
+
export const SESSION_INSTANT_WRITE_TOOL = {
|
|
3
|
+
name: "session_instant_write",
|
|
4
|
+
description: "Instantly overwrite a file with new code to bypass manual IDE diffs and maintain flow state.\n\n" +
|
|
5
|
+
"**How it works:**\n" +
|
|
6
|
+
"1. Copies the current state of the target file into a hidden snapshot in `.prism/snapshots`.\n" +
|
|
7
|
+
"2. Immediately replaces the target file contents with the desired code.\n" +
|
|
8
|
+
"3. Returns a `snapshot_id` which can be used to instantly revert the change if it breaks compilation or UI.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
file_path: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Absolute file path to write to.",
|
|
15
|
+
},
|
|
16
|
+
content: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "The complete raw code string to write to the file. Exclude backticks unless part of code.",
|
|
19
|
+
},
|
|
20
|
+
project: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Project identifier (for scoping snapshots if needed).",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["file_path", "content", "project"],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
// ─── session_snapshot_revert ─────────────────────────────────
|
|
29
|
+
export const SESSION_SNAPSHOT_REVERT_TOOL = {
|
|
30
|
+
name: "session_snapshot_revert",
|
|
31
|
+
description: "Revert a file change that was executed via `session_instant_write`.\n\n" +
|
|
32
|
+
"Use this tool when the developer clicks 'Revert' or if the background terminal detects a fatal compiler panic immediately after writing.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
snapshot_id: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "The unique snapshot ID returned by the previous write operation.",
|
|
39
|
+
},
|
|
40
|
+
file_path: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The path of the file to revert.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["snapshot_id", "file_path"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
// ─── session_spawn_monitored_process ──────────────────────────
|
|
49
|
+
export const SESSION_SPAWN_MONITORED_PROCESS_TOOL = {
|
|
50
|
+
name: "session_spawn_monitored_process",
|
|
51
|
+
description: "Launch a long-running process (like `npm run dev`) and monitor its output for compilation/critical errors.\n\n" +
|
|
52
|
+
"**How it works:**\n" +
|
|
53
|
+
"1. Spawns the process in the background.\n" +
|
|
54
|
+
"2. Continuously reads `stdout` and `stderr` looking for regex patterns (like Vite errors, TS compiler errors).\n" +
|
|
55
|
+
"3. If an error is detected, the MCP server updates the `telemetry://{project}/errors` resource, which triggers a notification to the IDE Client to auto-dispatch a self-correcting prompt.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
project: { type: "string" },
|
|
60
|
+
command: { type: "string" },
|
|
61
|
+
cwd: { type: "string" },
|
|
62
|
+
},
|
|
63
|
+
required: ["project", "command", "cwd"],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export function isInstantWriteArgs(args) {
|
|
67
|
+
if (typeof args !== "object" || args === null)
|
|
68
|
+
return false;
|
|
69
|
+
const a = args;
|
|
70
|
+
return (typeof a.file_path === "string" &&
|
|
71
|
+
typeof a.content === "string" &&
|
|
72
|
+
typeof a.project === "string");
|
|
73
|
+
}
|
|
74
|
+
export function isSnapshotRevertArgs(args) {
|
|
75
|
+
if (typeof args !== "object" || args === null)
|
|
76
|
+
return false;
|
|
77
|
+
const a = args;
|
|
78
|
+
return typeof a.snapshot_id === "string" && typeof a.file_path === "string";
|
|
79
|
+
}
|
|
80
|
+
export function isSpawnMonitoredProcessArgs(args) {
|
|
81
|
+
if (typeof args !== "object" || args === null)
|
|
82
|
+
return false;
|
|
83
|
+
const a = args;
|
|
84
|
+
return (typeof a.project === "string" &&
|
|
85
|
+
typeof a.command === "string" &&
|
|
86
|
+
typeof a.cwd === "string");
|
|
87
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { isInstantWriteArgs, isSnapshotRevertArgs, isSpawnMonitoredProcessArgs } from "./interactiveDefinitions.js";
|
|
6
|
+
// Minimal global mock store for caught telemetry errors (usually connected to MCP Resource Updates)
|
|
7
|
+
export const telemetryErrors = {};
|
|
8
|
+
// Helper to determine snapshot directory per target file.
|
|
9
|
+
function getSnapshotDir(filePath) {
|
|
10
|
+
const dir = path.dirname(filePath);
|
|
11
|
+
const snapDir = path.join(dir, ".prism", "snapshots");
|
|
12
|
+
if (!fs.existsSync(snapDir)) {
|
|
13
|
+
fs.mkdirSync(snapDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
return snapDir;
|
|
16
|
+
}
|
|
17
|
+
export async function sessionInstantWriteHandler(args) {
|
|
18
|
+
if (!isInstantWriteArgs(args)) {
|
|
19
|
+
throw new Error("Invalid arguments: expected file_path, content, and project");
|
|
20
|
+
}
|
|
21
|
+
const { file_path, content, project } = args;
|
|
22
|
+
try {
|
|
23
|
+
// 1. Snapshot the current file state if it exists.
|
|
24
|
+
let snapshotId = "no_snapshot";
|
|
25
|
+
if (fs.existsSync(file_path)) {
|
|
26
|
+
snapshotId = crypto.randomUUID();
|
|
27
|
+
const snapDir = getSnapshotDir(file_path);
|
|
28
|
+
const snapshotPath = path.join(snapDir, `${snapshotId}.snap`);
|
|
29
|
+
fs.copyFileSync(file_path, snapshotPath);
|
|
30
|
+
}
|
|
31
|
+
// 2. Write the new content to disk.
|
|
32
|
+
// Ensure parent directory exists for new files
|
|
33
|
+
const parentDir = path.dirname(file_path);
|
|
34
|
+
if (!fs.existsSync(parentDir)) {
|
|
35
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
fs.writeFileSync(file_path, content, "utf-8");
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: `SUCCESS: Instantly wrote to ${file_path}\n` +
|
|
43
|
+
`Snapshot ID: ${snapshotId}\n` +
|
|
44
|
+
`The development server should hot-reload automatically.`
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return {
|
|
51
|
+
isError: true,
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Failed to write file: ${error instanceof Error ? error.message : String(error)}`,
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function sessionSnapshotRevertHandler(args) {
|
|
62
|
+
if (!isSnapshotRevertArgs(args)) {
|
|
63
|
+
throw new Error("Invalid arguments: expected snapshot_id and file_path");
|
|
64
|
+
}
|
|
65
|
+
const { snapshot_id, file_path } = args;
|
|
66
|
+
try {
|
|
67
|
+
if (snapshot_id === "no_snapshot") {
|
|
68
|
+
// Revert means deleting the newly created file
|
|
69
|
+
if (fs.existsSync(file_path)) {
|
|
70
|
+
fs.unlinkSync(file_path);
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: `SUCCESS: Reverted the change by deleting newly created file ${file_path}.`
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return {
|
|
82
|
+
isError: true,
|
|
83
|
+
content: [{ type: "text", text: "File did not exist to delete." }]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const snapDir = getSnapshotDir(file_path);
|
|
88
|
+
const snapshotPath = path.join(snapDir, `${snapshot_id}.snap`);
|
|
89
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
90
|
+
throw new Error(`Snapshot ${snapshot_id} not found for this file.`);
|
|
91
|
+
}
|
|
92
|
+
// 1. Restore file
|
|
93
|
+
fs.copyFileSync(snapshotPath, file_path);
|
|
94
|
+
// 2. Cleanup snapshot payload
|
|
95
|
+
fs.unlinkSync(snapshotPath);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `SUCCESS: Reverted ${file_path} to previous state (Snapshot ID: ${snapshot_id}).`
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
isError: true,
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Failed to revert file: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function sessionSpawnMonitoredProcessHandler(args) {
|
|
118
|
+
if (!isSpawnMonitoredProcessArgs(args)) {
|
|
119
|
+
throw new Error("Invalid arguments: expected project, command, and cwd");
|
|
120
|
+
}
|
|
121
|
+
const { project, command, cwd } = args;
|
|
122
|
+
try {
|
|
123
|
+
const [cmd, ...cmdArgs] = command.split(" ");
|
|
124
|
+
const child = spawn(cmd, cmdArgs, { cwd, shell: true });
|
|
125
|
+
if (!telemetryErrors[project]) {
|
|
126
|
+
telemetryErrors[project] = [];
|
|
127
|
+
}
|
|
128
|
+
const interceptStream = (data) => {
|
|
129
|
+
const output = data.toString();
|
|
130
|
+
// Look for critical error signatures: NextJS ParseError, Vite Internal Error, TypeScript TS...
|
|
131
|
+
if (output.includes("SyntaxError:") ||
|
|
132
|
+
output.includes("Parsing error:") ||
|
|
133
|
+
output.match(/TS\d{4}:/) ||
|
|
134
|
+
output.includes("Internal server error") ||
|
|
135
|
+
output.includes("ERR_MODULE_NOT_FOUND")) {
|
|
136
|
+
telemetryErrors[project].push(`[${new Date().toISOString()}] TELEMETRY_CRITICAL: ${output.trim()}`);
|
|
137
|
+
// NOTE: In a full MCP implementation, this would trigger `notifyResourceUpdate`
|
|
138
|
+
// for `telemetry://${project}/errors` to alert the client immediately.
|
|
139
|
+
console.error(`[Telemetry Intercept] Caught critical compilation error for project: ${project}`);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
child.stdout.on("data", interceptStream);
|
|
143
|
+
child.stderr.on("data", interceptStream);
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: `Spawned monitored process: \`${command}\` in ${cwd}.\nAutonomic telemetry is active. Breaking changes via instant_write will auto-populate telemetry resources.`
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return {
|
|
155
|
+
isError: true,
|
|
156
|
+
content: [
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: `Failed to spawn monitored process: ${error instanceof Error ? error.message : String(error)}`,
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|