@plur-ai/mcp 0.9.9 → 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/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.9";
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-X5ZMO46X.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,10 +14,10 @@ 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
18
 
19
19
  // src/version.ts
20
- var VERSION = "0.9.9";
20
+ var VERSION = "0.9.10";
21
21
 
22
22
  // src/tools.ts
23
23
  function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
@@ -86,6 +86,17 @@ function sanitizeStatement(raw) {
86
86
  }
87
87
  return raw.slice(0, cut).trimEnd();
88
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
+ });
89
100
  function getToolDefinitions() {
90
101
  return [
91
102
  {
@@ -129,23 +140,29 @@ function getToolDefinitions() {
129
140
  const statement = sanitizeStatement(args.statement);
130
141
  try {
131
142
  const engram = await plur.learnRouted(statement, context);
143
+ const isOutbox = !!engram.structured_data?._outbox;
144
+ mcpCanary.signal("learn_activity");
132
145
  return {
133
146
  id: engram.id,
134
147
  statement: engram.statement,
135
148
  scope: engram.scope,
136
149
  type: engram.type,
137
150
  pinned: engram.pinned === true,
138
- 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." } : {}
139
153
  };
140
154
  } catch (err) {
141
155
  const engram = plur.learn(statement, context);
156
+ const isOutbox = !!engram.structured_data?._outbox;
157
+ mcpCanary.signal("learn_activity");
142
158
  return {
143
159
  id: engram.id,
144
160
  statement: engram.statement,
145
161
  scope: engram.scope,
146
162
  type: engram.type,
147
163
  decision: "ADD",
148
- 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.`
149
166
  };
150
167
  }
151
168
  }
@@ -412,10 +429,13 @@ function getToolDefinitions() {
412
429
  handler: async (args, plur) => {
413
430
  if (args.id) {
414
431
  const engram = plur.getById(args.id);
415
- if (!engram) throw new Error(`Engram not found: ${args.id}`);
416
- if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
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
+ }
417
437
  await plur.forget(args.id);
418
- return { success: true, retired: { id: engram.id, statement: engram.statement } };
438
+ return { success: true, retired: { id: args.id } };
419
439
  }
420
440
  if (args.search) {
421
441
  const matches = plur.recall(args.search, { limit: 100 });
@@ -648,7 +668,21 @@ function getToolDefinitions() {
648
668
  },
649
669
  handler: async (args, plur) => {
650
670
  const result = plur.sync(args.remote);
651
- 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
+ };
652
686
  }
653
687
  },
654
688
  {
@@ -821,6 +855,7 @@ function getToolDefinitions() {
821
855
  },
822
856
  handler: async (_args, plur) => {
823
857
  const status = plur.status();
858
+ const versionCheck = getCachedUpdateCheck("@plur-ai/mcp");
824
859
  return {
825
860
  version: VERSION,
826
861
  engram_count: status.engram_count,
@@ -829,7 +864,17 @@ function getToolDefinitions() {
829
864
  storage_root: status.storage_root,
830
865
  locked_count: status.locked_count,
831
866
  tension_count: status.tension_count,
832
- 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()
833
878
  };
834
879
  }
835
880
  },
@@ -898,6 +943,13 @@ function getToolDefinitions() {
898
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"
899
944
  );
900
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
+ }
901
953
  return {
902
954
  ok: checks.every((c) => c.ok),
903
955
  checks,
@@ -905,6 +957,7 @@ function getToolDefinitions() {
905
957
  before_probe: before,
906
958
  after_probe: after
907
959
  },
960
+ capabilities: canaryStatuses,
908
961
  remediation: remediation.length > 0 ? remediation : ["All checks passed \u2014 PLUR is healthy."]
909
962
  };
910
963
  }
@@ -922,10 +975,17 @@ function getToolDefinitions() {
922
975
  required: ["task"]
923
976
  },
924
977
  handler: async (args, plur) => {
978
+ mcpCanary.tick();
979
+ mcpCanary.signal("session_start_hook");
925
980
  const crypto = await import("crypto");
926
981
  const session_id = crypto.randomUUID();
927
982
  const task = args.task;
928
983
  const tags = args.tags;
984
+ let outbox_result;
985
+ try {
986
+ outbox_result = await plur.flushOutbox();
987
+ } catch {
988
+ }
929
989
  const status = plur.status();
930
990
  const store_stats = {
931
991
  engram_count: status.engram_count,
@@ -967,6 +1027,19 @@ function getToolDefinitions() {
967
1027
  You have ${store_stats.engram_count} engrams but none matched this task. Call plur_learn to capture new learnings from this session.`;
968
1028
  }
969
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
+ }
970
1043
  return {
971
1044
  session_id,
972
1045
  engrams: engrams ?? [],
@@ -975,7 +1048,17 @@ You have ${store_stats.engram_count} engrams but none matched this task. Call pl
975
1048
  // Ask LLM to check back — MCP can't push, but we can request a follow-up
976
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,
977
1050
  // On fresh install, suggest hook setup for reliable injection
978
- 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 } : {}
979
1062
  };
980
1063
  }
981
1064
  },
@@ -1078,7 +1161,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1078
1161
  inputSchema: { type: "object", properties: {} },
1079
1162
  handler: async (_args, plur) => {
1080
1163
  const stores = plur.listStores();
1081
- 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
+ };
1082
1170
  }
