@plur-ai/mcp 0.21.0 → 0.21.2

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.
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.21.0";
2
+ var VERSION = "0.21.2";
3
3
 
4
4
  export {
5
5
  VERSION
package/dist/index.d.ts CHANGED
@@ -48,6 +48,13 @@ interface HookEntry {
48
48
  async?: boolean;
49
49
  }>;
50
50
  }
51
+ /**
52
+ * Write the PLUR section into CLAUDE.md with core's `upsertInstructionSection`
53
+ * — the same logic as `plur init`: only a section PLUR shipped is replaced,
54
+ * one the user wrote or edited is kept and the new one added beside it, and a
55
+ * timestamped backup precedes any change to an existing file.
56
+ */
57
+ declare function installClaudeMd(claudeMdPath?: string): Promise<string>;
51
58
  /**
52
59
  * Merge PLUR hooks into Claude Code settings (#1279). No I/O apart from
53
60
  * reading plur-hook.meta.json for the exec-form check.
@@ -72,4 +79,4 @@ declare function applyPlurHooks(settings: Settings, hooksMap: Record<string, Hoo
72
79
  status: 'installed' | 'healed' | 'already';
73
80
  };
74
81
 
75
- export { type HookEntry, type Settings, applyPlurHooks, buildPlurHooks, compareSemver, extractManifestVersion };
82
+ export { type HookEntry, type Settings, applyPlurHooks, buildPlurHooks, compareSemver, extractManifestVersion, installClaudeMd };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-WA3JKTZ7.js";
4
+ } from "./chunk-OQDR6UF6.js";
5
5
 
6
6
  // src/index.ts
7
7
  import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
@@ -169,6 +169,8 @@ function buildPlurHooks(cli) {
169
169
  };
170
170
  }
171
171
  var PLUR_HOOKS = buildPlurHooks(CLI);
172
+ var PLUR_INSTRUCTIONS_MARKER = "<!-- plur-instructions-v4 -->";
173
+ var MEMORY_FOOTER_RULE = "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.";
172
174
  var CLAUDE_MD_SECTION = `## PLUR Memory
173
175
 
174
176
  You have persistent memory via PLUR. Corrections, preferences, and conventions persist across sessions as engrams.
@@ -189,37 +191,59 @@ Hooks inject engrams automatically on every first message \u2014 you do not need
189
191
 
190
192
  Do not ask permission to use these tools \u2014 they are your memory system.
191
193
 
194
+ ### Memory line on every reply
195
+
196
+ ${MEMORY_FOOTER_RULE}
197
+
192
198
  ### When corrected
193
199
 
194
200
  When the user corrects you ("no, use X not Y", "that's wrong"):
195
201
  1. Call \`plur_learn\` immediately \u2014 before continuing the task
196
202
  2. Call \`plur_feedback\` with negative signal on the wrong engram if one was injected
197
203
  3. Then continue with the corrected approach
204
+
205
+ ${PLUR_INSTRUCTIONS_MARKER}
198
206
  `;
199
- function installClaudeMd() {
200
- const marker = "## PLUR Memory";
207
+ function defaultClaudeMdPath() {
201
208
  const projectClaudeMd = join2(process.cwd(), "CLAUDE.md");
202
209
  const globalClaudeMd = join2(homedir2(), "CLAUDE.md");
203
- const claudeMdPath = existsSync(projectClaudeMd) ? projectClaudeMd : existsSync(globalClaudeMd) ? globalClaudeMd : projectClaudeMd;
204
- if (existsSync(claudeMdPath)) {
205
- const content = readFileSync2(claudeMdPath, "utf8");
206
- if (content.includes(marker)) {
207
- return `already in ${claudeMdPath}`;
208
- }
209
- writeFileSync(claudeMdPath, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION);
210
- return `added to ${claudeMdPath}`;
211
- }
212
- writeFileSync(claudeMdPath, `# CLAUDE.md
213
-
214
- ${CLAUDE_MD_SECTION}`);
215
- return `created ${claudeMdPath}`;
210
+ return existsSync(projectClaudeMd) ? projectClaudeMd : existsSync(globalClaudeMd) ? globalClaudeMd : projectClaudeMd;
216
211
  }
217
- function findMcpConfig() {
218
- const projectMcp = join2(process.cwd(), ".mcp.json");
219
- if (existsSync(projectMcp)) return projectMcp;
220
- const globalMcp = join2(homedir2(), ".claude", "mcp.json");
221
- if (existsSync(globalMcp)) return globalMcp;
222
- return projectMcp;
212
+ async function installClaudeMd(claudeMdPath = defaultClaudeMdPath()) {
213
+ const { upsertInstructionSection, writeWithBackup, SHIPPED_PLUR_SECTIONS } = await import("@plur-ai/core");
214
+ const existing = existsSync(claudeMdPath) ? readFileSync2(claudeMdPath, "utf8") : null;
215
+ const r = upsertInstructionSection(existing, {
216
+ section: CLAUDE_MD_SECTION,
217
+ title: "# CLAUDE.md",
218
+ heading: "## PLUR Memory",
219
+ marker: PLUR_INSTRUCTIONS_MARKER,
220
+ shipped: SHIPPED_PLUR_SECTIONS
221
+ });
222
+ if (r.status === "skipped") return `not written to ${claudeMdPath}: ${r.skipReason}`;
223
+ let backup;
224
+ try {
225
+ backup = r.status === "already" ? null : writeWithBackup(claudeMdPath, r.content, existing);
226
+ } catch (err) {
227
+ if (err.code === "PLUR_REFUSED") return `not written to ${claudeMdPath}: ${err.message}`;
228
+ throw err;
229
+ }
230
+ const head = {
231
+ created: `created ${claudeMdPath}`,
232
+ added: `added to ${claudeMdPath}`,
233
+ upgraded: `upgraded in ${claudeMdPath}`,
234
+ already: `already in ${claudeMdPath}`
235
+ }[r.status];
236
+ const notes = [];
237
+ if (backup) notes.push(`backup: ${backup}`);
238
+ if (r.keptSections > 0) {
239
+ notes.push(
240
+ `left ${r.keptSections} older "## PLUR Memory" section${r.keptSections === 1 ? "" : "s"} untouched because ${r.keptSections === 1 ? "it has" : "they have"} text PLUR did not write \u2014 remove it yourself once you have kept what you need`
241
+ );
242
+ }
243
+ if (r.closedOpenBlock) {
244
+ notes.push(`closed the ${r.closedOpenBlock === "fence" ? "code block" : "HTML comment"} left open at the end of the file before adding the section`);
245
+ }
246
+ return notes.length ? `${head} (${notes.join("; ")})` : head;
223
247
  }
224
248
  function readJsonObjectForWrite(path) {
225
249
  if (!existsSync(path)) return { data: {}, ok: true };
@@ -232,34 +256,46 @@ function readJsonObjectForWrite(path) {
232
256
  }
233
257
  return { data: {}, ok: false };
234
258
  }
235
- function writeMcpConfig(configPath) {
236
- const { data, ok } = readJsonObjectForWrite(configPath);
237
- if (!ok) {
238
- return `skipped \u2014 ${configPath} exists but is not a JSON object; writing would discard your other MCP servers. Fix it by hand, then re-run \`plur-mcp init\``;
239
- }
240
- const config = data;
241
- const servers = config.mcpServers ?? {};
259
+ function healRaceyEntry(servers) {
242
260
  const existing = servers.plur;
243
- if (existing) {
244
- const args = existing.args ?? [];
245
- const spec = args.filter((a) => a !== "-y" && !a.startsWith("-"))[0] ?? "";
246
- if (existing.command === "npx" && /^@plur-ai\/mcp(@latest)?$/.test(spec)) {
247
- const healed = { ...existing, ...MCP_SERVER_CONFIG };
248
- if (JSON.stringify(healed) !== JSON.stringify(existing)) {
249
- servers.plur = healed;
250
- config.mcpServers = servers;
251
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
252
- return `upgraded stale npx entry in ${configPath}`;
253
- }
254
- }
255
- return `already configured in ${configPath}`;
261
+ if (!existing || typeof existing !== "object") return false;
262
+ const args = existing.args ?? [];
263
+ const spec = args.filter((a) => a !== "-y" && !a.startsWith("-"))[0] ?? "";
264
+ if (existing.command !== "npx" || !/^@plur-ai\/mcp(@latest)?$/.test(spec)) return false;
265
+ const healed = { ...existing, ...MCP_SERVER_CONFIG };
266
+ if (JSON.stringify(healed) === JSON.stringify(existing)) return false;
267
+ servers.plur = healed;
268
+ return true;
269
+ }
270
+ async function installClaudeUserMcp() {
271
+ const { writeWithBackup, registerClaudeUserMcp } = await import("@plur-ai/core");
272
+ const legacyPath = join2(homedir2(), ".claude", "mcp.json");
273
+ const legacyRaw = existsSync(legacyPath) ? readFileSync2(legacyPath, "utf8") : null;
274
+ const legacy = readJsonObjectForWrite(legacyPath);
275
+ const oldServers = legacy.ok ? legacy.data.mcpServers : void 0;
276
+ const oldEntry = oldServers && typeof oldServers === "object" && !Array.isArray(oldServers) ? oldServers.plur : void 0;
277
+ const r = registerClaudeUserMcp({
278
+ userPath: join2(homedir2(), ".claude.json"),
279
+ entry: () => ({ ...MCP_SERVER_CONFIG }),
280
+ heal: (config) => healRaceyEntry(config.mcpServers) ? "upgraded stale npx entry" : null,
281
+ legacyEntry: oldEntry,
282
+ legacyPath,
283
+ rerunCommand: "plur-mcp init"
284
+ });
285
+ if (!r.ok || oldEntry === void 0) return { ok: r.ok, status: r.message };
286
+ let status = r.message;
287
+ const next = { ...legacy.data };
288
+ const rest = { ...oldServers };
289
+ delete rest.plur;
290
+ if (Object.keys(rest).length > 0) next.mcpServers = rest;
291
+ else delete next.mcpServers;
292
+ try {
293
+ const backup = writeWithBackup(legacyPath, JSON.stringify(next, null, 2) + "\n", legacyRaw);
294
+ status += `; removed the unused entry from ${legacyPath}${backup ? ` (backup: ${backup})` : ""}`;
295
+ } catch (err) {
296
+ status += `; could not remove the unused entry from ${legacyPath} (${err.message}) \u2014 Claude Code ignores it, remove it by hand`;
256
297
  }
257
- servers.plur = MCP_SERVER_CONFIG;
258
- config.mcpServers = servers;
259
- const dir = join2(configPath, "..");
260
- mkdirSync(dir, { recursive: true });
261
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
262
- return `added to ${configPath}`;
298
+ return { ok: true, status };
263
299
  }
264
300
  function isPlurCommandHook(h) {
265
301
  return typeof h.command === "string" && isPlurHookSpec({ command: h.command, args: h.args });
@@ -356,12 +392,13 @@ async function runInit() {
356
392
  } catch {
357
393
  }
358
394
  results.push(`Search: ${searchMode}`);
359
- const mcpConfigPath = findMcpConfig();
360
- const mcpStatus = writeMcpConfig(mcpConfigPath);
395
+ const mcp = await installClaudeUserMcp();
396
+ const mcpStatus = mcp.status;
397
+ if (!mcp.ok) process.exitCode = 1;
361
398
  results.push(`MCP: ${mcpStatus}`);
362
399
  const hooksStatus = installHooks();
363
400
  results.push(`Hooks: ${hooksStatus}`);
364
- const claudeMdStatus = installClaudeMd();
401
+ const claudeMdStatus = await installClaudeMd();
365
402
  results.push(`CLAUDE.md: ${claudeMdStatus}`);
366
403
  const { Plur } = await import("@plur-ai/core");
367
404
  const plur = new Plur({ path: paths.root });
@@ -441,14 +478,14 @@ if (arg === "--version" || arg === "-v") {
441
478
  }
442
479
  if (arg === "init") {
443
480
  await runInit();
444
- process.exit(0);
481
+ process.exit(process.exitCode ?? 0);
445
482
  }
446
483
  if (arg === "packs") {
447
484
  await runPacks();
448
485
  process.exit(0);
449
486
  }
450
487
  if (arg === "serve" || arg === void 0) {
451
- const { runStdio } = await import("./server-EUMEOVXB.js");
488
+ const { runStdio } = await import("./server-QOHVV2Z7.js");
452
489
  runStdio().catch((err) => {
453
490
  console.error("Failed to start PLUR MCP server:", err);
454
491
  process.exit(1);
@@ -462,5 +499,6 @@ export {
462
499
  applyPlurHooks,
463
500
  buildPlurHooks,
464
501
  compareSemver,
465
- extractManifestVersion
502
+ extractManifestVersion,
503
+ installClaudeMd
466
504
  };