@plur-ai/mcp 0.14.0 → 0.16.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 +12 -16
- package/dist/{server-LAXRBVKA.js → chunk-TPL7QPPO.js} +296 -540
- package/dist/index.js +6 -6
- package/dist/server-VHSO4XMV.js +378 -0
- package/dist/tools-export.d.ts +76 -0
- package/dist/tools-export.js +21 -0
- package/package.json +9 -3
- package/packs/effective-memory/SKILL.md +1 -1
- package/packs/effective-memory/engrams.yaml +3 -3
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
// src/server.ts
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
ListToolsRequestSchema,
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListResourcesRequestSchema,
|
|
8
|
-
ReadResourceRequestSchema,
|
|
9
|
-
ListPromptsRequestSchema,
|
|
10
|
-
GetPromptRequestSchema,
|
|
11
|
-
ErrorCode,
|
|
12
|
-
McpError
|
|
13
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
15
|
-
import { join as join2 } from "path";
|
|
16
|
-
import { homedir as homedir2 } from "os";
|
|
17
|
-
import { Plur as Plur2, checkForUpdate } from "@plur-ai/core";
|
|
18
|
-
|
|
19
|
-
// src/tools.ts
|
|
20
|
-
import { existsSync, unlinkSync } from "fs";
|
|
21
|
-
import { join } from "path";
|
|
22
|
-
import { homedir } from "os";
|
|
23
|
-
import { extractMetaEngrams, validateMetaEngram, confidenceBand, generateProfile, getProfileForInjection, selectModelForOperation, getCachedUpdateCheck, minorVersionsBehind, scanForTensions, CapabilityCanary, readProjectConfig, isSharedScope, resolveRerankerName, getReranker, classifyRerankerFailure, hfCacheDirName } from "@plur-ai/core";
|
|
24
|
-
|
|
25
1
|
// src/telemetry.ts
|
|
26
2
|
import { recordEvent, flushIfNeeded, registerFlushOnExit } from "@plur-ai/core";
|
|
27
3
|
function recordTelemetry(event) {
|
|
@@ -34,9 +10,13 @@ function recordTelemetry(event) {
|
|
|
34
10
|
}
|
|
35
11
|
|
|
36
12
|
// src/version.ts
|
|
37
|
-
var VERSION = "0.
|
|
13
|
+
var VERSION = "0.16.0";
|
|
38
14
|
|
|
39
15
|
// src/tools.ts
|
|
16
|
+
import { existsSync, unlinkSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
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";
|
|
40
20
|
import { z } from "zod";
|
|
41
21
|
function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
42
22
|
return async (prompt) => {
|
|
@@ -59,6 +39,95 @@ function makeHttpLlm(baseUrl, apiKey, model = "gpt-4o-mini") {
|
|
|
59
39
|
return data.choices?.[0]?.message?.content ?? "";
|
|
60
40
|
};
|
|
61
41
|
}
|
|
42
|
+
var recallHandler = async (args, plur) => {
|
|
43
|
+
const mode = args.mode ?? "hybrid";
|
|
44
|
+
if (mode === "keyword") {
|
|
45
|
+
const results = await plur.recall(args.query, {
|
|
46
|
+
scope: args.scope,
|
|
47
|
+
domain: args.domain,
|
|
48
|
+
limit: args.limit
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
results: results.map((e) => {
|
|
52
|
+
const supersededBy = e.relations?.superseded_by;
|
|
53
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
54
|
+
return {
|
|
55
|
+
id: e.id,
|
|
56
|
+
statement: e.statement + annotation,
|
|
57
|
+
type: e.type,
|
|
58
|
+
scope: e.scope,
|
|
59
|
+
domain: e.domain,
|
|
60
|
+
retrieval_strength: e.activation.retrieval_strength
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
count: results.length,
|
|
64
|
+
mode: "keyword"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const budget = args.budget;
|
|
68
|
+
const cap = budget?.max_results ?? args.limit ?? 20;
|
|
69
|
+
const fetchLimit = budget?.max_results != null ? cap + 1 : cap;
|
|
70
|
+
const meta = await plur.recallHybridWithMeta(args.query, {
|
|
71
|
+
scope: args.scope,
|
|
72
|
+
domain: args.domain,
|
|
73
|
+
limit: fetchLimit
|
|
74
|
+
});
|
|
75
|
+
recordTelemetry("recall");
|
|
76
|
+
const truncatedByCount = budget?.max_results != null && meta.engrams.length > cap;
|
|
77
|
+
let truncated = truncatedByCount;
|
|
78
|
+
let boundedResults = truncatedByCount ? meta.engrams.slice(0, cap) : meta.engrams;
|
|
79
|
+
if (budget?.max_tokens) {
|
|
80
|
+
let tokenCount = 0;
|
|
81
|
+
const withinBudget = [];
|
|
82
|
+
for (const e of boundedResults) {
|
|
83
|
+
const tokens = Math.ceil(e.statement.length / 4) + 20;
|
|
84
|
+
if (tokenCount + tokens > budget.max_tokens) {
|
|
85
|
+
truncated = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
withinBudget.push(e);
|
|
89
|
+
tokenCount += tokens;
|
|
90
|
+
}
|
|
91
|
+
boundedResults = withinBudget;
|
|
92
|
+
}
|
|
93
|
+
const includeEpisodes = args.include_episodes === true;
|
|
94
|
+
const response = {
|
|
95
|
+
results: boundedResults.map((e) => {
|
|
96
|
+
const raw = e;
|
|
97
|
+
const supersededBy = e.relations?.superseded_by;
|
|
98
|
+
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
99
|
+
const base = {
|
|
100
|
+
id: e.id,
|
|
101
|
+
statement: e.statement + annotation,
|
|
102
|
+
type: e.type,
|
|
103
|
+
scope: e.scope,
|
|
104
|
+
domain: e.domain,
|
|
105
|
+
retrieval_strength: e.activation.retrieval_strength
|
|
106
|
+
};
|
|
107
|
+
if (includeEpisodes && raw.episode_ids?.length > 0) {
|
|
108
|
+
const episodes = plur.timeline({ search: "" });
|
|
109
|
+
base.episodes = episodes.filter((ep) => raw.episode_ids.includes(ep.id)).map((ep) => ({ id: ep.id, summary: ep.summary, timestamp: ep.timestamp }));
|
|
110
|
+
}
|
|
111
|
+
return base;
|
|
112
|
+
}),
|
|
113
|
+
count: boundedResults.length,
|
|
114
|
+
truncated,
|
|
115
|
+
mode: meta.mode
|
|
116
|
+
};
|
|
117
|
+
if (meta.mode === "hybrid-degraded") {
|
|
118
|
+
response.warning = `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
|
|
119
|
+
}
|
|
120
|
+
if (resolveRerankerName() !== "off") {
|
|
121
|
+
response.reranked = meta.reranked ?? 0;
|
|
122
|
+
const rr = plur.rerankerStatus();
|
|
123
|
+
if (boundedResults.length > 0 && (meta.reranked ?? 0) === 0 && rr.lastError) {
|
|
124
|
+
const corruptNote = rr.lastErrorKind === "corrupt-cache" ? " The model cache looks corrupt (truncated download) \u2014 purge and re-download, see plur_doctor." : "";
|
|
125
|
+
response.reranker_warning = `PLUR_RERANKER is set but the reranker did not engage \u2014 results are RRF-only (fusion order, no cross-encoder rerank).${corruptNote} Last error: ${rr.lastError}. Run plur_doctor for diagnosis.`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return response;
|
|
129
|
+
};
|
|
130
|
+
var RECALL_HYBRID_DEPRECATION = "plur_recall_hybrid is deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). This alias will be removed in 0.18.";
|
|
62
131
|
function jsonSchemaPropToZod(prop) {
|
|
63
132
|
if (!prop || typeof prop !== "object") return z.unknown();
|
|
64
133
|
const variants = prop.anyOf ?? prop.oneOf;
|
|
@@ -83,7 +152,10 @@ function jsonSchemaPropToZod(prop) {
|
|
|
83
152
|
return val;
|
|
84
153
|
}
|
|
85
154
|
}
|
|
86
|
-
|
|
155
|
+
const items = prop.items;
|
|
156
|
+
const itemVariants = items?.anyOf ?? items?.oneOf;
|
|
157
|
+
const itemsAcceptString = items?.type === "string" || Array.isArray(itemVariants) && itemVariants.some((v) => v?.type === "string");
|
|
158
|
+
if (itemsAcceptString) {
|
|
87
159
|
return trimmed.length === 0 ? [] : trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
88
160
|
}
|
|
89
161
|
return val;
|
|
@@ -112,7 +184,10 @@ function validateToolArgs(tool, rawArgs) {
|
|
|
112
184
|
const receivedFields = Object.keys(rawArgs);
|
|
113
185
|
const details = parsed.error.issues.map((i) => `${i.path.join(".") || "root"}: ${i.message}`).join(", ");
|
|
114
186
|
const hasArrayParam = Object.values(schema.properties ?? {}).some((p) => p?.type === "array");
|
|
115
|
-
const
|
|
187
|
+
const missingArrayParams = parsed.error.issues.filter((i) => i.code === "invalid_type" && i.received === "undefined").map((i) => String(i.path[0] ?? "")).filter((k) => schema.properties?.[k]?.type === "array");
|
|
188
|
+
const totalDrop = receivedFields.length === 0 && hasArrayParam;
|
|
189
|
+
const partialDrop = missingArrayParams.length > 0;
|
|
190
|
+
const arrayBugHint = totalDrop || partialDrop ? " Known client-side bug (plur-ai/plur#297): some MCP clients drop " + (partialDrop ? `array-typed parameters from a large arguments payload while keeping the earlier fields (here: ${missingArrayParams.join(", ")}). This is size-sensitive \u2014 the same call often succeeds with a shorter payload, so shrink the other fields (e.g. a briefer summary) as well. ` : "the entire arguments payload when an array-typed parameter is included. ") + 'Retry passing array parameters as a JSON string (e.g. tags: "[\\"a\\",\\"b\\"]") or a comma-separated string (tags: "a, b") \u2014 the server coerces both back into arrays.' : "";
|
|
116
191
|
const receivedNote = receivedFields.length > 0 ? `Received fields: [${receivedFields.join(", ")}].` : "Received no fields (the arguments object was empty).";
|
|
117
192
|
return {
|
|
118
193
|
ok: false,
|
|
@@ -153,7 +228,7 @@ var PLUR_GUIDE = `## PLUR Quick Start
|
|
|
153
228
|
|
|
154
229
|
### Core Tools
|
|
155
230
|
- **plur_learn** \u2014 record corrections, preferences, patterns (CALL THIS OFTEN)
|
|
156
|
-
- **
|
|
231
|
+
- **plur_recall** \u2014 search engrams by topic (default: hybrid BM25 + embeddings; use mode:"keyword" for BM25-only)
|
|
157
232
|
- **plur_forget** \u2014 retire an outdated engram`;
|
|
158
233
|
function getLlmFunction() {
|
|
159
234
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
@@ -190,12 +265,22 @@ function _cleanExpiredSessions() {
|
|
|
190
265
|
if (new Date(state.started_at).getTime() < cutoff) _sessionTelemetry.delete(id);
|
|
191
266
|
}
|
|
192
267
|
}
|
|
193
|
-
|
|
268
|
+
function _implicitSessionId() {
|
|
269
|
+
_cleanExpiredSessions();
|
|
270
|
+
if (_sessionTelemetry.size !== 1) return void 0;
|
|
271
|
+
return _sessionTelemetry.keys().next().value;
|
|
272
|
+
}
|
|
273
|
+
function _resolveInjectionSession(args) {
|
|
274
|
+
const explicit = args.session_id;
|
|
275
|
+
if (typeof explicit === "string" && explicit.length > 0) return explicit;
|
|
276
|
+
return _implicitSessionId();
|
|
277
|
+
}
|
|
194
278
|
function _recordInjectionTelemetry(session_id, injected_packs) {
|
|
195
|
-
if (!session_id
|
|
279
|
+
if (!session_id) return;
|
|
196
280
|
const state = _sessionTelemetry.get(session_id);
|
|
197
281
|
if (!state) return;
|
|
198
282
|
state.injection_calls++;
|
|
283
|
+
if (!injected_packs) return;
|
|
199
284
|
for (const [pack, count] of Object.entries(injected_packs)) {
|
|
200
285
|
state.pack_counts[pack] = (state.pack_counts[pack] ?? 0) + count;
|
|
201
286
|
}
|
|
@@ -204,10 +289,11 @@ var CURSOR_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
204
289
|
"plur_session_start",
|
|
205
290
|
"plur_session_end",
|
|
206
291
|
"plur_learn",
|
|
207
|
-
"
|
|
292
|
+
"plur_recall",
|
|
208
293
|
"plur_feedback",
|
|
209
294
|
"plur_forget",
|
|
210
295
|
"plur_status",
|
|
296
|
+
"plur_receipt",
|
|
211
297
|
"plur_doctor",
|
|
212
298
|
"plur_packs_uninstall",
|
|
213
299
|
"plur_tensions_purge"
|
|
@@ -240,6 +326,13 @@ function buildAdminDispatchTool(all) {
|
|
|
240
326
|
if (!target) {
|
|
241
327
|
return { error: `Unknown action "${action}". Valid actions: ${adminActions.join(", ")}`, success: false, _isError: true };
|
|
242
328
|
}
|
|
329
|
+
if (target.annotations?.destructiveHint === true) {
|
|
330
|
+
return {
|
|
331
|
+
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.`,
|
|
332
|
+
success: false,
|
|
333
|
+
_isError: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
243
336
|
const innerArgs = args.args ?? {};
|
|
244
337
|
const validated = validateToolArgs(target, innerArgs);
|
|
245
338
|
if (!validated.ok) {
|
|
@@ -254,12 +347,20 @@ function buildAdminDispatchTool(all) {
|
|
|
254
347
|
}
|
|
255
348
|
};
|
|
256
349
|
}
|
|
257
|
-
function getToolDefinitions(profile = "
|
|
350
|
+
function getToolDefinitions(profile = "lean") {
|
|
258
351
|
const all = getAllToolDefinitions();
|
|
259
|
-
if (profile
|
|
352
|
+
if (profile === "full") return all;
|
|
260
353
|
const core = all.filter((t) => CURSOR_CORE_TOOL_NAMES.has(t.name));
|
|
261
354
|
return [...core, buildAdminDispatchTool(all)];
|
|
262
355
|
}
|
|
356
|
+
function receiptSummary(r) {
|
|
357
|
+
if (r.coverage.source === "none") {
|
|
358
|
+
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.";
|
|
359
|
+
}
|
|
360
|
+
const since = r.window.windowed ? `the last ${r.window.requested_days} days` : `${r.coverage.complete_from}`;
|
|
361
|
+
const pct = Math.round(r.retrieved.activation_rate * 100);
|
|
362
|
+
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.`;
|
|
363
|
+
}
|
|
263
364
|
function getAllToolDefinitions() {
|
|
264
365
|
return [
|
|
265
366
|
{
|
|
@@ -319,6 +420,18 @@ function getAllToolDefinitions() {
|
|
|
319
420
|
const scopes = remote.map((s) => `"${s.scope}"`).join(", ");
|
|
320
421
|
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.` };
|
|
321
422
|
};
|
|
423
|
+
const domainHint = (wasRouted) => {
|
|
424
|
+
if (typeof args.domain === "string" && args.domain.length > 0) return {};
|
|
425
|
+
if (explicitScope || wasRouted) return {};
|
|
426
|
+
let coversScopes = [];
|
|
427
|
+
try {
|
|
428
|
+
coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
|
|
429
|
+
} catch {
|
|
430
|
+
return {};
|
|
431
|
+
}
|
|
432
|
+
if (coversScopes.length === 0) return {};
|
|
433
|
+
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.` };
|
|
434
|
+
};
|
|
322
435
|
const temporalEcho = (engram) => {
|
|
323
436
|
const extracted = engram.structured_data?._expiry_extracted;
|
|
324
437
|
return {
|
|
@@ -344,12 +457,13 @@ function getAllToolDefinitions() {
|
|
|
344
457
|
decision: "ADD",
|
|
345
458
|
...temporalEcho(engram),
|
|
346
459
|
...scopeHint(engram.scope, !!routed),
|
|
460
|
+
...domainHint(!!routed),
|
|
347
461
|
...isOutbox ? { outbox: true, warning: "Remote write failed; engram queued locally for retry on next session start or plur_sync." } : {},
|
|
348
462
|
...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.` } : {},
|
|
349
463
|
...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.` } : {}
|
|
350
464
|
};
|
|
351
465
|
} catch (err) {
|
|
352
|
-
const engram = plur.learn(statement, context);
|
|
466
|
+
const engram = await plur.learn(statement, context);
|
|
353
467
|
const isOutbox = !!engram.structured_data?._outbox;
|
|
354
468
|
const routedFallback = engram.structured_data?._routed;
|
|
355
469
|
mcpCanary.signal("learn_activity");
|
|
@@ -362,6 +476,7 @@ function getAllToolDefinitions() {
|
|
|
362
476
|
decision: "ADD",
|
|
363
477
|
...temporalEcho(engram),
|
|
364
478
|
...scopeHint(engram.scope, !!routedFallback),
|
|
479
|
+
...domainHint(!!routedFallback),
|
|
365
480
|
...isOutbox ? { outbox: true } : {},
|
|
366
481
|
warning: `Remote write failed (${err.message}); engram queued for retry.`
|
|
367
482
|
};
|
|
@@ -433,6 +548,17 @@ function getAllToolDefinitions() {
|
|
|
433
548
|
for (const r of results) {
|
|
434
549
|
if (r.input_index !== void 0) ids[r.input_index] = r.engram.id;
|
|
435
550
|
}
|
|
551
|
+
let batchDomainHint = {};
|
|
552
|
+
const noDomainCount = raw.filter((e) => !(typeof e.domain === "string" && e.domain.length > 0) && !(typeof e.scope === "string" && e.scope.length > 0)).length;
|
|
553
|
+
if (noDomainCount > 0) {
|
|
554
|
+
try {
|
|
555
|
+
const coversScopes = plur.listScopeMetadata().filter((md) => (md.covers?.length ?? 0) > 0).map((md) => md.scope);
|
|
556
|
+
if (coversScopes.length > 0) {
|
|
557
|
+
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.` };
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
436
562
|
return {
|
|
437
563
|
ids,
|
|
438
564
|
results: results.map((r) => ({
|
|
@@ -445,53 +571,35 @@ function getAllToolDefinitions() {
|
|
|
445
571
|
...r.existing_id ? { existing_id: r.existing_id } : {}
|
|
446
572
|
})),
|
|
447
573
|
stats,
|
|
574
|
+
...batchDomainHint,
|
|
448
575
|
...failures.length > 0 ? { failures, warning: `${failures.length} of ${raw.length} engram(s) failed to persist; the rest were written.` } : {}
|
|
449
576
|
};
|
|
450
577
|
}
|
|
451
578
|
},
|
|
452
579
|
{
|
|
453
580
|
name: "plur_recall",
|
|
454
|
-
description:
|
|
455
|
-
annotations: { title: "Recall
|
|
581
|
+
description: 'Search engrams by topic. Default mode is hybrid (BM25 + local embeddings via RRF) \u2014 set mode:"keyword" for BM25-only. No API calls, fully local. 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.',
|
|
582
|
+
annotations: { title: "Recall", readOnlyHint: true, idempotentHint: true },
|
|
456
583
|
inputSchema: {
|
|
457
584
|
type: "object",
|
|
458
585
|
properties: {
|
|
459
586
|
query: { type: "string", description: "Search query to find relevant engrams" },
|
|
587
|
+
mode: { type: "string", enum: ["hybrid", "keyword"], description: "Search mode \u2014 hybrid (default): BM25 + embeddings via RRF; keyword: BM25-only (faster, embeddings-independent). budget, caller_session_id and include_episodes apply to hybrid mode only \u2014 in keyword mode use limit to bound results." },
|
|
460
588
|
scope: { type: "string", description: "Filter by scope (also includes global)" },
|
|
461
589
|
domain: { type: "string", description: "Filter by domain prefix" },
|
|
462
590
|
limit: { type: "number", description: "Max results to return (default 20)" },
|
|
463
|
-
budget: { type: "object", description:
|
|
464
|
-
caller_session_id: { type: "string", description:
|
|
591
|
+
budget: { type: "object", description: 'Budget constraints for sub-agents. Hybrid mode only \u2014 ignored when mode:"keyword".', properties: { max_tokens: { type: "number" }, max_results: { type: "number" }, ttl_seconds: { type: "number" } } },
|
|
592
|
+
caller_session_id: { type: "string", description: 'Session ID of calling agent for budget enforcement. Hybrid mode only \u2014 ignored when mode:"keyword".' },
|
|
593
|
+
include_episodes: { type: "boolean", description: 'If true, include linked episode summaries for each engram (SP2 episodic anchoring). Hybrid mode only \u2014 ignored when mode:"keyword".' }
|
|
465
594
|
},
|
|
466
595
|
required: ["query"]
|
|
467
596
|
},
|
|
468
|
-
handler:
|
|
469
|
-
const results = plur.recall(args.query, {
|
|
470
|
-
scope: args.scope,
|
|
471
|
-
domain: args.domain,
|
|
472
|
-
limit: args.limit
|
|
473
|
-
});
|
|
474
|
-
return {
|
|
475
|
-
results: results.map((e) => {
|
|
476
|
-
const supersededBy = e.relations?.superseded_by;
|
|
477
|
-
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
478
|
-
return {
|
|
479
|
-
id: e.id,
|
|
480
|
-
statement: e.statement + annotation,
|
|
481
|
-
type: e.type,
|
|
482
|
-
scope: e.scope,
|
|
483
|
-
domain: e.domain,
|
|
484
|
-
retrieval_strength: e.activation.retrieval_strength
|
|
485
|
-
};
|
|
486
|
-
}),
|
|
487
|
-
count: results.length
|
|
488
|
-
};
|
|
489
|
-
}
|
|
597
|
+
handler: recallHandler
|
|
490
598
|
},
|
|
491
599
|
{
|
|
492
600
|
name: "plur_recall_hybrid",
|
|
493
|
-
description: "Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion. No API calls, fully local.
|
|
494
|
-
annotations: { title: "Recall (hybrid)", readOnlyHint: true, idempotentHint: true },
|
|
601
|
+
description: "[Deprecated since 0.16 \u2014 use plur_recall (mode defaults to hybrid). Alias kept for backwards compatibility; removal earliest 0.18.] Hybrid search \u2014 BM25 + local embeddings merged via Reciprocal Rank Fusion. No API calls, fully local.",
|
|
602
|
+
annotations: { title: "Recall (hybrid) [deprecated alias]", readOnlyHint: true, idempotentHint: true },
|
|
495
603
|
inputSchema: {
|
|
496
604
|
type: "object",
|
|
497
605
|
properties: {
|
|
@@ -505,72 +613,12 @@ function getAllToolDefinitions() {
|
|
|
505
613
|
},
|
|
506
614
|
required: ["query"]
|
|
507
615
|
},
|
|
616
|
+
// True forwarder — delegates to the canonical plur_recall handler and
|
|
617
|
+
// prepends the deprecation notice. No duplicated budget/episode/
|
|
618
|
+
// reranker logic, so a fix to plur_recall reaches this alias too.
|
|
508
619
|
handler: async (args, plur) => {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
const meta = await plur.recallHybridWithMeta(args.query, {
|
|
512
|
-
scope: args.scope,
|
|
513
|
-
domain: args.domain,
|
|
514
|
-
limit: effectiveLimit
|
|
515
|
-
});
|
|
516
|
-
recordTelemetry("recall");
|
|
517
|
-
const results = meta.engrams;
|
|
518
|
-
let truncated = false;
|
|
519
|
-
let boundedResults = results;
|
|
520
|
-
if (budget?.max_results && results.length > budget.max_results) {
|
|
521
|
-
boundedResults = results.slice(0, budget.max_results);
|
|
522
|
-
truncated = true;
|
|
523
|
-
}
|
|
524
|
-
if (budget?.max_tokens) {
|
|
525
|
-
let tokenCount = 0;
|
|
526
|
-
const withinBudget = [];
|
|
527
|
-
for (const e of boundedResults) {
|
|
528
|
-
const tokens = Math.ceil(e.statement.length / 4) + 20;
|
|
529
|
-
if (tokenCount + tokens > budget.max_tokens) {
|
|
530
|
-
truncated = true;
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
withinBudget.push(e);
|
|
534
|
-
tokenCount += tokens;
|
|
535
|
-
}
|
|
536
|
-
boundedResults = withinBudget;
|
|
537
|
-
}
|
|
538
|
-
const includeEpisodes = args.include_episodes === true;
|
|
539
|
-
const response = {
|
|
540
|
-
results: boundedResults.map((e) => {
|
|
541
|
-
const raw = e;
|
|
542
|
-
const supersededBy = e.relations?.superseded_by;
|
|
543
|
-
const annotation = supersededBy?.length ? ` [superseded by ${supersededBy.join(", ")}]` : "";
|
|
544
|
-
const base = {
|
|
545
|
-
id: e.id,
|
|
546
|
-
statement: e.statement + annotation,
|
|
547
|
-
type: e.type,
|
|
548
|
-
scope: e.scope,
|
|
549
|
-
domain: e.domain,
|
|
550
|
-
retrieval_strength: e.activation.retrieval_strength
|
|
551
|
-
};
|
|
552
|
-
if (includeEpisodes && raw.episode_ids?.length > 0) {
|
|
553
|
-
const episodes = plur.timeline({ search: "" });
|
|
554
|
-
base.episodes = episodes.filter((ep) => raw.episode_ids.includes(ep.id)).map((ep) => ({ id: ep.id, summary: ep.summary, timestamp: ep.timestamp }));
|
|
555
|
-
}
|
|
556
|
-
return base;
|
|
557
|
-
}),
|
|
558
|
-
count: boundedResults.length,
|
|
559
|
-
truncated,
|
|
560
|
-
mode: meta.mode
|
|
561
|
-
};
|
|
562
|
-
if (meta.mode === "hybrid-degraded") {
|
|
563
|
-
response.warning = `Embedding layer unavailable \u2014 results are BM25-only. Run plur_doctor for diagnosis. Last error: ${meta.embedderError ?? "unknown"}`;
|
|
564
|
-
}
|
|
565
|
-
if (resolveRerankerName() !== "off") {
|
|
566
|
-
response.reranked = meta.reranked ?? 0;
|
|
567
|
-
const rr = plur.rerankerStatus();
|
|
568
|
-
if (boundedResults.length > 0 && (meta.reranked ?? 0) === 0 && rr.lastError) {
|
|
569
|
-
const corruptNote = rr.lastErrorKind === "corrupt-cache" ? " The model cache looks corrupt (truncated download) \u2014 purge and re-download, see plur_doctor." : "";
|
|
570
|
-
response.reranker_warning = `PLUR_RERANKER is set but the reranker did not engage \u2014 results are RRF-only (fusion order, no cross-encoder rerank).${corruptNote} Last error: ${rr.lastError}. Run plur_doctor for diagnosis.`;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return response;
|
|
620
|
+
const result = await recallHandler({ ...args, mode: "hybrid" }, plur);
|
|
621
|
+
return { deprecated: RECALL_HYBRID_DEPRECATION, ...result };
|
|
574
622
|
}
|
|
575
623
|
},
|
|
576
624
|
{
|
|
@@ -582,16 +630,20 @@ function getAllToolDefinitions() {
|
|
|
582
630
|
properties: {
|
|
583
631
|
task: { type: "string", description: "The task description to inject context for" },
|
|
584
632
|
budget: { type: "number", description: "Token budget for injection (default 2000)" },
|
|
585
|
-
scope: { type: "string", description: "Scope filter for engram selection" }
|
|
633
|
+
scope: { type: "string", description: "Scope filter for engram selection" },
|
|
634
|
+
session_id: { type: "string", description: "Session this injection belongs to (from plur_session_start). Optional when one session is open; required for correct attribution when several are." }
|
|
586
635
|
},
|
|
587
636
|
required: ["task"]
|
|
588
637
|
},
|
|
589
638
|
handler: async (args, plur) => {
|
|
590
|
-
const
|
|
639
|
+
const session_id = _resolveInjectionSession(args);
|
|
640
|
+
const result = await plur.inject(args.task, {
|
|
591
641
|
budget: args.budget,
|
|
592
|
-
scope: args.scope
|
|
642
|
+
scope: args.scope,
|
|
643
|
+
source: "inject",
|
|
644
|
+
session_id
|
|
593
645
|
});
|
|
594
|
-
_recordInjectionTelemetry(
|
|
646
|
+
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
595
647
|
return {
|
|
596
648
|
directives: result.directives,
|
|
597
649
|
consider: result.consider,
|
|
@@ -612,16 +664,20 @@ function getAllToolDefinitions() {
|
|
|
612
664
|
properties: {
|
|
613
665
|
task: { type: "string", description: "The task description to inject context for" },
|
|
614
666
|
budget: { type: "number", description: "Token budget for injection (default 2000)" },
|
|
615
|
-
scope: { type: "string", description: "Scope filter for engram selection" }
|
|
667
|
+
scope: { type: "string", description: "Scope filter for engram selection" },
|
|
668
|
+
session_id: { type: "string", description: "Session this injection belongs to (from plur_session_start). Optional when one session is open; required for correct attribution when several are." }
|
|
616
669
|
},
|
|
617
670
|
required: ["task"]
|
|
618
671
|
},
|
|
619
672
|
handler: async (args, plur) => {
|
|
673
|
+
const session_id = _resolveInjectionSession(args);
|
|
620
674
|
const result = await plur.injectHybrid(args.task, {
|
|
621
675
|
budget: args.budget,
|
|
622
|
-
scope: args.scope
|
|
676
|
+
scope: args.scope,
|
|
677
|
+
source: "inject",
|
|
678
|
+
session_id
|
|
623
679
|
});
|
|
624
|
-
_recordInjectionTelemetry(
|
|
680
|
+
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
625
681
|
return {
|
|
626
682
|
directives: result.directives,
|
|
627
683
|
consider: result.consider,
|
|
@@ -701,7 +757,7 @@ function getAllToolDefinitions() {
|
|
|
701
757
|
},
|
|
702
758
|
handler: async (args, plur) => {
|
|
703
759
|
if (args.list === true) {
|
|
704
|
-
const pinned = plur.listPinned();
|
|
760
|
+
const pinned = await plur.listPinned();
|
|
705
761
|
return {
|
|
706
762
|
count: pinned.length,
|
|
707
763
|
pinned: pinned.map((e) => ({ id: e.id, statement: e.statement, scope: e.scope, domain: e.domain }))
|
|
@@ -731,7 +787,7 @@ function getAllToolDefinitions() {
|
|
|
731
787
|
},
|
|
732
788
|
handler: async (args, plur) => {
|
|
733
789
|
if (args.id) {
|
|
734
|
-
const engram = plur.getById(args.id);
|
|
790
|
+
const engram = await plur.getById(args.id);
|
|
735
791
|
if (engram) {
|
|
736
792
|
if (engram.status === "retired") return { success: false, error: `Already retired: ${args.id}` };
|
|
737
793
|
await plur.forget(args.id);
|
|
@@ -741,7 +797,7 @@ function getAllToolDefinitions() {
|
|
|
741
797
|
return { success: true, retired: { id: args.id } };
|
|
742
798
|
}
|
|
743
799
|
if (args.search) {
|
|
744
|
-
const matches = plur.recall(args.search, { limit: 100 });
|
|
800
|
+
const matches = await plur.recall(args.search, { limit: 100 });
|
|
745
801
|
if (matches.length === 0) return { success: false, error: `No active engrams matching "${args.search}"` };
|
|
746
802
|
if (matches.length === 1) {
|
|
747
803
|
await plur.forget(matches[0].id);
|
|
@@ -840,7 +896,7 @@ function getAllToolDefinitions() {
|
|
|
840
896
|
required: ["content"]
|
|
841
897
|
},
|
|
842
898
|
handler: async (args, plur) => {
|
|
843
|
-
const candidates = plur.ingest(args.content, {
|
|
899
|
+
const candidates = await plur.ingest(args.content, {
|
|
844
900
|
source: args.source,
|
|
845
901
|
extract_only: args.extract_only,
|
|
846
902
|
scope: args.scope,
|
|
@@ -859,32 +915,32 @@ function getAllToolDefinitions() {
|
|
|
859
915
|
},
|
|
860
916
|
{
|
|
861
917
|
name: "plur_packs_preview",
|
|
862
|
-
description: "Preview a pack before installing \u2014 shows manifest, engram list, security scan, and warnings. Always call this before plur_packs_install to let the user review what they are importing.",
|
|
918
|
+
description: "Preview a pack before installing \u2014 shows manifest, engram list, security scan, and warnings. Always call this before plur_packs_install to let the user review what they are importing. Accepts a local directory path or an https:// URL pointing to a .tar.gz archive.",
|
|
863
919
|
annotations: { title: "Preview pack", readOnlyHint: true, idempotentHint: true },
|
|
864
920
|
inputSchema: {
|
|
865
921
|
type: "object",
|
|
866
922
|
properties: {
|
|
867
|
-
source: { type: "string", description: "Path to the pack directory to
|
|
923
|
+
source: { type: "string", description: "Path to the pack directory, or an https:// URL to a .tar.gz pack archive" }
|
|
868
924
|
},
|
|
869
925
|
required: ["source"]
|
|
870
926
|
},
|
|
871
927
|
handler: async (args, plur) => {
|
|
872
|
-
return plur.previewPack(args.source);
|
|
928
|
+
return await plur.previewPack(args.source);
|
|
873
929
|
}
|
|
874
930
|
},
|
|
875
931
|
{
|
|
876
932
|
name: "plur_packs_install",
|
|
877
|
-
description: "Install an engram pack from a directory path. Runs a mandatory security scan (blocks if secrets found), detects conflicts with existing engrams, and records install metadata in the registry. Call plur_packs_preview first to show the user what the pack contains.",
|
|
933
|
+
description: "Install an engram pack from a local directory path or an https:// URL pointing to a .tar.gz archive. Runs a mandatory security scan (blocks if secrets found), detects conflicts with existing engrams, and records install metadata in the registry. Call plur_packs_preview first to show the user what the pack contains.",
|
|
878
934
|
annotations: { title: "Install pack", destructiveHint: false, idempotentHint: true },
|
|
879
935
|
inputSchema: {
|
|
880
936
|
type: "object",
|
|
881
937
|
properties: {
|
|
882
|
-
source: { type: "string", description: "Path to the pack directory to
|
|
938
|
+
source: { type: "string", description: "Path to the pack directory, or an https:// URL to a .tar.gz pack archive" }
|
|
883
939
|
},
|
|
884
940
|
required: ["source"]
|
|
885
941
|
},
|
|
886
942
|
handler: async (args, plur) => {
|
|
887
|
-
const result = plur.installPack(args.source);
|
|
943
|
+
const result = await plur.installPack(args.source);
|
|
888
944
|
return {
|
|
889
945
|
installed: result.installed,
|
|
890
946
|
name: result.name,
|
|
@@ -970,17 +1026,27 @@ function getAllToolDefinitions() {
|
|
|
970
1026
|
full: {
|
|
971
1027
|
type: "boolean",
|
|
972
1028
|
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."
|
|
1029
|
+
},
|
|
1030
|
+
remote_type: {
|
|
1031
|
+
type: "string",
|
|
1032
|
+
enum: ["personal", "shared"],
|
|
1033
|
+
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."
|
|
973
1034
|
}
|
|
974
1035
|
}
|
|
975
1036
|
},
|
|
976
1037
|
handler: async (args, plur) => {
|
|
977
|
-
const result = plur.sync(args.remote, {
|
|
1038
|
+
const result = await plur.sync(args.remote, {
|
|
1039
|
+
full: args.full === true,
|
|
1040
|
+
...args.remote_type === "personal" || args.remote_type === "shared" ? { remoteType: args.remote_type } : {}
|
|
1041
|
+
});
|
|
978
1042
|
await plur.waitForIndex();
|
|
979
1043
|
const indexError = plur.lastIndexError();
|
|
980
1044
|
let outbox_result;
|
|
1045
|
+
let outbox_error;
|
|
981
1046
|
try {
|
|
982
1047
|
outbox_result = await plur.flushOutbox();
|
|
983
|
-
} catch {
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
outbox_error = err.message;
|
|
984
1050
|
}
|
|
985
1051
|
return {
|
|
986
1052
|
...result,
|
|
@@ -994,6 +1060,10 @@ function getAllToolDefinitions() {
|
|
|
994
1060
|
pending: outbox_result.failed,
|
|
995
1061
|
warnings: outbox_result.expired_warnings
|
|
996
1062
|
}
|
|
1063
|
+
} : {},
|
|
1064
|
+
...outbox_error ? {
|
|
1065
|
+
outbox_error,
|
|
1066
|
+
outbox_warning: `The outbox flush failed \u2014 ${outbox_error}. Engrams routed to a remote store are still queued locally and were NOT pushed. They retry on the next session_start or plur_sync.`
|
|
997
1067
|
} : {}
|
|
998
1068
|
};
|
|
999
1069
|
}
|
|
@@ -1033,11 +1103,11 @@ function getAllToolDefinitions() {
|
|
|
1033
1103
|
args.llm_api_key,
|
|
1034
1104
|
args.llm_model
|
|
1035
1105
|
);
|
|
1036
|
-
const sourceEngrams = plur.list({
|
|
1106
|
+
const sourceEngrams = await plur.list({
|
|
1037
1107
|
domain: args.domain,
|
|
1038
1108
|
scope: args.scope
|
|
1039
1109
|
});
|
|
1040
|
-
const existingMetas = plur.list().filter((e) => e.id.startsWith("META-"));
|
|
1110
|
+
const existingMetas = (await plur.list()).filter((e) => e.id.startsWith("META-"));
|
|
1041
1111
|
const result = await extractMetaEngrams(sourceEngrams, llm, {
|
|
1042
1112
|
run_validation: args.run_validation,
|
|
1043
1113
|
existing_metas: existingMetas
|
|
@@ -1045,7 +1115,7 @@ function getAllToolDefinitions() {
|
|
|
1045
1115
|
const isDryRun = args.dry_run === true;
|
|
1046
1116
|
let saveStats = null;
|
|
1047
1117
|
if (!isDryRun && result.results.length > 0) {
|
|
1048
|
-
saveStats = plur.saveMetaEngrams(result.results);
|
|
1118
|
+
saveStats = await plur.saveMetaEngrams(result.results);
|
|
1049
1119
|
}
|
|
1050
1120
|
return {
|
|
1051
1121
|
engrams_analyzed: result.engrams_analyzed,
|
|
@@ -1081,7 +1151,7 @@ function getAllToolDefinitions() {
|
|
|
1081
1151
|
}
|
|
1082
1152
|
},
|
|
1083
1153
|
handler: async (args, plur) => {
|
|
1084
|
-
const allEngrams = plur.list();
|
|
1154
|
+
const allEngrams = await plur.list();
|
|
1085
1155
|
const metaEngrams = allEngrams.filter((e) => e.id.startsWith("META-"));
|
|
1086
1156
|
const minConfidence = args.min_confidence ?? 0;
|
|
1087
1157
|
const levelFilter = args.hierarchy_level;
|
|
@@ -1132,20 +1202,20 @@ function getAllToolDefinitions() {
|
|
|
1132
1202
|
required: ["meta_engram_id", "test_domain", "llm_base_url", "llm_api_key"]
|
|
1133
1203
|
},
|
|
1134
1204
|
handler: async (args, plur) => {
|
|
1135
|
-
const allEngrams = plur.list();
|
|
1205
|
+
const allEngrams = await plur.list();
|
|
1136
1206
|
const meta = allEngrams.find((e) => e.id === args.meta_engram_id);
|
|
1137
1207
|
if (!meta) {
|
|
1138
1208
|
throw new Error(`Meta-engram not found: ${args.meta_engram_id}`);
|
|
1139
1209
|
}
|
|
1140
1210
|
const testDomain = args.test_domain;
|
|
1141
|
-
const testEngrams = plur.list({ domain: testDomain });
|
|
1211
|
+
const testEngrams = await plur.list({ domain: testDomain });
|
|
1142
1212
|
const llm = makeHttpLlm(
|
|
1143
1213
|
args.llm_base_url,
|
|
1144
1214
|
args.llm_api_key,
|
|
1145
1215
|
args.llm_model
|
|
1146
1216
|
);
|
|
1147
1217
|
const result = await validateMetaEngram(meta, testEngrams, testDomain, llm);
|
|
1148
|
-
plur.updateEngram(meta);
|
|
1218
|
+
await plur.updateEngram(meta);
|
|
1149
1219
|
return {
|
|
1150
1220
|
meta_engram_id: result.meta_engram_id,
|
|
1151
1221
|
test_domain: result.test_domain,
|
|
@@ -1170,7 +1240,7 @@ function getAllToolDefinitions() {
|
|
|
1170
1240
|
}
|
|
1171
1241
|
},
|
|
1172
1242
|
handler: async (args, plur) => {
|
|
1173
|
-
const status = plur.status({
|
|
1243
|
+
const status = await plur.status({
|
|
1174
1244
|
domain: args.domain,
|
|
1175
1245
|
created_after: args.created_after
|
|
1176
1246
|
});
|
|
@@ -1202,10 +1272,26 @@ function getAllToolDefinitions() {
|
|
|
1202
1272
|
behind: minorVersionsBehind(versionCheck.current, versionCheck.latest)
|
|
1203
1273
|
}
|
|
1204
1274
|
} : {},
|
|
1205
|
-
capabilities: mcpCanary.status()
|
|
1275
|
+
capabilities: await mcpCanary.status()
|
|
1206
1276
|
};
|
|
1207
1277
|
}
|
|
1208
1278
|
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "plur_receipt",
|
|
1281
|
+
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.',
|
|
1282
|
+
annotations: { title: "Memory receipt", readOnlyHint: true, idempotentHint: true },
|
|
1283
|
+
inputSchema: {
|
|
1284
|
+
type: "object",
|
|
1285
|
+
properties: {
|
|
1286
|
+
days: { type: "number", description: "Restrict to the last N days (integer). Omit for all recorded history." }
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
handler: async (args, plur) => {
|
|
1290
|
+
const days = typeof args.days === "number" && Number.isFinite(args.days) && args.days >= 1 ? Math.floor(args.days) : void 0;
|
|
1291
|
+
const receipt = await plur.receipt(days ? { days } : void 0);
|
|
1292
|
+
return { summary: receiptSummary(receipt), ...receipt };
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1209
1295
|
{
|
|
1210
1296
|
name: "plur_doctor",
|
|
1211
1297
|
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).',
|
|
@@ -1222,7 +1308,7 @@ function getAllToolDefinitions() {
|
|
|
1222
1308
|
plur.resetEmbedder();
|
|
1223
1309
|
plur.resetReranker();
|
|
1224
1310
|
}
|
|
1225
|
-
const status = plur.status();
|
|
1311
|
+
const status = await plur.status();
|
|
1226
1312
|
const before = plur.embedderStatus();
|
|
1227
1313
|
if (!before.disabled) {
|
|
1228
1314
|
try {
|
|
@@ -1325,7 +1411,7 @@ function getAllToolDefinitions() {
|
|
|
1325
1411
|
evalStatus = { result: run.result, stale: false };
|
|
1326
1412
|
freshlyRun = !run.cached;
|
|
1327
1413
|
} else {
|
|
1328
|
-
evalStatus = plur.rerankerEvalStatus(rerankerName);
|
|
1414
|
+
evalStatus = await plur.rerankerEvalStatus(rerankerName);
|
|
1329
1415
|
}
|
|
1330
1416
|
if (!evalStatus) {
|
|
1331
1417
|
checks.push({
|
|
@@ -1358,7 +1444,7 @@ function getAllToolDefinitions() {
|
|
|
1358
1444
|
});
|
|
1359
1445
|
}
|
|
1360
1446
|
}
|
|
1361
|
-
const canaryStatuses = mcpCanary.status();
|
|
1447
|
+
const canaryStatuses = await mcpCanary.status();
|
|
1362
1448
|
for (const cs of canaryStatuses) {
|
|
1363
1449
|
if (!cs.healthy) {
|
|
1364
1450
|
checks.push({ check: `capability: ${cs.capability}`, ok: false, detail: cs.warning });
|
|
@@ -1420,16 +1506,17 @@ function getAllToolDefinitions() {
|
|
|
1420
1506
|
const task = args.task;
|
|
1421
1507
|
const tags = args.tags;
|
|
1422
1508
|
_cleanExpiredSessions();
|
|
1423
|
-
_activeSessionId = session_id;
|
|
1424
1509
|
_sessionTelemetry.set(session_id, {
|
|
1425
1510
|
pack_counts: {},
|
|
1426
1511
|
injection_calls: 0,
|
|
1427
1512
|
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1428
1513
|
});
|
|
1429
1514
|
let outbox_result;
|
|
1515
|
+
let outbox_error;
|
|
1430
1516
|
try {
|
|
1431
1517
|
outbox_result = await plur.flushOutbox();
|
|
1432
|
-
} catch {
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
outbox_error = err.message;
|
|
1433
1520
|
}
|
|
1434
1521
|
const remote_scopes = plur.getWritableRemoteScopes().map((s) => {
|
|
1435
1522
|
const md = plur.getScopeMetadata(s.scope);
|
|
@@ -1444,7 +1531,7 @@ function getAllToolDefinitions() {
|
|
|
1444
1531
|
const default_scope = explicit_default_scope ?? projectConfig.scope ?? null;
|
|
1445
1532
|
const scope_source = explicit_default_scope ? "caller" : projectConfig.scope ? "project-config" : "none";
|
|
1446
1533
|
plur.setSessionScope(default_scope);
|
|
1447
|
-
const status = plur.status();
|
|
1534
|
+
const status = await plur.status();
|
|
1448
1535
|
const store_stats = {
|
|
1449
1536
|
engram_count: status.engram_count,
|
|
1450
1537
|
episode_count: status.episode_count,
|
|
@@ -1456,8 +1543,9 @@ function getAllToolDefinitions() {
|
|
|
1456
1543
|
try {
|
|
1457
1544
|
const result = await plur.injectHybrid(task, {
|
|
1458
1545
|
scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
|
|
1459
|
-
session_id
|
|
1546
|
+
session_id,
|
|
1460
1547
|
// stamped on the co_injection provenance event (#452)
|
|
1548
|
+
source: "session_start"
|
|
1461
1549
|
});
|
|
1462
1550
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1463
1551
|
if (result.count > 0) {
|
|
@@ -1468,9 +1556,10 @@ function getAllToolDefinitions() {
|
|
|
1468
1556
|
engrams = { text: lines.join("\n"), count: result.count, injected_ids: result.injected_ids };
|
|
1469
1557
|
}
|
|
1470
1558
|
} catch {
|
|
1471
|
-
const result = plur.inject(task, {
|
|
1559
|
+
const result = await plur.inject(task, {
|
|
1472
1560
|
scope: tags?.length ? `tags:${tags.join(",")}` : void 0,
|
|
1473
|
-
session_id
|
|
1561
|
+
session_id,
|
|
1562
|
+
source: "session_start"
|
|
1474
1563
|
});
|
|
1475
1564
|
_recordInjectionTelemetry(session_id, result.injected_packs);
|
|
1476
1565
|
if (result.count > 0) {
|
|
@@ -1512,7 +1601,7 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
|
|
|
1512
1601
|
} else if (scope_source === "none") {
|
|
1513
1602
|
guide += `
|
|
1514
1603
|
|
|
1515
|
-
\u26A0\uFE0F No project scope detected. plur_learn calls without explicit scope
|
|
1604
|
+
\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.`;
|
|
1516
1605
|
}
|
|
1517
1606
|
if (remote_scopes.length > 0) {
|
|
1518
1607
|
const safe = (x) => String(x ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
@@ -1527,9 +1616,10 @@ Auto-detected project scope: "${default_scope}" (from .plur.yaml in the current
|
|
|
1527
1616
|
|
|
1528
1617
|
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}).` : `
|
|
1529
1618
|
|
|
1530
|
-
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 (
|
|
1619
|
+
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.`;
|
|
1531
1620
|
try {
|
|
1532
1621
|
const discoveries = await plur.discoverRemoteScopes({ timeoutMs: 3e3 });
|
|
1622
|
+
plur.persistScopeMetadata(discoveries);
|
|
1533
1623
|
const failures = discoveries.filter((d) => !d.ok);
|
|
1534
1624
|
if (failures.length > 0) {
|
|
1535
1625
|
const authExpired = failures.some((f) => /\b40[13]\b/.test(f.error ?? ""));
|
|
@@ -1541,12 +1631,11 @@ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: wh
|
|
|
1541
1631
|
|
|
1542
1632
|
\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.`;
|
|
1543
1633
|
}
|
|
1544
|
-
const
|
|
1545
|
-
if (
|
|
1546
|
-
const list = unregistered.map((s) => `"${safe(s)}"`).join(", ");
|
|
1634
|
+
const offerable = [...new Set(discoveries.filter((d) => d.ok).flatMap((d) => d.unregistered))].filter(isSharedScope);
|
|
1635
|
+
if (offerable.length > 0) {
|
|
1547
1636
|
guide += `
|
|
1548
1637
|
|
|
1549
|
-
\u{1F50E}
|
|
1638
|
+
\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).`;
|
|
1550
1639
|
}
|
|
1551
1640
|
} catch {
|
|
1552
1641
|
}
|
|
@@ -1585,6 +1674,10 @@ Remote store scopes available: ${scopeList}. Set scope PER ENGRAM by content: wh
|
|
|
1585
1674
|
warnings: outbox_result.expired_warnings
|
|
1586
1675
|
}
|
|
1587
1676
|
} : {},
|
|
1677
|
+
...outbox_error ? {
|
|
1678
|
+
outbox_error,
|
|
1679
|
+
outbox_warning: `The outbox flush failed \u2014 ${outbox_error}. Engrams routed to a remote store are still queued locally and were NOT pushed. They retry on the next session_start or plur_sync.`
|
|
1680
|
+
} : {},
|
|
1588
1681
|
// Version staleness warning (issue #151)
|
|
1589
1682
|
...version_warning ? { version_warning, version: VERSION } : {}
|
|
1590
1683
|
};
|
|
@@ -1650,7 +1743,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1650
1743
|
`engram_suggestions[${i}] must be a string or {statement: string, type?: string}, got ${typeof s}`
|
|
1651
1744
|
);
|
|
1652
1745
|
}
|
|
1653
|
-
plur.learn(statement, { type });
|
|
1746
|
+
await plur.learn(statement, { type });
|
|
1654
1747
|
engrams_created++;
|
|
1655
1748
|
}
|
|
1656
1749
|
}
|
|
@@ -1666,7 +1759,6 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1666
1759
|
} : void 0;
|
|
1667
1760
|
if (session_id) {
|
|
1668
1761
|
_sessionTelemetry.delete(session_id);
|
|
1669
|
-
if (_activeSessionId === session_id) _activeSessionId = void 0;
|
|
1670
1762
|
}
|
|
1671
1763
|
try {
|
|
1672
1764
|
const plurDir = process.env.PLUR_PATH ?? join(homedir(), ".plur");
|
|
@@ -1681,7 +1773,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1681
1773
|
}
|
|
1682
1774
|
} catch {
|
|
1683
1775
|
}
|
|
1684
|
-
const status = plur.status();
|
|
1776
|
+
const status = await plur.status();
|
|
1685
1777
|
return {
|
|
1686
1778
|
engrams_created,
|
|
1687
1779
|
episode_id: episode.id,
|
|
@@ -1743,7 +1835,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1743
1835
|
inputSchema: { type: "object", properties: {} },
|
|
1744
1836
|
handler: async (_args, plur) => {
|
|
1745
1837
|
const stores = await plur.listStoresAsync();
|
|
1746
|
-
const outboxCount = plur.outboxCount();
|
|
1838
|
+
const outboxCount = await plur.outboxCount();
|
|
1747
1839
|
return {
|
|
1748
1840
|
stores,
|
|
1749
1841
|
count: stores.length,
|
|
@@ -1753,29 +1845,33 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1753
1845
|
},
|
|
1754
1846
|
{
|
|
1755
1847
|
name: "plur_suggest_scope",
|
|
1756
|
-
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).',
|
|
1848
|
+
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.',
|
|
1757
1849
|
annotations: { title: "Suggest scope", readOnlyHint: true, idempotentHint: true },
|
|
1758
1850
|
inputSchema: {
|
|
1759
1851
|
type: "object",
|
|
1760
1852
|
properties: {
|
|
1761
1853
|
statement: { type: "string", description: "The engram statement to route" },
|
|
1762
1854
|
domain: { type: "string", description: 'Optional dotted namespace for the engram (e.g. "plur.core.security") \u2014 strongest routing signal' },
|
|
1763
|
-
tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" }
|
|
1855
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional tags on the engram" },
|
|
1856
|
+
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." }
|
|
1764
1857
|
},
|
|
1765
1858
|
required: ["statement"]
|
|
1766
1859
|
},
|
|
1767
1860
|
handler: async (args, plur) => {
|
|
1768
|
-
const
|
|
1861
|
+
const raw = args.min_confidence;
|
|
1862
|
+
const explicit = typeof raw === "number" && Number.isFinite(raw) ? Math.min(1, Math.max(0, raw)) : void 0;
|
|
1863
|
+
const minConfidence = explicit ?? plur.getScopeRoutingConfig().min_confidence ?? SUGGEST_DISPLAY_MIN_CONFIDENCE;
|
|
1864
|
+
const candidates = await plur.suggestScope({
|
|
1769
1865
|
statement: args.statement,
|
|
1770
1866
|
domain: args.domain,
|
|
1771
1867
|
tags: args.tags
|
|
1772
|
-
});
|
|
1773
|
-
return { candidates, count: candidates.length };
|
|
1868
|
+
}, { minConfidence });
|
|
1869
|
+
return { candidates, count: candidates.length, min_confidence: minConfidence };
|
|
1774
1870
|
}
|
|
1775
1871
|
},
|
|
1776
1872
|
{
|
|
1777
1873
|
name: "plur_scopes_discover",
|
|
1778
|
-
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.",
|
|
1874
|
+
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.",
|
|
1779
1875
|
annotations: { title: "Discover scopes", readOnlyHint: false, idempotentHint: true },
|
|
1780
1876
|
inputSchema: {
|
|
1781
1877
|
type: "object",
|
|
@@ -1831,7 +1927,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1831
1927
|
const promoted = [];
|
|
1832
1928
|
const errors = [];
|
|
1833
1929
|
for (const id of targetIds) {
|
|
1834
|
-
const engram = plur.getById(id);
|
|
1930
|
+
const engram = await plur.getById(id);
|
|
1835
1931
|
if (!engram) {
|
|
1836
1932
|
errors.push({ id, error: "Not found" });
|
|
1837
1933
|
continue;
|
|
@@ -1848,7 +1944,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1848
1944
|
engram.activation.retrieval_strength = 0.7;
|
|
1849
1945
|
engram.activation.storage_strength = 1;
|
|
1850
1946
|
engram.activation.last_accessed = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1851
|
-
plur.updateEngram(engram);
|
|
1947
|
+
await plur.updateEngram(engram);
|
|
1852
1948
|
promoted.push({ id, statement: engram.statement });
|
|
1853
1949
|
}
|
|
1854
1950
|
return { promoted, errors, success: errors.length === 0 };
|
|
@@ -1893,12 +1989,12 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1893
1989
|
if (args.action === "resolve") {
|
|
1894
1990
|
const winner = args.winner;
|
|
1895
1991
|
if (!winner) throw new Error('action:"resolve" requires winner (the engram id to keep)');
|
|
1896
|
-
const { record, retired_id } = plur.resolveTension(id, winner);
|
|
1992
|
+
const { record, retired_id } = await plur.resolveTension(id, winner);
|
|
1897
1993
|
return { record, retired: retired_id, message: `Tension ${id} resolved: ${winner} wins, ${retired_id} retired.` };
|
|
1898
1994
|
}
|
|
1899
1995
|
throw new Error(`Unknown action: ${args.action}. Use confirm, dismiss, or resolve.`);
|
|
1900
1996
|
}
|
|
1901
|
-
const engrams = plur.list({
|
|
1997
|
+
const engrams = await plur.list({
|
|
1902
1998
|
scope: args.scope,
|
|
1903
1999
|
domain: args.domain
|
|
1904
2000
|
});
|
|
@@ -1922,7 +2018,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1922
2018
|
temporal_discount: args.temporal_discount ?? tensionsConfig.temporal_discount,
|
|
1923
2019
|
...persist ? { exclude_pairs: new Set(plur.suppressedTensionPairKeys()) } : {}
|
|
1924
2020
|
});
|
|
1925
|
-
const persisted = persist && result.tensions.length > 0 ? plur.recordTensions(result.tensions) : void 0;
|
|
2021
|
+
const persisted = persist && result.tensions.length > 0 ? await plur.recordTensions(result.tensions) : void 0;
|
|
1926
2022
|
return {
|
|
1927
2023
|
pairs_checked: result.pairs_checked,
|
|
1928
2024
|
count: result.new_tensions,
|
|
@@ -1975,7 +2071,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1975
2071
|
annotations: { title: "Purge Tensions", destructiveHint: true, idempotentHint: true },
|
|
1976
2072
|
inputSchema: { type: "object", properties: {} },
|
|
1977
2073
|
handler: async (_args, plur) => {
|
|
1978
|
-
const result = plur.purgeTensions();
|
|
2074
|
+
const result = await plur.purgeTensions();
|
|
1979
2075
|
return {
|
|
1980
2076
|
purged_conflict_refs: result.purged_count,
|
|
1981
2077
|
engrams_modified: result.engrams_modified,
|
|
@@ -1998,7 +2094,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
1998
2094
|
required: ["episode_id"]
|
|
1999
2095
|
},
|
|
2000
2096
|
handler: async (args, plur) => {
|
|
2001
|
-
const engram = plur.episodeToEngram(args.episode_id, {
|
|
2097
|
+
const engram = await plur.episodeToEngram(args.episode_id, {
|
|
2002
2098
|
scope: args.scope,
|
|
2003
2099
|
domain: args.domain,
|
|
2004
2100
|
tags: args.tags
|
|
@@ -2035,7 +2131,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2035
2131
|
};
|
|
2036
2132
|
}
|
|
2037
2133
|
const { listHistoryMonths, readHistory } = await import("@plur-ai/core");
|
|
2038
|
-
const status = plur.status();
|
|
2134
|
+
const status = await plur.status();
|
|
2039
2135
|
const months = listHistoryMonths(status.storage_root);
|
|
2040
2136
|
const allEvents = [];
|
|
2041
2137
|
for (const month of months.reverse()) {
|
|
@@ -2108,7 +2204,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2108
2204
|
},
|
|
2109
2205
|
handler: async (args, plur) => {
|
|
2110
2206
|
const name = args.name;
|
|
2111
|
-
let engrams = plur.list({
|
|
2207
|
+
let engrams = await plur.list({
|
|
2112
2208
|
domain: args.filter_domain,
|
|
2113
2209
|
scope: args.filter_scope
|
|
2114
2210
|
});
|
|
@@ -2122,9 +2218,9 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2122
2218
|
if (filterType) {
|
|
2123
2219
|
engrams = engrams.filter((e) => e.type === filterType);
|
|
2124
2220
|
}
|
|
2125
|
-
const { homedir:
|
|
2126
|
-
const { join:
|
|
2127
|
-
const outputDir = args.output_dir ||
|
|
2221
|
+
const { homedir: homedir2 } = await import("os");
|
|
2222
|
+
const { join: join2 } = await import("path");
|
|
2223
|
+
const outputDir = args.output_dir || join2(homedir2(), "plur-packs", name);
|
|
2128
2224
|
const result = plur.exportPack(engrams, outputDir, {
|
|
2129
2225
|
name,
|
|
2130
2226
|
version: "1.0.0",
|
|
@@ -2189,7 +2285,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2189
2285
|
}
|
|
2190
2286
|
},
|
|
2191
2287
|
handler: async (args, plur) => {
|
|
2192
|
-
const status = plur.status();
|
|
2288
|
+
const status = await plur.status();
|
|
2193
2289
|
const storagePath = status.storage_root;
|
|
2194
2290
|
if (!args.force_regenerate) {
|
|
2195
2291
|
const cached = getProfileForInjection(storagePath);
|
|
@@ -2202,7 +2298,7 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2202
2298
|
}
|
|
2203
2299
|
const model = args.llm_model ?? selectModelForOperation("profile", status.config?.llm);
|
|
2204
2300
|
const llm = makeHttpLlm(args.llm_base_url, args.llm_api_key, model);
|
|
2205
|
-
const engrams = plur.list({ scope: args.scope });
|
|
2301
|
+
const engrams = await plur.list({ scope: args.scope });
|
|
2206
2302
|
const profile = await generateProfile(engrams, llm, storagePath, status.config?.profile?.cache_ttl_hours ?? 24);
|
|
2207
2303
|
return { profile, source: "generated", engram_count: engrams.length, model };
|
|
2208
2304
|
}
|
|
@@ -2210,351 +2306,11 @@ Include at least one engram_suggestion if ANYTHING was learned. An empty suggest
|
|
|
2210
2306
|
];
|
|
2211
2307
|
}
|
|
2212
2308
|
|
|
2213
|
-
// src/server.ts
|
|
2214
|
-
function serverPidPath(baseDir) {
|
|
2215
|
-
return join2(baseDir ?? join2(homedir2(), ".plur"), "server.pid");
|
|
2216
|
-
}
|
|
2217
|
-
function readEnterpriseToken(baseDir) {
|
|
2218
|
-
const configPath = join2(baseDir ?? join2(homedir2(), ".plur"), "config.json");
|
|
2219
|
-
if (!existsSync2(configPath)) return void 0;
|
|
2220
|
-
try {
|
|
2221
|
-
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2222
|
-
const ent = cfg?.enterprise;
|
|
2223
|
-
if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0;
|
|
2224
|
-
return { url: ent.url, token: ent.token, username: ent.username };
|
|
2225
|
-
} catch {
|
|
2226
|
-
return void 0;
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
var _pendingReload = false;
|
|
2230
|
-
function isPendingReload() {
|
|
2231
|
-
return _pendingReload;
|
|
2232
|
-
}
|
|
2233
|
-
function clearPendingReload() {
|
|
2234
|
-
_pendingReload = false;
|
|
2235
|
-
}
|
|
2236
|
-
var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
|
|
2237
|
-
|
|
2238
|
-
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.
|
|
2239
|
-
|
|
2240
|
-
SESSION LIFECYCLE:
|
|
2241
|
-
- 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.
|
|
2242
|
-
- Without hooks: call plur_session_start at the start, plur_session_end at the end.
|
|
2243
|
-
|
|
2244
|
-
DURING the session:
|
|
2245
|
-
- When user corrects you ("no, use X not Y") \u2192 call plur_learn immediately
|
|
2246
|
-
- When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately
|
|
2247
|
-
- When you discover a codebase convention or pattern \u2192 call plur_learn
|
|
2248
|
-
|
|
2249
|
-
SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session):
|
|
2250
|
-
A single session produces engrams that belong in different stores. Choose the
|
|
2251
|
-
"scope" on EACH plur_learn call from what the engram is about:
|
|
2252
|
-
- Team / engineering / architecture / shared-convention knowledge \u2192 the matching
|
|
2253
|
-
team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the
|
|
2254
|
-
scopes this install can write to \u2014 use them.
|
|
2255
|
-
- Personal preferences, your workflow, or details specific to one project \u2192 the
|
|
2256
|
-
default/local scope (or "project:<name>").
|
|
2257
|
-
- "global" is for genuinely cross-project facts only (language gotchas, tool
|
|
2258
|
-
quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting
|
|
2259
|
-
scope \u2014 with a team store configured it never reaches the team store. If no
|
|
2260
|
-
scope fits, prefer the project/local default over global.
|
|
2261
|
-
|
|
2262
|
-
OPTIONAL but improves quality:
|
|
2263
|
-
- Call plur_feedback to rate which injected engrams helped (positive/negative)
|
|
2264
|
-
- Call plur_recall_hybrid before answering factual questions \u2014 the answer may be in memory
|
|
2265
|
-
|
|
2266
|
-
Do not ask permission to use these tools \u2014 they are your memory system.
|
|
2267
|
-
|
|
2268
|
-
Setup: If this is a fresh install, suggest the user run: npx @plur-ai/mcp init
|
|
2269
|
-
This installs hooks for automatic injection + session management. One-time global setup.`;
|
|
2270
|
-
var GUIDE_RESOURCE = `# PLUR \u2014 Agent Guide
|
|
2271
|
-
|
|
2272
|
-
## What is PLUR?
|
|
2273
|
-
|
|
2274
|
-
Persistent memory for AI agents. Corrections, preferences, and conventions are stored as **engrams** \u2014 small assertions that strengthen with use and decay when irrelevant (ACT-R model). Storage is plain YAML on disk. Search is fully local (BM25 + embeddings). Zero API calls.
|
|
2275
|
-
|
|
2276
|
-
## Quick Start
|
|
2277
|
-
|
|
2278
|
-
1. \`plur_session_start\` \u2014 start a session, inject relevant context
|
|
2279
|
-
2. \`plur_learn\` \u2014 store a new learning
|
|
2280
|
-
3. \`plur_feedback\` \u2014 rate injected engrams
|
|
2281
|
-
4. \`plur_session_end\` \u2014 capture summary and new learnings
|
|
2282
|
-
|
|
2283
|
-
## When to Call Each Tool
|
|
2284
|
-
|
|
2285
|
-
| Trigger | Tool |
|
|
2286
|
-
|---------|------|
|
|
2287
|
-
| Session starts | \`plur_session_start\` with task description |
|
|
2288
|
-
| User corrects you | \`plur_learn\` with the correction |
|
|
2289
|
-
| User states preference ("always X", "never Y") | \`plur_learn\` with scope and type |
|
|
2290
|
-
| You used a recalled engram successfully | \`plur_feedback\` with "positive" |
|
|
2291
|
-
| A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" |
|
|
2292
|
-
| User says "forget X" or a memory is outdated | \`plur_forget\` |
|
|
2293
|
-
| You need to check what's stored | \`plur_status\` or \`plur_packs_list\` |
|
|
2294
|
-
| End of session | \`plur_session_end\` with summary and suggestions |
|
|
2295
|
-
|
|
2296
|
-
## Tool Categories
|
|
2297
|
-
|
|
2298
|
-
### Session Management
|
|
2299
|
-
- **plur_session_start** \u2014 start a session, inject relevant context
|
|
2300
|
-
- **plur_session_end** \u2014 end a session, capture summary and new learnings
|
|
2301
|
-
|
|
2302
|
-
### Core Memory
|
|
2303
|
-
- **plur_learn** \u2014 store a correction, preference, or convention
|
|
2304
|
-
- **plur_recall** \u2014 BM25 keyword search
|
|
2305
|
-
- **plur_recall_hybrid** \u2014 BM25 + embeddings (recommended default)
|
|
2306
|
-
- **plur_feedback** \u2014 rate an engram (trains relevance)
|
|
2307
|
-
- **plur_forget** \u2014 retire an outdated engram
|
|
2308
|
-
- **plur_promote** \u2014 activate a candidate engram
|
|
2309
|
-
|
|
2310
|
-
### Context Injection
|
|
2311
|
-
- **plur_inject** \u2014 select engrams for a task (BM25)
|
|
2312
|
-
- **plur_inject_hybrid** \u2014 select engrams for a task (BM25 + embeddings, recommended)
|
|
2313
|
-
|
|
2314
|
-
### Episodic Timeline
|
|
2315
|
-
- **plur_capture** \u2014 record what happened in a session
|
|
2316
|
-
- **plur_timeline** \u2014 query past episodes
|
|
2317
|
-
|
|
2318
|
-
### Knowledge Management
|
|
2319
|
-
- **plur_ingest** \u2014 extract engrams from text content
|
|
2320
|
-
- **plur_packs_install** \u2014 install curated engram packs
|
|
2321
|
-
- **plur_packs_list** \u2014 list installed packs
|
|
2322
|
-
- **plur_packs_export** \u2014 export engrams as a shareable pack
|
|
2323
|
-
|
|
2324
|
-
### Multi-Store
|
|
2325
|
-
- **plur_stores_add** \u2014 register an additional engram store
|
|
2326
|
-
- **plur_stores_list** \u2014 list all configured stores
|
|
2327
|
-
|
|
2328
|
-
**Note:** Multi-store is currently config-only. Recall and inject search the primary store. Cross-store search coming in a future release.
|
|
2329
|
-
|
|
2330
|
-
### Sync & Status
|
|
2331
|
-
- **plur_sync** \u2014 sync engrams across devices via git
|
|
2332
|
-
- **plur_sync_status** \u2014 check sync state
|
|
2333
|
-
- **plur_status** \u2014 system health
|
|
2334
|
-
|
|
2335
|
-
## Scoping
|
|
2336
|
-
|
|
2337
|
-
Use \`scope\` to namespace engrams per project:
|
|
2338
|
-
- \`scope: "global"\` \u2014 applies everywhere (default)
|
|
2339
|
-
- \`scope: "project:my-app"\` \u2014 applies only to my-app
|
|
2340
|
-
- Scoped recall automatically includes global engrams
|
|
2341
|
-
|
|
2342
|
-
## Storage
|
|
2343
|
-
|
|
2344
|
-
\`\`\`
|
|
2345
|
-
~/.plur/
|
|
2346
|
-
\u251C\u2500\u2500 engrams.yaml # learned knowledge
|
|
2347
|
-
\u251C\u2500\u2500 episodes.yaml # session timeline
|
|
2348
|
-
\u2514\u2500\u2500 config.yaml # settings
|
|
2349
|
-
\`\`\`
|
|
2350
|
-
|
|
2351
|
-
Override with \`PLUR_PATH\` environment variable.
|
|
2352
|
-
`;
|
|
2353
|
-
async function createServer(plur, options) {
|
|
2354
|
-
const instance = plur ?? new Plur2();
|
|
2355
|
-
const tools = getToolDefinitions(options?.profile ?? "full");
|
|
2356
|
-
checkForUpdate("@plur-ai/mcp", VERSION, (r) => {
|
|
2357
|
-
if (r.updateAvailable) {
|
|
2358
|
-
console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npx @plur-ai/mcp@latest`);
|
|
2359
|
-
}
|
|
2360
|
-
});
|
|
2361
|
-
const server = new Server(
|
|
2362
|
-
{ name: "plur-mcp", version: VERSION },
|
|
2363
|
-
{
|
|
2364
|
-
capabilities: {
|
|
2365
|
-
tools: {},
|
|
2366
|
-
resources: {},
|
|
2367
|
-
prompts: {},
|
|
2368
|
-
logging: {}
|
|
2369
|
-
},
|
|
2370
|
-
instructions: INSTRUCTIONS
|
|
2371
|
-
}
|
|
2372
|
-
);
|
|
2373
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2374
|
-
tools: tools.map((t) => ({
|
|
2375
|
-
name: t.name,
|
|
2376
|
-
description: t.description,
|
|
2377
|
-
inputSchema: t.inputSchema,
|
|
2378
|
-
...t.annotations && { annotations: t.annotations }
|
|
2379
|
-
}))
|
|
2380
|
-
}));
|
|
2381
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2382
|
-
const tool = tools.find((t) => t.name === request.params.name);
|
|
2383
|
-
if (!tool) {
|
|
2384
|
-
return {
|
|
2385
|
-
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
|
|
2386
|
-
isError: true
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
mcpCanary.tick();
|
|
2390
|
-
try {
|
|
2391
|
-
let args = request.params.arguments ?? {};
|
|
2392
|
-
const validated = validateToolArgs(tool, args);
|
|
2393
|
-
if (!validated.ok) {
|
|
2394
|
-
return {
|
|
2395
|
-
content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }],
|
|
2396
|
-
isError: true
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
args = validated.data;
|
|
2400
|
-
const result = await tool.handler(args, instance);
|
|
2401
|
-
let payload = result;
|
|
2402
|
-
let resultIsError = false;
|
|
2403
|
-
if (result && typeof result === "object" && result._isError === true) {
|
|
2404
|
-
resultIsError = true;
|
|
2405
|
-
const { _isError, ...rest } = result;
|
|
2406
|
-
payload = rest;
|
|
2407
|
-
}
|
|
2408
|
-
return {
|
|
2409
|
-
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
2410
|
-
...resultIsError ? { isError: true } : {}
|
|
2411
|
-
};
|
|
2412
|
-
} catch (err) {
|
|
2413
|
-
const message = err?.message ?? String(err);
|
|
2414
|
-
server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
|
|
2415
|
-
return {
|
|
2416
|
-
content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }],
|
|
2417
|
-
isError: true
|
|
2418
|
-
};
|
|
2419
|
-
}
|
|
2420
|
-
});
|
|
2421
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
2422
|
-
resources: [
|
|
2423
|
-
{
|
|
2424
|
-
uri: "plur://guide",
|
|
2425
|
-
name: "PLUR Agent Guide",
|
|
2426
|
-
description: "Complete reference for all PLUR tools, when to use them, scoping, and storage",
|
|
2427
|
-
mimeType: "text/markdown"
|
|
2428
|
-
},
|
|
2429
|
-
{
|
|
2430
|
-
uri: "plur://status",
|
|
2431
|
-
name: "PLUR Status",
|
|
2432
|
-
description: "Live system health \u2014 engram count, episode count, pack count, storage path",
|
|
2433
|
-
mimeType: "application/json"
|
|
2434
|
-
}
|
|
2435
|
-
]
|
|
2436
|
-
}));
|
|
2437
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2438
|
-
const uri = request.params.uri;
|
|
2439
|
-
if (uri === "plur://guide") {
|
|
2440
|
-
const cursorNote = options?.profile === "cursor" ? `
|
|
2441
|
-
|
|
2442
|
-
## Cursor tool profile
|
|
2443
|
-
|
|
2444
|
-
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: {...} }\`.` : "";
|
|
2445
|
-
return {
|
|
2446
|
-
contents: [{
|
|
2447
|
-
uri: "plur://guide",
|
|
2448
|
-
mimeType: "text/markdown",
|
|
2449
|
-
text: GUIDE_RESOURCE + cursorNote
|
|
2450
|
-
}]
|
|
2451
|
-
};
|
|
2452
|
-
}
|
|
2453
|
-
if (uri === "plur://status") {
|
|
2454
|
-
const status = instance.status();
|
|
2455
|
-
return {
|
|
2456
|
-
contents: [{
|
|
2457
|
-
uri: "plur://status",
|
|
2458
|
-
mimeType: "application/json",
|
|
2459
|
-
text: JSON.stringify({
|
|
2460
|
-
engram_count: status.engram_count,
|
|
2461
|
-
episode_count: status.episode_count,
|
|
2462
|
-
pack_count: status.pack_count,
|
|
2463
|
-
storage_root: status.storage_root,
|
|
2464
|
-
version: VERSION
|
|
2465
|
-
}, null, 2)
|
|
2466
|
-
}]
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
2470
|
-
});
|
|
2471
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
2472
|
-
prompts: [
|
|
2473
|
-
{
|
|
2474
|
-
name: "plur-getting-started",
|
|
2475
|
-
description: "Step-by-step guide to set up and start using PLUR memory"
|
|
2476
|
-
},
|
|
2477
|
-
{
|
|
2478
|
-
name: "plur-session-start",
|
|
2479
|
-
description: "Load relevant context for a task \u2014 call at the start of each session",
|
|
2480
|
-
arguments: [
|
|
2481
|
-
{ name: "task", description: "Brief description of the task or goal", required: true },
|
|
2482
|
-
{ name: "scope", description: "Project scope (e.g. project:my-app)", required: false }
|
|
2483
|
-
]
|
|
2484
|
-
}
|
|
2485
|
-
]
|
|
2486
|
-
}));
|
|
2487
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
2488
|
-
const name = request.params.name;
|
|
2489
|
-
if (name === "plur-getting-started") {
|
|
2490
|
-
const status = instance.status();
|
|
2491
|
-
return {
|
|
2492
|
-
description: "Get started with PLUR memory",
|
|
2493
|
-
messages: [{
|
|
2494
|
-
role: "user",
|
|
2495
|
-
content: {
|
|
2496
|
-
type: "text",
|
|
2497
|
-
text: `I just set up PLUR. Here's my current status:
|
|
2498
|
-
|
|
2499
|
-
- Engrams stored: ${status.engram_count}
|
|
2500
|
-
- Episodes recorded: ${status.episode_count}
|
|
2501
|
-
- Packs installed: ${status.pack_count}
|
|
2502
|
-
- Storage: ${status.storage_root}
|
|
2503
|
-
|
|
2504
|
-
${status.engram_count === 0 ? `I have no memories yet. Help me get started by:
|
|
2505
|
-
1. Teaching me a coding preference or convention (I'll use plur_learn)
|
|
2506
|
-
2. Then recalling it to verify it works (I'll use plur_recall_hybrid)
|
|
2507
|
-
3. Rating the recall quality (I'll use plur_feedback)` : `I have ${status.engram_count} engrams stored. Try asking me something related to your project \u2014 I'll check my memory first.`}`
|
|
2508
|
-
}
|
|
2509
|
-
}]
|
|
2510
|
-
};
|
|
2511
|
-
}
|
|
2512
|
-
if (name === "plur-session-start") {
|
|
2513
|
-
const task = request.params.arguments?.task ?? "general work";
|
|
2514
|
-
const scope = request.params.arguments?.scope;
|
|
2515
|
-
return {
|
|
2516
|
-
description: "Load relevant context for this session",
|
|
2517
|
-
messages: [{
|
|
2518
|
-
role: "user",
|
|
2519
|
-
content: {
|
|
2520
|
-
type: "text",
|
|
2521
|
-
text: `Starting a new session. Task: ${task}${scope ? ` (scope: ${scope})` : ""}
|
|
2522
|
-
|
|
2523
|
-
Please:
|
|
2524
|
-
1. Call plur_recall_hybrid with query "${task}"${scope ? ` and scope "${scope}"` : ""} to load relevant memories
|
|
2525
|
-
2. Review the recalled engrams and apply any relevant conventions or preferences
|
|
2526
|
-
3. If any recalled engrams are helpful, call plur_feedback with "positive"
|
|
2527
|
-
4. If any are irrelevant, call plur_feedback with "negative"`
|
|
2528
|
-
}
|
|
2529
|
-
}]
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
throw new McpError(ErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
|
|
2533
|
-
});
|
|
2534
|
-
return server;
|
|
2535
|
-
}
|
|
2536
|
-
async function runStdio() {
|
|
2537
|
-
const profile = process.env.PLUR_TOOL_PROFILE === "cursor" ? "cursor" : "full";
|
|
2538
|
-
const server = await createServer(void 0, { profile });
|
|
2539
|
-
registerFlushOnExit({});
|
|
2540
|
-
try {
|
|
2541
|
-
writeFileSync(serverPidPath(), String(process.pid));
|
|
2542
|
-
} catch {
|
|
2543
|
-
}
|
|
2544
|
-
if (process.platform !== "win32") {
|
|
2545
|
-
process.on("SIGUSR1", () => {
|
|
2546
|
-
_pendingReload = true;
|
|
2547
|
-
});
|
|
2548
|
-
}
|
|
2549
|
-
const transport = new StdioServerTransport();
|
|
2550
|
-
await server.connect(transport);
|
|
2551
|
-
}
|
|
2552
2309
|
export {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
serverPidPath
|
|
2310
|
+
registerFlushOnExit,
|
|
2311
|
+
VERSION,
|
|
2312
|
+
validateToolArgs,
|
|
2313
|
+
mcpCanary,
|
|
2314
|
+
CURSOR_CORE_TOOL_NAMES,
|
|
2315
|
+
getToolDefinitions
|
|
2560
2316
|
};
|