prism-mcp-server 20.18.1 → 20.18.2
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/config.js +13 -4
- package/dist/skillManifestSync.js +113 -0
- package/dist/tools/ledgerHandlers.js +23 -5
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -8,7 +8,10 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
* validates required ones, and exports them for use throughout the server.
|
|
9
9
|
*
|
|
10
10
|
* Environment variable guide:
|
|
11
|
-
* BRAVE_API_KEY — (required) API key for Brave Search Pro
|
|
11
|
+
* BRAVE_API_KEY — (required for search) API key for Brave Search Pro, and the only
|
|
12
|
+
* thing search needs in fully-local operation. A configured Synalux
|
|
13
|
+
* account supplies it portal-side instead, so exactly one of the two
|
|
14
|
+
* is needed. Get one at https://brave.com/search/api/
|
|
12
15
|
* GOOGLE_API_KEY — (optional) API key for Google AI Studio / Gemini. Enables paper analysis.
|
|
13
16
|
* BRAVE_ANSWERS_API_KEY — (optional) API key for Brave Answers (AI grounding). Enables brave_answers tool.
|
|
14
17
|
* SUPABASE_URL — (optional) Your Supabase project URL. Enables session memory tools.
|
|
@@ -18,9 +21,15 @@ import { fileURLToPath } from "node:url";
|
|
|
18
21
|
* VOYAGE_API_KEY — (optional) Voyage AI API key for embeddings.
|
|
19
22
|
* Set embedding_provider=voyage to use. https://dash.voyageai.com
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* No key is enforced at startup. Nothing here calls process.exit, so a missing
|
|
25
|
+
* key never stops the server — it starts with reduced functionality and the
|
|
26
|
+
* affected tools error when they are actually called. Missing keys log a
|
|
27
|
+
* warning, except BRAVE_API_KEY, which warns only under PRISM_DEBUG_LOGGING.
|
|
28
|
+
*
|
|
29
|
+
* Said plainly because the old wording ("the process exits immediately") was
|
|
30
|
+
* describing behaviour no key has ever had, and read as a hard startup
|
|
31
|
+
* dependency on a Brave + Firecrawl pair — see issue #188, filed on exactly
|
|
32
|
+
* that premise.
|
|
24
33
|
*/
|
|
25
34
|
// ─── Server Identity ──────────────────────────────────────────
|
|
26
35
|
function resolveServerVersion() {
|
|
@@ -256,6 +256,109 @@ async function listFiles(root, current = root) {
|
|
|
256
256
|
}
|
|
257
257
|
return files.sort();
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Artifacts nobody authors. ADMISSION RULE for these lists — an entry must be
|
|
261
|
+
* (1) machine-generated as a side effect of USING or BROWSING the directory,
|
|
262
|
+
* (2) regenerated on demand at no cost, and (3) never shippable content. Any
|
|
263
|
+
* other untracked file is a local edit and must keep conflicting. Do not widen
|
|
264
|
+
* this into "ignore files we do not recognise".
|
|
265
|
+
*/
|
|
266
|
+
const DERIVED_ARTIFACT_DIRS = new Set(["__pycache__"]);
|
|
267
|
+
/** Compared lowercased: Explorer writes both `Thumbs.db` and `thumbs.db`. */
|
|
268
|
+
const DERIVED_ARTIFACT_FILES = new Set([".ds_store", "thumbs.db", "desktop.ini"]);
|
|
269
|
+
const DERIVED_ARTIFACT_EXT = /\.py[co]$/;
|
|
270
|
+
function isDerivedArtifactFile(name) {
|
|
271
|
+
const lower = name.toLocaleLowerCase("en-US");
|
|
272
|
+
return DERIVED_ARTIFACT_FILES.has(lower) || DERIVED_ARTIFACT_EXT.test(lower);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Remove regenerable junk from a marker-owned skill directory.
|
|
276
|
+
*
|
|
277
|
+
* `isPristineMarkedSkill` compares the FULL recursive file list against the
|
|
278
|
+
* marker's key list, so ONE untracked file classifies the skill as a conflict —
|
|
279
|
+
* frozen, silently skipped by every later sync. Two ways that happens without
|
|
280
|
+
* anyone editing anything:
|
|
281
|
+
*
|
|
282
|
+
* - CPython writes `__pycache__/<mod>.cpython-XX.pyc` beside any module it
|
|
283
|
+
* IMPORTS (exec'ing a script does not). Observed 2026-09-14:
|
|
284
|
+
* dead-link-prevention froze after something imported check_links.py.
|
|
285
|
+
* - Finder drops `.DS_Store` into any directory a user OPENS, and Explorer
|
|
286
|
+
* drops `Thumbs.db`/`desktop.ini`. Reproduced 2026-09-14: browsing a
|
|
287
|
+
* managed skill folder is enough to stop it updating. Windows is a
|
|
288
|
+
* supported host, so both families belong here.
|
|
289
|
+
*
|
|
290
|
+
* PURGE, never ignore. Exempting these from the integrity walk would turn a
|
|
291
|
+
* noisy false conflict into a code-execution hole: CPython loads a cached .pyc
|
|
292
|
+
* in preference to its source whenever the pyc header's recorded source mtime
|
|
293
|
+
* and size match the .py, and both of those fields live in the attacker-written
|
|
294
|
+
* pyc. An ignored .pyc is arbitrary bytecode that executes while sync still
|
|
295
|
+
* reports the skill pristine and keeps updating it. Deleting restores integrity
|
|
296
|
+
* instead — the next import regenerates the cache from the .py the marker
|
|
297
|
+
* actually verifies, and any OTHER untracked file still conflicts as before.
|
|
298
|
+
*
|
|
299
|
+
* Touches nothing Prism does not own, nothing the manifest shipped, and never
|
|
300
|
+
* follows a symlink out of the skill root.
|
|
301
|
+
*/
|
|
302
|
+
async function purgeDerivedArtifacts(path) {
|
|
303
|
+
// Self-guarding, not caller-guarded. Three sites call this now and the
|
|
304
|
+
// symlink check is the only thing standing between a delete primitive and an
|
|
305
|
+
// arbitrary target, so it lives HERE — a fourth caller that forgets to lstat
|
|
306
|
+
// must not be able to reintroduce the escape.
|
|
307
|
+
let rootStat;
|
|
308
|
+
try {
|
|
309
|
+
rootStat = await lstat(path);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
315
|
+
return;
|
|
316
|
+
const marker = await readJson(join(path, MARKER));
|
|
317
|
+
if (!marker || marker.owner !== OWNER || !marker.files || typeof marker.files !== "object")
|
|
318
|
+
return;
|
|
319
|
+
const shipped = new Set(Object.keys(marker.files));
|
|
320
|
+
const shippedPaths = [...shipped];
|
|
321
|
+
// BEST EFFORT, always. This runs inside materializeNative's try block before
|
|
322
|
+
// anything is staged, so letting an EACCES escape would convert one skill's
|
|
323
|
+
// junk file into a `partial` sync that freezes EVERY other skill — trading a
|
|
324
|
+
// one-skill problem for the whole-root outage shape this file's header
|
|
325
|
+
// describes. A delete we cannot do just means the pristine check conflicts
|
|
326
|
+
// that one skill, exactly as it did before this function existed.
|
|
327
|
+
const discard = async (child, recursive) => {
|
|
328
|
+
try {
|
|
329
|
+
await rm(child, { recursive, force: true });
|
|
330
|
+
}
|
|
331
|
+
catch { /* leave it; it conflicts */ }
|
|
332
|
+
};
|
|
333
|
+
const walk = async (current) => {
|
|
334
|
+
let entries;
|
|
335
|
+
try {
|
|
336
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
const child = join(current, entry.name);
|
|
343
|
+
const rel = relative(path, child).split(sep).join("/");
|
|
344
|
+
// Never follow a symlink out of the skill root; listFiles still rejects it.
|
|
345
|
+
if (entry.isSymbolicLink())
|
|
346
|
+
continue;
|
|
347
|
+
if (entry.isDirectory()) {
|
|
348
|
+
const shippedUnder = shippedPaths.some((file) => file === rel || file.startsWith(`${rel}/`));
|
|
349
|
+
if (DERIVED_ARTIFACT_DIRS.has(entry.name) && !shippedUnder) {
|
|
350
|
+
await discard(child, true);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
await walk(child);
|
|
354
|
+
}
|
|
355
|
+
else if (entry.isFile() && isDerivedArtifactFile(entry.name) && !shipped.has(rel)) {
|
|
356
|
+
await discard(child, false);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
await walk(path);
|
|
361
|
+
}
|
|
259
362
|
async function isPristineMarkedSkill(path, generation) {
|
|
260
363
|
const marker = await readJson(join(path, MARKER));
|
|
261
364
|
if (!marker || marker.owner !== OWNER || (generation && marker.generation !== generation) ||
|
|
@@ -404,6 +507,12 @@ async function enforceNativeEntitlements(incomingNames, agentsSkillsDir) {
|
|
|
404
507
|
const target = join(agentsSkillsDir, name);
|
|
405
508
|
if (!(await exists(target)))
|
|
406
509
|
continue;
|
|
510
|
+
// Same reason as the materialize paths: junk nobody wrote must not make a
|
|
511
|
+
// clean skill look hand-edited. Here the misread is quieter but permanent —
|
|
512
|
+
// the skill leaves discovery either way, but a false "locally modified"
|
|
513
|
+
// parks it in quarantine forever instead of deleting it, and nothing ever
|
|
514
|
+
// collects that directory.
|
|
515
|
+
await purgeDerivedArtifacts(target);
|
|
407
516
|
if (await isPristineMarkedSkill(target)) {
|
|
408
517
|
await rm(target, { recursive: true, force: true });
|
|
409
518
|
continue;
|
|
@@ -604,6 +713,8 @@ async function materializeNative(manifest, agentsSkillsDir, hooks) {
|
|
|
604
713
|
if (!(await exists(target)))
|
|
605
714
|
continue;
|
|
606
715
|
const stat = await lstat(target);
|
|
716
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
717
|
+
await purgeDerivedArtifacts(target);
|
|
607
718
|
const pristine = stat.isDirectory() && !stat.isSymbolicLink() && await isPristineManagedSkill(target, true);
|
|
608
719
|
if (!pristine) {
|
|
609
720
|
// Preserve locally modified managed content, but quarantine it outside
|
|
@@ -659,6 +770,8 @@ async function materializeNative(manifest, agentsSkillsDir, hooks) {
|
|
|
659
770
|
}
|
|
660
771
|
const stat = await lstat(target);
|
|
661
772
|
const managedSkill = managedCandidates.has(skill.name);
|
|
773
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
774
|
+
await purgeDerivedArtifacts(target);
|
|
662
775
|
if (!stat.isDirectory() || stat.isSymbolicLink() || !(await isPristineManagedSkill(target, managedSkill))) {
|
|
663
776
|
conflicts.push(skill.name);
|
|
664
777
|
if (managedSkill)
|
|
@@ -496,13 +496,31 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
496
496
|
const conflictSuffix = snapshot.conflicts.length > 0
|
|
497
497
|
? ` · ${snapshot.conflicts.length} conflict${snapshot.conflicts.length === 1 ? "" : "s"} — see warning`
|
|
498
498
|
: "";
|
|
499
|
+
// Name the CONDITION, then hand over the procedure that reveals the cause.
|
|
500
|
+
// This used to read "local copy has no Prism ownership marker", which is one
|
|
501
|
+
// of several causes and not the common one. On 2026-09-14 dead-link-prevention
|
|
502
|
+
// tripped it with a marker that was present and digest-exact; the real cause
|
|
503
|
+
// was one extra untracked file. The sentence sent the operator to quarantine
|
|
504
|
+
// the directory and rerun `prism connect` (which rewrites host MCP config and
|
|
505
|
+
// wants every host closed) when the fix was deleting a regenerable cache dir.
|
|
506
|
+
// A wrong cause is worse than no cause: it buys a confident wrong repair.
|
|
507
|
+
//
|
|
508
|
+
// Deliberately a PROCEDURE, not an enumeration of causes. Listing all of them
|
|
509
|
+
// is both longer and weaker — a list still leaves the operator guessing, while
|
|
510
|
+
// the file-list-vs-marker diff names the offender outright. Length is not
|
|
511
|
+
// cosmetic here: this block renders in the HEAD, which capNativeStartupText
|
|
512
|
+
// keeps while cutting the tail, so every extra character evicts session
|
|
513
|
+
// context. An enumerated draft measured 858 chars — 21% of the whole 4,000
|
|
514
|
+
// quick budget, against 9% for the wrong-but-short original.
|
|
499
515
|
const conflictWarning = snapshot.conflicts.length > 0
|
|
500
|
-
? `\n> - ⚠️ **SKILLS NOT UPDATING (
|
|
516
|
+
? `\n> - ⚠️ **SKILLS NOT UPDATING (on-disk copy differs from the managed one):** ` +
|
|
501
517
|
`${formatBoundedSkillNames([...snapshot.conflicts].sort(), "blocked")}. ` +
|
|
502
|
-
`Each
|
|
503
|
-
`
|
|
504
|
-
`
|
|
505
|
-
`
|
|
518
|
+
`Each is frozen at whatever version is on disk — updates are withheld to ` +
|
|
519
|
+
`protect local edits. To see why, compare the directory's file list with ` +
|
|
520
|
+
`the \`files\` keys in its \`.prism-managed.json\`. An extra file you do ` +
|
|
521
|
+
`not need? Delete it; the next sync adopts the skill again. A local edit ` +
|
|
522
|
+
`you want to discard? Move the directory out of the native skills folder ` +
|
|
523
|
+
`and rerun \`prism connect\` (or a session bootstrap).`
|
|
506
524
|
: "";
|
|
507
525
|
// Durable, not per-run: a later sync reports "unchanged" while the roots stay
|
|
508
526
|
// frozen from an earlier failed materialization. This line is the difference
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.18.
|
|
3
|
+
"version": "20.18.2",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
|
|
6
6
|
"module": "index.ts",
|