@pyxmate/memory 1.27.0 → 1.27.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.
@@ -0,0 +1,793 @@
1
+ import {
2
+ PERSISTENT_MEMORY_SECTION
3
+ } from "./chunk-QD7G23CN.mjs";
4
+ import {
5
+ EXIT
6
+ } from "./chunk-4DXEQC6X.mjs";
7
+
8
+ // src/cli/commands/mcp-install.ts
9
+ import { spawnSync as nodeSpawnSync } from "child_process";
10
+ import { existsSync as existsSync3 } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+
14
+ // src/cli/commands/mcp-install-merge.ts
15
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
16
+ import { dirname } from "path";
17
+ function mergeWriteJsonMcpEntry(opts) {
18
+ const { filePath, serverName, entry } = opts;
19
+ let existingDoc = {};
20
+ if (existsSync(filePath)) {
21
+ const raw = readFileSync(filePath, "utf8").trim();
22
+ if (raw.length > 0) {
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
26
+ return {
27
+ exitCode: EXIT.USAGE,
28
+ status: "parse-error",
29
+ message: `Refusing to write: ${filePath} is not a JSON object at its root.
30
+ Fix the file manually, then re-run.
31
+ `
32
+ };
33
+ }
34
+ existingDoc = parsed;
35
+ } catch (err) {
36
+ const reason = err instanceof Error ? err.message : String(err);
37
+ return {
38
+ exitCode: EXIT.USAGE,
39
+ status: "parse-error",
40
+ message: `Refusing to write: ${filePath} contains invalid JSON (${reason}).
41
+ Fix the file manually, then re-run.
42
+ `
43
+ };
44
+ }
45
+ }
46
+ }
47
+ const rawServers = existingDoc.mcpServers;
48
+ const servers = rawServers !== null && typeof rawServers === "object" && !Array.isArray(rawServers) ? { ...rawServers } : {};
49
+ const existingEntry = servers[serverName];
50
+ if (existingEntry !== void 0) {
51
+ if (entriesEqual(existingEntry, entry)) {
52
+ return {
53
+ exitCode: EXIT.OK,
54
+ status: "already-installed",
55
+ message: `pyx-memory is already installed in ${filePath} (no changes written).
56
+ `
57
+ };
58
+ }
59
+ return {
60
+ exitCode: EXIT.USAGE,
61
+ status: "conflict",
62
+ message: `Refusing to overwrite existing \`${serverName}\` entry in ${filePath}.
63
+
64
+ Found:
65
+ ${indent(JSON.stringify(existingEntry, null, 2))}
66
+
67
+ Proposed:
68
+ ${indent(JSON.stringify(entry, null, 2))}
69
+
70
+ Remove or rename the existing entry, then re-run.
71
+ `
72
+ };
73
+ }
74
+ servers[serverName] = entry;
75
+ const nextDoc = { ...existingDoc, mcpServers: servers };
76
+ const payload = `${JSON.stringify(nextDoc, null, 2)}
77
+ `;
78
+ const parent = dirname(filePath);
79
+ if (!existsSync(parent)) {
80
+ mkdirSync(parent, { recursive: true });
81
+ }
82
+ const tmpPath = `${filePath}.tmp`;
83
+ writeFileSync(tmpPath, payload, { mode: 384 });
84
+ renameSync(tmpPath, filePath);
85
+ return {
86
+ exitCode: EXIT.OK,
87
+ status: "installed",
88
+ message: `Installed \`${serverName}\` MCP server entry in ${filePath}.
89
+ `
90
+ };
91
+ }
92
+ function entriesEqual(a, b) {
93
+ if (a === null || typeof a !== "object") return false;
94
+ const left = a;
95
+ if (left.command !== b.command) return false;
96
+ if (!arraysEqual(left.args, b.args)) return false;
97
+ if (!recordsEqual(left.env, b.env)) return false;
98
+ return true;
99
+ }
100
+ function arraysEqual(a, b) {
101
+ const left = Array.isArray(a) ? a : void 0;
102
+ const right = b;
103
+ if (left === void 0 && right === void 0) return true;
104
+ if (left === void 0 || right === void 0)
105
+ return (left ?? []).length === 0 && (right ?? []).length === 0;
106
+ if (left.length !== right.length) return false;
107
+ for (let i = 0; i < left.length; i++) {
108
+ if (left[i] !== right[i]) return false;
109
+ }
110
+ return true;
111
+ }
112
+ function recordsEqual(a, b) {
113
+ const left = a !== null && typeof a === "object" && !Array.isArray(a) ? a : void 0;
114
+ const right = b;
115
+ if (left === void 0 && right === void 0) return true;
116
+ const leftKeys = left ? Object.keys(left) : [];
117
+ const rightKeys = right ? Object.keys(right) : [];
118
+ if (leftKeys.length === 0 && rightKeys.length === 0) return true;
119
+ if (leftKeys.length !== rightKeys.length) return false;
120
+ for (const k of leftKeys) {
121
+ if (left?.[k] !== right?.[k]) return false;
122
+ }
123
+ return true;
124
+ }
125
+ function indent(text) {
126
+ return text.split("\n").map((line) => ` ${line}`).join("\n");
127
+ }
128
+
129
+ // src/cli/commands/memory-rules-merge.ts
130
+ import { randomUUID } from "crypto";
131
+ import {
132
+ chmodSync,
133
+ existsSync as existsSync2,
134
+ linkSync,
135
+ lstatSync,
136
+ mkdirSync as mkdirSync2,
137
+ readFileSync as readFileSync2,
138
+ realpathSync,
139
+ renameSync as renameSync2,
140
+ rmSync,
141
+ statSync,
142
+ writeFileSync as writeFileSync2
143
+ } from "fs";
144
+ import { dirname as dirname2 } from "path";
145
+ var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
146
+ var RULES_END_MARKER = "<!-- pyx-memory:end -->";
147
+ var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
148
+ var PM_HEADING_RE = /^ {0,3}##[ \t]+Persistent Memory[ \t]*$/;
149
+ var H1_OR_H2_RE = /^ {0,3}#{1,2}[ \t]+\S/;
150
+ var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
151
+ var CLOSING_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
152
+ var BEGIN_MARKER_RE = /^ {0,3}<!-- pyx-memory:begin(?:[ \t][^>]*)?-->[ \t]*$/;
153
+ var END_MARKER_RE = /^ {0,3}<!-- pyx-memory:end -->[ \t]*$/;
154
+ function isBeginMarker(line) {
155
+ return BEGIN_MARKER_RE.test(line);
156
+ }
157
+ function isEndMarker(line) {
158
+ return END_MARKER_RE.test(line);
159
+ }
160
+ function fenceMask(lines) {
161
+ const mask = new Array(lines.length).fill(false);
162
+ let openIdx = -1;
163
+ let delim = "";
164
+ let openLen = 0;
165
+ for (let i = 0; i < lines.length; i++) {
166
+ const fence = FENCE_RE.exec(lines[i] ?? "")?.[1];
167
+ if (!fence) continue;
168
+ if (openIdx === -1) {
169
+ openIdx = i;
170
+ delim = fence.charAt(0);
171
+ openLen = fence.length;
172
+ continue;
173
+ }
174
+ if (CLOSING_FENCE_RE.test(lines[i] ?? "") && fence.charAt(0) === delim && fence.length >= openLen) {
175
+ for (let j = openIdx; j <= i; j++) mask[j] = true;
176
+ openIdx = -1;
177
+ delim = "";
178
+ openLen = 0;
179
+ }
180
+ }
181
+ return mask;
182
+ }
183
+ function nextH1OrH2(lines, mask, from) {
184
+ for (let i = from; i < lines.length; i++) {
185
+ const line = lines[i];
186
+ if (line !== void 0 && !mask[i] && H1_OR_H2_RE.test(line)) return i;
187
+ }
188
+ return lines.length;
189
+ }
190
+ function findCompleteManagedRange(lines, mask) {
191
+ let begin = -1;
192
+ for (let i = 0; i < lines.length; i++) {
193
+ const line = lines[i];
194
+ if (line === void 0 || mask[i]) continue;
195
+ if (isBeginMarker(line)) begin = i;
196
+ else if (begin !== -1 && isEndMarker(line)) return [begin, i + 1];
197
+ }
198
+ return null;
199
+ }
200
+ function findBeginLines(lines, mask) {
201
+ const found = [];
202
+ for (let i = 0; i < lines.length; i++) {
203
+ const line = lines[i];
204
+ if (line !== void 0 && !mask[i] && isBeginMarker(line)) found.push(i);
205
+ }
206
+ return found;
207
+ }
208
+ function findLegacyPyxRange(lines, mask) {
209
+ for (let i = 0; i < lines.length; i++) {
210
+ const line = lines[i];
211
+ if (line === void 0 || mask[i] || !PM_HEADING_RE.test(line)) continue;
212
+ const end = nextH1OrH2(lines, mask, i + 1);
213
+ const own = lines.slice(i, end).filter((l, k) => !mask[i + k] && !isBeginMarker(l) && !isEndMarker(l));
214
+ if (own.join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
215
+ }
216
+ return null;
217
+ }
218
+ function spliceBlock(lines, from, toExclusive, blockLines) {
219
+ const before = lines.slice(0, from);
220
+ const after = lines.slice(toExclusive);
221
+ while (before.length && (before[before.length - 1] ?? "").trim() === "") before.pop();
222
+ while (after.length && (after[0] ?? "").trim() === "") after.shift();
223
+ const out = [...before];
224
+ if (out.length) out.push("");
225
+ out.push(...blockLines);
226
+ if (after.length) out.push("");
227
+ out.push(...after);
228
+ return out;
229
+ }
230
+ function joinWithTrailingNewline(lines, eol) {
231
+ const body = lines.join(eol);
232
+ return body.endsWith(eol) ? body : `${body}${eol}`;
233
+ }
234
+ var BOM = "\uFEFF";
235
+ function mergeMemoryRules(existing, section) {
236
+ const bom = existing.startsWith(BOM) ? BOM : "";
237
+ const merged = mergeBody(existing.slice(bom.length), section);
238
+ return { ...merged, content: bom + merged.content };
239
+ }
240
+ function mergeBody(existing, section) {
241
+ if (section.includes(BEGIN_MARKER_PREFIX) || section.includes(RULES_END_MARKER)) {
242
+ throw new Error("pyx-memory rules section must not contain the managed markers.");
243
+ }
244
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
245
+ const blockLines = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER];
246
+ const normalized = existing.replace(/\r\n/g, "\n");
247
+ if (normalized.trim().length === 0) {
248
+ return { content: joinWithTrailingNewline(blockLines, eol), action: "created" };
249
+ }
250
+ let lines = normalized.split("\n");
251
+ let mask = fenceMask(lines);
252
+ const complete = findCompleteManagedRange(lines, mask);
253
+ if (complete) {
254
+ const next2 = spliceBlock(lines, complete[0], complete[1], blockLines);
255
+ return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
256
+ }
257
+ const orphans = new Set(findBeginLines(lines, mask));
258
+ if (orphans.size > 0) {
259
+ lines = lines.filter((_, i) => !orphans.has(i));
260
+ mask = fenceMask(lines);
261
+ }
262
+ const legacy = findLegacyPyxRange(lines, mask);
263
+ if (legacy) {
264
+ const next2 = spliceBlock(lines, legacy[0], legacy[1], blockLines);
265
+ return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
266
+ }
267
+ const next = spliceBlock(lines, lines.length, lines.length, blockLines);
268
+ return { content: joinWithTrailingNewline(next, eol), action: "appended" };
269
+ }
270
+ function refreshMemoryRules(file, section) {
271
+ const bom = file.startsWith(BOM) ? BOM : "";
272
+ const refreshed = refreshBody(file.slice(bom.length), section);
273
+ return refreshed === null ? null : bom + refreshed;
274
+ }
275
+ function refreshBody(existing, section) {
276
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
277
+ const complete = findCompleteManagedRange(lines, fenceMask(lines));
278
+ if (!complete) return null;
279
+ const [first, last] = [complete[0], complete[1] - 1];
280
+ const starts = [0];
281
+ for (let i = 0; i < existing.length; i++) if (existing[i] === "\n") starts.push(i + 1);
282
+ const from = starts[first] ?? 0;
283
+ const lastStart = starts[last] ?? 0;
284
+ const to = lastStart + (lines[last] ?? "").length;
285
+ const eol = existing.slice(from).match(/\r?\n/)?.[0] ?? "\n";
286
+ const block = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER].join(eol);
287
+ return existing.slice(0, from) + block + existing.slice(to);
288
+ }
289
+ function changedDuringWrite(filePath) {
290
+ return new Error(`${filePath} changed while updating the pyx-memory rules; retry.`);
291
+ }
292
+ function readUtf8Strict(path) {
293
+ try {
294
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFileSync2(path));
295
+ } catch (err) {
296
+ if (err instanceof TypeError) {
297
+ throw new Error(`${path} is not valid UTF-8; the pyx-memory rules were not changed.`);
298
+ }
299
+ throw err;
300
+ }
301
+ }
302
+ function writeMemoryRulesFile(opts) {
303
+ const { filePath, section } = opts;
304
+ const fileExists = existsSync2(filePath);
305
+ if (!fileExists && lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
306
+ throw new Error(`${filePath} is a broken link; fix or remove it, then retry.`);
307
+ }
308
+ const target = fileExists ? realpathSync(filePath) : filePath;
309
+ const existing = fileExists ? readUtf8Strict(target) : "";
310
+ let merged = null;
311
+ if (!opts.refreshOnly) merged = mergeMemoryRules(existing, section);
312
+ else {
313
+ const content = refreshMemoryRules(existing, section);
314
+ if (content !== null) merged = { content, action: "updated" };
315
+ }
316
+ if (merged === null || merged.content === existing) {
317
+ return {
318
+ action: "unchanged",
319
+ filePath,
320
+ message: `pyx-memory rules already current in ${filePath} (no changes written).
321
+ `
322
+ };
323
+ }
324
+ const parent = dirname2(target);
325
+ if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
326
+ const mode = fileExists ? statSync(target).mode & 511 : 420;
327
+ const tmpPath = `${target}.${process.pid}.${randomUUID()}.tmp`;
328
+ try {
329
+ writeFileSync2(tmpPath, merged.content, { mode });
330
+ chmodSync(tmpPath, mode);
331
+ if (fileExists) {
332
+ if (readUtf8Strict(target) !== existing) throw changedDuringWrite(filePath);
333
+ renameSync2(tmpPath, target);
334
+ } else {
335
+ try {
336
+ linkSync(tmpPath, target);
337
+ } catch (err) {
338
+ if (err.code === "EEXIST") throw changedDuringWrite(filePath);
339
+ throw err;
340
+ }
341
+ rmSync(tmpPath);
342
+ }
343
+ } catch (err) {
344
+ rmSync(tmpPath, { force: true });
345
+ throw err;
346
+ }
347
+ const verb = merged.action === "created" ? "Wrote" : merged.action === "appended" ? "Added" : "Updated";
348
+ return {
349
+ action: merged.action,
350
+ filePath,
351
+ message: `${verb} the pyx-memory rules in ${filePath}.
352
+ `
353
+ };
354
+ }
355
+
356
+ // src/cli/commands/mcp-install.ts
357
+ var VALID_SCOPES = /* @__PURE__ */ new Set(["local", "user", "project"]);
358
+ var SERVER_NAME = "pyx-memory";
359
+ function pyxMemArgs(remote) {
360
+ return remote ? ["mcp", "--remote"] : ["mcp"];
361
+ }
362
+ function mcpEntry(remote) {
363
+ return { command: "pyx-mem", args: pyxMemArgs(remote) };
364
+ }
365
+ function mcpInstallClaudeCodeCommand(opts = {}) {
366
+ const scope = opts.scope ?? "local";
367
+ if (!validateScope(scope)) return EXIT.USAGE;
368
+ const remote = opts.remote ?? false;
369
+ const spawnSync = opts._spawnSync ?? nodeSpawnSync;
370
+ const probe = spawnSync("claude", ["--version"], { stdio: "pipe" });
371
+ if (probe.error || probe.status !== 0) {
372
+ printClaudeCodeManualInstructions(scope, remote);
373
+ return EXIT.OK;
374
+ }
375
+ const cmd = ["mcp", "add", SERVER_NAME, "-s", scope, "--", "pyx-mem", ...pyxMemArgs(remote)];
376
+ const res = spawnSync("claude", cmd, { stdio: "inherit" });
377
+ if (res.status !== 0) {
378
+ process.stderr.write(`\`claude ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
379
+ `);
380
+ printClaudeCodeManualInstructions(scope, remote);
381
+ return EXIT.USAGE;
382
+ }
383
+ process.stdout.write(
384
+ `Installed pyx-memory MCP server in Claude Code (scope: ${scope}).
385
+ Restart Claude Code to make the tools available. No API key was written to .mcp.json \u2014 credentials live in the OS credential store.
386
+ To populate the knowledge graph, YOU pass entities and relationships (or triples) \u2014 a multi-entity store needs at least one connecting edge or it is refused. The server does not extract for you unless a self-host operator configured a BYO extraction endpoint (then store_memory can forward extractEntities:true). Images require caller-provided descriptions/hooks.
387
+ `
388
+ );
389
+ return EXIT.OK;
390
+ }
391
+ function printClaudeCodeManualInstructions(scope, remote) {
392
+ process.stdout.write(
393
+ [
394
+ "The `claude` CLI is not on PATH, or the install command failed.",
395
+ "",
396
+ "Run this once Claude Code is installed:",
397
+ ` claude mcp add ${SERVER_NAME} -s ${scope} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
398
+ "",
399
+ "Or add this entry manually to your Claude Code MCP config (.mcp.json):",
400
+ JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpEntry(remote) } }, null, 2),
401
+ "",
402
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
403
+ ""
404
+ ].join("\n")
405
+ );
406
+ }
407
+ function mcpInstallCodexCommand(opts = {}) {
408
+ const scope = opts.scope ?? "user";
409
+ if (!validateScope(scope)) return EXIT.USAGE;
410
+ const remote = opts.remote ?? false;
411
+ const spawnSync = opts._spawnSync ?? nodeSpawnSync;
412
+ const home = opts._homeDir ?? homedir();
413
+ const codexHome = (opts._env ?? process.env).CODEX_HOME;
414
+ const probe = spawnSync("codex", ["--version"], { stdio: "pipe" });
415
+ if (probe.error || probe.status !== 0) {
416
+ printCodexManualInstructions(home, remote, codexHome);
417
+ return EXIT.OK;
418
+ }
419
+ const cmd = [
420
+ "mcp",
421
+ "add",
422
+ SERVER_NAME,
423
+ ...codexHome ? ["--env", `CODEX_HOME=${codexHome}`] : [],
424
+ "--",
425
+ "pyx-mem",
426
+ ...pyxMemArgs(remote)
427
+ ];
428
+ const res = spawnSync("codex", cmd, { stdio: "inherit" });
429
+ if (res.status !== 0) {
430
+ process.stderr.write(`\`codex ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
431
+ `);
432
+ printCodexManualInstructions(home, remote, codexHome);
433
+ return EXIT.USAGE;
434
+ }
435
+ process.stdout.write(
436
+ `Installed pyx-memory MCP server in Codex CLI.
437
+ Restart Codex CLI to make the tools available. No API key was written to config.toml \u2014 credentials live in the OS credential store.
438
+ `
439
+ );
440
+ return EXIT.OK;
441
+ }
442
+ function printCodexManualInstructions(home, remote, codexHome) {
443
+ const configPath = join(codexHome ?? join(home, ".codex"), "config.toml");
444
+ process.stdout.write(
445
+ [
446
+ "The `codex` CLI is not on PATH, or the install command failed.",
447
+ "",
448
+ "Run this once Codex CLI is installed:",
449
+ ` codex mcp add ${SERVER_NAME}${codexHome ? ` --env ${JSON.stringify(`CODEX_HOME=${codexHome}`)}` : ""} -- pyx-mem ${pyxMemArgs(remote).join(" ")}`,
450
+ "",
451
+ `Or add this section manually to ${configPath}:`,
452
+ "",
453
+ `[mcp_servers.${SERVER_NAME}]`,
454
+ `command = "pyx-mem"`,
455
+ `args = ${JSON.stringify(pyxMemArgs(remote))}`,
456
+ ...codexHome ? [`env = { CODEX_HOME = ${JSON.stringify(codexHome)} }`] : [],
457
+ "",
458
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
459
+ ""
460
+ ].join("\n")
461
+ );
462
+ }
463
+ function mcpInstallCursorCommand(opts = {}) {
464
+ const scope = opts.scope ?? "user";
465
+ if (!validateScope(scope)) return EXIT.USAGE;
466
+ const home = opts._homeDir ?? homedir();
467
+ const filePath = scope === "project" ? join(process.cwd(), ".cursor", "mcp.json") : join(home, ".cursor", "mcp.json");
468
+ return writeJsonAndReport(filePath, "Cursor", mcpEntry(opts.remote ?? false));
469
+ }
470
+ function mcpInstallClineCommand(opts = {}) {
471
+ const scope = opts.scope ?? "user";
472
+ if (!validateScope(scope)) return EXIT.USAGE;
473
+ const remote = opts.remote ?? false;
474
+ process.stdout.write(
475
+ [
476
+ "Cline MCP config is managed through the Cline UI in VS Code (the file",
477
+ "path differs by OS, VS Code variant, and Cline version).",
478
+ "",
479
+ "1. Open VS Code (or Cursor / Windsurf running the Cline extension).",
480
+ "2. Click the Cline icon in the sidebar.",
481
+ '3. Click "MCP Servers" \u2192 "Configure MCP Servers".',
482
+ `4. Add this entry under "mcpServers":`,
483
+ "",
484
+ JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpEntry(remote) } }, null, 2),
485
+ "",
486
+ "5. Save the file. Cline will reload the MCP servers automatically.",
487
+ "",
488
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
489
+ ""
490
+ ].join("\n")
491
+ );
492
+ return EXIT.OK;
493
+ }
494
+ function mcpInstallContinueCommand(opts = {}) {
495
+ const scope = opts.scope ?? "user";
496
+ if (!validateScope(scope)) return EXIT.USAGE;
497
+ const remote = opts.remote ?? false;
498
+ const home = opts._homeDir ?? homedir();
499
+ const yamlPath = join(home, ".continue", "mcpServers", `${SERVER_NAME}.yaml`);
500
+ process.stdout.write(
501
+ [
502
+ "Continue uses YAML config. The recommended way to add an MCP server is a",
503
+ "standalone block file:",
504
+ "",
505
+ ` ${yamlPath}`,
506
+ "",
507
+ "Create that file with these contents:",
508
+ "",
509
+ `name: ${SERVER_NAME}`,
510
+ "version: 0.0.1",
511
+ "schema: v1",
512
+ "mcpServers:",
513
+ ` - name: ${SERVER_NAME}`,
514
+ ` command: pyx-mem`,
515
+ ` args:`,
516
+ ...pyxMemArgs(remote).map((arg) => ` - ${arg}`),
517
+ "",
518
+ "Continue auto-loads new YAML blocks on next chat session \u2014 no restart",
519
+ "needed in most setups.",
520
+ "",
521
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
522
+ ""
523
+ ].join("\n")
524
+ );
525
+ return EXIT.OK;
526
+ }
527
+ function mcpInstallWindsurfCommand(opts = {}) {
528
+ const scope = opts.scope ?? "user";
529
+ if (!validateScope(scope)) return EXIT.USAGE;
530
+ const home = opts._homeDir ?? homedir();
531
+ const filePath = join(home, ".codeium", "windsurf", "mcp_config.json");
532
+ return writeJsonAndReport(filePath, "Windsurf", mcpEntry(opts.remote ?? false));
533
+ }
534
+ function mcpInstallGeminiCliCommand(opts = {}) {
535
+ const scope = opts.scope ?? "user";
536
+ if (!validateScope(scope)) return EXIT.USAGE;
537
+ const remote = opts.remote ?? false;
538
+ const spawnSync = opts._spawnSync ?? nodeSpawnSync;
539
+ const home = opts._homeDir ?? homedir();
540
+ const probe = spawnSync("gemini", ["--version"], { stdio: "pipe" });
541
+ if (probe.error || probe.status !== 0) {
542
+ return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
543
+ }
544
+ const geminiScope = scope === "local" ? "project" : scope;
545
+ const cmd = ["mcp", "add", "-s", geminiScope, SERVER_NAME, "pyx-mem", ...pyxMemArgs(remote)];
546
+ const res = spawnSync("gemini", cmd, { stdio: "inherit" });
547
+ if (res.status !== 0) {
548
+ process.stderr.write(`\`gemini ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
549
+ `);
550
+ return writeJsonAndReport(geminiConfigPath(home, scope), "Gemini CLI", mcpEntry(remote));
551
+ }
552
+ process.stdout.write(
553
+ `Installed pyx-memory MCP server in Gemini CLI (scope: ${geminiScope}).
554
+ Restart Gemini CLI to make the tools available. No API key was written to settings.json \u2014 credentials live in the OS credential store.
555
+ `
556
+ );
557
+ return EXIT.OK;
558
+ }
559
+ function geminiConfigPath(home, scope) {
560
+ return scope === "project" ? join(process.cwd(), ".gemini", "settings.json") : join(home, ".gemini", "settings.json");
561
+ }
562
+ function mcpInstallPiCommand(opts = {}) {
563
+ const scope = opts.scope ?? "user";
564
+ if (!validateScope(scope)) return EXIT.USAGE;
565
+ const home = opts._homeDir ?? homedir();
566
+ const filePath = scope === "user" ? join(home, ".pi", "agent", "mcp.json") : join(process.cwd(), ".pi", "mcp.json");
567
+ return writeJsonAndReport(filePath, "Pi", mcpEntry(opts.remote ?? false), {
568
+ prereq: "pi has no native MCP support \u2014 install the third-party adapter once:\n pi install npm:pi-mcp-adapter"
569
+ });
570
+ }
571
+ function mcpInstallOhMyPiCommand(opts = {}) {
572
+ const scope = opts.scope ?? "user";
573
+ if (!validateScope(scope)) return EXIT.USAGE;
574
+ const home = opts._homeDir ?? homedir();
575
+ const filePath = scope === "user" ? join(home, ".omp", "agent", "mcp.json") : join(process.cwd(), ".omp", "mcp.json");
576
+ return writeJsonAndReport(filePath, "oh-my-pi", mcpEntry(opts.remote ?? false));
577
+ }
578
+ function mcpInstallOpenClawCommand(opts = {}) {
579
+ const scope = opts.scope ?? "user";
580
+ if (!validateScope(scope)) return EXIT.USAGE;
581
+ const remote = opts.remote ?? false;
582
+ const spawnSync = opts._spawnSync ?? nodeSpawnSync;
583
+ const probe = spawnSync("openclaw", ["--version"], { stdio: "pipe" });
584
+ if (probe.error || probe.status !== 0) {
585
+ printOpenClawManualInstructions(remote);
586
+ return EXIT.OK;
587
+ }
588
+ const entryJson = JSON.stringify(mcpEntry(remote));
589
+ const cmd = ["mcp", "set", SERVER_NAME, entryJson];
590
+ const res = spawnSync("openclaw", cmd, { stdio: "inherit" });
591
+ if (res.status !== 0) {
592
+ process.stderr.write(
593
+ `\`openclaw ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
594
+ `
595
+ );
596
+ printOpenClawManualInstructions(remote);
597
+ return EXIT.USAGE;
598
+ }
599
+ process.stdout.write(
600
+ `Installed pyx-memory MCP server in OpenClaw.
601
+ Restart OpenClaw to make the tools available. No API key was written to openclaw.json \u2014 credentials live in the OS credential store.
602
+ `
603
+ );
604
+ return EXIT.OK;
605
+ }
606
+ function printOpenClawManualInstructions(remote) {
607
+ const entry = mcpEntry(remote);
608
+ const entryJson = JSON.stringify(entry);
609
+ process.stdout.write(
610
+ [
611
+ "The `openclaw` CLI is not on PATH, or the install command failed.",
612
+ "",
613
+ "Run this once OpenClaw is installed:",
614
+ ` openclaw mcp set ${SERVER_NAME} '${entryJson}'`,
615
+ "",
616
+ "Or add this entry manually under `mcp.servers` in your OpenClaw config",
617
+ "(run `openclaw config file` to print the active config path):",
618
+ "",
619
+ JSON.stringify({ mcp: { servers: { [SERVER_NAME]: entry } } }, null, 2),
620
+ "",
621
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
622
+ ""
623
+ ].join("\n")
624
+ );
625
+ }
626
+ function mcpInstallHermesCommand(opts = {}) {
627
+ const scope = opts.scope ?? "user";
628
+ if (!validateScope(scope)) return EXIT.USAGE;
629
+ const remote = opts.remote ?? false;
630
+ const spawnSync = opts._spawnSync ?? nodeSpawnSync;
631
+ const probe = spawnSync("hermes", ["--version"], { stdio: "pipe" });
632
+ if (probe.error || probe.status !== 0) {
633
+ printHermesManualInstructions(remote);
634
+ return EXIT.OK;
635
+ }
636
+ const cmd = ["mcp", "add", SERVER_NAME, "--command", "pyx-mem", "--args", ...pyxMemArgs(remote)];
637
+ const res = spawnSync("hermes", cmd, { stdio: "inherit" });
638
+ if (res.status !== 0) {
639
+ process.stderr.write(`\`hermes ${cmd.join(" ")}\` exited with code ${res.status ?? "null"}.
640
+ `);
641
+ printHermesManualInstructions(remote);
642
+ return EXIT.USAGE;
643
+ }
644
+ process.stdout.write(
645
+ `Installed pyx-memory MCP server in Hermes.
646
+ Restart Hermes to make the tools available. No API key was written to config.yaml \u2014 credentials live in the OS credential store.
647
+ `
648
+ );
649
+ return EXIT.OK;
650
+ }
651
+ function printHermesManualInstructions(remote) {
652
+ process.stdout.write(
653
+ [
654
+ "The `hermes` CLI is not on PATH, or the install command failed.",
655
+ "",
656
+ "Run this once Hermes is installed:",
657
+ ` hermes mcp add ${SERVER_NAME} --command pyx-mem --args ${pyxMemArgs(remote).join(" ")}`,
658
+ "",
659
+ "Or add this block under `mcp_servers:` in your Hermes config",
660
+ "(typically ~/.hermes/config.yaml):",
661
+ "",
662
+ `${SERVER_NAME}:`,
663
+ " command: pyx-mem",
664
+ " args:",
665
+ ...pyxMemArgs(remote).map((arg) => ` - ${arg}`),
666
+ "",
667
+ "Do NOT add your pyx-memory API key here. Run `pyx-mem login` to store it in the OS credential store.",
668
+ ""
669
+ ].join("\n")
670
+ );
671
+ }
672
+ var RULES_FILE_TARGETS = {
673
+ "claude-code": { envDir: "CLAUDE_CONFIG_DIR", dir: [".claude"], file: "CLAUDE.md" },
674
+ codex: { envDir: "CODEX_HOME", dir: [".codex"], file: "AGENTS.md" },
675
+ "gemini-cli": { dir: [".gemini"], file: "GEMINI.md" },
676
+ "oh-my-pi": { dir: [".omp", "agent"], file: "RULES.md" }
677
+ };
678
+ function rulesFilePath(target, opts) {
679
+ if (!Object.hasOwn(RULES_FILE_TARGETS, target)) return void 0;
680
+ const spec = RULES_FILE_TARGETS[target];
681
+ const env = opts._env ?? (opts._homeDir === void 0 ? process.env : {});
682
+ const configDir = spec.envDir ? env[spec.envDir] : void 0;
683
+ return configDir ? join(configDir, spec.file) : join(opts._homeDir ?? homedir(), ...spec.dir, spec.file);
684
+ }
685
+ function installMemoryRules(opts) {
686
+ const filePath = rulesFilePath(opts.target, opts);
687
+ if (!filePath) return;
688
+ if (opts.noRules) {
689
+ process.stdout.write(
690
+ 'Skipped writing the memory rules (--no-rules). The tools are installed, but the agent will reach for memory only when you ask; add the "## Persistent Memory" rules yourself to change that.\n'
691
+ );
692
+ return;
693
+ }
694
+ const result = writeMemoryRulesFile({ filePath, section: PERSISTENT_MEMORY_SECTION });
695
+ process.stdout.write(result.message);
696
+ }
697
+ function installMemoryRulesOnly(target, opts = {}) {
698
+ if (target === void 0 || !rulesFilePath(target, opts)) {
699
+ process.stderr.write(
700
+ `Error: \`pyx-mem mcp rules\` needs a host with a known rules file: ${Object.keys(RULES_FILE_TARGETS).join(", ")}.
701
+ `
702
+ );
703
+ return EXIT.USAGE;
704
+ }
705
+ installMemoryRules({ ...opts, target, noRules: false });
706
+ return EXIT.OK;
707
+ }
708
+ function reconcileMemoryRules(opts = {}) {
709
+ const section = opts.section ?? PERSISTENT_MEMORY_SECTION;
710
+ for (const target of Object.keys(RULES_FILE_TARGETS)) {
711
+ const filePath = rulesFilePath(target, opts);
712
+ try {
713
+ if (!existsSync3(filePath)) continue;
714
+ const result = writeMemoryRulesFile({ filePath, section, refreshOnly: true });
715
+ if (result.action !== "unchanged") {
716
+ process.stderr.write(
717
+ `pyx-memory: refreshed the "## Persistent Memory" rules in ${filePath}
718
+ `
719
+ );
720
+ }
721
+ } catch {
722
+ }
723
+ }
724
+ }
725
+ var MAX_SERVER_RULES_BYTES = 16 * 1024;
726
+ async function syncMemoryRulesFromServer(client, opts = {}) {
727
+ try {
728
+ const { tools } = await client.listTools();
729
+ if (!tools.some((tool) => tool.name === "get_memory_rules")) {
730
+ reconcileMemoryRules(opts);
731
+ return;
732
+ }
733
+ const result = await client.callTool({ name: "get_memory_rules", arguments: {} });
734
+ const [first] = result.content ?? [];
735
+ const section = first?.type === "text" ? first.text : void 0;
736
+ if (result.isError || !section || !isRulesSection(section)) {
737
+ throw new Error('the server returned no valid "## Persistent Memory" section');
738
+ }
739
+ reconcileMemoryRules({ ...opts, section });
740
+ } catch (err) {
741
+ process.stderr.write(
742
+ `pyx-memory: left the installed memory rules unchanged \u2014 could not fetch current rules (${err instanceof Error ? err.message : String(err)}).
743
+ `
744
+ );
745
+ }
746
+ }
747
+ function isRulesSection(text) {
748
+ return text.startsWith("## Persistent Memory\n") && Buffer.byteLength(text, "utf8") <= MAX_SERVER_RULES_BYTES && !text.includes("<!-- pyx-memory:") && !/^\s*(```|~~~)/m.test(text);
749
+ }
750
+ function validateScope(scope) {
751
+ if (VALID_SCOPES.has(scope)) return true;
752
+ process.stderr.write(`Error: invalid --scope \`${scope}\`. Expected: local | user | project.
753
+ `);
754
+ return false;
755
+ }
756
+ function writeJsonAndReport(filePath, agentLabel, entry, opts = {}) {
757
+ const result = mergeWriteJsonMcpEntry({
758
+ filePath,
759
+ serverName: SERVER_NAME,
760
+ entry
761
+ });
762
+ const stream = result.exitCode === EXIT.OK ? process.stdout : process.stderr;
763
+ stream.write(result.message);
764
+ if (result.status === "installed") {
765
+ process.stdout.write(
766
+ `Restart ${agentLabel} to make the tools available. No API key was written \u2014 credentials live in the OS credential store.
767
+ `
768
+ );
769
+ }
770
+ if ((result.status === "installed" || result.status === "already-installed") && opts.prereq) {
771
+ process.stdout.write(`Prerequisite: ${opts.prereq}
772
+ `);
773
+ }
774
+ return result.exitCode;
775
+ }
776
+
777
+ export {
778
+ mcpInstallClaudeCodeCommand,
779
+ mcpInstallCodexCommand,
780
+ mcpInstallCursorCommand,
781
+ mcpInstallClineCommand,
782
+ mcpInstallContinueCommand,
783
+ mcpInstallWindsurfCommand,
784
+ mcpInstallGeminiCliCommand,
785
+ mcpInstallPiCommand,
786
+ mcpInstallOhMyPiCommand,
787
+ mcpInstallOpenClawCommand,
788
+ mcpInstallHermesCommand,
789
+ installMemoryRules,
790
+ installMemoryRulesOnly,
791
+ reconcileMemoryRules,
792
+ syncMemoryRulesFromServer
793
+ };