prism-mcp-server 20.11.0 → 20.11.1
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.
|
@@ -499,25 +499,19 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
499
499
|
};
|
|
500
500
|
}
|
|
501
501
|
const storage = await getStorage();
|
|
502
|
-
// ─── Project
|
|
503
|
-
//
|
|
504
|
-
//
|
|
502
|
+
// ─── Project validation (v14 — warns, NEVER refuses the write) ───
|
|
503
|
+
// v13 hard-rejected on mismatch, trusting auto-created registry rows that
|
|
504
|
+
// turned out to be junk on a live machine (home-dir and relative-path
|
|
505
|
+
// entries) — agents got contradictory rejections and sessions ended
|
|
506
|
+
// UNSAVED. A memory product must not drop data to enforce taxonomy.
|
|
505
507
|
let resolverNote = "";
|
|
506
508
|
const resolved = await resolveProject(project, files_changed);
|
|
507
|
-
if (!resolved.ok) {
|
|
508
|
-
return {
|
|
509
|
-
content: [{
|
|
510
|
-
type: "text",
|
|
511
|
-
text: `❌ ${resolved.error}\n` +
|
|
512
|
-
(resolved.hint ? `Hint: ${resolved.hint}\n` : "") +
|
|
513
|
-
`\nNo ledger entry was written. Re-issue the call with the correct project.`,
|
|
514
|
-
}],
|
|
515
|
-
isError: true,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
509
|
project = resolved.project;
|
|
510
|
+
if (resolved.warning) {
|
|
511
|
+
resolverNote = `\n⚠️ ${resolved.warning}`;
|
|
512
|
+
}
|
|
519
513
|
if (resolved.autoCreated) {
|
|
520
|
-
resolverNote
|
|
514
|
+
resolverNote += `\n📝 Auto-registered project "${project}" with repo_path derived from files_changed.`;
|
|
521
515
|
}
|
|
522
516
|
debugLog(`[session_save_ledger] Saving ledger entry for project="${project}"`);
|
|
523
517
|
// Auto-extract keywords from summary + decisions for knowledge accumulation
|
|
@@ -81,12 +81,22 @@ export async function resolveProject(declaredProject, filesChanged) {
|
|
|
81
81
|
}
|
|
82
82
|
const registry = await loadRegistry();
|
|
83
83
|
const derivedProject = pickFromRegistry(registry, filesChanged);
|
|
84
|
+
// 2026-08-13: this used to HARD-REJECT on mismatch — and the registry it
|
|
85
|
+
// trusted was auto-created junk (a live machine carried a repo_path entry
|
|
86
|
+
// pointing at the HOME DIRECTORY, which contains every absolute path, plus
|
|
87
|
+
// five relative-path rows). Agents got contradictory rejections, ping-ponged
|
|
88
|
+
// by the hint, and sessions ended UNSAVED — a memory product dropping data to
|
|
89
|
+
// enforce taxonomy. The declaration now always wins; the derivation is an
|
|
90
|
+
// advisory warning. (The wrong-project class this gate was built for —
|
|
91
|
+
// a 2026-04-30 memory-loss incident — is still surfaced, as a warning
|
|
92
|
+
// the agent sees at save time instead of a refusal the user never does.)
|
|
84
93
|
if (derivedProject && derivedProject !== declaredProject) {
|
|
85
94
|
return {
|
|
86
|
-
ok:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`
|
|
95
|
+
ok: true,
|
|
96
|
+
project: declaredProject,
|
|
97
|
+
warning: `Registry suggests these files belong to "${derivedProject}", but the entry was saved under ` +
|
|
98
|
+
`"${declaredProject}" as declared. If "${derivedProject}" is correct, re-save with that project; ` +
|
|
99
|
+
`if the registry is wrong, its repo_path for "${derivedProject}" needs repair.`,
|
|
90
100
|
};
|
|
91
101
|
}
|
|
92
102
|
if (derivedProject && derivedProject === declaredProject) {
|
|
@@ -96,7 +106,7 @@ export async function resolveProject(declaredProject, filesChanged) {
|
|
|
96
106
|
return { ok: true, project: declaredProject };
|
|
97
107
|
}
|
|
98
108
|
const prefix = commonPathPrefix(filesChanged);
|
|
99
|
-
if (prefix) {
|
|
109
|
+
if (prefix && isRegistrablePrefix(prefix, registry)) {
|
|
100
110
|
try {
|
|
101
111
|
await setSetting(`${REPO_PATH_PREFIX}${declaredProject}`, prefix);
|
|
102
112
|
debugLog(`[projectResolver] auto-created repo_path:${declaredProject} = ${prefix}`);
|
|
@@ -108,3 +118,26 @@ export async function resolveProject(declaredProject, filesChanged) {
|
|
|
108
118
|
}
|
|
109
119
|
return { ok: true, project: declaredProject };
|
|
110
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Auto-create hygiene, added 2026-08-13. Every class rejected here was FOUND
|
|
123
|
+
* in a live registry, where it poisoned later saves:
|
|
124
|
+
* - relative prefixes ("Tests/UITests") match or miss depending on the path
|
|
125
|
+
* style a later save happens to use;
|
|
126
|
+
* - short prefixes (a 2-segment home directory) contain every absolute
|
|
127
|
+
* path on the machine;
|
|
128
|
+
* - an ancestor of an existing entry re-creates the same containment bomb
|
|
129
|
+
* one level down.
|
|
130
|
+
* Refusing to register is safe: the save proceeds either way, and the next
|
|
131
|
+
* save with a cleaner file list can still register the project.
|
|
132
|
+
*/
|
|
133
|
+
function isRegistrablePrefix(prefix, registry) {
|
|
134
|
+
if (!prefix.startsWith("/"))
|
|
135
|
+
return false;
|
|
136
|
+
if (prefix.split("/").filter(Boolean).length < 3)
|
|
137
|
+
return false;
|
|
138
|
+
for (const entry of registry) {
|
|
139
|
+
if (isUnder(entry.repo_path, prefix))
|
|
140
|
+
return false; // ancestor of an existing entry
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.11.
|
|
3
|
+
"version": "20.11.1",
|
|
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",
|
package/scripts/dev/browse.py
CHANGED
|
@@ -1259,6 +1259,22 @@ def _enforce_max_edge(path, max_edge):
|
|
|
1259
1259
|
try:
|
|
1260
1260
|
import shutil as _shutil, subprocess as _subprocess
|
|
1261
1261
|
if _shutil.which("sips"):
|
|
1262
|
+
# `sips -Z` resamples in BOTH directions — it UPSCALES an image that
|
|
1263
|
+
# is already smaller than max_edge. Unguarded, every macOS capture
|
|
1264
|
+
# came out at exactly the cap on its long edge: a 1440x900 viewport
|
|
1265
|
+
# was written as 1900x1187 and a 1920x1280 one as 1900x1266. That is
|
|
1266
|
+
# not evidence of what rendered, and it breaks any acceptance gate
|
|
1267
|
+
# that asserts a capture is viewport-bound. Measure first and only
|
|
1268
|
+
# shrink when the image genuinely exceeds the cap — the Pillow branch
|
|
1269
|
+
# below already guards this way.
|
|
1270
|
+
probe = _subprocess.run(
|
|
1271
|
+
["sips", "-g", "pixelWidth", "-g", "pixelHeight", str(path)],
|
|
1272
|
+
capture_output=True, timeout=30, text=True)
|
|
1273
|
+
edges = [int(part.split(":")[1].strip())
|
|
1274
|
+
for part in probe.stdout.splitlines()
|
|
1275
|
+
if "pixelWidth:" in part or "pixelHeight:" in part]
|
|
1276
|
+
if edges and max(edges) <= max_edge:
|
|
1277
|
+
return None
|
|
1262
1278
|
_subprocess.run(["sips", "-Z", str(max_edge), str(path)],
|
|
1263
1279
|
capture_output=True, timeout=30, check=True)
|
|
1264
1280
|
return None
|
|
@@ -1306,11 +1322,15 @@ def cmd_screenshot(session, output=None, cleanup=False, full_page=True, selector
|
|
|
1306
1322
|
# oversized capture early in a session poisons every later attach — the
|
|
1307
1323
|
# agent that must LOOK at screenshots loses the ability to see them,
|
|
1308
1324
|
# mid-conversation, permanently. Full-page captures routinely exceed
|
|
1309
|
-
# 2000px in height, so
|
|
1310
|
-
#
|
|
1311
|
-
#
|
|
1312
|
-
#
|
|
1313
|
-
|
|
1325
|
+
# 2000px in height, so oversized captures are shrunk here, at the source.
|
|
1326
|
+
# sips is macOS-only; elsewhere we fall back to Pillow if present and
|
|
1327
|
+
# otherwise WARN LOUDLY rather than emit poison silently.
|
|
1328
|
+
#
|
|
1329
|
+
# 2000, not 1900: the API limit quoted above is 2000px per dimension, and a
|
|
1330
|
+
# 1900 cap sits just under the standard 1920-wide desktop viewport — so the
|
|
1331
|
+
# single most common UI-evidence capture was resampled to 1900x1266 for no
|
|
1332
|
+
# benefit, failing every gate that asserts a capture is viewport-bound.
|
|
1333
|
+
_downscale_warning = _enforce_max_edge(path, 2000)
|
|
1314
1334
|
|
|
1315
1335
|
size = path.stat().st_size
|
|
1316
1336
|
audit_log("screenshot", str(path), f"size={size},ephemeral={cleanup}")
|