@plur-ai/mcp 0.20.1 → 0.21.1
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 +34 -1
- package/dist/{chunk-445S5QNU.js → chunk-O2NJFMNP.js} +1 -1
- package/dist/{chunk-SWQDW42Z.js → chunk-OETJSRYE.js} +523 -90
- package/dist/index.d.ts +52 -1
- package/dist/index.js +286 -121
- package/dist/server-EHKHU4JH.js +1006 -0
- package/dist/tools-export.js +2 -2
- package/package.json +2 -2
- package/dist/server-LODRVEQB.js +0 -483
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURSOR_CORE_TOOL_NAMES,
|
|
3
|
+
FOLDER_SCOPE,
|
|
4
|
+
folderMapAdvice,
|
|
5
|
+
folderOnCommand,
|
|
6
|
+
getToolDefinitions,
|
|
7
|
+
mcpCanary,
|
|
8
|
+
registerFlushOnExit,
|
|
9
|
+
resolveToolProfile,
|
|
10
|
+
setActiveToolProfile,
|
|
11
|
+
validateToolArgs
|
|
12
|
+
} from "./chunk-OETJSRYE.js";
|
|
13
|
+
import {
|
|
14
|
+
VERSION
|
|
15
|
+
} from "./chunk-O2NJFMNP.js";
|
|
16
|
+
|
|
17
|
+
// src/server.ts
|
|
18
|
+
import { Server, ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
20
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
21
|
+
import { join as join2 } from "path";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { Plur, checkForUpdate, VERSION_CHECK_SUCCESS_TTL_MS } from "@plur-ai/core";
|
|
24
|
+
|
|
25
|
+
// src/drop-log.ts
|
|
26
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
27
|
+
import { dirname, join } from "path";
|
|
28
|
+
import { atomicWrite, withLock } from "@plur-ai/core";
|
|
29
|
+
var PAYLOAD_DROP_LOG_MAX_ENTRIES = 500;
|
|
30
|
+
function payloadDropLogPath(storageRoot) {
|
|
31
|
+
return join(storageRoot, "logs", "payload-drops.jsonl");
|
|
32
|
+
}
|
|
33
|
+
function recordPayloadDrop(storageRoot, record) {
|
|
34
|
+
try {
|
|
35
|
+
const path = payloadDropLogPath(storageRoot);
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
withLock(path, () => {
|
|
38
|
+
appendFileSync(path, JSON.stringify(record) + "\n");
|
|
39
|
+
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.length > 0);
|
|
40
|
+
if (lines.length > PAYLOAD_DROP_LOG_MAX_ENTRIES) {
|
|
41
|
+
atomicWrite(path, lines.slice(-PAYLOAD_DROP_LOG_MAX_ENTRIES).join("\n") + "\n", { durable: false });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/folder-gate.ts
|
|
49
|
+
import {
|
|
50
|
+
folderOffEntries,
|
|
51
|
+
folderMapProblem,
|
|
52
|
+
folderAsk,
|
|
53
|
+
hostFolderAsk,
|
|
54
|
+
folderNonceOutstanding,
|
|
55
|
+
endFolderNonceSession,
|
|
56
|
+
sweepFolderNonces,
|
|
57
|
+
coversHomeOrRoot,
|
|
58
|
+
workspaceFolderScope,
|
|
59
|
+
FOLDER_NONCE_TTL_MS
|
|
60
|
+
} from "@plur-ai/core";
|
|
61
|
+
import { fileURLToPath } from "url";
|
|
62
|
+
import { randomBytes } from "crypto";
|
|
63
|
+
import { execFileSync } from "child_process";
|
|
64
|
+
import { realpathSync } from "fs";
|
|
65
|
+
import { dirname as dirname2, resolve } from "path";
|
|
66
|
+
var FOLDER_GATED_TOOLS = /* @__PURE__ */ new Set([
|
|
67
|
+
// write engrams
|
|
68
|
+
"plur_learn",
|
|
69
|
+
"plur_learn_batch",
|
|
70
|
+
"plur_feedback",
|
|
71
|
+
"plur_pin",
|
|
72
|
+
"plur_forget",
|
|
73
|
+
"plur_ingest",
|
|
74
|
+
"plur_promote",
|
|
75
|
+
"plur_rescope",
|
|
76
|
+
"plur_episode_to_engram",
|
|
77
|
+
"plur_report_failure",
|
|
78
|
+
"plur_extract_meta",
|
|
79
|
+
"plur_validate_meta",
|
|
80
|
+
"plur_tensions",
|
|
81
|
+
"plur_tensions_purge",
|
|
82
|
+
"plur_packs_install",
|
|
83
|
+
"plur_packs_uninstall",
|
|
84
|
+
// read engrams
|
|
85
|
+
"plur_recall",
|
|
86
|
+
"plur_recall_hybrid",
|
|
87
|
+
"plur_inject",
|
|
88
|
+
"plur_inject_hybrid",
|
|
89
|
+
"plur_similarity_search",
|
|
90
|
+
"plur_meta_engrams",
|
|
91
|
+
"plur_history",
|
|
92
|
+
"plur_provenance",
|
|
93
|
+
"plur_profile",
|
|
94
|
+
"plur_packs_export",
|
|
95
|
+
// episodes and sessions
|
|
96
|
+
"plur_capture",
|
|
97
|
+
"plur_timeline",
|
|
98
|
+
"plur_session_start",
|
|
99
|
+
"plur_session_end",
|
|
100
|
+
// move engrams between the local store and a remote
|
|
101
|
+
"plur_sync",
|
|
102
|
+
"plur_outbox",
|
|
103
|
+
// returns the statements of recently retrieved engrams
|
|
104
|
+
"plur_receipt"
|
|
105
|
+
]);
|
|
106
|
+
function log(line) {
|
|
107
|
+
try {
|
|
108
|
+
process.stderr.write(`[plur] ${line}
|
|
109
|
+
`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function unreadable(problem, root) {
|
|
114
|
+
const advice = folderMapAdvice(problem, root);
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
plur: "off",
|
|
118
|
+
reason: "folder-map-unreadable",
|
|
119
|
+
file: problem.file,
|
|
120
|
+
...problem.line !== void 0 ? { line: problem.line } : {},
|
|
121
|
+
...problem.column !== void 0 ? { column: problem.column } : {},
|
|
122
|
+
fixable: problem.fixable,
|
|
123
|
+
...advice.command ? { repair_command: advice.command } : {},
|
|
124
|
+
...advice.summary ? { repair_summary: advice.summary } : {},
|
|
125
|
+
message: `PLUR memory is paused: the folder map ${JSON.stringify(problem.file)} ${problem.problem}. Until it is fixed, PLUR cannot tell whether memory is allowed in this folder, so nothing was read from or written to memory. This is not an error \u2014 carry on without memory. ${advice.text}`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function folderOffAnswer(plur, dirs) {
|
|
129
|
+
const root = plur.storageRoot;
|
|
130
|
+
let problem;
|
|
131
|
+
try {
|
|
132
|
+
problem = folderMapProblem(root);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
problem = { file: `${root}/folders.yaml`, problem: `could not be checked (${err?.message ?? err})`, fixable: false };
|
|
135
|
+
}
|
|
136
|
+
if (problem) {
|
|
137
|
+
log(`folder map ${problem.file} ${problem.problem}; memory tools do nothing until it is fixed.`);
|
|
138
|
+
return unreadable(problem, root);
|
|
139
|
+
}
|
|
140
|
+
for (const dir of dirs) {
|
|
141
|
+
let mode;
|
|
142
|
+
try {
|
|
143
|
+
mode = plur.resolveFolderPolicy(dir).mode;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const why = `could not be applied to ${JSON.stringify(dir)} (${err?.message ?? err})`;
|
|
146
|
+
log(`folder map: ${why}; memory tools do nothing.`);
|
|
147
|
+
return unreadable({ file: `${root}/folders.yaml`, problem: why, fixable: false }, root);
|
|
148
|
+
}
|
|
149
|
+
if (mode !== "off") continue;
|
|
150
|
+
let entries = [];
|
|
151
|
+
try {
|
|
152
|
+
entries = folderOffEntries(dir, { root }).map((e) => e.path);
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
if (entries.length === 0) entries = [dir];
|
|
156
|
+
const cmds = entries.map((e) => folderOnCommand(e, root));
|
|
157
|
+
const how = cmds.every((c) => c !== null) ? cmds.join(entries.length > 1 ? " and " : "") : `plur folders set <folder> --on for ${entries.length > 1 ? "each of " : ""}${entries.map((e) => JSON.stringify(e)).join(", ")} (see plur folders list)`;
|
|
158
|
+
const through = entries.length === 1 && entries[0] === dir ? "" : ` through ${entries.length > 1 ? "the entries" : "the entry"} ${entries.map((e) => JSON.stringify(e)).join(", ")}`;
|
|
159
|
+
return {
|
|
160
|
+
success: true,
|
|
161
|
+
plur: "off",
|
|
162
|
+
folder: dir,
|
|
163
|
+
message: `PLUR is off for this folder (${JSON.stringify(dir)}): your folder map turns memory off here${through}, so nothing was read from or written to memory. This is not an error \u2014 carry on without memory. Only the user can turn it back on, from a terminal: ${how}`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function createWorkspaceDirs(server, opts = {}) {
|
|
169
|
+
const timeout = opts.timeoutMs ?? 2e3;
|
|
170
|
+
const cwd = opts.cwd ?? (() => process.cwd());
|
|
171
|
+
let cached = null;
|
|
172
|
+
let gen = 0;
|
|
173
|
+
let pending = null;
|
|
174
|
+
server.setNotificationHandler("notifications/roots/list_changed", () => {
|
|
175
|
+
gen++;
|
|
176
|
+
cached = null;
|
|
177
|
+
pending = null;
|
|
178
|
+
});
|
|
179
|
+
const request = (g) => server.listRoots(void 0, { timeout }).then(
|
|
180
|
+
({ roots }) => {
|
|
181
|
+
const dirs = [];
|
|
182
|
+
for (const r of roots) {
|
|
183
|
+
let dir = null;
|
|
184
|
+
try {
|
|
185
|
+
if (typeof r.uri === "string" && /^file:\/\//i.test(r.uri)) dir = fileURLToPath(r.uri.replace(/^file:/i, "file:"));
|
|
186
|
+
} catch {
|
|
187
|
+
dir = null;
|
|
188
|
+
}
|
|
189
|
+
if (dir === null) {
|
|
190
|
+
log(`a workspace root the server cannot resolve to a local folder (${JSON.stringify(String(r.uri).slice(0, 200))}); memory tools do nothing for this call.`);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
dirs.push(dir);
|
|
194
|
+
}
|
|
195
|
+
if (g === gen && listChanged()) cached = { gen: g, dirs };
|
|
196
|
+
return dirs;
|
|
197
|
+
},
|
|
198
|
+
(err) => {
|
|
199
|
+
log(`roots/list failed (${err?.message ?? err}); memory tools do nothing for this call, the next call asks the client again.`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
).finally(() => {
|
|
203
|
+
if (pending?.gen === g) pending = null;
|
|
204
|
+
});
|
|
205
|
+
const listChanged = () => {
|
|
206
|
+
const roots = server.getClientCapabilities()?.roots;
|
|
207
|
+
return roots?.listChanged === true;
|
|
208
|
+
};
|
|
209
|
+
const clientRoots = async () => {
|
|
210
|
+
if (!server.getClientCapabilities()?.roots) return [];
|
|
211
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
212
|
+
if (cached && cached.gen === gen) return cached.dirs;
|
|
213
|
+
const g = gen;
|
|
214
|
+
let promise;
|
|
215
|
+
if (listChanged()) {
|
|
216
|
+
if (!pending || pending.gen !== g) pending = { gen: g, promise: request(g) };
|
|
217
|
+
promise = pending.promise;
|
|
218
|
+
} else {
|
|
219
|
+
promise = request(g);
|
|
220
|
+
}
|
|
221
|
+
const dirs = await promise;
|
|
222
|
+
if (dirs === null) return null;
|
|
223
|
+
if (g === gen) return dirs;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
async dirs() {
|
|
229
|
+
const roots = await clientRoots();
|
|
230
|
+
return roots === null ? null : [.../* @__PURE__ */ new Set([...roots, cwd()])];
|
|
231
|
+
},
|
|
232
|
+
async workspace() {
|
|
233
|
+
const roots = await clientRoots();
|
|
234
|
+
return roots === null ? null : { roots: [...new Set(roots)], cwd: cwd() };
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function workspaceUnknownAnswer() {
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
plur: "off",
|
|
242
|
+
reason: "workspace-unknown",
|
|
243
|
+
message: `PLUR couldn't get the editor's workspace folders (the MCP roots request failed, timed out, or named a folder that is not on this machine), so it cannot tell whether memory is allowed here: memory is off for this call, and nothing was read from or written to memory. This is not an error. The next call will ask again; if the editor's roots keep failing, memory stays off until they work.`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseEtime(etime) {
|
|
247
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim());
|
|
248
|
+
if (!m) return null;
|
|
249
|
+
return Number(m[1] ?? 0) * 86400 + Number(m[2] ?? 0) * 3600 + Number(m[3]) * 60 + Number(m[4]);
|
|
250
|
+
}
|
|
251
|
+
function ancestorHosts(now = Date.now()) {
|
|
252
|
+
if (process.platform === "win32") return [];
|
|
253
|
+
let table;
|
|
254
|
+
try {
|
|
255
|
+
table = execFileSync("ps", ["-A", "-o", "pid=,ppid=,etime="], { encoding: "utf8", timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] });
|
|
256
|
+
} catch {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
const info = /* @__PURE__ */ new Map();
|
|
260
|
+
for (const line of table.split("\n")) {
|
|
261
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
|
|
262
|
+
if (!m) continue;
|
|
263
|
+
const secs = parseEtime(m[3]);
|
|
264
|
+
if (secs === null) continue;
|
|
265
|
+
info.set(Number(m[1]), { ppid: Number(m[2]), startedAt: now - secs * 1e3 });
|
|
266
|
+
}
|
|
267
|
+
const out = [];
|
|
268
|
+
let pid = process.ppid;
|
|
269
|
+
while (pid > 1 && out.length < 8 && !out.some((h) => h.pid === pid)) {
|
|
270
|
+
const i = info.get(pid);
|
|
271
|
+
if (!i) break;
|
|
272
|
+
out.push({ pid, startedAt: i.startedAt });
|
|
273
|
+
pid = i.ppid;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
function workspaceWriteScope(plur, ws) {
|
|
278
|
+
return workspaceFolderScope(ws.roots.length > 0 ? ws.roots : [ws.cwd], (dir) => plur.resolveFolderPolicy(dir));
|
|
279
|
+
}
|
|
280
|
+
function workspaceKey(ws) {
|
|
281
|
+
const inputs = ws.roots.length > 0 ? ws.roots : [ws.cwd];
|
|
282
|
+
const real = inputs.map((i) => {
|
|
283
|
+
try {
|
|
284
|
+
return realpathSync.native(i);
|
|
285
|
+
} catch {
|
|
286
|
+
return resolve(i);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return JSON.stringify([ws.roots.length > 0 ? "roots" : "cwd", ...[...new Set(real)].sort()]);
|
|
290
|
+
}
|
|
291
|
+
function isHomeOrAbove(dir) {
|
|
292
|
+
try {
|
|
293
|
+
return coversHomeOrRoot(dir);
|
|
294
|
+
} catch {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function isFilesystemRoot(dir) {
|
|
299
|
+
const r = resolve(dir);
|
|
300
|
+
return dirname2(r) === r;
|
|
301
|
+
}
|
|
302
|
+
function createFolderGate(plur, opts = {}) {
|
|
303
|
+
const sessionId = opts.sessionId ?? `mcp-${randomBytes(8).toString("hex")}`;
|
|
304
|
+
try {
|
|
305
|
+
sweepFolderNonces(plur.storageRoot);
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
const asked = /* @__PURE__ */ new Map();
|
|
309
|
+
const notNow = /* @__PURE__ */ new Set();
|
|
310
|
+
let ended = false;
|
|
311
|
+
const reissueAfter = Math.max(0, FOLDER_NONCE_TTL_MS - 6e4);
|
|
312
|
+
let hosts = null;
|
|
313
|
+
const hostList = () => {
|
|
314
|
+
if (hosts === null) {
|
|
315
|
+
try {
|
|
316
|
+
hosts = (opts.hosts ?? ancestorHosts)();
|
|
317
|
+
} catch {
|
|
318
|
+
hosts = [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return hosts;
|
|
322
|
+
};
|
|
323
|
+
const hosted = /* @__PURE__ */ new Map();
|
|
324
|
+
const notNowAnswer = (folder) => ({
|
|
325
|
+
success: true,
|
|
326
|
+
plur: "off",
|
|
327
|
+
reason: "not-now",
|
|
328
|
+
folder,
|
|
329
|
+
message: `PLUR memory is off in this folder (${JSON.stringify(folder)}) for the rest of this session: the user answered "not now" to the folder question, so nothing was read from or written to memory. This is not an error \u2014 carry on without memory, and do not ask again. The folder is still undecided; a new session will ask.`
|
|
330
|
+
});
|
|
331
|
+
const askFor = (dir, policy) => {
|
|
332
|
+
const outstanding = (nonce) => {
|
|
333
|
+
try {
|
|
334
|
+
return folderNonceOutstanding(plur.storageRoot, sessionId, nonce);
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
let entry = asked.get(dir);
|
|
340
|
+
if (entry?.ask.notNowNonce !== void 0) {
|
|
341
|
+
const notNowNonce = entry.ask.notNowNonce;
|
|
342
|
+
const answered = !outstanding(notNowNonce) && entry.ask.nonces.some((n) => n !== notNowNonce && outstanding(n));
|
|
343
|
+
if (answered) {
|
|
344
|
+
log(`folder ${JSON.stringify(entry.ask.folder)}: the user answered "not now"; memory tools do nothing for the rest of this session.`);
|
|
345
|
+
notNow.add(dir);
|
|
346
|
+
asked.delete(dir);
|
|
347
|
+
return notNowAnswer(entry.ask.folder);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (entry && (!entry.ask.notice && Date.now() - entry.issuedAt > reissueAfter || entry.reason !== policy.reason)) {
|
|
351
|
+
asked.delete(dir);
|
|
352
|
+
entry = void 0;
|
|
353
|
+
}
|
|
354
|
+
if (entry && entry.ask.nonces.some((n) => !outstanding(n))) {
|
|
355
|
+
asked.delete(dir);
|
|
356
|
+
entry = void 0;
|
|
357
|
+
}
|
|
358
|
+
if (!entry && !ended) {
|
|
359
|
+
const shown = hosted.get(dir);
|
|
360
|
+
if (shown) {
|
|
361
|
+
const hostSession = shown.hostSession;
|
|
362
|
+
const inHost = (n) => {
|
|
363
|
+
try {
|
|
364
|
+
return hostSession !== void 0 && folderNonceOutstanding(plur.storageRoot, hostSession, n);
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const nn = shown.notNowNonce;
|
|
370
|
+
if (nn !== void 0 && !inHost(nn) && shown.nonces.some((n) => n !== nn && inHost(n))) {
|
|
371
|
+
log(`folder ${JSON.stringify(shown.folder)}: the user answered "not now"; memory tools do nothing for the rest of this session.`);
|
|
372
|
+
hosted.delete(dir);
|
|
373
|
+
notNow.add(dir);
|
|
374
|
+
return notNowAnswer(shown.folder);
|
|
375
|
+
}
|
|
376
|
+
let still = null;
|
|
377
|
+
try {
|
|
378
|
+
still = hostFolderAsk({ dir, policy, root: plur.storageRoot, hosts: hostList(), plur });
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
if (still && still.nonces.every((n) => shown.nonces.includes(n))) {
|
|
382
|
+
return { success: true, plur: "ask", folder: shown.folder, question: shown.text, answers: shown.answers };
|
|
383
|
+
}
|
|
384
|
+
hosted.delete(dir);
|
|
385
|
+
} else {
|
|
386
|
+
let found = null;
|
|
387
|
+
try {
|
|
388
|
+
const list = hostList();
|
|
389
|
+
if (list.length > 0) found = hostFolderAsk({ dir, policy, root: plur.storageRoot, hosts: list, offerNotNow: true, plur });
|
|
390
|
+
} catch (err) {
|
|
391
|
+
log(`folder question of the host process could not be read (${err?.message ?? err}).`);
|
|
392
|
+
}
|
|
393
|
+
if (found && !found.notice) {
|
|
394
|
+
hosted.set(dir, found);
|
|
395
|
+
return { success: true, plur: "ask", folder: found.folder, question: found.text, answers: found.answers };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!entry && ended) {
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
plur: "off",
|
|
403
|
+
reason: "folder-ask-failed",
|
|
404
|
+
folder: dir,
|
|
405
|
+
message: `PLUR has no decision for this folder (${JSON.stringify(dir)}) and this session is closing, so it did not ask; nothing was read from or written to memory. This is not an error. A new session will ask.`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (!entry) {
|
|
409
|
+
let ask = null;
|
|
410
|
+
try {
|
|
411
|
+
ask = folderAsk({ dir, policy, sessionId, root: plur.storageRoot, plur, claim: () => true, bindSession: true, mcp: true });
|
|
412
|
+
} catch (err) {
|
|
413
|
+
log(`folder question for ${JSON.stringify(dir)} could not be built (${err?.message ?? err}).`);
|
|
414
|
+
}
|
|
415
|
+
if (!ask) {
|
|
416
|
+
return {
|
|
417
|
+
success: true,
|
|
418
|
+
plur: "off",
|
|
419
|
+
reason: "folder-ask-failed",
|
|
420
|
+
folder: dir,
|
|
421
|
+
message: `PLUR has no decision for this folder (${JSON.stringify(dir)}) and could not prepare the question that asks the user, so nothing was read from or written to memory. This is not an error \u2014 carry on without memory. The next call tries again; the user can also decide from a terminal: plur folders set <folder> --on | --off.`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
entry = { ask, issuedAt: Date.now(), ...policy.reason ? { reason: policy.reason } : {} };
|
|
425
|
+
asked.set(dir, entry);
|
|
426
|
+
}
|
|
427
|
+
if (entry.ask.notice) {
|
|
428
|
+
return { success: true, plur: "off", reason: "folder-cannot-be-asked", folder: entry.ask.folder, message: entry.ask.text };
|
|
429
|
+
}
|
|
430
|
+
return { success: true, plur: "ask", folder: entry.ask.folder, question: entry.ask.text, answers: entry.ask.answers };
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
sessionId,
|
|
434
|
+
check(workspace) {
|
|
435
|
+
const off = folderOffAnswer(plur, [.../* @__PURE__ */ new Set([...workspace.roots, workspace.cwd])]);
|
|
436
|
+
if (off) return off;
|
|
437
|
+
const usableRoots = workspace.roots.filter((r) => !isHomeOrAbove(r));
|
|
438
|
+
const askable = usableRoots.length > 0 ? usableRoots : isHomeOrAbove(workspace.cwd) ? [] : [workspace.cwd];
|
|
439
|
+
const on = [];
|
|
440
|
+
for (const dir of askable) {
|
|
441
|
+
if (isFilesystemRoot(dir)) continue;
|
|
442
|
+
let policy;
|
|
443
|
+
try {
|
|
444
|
+
policy = plur.resolveFolderPolicy(dir);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const why = `could not be applied to ${JSON.stringify(dir)} (${err?.message ?? err})`;
|
|
447
|
+
log(`folder map: ${why}; memory tools do nothing.`);
|
|
448
|
+
return unreadable({ file: `${plur.storageRoot}/folders.yaml`, problem: why, fixable: false }, plur.storageRoot);
|
|
449
|
+
}
|
|
450
|
+
if (policy.mode === "ask") {
|
|
451
|
+
if (notNow.has(dir)) return notNowAnswer(asked.get(dir)?.ask.folder ?? dir);
|
|
452
|
+
return askFor(dir, policy);
|
|
453
|
+
}
|
|
454
|
+
on.push({ dir, ...policy.scope ? { scope: policy.scope } : {} });
|
|
455
|
+
}
|
|
456
|
+
const scope = workspaceWriteScope(plur, workspace) ?? void 0;
|
|
457
|
+
for (const { dir } of on) {
|
|
458
|
+
hosted.delete(dir);
|
|
459
|
+
if (!asked.has(dir)) continue;
|
|
460
|
+
asked.delete(dir);
|
|
461
|
+
notNow.delete(dir);
|
|
462
|
+
}
|
|
463
|
+
return { plur: "on", ...scope ? { scope } : {} };
|
|
464
|
+
},
|
|
465
|
+
end() {
|
|
466
|
+
ended = true;
|
|
467
|
+
asked.clear();
|
|
468
|
+
hosted.clear();
|
|
469
|
+
try {
|
|
470
|
+
endFolderNonceSession(plur.storageRoot, sessionId);
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/server.ts
|
|
478
|
+
function serverPidPath(baseDir) {
|
|
479
|
+
return join2(baseDir ?? join2(homedir(), ".plur"), "server.pid");
|
|
480
|
+
}
|
|
481
|
+
function readEnterpriseToken(baseDir) {
|
|
482
|
+
const configPath = join2(baseDir ?? join2(homedir(), ".plur"), "config.json");
|
|
483
|
+
if (!existsSync2(configPath)) return void 0;
|
|
484
|
+
try {
|
|
485
|
+
const cfg = JSON.parse(readFileSync2(configPath, "utf8"));
|
|
486
|
+
const ent = cfg?.enterprise;
|
|
487
|
+
if (!ent || typeof ent.url !== "string" || typeof ent.token !== "string") return void 0;
|
|
488
|
+
return { url: ent.url, token: ent.token, username: ent.username };
|
|
489
|
+
} catch {
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
var _pendingReload = false;
|
|
494
|
+
function isPendingReload() {
|
|
495
|
+
return _pendingReload;
|
|
496
|
+
}
|
|
497
|
+
function clearPendingReload() {
|
|
498
|
+
_pendingReload = false;
|
|
499
|
+
}
|
|
500
|
+
var INSTRUCTIONS = `PLUR is your persistent memory. Corrections, preferences, and conventions persist across sessions as engrams.
|
|
501
|
+
|
|
502
|
+
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.
|
|
503
|
+
|
|
504
|
+
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. A plur_* name missing from tools/list means it MOVED behind plur_admin, not that the MCP is down \u2014 never conclude the server is unavailable from a name-lookup miss. Discover the live surface with plur_admin { action: "help" } (every action with description + argument schema) or plur_doctor (tool_surface). PLUR_TOOL_PROFILE=full exposes everything directly.
|
|
505
|
+
|
|
506
|
+
SESSION LIFECYCLE:
|
|
507
|
+
- 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.
|
|
508
|
+
- Without hooks: call plur_session_start at the start, plur_session_end at the end.
|
|
509
|
+
|
|
510
|
+
DURING the session:
|
|
511
|
+
- When user corrects you ("no, use X not Y") \u2192 call plur_learn immediately
|
|
512
|
+
- When user states a preference ("always X", "never Y") \u2192 call plur_learn immediately
|
|
513
|
+
- When you discover a codebase convention or pattern \u2192 call plur_learn
|
|
514
|
+
|
|
515
|
+
EVERY REPLY: End every reply with one short line: \`Memory \u2014 recalled N \xB7 used: ENG-\u2026, ENG-\u2026 \xB7 written: ENG-\u2026\` (recalled as a count; used and written as ids only, no statements), or \`Memory \u2014 none\`. Only count/list ids you actually saw this turn; never invent an id. Give details only if the user asks.
|
|
516
|
+
|
|
517
|
+
SCOPE SELECTION (set scope PER engram, by content \u2014 not once per session):
|
|
518
|
+
A single session produces engrams that belong in different stores. Choose the
|
|
519
|
+
"scope" on EACH plur_learn call from what the engram is about:
|
|
520
|
+
- Team / engineering / architecture / shared-convention knowledge \u2192 the matching
|
|
521
|
+
team store scope (e.g. "group:<org>/<team>"). plur_session_start lists the
|
|
522
|
+
scopes this install can write to \u2014 use them.
|
|
523
|
+
- Personal preferences, your workflow, or details specific to one project \u2192 the
|
|
524
|
+
default/local scope (or "project:<name>").
|
|
525
|
+
- "global" is for genuinely cross-project facts only (language gotchas, tool
|
|
526
|
+
quirks). Do NOT let team-relevant knowledge fall back to "global" by omitting
|
|
527
|
+
scope \u2014 with a team store configured it never reaches the team store. If no
|
|
528
|
+
scope fits, prefer the project/local default over global.
|
|
529
|
+
|
|
530
|
+
OPTIONAL but improves quality:
|
|
531
|
+
- Call plur_feedback to rate which injected engrams helped (positive/negative)
|
|
532
|
+
- Call plur_recall before answering factual questions \u2014 the answer may be in memory
|
|
533
|
+
|
|
534
|
+
Do not ask permission to use these tools \u2014 they are your memory system.
|
|
535
|
+
|
|
536
|
+
FOLDERS WHERE PLUR IS OFF: the user can turn memory off for a folder (plur folders set <folder> --off). In that folder the memory tools (learn, recall, inject, session start/end, capture, feedback, receipt, \u2026) read and write nothing and answer { plur: "off", message } instead \u2014 not an error. The same answer, with reason "folder-map-unreadable", comes back when the user's folder map is broken, and with reason "workspace-unknown" when your client's workspace roots could not be fetched (that one retries by itself on the next call). Carry on without memory and do not retry or work around it; only the user can turn it back on, from a terminal (the message names the command). For a broken map the message names the line and the problem; when it carries a repair_command (plur folders repair --yes), tell the user what is wrong, show them repair_summary (what the repair changes), and run that command only after they agree. plur_status and plur_doctor keep working.
|
|
537
|
+
|
|
538
|
+
Setup: If this is a fresh install, suggest the user run: npx @plur-ai/mcp init
|
|
539
|
+
This installs hooks for automatic injection + session management. One-time global setup.`;
|
|
540
|
+
var GUIDE_RESOURCE = `# PLUR \u2014 Agent Guide
|
|
541
|
+
|
|
542
|
+
## What is PLUR?
|
|
543
|
+
|
|
544
|
+
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 runs locally (BM25 + embeddings); when an enterprise/remote store is configured AND relevant to the current project, recall additionally makes one live timeout-bounded call per remote host and merges the results \u2014 degradation is surfaced per host via a \`remote_stores\` block, never silent. With no remote store configured, search is fully local with zero API calls.
|
|
545
|
+
|
|
546
|
+
## Quick Start
|
|
547
|
+
|
|
548
|
+
1. \`plur_session_start\` \u2014 start a session, inject relevant context
|
|
549
|
+
2. \`plur_learn\` \u2014 store a new learning
|
|
550
|
+
3. \`plur_feedback\` \u2014 rate injected engrams
|
|
551
|
+
4. \`plur_session_end\` \u2014 capture summary and new learnings
|
|
552
|
+
|
|
553
|
+
## When to Call Each Tool
|
|
554
|
+
|
|
555
|
+
| Trigger | Tool |
|
|
556
|
+
|---------|------|
|
|
557
|
+
| Session starts | \`plur_session_start\` with task description |
|
|
558
|
+
| User corrects you | \`plur_learn\` with the correction |
|
|
559
|
+
| User states preference ("always X", "never Y") | \`plur_learn\` with scope and type |
|
|
560
|
+
| You used a recalled engram successfully | \`plur_feedback\` with "positive" |
|
|
561
|
+
| A recalled engram was wrong or irrelevant | \`plur_feedback\` with "negative" |
|
|
562
|
+
| User says "forget X" or a memory is outdated | \`plur_forget\` |
|
|
563
|
+
| You need to check what's stored | \`plur_status\` or \`plur_packs_list\` |
|
|
564
|
+
| User asks what memory did for them / is memory working | \`plur_receipt\` (relay its \`summary\`; activation_rate is coverage, not quality) |
|
|
565
|
+
| End of session | \`plur_session_end\` with summary and suggestions |
|
|
566
|
+
|
|
567
|
+
## Tool Categories
|
|
568
|
+
|
|
569
|
+
### Session Management
|
|
570
|
+
- **plur_session_start** \u2014 start a session, inject relevant context
|
|
571
|
+
- **plur_session_end** \u2014 end a session, capture summary and new learnings
|
|
572
|
+
|
|
573
|
+
### Core Memory
|
|
574
|
+
- **plur_learn** \u2014 store a correction, preference, or convention
|
|
575
|
+
- **plur_recall** \u2014 hybrid search by default (BM25 + embeddings); pass mode:"keyword" for BM25-only
|
|
576
|
+
- **plur_feedback** \u2014 rate an engram (trains relevance)
|
|
577
|
+
- **plur_forget** \u2014 retire an outdated engram
|
|
578
|
+
- **plur_promote** \u2014 activate a candidate engram (status only \u2014 never changes scope)
|
|
579
|
+
- **plur_rescope** \u2014 move an engram to another scope (e.g. promote a local engram into a team store)
|
|
580
|
+
|
|
581
|
+
### Context Injection
|
|
582
|
+
- **plur_inject** \u2014 select engrams for a task (BM25)
|
|
583
|
+
- **plur_inject_hybrid** \u2014 select engrams for a task (BM25 + embeddings, recommended)
|
|
584
|
+
|
|
585
|
+
### Episodic Timeline
|
|
586
|
+
- **plur_capture** \u2014 record what happened in a session
|
|
587
|
+
- **plur_timeline** \u2014 query past episodes
|
|
588
|
+
|
|
589
|
+
### Knowledge Management
|
|
590
|
+
- **plur_ingest** \u2014 extract engrams from text content
|
|
591
|
+
- **plur_packs_install** \u2014 install curated engram packs
|
|
592
|
+
- **plur_packs_list** \u2014 list installed packs
|
|
593
|
+
- **plur_packs_export** \u2014 export engrams as a shareable pack
|
|
594
|
+
|
|
595
|
+
### Multi-Store
|
|
596
|
+
- **plur_stores_add** \u2014 register an additional engram store
|
|
597
|
+
- **plur_stores_list** \u2014 list all configured stores
|
|
598
|
+
|
|
599
|
+
**Note:** Multi-store is currently config-only. Recall and inject search the primary store. Cross-store search coming in a future release.
|
|
600
|
+
|
|
601
|
+
### Sync & Status
|
|
602
|
+
- **plur_sync** \u2014 sync engrams across devices via git
|
|
603
|
+
- **plur_sync_status** \u2014 check sync state
|
|
604
|
+
- **plur_status** \u2014 system health
|
|
605
|
+
- **plur_receipt** \u2014 counted report of what memory retrieved for the user (local, read-only)
|
|
606
|
+
|
|
607
|
+
## Scoping
|
|
608
|
+
|
|
609
|
+
Use \`scope\` to namespace engrams per project:
|
|
610
|
+
- \`scope: "global"\` \u2014 applies everywhere (default)
|
|
611
|
+
- \`scope: "project:my-app"\` \u2014 applies only to my-app
|
|
612
|
+
- Scoped recall automatically includes global engrams
|
|
613
|
+
|
|
614
|
+
## Storage
|
|
615
|
+
|
|
616
|
+
\`\`\`
|
|
617
|
+
~/.plur/
|
|
618
|
+
\u251C\u2500\u2500 engrams.yaml # learned knowledge
|
|
619
|
+
\u251C\u2500\u2500 episodes.yaml # session timeline
|
|
620
|
+
\u2514\u2500\u2500 config.yaml # settings
|
|
621
|
+
\`\`\`
|
|
622
|
+
|
|
623
|
+
Override with \`PLUR_PATH\` environment variable.
|
|
624
|
+
`;
|
|
625
|
+
var _whenIdle = /* @__PURE__ */ new WeakMap();
|
|
626
|
+
var _endFolderQuestion = /* @__PURE__ */ new WeakMap();
|
|
627
|
+
async function closeWhenIdle(server, timeoutMs) {
|
|
628
|
+
try {
|
|
629
|
+
_endFolderQuestion.get(server)?.();
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
633
|
+
await (_whenIdle.get(server)?.(timeoutMs) ?? Promise.resolve());
|
|
634
|
+
await server.close().catch(() => {
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
var versionRecheckTimer;
|
|
638
|
+
async function createServer(plur, options) {
|
|
639
|
+
const instance = plur ?? new Plur();
|
|
640
|
+
const profile = options?.profile ?? "lean";
|
|
641
|
+
setActiveToolProfile(profile);
|
|
642
|
+
const tools = getToolDefinitions(profile);
|
|
643
|
+
const announceUpdate = (r) => {
|
|
644
|
+
if (r.updateAvailable) {
|
|
645
|
+
console.error(`[plur] Update available: ${r.current} \u2192 ${r.latest}. Run: npm i -g @plur-ai/cli@latest && plur init (configs pin versions)`);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
checkForUpdate("@plur-ai/mcp", VERSION, announceUpdate);
|
|
649
|
+
if (!versionRecheckTimer) {
|
|
650
|
+
versionRecheckTimer = setInterval(() => {
|
|
651
|
+
checkForUpdate("@plur-ai/mcp", VERSION, announceUpdate);
|
|
652
|
+
}, VERSION_CHECK_SUCCESS_TTL_MS);
|
|
653
|
+
versionRecheckTimer.unref?.();
|
|
654
|
+
}
|
|
655
|
+
const server = new Server(
|
|
656
|
+
{ name: "plur-mcp", version: VERSION },
|
|
657
|
+
{
|
|
658
|
+
capabilities: {
|
|
659
|
+
tools: {},
|
|
660
|
+
resources: {},
|
|
661
|
+
prompts: {},
|
|
662
|
+
logging: {}
|
|
663
|
+
},
|
|
664
|
+
instructions: INSTRUCTIONS
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
let inFlight = 0;
|
|
668
|
+
let idleWaiters = [];
|
|
669
|
+
const trackInFlight = async (fn) => {
|
|
670
|
+
inFlight++;
|
|
671
|
+
try {
|
|
672
|
+
return await fn();
|
|
673
|
+
} finally {
|
|
674
|
+
inFlight--;
|
|
675
|
+
if (inFlight === 0) {
|
|
676
|
+
setImmediate(() => {
|
|
677
|
+
if (inFlight === 0) {
|
|
678
|
+
const w = idleWaiters;
|
|
679
|
+
idleWaiters = [];
|
|
680
|
+
w.forEach((f) => f());
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
_whenIdle.set(server, (timeoutMs) => new Promise((resolve2) => {
|
|
687
|
+
if (inFlight === 0) return resolve2();
|
|
688
|
+
const timer = timeoutMs === void 0 ? void 0 : setTimeout(resolve2, timeoutMs);
|
|
689
|
+
timer?.unref?.();
|
|
690
|
+
idleWaiters.push(() => {
|
|
691
|
+
if (timer) clearTimeout(timer);
|
|
692
|
+
resolve2();
|
|
693
|
+
});
|
|
694
|
+
}));
|
|
695
|
+
const workspace = createWorkspaceDirs(server);
|
|
696
|
+
const folderGate = createFolderGate(instance);
|
|
697
|
+
_endFolderQuestion.set(server, () => folderGate.end());
|
|
698
|
+
const priorOnClose = server.onclose;
|
|
699
|
+
server.onclose = () => {
|
|
700
|
+
folderGate.end();
|
|
701
|
+
priorOnClose?.();
|
|
702
|
+
};
|
|
703
|
+
server.setRequestHandler("tools/list", async () => ({
|
|
704
|
+
tools: tools.map((t) => ({
|
|
705
|
+
name: t.name,
|
|
706
|
+
description: t.description,
|
|
707
|
+
inputSchema: t.inputSchema,
|
|
708
|
+
...t.annotations && { annotations: t.annotations }
|
|
709
|
+
}))
|
|
710
|
+
}));
|
|
711
|
+
server.setRequestHandler("tools/call", (request) => trackInFlight(async () => {
|
|
712
|
+
const tool = tools.find((t) => t.name === request.params.name);
|
|
713
|
+
if (!tool) {
|
|
714
|
+
const hidden = getToolDefinitions("full").find((t) => t.name === request.params.name);
|
|
715
|
+
if (hidden) {
|
|
716
|
+
return {
|
|
717
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
718
|
+
error: `Tool "${request.params.name}" exists but is not directly callable under the current tool profile.`,
|
|
719
|
+
success: false,
|
|
720
|
+
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.`
|
|
721
|
+
}) }],
|
|
722
|
+
isError: true
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${request.params.name}`, success: false }) }],
|
|
727
|
+
isError: true
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const gated = tool.name === "plur_admin" ? request.params.arguments?.action : tool.name;
|
|
731
|
+
let admittedWs = null;
|
|
732
|
+
if (typeof gated === "string" && FOLDER_GATED_TOOLS.has(gated)) {
|
|
733
|
+
const ws = await workspace.workspace();
|
|
734
|
+
const decision = ws === null ? workspaceUnknownAnswer() : folderGate.check(ws);
|
|
735
|
+
if (decision.plur !== "on") return { content: [{ type: "text", text: JSON.stringify(decision, null, 2) }] };
|
|
736
|
+
admittedWs = ws;
|
|
737
|
+
}
|
|
738
|
+
mcpCanary.tick();
|
|
739
|
+
try {
|
|
740
|
+
const rawArguments = request.params.arguments;
|
|
741
|
+
let args = rawArguments ?? {};
|
|
742
|
+
const validated = validateToolArgs(tool, args);
|
|
743
|
+
if (!validated.ok) {
|
|
744
|
+
const drop = validated.errorPayload.drop;
|
|
745
|
+
if (drop) {
|
|
746
|
+
recordPayloadDrop(instance.storageRoot, {
|
|
747
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
748
|
+
tool: request.params.name,
|
|
749
|
+
arguments_wire: drop === "whole_payload" ? rawArguments === void 0 ? "absent" : "empty_object" : "partial",
|
|
750
|
+
params_keys: Object.keys(request.params ?? {}),
|
|
751
|
+
received_fields: validated.errorPayload.received_fields,
|
|
752
|
+
missing_fields: validated.errorPayload.missing_fields,
|
|
753
|
+
missing_array_params: validated.missingArrayParams,
|
|
754
|
+
request_id: request.id,
|
|
755
|
+
server_version: VERSION
|
|
756
|
+
});
|
|
757
|
+
try {
|
|
758
|
+
void Promise.resolve(server.sendLoggingMessage({
|
|
759
|
+
level: "warning",
|
|
760
|
+
data: `Payload drop (#772) on ${request.params.name}: arguments ${rawArguments === void 0 ? "key absent from frame" : `arrived with fields [${validated.errorPayload.received_fields.join(", ")}]`}. Recorded in ${payloadDropLogPath(instance.storageRoot)}.`
|
|
761
|
+
})).catch(() => {
|
|
762
|
+
});
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
content: [{ type: "text", text: JSON.stringify(validated.errorPayload) }],
|
|
768
|
+
isError: true
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
args = validated.data;
|
|
772
|
+
const folderContext = {
|
|
773
|
+
resolve: async () => {
|
|
774
|
+
const ws = await workspace.workspace();
|
|
775
|
+
return ws === null ? null : { scope: workspaceWriteScope(instance, ws), key: workspaceKey(ws) };
|
|
776
|
+
},
|
|
777
|
+
...admittedWs !== null ? { admitted: () => ({ scope: workspaceWriteScope(instance, admittedWs), key: workspaceKey(admittedWs) }) } : {}
|
|
778
|
+
};
|
|
779
|
+
args = { ...args, [FOLDER_SCOPE]: folderContext };
|
|
780
|
+
const result = await tool.handler(args, instance);
|
|
781
|
+
let payload = result;
|
|
782
|
+
let resultIsError = false;
|
|
783
|
+
if (result && typeof result === "object" && result._isError === true) {
|
|
784
|
+
resultIsError = true;
|
|
785
|
+
const { _isError, ...rest } = result;
|
|
786
|
+
payload = rest;
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
790
|
+
...resultIsError ? { isError: true } : {}
|
|
791
|
+
};
|
|
792
|
+
} catch (err) {
|
|
793
|
+
const message = String(err?.message ?? err).split("\n", 1)[0];
|
|
794
|
+
server.sendLoggingMessage({ level: "error", data: `Tool ${request.params.name} failed: ${message}` });
|
|
795
|
+
return {
|
|
796
|
+
content: [{ type: "text", text: JSON.stringify({ error: message, success: false }) }],
|
|
797
|
+
isError: true
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}));
|
|
801
|
+
server.setRequestHandler("resources/list", async () => ({
|
|
802
|
+
resources: [
|
|
803
|
+
{
|
|
804
|
+
uri: "plur://guide",
|
|
805
|
+
name: "PLUR Agent Guide",
|
|
806
|
+
description: "Complete reference for all PLUR tools, when to use them, scoping, and storage",
|
|
807
|
+
mimeType: "text/markdown"
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
uri: "plur://status",
|
|
811
|
+
name: "PLUR Status",
|
|
812
|
+
description: "Live system health \u2014 engram count, episode count, pack count, storage path",
|
|
813
|
+
mimeType: "application/json"
|
|
814
|
+
}
|
|
815
|
+
]
|
|
816
|
+
}));
|
|
817
|
+
server.setRequestHandler("resources/read", async (request) => {
|
|
818
|
+
const uri = request.params.uri;
|
|
819
|
+
if (uri === "plur://guide") {
|
|
820
|
+
const cursorNote = options?.profile === "cursor" || options?.profile === "lean" || options?.profile == null ? `
|
|
821
|
+
|
|
822
|
+
## Lean tool profile (default)
|
|
823
|
+
|
|
824
|
+
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.` : "";
|
|
825
|
+
return {
|
|
826
|
+
contents: [{
|
|
827
|
+
uri: "plur://guide",
|
|
828
|
+
mimeType: "text/markdown",
|
|
829
|
+
text: GUIDE_RESOURCE + cursorNote
|
|
830
|
+
}]
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
if (uri === "plur://status") {
|
|
834
|
+
const status = await instance.status();
|
|
835
|
+
return {
|
|
836
|
+
contents: [{
|
|
837
|
+
uri: "plur://status",
|
|
838
|
+
mimeType: "application/json",
|
|
839
|
+
text: JSON.stringify({
|
|
840
|
+
engram_count: status.engram_count,
|
|
841
|
+
episode_count: status.episode_count,
|
|
842
|
+
pack_count: status.pack_count,
|
|
843
|
+
storage_root: status.storage_root,
|
|
844
|
+
version: VERSION
|
|
845
|
+
}, null, 2)
|
|
846
|
+
}]
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
|
|
850
|
+
});
|
|
851
|
+
server.setRequestHandler("prompts/list", async () => ({
|
|
852
|
+
prompts: [
|
|
853
|
+
{
|
|
854
|
+
name: "plur-getting-started",
|
|
855
|
+
description: "Step-by-step guide to set up and start using PLUR memory"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
name: "plur-session-start",
|
|
859
|
+
description: "Load relevant context for a task \u2014 call at the start of each session",
|
|
860
|
+
arguments: [
|
|
861
|
+
{ name: "task", description: "Brief description of the task or goal", required: true },
|
|
862
|
+
{ name: "scope", description: "Project scope (e.g. project:my-app)", required: false }
|
|
863
|
+
]
|
|
864
|
+
}
|
|
865
|
+
]
|
|
866
|
+
}));
|
|
867
|
+
server.setRequestHandler("prompts/get", async (request) => {
|
|
868
|
+
const name = request.params.name;
|
|
869
|
+
if (name === "plur-getting-started") {
|
|
870
|
+
const status = await instance.status();
|
|
871
|
+
return {
|
|
872
|
+
description: "Get started with PLUR memory",
|
|
873
|
+
messages: [{
|
|
874
|
+
role: "user",
|
|
875
|
+
content: {
|
|
876
|
+
type: "text",
|
|
877
|
+
text: `I just set up PLUR. Here's my current status:
|
|
878
|
+
|
|
879
|
+
- Engrams stored: ${status.engram_count}
|
|
880
|
+
- Episodes recorded: ${status.episode_count}
|
|
881
|
+
- Packs installed: ${status.pack_count}
|
|
882
|
+
- Storage: ${status.storage_root}
|
|
883
|
+
|
|
884
|
+
${status.engram_count === 0 ? `I have no memories yet. Help me get started by:
|
|
885
|
+
1. Teaching me a coding preference or convention (I'll use plur_learn)
|
|
886
|
+
2. Then recalling it to verify it works (I'll use plur_recall)
|
|
887
|
+
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.`}`
|
|
888
|
+
}
|
|
889
|
+
}]
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (name === "plur-session-start") {
|
|
893
|
+
const task = request.params.arguments?.task ?? "general work";
|
|
894
|
+
const scope = request.params.arguments?.scope;
|
|
895
|
+
return {
|
|
896
|
+
description: "Load relevant context for this session",
|
|
897
|
+
messages: [{
|
|
898
|
+
role: "user",
|
|
899
|
+
content: {
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `Starting a new session. Task: ${task}${scope ? ` (scope: ${scope})` : ""}
|
|
902
|
+
|
|
903
|
+
Please:
|
|
904
|
+
1. Call plur_recall with query "${task}"${scope ? ` and scope "${scope}"` : ""} to load relevant memories
|
|
905
|
+
2. Review the recalled engrams and apply any relevant conventions or preferences
|
|
906
|
+
3. If any recalled engrams are helpful, call plur_feedback with "positive"
|
|
907
|
+
4. If any are irrelevant, call plur_feedback with "negative"`
|
|
908
|
+
}
|
|
909
|
+
}]
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown prompt: ${name}`);
|
|
913
|
+
});
|
|
914
|
+
return server;
|
|
915
|
+
}
|
|
916
|
+
async function runStdio() {
|
|
917
|
+
const LOG_WINDOW_MS = 6e4;
|
|
918
|
+
const LOG_MAX_PER_WINDOW = 20;
|
|
919
|
+
let logWindowStart = 0;
|
|
920
|
+
let logCount = 0;
|
|
921
|
+
let suppressed = 0;
|
|
922
|
+
const logFault = (kind, err) => {
|
|
923
|
+
const now = Date.now();
|
|
924
|
+
if (now - logWindowStart > LOG_WINDOW_MS) {
|
|
925
|
+
if (suppressed > 0) {
|
|
926
|
+
process.stderr.write(`[plur:mcp] (${suppressed} further fault log lines suppressed in the last minute)
|
|
927
|
+
`);
|
|
928
|
+
}
|
|
929
|
+
logWindowStart = now;
|
|
930
|
+
logCount = 0;
|
|
931
|
+
suppressed = 0;
|
|
932
|
+
}
|
|
933
|
+
if (logCount >= LOG_MAX_PER_WINDOW) {
|
|
934
|
+
suppressed++;
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
logCount++;
|
|
938
|
+
const detail = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
939
|
+
process.stderr.write(`[plur:mcp] ${kind} (#1070): ${detail}
|
|
940
|
+
`);
|
|
941
|
+
};
|
|
942
|
+
process.on("unhandledRejection", (err) => logFault("unhandled rejection \u2014 server stays up", err));
|
|
943
|
+
process.on("uncaughtException", (err) => {
|
|
944
|
+
logFault("UNCAUGHT EXCEPTION \u2014 exiting so lock recovery and the harness can see the failure", err);
|
|
945
|
+
setTimeout(() => process.exit(1), 100);
|
|
946
|
+
});
|
|
947
|
+
const induceMs = Number(process.env.PLUR_TEST_INDUCE_UNHANDLED_REJECTION_MS ?? "");
|
|
948
|
+
if (Number.isFinite(induceMs) && induceMs > 0) {
|
|
949
|
+
setTimeout(() => {
|
|
950
|
+
void Promise.reject(new Error("induced test rejection (#1070)"));
|
|
951
|
+
}, induceMs);
|
|
952
|
+
}
|
|
953
|
+
const induceThrowMs = Number(process.env.PLUR_TEST_INDUCE_UNCAUGHT_EXCEPTION_MS ?? "");
|
|
954
|
+
if (Number.isFinite(induceThrowMs) && induceThrowMs > 0) {
|
|
955
|
+
setTimeout(() => {
|
|
956
|
+
throw new Error("induced test exception (#1070)");
|
|
957
|
+
}, induceThrowMs);
|
|
958
|
+
}
|
|
959
|
+
const profile = resolveToolProfile();
|
|
960
|
+
const server = await createServer(void 0, { profile });
|
|
961
|
+
registerFlushOnExit({});
|
|
962
|
+
try {
|
|
963
|
+
writeFileSync(serverPidPath(), String(process.pid));
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
if (process.platform !== "win32") {
|
|
967
|
+
process.on("SIGUSR1", () => {
|
|
968
|
+
_pendingReload = true;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
const transport = new StdioServerTransport();
|
|
972
|
+
await server.connect(transport);
|
|
973
|
+
let closing = null;
|
|
974
|
+
const closeSession = (timeoutMs) => {
|
|
975
|
+
if (!closing) closing = closeWhenIdle(server, timeoutMs);
|
|
976
|
+
return closing;
|
|
977
|
+
};
|
|
978
|
+
process.stdin.once("end", () => {
|
|
979
|
+
void closeSession();
|
|
980
|
+
});
|
|
981
|
+
process.stdin.once("close", () => {
|
|
982
|
+
void closeSession();
|
|
983
|
+
});
|
|
984
|
+
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
985
|
+
process.once(signal, () => {
|
|
986
|
+
const code = signal === "SIGTERM" ? 143 : 130;
|
|
987
|
+
const timer = setTimeout(() => process.exit(code), 3e3);
|
|
988
|
+
timer.unref?.();
|
|
989
|
+
void closeSession(2e3).finally(() => {
|
|
990
|
+
if (process.stdout.writableLength > 0) process.stdout.once("drain", () => process.exit(code));
|
|
991
|
+
else process.exit(code);
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
export {
|
|
997
|
+
GUIDE_RESOURCE,
|
|
998
|
+
INSTRUCTIONS,
|
|
999
|
+
clearPendingReload,
|
|
1000
|
+
closeWhenIdle,
|
|
1001
|
+
createServer,
|
|
1002
|
+
isPendingReload,
|
|
1003
|
+
readEnterpriseToken,
|
|
1004
|
+
runStdio,
|
|
1005
|
+
serverPidPath
|
|
1006
|
+
};
|