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 mismatch validation (v13hard-rejects on mismatch) ───
503
- // Replaces the old soft-warning behavior that allowed cross-project
504
- // writes. See projectResolver.ts for the lookup logic.
502
+ // ─── Project validation (v14warns, 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 = `\n📝 Auto-registered project "${project}" with repo_path derived from files_changed.`;
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: false,
87
- error: `Project mismatch: declared "${declaredProject}" but files_changed indicate "${derivedProject}".`,
88
- hint: `Re-issue the request with project="${derivedProject}". ` +
89
- `If you genuinely intended "${declaredProject}", first add it to the registry with a non-overlapping repo_path.`,
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.0",
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",
@@ -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 every capture is normalized to a 1900px long edge
1310
- # here, at the source. sips is macOS-only; elsewhere we fall back to
1311
- # Pillow if present and otherwise WARN LOUDLY rather than emit poison
1312
- # silently.
1313
- _downscale_warning = _enforce_max_edge(path, 1900)
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}")