@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 +1 -1
- package/dist/index.js +6 -4
- package/dist/{server-4IHAOMXN.js → server-GRHXPO6E.js} +184 -71
- package/package.json +2 -2
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
|
|
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.
|
|
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
|
|
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-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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:
|
|
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.
|
|
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.
|
|
16
|
+
"@plur-ai/core": "0.9.10"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/node": "^25.5.0"
|