@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
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,56 @@ declare function compareSemver(a: string, b: string): number;
|
|
|
27
27
|
* anchors to start-of-line so a leading space breaks the match.
|
|
28
28
|
*/
|
|
29
29
|
declare function extractManifestVersion(skillMdPath: string): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Hook set for `plur-mcp init`. Mirrors `buildInjectionHooks` in
|
|
32
|
+
* @plur-ai/cli's init.ts, which this package cannot import. The rehydrate
|
|
33
|
+
* entry must stay identical to the cli's; test/init-hooks.test.ts fails if
|
|
34
|
+
* the two diverge (#1279).
|
|
35
|
+
*/
|
|
36
|
+
declare function buildPlurHooks(cli: string): Record<string, HookEntry[]>;
|
|
37
|
+
interface Settings {
|
|
38
|
+
hooks?: Record<string, HookEntry[]>;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
interface HookEntry {
|
|
42
|
+
matcher?: string;
|
|
43
|
+
hooks: Array<{
|
|
44
|
+
type: string;
|
|
45
|
+
command: string;
|
|
46
|
+
args?: string[];
|
|
47
|
+
timeout?: number;
|
|
48
|
+
async?: boolean;
|
|
49
|
+
}>;
|
|
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>;
|
|
58
|
+
/**
|
|
59
|
+
* Merge PLUR hooks into Claude Code settings (#1279). No I/O apart from
|
|
60
|
+
* reading plur-hook.meta.json for the exec-form check.
|
|
61
|
+
* A hook is PLUR's only when isPlurHookSpec says so: the whole command is
|
|
62
|
+
* PLUR's launcher (the shim, with any slash direction or quoting, or the npx
|
|
63
|
+
* fallback, or on Windows the exec form `plur init` writes) followed by a
|
|
64
|
+
* `hook-*` subcommand and plain arguments (decisions H2, F4).
|
|
65
|
+
* - No PLUR hook present: append the full set ('installed').
|
|
66
|
+
* - PLUR hooks present: remove PLUR's hooks from PostCompact, which cannot
|
|
67
|
+
* carry context (#1274), one hook at a time, dropping an entry only when
|
|
68
|
+
* it has no hooks left. If a PLUR rehydrate was among them, add the
|
|
69
|
+
* SessionStart(compact) entry unless one is there ('healed'). A file with
|
|
70
|
+
* PLUR hooks but no rehydrate (the global file `plur init --project` writes) gets
|
|
71
|
+
* none added. Nothing else changes, so a fuller
|
|
72
|
+
* `plur init` install is not replaced by this smaller set.
|
|
73
|
+
* - Otherwise 'already', and the settings come back unchanged.
|
|
74
|
+
* User hooks are never removed or reordered, including a user hook that
|
|
75
|
+
* shares an entry with a PLUR hook.
|
|
76
|
+
*/
|
|
77
|
+
declare function applyPlurHooks(settings: Settings, hooksMap: Record<string, HookEntry[]>): {
|
|
78
|
+
settings: Settings;
|
|
79
|
+
status: 'installed' | 'healed' | 'already';
|
|
80
|
+
};
|
|
30
81
|
|
|
31
|
-
export { compareSemver, extractManifestVersion };
|
|
82
|
+
export { type HookEntry, type Settings, applyPlurHooks, buildPlurHooks, compareSemver, extractManifestVersion, installClaudeMd };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-O2NJFMNP.js";
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
8
|
-
import { join } from "path";
|
|
7
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
8
|
+
import { join as join2 } from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
|
-
import { homedir, platform } from "os";
|
|
10
|
+
import { homedir as homedir2, platform } from "os";
|
|
11
|
+
|
|
12
|
+
// src/hook-command.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { readFileSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
function isPlurHookSpec(spec) {
|
|
17
|
+
const command = typeof spec.command === "string" ? spec.command : "";
|
|
18
|
+
const args = Array.isArray(spec.args) ? spec.args.filter((a) => typeof a === "string") : [];
|
|
19
|
+
if (args.length === 0) return isPlurHookCommand(command);
|
|
20
|
+
const norm = (p) => p.replace(/\\/g, "/").replace(/"/g, "").toLowerCase();
|
|
21
|
+
const cmd = norm(command);
|
|
22
|
+
if (/(^|\/)cmd(\.exe)?$/.test(cmd)) return isPlurHookCommand(args.filter((a) => a.toLowerCase() !== "/c").join(" "));
|
|
23
|
+
if (!/(^|\/)node(\.exe)?$/.test(cmd)) return false;
|
|
24
|
+
if (!/^hook-[a-z0-9][a-z0-9-]*$/.test(args[1] ?? "")) return false;
|
|
25
|
+
const target = normEntry(args[0]);
|
|
26
|
+
return recordedCliEntries().some((e) => normEntry(e) === target);
|
|
27
|
+
}
|
|
28
|
+
function normEntry(p) {
|
|
29
|
+
return p.replace(/\\/g, "/").replace(/"/g, "").toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
function entriesOf(meta) {
|
|
32
|
+
if (!meta || typeof meta !== "object") return [];
|
|
33
|
+
const m = meta;
|
|
34
|
+
if (Array.isArray(m.entrypoints)) return m.entrypoints.filter((e) => typeof e === "string" && e.length > 0);
|
|
35
|
+
return typeof m.entrypoint === "string" && m.entrypoint.length > 0 ? [m.entrypoint] : [];
|
|
36
|
+
}
|
|
37
|
+
function recordedCliEntries() {
|
|
38
|
+
try {
|
|
39
|
+
return entriesOf(JSON.parse(readFileSync(join(homedir(), ".plur", "bin", "plur-hook.meta.json"), "utf8")));
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
var PLAIN = "[^\\s\"'`$&;|<>()]";
|
|
45
|
+
var HOOK_TAIL = `[ \\t]+hook-[a-z0-9][a-z0-9-]*(?:[ \\t]+${PLAIN}+)*[ \\t]*$`;
|
|
46
|
+
var SHIM_FILE = "(?:plur-hook(?:\\.cmd)?|(?<=/(?:\\.plur|plur~\\d+)/bin/)plur-h~\\d+\\.cmd)";
|
|
47
|
+
var SHIM_PATH = [
|
|
48
|
+
`(?:${PLAIN}*/)?${SHIM_FILE}`,
|
|
49
|
+
`"(?:[^"]*/)?${SHIM_FILE}"`
|
|
50
|
+
].join("|");
|
|
51
|
+
var SHIM_FORM = new RegExp(`^(?:&[ \\t]+)?(?:${SHIM_PATH})${HOOK_TAIL}`);
|
|
52
|
+
var NPX_FORM = new RegExp(`^npx(?:[ \\t]+-y)?[ \\t]+@plur-ai/cli(?:@${PLAIN}+)?${HOOK_TAIL}`);
|
|
53
|
+
var HOME_SHIM_TAIL = new RegExp(`^${HOOK_TAIL}`);
|
|
54
|
+
function homeShimPaths() {
|
|
55
|
+
const bin = `${homedir().replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "")}/.plur/bin/`;
|
|
56
|
+
if (/[\r\n]/.test(bin)) return [];
|
|
57
|
+
return [`${bin}plur-hook.cmd`, `${bin}plur-hook`];
|
|
58
|
+
}
|
|
59
|
+
var MAX_HOOK_COMMAND_LENGTH = 8192;
|
|
60
|
+
function isPlurHookCommand(command) {
|
|
61
|
+
return command.length <= MAX_HOOK_COMMAND_LENGTH && matchesPlurHookLauncher(command);
|
|
62
|
+
}
|
|
63
|
+
function matchesPlurHookLauncher(command) {
|
|
64
|
+
const normalised = command.trim().replace(/\\/g, "/").toLowerCase();
|
|
65
|
+
if (SHIM_FORM.test(normalised) || NPX_FORM.test(normalised)) return true;
|
|
66
|
+
return homeShimPaths().some((p) => normalised.startsWith(p) && HOME_SHIM_TAIL.test(normalised.slice(p.length)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/index.ts
|
|
11
70
|
var HELP = `plur-mcp v${VERSION} \u2014 persistent memory for AI agents
|
|
12
71
|
|
|
13
72
|
Usage:
|
|
@@ -49,7 +108,7 @@ function compareSemver(a, b) {
|
|
|
49
108
|
}
|
|
50
109
|
function extractManifestVersion(skillMdPath) {
|
|
51
110
|
try {
|
|
52
|
-
const content =
|
|
111
|
+
const content = readFileSync2(skillMdPath, "utf8");
|
|
53
112
|
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/m);
|
|
54
113
|
if (!fmMatch) return null;
|
|
55
114
|
const versionMatch = fmMatch[1].match(/^version:\s*"?([^"\n]+)"?\s*$/m);
|
|
@@ -59,40 +118,59 @@ function extractManifestVersion(skillMdPath) {
|
|
|
59
118
|
}
|
|
60
119
|
}
|
|
61
120
|
var _shimName = platform() === "win32" ? "plur-hook.cmd" : "plur-hook";
|
|
62
|
-
var _shimCandidate =
|
|
121
|
+
var _shimCandidate = join2(homedir2(), ".plur", "bin", _shimName);
|
|
63
122
|
var CLI = existsSync(_shimCandidate) ? _shimCandidate : "npx @plur-ai/cli";
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
function buildPlurHooks(cli) {
|
|
124
|
+
return {
|
|
125
|
+
// --- Session lifecycle ---
|
|
126
|
+
// Sync with a 20s budget, like `plur init` (#1313): async context reaches
|
|
127
|
+
// Claude Code only at the next safe point, so a first reply without tool
|
|
128
|
+
// calls had no memory. hook-inject exits by itself after 15s, so the
|
|
129
|
+
// timeout sits above that. Keep in step with CLAUDE_INJECT_TIMEOUT_S in
|
|
130
|
+
// @plur-ai/cli's lib/claude-inject-budget.ts.
|
|
131
|
+
UserPromptSubmit: [{
|
|
132
|
+
hooks: [{ type: "command", command: `${cli} hook-inject`, timeout: 20 }]
|
|
133
|
+
}],
|
|
134
|
+
// Re-inject after compaction. SessionStart with matcher "compact" fires
|
|
135
|
+
// right after compaction and can carry context; PostCompact cannot
|
|
136
|
+
// (#1274, #1279). Re-running init moves an old PostCompact entry here.
|
|
137
|
+
SessionStart: [{
|
|
138
|
+
matcher: "compact",
|
|
139
|
+
hooks: [{ type: "command", command: `${cli} hook-inject --rehydrate`, timeout: 20 }]
|
|
140
|
+
}, {
|
|
141
|
+
// `claude --resume` keeps the session id, and SessionEnd below deleted
|
|
142
|
+
// its folder-question nonces: the resumed session is asked again with a
|
|
143
|
+
// fresh nonce (#1347, option C). Identical to `plur init`'s entry.
|
|
144
|
+
matcher: "resume",
|
|
145
|
+
hooks: [{ type: "command", command: `${cli} hook-session-resume`, timeout: 3 }]
|
|
146
|
+
}],
|
|
147
|
+
// Auto-close the memory lifecycle at session end (Claude Code SessionEnd,
|
|
148
|
+
// shipped v1.0.85) — captures a closing episode and cleans up the session
|
|
149
|
+
// checkpoint even if the agent forgot to call plur_session_end (#217).
|
|
150
|
+
SessionEnd: [{
|
|
151
|
+
hooks: [{ type: "command", command: `${cli} hook-session-end`, timeout: 5 }]
|
|
152
|
+
}],
|
|
153
|
+
// --- Contextual injection ---
|
|
154
|
+
PreToolUse: [
|
|
155
|
+
{ matcher: "EnterPlanMode", hooks: [{ type: "command", command: `${cli} hook-inject --event plan_mode`, timeout: 10 }] },
|
|
156
|
+
{ matcher: "Skill", hooks: [{ type: "command", command: `${cli} hook-inject --event skill`, timeout: 10 }] },
|
|
157
|
+
{ matcher: "Agent", hooks: [{ type: "command", command: `${cli} hook-inject --event agent`, timeout: 10 }] },
|
|
158
|
+
{ matcher: "Bash|Edit|Write|Agent", hooks: [{ type: "command", command: `${cli} hook-observe`, timeout: 3 }] }
|
|
159
|
+
],
|
|
160
|
+
PostToolUse: [
|
|
161
|
+
{ matcher: "Bash|Edit|Write|Agent", hooks: [{ type: "command", command: `${cli} hook-observe --post`, timeout: 3 }] }
|
|
162
|
+
],
|
|
163
|
+
SubagentStart: [
|
|
164
|
+
{ matcher: ".*", hooks: [{ type: "command", command: `${cli} hook-inject --event subagent`, timeout: 10 }] }
|
|
165
|
+
],
|
|
166
|
+
Stop: [
|
|
167
|
+
{ matcher: "*", hooks: [{ type: "command", command: `${cli} hook-learn-check`, timeout: 2 }] }
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
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.";
|
|
96
174
|
var CLAUDE_MD_SECTION = `## PLUR Memory
|
|
97
175
|
|
|
98
176
|
You have persistent memory via PLUR. Corrections, preferences, and conventions persist across sessions as engrams.
|
|
@@ -113,42 +191,64 @@ Hooks inject engrams automatically on every first message \u2014 you do not need
|
|
|
113
191
|
|
|
114
192
|
Do not ask permission to use these tools \u2014 they are your memory system.
|
|
115
193
|
|
|
194
|
+
### Memory line on every reply
|
|
195
|
+
|
|
196
|
+
${MEMORY_FOOTER_RULE}
|
|
197
|
+
|
|
116
198
|
### When corrected
|
|
117
199
|
|
|
118
200
|
When the user corrects you ("no, use X not Y", "that's wrong"):
|
|
119
201
|
1. Call \`plur_learn\` immediately \u2014 before continuing the task
|
|
120
202
|
2. Call \`plur_feedback\` with negative signal on the wrong engram if one was injected
|
|
121
203
|
3. Then continue with the corrected approach
|
|
122
|
-
`;
|
|
123
|
-
function installClaudeMd() {
|
|
124
|
-
const marker = "## PLUR Memory";
|
|
125
|
-
const projectClaudeMd = join(process.cwd(), "CLAUDE.md");
|
|
126
|
-
const globalClaudeMd = join(homedir(), "CLAUDE.md");
|
|
127
|
-
const claudeMdPath = existsSync(projectClaudeMd) ? projectClaudeMd : existsSync(globalClaudeMd) ? globalClaudeMd : projectClaudeMd;
|
|
128
|
-
if (existsSync(claudeMdPath)) {
|
|
129
|
-
const content = readFileSync(claudeMdPath, "utf8");
|
|
130
|
-
if (content.includes(marker)) {
|
|
131
|
-
return `already in ${claudeMdPath}`;
|
|
132
|
-
}
|
|
133
|
-
writeFileSync(claudeMdPath, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION);
|
|
134
|
-
return `added to ${claudeMdPath}`;
|
|
135
|
-
}
|
|
136
|
-
writeFileSync(claudeMdPath, `# CLAUDE.md
|
|
137
204
|
|
|
138
|
-
${
|
|
139
|
-
|
|
205
|
+
${PLUR_INSTRUCTIONS_MARKER}
|
|
206
|
+
`;
|
|
207
|
+
function defaultClaudeMdPath() {
|
|
208
|
+
const projectClaudeMd = join2(process.cwd(), "CLAUDE.md");
|
|
209
|
+
const globalClaudeMd = join2(homedir2(), "CLAUDE.md");
|
|
210
|
+
return existsSync(projectClaudeMd) ? projectClaudeMd : existsSync(globalClaudeMd) ? globalClaudeMd : projectClaudeMd;
|
|
140
211
|
}
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
147
247
|
}
|
|
148
248
|
function readJsonObjectForWrite(path) {
|
|
149
249
|
if (!existsSync(path)) return { data: {}, ok: true };
|
|
150
250
|
try {
|
|
151
|
-
const parsed = JSON.parse(
|
|
251
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
152
252
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
153
253
|
return { data: parsed, ok: true };
|
|
154
254
|
}
|
|
@@ -156,64 +256,125 @@ function readJsonObjectForWrite(path) {
|
|
|
156
256
|
}
|
|
157
257
|
return { data: {}, ok: false };
|
|
158
258
|
}
|
|
159
|
-
function
|
|
160
|
-
const { data, ok } = readJsonObjectForWrite(configPath);
|
|
161
|
-
if (!ok) {
|
|
162
|
-
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\``;
|
|
163
|
-
}
|
|
164
|
-
const config = data;
|
|
165
|
-
const servers = config.mcpServers ?? {};
|
|
259
|
+
function healRaceyEntry(servers) {
|
|
166
260
|
const existing = servers.plur;
|
|
167
|
-
if (existing)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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`;
|
|
297
|
+
}
|
|
298
|
+
return { ok: true, status };
|
|
299
|
+
}
|
|
300
|
+
function isPlurCommandHook(h) {
|
|
301
|
+
return typeof h.command === "string" && isPlurHookSpec({ command: h.command, args: h.args });
|
|
302
|
+
}
|
|
303
|
+
function isPlurHook(entry) {
|
|
304
|
+
return (entry.hooks ?? []).some(isPlurCommandHook);
|
|
305
|
+
}
|
|
306
|
+
var REHYDRATE = /(?:^|\s)hook-inject\s+--rehydrate(?:\s|$)/;
|
|
307
|
+
function launchLine(h) {
|
|
308
|
+
const args = Array.isArray(h.args) ? h.args.filter((a) => typeof a === "string") : [];
|
|
309
|
+
return [h.command, ...args].join(" ");
|
|
310
|
+
}
|
|
311
|
+
function isPlurRehydrateHook(h) {
|
|
312
|
+
return isPlurCommandHook(h) && REHYDRATE.test(launchLine(h).replace(/\\/g, "/").toLowerCase());
|
|
313
|
+
}
|
|
314
|
+
function isPlurRehydrate(entry) {
|
|
315
|
+
return (entry.hooks ?? []).some(isPlurRehydrateHook);
|
|
316
|
+
}
|
|
317
|
+
function hasPlurSubcommand(entries, subcommand) {
|
|
318
|
+
const re = new RegExp(`(?:^|\\s)${subcommand}(?:\\s|$)`);
|
|
319
|
+
return (entries ?? []).some((e) => (e.hooks ?? []).some((h) => isPlurCommandHook(h) && re.test(h.command.replace(/\\/g, "/").toLowerCase())));
|
|
320
|
+
}
|
|
321
|
+
var isResumeEntry = (e) => e.matcher === "resume";
|
|
322
|
+
function stripPlurHooks(entries) {
|
|
323
|
+
const kept = [];
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
if (!isPlurHook(entry)) {
|
|
326
|
+
kept.push(entry);
|
|
327
|
+
continue;
|
|
178
328
|
}
|
|
179
|
-
|
|
329
|
+
const rest = entry.hooks.filter((h) => !isPlurCommandHook(h));
|
|
330
|
+
if (rest.length > 0) kept.push({ ...entry, hooks: rest });
|
|
180
331
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
332
|
+
return kept;
|
|
333
|
+
}
|
|
334
|
+
function applyPlurHooks(settings, hooksMap) {
|
|
335
|
+
const hooks = { ...settings.hooks ?? {} };
|
|
336
|
+
const installed = Object.values(hooks).some((entries) => (entries ?? []).some(isPlurHook));
|
|
337
|
+
if (!installed) {
|
|
338
|
+
for (const [event, entries] of Object.entries(hooksMap)) {
|
|
339
|
+
hooks[event] = [...hooks[event] ?? [], ...entries];
|
|
340
|
+
}
|
|
341
|
+
return { settings: { ...settings, hooks }, status: "installed" };
|
|
342
|
+
}
|
|
343
|
+
let changed = false;
|
|
344
|
+
let movedRehydrate = false;
|
|
345
|
+
if (hooks.PostCompact?.some(isPlurHook)) {
|
|
346
|
+
movedRehydrate = hooks.PostCompact.some(isPlurRehydrate);
|
|
347
|
+
const kept = stripPlurHooks(hooks.PostCompact);
|
|
348
|
+
if (kept.length > 0) hooks.PostCompact = kept;
|
|
349
|
+
else delete hooks.PostCompact;
|
|
350
|
+
changed = true;
|
|
351
|
+
}
|
|
352
|
+
if (movedRehydrate && !(hooks.SessionStart ?? []).some(isPlurRehydrate)) {
|
|
353
|
+
hooks.SessionStart = [...hooks.SessionStart ?? [], ...(hooksMap.SessionStart ?? []).filter((e) => !isResumeEntry(e))];
|
|
354
|
+
changed = true;
|
|
355
|
+
}
|
|
356
|
+
if (hasPlurSubcommand(hooks.SessionEnd, "hook-session-end") && !hasPlurSubcommand(hooks.SessionStart, "hook-session-resume")) {
|
|
357
|
+
const resume = (hooksMap.SessionStart ?? []).filter(isResumeEntry);
|
|
358
|
+
if (resume.length > 0) {
|
|
359
|
+
hooks.SessionStart = [...hooks.SessionStart ?? [], ...resume];
|
|
360
|
+
changed = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return changed ? { settings: { ...settings, hooks }, status: "healed" } : { settings, status: "already" };
|
|
187
364
|
}
|
|
188
365
|
function installHooks() {
|
|
189
|
-
const
|
|
190
|
-
const globalSettings = join(homedir(), ".claude", "settings.json");
|
|
191
|
-
const settingsPath = existsSync(join(process.cwd(), ".claude")) ? projectSettings : globalSettings;
|
|
366
|
+
const settingsPath = join2(homedir2(), ".claude", "settings.json");
|
|
192
367
|
const settingsRead = readJsonObjectForWrite(settingsPath);
|
|
193
368
|
if (!settingsRead.ok) {
|
|
194
369
|
return `skipped \u2014 ${settingsPath} exists but is not a JSON object; writing would discard your other settings. Fix it by hand, then re-run \`plur-mcp init\``;
|
|
195
370
|
}
|
|
196
371
|
const settings = settingsRead.data;
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const h of entry.hooks ?? []) {
|
|
201
|
-
if (h.command.includes("@plur-ai/cli")) {
|
|
202
|
-
return `already installed in ${settingsPath}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
const existing = settings.hooks ?? {};
|
|
208
|
-
const merged = { ...existing };
|
|
209
|
-
for (const [event, newEntries] of Object.entries(PLUR_HOOKS)) {
|
|
210
|
-
merged[event] = [...merged[event] ?? [], ...newEntries];
|
|
211
|
-
}
|
|
212
|
-
settings.hooks = merged;
|
|
213
|
-
const dir = join(settingsPath, "..");
|
|
372
|
+
const { settings: next, status } = applyPlurHooks(settings, PLUR_HOOKS);
|
|
373
|
+
if (status === "already") return `already installed in ${settingsPath}`;
|
|
374
|
+
const dir = join2(settingsPath, "..");
|
|
214
375
|
mkdirSync(dir, { recursive: true });
|
|
215
|
-
writeFileSync(settingsPath, JSON.stringify(
|
|
216
|
-
return `installed in ${settingsPath}`;
|
|
376
|
+
writeFileSync(settingsPath, JSON.stringify(next, null, 2) + "\n");
|
|
377
|
+
return status === "healed" ? `updated PLUR hooks (SessionStart compact/resume) in ${settingsPath}` : `installed in ${settingsPath}`;
|
|
217
378
|
}
|
|
218
379
|
async function runInit() {
|
|
219
380
|
const results = [];
|
|
@@ -231,36 +392,37 @@ async function runInit() {
|
|
|
231
392
|
} catch {
|
|
232
393
|
}
|
|
233
394
|
results.push(`Search: ${searchMode}`);
|
|
234
|
-
const
|
|
235
|
-
const mcpStatus =
|
|
395
|
+
const mcp = await installClaudeUserMcp();
|
|
396
|
+
const mcpStatus = mcp.status;
|
|
397
|
+
if (!mcp.ok) process.exitCode = 1;
|
|
236
398
|
results.push(`MCP: ${mcpStatus}`);
|
|
237
399
|
const hooksStatus = installHooks();
|
|
238
400
|
results.push(`Hooks: ${hooksStatus}`);
|
|
239
|
-
const claudeMdStatus = installClaudeMd();
|
|
401
|
+
const claudeMdStatus = await installClaudeMd();
|
|
240
402
|
results.push(`CLAUDE.md: ${claudeMdStatus}`);
|
|
241
403
|
const { Plur } = await import("@plur-ai/core");
|
|
242
404
|
const plur = new Plur({ path: paths.root });
|
|
243
|
-
const bundledPacksDir =
|
|
405
|
+
const bundledPacksDir = join2(fileURLToPath(import.meta.url), "..", "..", "packs");
|
|
244
406
|
let packsStatus = "no bundled packs found";
|
|
245
407
|
if (existsSync(bundledPacksDir)) {
|
|
246
408
|
const installed = plur.listPacks();
|
|
247
409
|
const installedByName = new Map(installed.map((p) => [p.name, p.manifest?.version]));
|
|
248
|
-
const entries = readdirSync(bundledPacksDir).filter((e) => statSync(
|
|
410
|
+
const entries = readdirSync(bundledPacksDir).filter((e) => statSync(join2(bundledPacksDir, e)).isDirectory());
|
|
249
411
|
const newPacks = [];
|
|
250
412
|
const upgradedPacks = [];
|
|
251
413
|
for (const entry of entries) {
|
|
252
|
-
const bundledManifestPath =
|
|
414
|
+
const bundledManifestPath = join2(bundledPacksDir, entry, "SKILL.md");
|
|
253
415
|
const bundledVersion = existsSync(bundledManifestPath) ? extractManifestVersion(bundledManifestPath) : null;
|
|
254
416
|
const installedVersion = installedByName.get(entry);
|
|
255
417
|
if (!installedByName.has(entry)) {
|
|
256
418
|
try {
|
|
257
|
-
await plur.installPack(
|
|
419
|
+
await plur.installPack(join2(bundledPacksDir, entry));
|
|
258
420
|
newPacks.push(entry);
|
|
259
421
|
} catch {
|
|
260
422
|
}
|
|
261
423
|
} else if (bundledVersion && (!installedVersion || compareSemver(bundledVersion, installedVersion) > 0)) {
|
|
262
424
|
try {
|
|
263
|
-
await plur.installPack(
|
|
425
|
+
await plur.installPack(join2(bundledPacksDir, entry));
|
|
264
426
|
upgradedPacks.push(
|
|
265
427
|
`${entry} ${installedVersion ?? "unknown"}\u2192${bundledVersion}`
|
|
266
428
|
);
|
|
@@ -295,7 +457,7 @@ async function runInit() {
|
|
|
295
457
|
}
|
|
296
458
|
}
|
|
297
459
|
async function runPacks() {
|
|
298
|
-
const plurPath = process.env.PLUR_PATH ??
|
|
460
|
+
const plurPath = process.env.PLUR_PATH ?? join2(homedir2(), ".plur");
|
|
299
461
|
const { Plur } = await import("@plur-ai/core");
|
|
300
462
|
const { packsCommand } = await import("./packs-cli-XXZTSBHW.js");
|
|
301
463
|
const plur = new Plur({ path: plurPath });
|
|
@@ -316,14 +478,14 @@ if (arg === "--version" || arg === "-v") {
|
|
|
316
478
|
}
|
|
317
479
|
if (arg === "init") {
|
|
318
480
|
await runInit();
|
|
319
|
-
process.exit(0);
|
|
481
|
+
process.exit(process.exitCode ?? 0);
|
|
320
482
|
}
|
|
321
483
|
if (arg === "packs") {
|
|
322
484
|
await runPacks();
|
|
323
485
|
process.exit(0);
|
|
324
486
|
}
|
|
325
487
|
if (arg === "serve" || arg === void 0) {
|
|
326
|
-
const { runStdio } = await import("./server-
|
|
488
|
+
const { runStdio } = await import("./server-EHKHU4JH.js");
|
|
327
489
|
runStdio().catch((err) => {
|
|
328
490
|
console.error("Failed to start PLUR MCP server:", err);
|
|
329
491
|
process.exit(1);
|
|
@@ -334,6 +496,9 @@ Run plur-mcp --help for usage.`);
|
|
|
334
496
|
process.exit(1);
|
|
335
497
|
}
|
|
336
498
|
export {
|
|
499
|
+
applyPlurHooks,
|
|
500
|
+
buildPlurHooks,
|
|
337
501
|
compareSemver,
|
|
338
|
-
extractManifestVersion
|
|
502
|
+
extractManifestVersion,
|
|
503
|
+
installClaudeMd
|
|
339
504
|
};
|