@plur-ai/mcp 0.13.0 → 0.15.0
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 +10 -14
- package/dist/index.js +79 -7
- package/dist/{server-B6OJN4ZX.js → server-7JVOIBJV.js} +350 -81
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -46,25 +46,23 @@ Knowledge is stored as **engrams** — small assertions that strengthen with use
|
|
|
46
46
|
|
|
47
47
|
## Tools
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
By default (lean profile), your agent gets 11 tools. Everything else is reachable through `plur_admin`:
|
|
50
50
|
|
|
51
51
|
| Tool | What it does |
|
|
52
52
|
|------|-------------|
|
|
53
53
|
| `plur_session_start` | Start a session — injects relevant engrams for your task |
|
|
54
54
|
| `plur_learn` | Store a memory — correction, preference, convention, or decision |
|
|
55
55
|
| `plur_recall_hybrid` | **Best default** — BM25 + embeddings merged via RRF. Zero cost. |
|
|
56
|
-
| `plur_recall` | Keyword search (BM25 only, instant) |
|
|
57
|
-
| `plur_inject_hybrid` | Load relevant memories for the current task |
|
|
58
56
|
| `plur_feedback` | Rate a memory — trains relevance over time |
|
|
59
57
|
| `plur_forget` | Retire a memory (history preserved) |
|
|
60
58
|
| `plur_session_end` | End a session — captures summary and new learnings |
|
|
61
|
-
| `plur_capture` | Record a session event |
|
|
62
|
-
| `plur_timeline` | Query session history |
|
|
63
|
-
| `plur_ingest` | Extract learnings from text |
|
|
64
|
-
| `plur_sync` | Sync memory across machines via git |
|
|
65
|
-
| `plur_packs_install` | Install a shareable memory pack |
|
|
66
|
-
| `plur_packs_list` | List installed packs |
|
|
67
59
|
| `plur_status` | System health |
|
|
60
|
+
| `plur_doctor` | Diagnose embedder, hybrid search, and remote-store auth |
|
|
61
|
+
| `plur_packs_uninstall` | Remove an installed pack |
|
|
62
|
+
| `plur_tensions_purge` | Clear stale/resolved tensions |
|
|
63
|
+
| `plur_admin` | Dispatch to any other tool: `{ action: "plur_packs_install", args: {...} }` |
|
|
64
|
+
|
|
65
|
+
Less commonly needed tools (`plur_recall`, `plur_inject_hybrid`, `plur_learn_batch`, `plur_ingest`, `plur_sync`, `plur_packs_install`, `plur_packs_list`, `plur_capture`, `plur_timeline`, and more) are all reachable via `plur_admin`. Set `PLUR_TOOL_PROFILE=full` to expose all 40 tools directly.
|
|
68
66
|
|
|
69
67
|
## Sync across machines
|
|
70
68
|
|
|
@@ -101,11 +99,9 @@ Default: `~/.plur/`. Everything is plain YAML — open it, read it, edit it.
|
|
|
101
99
|
|
|
102
100
|
## Benchmark
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| A/B win rate vs no memory | 89% |
|
|
108
|
-
| House rules accuracy | 100% |
|
|
102
|
+
**Retrieval** (LongMemEval R@5): **76.7%** out-of-the-box · **97.0%** with openai-3-large embeddings
|
|
103
|
+
|
|
104
|
+
**Agent task impact:** Haiku + PLUR outperforms Opus *without* memory at ~10× less cost. House rules: **12–0** across Haiku, Sonnet, Opus. A/B win rate: **89%**.
|
|
109
105
|
|
|
110
106
|
[Full methodology →](https://plur.ai/benchmark.html)
|
|
111
107
|
|
package/dist/index.js
CHANGED
|
@@ -5,14 +5,17 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSy
|
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import { homedir, platform } from "os";
|
|
8
|
-
var VERSION = "0.
|
|
8
|
+
var VERSION = "0.15.0";
|
|
9
9
|
var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
plur-mcp
|
|
13
|
-
plur-mcp init
|
|
14
|
-
plur-mcp
|
|
15
|
-
plur-mcp
|
|
12
|
+
plur-mcp Start the MCP server (stdio transport)
|
|
13
|
+
plur-mcp init Set up PLUR: storage + MCP config + hooks + CLAUDE.md
|
|
14
|
+
plur-mcp packs install <path> Install a knowledge pack from a local directory
|
|
15
|
+
plur-mcp packs list List installed knowledge packs
|
|
16
|
+
plur-mcp packs uninstall <name> Uninstall a knowledge pack by name
|
|
17
|
+
plur-mcp --help Show this help message
|
|
18
|
+
plur-mcp --version Show version
|
|
16
19
|
|
|
17
20
|
Environment:
|
|
18
21
|
PLUR_PATH Storage location (default: ~/.plur/)
|
|
@@ -65,6 +68,12 @@ var PLUR_HOOKS = {
|
|
|
65
68
|
matcher: "auto|manual",
|
|
66
69
|
hooks: [{ type: "command", command: `${CLI} hook-inject --rehydrate`, timeout: 15 }]
|
|
67
70
|
}],
|
|
71
|
+
// Auto-close the memory lifecycle at session end (Claude Code SessionEnd,
|
|
72
|
+
// shipped v1.0.85) — captures a closing episode and cleans up the session
|
|
73
|
+
// checkpoint even if the agent forgot to call plur_session_end (#217).
|
|
74
|
+
SessionEnd: [{
|
|
75
|
+
hooks: [{ type: "command", command: `${CLI} hook-session-end`, timeout: 5 }]
|
|
76
|
+
}],
|
|
68
77
|
// --- Contextual injection ---
|
|
69
78
|
PreToolUse: [
|
|
70
79
|
{ matcher: "EnterPlanMode", hooks: [{ type: "command", command: `${CLI} hook-inject --event plan_mode`, timeout: 10 }] },
|
|
@@ -98,7 +107,7 @@ Hooks inject engrams automatically on every first message \u2014 you do not need
|
|
|
98
107
|
2. **Learn**: When corrected or discovering something new, call \`plur_learn\` immediately
|
|
99
108
|
3. **Recall**: Before answering factual questions, call \`plur_recall_hybrid\` \u2014 check memory first
|
|
100
109
|
4. **Feedback**: Rate injected engrams with \`plur_feedback\` (positive/negative) \u2014 trains relevance
|
|
101
|
-
5. **End**: Call \`plur_session_end\` with summary + engram_suggestions
|
|
110
|
+
5. **End**: Call \`plur_session_end\` with summary + engram_suggestions \u2014 a SessionEnd hook auto-closes the lifecycle if you forget, but calling it yourself captures higher-quality learnings
|
|
102
111
|
|
|
103
112
|
Do not ask permission to use these tools \u2014 they are your memory system.
|
|
104
113
|
|
|
@@ -264,6 +273,65 @@ async function runInit() {
|
|
|
264
273
|
`);
|
|
265
274
|
}
|
|
266
275
|
}
|
|
276
|
+
async function runPacks() {
|
|
277
|
+
const sub = process.argv[3];
|
|
278
|
+
const plurPath = process.env.PLUR_PATH ?? join(homedir(), ".plur");
|
|
279
|
+
const { Plur } = await import("@plur-ai/core");
|
|
280
|
+
const plur = new Plur({ path: plurPath });
|
|
281
|
+
if (sub === "install") {
|
|
282
|
+
const source = process.argv[4];
|
|
283
|
+
if (!source) {
|
|
284
|
+
process.stderr.write("Usage: plur-mcp packs install <path>\n");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const result = plur.installPack(source);
|
|
289
|
+
process.stdout.write(`Installed pack '${result.name}' (${result.installed} engrams)
|
|
290
|
+
`);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
process.stderr.write(`Error: ${err.message}
|
|
293
|
+
`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
} else if (sub === "list") {
|
|
297
|
+
const packs = plur.listPacks();
|
|
298
|
+
if (packs.length === 0) {
|
|
299
|
+
process.stdout.write("No packs installed.\n");
|
|
300
|
+
} else {
|
|
301
|
+
for (const pack of packs) {
|
|
302
|
+
const version = pack.manifest?.version ? ` v${pack.manifest.version}` : "";
|
|
303
|
+
process.stdout.write(`${pack.name}${version} (${pack.engram_count} engrams)
|
|
304
|
+
`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else if (sub === "uninstall") {
|
|
308
|
+
const name = process.argv[4];
|
|
309
|
+
if (!name) {
|
|
310
|
+
process.stderr.write("Usage: plur-mcp packs uninstall <name>\n");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const result = plur.uninstallPack(name);
|
|
315
|
+
if (result.removed) {
|
|
316
|
+
process.stdout.write(`Uninstalled pack '${result.name}' (${result.engram_count} engrams removed)
|
|
317
|
+
`);
|
|
318
|
+
} else {
|
|
319
|
+
process.stderr.write(`Pack '${name}' not found.
|
|
320
|
+
`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
process.stderr.write(`Error: ${err.message}
|
|
325
|
+
`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
process.stderr.write(`Unknown packs subcommand: ${sub ?? "(none)"}
|
|
330
|
+
Available: install, list, uninstall
|
|
331
|
+
`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
267
335
|
var arg = process.argv[2];
|
|
268
336
|
if (arg === "--help" || arg === "-h") {
|
|
269
337
|
process.stdout.write(HELP);
|
|
@@ -278,8 +346,12 @@ if (arg === "init") {
|
|
|
278
346
|
await runInit();
|
|
279
347
|
process.exit(0);
|
|
280
348
|
}
|
|
349
|
+
if (arg === "packs") {
|
|
350
|
+
await runPacks();
|
|
351
|
+
process.exit(0);
|
|
352
|
+
}
|
|
281
353
|
if (arg === "serve" || arg === void 0) {
|
|
282
|
-
const { runStdio } = await import("./server-
|
|
354
|
+
const { runStdio } = await import("./server-7JVOIBJV.js");
|
|
283
355
|
runStdio().catch((err) => {
|
|
284
356
|
console.error("Failed to start PLUR MCP server:", err);
|
|
285
357
|
process.exit(1);
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
|
-
import { Server } from "@modelcontextprotocol/
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ListResourcesRequestSchema,
|
|
8
|
-
ReadResourceRequestSchema,
|
|
9
|
-
ListPromptsRequestSchema,
|
|
10
|
-
GetPromptRequestSchema,
|
|
11
|
-
ErrorCode,
|
|
12
|
-
McpError
|
|
13
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
4
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { join as join2 } from "path";
|
|
6
|
+
import { homedir as homedir2 } from "os";
|
|
14
7
|
import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
|
|
15
8
|
|
|
16
9
|
// src/tools.ts
|
|
17
10
|
import { existsSync, unlinkSync } from "fs";
|
|
18
11
|
import { join } from "path";
|
|
19
12
|
import { homedir } from "os";
|
|
20
|
-
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName } from "@plur-ai/core";
|
|
13
|
+
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName, SUGGEST_DISPLAY_MIN_CONFIDENCE } from "@plur-ai/core";
|
|
21
14
|
|
|
22
15
|
// src/telemetry.ts
|
|
23
16
|
import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
|
|
@@ -31,7 +24,7 @@ function recordTelemetry(event) {
|
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
// src/version.ts
|
|
34
|
-
var VERSION = "0.
|
|
27
|
+
var VERSION = "0.15.0";
|
|
35
28
|
|
|
36
29
|
// src/tools.ts
|
|
37
30
|
import { z } from "zod";
|
|
@@ -179,6 +172,24 @@ mcpCanary.expect({
|
|
|
179
172
|
description: "Learning from corrections",
|
|
180
173
|
fix: "Call plur_learn when corrected. If using hooks, verify they are installed."
|
|
181
174
|
});
|
|
175
|
+
var _sessionTelemetry = /* @__PURE__ */ new Map();
|
|
176
|
+
var SESSION_TTL_MS = 8 * 60 * 60 * 1e3;
|
|
177
|
+
function _cleanExpiredSessions() {
|
|
178
|
+
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
179
|
+
for (const [id, state] of _sessionTelemetry) {
|
|
180
|
+
if (new Date(state.started_at).getTime() < cutoff) _sessionTelemetry.delete(id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
var _activeSessionId;
|
|
184
|
+
function _recordInjectionTelemetry(session_id, injected_packs) {
|
|
185
|
+
if (!session_id || !injected_packs) return;
|
|
186
|
+
const state = _sessionTelemetry.get(session_id);
|
|
187
|
+
if (!state) return;
|
|
188
|
+
state.injection_calls++;
|
|
189
|
+
for (const [pack, count] of Object.entries(injected_packs)) {
|
|
190
|
+
state.pack_counts[pack] = (state.pack_counts[pack] ?? 0) + count;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
182
193
|
var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
183
194
|
"plur_session_start",
|
|
184
195
|
"plur_session_end",
|
|
@@ -187,6 +198,7 @@ var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
187
198
|
"plur_feedback",
|
|
188
199
|
"plur_forget",
|
|
189
200
|
"plur_status",
|
|
201
|
+
"plur_receipt",
|
|
190
202
|
"plur_doctor",
|
|
191
203
|
"plur_packs_uninstall",
|
|
192
204
|
"plur_tensions_purge"
|
|
@@ -219,6 +231,13 @@ function buildAdminDispatchTool(all) {
|
|
|
219
231
|
if (!target) {
|
|
220
232
|
return { error: `Unknown action "${action}". Valid actions: ${adminActions.join(", ")}`, success: false, _isError: true };
|
|
221
233
|
}
|
|
234
|
+
if (target.annotations?.destructiveHint === true) {
|
|
235
|
+
return {
|
|
236
|
+
error: `"${action}" is a destructive operation and cannot be dispatched via plur_admin \u2014 call the ${action} tool directly (it is exposed in every profile) so your client sees its destructiveHint annotation.`,
|
|
237
|
+
success: false,
|
|
238
|
+
_isError: true
|
|
239
|
+
};
|
|
240
|
+
}
|
|
222
241
|
const innerArgs = args.args ?? {};
|
|
223
242
|
const validated = validateToolArgs(target, innerArgs);
|
|
224
243
|
if (!validated.ok) {
|
|
@@ -233,12 +252,20 @@ function buildAdminDispatchTool(all) {
|
|
|
233
252
|
}
|
|
234
253
|
};
|
|
235
254
|
}
|
|
236
|
-
function getToolDefinitions(profile = "
|
|
255
|
+
function getToolDefinitions(profile = "lean") {
|
|
237
256
|
const all = getAllToolDefinitions();
|
|
238
|
-
if (profile
|
|
257
|
+
if (profile === "full") return all;
|
|
239
258
|
const core = all.filter((t) => CURSOR_CORE_TOOL_NAMES.has(t.name));
|
|
240
259
|
return [...core, buildAdminDispatchTool(all)];
|
|
241
260
|
}
|
|
261
|
+
function receiptSummary(r) {
|
|
262
|
+
if (r.coverage.source === "none") {
|
|
263
|
+
return r.window.windowed ? `No retrievals recorded in the last ${r.window.requested_days} days.` : "No retrieval history yet \u2014 logging begins once memory is used.";
|
|
264
|
+
}
|
|
265
|
+
const since = r.window.windowed ? `the last ${r.window.requested_days} days` : `${r.coverage.complete_from}`;
|
|
266
|
+
const pct = Math.round(r.retrieved.activation_rate * 100);
|
|
267
|
+
return `Since ${since}, ${r.retrieved.taught_pairs} times a memory the user taught was retrieved into context (across ${r.retrieved.retrievals} retrievals in ${r.window.sessions} sessions; ${r.retrieved.engrams} distinct engrams). Activation ${pct}% is store COVERAGE over the logging window, not a quality score \u2014 it is expected to be low and to fall as more engrams are added.`;
|
|
268
|
+
}
|
|
242
269
|
function getAllToolDefinitions() {
|
|
243
270
|
return [
|
|
244
271
|
{
|
|
@@ -298,6 +325,18 @@ function getAllToolDefinitions() {
|
|
|
298
325
|
const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
|
|
299
326
|
return { scope_hint: `Stored at "${engramScope}" because no scope was passed, but a team store is configured (${scopes}). If this is team/engineering knowledge, re-learn it with an explicit scope so it reaches the shared store; keep genuinely personal notes at the default scope.` };
|
|
300
327
|
};
|
|
328
|
+
const domainHint = (wasRouted) => {
|
|
329
|
+
if (typeof args.domain === "string" && args.domain.length > 0) return {};
|
|
330
|
+
if (explicitScope || wasRouted) return {};
|
|
331
|
+
let coversScopes = [];
|
|
332
|
+
try {
|
|
333
|
+
coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
|
|
334
|
+
} catch {
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
if (coversScopes.length === 0) return {};
|
|
338
|
+
return { domain_hint: `No domain set \u2014 without a dotted domain this engram cannot auto-route to a covers-declaring scope (${coversScopes.join(", ")}) and is harder to re-scope later. Set domain on every plur_learn, shape "<org>.<team>.<area>" (e.g. "plur.engineering.mcp") \u2014 see the domain convention in CLAUDE.md.` };
|
|
339
|
+
};
|
|
301
340
|
const temporalEcho = (engram) => {
|
|
302
341
|
const extracted = engram.structured_data?._expiry_extracted;
|
|
303
342
|
return {
|
|
@@ -323,6 +362,7 @@ function getAllToolDefinitions() {
|
|
|
323
362
|
decision: "ADD",
|
|
324
363
|
...temporalEcho(engram),
|
|
325
364
|
...scopeHint(engram.scope, !!routed),
|
|
365
|
+
...domainHint(!!routed),
|
|
326
366
|
...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
327
367
|
...demoted ? { demoted: true, requested_scope: demoted.from, warning: `Sensitive content (${demoted.patterns}) detected \u2014 stored at "${demoted.to}"/private instead of the requested shared scope "${demoted.from}". If this is a false positive, re-scope deliberately.` } : {},
|
|
328
368
|
...routed ? { routed: { scope: routed.scope, confidence: routed.confidence, reason: routed.reason }, info: `No scope was provided; auto-routed to "${routed.scope}" (confidence ${routed.confidence}) because its content matched that scope's covers. Pass an explicit scope to override.` } : {}
|
|
@@ -341,12 +381,106 @@ function getAllToolDefinitions() {
|
|
|
341
381
|
decision: "ADD",
|
|
342
382
|
...temporalEcho(engram),
|
|
343
383
|
...scopeHint(engram.scope, !!routedFallback),
|
|
384
|
+
...domainHint(!!routedFallback),
|
|
344
385
|
...isOutbox ? { outbox: true } : {},
|
|
345
386
|
warning: `Remote write failed (${err.message}); engram queued for retry.`
|
|
346
387
|
};
|
|
347
388
|
}
|
|
348
389
|
}
|
|
349
390
|
},
|
|
391
|
+
{
|
|
392
|
+
name: "plur_learn_batch",
|
|
393
|
+
description: "Create many engrams in one call \u2014 the batch form of plur_learn. Accepts an array of engram objects and writes them sequentially through the SAME dedup + policy pipeline as plur_learn (content-hash NOOP \u2192 semantic recall \u2192 LLM ADD/UPDATE/MERGE decision). Dedup also applies WITHIN the batch: a statement duplicating an earlier item in the same array resolves to NOOP against it. Returns `ids` aligned 1:1 with the input array (ids[i] is the engram id for input i, or null if input i failed), the per-item decisions (each carrying its input_index), aggregate stats, and any per-item failures (each with its input index) \u2014 a single bad item does not abort the batch. Use this when an orchestration fans out and wants to persist consolidated findings without N separate calls. LLM dedup calls are capped (default 50, override with max_llm_calls) to bound bulk-import cost. Note: unlike plur_learn, batch items take the LOCAL learn path \u2014 remote-scope auto-routing (learnRouted) is not applied per item, so for shared/remote-store writes prefer plur_learn or pass an explicit local scope. See plur-ai/plur#281.",
|
|
394
|
+
annotations: { title: "Learn (batch)", destructiveHint: false, idempotentHint: false },
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: {
|
|
398
|
+
engrams: {
|
|
399
|
+
type: "array",
|
|
400
|
+
description: "Engram objects to persist. Each requires `statement`; the other fields mirror plur_learn.",
|
|
401
|
+
items: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
statement: { type: "string", description: "The knowledge assertion to store" },
|
|
405
|
+
type: { type: "string", enum: ["behavioral", "terminological", "procedural", "architectural"], description: "Category of the engram" },
|
|
406
|
+
scope: { type: "string", description: "Namespace, e.g. global, project:myapp" },
|
|
407
|
+
domain: { type: "string", description: "Domain tag, e.g. software.deployment" },
|
|
408
|
+
tags: { type: "array", items: { type: "string" }, description: "Searchable keyword tags \u2014 contribute to BM25/embedding recall" },
|
|
409
|
+
rationale: { type: "string", description: "Why this knowledge matters \u2014 also enters the search corpus" },
|
|
410
|
+
source: { type: "string", description: "Origin of this knowledge (URL, conversation ref, etc.)" },
|
|
411
|
+
pinned: { type: "boolean", description: "Always-load flag. Use sparingly: meta-rules, safety conventions, core principles." },
|
|
412
|
+
commitment: { type: "string", enum: ["exploring", "leaning", "decided", "locked"], description: "How firmly the user has committed (default: leaning)" },
|
|
413
|
+
valid_from: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge becomes valid" },
|
|
414
|
+
valid_until: { type: "string", description: "ISO date (YYYY-MM-DD) the knowledge expires" }
|
|
415
|
+
},
|
|
416
|
+
required: ["statement"]
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
max_llm_calls: { type: "number", description: "Max LLM dedup calls across the whole batch (default 50). Once spent, remaining items use the cheap hash/cosine path. Pass a large number to opt out." }
|
|
420
|
+
},
|
|
421
|
+
required: ["engrams"]
|
|
422
|
+
},
|
|
423
|
+
handler: async (args, plur) => {
|
|
424
|
+
const llm = getLlmFunction();
|
|
425
|
+
const raw = Array.isArray(args.engrams) ? args.engrams : [];
|
|
426
|
+
if (raw.length === 0) {
|
|
427
|
+
return { ids: [], results: [], stats: { added: 0, updated: 0, merged: 0, noops: 0, failed: 0 }, failures: [], warning: "No engrams provided \u2014 pass a non-empty `engrams` array." };
|
|
428
|
+
}
|
|
429
|
+
const items = raw.map((e) => ({
|
|
430
|
+
statement: sanitizeStatement(e.statement),
|
|
431
|
+
context: {
|
|
432
|
+
type: e.type,
|
|
433
|
+
scope: e.scope,
|
|
434
|
+
domain: e.domain,
|
|
435
|
+
source: e.source,
|
|
436
|
+
tags: e.tags,
|
|
437
|
+
rationale: e.rationale,
|
|
438
|
+
commitment: e.commitment,
|
|
439
|
+
pinned: e.pinned,
|
|
440
|
+
valid_from: e.valid_from,
|
|
441
|
+
valid_until: e.valid_until
|
|
442
|
+
}
|
|
443
|
+
}));
|
|
444
|
+
const maxLlmCalls = typeof args.max_llm_calls === "number" ? args.max_llm_calls : void 0;
|
|
445
|
+
const { results, stats, failures } = await plur.learnBatch(
|
|
446
|
+
items,
|
|
447
|
+
llm,
|
|
448
|
+
maxLlmCalls !== void 0 ? { maxLlmCalls } : void 0
|
|
449
|
+
);
|
|
450
|
+
mcpCanary.signal("learn_activity");
|
|
451
|
+
recordTelemetry("learn");
|
|
452
|
+
const ids = raw.map(() => null);
|
|
453
|
+
for (const r of results) {
|
|
454
|
+
if (r.input_index !== void 0) ids[r.input_index] = r.engram.id;
|
|
455
|
+
}
|
|
456
|
+
let batchDomainHint = {};
|
|
457
|
+
const noDomainCount = raw.filter((e) => !(typeof e.domain === "string" && e.domain.length > 0) && !(typeof e.scope === "string" && e.scope.length > 0)).length;
|
|
458
|
+
if (noDomainCount > 0) {
|
|
459
|
+
try {
|
|
460
|
+
const coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
|
|
461
|
+
if (coversScopes.length > 0) {
|
|
462
|
+
batchDomainHint = { domain_hint: `${noDomainCount} of ${raw.length} item(s) had no domain and no explicit scope \u2014 they cannot auto-route to a covers-declaring scope (${coversScopes.join(", ")}) and are harder to re-scope later. Set domain on every item, shape "<org>.<team>.<area>" \u2014 see the domain convention in CLAUDE.md.` };
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
ids,
|
|
469
|
+
results: results.map((r) => ({
|
|
470
|
+
input_index: r.input_index,
|
|
471
|
+
id: r.engram.id,
|
|
472
|
+
statement: r.engram.statement,
|
|
473
|
+
scope: r.engram.scope,
|
|
474
|
+
type: r.engram.type,
|
|
475
|
+
decision: r.decision,
|
|
476
|
+
...r.existing_id ? { existing_id: r.existing_id } : {}
|
|
477
|
+
})),
|
|
478
|
+
stats,
|
|
479
|
+
...batchDomainHint,
|
|
480
|
+
...failures.length > 0 ? { failures, warning: `${failures.length} of ${raw.length} engram(s) failed to persist; the rest were written.` } : {}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
},
|
|
350
484
|
{
|
|
351
485
|
name: "plur_recall",
|
|
352
486
|
description: "Query engrams by BM25 keyword matching \u2014 use plur_recall_hybrid for semantic similarity. Note: a project-scope filter also returns personal-family engrams (local, global, user:*, agent:*); an explicit scope=global recall returns ALL personal-family engrams \u2014 wider than scope=global INJECT, which is targeted to the global namespace only.",
|
|
@@ -370,14 +504,18 @@ function getAllToolDefinitions() {
|
|
|
370
504
|
limit: args.limit
|
|
371
505
|
});
|
|
372
506
|
return {
|
|
373
|
-
results: results.map((e) =>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
507
|
+
results: results.map((e) => {
|
|
508
|
+
const supersededBy = e.relations?.superseded_by;
|
|
509
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
510
|
+
return {
|
|
511
|
+
id: e.id,
|
|
512
|
+
statement: e.statement + annotation,
|
|
513
|
+
type: e.type,
|
|
514
|
+
scope: e.scope,
|
|
515
|
+
domain: e.domain,
|
|
516
|
+
retrieval_strength: e.activation.retrieval_strength
|
|
517
|
+
};
|
|
518
|
+
}),
|
|
381
519
|
count: results.length
|
|
382
520
|
};
|
|
383
521
|
}
|
|
@@ -433,9 +571,11 @@ function getAllToolDefinitions() {
|
|
|
433
571
|
const response = {
|
|
434
572
|
results: boundedResults.map((e) => {
|
|
435
573
|
const raw = e;
|
|
574
|
+
const supersededBy = e.relations?.superseded_by;
|
|
575
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
436
576
|
const base = {
|
|
437
577
|
id: e.id,
|
|
438
|
-
statement: e.statement,
|
|
578
|
+
statement: e.statement + annotation,
|
|
439
579
|
type: e.type,
|
|
440
580
|
scope: e.scope,
|
|
441
581
|
domain: e.domain,
|
|
@@ -481,8 +621,11 @@ function getAllToolDefinitions() {
|
|
|
481
621
|
handler: async (args, plur) => {
|
|
482
622
|
const result = plur.inject(args.task, {
|
|
483
623
|
budget: args.budget,
|
|
484
|
-
scope: args.scope
|
|
624
|
+
scope: args.scope,
|
|
625
|
+
source: "inject",
|
|
626
|
+
session_id: _activeSessionId
|
|
485
627
|
});
|
|
628
|
+
_recordInjectionTelemetry(_activeSessionId, result.injected_packs);
|
|
486
629
|
return {
|
|
487
630
|
directives: result.directives,
|
|
488
631
|
consider: result.consider,
|
|
@@ -510,8 +653,11 @@ function getAllToolDefinitions() {
|
|
|
510
653
|
handler: async (args, plur) => {
|
|
511
654
|
const result = await plur.injectHybrid(args.task, {
|
|
512
655
|
budget: args.budget,
|
|
513
|
-
scope: args.scope
|
|
656
|
+
scope: args.scope,
|
|
657
|
+
source: "inject",
|
|
658
|
+
session_id: _activeSessionId
|
|
514
659
|
});
|
|
660
|
+
_recordInjectionTelemetry(_activeSessionId, result.injected_packs);
|
|
515
661
|
return {
|
|
516
662
|
directives: result.directives,
|
|
517
663
|
consider: result.consider,
|
|
@@ -860,11 +1006,19 @@ function getAllToolDefinitions() {
|
|
|
860
1006
|
full: {
|
|
861
1007
|
type: "boolean",
|
|
862
1008
|
description: "Full reindex: drop the derived index (PGLite/SQLite) and rebuild from YAML. YAML is never modified. Use to recover from an out-of-sync index."
|
|
1009
|
+
},
|
|
1010
|
+
remote_type: {
|
|
1011
|
+
type: "string",
|
|
1012
|
+
enum: ["personal", "shared"],
|
|
1013
|
+
description: "What the sync remote is for (#640). personal (default): mirror everything non-local, private included \u2014 a solo user's own backup. shared: push ONLY shared-scope, non-private engrams \u2014 personal-family and private engrams never reach the remote. Persist the choice in config.yaml as sync.remote_type instead of passing it per call."
|
|
863
1014
|
}
|
|
864
1015
|
}
|
|
865
1016
|
},
|
|
866
1017
|
handler: async (args, plur) => {
|
|
867
|
-
const result = plur.sync(args.remote, {
|
|
1018
|
+
const result = plur.sync(args.remote, {
|
|
1019
|
+
full: args.full === true,
|
|
1020
|
+
...args.remote_type === "personal" || args.remote_type === "shared" ? { remoteType: args.remote_type } : {}
|
|
1021
|
+
});
|
|
868
1022
|
await plur.waitForIndex();
|
|
869
1023
|
const indexError = plur.lastIndexError();
|
|
870
1024
|
let outbox_result;
|
|
@@ -1050,14 +1204,20 @@ function getAllToolDefinitions() {
|
|
|
1050
1204
|
},
|
|
1051
1205
|
{
|
|
1052
1206
|
name: "plur_status",
|
|
1053
|
-
description: "Return system health \u2014 running version, engram count, episode count, pack count, storage root",
|
|
1207
|
+
description: "Return system health \u2014 running version, engram count, episode count, pack count, storage root. Optionally filter engram counts by domain prefix and/or creation date.",
|
|
1054
1208
|
annotations: { title: "Status", readOnlyHint: true, idempotentHint: true },
|
|
1055
1209
|
inputSchema: {
|
|
1056
1210
|
type: "object",
|
|
1057
|
-
properties: {
|
|
1211
|
+
properties: {
|
|
1212
|
+
domain: { type: "string", description: 'Only count engrams whose domain starts with this prefix (e.g. "meridian")' },
|
|
1213
|
+
created_after: { type: "string", description: "ISO-8601 date (YYYY-MM-DD). Only count engrams learned on or after this date." }
|
|
1214
|
+
}
|
|
1058
1215
|
},
|
|
1059
|
-
handler: async (
|
|
1060
|
-
const status = plur.status(
|
|
1216
|
+
handler: async (args, plur) => {
|
|
1217
|
+
const status = plur.status({
|
|
1218
|
+
domain: args.domain,
|
|
1219
|
+
created_after: args.created_after
|
|
1220
|
+
});
|
|
1061
1221
|
const versionCheck = getCachedUpdateCheck("@plur-ai/mcp");
|
|
1062
1222
|
return {
|
|
1063
1223
|
version: VERSION,
|
|
@@ -1090,6 +1250,22 @@ function getAllToolDefinitions() {
|
|
|
1090
1250
|
};
|
|
1091
1251
|
}
|
|
1092
1252
|
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "plur_receipt",
|
|
1255
|
+
description: 'Counted report of what your memory retrieved for you: engrams stored, how many were retrieved and how often, which are most relied on, and how much of the store is dormant. Local and read-only; every figure is directly counted, never estimated. IMPORTANT when relaying to the user: `activation_rate` is COVERAGE over the logging window (\u2248 how much of the store was surfaced), NOT a quality or effectiveness score \u2014 it is naturally low and FALLS as more engrams are added, so never present it as "memory is N% effective". A `summary` line is included; prefer relaying that.',
|
|
1256
|
+
annotations: { title: "Memory receipt", readOnlyHint: true, idempotentHint: true },
|
|
1257
|
+
inputSchema: {
|
|
1258
|
+
type: "object",
|
|
1259
|
+
properties: {
|
|
1260
|
+
days: { type: "number", description: "Restrict to the last N days (integer). Omit for all recorded history." }
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
handler: async (args, plur) => {
|
|
1264
|
+
const days = typeof args.days === "number" && Number.isFinite(args.days) && args.days >= 1 ? Math.floor(args.days) : void 0;
|
|
1265
|
+
const receipt = plur.receipt(days ? { days } : void 0);
|
|
1266
|
+
return { summary: receiptSummary(receipt), ...receipt };
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1093
1269
|
{
|
|
1094
1270
|
name: "plur_doctor",
|
|
1095
1271
|
description: 'Diagnose the PLUR ENGINE (embedder, hybrid search, remote-store auth) \u2014 not hook/MCP wiring. Reports whether the embedding model loaded, whether hybrid search is fully operational, and \u2014 for any configured enterprise/remote store \u2014 whether its auth is valid (probes /api/v1/me and decodes token expiry), so a dead or soon-to-expire token surfaces instead of hiding behind a "healthy" report. Run this first when recall feels off or team engrams stop syncing. Does NOT check .cursor/mcp.json, .cursor/hooks.json, or the live MCP tool count \u2014 for that, run the `plur doctor` CLI command in a terminal (a different, more thorough check with the same name).',
|
|
@@ -1184,6 +1360,23 @@ function getAllToolDefinitions() {
|
|
|
1184
1360
|
}
|
|
1185
1361
|
}
|
|
1186
1362
|
checks.push({ check: "reranker available", ok: rerankerOk, detail: rerankerDetail });
|
|
1363
|
+
if (rerankerOk) {
|
|
1364
|
+
try {
|
|
1365
|
+
const fitResult = await plur.checkRerankerFit({ rerankerName });
|
|
1366
|
+
const sep = fitResult.separability.toFixed(3);
|
|
1367
|
+
checks.push({
|
|
1368
|
+
check: "reranker domain fit",
|
|
1369
|
+
ok: fitResult.fit,
|
|
1370
|
+
detail: fitResult.n_pairs === 0 ? "Not enough engrams to evaluate fit (< 2) \u2014 assuming fit" : fitResult.fit ? `Good fit \u2014 separability ${sep} on ${fitResult.n_pairs} pairs (threshold \u2265 0.05)` : `Poor fit \u2014 separability ${sep} on ${fitResult.n_pairs} pairs (threshold \u2265 0.05). Reranker may be net-negative on this store's domain mix.`
|
|
1371
|
+
});
|
|
1372
|
+
if (!fitResult.fit && fitResult.n_pairs > 0) {
|
|
1373
|
+
remediation.push(
|
|
1374
|
+
`Reranker "${rerankerName}" shows poor separability (${sep}) on this store's engrams \u2014 it may be scoring irrelevant pairs higher than relevant ones. Consider unsetting PLUR_RERANKER or switching to a different tier. The fit check compares same-domain vs cross-domain pair scores; low separability means the model lacks signal on your content.`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1187
1380
|
try {
|
|
1188
1381
|
let evalStatus;
|
|
1189
1382
|
let freshlyRun = false;
|
|
@@ -1286,6 +1479,13 @@ function getAllToolDefinitions() {
|
|
|
1286
1479
|
const session_id = crypto.randomUUID();
|
|
1287
1480
|
const task = args.task;
|
|
1288
1481
|
const tags = args.tags;
|
|
1482
|
+
_cleanExpiredSessions();
|
|
1483
|
+
_activeSessionId = session_id;
|
|
1484
|
+
_sessionTelemetry.set(session_id, {
|
|
1485
|
+
pack_counts: {},
|
|
1486
|
+
injection_calls: 0,
|
|
1487
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
+
});
|
|
1289
1489
|
let outbox_result;
|
|
1290
1490
|
try {
|
|
1291
1491
|
outbox_result = await plur.flushOutbox();
|
|
@@ -1316,9 +1516,11 @@ function getAllToolDefinitions() {
|
|
|
1316
1516
|
try {
|
|
1317
1517
|
const result = await plur.injectHybrid(task, {
|
|
1318
1518
|
scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
|
|
1319
|
-
session_id
|
|
1519
|
+
session_id,
|
|
1320
1520
|
// stamped on the co_injection provenance event (#452)
|
|
1521
|
+
source: "session_start"
|
|
1321
1522
|
});
|
|
1523
|
+
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1322
1524
|
if (result.count > 0) {
|
|
1323
1525
|
const lines = [];
|
|
1324
1526
|
if (result.directives) lines.push("## DIRECTIVES\n", result.directives);
|
|
@@ -1329,8 +1531,10 @@ function getAllToolDefinitions() {
|
|
|
1329
1531
|
} catch {
|
|
1330
1532
|
const result = plur.inject(task, {
|
|
1331
1533
|
scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
|
|
1332
|
-
session_id
|
|
1534
|
+
session_id,
|
|
1535
|
+
source: "session_start"
|
|
1333
1536
|
});
|
|
1537
|
+
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1334
1538
|
if (result.count > 0) {
|
|
1335
1539
|
const lines = [];
|
|
1336
1540
|
if (result.directives) lines.push("## DIRECTIVES\n", result.directives);
|
|
@@ -1370,7 +1574,7 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
|
|
|
1370
1574
|
} else if (scope_source === "none") {
|
|
1371
1575
|
guide += `
|
|
1372
1576
|
|
|
1373
|
-
\u26A0\uFE0F No project scope detected. plur_learn calls without explicit scope
|
|
1577
|
+
\u26A0\uFE0F No project scope detected. plur_learn calls without explicit scope may AUTO-ROUTE to a registered team scope whose covers confidently match the engram's domain/tags (the response reports \`routed\` when that happens); otherwise they land at the unscoped default "global" and will appear in EVERY project's future sessions. Create a .plur.yaml NOW to prevent this: scope: "project:<your-project-name>". (This is every project's PERSONAL recall context, NOT team shared stores \u2014 use an explicit shared scope like project:/group: to reach a team store.) Note: an explicit scope=global RECALL surfaces all your personal engrams, but scope=global INJECT is targeted to the global namespace only \u2014 don't be surprised if a local engram a global recall finds is absent from a global inject.`;
|
|
1374
1578
|
}
|
|
1375
1579
|
if (remote_scopes.length > 0) {
|
|
1376
1580
|
const safe = (x) => String(x ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
@@ -1385,9 +1589,10 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
|
|
|
1385
1589
|
|
|
1386
1590
|
Session default scope is set to "${default_scope}". To route an engram to a remote enterprise store instead, pass scope explicitly to plur_learn (available remote scopes: ${scopeList}).` : `
|
|
1387
1591
|
|
|
1388
|
-
Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: when an engram is relevant to the team (engineering patterns, architecture decisions, project conventions), set scope to the matching remote scope in plur_learn. Personal preferences, local project details, and corrections specific to your workflow can be left unscoped (
|
|
1592
|
+
Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: when an engram is relevant to the team (engineering patterns, architecture decisions, project conventions), set scope to the matching remote scope in plur_learn. Personal preferences, local project details, and corrections specific to your workflow can be left unscoped \u2014 but note an unscoped write whose domain/tags confidently match a team scope's covers AUTO-ROUTES to that shared team store (the response reports \`routed\` when that happens); otherwise it lands at the unscoped default, "global" \u2014 the cross-project personal namespace. Do NOT rely on auto-routing for TEAM knowledge \u2014 set the matching scope explicitly; a weak or absent covers match falls back to "global" and never reaches the shared store.`;
|
|
1389
1593
|
try {
|
|
1390
1594
|
const discoveries = await plur.discoverRemoteScopes({ timeoutMs: 3e3 });
|
|
1595
|
+
plur.persistScopeMetadata(discoveries);
|
|
1391
1596
|
const failures = discoveries.filter((d) => !d.ok);
|
|
1392
1597
|
if (failures.length > 0) {
|
|
1393
1598
|
const authExpired = failures.some((f) => /\b40[13]\b/.test(f.error ?? ""));
|
|
@@ -1399,12 +1604,11 @@ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: wh
|
|
|
1399
1604
|
|
|
1400
1605
|
\u26A0\uFE0F ENTERPRISE STORE UNREACHABLE: ${urls}. Reads fall back to local; team-scoped writes queue in the outbox` + (pending > 0 ? ` (${pending} pending)` : "") + ` until it recovers. Check connectivity/VPN.`;
|
|
1401
1606
|
}
|
|
1402
|
-
const
|
|
1403
|
-
if (
|
|
1404
|
-
const list = unregistered.map((s) => `"${safe(s)}"`).join(", ");
|
|
1607
|
+
const offerable = [...new Set(discoveries.filter((d) => d.ok).flatMap((d) => d.unregistered))].filter(isSharedScope);
|
|
1608
|
+
if (offerable.length > 0) {
|
|
1405
1609
|
guide += `
|
|
1406
1610
|
|
|
1407
|
-
\u{1F50E}
|
|
1611
|
+
\u{1F50E} ${offerable.length} authorized scope(s) not yet registered. Tell the user they can run \`plur scopes\` to register or dismiss them per-scope (dismissed scopes stop being offered; \`plur scopes --reoffer\` re-surfaces them).`;
|
|
1408
1612
|
}
|
|
1409
1613
|
} catch {
|
|
1410
1614
|
}
|
|
@@ -1516,6 +1720,16 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1516
1720
|
session_id,
|
|
1517
1721
|
channel: "mcp"
|
|
1518
1722
|
});
|
|
1723
|
+
const telemetry = session_id ? _sessionTelemetry.get(session_id) : void 0;
|
|
1724
|
+
const injection_summary = telemetry && telemetry.injection_calls > 0 ? {
|
|
1725
|
+
pack_counts: { ...telemetry.pack_counts },
|
|
1726
|
+
total_injections: telemetry.injection_calls,
|
|
1727
|
+
session_duration_ms: Date.now() - new Date(telemetry.started_at).getTime()
|
|
1728
|
+
} : void 0;
|
|
1729
|
+
if (session_id) {
|
|
1730
|
+
_sessionTelemetry.delete(session_id);
|
|
1731
|
+
if (_activeSessionId === session_id) _activeSessionId = void 0;
|
|
1732
|
+
}
|
|
1519
1733
|
try {
|
|
1520
1734
|
const plurDir = process.env.PLUR_PATH ?? join(homedir(), ".plur");
|
|
1521
1735
|
const sessionsDir = join(plurDir, "sessions");
|
|
@@ -1534,6 +1748,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1534
1748
|
engrams_created,
|
|
1535
1749
|
episode_id: episode.id,
|
|
1536
1750
|
total_engrams: status.engram_count,
|
|
1751
|
+
...injection_summary ? { injection_summary } : {},
|
|
1537
1752
|
hint: engrams_created === 0 ? "No engrams captured this session. If any corrections, preferences, or patterns came up, consider calling plur_learn before ending." : void 0
|
|
1538
1753
|
};
|
|
1539
1754
|
}
|
|
@@ -1600,29 +1815,33 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1600
1815
|
},
|
|
1601
1816
|
{
|
|
1602
1817
|
name: "plur_suggest_scope",
|
|
1603
|
-
description: 'Suggest which registered scope(s) an engram belongs in, ranked by fit. Deterministic \u2014 no LLM, no network. Scores the statement keywords, optional domain (a dotted namespace like "plur.core.security"), and tags against the covers[] each scope declares. ADVISORY ONLY: this does not route or store anything; pass the chosen scope to plur_learn yourself. Returns candidates sorted by confidence (empty when nothing matches).',
|
|
1818
|
+
description: 'Suggest which registered scope(s) an engram belongs in, ranked by fit. Deterministic \u2014 no LLM, no network. Scores the statement keywords, optional domain (a dotted namespace like "plur.core.security"), and tags against the covers[] each scope declares. ADVISORY ONLY: this does not route or store anything; pass the chosen scope to plur_learn yourself. Returns candidates sorted by confidence (empty when nothing matches). Candidates below min_confidence (default: scope_routing.min_confidence config, else 0.15) are suppressed \u2014 a lone coincidental keyword scores \u22480.12 and is noise, not signal (#670); pass min_confidence: 0 to see every scored candidate.',
|
|
1604
1819
|
annotations: { title: "Suggest scope", readOnlyHint: true, idempotentHint: true },
|
|
1605
1820
|
inputSchema: {
|
|
1606
1821
|
type: "object",
|
|
1607
1822
|
properties: {
|
|
1608
1823
|
statement: { type: "string", description: "The engram statement to route" },
|
|
1609
1824
|
domain: { type: "string", description: 'Optional dotted namespace for the engram (e.g. "plur.core.security") \u2014 strongest routing signal' },
|
|
1610
|
-
tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" }
|
|
1825
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" },
|
|
1826
|
+
min_confidence: { type: "number", minimum: 0, maximum: 1, description: "Suppress candidates below this confidence (0-1; out-of-range values are clamped). Default: scope_routing.min_confidence from config, else 0.15 \u2014 clips lone-keyword noise (\u22480.12) while keeping real multi-signal matches. Pass 0 for the unfiltered list." }
|
|
1611
1827
|
},
|
|
1612
1828
|
required: ["statement"]
|
|
1613
1829
|
},
|
|
1614
1830
|
handler: async (args, plur) => {
|
|
1831
|
+
const raw = args.min_confidence;
|
|
1832
|
+
const explicit = typeof raw === "number" && Number.isFinite(raw) ? Math.min(1, Math.max(0, raw)) : void 0;
|
|
1833
|
+
const minConfidence = explicit ?? plur.getScopeRoutingConfig().min_confidence ?? SUGGEST_DISPLAY_MIN_CONFIDENCE;
|
|
1615
1834
|
const candidates = plur.suggestScope({
|
|
1616
1835
|
statement: args.statement,
|
|
1617
1836
|
domain: args.domain,
|
|
1618
1837
|
tags: args.tags
|
|
1619
|
-
});
|
|
1620
|
-
return { candidates, count: candidates.length };
|
|
1838
|
+
}, { minConfidence });
|
|
1839
|
+
return { candidates, count: candidates.length, min_confidence: minConfidence };
|
|
1621
1840
|
}
|
|
1622
1841
|
},
|
|
1623
1842
|
{
|
|
1624
1843
|
name: "plur_scopes_discover",
|
|
1625
|
-
description: "Discover which scopes your remote token is authorized for via the enterprise server (GET /api/v1/me), and which of those are not yet registered locally. Read-only by default; pass register:true to register all authorized-but-unregistered scopes in one step. Only shared-family scopes (group:/project:/space:/team:/org:/public) are auto-registered \u2014 personal-family scopes (global/local/user:*/agent:*) advertised by /me are skipped and surfaced in the result. Use this when you have access to multiple team scopes on one server.",
|
|
1844
|
+
description: "Discover which scopes your remote token is authorized for via the enterprise server (GET /api/v1/me), and which of those are not yet registered locally. Read-only by default; pass register:true to register all authorized-but-unregistered scopes in one step. Only shared-family scopes (group:/project:/space:/team:/org:/public) are auto-registered \u2014 personal-family scopes (global/local/user:*/agent:*) advertised by /me are skipped and surfaced in the result, and scopes the user has dismissed are respected (NOT registered by the batch path; register one individually via the CLI `plur scopes register <scope>` to override, which also clears the dismissal). Use this when you have access to multiple team scopes on one server.",
|
|
1626
1845
|
annotations: { title: "Discover scopes", readOnlyHint: false, idempotentHint: true },
|
|
1627
1846
|
inputSchema: {
|
|
1628
1847
|
type: "object",
|
|
@@ -1638,11 +1857,27 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1638
1857
|
if (discoveries.length === 0) {
|
|
1639
1858
|
return { discovered: [], note: "No remote stores configured. Register one scope first with plur_stores_add, then discover the rest." };
|
|
1640
1859
|
}
|
|
1860
|
+
const enrich = (d) => {
|
|
1861
|
+
if (!d.ok) return d;
|
|
1862
|
+
const byScope = new Map(d.metadata.map((m) => [m.scope, m]));
|
|
1863
|
+
const registeredSet = new Set(d.registered);
|
|
1864
|
+
const scopes = d.authorized.map((scope) => {
|
|
1865
|
+
const m = byScope.get(scope);
|
|
1866
|
+
return {
|
|
1867
|
+
scope,
|
|
1868
|
+
registered: registeredSet.has(scope),
|
|
1869
|
+
...m?.description ? { description: m.description } : {},
|
|
1870
|
+
...m && m.covers.length ? { covers: m.covers } : {}
|
|
1871
|
+
};
|
|
1872
|
+
});
|
|
1873
|
+
return { ...d, scopes };
|
|
1874
|
+
};
|
|
1875
|
+
const discovered = discoveries.map(enrich);
|
|
1641
1876
|
if (!register) {
|
|
1642
|
-
return { discovered
|
|
1877
|
+
return { discovered };
|
|
1643
1878
|
}
|
|
1644
1879
|
const registered = await plur.registerDiscoveredScopes({ url });
|
|
1645
|
-
return { discovered
|
|
1880
|
+
return { discovered, registered };
|
|
1646
1881
|
}
|
|
1647
1882
|
},
|
|
1648
1883
|
{
|
|
@@ -1953,9 +2188,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1953
2188
|
if (filterType) {
|
|
1954
2189
|
engrams = engrams.filter((e) => e.type === filterType);
|
|
1955
2190
|
}
|
|
1956
|
-
const { homedir:
|
|
1957
|
-
const { join:
|
|
1958
|
-
const outputDir = args.output_dir ||
|
|
2191
|
+
const { homedir: homedir3 } = await import("os");
|
|
2192
|
+
const { join: join3 } = await import("path");
|
|
2193
|
+
const outputDir = args.output_dir || join3(homedir3(), "plur-packs", name);
|
|
1959
2194
|
const result = plur.exportPack(engrams, outputDir, {
|
|
1960
2195
|
name,
|
|
1961
2196
|
version: "1.0.0",
|
|
@@ -2005,23 +2240,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2005
2240
|
};
|
|
2006
2241
|
}
|
|
2007
2242
|
},
|
|
2008
|
-
{
|
|
2009
|
-
name: "plur_batch_decay",
|
|
2010
|
-
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.",
|
|
2011
|
-
annotations: { title: "Batch decay", destructiveHint: false, idempotentHint: false },
|
|
2012
|
-
inputSchema: {
|
|
2013
|
-
type: "object",
|
|
2014
|
-
properties: {
|
|
2015
|
-
context_scope: { type: "string", description: "Scope to skip during decay (engrams in active scope are not decayed)" }
|
|
2016
|
-
}
|
|
2017
|
-
},
|
|
2018
|
-
handler: async (args, plur) => {
|
|
2019
|
-
const result = plur.batchDecay({
|
|
2020
|
-
contextScope: args.context_scope
|
|
2021
|
-
});
|
|
2022
|
-
return result;
|
|
2023
|
-
}
|
|
2024
|
-
},
|
|
2025
2243
|
{
|
|
2026
2244
|
name: "plur_profile",
|
|
2027
2245
|
description: "Generate or retrieve a cognitive profile \u2014 a narrative summary synthesized from stored engrams. Cached for 24h.",
|
|
@@ -2059,10 +2277,34 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2059
2277
|
}
|
|
2060
2278
|
|
|
2061
2279
|
// src/server.ts
|
|
2280
|
+
function serverPidPath(baseDir) {
|
|
2281
|
+
return join2(baseDir ?? join2(homedir2(), ".plur"), "server.pid");
|
|
2282
|
+
}
|
|
2283
|
+
function readEnterpriseToken(baseDir) {
|
|
2284
|
+
const configPath = join2(baseDir ?? join2(homedir2(), ".plur"), "config.json");
|
|
2285
|
+
if (!existsSync2(configPath)) return void 0;
|
|
2286
|
+
try {
|
|
2287
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2288
|
+
const ent = cfg?.enterprise;
|
|
2289
|
+
if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0;
|
|
2290
|
+
return { url: ent.url, token: ent.token, username: ent.username };
|
|
2291
|
+
} catch {
|
|
2292
|
+
return void 0;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
var _pendingReload = false;
|
|
2296
|
+
function isPendingReload() {
|
|
2297
|
+
return _pendingReload;
|
|
2298
|
+
}
|
|
2299
|
+
function clearPendingReload() {
|
|
2300
|
+
_pendingReload = false;
|
|
2301
|
+
}
|
|
2062
2302
|
var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
|
|
2063
2303
|
|
|
2064
2304
|
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.
|
|
2065
2305
|
|
|
2306
|
+
TOOL PROFILE: by default only the core session tools are exposed directly (lean profile). Every other plur_* operation is reachable via plur_admin: { action: "<tool name>", args: {...} } \u2014 same arguments and validation as a direct call. PLUR_TOOL_PROFILE=full exposes everything directly.
|
|
2307
|
+
|
|
2066
2308
|
SESSION LIFECYCLE:
|
|
2067
2309
|
- With hooks installed (plur init): engrams are injected automatically on first message. You do NOT need to call plur_session_start \u2014 it happens via hooks. Just call plur_session_end before the conversation ends.
|
|
2068
2310
|
- Without hooks: call plur_session_start at the start, plur_session_end at the end.
|
|
@@ -2117,6 +2359,7 @@ Persistent memory for AI agents. Corrections, preferences, and conventions are s
|
|
|
2117
2359
|
| A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" |
|
|
2118
2360
|
| User says "forget X" or a memory is outdated | \`plur_forget\` |
|
|
2119
2361
|
| You need to check what's stored | \`plur_status\` or \`plur_packs_list\` |
|
|
2362
|
+
| User asks what memory did for them / is memory working | \`plur_receipt\` (relay its \`summary\`; activation_rate is coverage, not quality) |
|
|
2120
2363
|
| End of session | \`plur_session_end\` with summary and suggestions |
|
|
2121
2364
|
|
|
2122
2365
|
## Tool Categories
|
|
@@ -2157,6 +2400,7 @@ Persistent memory for AI agents. Corrections, preferences, and conventions are s
|
|
|
2157
2400
|
- **plur_sync** \u2014 sync engrams across devices via git
|
|
2158
2401
|
- **plur_sync_status** \u2014 check sync state
|
|
2159
2402
|
- **plur_status** \u2014 system health
|
|
2403
|
+
- **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only)
|
|
2160
2404
|
|
|
2161
2405
|
## Scoping
|
|
2162
2406
|
|
|
@@ -2178,7 +2422,7 @@ Override with \`PLUR_PATH\` environment variable.
|
|
|
2178
2422
|
`;
|
|
2179
2423
|
async function createServer(plur, options) {
|
|
2180
2424
|
const instance = plur ?? new Plur2();
|
|
2181
|
-
const tools = getToolDefinitions(options?.profile ?? "
|
|
2425
|
+
const tools = getToolDefinitions(options?.profile ?? "lean");
|
|
2182
2426
|
checkForUpdate("@plur-ai/mcp", VERSION, (r) => {
|
|
2183
2427
|
if (r.updateAvailable) {
|
|
2184
2428
|
console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`);
|
|
@@ -2196,7 +2440,7 @@ async function createServer(plur, options) {
|
|
|
2196
2440
|
instructions: INSTRUCTIONS
|
|
2197
2441
|
}
|
|
2198
2442
|
);
|
|
2199
|
-
server.setRequestHandler(
|
|
2443
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
2200
2444
|
tools: tools.map((t) => ({
|
|
2201
2445
|
name: t.name,
|
|
2202
2446
|
description: t.description,
|
|
@@ -2204,9 +2448,20 @@ async function createServer(plur, options) {
|
|
|
2204
2448
|
...t.annotations && { annotations: t.annotations }
|
|
2205
2449
|
}))
|
|
2206
2450
|
}));
|
|
2207
|
-
server.setRequestHandler(
|
|
2451
|
+
server.setRequestHandler("tools/call", async (request) => {
|
|
2208
2452
|
const tool = tools.find((t) => t.name === request.params.name);
|
|
2209
2453
|
if (!tool) {
|
|
2454
|
+
const hidden = getToolDefinitions("full").find((t) => t.name === request.params.name);
|
|
2455
|
+
if (hidden) {
|
|
2456
|
+
return {
|
|
2457
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2458
|
+
error: `Tool "${request.params.name}" exists but is not directly callable under the current tool profile.`,
|
|
2459
|
+
success: false,
|
|
2460
|
+
hint: `Call it via plur_admin: { action: "${request.params.name}", args: { ... } } \u2014 same arguments, same validation, same result. To expose all tools directly, set PLUR_TOOL_PROFILE=full.`
|
|
2461
|
+
}) }],
|
|
2462
|
+
isError: true
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2210
2465
|
return {
|
|
2211
2466
|
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
|
|
2212
2467
|
isError: true
|
|
@@ -2244,7 +2499,7 @@ async function createServer(plur, options) {
|
|
|
2244
2499
|
};
|
|
2245
2500
|
}
|
|
2246
2501
|
});
|
|
2247
|
-
server.setRequestHandler(
|
|
2502
|
+
server.setRequestHandler("resources/list", async () => ({
|
|
2248
2503
|
resources: [
|
|
2249
2504
|
{
|
|
2250
2505
|
uri: "plur://guide",
|
|
@@ -2260,14 +2515,14 @@ async function createServer(plur, options) {
|
|
|
2260
2515
|
}
|
|
2261
2516
|
]
|
|
2262
2517
|
}));
|
|
2263
|
-
server.setRequestHandler(
|
|
2518
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
2264
2519
|
const uri = request.params.uri;
|
|
2265
2520
|
if (uri === "plur://guide") {
|
|
2266
|
-
const cursorNote = options?.profile === "cursor" ? `
|
|
2521
|
+
const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? `
|
|
2267
2522
|
|
|
2268
|
-
##
|
|
2523
|
+
## Lean tool profile (default)
|
|
2269
2524
|
|
|
2270
|
-
Most tools above are NOT directly callable in this session \u2014 only ${[...CURSOR_CORE_TOOL_NAMES].join(", ")} are top-level tools here. Everything else in this guide is reachable through **plur_admin**: call it with \`{ action: "<tool name above>", args: {...} }
|
|
2525
|
+
Most tools above are NOT directly callable in this session \u2014 only ${[...CURSOR_CORE_TOOL_NAMES].join(", ")} are top-level tools here. Everything else in this guide is reachable through **plur_admin**: call it with \`{ action: "<tool name above>", args: {...} }\`. Set \`PLUR_TOOL_PROFILE=full\` to expose all ${getToolDefinitions("full").length} tools directly.` : "";
|
|
2271
2526
|
return {
|
|
2272
2527
|
contents: [{
|
|
2273
2528
|
uri: "plur://guide",
|
|
@@ -2292,9 +2547,9 @@ Most tools above are NOT directly callable in this session \u2014 only ${[...CUR
|
|
|
2292
2547
|
}]
|
|
2293
2548
|
};
|
|
2294
2549
|
}
|
|
2295
|
-
throw new
|
|
2550
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
2296
2551
|
});
|
|
2297
|
-
server.setRequestHandler(
|
|
2552
|
+
server.setRequestHandler("prompts/list", async () => ({
|
|
2298
2553
|
prompts: [
|
|
2299
2554
|
{
|
|
2300
2555
|
name: "plur-getting-started",
|
|
@@ -2310,7 +2565,7 @@ Most tools above are NOT directly callable in this session \u2014 only ${[...CUR
|
|
|
2310
2565
|
}
|
|
2311
2566
|
]
|
|
2312
2567
|
}));
|
|
2313
|
-
server.setRequestHandler(
|
|
2568
|
+
server.setRequestHandler("prompts/get", async (request) => {
|
|
2314
2569
|
const name = request.params.name;
|
|
2315
2570
|
if (name === "plur-getting-started") {
|
|
2316
2571
|
const status = instance.status();
|
|
@@ -2355,19 +2610,33 @@ Please:
|
|
|
2355
2610
|
}]
|
|
2356
2611
|
};
|
|
2357
2612
|
}
|
|
2358
|
-
throw new
|
|
2613
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
|
|
2359
2614
|
});
|
|
2360
2615
|
return server;
|
|
2361
2616
|
}
|
|
2362
2617
|
async function runStdio() {
|
|
2363
|
-
const
|
|
2618
|
+
const envProfile = process.env.PLUR_TOOL_PROFILE;
|
|
2619
|
+
const profile = envProfile === "full" ? "full" : envProfile === "cursor" ? "cursor" : "lean";
|
|
2364
2620
|
const server = await createServer(void 0, { profile });
|
|
2365
2621
|
registerFlushOnExit({});
|
|
2622
|
+
try {
|
|
2623
|
+
writeFileSync(serverPidPath(), String(process.pid));
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
if (process.platform !== "win32") {
|
|
2627
|
+
process.on("SIGUSR1", () => {
|
|
2628
|
+
_pendingReload = true;
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2366
2631
|
const transport = new StdioServerTransport();
|
|
2367
2632
|
await server.connect(transport);
|
|
2368
2633
|
}
|
|
2369
2634
|
export {
|
|
2370
2635
|
INSTRUCTIONS,
|
|
2636
|
+
clearPendingReload,
|
|
2371
2637
|
createServer,
|
|
2372
|
-
|
|
2638
|
+
isPendingReload,
|
|
2639
|
+
readEnterpriseToken,
|
|
2640
|
+
runStdio,
|
|
2641
|
+
serverPidPath
|
|
2373
2642
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plur-ai/mcp",
|
|
3
3
|
"mcpName": "io.github.plur-ai/plur",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"plur-mcp": "dist/index.js"
|
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
"packs"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/
|
|
15
|
+
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
16
|
+
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
|
17
|
+
"@modelcontextprotocol/core": "2.0.0-beta.4",
|
|
16
18
|
"zod": "^3.23.0",
|
|
17
|
-
"@plur-ai/core": "0.
|
|
19
|
+
"@plur-ai/core": "0.15.0"
|
|
18
20
|
},
|
|
19
21
|
"devDependencies": {
|
|
20
22
|
"@types/node": "^25.5.0"
|