1083
1171
  },
1084
1172
  {
@@ -1123,13 +1211,19 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1123
1211
  },
1124
1212
  {
1125
1213
  name: "plur_tensions",
1126
- 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.",
1127
1215
  annotations: { title: "Tensions", readOnlyHint: true, idempotentHint: true },
1128
1216
  inputSchema: {
1129
1217
  type: "object",
1130
1218
  properties: {
1131
1219
  scope: { type: "string", description: "Filter by scope" },
1132
- 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)" }
1133
1227
  }
1134
1228
  },
1135
1229
  handler: async (args, plur) => {
@@ -1137,6 +1231,30 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1137
1231
  scope: args.scope,
1138
1232
  domain: args.domain
1139
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
+ }
1140
1258
  const tensions = [];
1141
1259
  const seen = /* @__PURE__ */ new Set();
1142
1260
  for (const engram of engrams) {
@@ -1150,11 +1268,13 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1150
1268
  tensions.push({
1151
1269
  engram_a: { id: engram.id, statement: engram.statement, type: engram.type },
1152
1270
  engram_b: { id: other.id, statement: other.statement, type: other.type },
1153
- 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."
1154
1273
  });
1155
1274
  }
1156
1275
  }
1157
- 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 } : {} };
1158
1278
  }
1159
1279
  },
1160
1280
  {
@@ -1364,7 +1484,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
1364
1484
  },
1365
1485
  {
1366
1486
  name: "plur_batch_decay",
1367
- 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.",
1368
1488
  annotations: { title: "Batch decay", destructiveHint: false, idempotentHint: false },
1369
1489
  inputSchema: {
1370
1490
  type: "object",
@@ -1553,7 +1673,7 @@ async function createServer(plur) {
1553
1673
  const tool = tools.find((t) => t.name === request.params.name);
1554
1674
  if (!tool) {
1555
1675
  return {
1556
- content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
1676
+ content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
1557
1677
  isError: true
1558
1678
  };
1559
1679
  }
@@ -1573,8 +1693,9 @@ async function createServer(plur) {
1573
1693
  }
1574
1694
  const parsed = z.object(shape).passthrough().safeParse(args);
1575
1695
  if (!parsed.success) {
1696
+ const details = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
1576
1697
  return {
1577
- 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 }) }],
1578
1699
  isError: true
1579
1700
  };
1580
1701
  }
@@ -1585,7 +1706,7 @@ async function createServer(plur) {
1585
1706
  const message = err?.message ?? String(err);
1586
1707
  server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
1587
1708
  return {
1588
- content: [{ type: "text", text: `Error: ${message}` }],
1709
+ content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }],
1589
1710
  isError: true
1590
1711
  };
1591
1712
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plur-ai/mcp",
3
- "version": "0.9.9",
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.9"
16
+ "@plur-ai/core": "0.9.10"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^25.5.0"