@plur-ai/mcp 0.9.8 → 0.9.10

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Give your AI agent persistent memory. One line in your MCP config — corrections, preferences, and conventions persist across sessions. No workflow changes, no cloud, no API costs for search.
4
4
 
5
- Part of [PLUR](https://plur.ai) — where **Haiku with memory outperforms Opus without it** at 10x less cost.
5
+ Part of [PLUR](https://plur.ai) — where, in our tool-routing and local-knowledge benchmark, **Haiku with memory outperformed Opus without it** at 10x less cost.
6
6
 
7
7
  ## Setup (30 seconds)
8
8
 
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { homedir } from "os";
8
- var VERSION = "0.9.8";
7
+ import { homedir, platform } from "os";
8
+ var VERSION = "0.9.10";
9
9
  var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
10
10
 
11
11
  Usage:
@@ -53,7 +53,9 @@ function extractManifestVersion(skillMdPath) {
53
53
  return null;
54
54
  }
55
55
  }
56
- var CLI = "npx @plur-ai/cli";
56
+ var _shimName = platform() === "win32" ? "plur-hook.cmd" : "plur-hook";
57
+ var _shimCandidate = join(homedir(), ".plur", "bin", _shimName);
58
+ var CLI = existsSync(_shimCandidate) ? _shimCandidate : "npx @plur-ai/cli";
57
59
  var PLUR_HOOKS = {
58
60
  // --- Session lifecycle ---
59
61
  UserPromptSubmit: [{
@@ -277,7 +279,7 @@ if (arg === "init") {
277
279
  process.exit(0);
278
280
  }
279
281
  if (arg === "serve" || arg === void 0) {
280
- const { runStdio } = await import("./server-4IHAOMXN.js");
282
+ const { runStdio } = await import("./server-GRHXPO6E.js");
281
283
  runStdio().catch((err) => {
282
284
  console.error("Failed to start PLUR MCP server:", err);
283
285
  process.exit(1);
@@ -14,7 +14,12 @@ import {
14
14
  import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
15
15
 
16
16
  // src/tools.ts
17
- import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation } from "@plur-ai/core";
17
+ import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary } from "@plur-ai/core";
18
+
19
+ // src/version.ts
20
+ var VERSION = "0.9.10";
21
+
22
+ // src/tools.ts
18
23
  function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
19
24
  return async (prompt) => {
20
25
  const response = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
@@ -72,6 +77,26 @@ function getLlmFunction() {
72
77
  if (openaiKey) return makeHttpLlm("https://api.openai.com/v1", openaiKey, "gpt-4o-mini");
73
78
  return void 0;
74
79
  }
80
+ function sanitizeStatement(raw) {
81
+ const markers = ["</statement>", "<parameter name="];
82
+ let cut = raw.length;
83
+ for (const m of markers) {
84
+ const pos = raw.indexOf(m);
85
+ if (pos !== -1 && pos < cut) cut = pos;
86
+ }
87
+ return raw.slice(0, cut).trimEnd();
88
+ }
89
+ var mcpCanary = new CapabilityCanary({ threshold: 10 });
90
+ mcpCanary.expect({
91
+ id: "session_start_hook",
92
+ description: "Automatic memory injection via hooks",
93
+ fix: "Run: npx @plur-ai/mcp init"
94
+ });
95
+ mcpCanary.expect({
96
+ id: "learn_activity",
97
+ description: "Learning from corrections",
98
+ fix: "Call plur_learn when corrected. If using hooks, verify they are installed."
99
+ });
75
100
  function getToolDefinitions() {
76
101
  return [
77
102
  {
@@ -89,38 +114,12 @@ function getToolDefinitions() {
89
114
  },
90
115
  scope: { type: "string", description: "Namespace, e.g. global, project:myapp" },
91
116
  domain: { type: "string", description: "Domain tag, e.g. software.deployment" },
92
- source: { type: "string", description: "Origin of this knowledge" },
93
- tags: { type: "array", items: { type: "string" }, description: "Searchable keyword tags" },
94
- rationale: { type: "string", description: "Why this knowledge matters" },
95
- visibility: { type: "string", enum: ["private", "public", "template"], description: "Visibility level" },
96
- knowledge_anchors: {
97
- type: "array",
98
- items: {
99
- type: "object",
100
- properties: {
101
- path: { type: "string", description: "Path to related document" },
102
- relevance: { type: "string", enum: ["primary", "supporting", "example"] },
103
- snippet: { type: "string", description: "Short snippet (max 200 chars)" }
104
- },
105
- required: ["path"]
106
- },
107
- description: "Links to related knowledge documents"
108
- },
109
- dual_coding: {
110
- type: "object",
111
- properties: {
112
- example: { type: "string", description: "Concrete example" },
113
- analogy: { type: "string", description: "Analogy to aid understanding" }
114
- },
115
- description: "Dual coding for richer encoding"
116
- },
117
- abstract: { type: "string", description: "Abstract engram ID this was derived from" },
118
- derived_from: { type: "string", description: "Source engram ID this was derived from" },
119
- commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked"], description: "Commitment level (default: leaning)" },
120
- locked_reason: { type: "string", description: "Reason for locking (when commitment=locked)" },
121
- memory_class: { type: "string", enum: ["semantic", "episodic", "procedural", "metacognitive"], description: "Memory classification (auto-set from type if omitted)" },
122
- session_episode_id: { type: "string", description: "Link to current session episode for episodic anchoring" },
123
- pinned: { type: "boolean", description: "Always-load flag. If true, this engram bypasses the keyword-relevance gate and is eligible for injection on every session, regardless of overlap with the user task. Use sparingly: meta-rules, safety conventions, core operating principles only." }
117
+ tags: { type: "array", items: { type: "string" }, description: "Searchable keyword tags \u2014 contribute to BM25/embedding recall, so concrete keywords pay off" },
118
+ rationale: { type: "string", description: "Why this knowledge matters \u2014 also enters the search corpus, helps recall by intent not just statement" },
119
+ source: { type: "string", description: "Origin of this knowledge (URL, conversation ref, etc.)" },
120
+ pinned: { type: "boolean", description: "Always-load flag. If true, this engram bypasses the keyword-relevance gate at injection time. Use sparingly: meta-rules, safety conventions, core operating principles only." },
121
+ commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked"], description: "How firmly the user has committed to this belief (default: leaning)" },
122
+ locked_reason: { type: "string", description: "Why this engram is locked (only meaningful when commitment=locked)" }
124
123
  },
125
124
  required: ["statement"]
126
125
  },
@@ -133,37 +132,37 @@ function getToolDefinitions() {
133
132
  source: args.source,
134
133
  tags: args.tags,
135
134
  rationale: args.rationale,
136
- visibility: args.visibility,
137
- knowledge_anchors: args.knowledge_anchors,
138
- dual_coding: args.dual_coding,
139
- abstract: args.abstract,
140
- derived_from: args.derived_from,
141
135
  commitment: args.commitment,
142
136
  locked_reason: args.locked_reason,
143
- memory_class: args.memory_class,
144
- session_episode_id: args.session_episode_id,
145
137
  pinned: args.pinned,
146
138
  llm
147
139
  };
140
+ const statement = sanitizeStatement(args.statement);
148
141
  try {
149
- const engram = await plur.learnRouted(args.statement, context);
142
+ const engram = await plur.learnRouted(statement, context);
143
+ const isOutbox = !!engram.structured_data?._outbox;
144
+ mcpCanary.signal("learn_activity");
150
145
  return {
151
146
  id: engram.id,
152
147
  statement: engram.statement,
153
148
  scope: engram.scope,
154
149
  type: engram.type,
155
150
  pinned: engram.pinned === true,
156
- decision: "ADD"
151
+ decision: "ADD",
152
+ ...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {}
157
153
  };
158
154
  } catch (err) {
159
- const engram = plur.learn(args.statement, context);
155
+ const engram = plur.learn(statement, context);
156
+ const isOutbox = !!engram.structured_data?._outbox;
157
+ mcpCanary.signal("learn_activity");
160
158
  return {
161
159
  id: engram.id,
162
160
  statement: engram.statement,
163
161
  scope: engram.scope,
164
162
  type: engram.type,
165
163
  decision: "ADD",
166
- warning: `Remote write failed (${err.message}); fell back to local. The id above is the local placeholder \u2014 the canonical engram is NOT on the server.`
164
+ ...isOutbox ? { outbox: true } : {},
165
+ warning: `Remote write failed (${err.message}); engram queued for retry.`
167
166
  };
168
167
  }
169
168
  }
@@ -179,7 +178,6 @@ function getToolDefinitions() {
179
178
  scope: { type: "string", description: "Filter by scope (also includes global)" },
180
179
  domain: { type: "string", description: "Filter by domain prefix" },
181
180
  limit: { type: "number", description: "Max results to return (default 20)" },
182
- min_strength: { type: "number", description: "Minimum retrieval strength (0-1)" },
183
181
  budget: { type: "object", description: "Budget constraints for sub-agents", properties: { max_tokens: { type: "number" }, max_results: { type: "number" } } },
184
182
  caller_session_id: { type: "string", description: "Caller session ID for budget enforcement" }
185
183
  },
@@ -189,8 +187,7 @@ function getToolDefinitions() {
189
187
  const results = plur.recall(args.query, {
190
188
  scope: args.scope,
191
189
  domain: args.domain,
192
- limit: args.limit,
193
- min_strength: args.min_strength
190
+ limit: args.limit
194
191
  });
195
192
  return {
196
193
  results: results.map((e) => ({
@@ -216,7 +213,6 @@ function getToolDefinitions() {
216
213
  scope: { type: "string", description: "Filter by scope (also includes global)" },
217
214
  domain: { type: "string", description: "Filter by domain prefix" },
218
215
  limit: { type: "number", description: "Max results to return (default 20)" },
219
- min_strength: { type: "number", description: "Minimum retrieval strength (0-1)" },
220
216
  budget: { type: "object", description: "Budget constraints for sub-agents", properties: { max_tokens: { type: "number" }, max_results: { type: "number" }, ttl_seconds: { type: "number" } } },
221
217
  caller_session_id: { type: "string", description: "Session ID of calling agent for budget enforcement" },
222
218
  include_episodes: { type: "boolean", description: "If true, include linked episode summaries for each engram (SP2 episodic anchoring)" }
@@ -229,8 +225,7 @@ function getToolDefinitions() {
229
225
  const meta = await plur.recallHybridWithMeta(args.query, {
230
226
  scope: args.scope,
231
227
  domain: args.domain,
232
- limit: effectiveLimit,
233
- min_strength: args.min_strength
228
+ limit: effectiveLimit
234
229
  });
235
230
  const results = meta.engrams;
236
231
  let truncated = false;
@@ -369,7 +364,7 @@ function getToolDefinitions() {
369
364
  const summary = { positive: 0, negative: 0, neutral: 0 };
370
365
  for (const { id, signal } of args.signals) {
371
366
  try {
372
- plur.feedback(id, signal);
367
+ await plur.feedback(id, signal);
373
368
  results.push({ id, signal, success: true });
374
369
  summary[signal]++;
375
370
  } catch (err) {
@@ -379,7 +374,7 @@ function getToolDefinitions() {
379
374
  return { mode: "batch", results, summary };
380
375
  }
381
376
  try {
382
- plur.feedback(args.id, args.signal);
377
+ await plur.feedback(args.id, args.signal);
383
378
  return { success: true, id: args.id, signal: args.signal };
384
379
  } catch (err) {
385
380
  if (err.message?.includes("readonly store")) {
@@ -434,16 +429,19 @@ function getToolDefinitions() {
434
429
  handler: async (args, plur) => {
435
430
  if (args.id) {
436
431
  const engram = plur.getById(args.id);
437
- if (!engram) throw new Error(`Engram not found: ${args.id}`);
438
- if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
439
- plur.forget(args.id);
440
- return { success: true, retired: { id: engram.id, statement: engram.statement } };
432
+ if (engram) {
433
+ if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
434
+ await plur.forget(args.id);
435
+ return { success: true, retired: { id: engram.id, statement: engram.statement } };
436
+ }
437
+ await plur.forget(args.id);
438
+ return { success: true, retired: { id: args.id } };
441
439
  }
442
440
  if (args.search) {
443
441
  const matches = plur.recall(args.search, { limit: 100 });
444
442
  if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
445
443
  if (matches.length === 1) {
446
- plur.forget(matches[0].id);
444
+ await plur.forget(matches[0].id);
447
445
  return { success: true, retired: { id: matches[0].id, statement: matches[0].statement } };
448
446
  }
449
447
  return {
@@ -670,7 +668,21 @@ function getToolDefinitions() {
670
668
  },
671
669
  handler: async (args, plur) => {
672
670
  const result = plur.sync(args.remote);
673
- return result;
671
+ let outbox_result;
672
+ try {
673
+ outbox_result = await plur.flushOutbox();
674
+ } catch {
675
+ }
676
+ return {
677
+ ...result,
678
+ ...outbox_result && (outbox_result.flushed > 0 || outbox_result.failed > 0) ? {
679
+ outbox: {
680
+ flushed: outbox_result.flushed,
681
+ pending: outbox_result.failed,
682
+ warnings: outbox_result.expired_warnings
683
+ }
684
+ } : {}
685
+ };
674
686
  }
675
687
  },
676
688
  {
@@ -835,7 +847,7 @@ function getToolDefinitions() {
835
847
  },
836
848
  {
837
849
  name: "plur_status",
838
- description: "Return system health \u2014 engram count, episode count, pack count, storage root",
850
+ description: "Return system health \u2014 running version, engram count, episode count, pack count, storage root",
839
851
  annotations: { title: "Status", readOnlyHint: true, idempotentHint: true },
840
852
  inputSchema: {
841
853
  type: "object",
@@ -843,14 +855,26 @@ function getToolDefinitions() {
843
855
  },
844
856
  handler: async (_args, plur) => {
845
857
  const status = plur.status();
858
+ const versionCheck = getCachedUpdateCheck("@plur-ai/mcp");
846
859
  return {
860
+ version: VERSION,
847
861
  engram_count: status.engram_count,
848
862
  episode_count: status.episode_count,
849
863
  pack_count: status.pack_count,
850
864
  storage_root: status.storage_root,
851
865
  locked_count: status.locked_count,
852
866
  tension_count: status.tension_count,
853
- versioned_engram_count: status.versioned_engram_count ?? 0
867
+ versioned_engram_count: status.versioned_engram_count ?? 0,
868
+ outbox_count: status.outbox_count ?? 0,
869
+ // Version check (issue #151)
870
+ ...versionCheck?.updateAvailable && versionCheck.latest ? {
871
+ update_available: {
872
+ current: versionCheck.current,
873
+ latest: versionCheck.latest,
874
+ behind: minorVersionsBehind(versionCheck.current, versionCheck.latest)
875
+ }
876
+ } : {},
877
+ capabilities: mcpCanary.status()
854
878
  };
855
879
  }
856
880
  },
@@ -919,6 +943,13 @@ function getToolDefinitions() {
919
943
  " \u2022 Or opt out: set PLUR_DISABLE_EMBEDDINGS=1, or write `embeddings: { enabled: false }` to ~/.plur/config.yaml \u2014 hybrid search will run BM25-only"
920
944
  );
921
945
  }
946
+ const canaryStatuses = mcpCanary.status();
947
+ for (const cs of canaryStatuses) {
948
+ if (!cs.healthy) {
949
+ checks.push({ check: `capability: ${cs.capability}`, ok: false, detail: cs.warning });
950
+ if (cs.warning) remediation.push(cs.warning);
951
+ }
952
+ }
922
953
  return {
923
954
  ok: checks.every((c) => c.ok),
924
955
  checks,
@@ -926,6 +957,7 @@ function getToolDefinitions() {
926
957
  before_probe: before,
927
958
  after_probe: after
928
959
  },
960
+ capabilities: canaryStatuses,
929
961
  remediation: remediation.length > 0 ? remediation : ["All checks passed \u2014 PLUR is healthy."]
930
962
  };
931
963
  }
@@ -943,10 +975,17 @@ function getToolDefinitions() {
943
975
  required: ["task"]
944
976
  },
945
977
  handler: async (args, plur) => {
978
+ mcpCanary.tick();
979
+ mcpCanary.signal("session_start_hook");
946
980
  const crypto = await import("crypto");
947
981
  const session_id = crypto.randomUUID();
948
982
  const task = args.task;
949
983
  const tags = args.tags;
984
+ let outbox_result;
985
+ try {
986
+ outbox_result = await plur.flushOutbox();
987
+ } catch {
988
+ }
950
989
  const status = plur.status();
951
990
  const store_stats = {
952
991
  engram_count: status.engram_count,
@@ -988,6 +1027,19 @@ function getToolDefinitions() {
988
1027
  You have ${store_stats.engram_count} engrams but none matched this task. Call plur_learn to capture new learnings from this session.`;
989
1028
  }
990
1029
  const isFreshInstall = store_stats.engram_count === 0 && store_stats.episode_count === 0;
1030
+ const versionCheck = getCachedUpdateCheck("@plur-ai/mcp");
1031
+ let version_warning;
1032
+ if (versionCheck?.updateAvailable && versionCheck.latest) {
1033
+ const behind = minorVersionsBehind(versionCheck.current, versionCheck.latest);
1034
+ if (behind > 2) {
1035
+ version_warning = `CRITICAL: Running PLUR v${versionCheck.current} \u2014 latest is v${versionCheck.latest} (${behind} minor versions behind). Known bugs may be present. Update immediately: npx @plur-ai/mcp@latest`;
1036
+ guide = `\u26A0\uFE0F ${version_warning}
1037
+
1038
+ ${guide}`;
1039
+ } else {
1040
+ version_warning = `Update available: PLUR v${versionCheck.current} \u2192 v${versionCheck.latest}. Run: npx @plur-ai/mcp@latest`;
1041
+ }
1042
+ }
991
1043
  return {
992
1044
  session_id,
993
1045
  engrams: engrams ?? [],
@@ -996,7 +1048,17 @@ You have ${store_stats.engram_count} engrams but none matched this task. Call pl
996
1048
  // Ask LLM to check back — MCP can't push, but we can request a follow-up
997
1049
  follow_up: store_stats.engram_count === 0 ? "This is a fresh store with 0 engrams. After your first exchange with the user, review what you learned and call plur_learn for any corrections, preferences, or patterns. Build the memory from this session." : void 0,
998
1050
  // On fresh install, suggest hook setup for reliable injection
999
- setup_hint: isFreshInstall ? "IMPORTANT: For reliable memory injection, ask the user to run: npx @plur-ai/cli init \u2014 this installs Claude Code hooks that automatically inject engrams at conversation start and after context compaction. Without hooks, memory injection depends on you remembering to call plur_session_start." : void 0
1051
+ setup_hint: isFreshInstall ? "IMPORTANT: For reliable memory injection, ask the user to run: npx @plur-ai/cli init \u2014 this installs Claude Code hooks that automatically inject engrams at conversation start and after context compaction. Without hooks, memory injection depends on you remembering to call plur_session_start." : void 0,
1052
+ // Outbox flush results (issue #26)
1053
+ ...outbox_result && (outbox_result.flushed > 0 || outbox_result.failed > 0) ? {
1054
+ outbox: {
1055
+ flushed: outbox_result.flushed,
1056
+ pending: outbox_result.failed,
1057
+ warnings: outbox_result.expired_warnings
1058
+ }
1059
+ } : {},
1060
+ // Version staleness warning (issue #151)
1061
+ ...version_warning ? { version_warning, version: VERSION } : {}
1000
1062
  };
1001
1063
  }
1002
1064
  },
@@ -1099,7 +1161,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1099
1161
  inputSchema: { type: "object", properties: {} },
1100
1162
  handler: async (_args, plur) => {
1101
1163
  const stores = plur.listStores();
1102
- return { stores, count: stores.length };
1164
+ const outboxCount = plur.outboxCount();
1165
+ return {
1166
+ stores,
1167
+ count: stores.length,
1168
+ ...outboxCount > 0 ? { outbox_pending: outboxCount } : {}
1169
+ };
1103
1170
  }
1104
1171
  },
1105
1172
  {
@@ -1144,13 +1211,19 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1144
1211
  },
1145
1212
  {
1146
1213
  name: "plur_tensions",
1147
- description: "List engram pairs that have conflicting knowledge \u2014 shows tensions in your memory that may need resolution",
1214
+ description: "List or scan for engram pairs that have conflicting knowledge. Without scan mode, shows previously detected conflicts. With scan:true, runs an active LLM-powered contradiction scan and returns only high-confidence tensions.",
1148
1215
  annotations: { title: "Tensions", readOnlyHint: true, idempotentHint: true },
1149
1216
  inputSchema: {
1150
1217
  type: "object",
1151
1218
  properties: {
1152
1219
  scope: { type: "string", description: "Filter by scope" },
1153
- domain: { type: "string", description: "Filter by domain prefix" }
1220
+ domain: { type: "string", description: "Filter by domain prefix" },
1221
+ scan: { type: "boolean", description: "Run an active contradiction scan using an LLM judge. Requires OPENAI_API_KEY or OPENROUTER_API_KEY env var, or explicit llm_base_url + llm_api_key args." },
1222
+ llm_base_url: { type: "string", description: "OpenAI-compatible API base URL for scan mode (e.g. https://api.openai.com/v1)" },
1223
+ llm_api_key: { type: "string", description: "API key for the LLM (scan mode)" },
1224
+ llm_model: { type: "string", description: "Model name for scan mode (default: gpt-4o-mini)" },
1225
+ min_confidence: { type: "number", description: "Minimum confidence threshold for scan mode (0\u20131, default: 0.7)" },
1226
+ max_pairs: { type: "number", description: "Maximum candidate pairs to evaluate in scan mode (default: 50)" }
1154
1227
  }
1155
1228
  },
1156
1229
  handler: async (args, plur) => {
@@ -1158,6 +1231,30 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1158
1231
  scope: args.scope,
1159
1232
  domain: args.domain
1160
1233
  });
1234
+ if (args.scan) {
1235
+ const llm = args.llm_base_url && args.llm_api_key ? makeHttpLlm(args.llm_base_url, args.llm_api_key, args.llm_model) : getLlmFunction();
1236
+ if (!llm) {
1237
+ return {
1238
+ error: "scan mode requires an LLM. Set OPENAI_API_KEY or OPENROUTER_API_KEY, or pass llm_base_url + llm_api_key.",
1239
+ tensions: [],
1240
+ count: 0
1241
+ };
1242
+ }
1243
+ const result = await scanForTensions(engrams, llm, {
1244
+ min_confidence: args.min_confidence,
1245
+ max_pairs: args.max_pairs
1246
+ });
1247
+ return {
1248
+ pairs_checked: result.pairs_checked,
1249
+ count: result.new_tensions,
1250
+ tensions: result.tensions.map((t) => ({
1251
+ engram_a: { id: t.id_a, statement: t.statement_a },
1252
+ engram_b: { id: t.id_b, statement: t.statement_b },
1253
+ confidence: t.confidence,
1254
+ reason: t.reason
1255
+ }))
1256
+ };
1257
+ }
1161
1258
  const tensions = [];
1162
1259
  const seen = /* @__PURE__ */ new Set();
1163
1260
  for (const engram of engrams) {
@@ -1171,11 +1268,27 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1171
1268
  tensions.push({
1172
1269
  engram_a: { id: engram.id, statement: engram.statement, type: engram.type },
1173
1270
  engram_b: { id: other.id, statement: other.statement, type: other.type },
1174
- detected_at: engram.activation.last_accessed
1271
+ detected_at: engram.activation.last_accessed,
1272
+ purge_hint: "These conflicts are from the legacy detection system. Run plur_tensions_purge to clear them, then use scan:true for active contradiction detection."
1175
1273
  });
1176
1274
  }
1177
1275
  }
1178
- return { tensions, count: tensions.length };
1276
+ const purge_hint = tensions.length > 0 ? "These are legacy conflict relations. Run plur_tensions_purge to clear them." : void 0;
1277
+ return { tensions, count: tensions.length, ...purge_hint ? { purge_hint } : {} };
1278
+ }
1279
+ },
1280
+ {
1281
+ name: "plur_tensions_purge",
1282
+ description: "Purge all conflict relations from local engrams \u2014 removes accumulated false positives from the legacy tension-detection system",
1283
+ annotations: { title: "Purge Tensions", destructiveHint: true, idempotentHint: true },
1284
+ inputSchema: { type: "object", properties: {} },
1285
+ handler: async (_args, plur) => {
1286
+ const result = plur.purgeTensions();
1287
+ return {
1288
+ purged_conflict_refs: result.purged_count,
1289
+ engrams_modified: result.engrams_modified,
1290
+ message: `Purged ${result.purged_count} conflict references from ${result.engrams_modified} engrams.`
1291
+ };
1179
1292
  }
1180
1293
  },
1181
1294
  {
@@ -1371,7 +1484,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1371
1484
  },
1372
1485
  {
1373
1486
  name: "plur_batch_decay",
1374
- description: "Apply ACT-R decay to all engrams. Run weekly. Returns status transitions only.",
1487
+ description: "Apply ACT-R decay to all local engrams. Run weekly. Only decays engrams in the local YAML store \u2014 remote-store engrams are not decayed client-side. Returns status transitions only.",
1375
1488
  annotations: { title: "Batch decay", destructiveHint: false, idempotentHint: false },
1376
1489
  inputSchema: {
1377
1490
  type: "object",
@@ -1424,7 +1537,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1424
1537
 
1425
1538
  // src/server.ts
1426
1539
  import { z } from "zod";
1427
- var VERSION = "0.9.8";
1428
1540
  var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
1429
1541
 
1430
1542
  PLUR is a GLOBAL tool \u2014 one MCP server, one engram store (~/.plur/), available in every project. Multi-project scoping uses domain/scope fields on engrams, not separate installations.
@@ -1561,7 +1673,7 @@ async function createServer(plur) {
1561
1673
  const tool = tools.find((t) => t.name === request.params.name);
1562
1674
  if (!tool) {
1563
1675
  return {
1564
- content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
1676
+ content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
1565
1677
  isError: true
1566
1678
  };
1567
1679
  }
@@ -1581,8 +1693,9 @@ async function createServer(plur) {
1581
1693
  }
1582
1694
  const parsed = z.object(shape).passthrough().safeParse(args);
1583
1695
  if (!parsed.success) {
1696
+ const details = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
1584
1697
  return {
1585
- content: [{ type: "text", text: `Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")}` }],
1698
+ content: [{ type: "text", text: JSON.stringify({ error: `Invalid arguments: ${details}`, success: false }) }],
1586
1699
  isError: true
1587
1700
  };
1588
1701
  }
@@ -1593,7 +1706,7 @@ async function createServer(plur) {
1593
1706
  const message = err?.message ?? String(err);
1594
1707
  server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
1595
1708
  return {
1596
- content: [{ type: "text", text: `Error: ${message}` }],
1709
+ content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }],
1597
1710
  isError: true
1598
1711
  };
1599
1712
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/mcp",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "plur-mcp": "dist/index.js"
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "@modelcontextprotocol/sdk": "^1.12.0",
15
15
  "zod": "^3.23.0",
16
- "@plur-ai/core": "0.9.8"
16
+ "@plur-ai/core": "0.9.10"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^25.5.0"