@pyxmate/memory 1.23.0 → 1.23.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.
Files changed (2) hide show
  1. package/dist/cli/pyx-mem.mjs +112 -34
  2. package/package.json +1 -1
@@ -2223,7 +2223,7 @@ function createProxyServer(client, version, uploadLocalFile) {
2223
2223
  return server;
2224
2224
  }
2225
2225
  async function runMcpProxyServer(opts) {
2226
- const version = opts.version ?? (true ? "1.23.0" : "0.0.0-dev");
2226
+ const version = opts.version ?? (true ? "1.23.1" : "0.0.0-dev");
2227
2227
  const read = await opts.readCredentials();
2228
2228
  if (!read.ok) {
2229
2229
  throw new Error(
@@ -2269,7 +2269,7 @@ async function runMcpProxyServer(opts) {
2269
2269
 
2270
2270
  // src/cli/commands/mcp-install.ts
2271
2271
  import { spawnSync as nodeSpawnSync } from "child_process";
2272
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2272
+ import { existsSync as existsSync3 } from "fs";
2273
2273
  import { homedir as homedir2 } from "os";
2274
2274
  import { join as join2 } from "path";
2275
2275
 
@@ -2389,7 +2389,20 @@ function indent(text) {
2389
2389
  }
2390
2390
 
2391
2391
  // src/cli/commands/memory-rules-merge.ts
2392
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
2392
+ import { randomUUID as randomUUID4 } from "crypto";
2393
+ import {
2394
+ chmodSync,
2395
+ existsSync as existsSync2,
2396
+ linkSync,
2397
+ lstatSync,
2398
+ mkdirSync as mkdirSync2,
2399
+ readFileSync as readFileSync2,
2400
+ realpathSync,
2401
+ renameSync as renameSync2,
2402
+ rmSync,
2403
+ statSync,
2404
+ writeFileSync as writeFileSync2
2405
+ } from "fs";
2393
2406
  import { dirname as dirname3 } from "path";
2394
2407
  var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
2395
2408
  var RULES_END_MARKER = "<!-- pyx-memory:end -->";
@@ -2397,6 +2410,15 @@ var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
2397
2410
  var PM_HEADING_RE = /^ {0,3}##[ \t]+Persistent Memory[ \t]*$/;
2398
2411
  var H1_OR_H2_RE = /^ {0,3}#{1,2}[ \t]+\S/;
2399
2412
  var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
2413
+ var CLOSING_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
2414
+ var BEGIN_MARKER_RE = /^ {0,3}<!-- pyx-memory:begin(?:[ \t][^>]*)?-->[ \t]*$/;
2415
+ var END_MARKER_RE = /^ {0,3}<!-- pyx-memory:end -->[ \t]*$/;
2416
+ function isBeginMarker(line) {
2417
+ return BEGIN_MARKER_RE.test(line);
2418
+ }
2419
+ function isEndMarker(line) {
2420
+ return END_MARKER_RE.test(line);
2421
+ }
2400
2422
  function fenceMask(lines) {
2401
2423
  const mask = new Array(lines.length).fill(false);
2402
2424
  let openIdx = -1;
@@ -2411,7 +2433,7 @@ function fenceMask(lines) {
2411
2433
  openLen = fence.length;
2412
2434
  continue;
2413
2435
  }
2414
- if (fence.charAt(0) === delim && fence.length >= openLen) {
2436
+ if (CLOSING_FENCE_RE.test(lines[i] ?? "") && fence.charAt(0) === delim && fence.length >= openLen) {
2415
2437
  for (let j = openIdx; j <= i; j++) mask[j] = true;
2416
2438
  openIdx = -1;
2417
2439
  delim = "";
@@ -2432,36 +2454,29 @@ function findCompleteManagedRange(lines, mask) {
2432
2454
  for (let i = 0; i < lines.length; i++) {
2433
2455
  const line = lines[i];
2434
2456
  if (line === void 0 || mask[i]) continue;
2435
- const t = line.trim();
2436
- if (begin === -1) {
2437
- if (t.startsWith(BEGIN_MARKER_PREFIX)) begin = i;
2438
- } else if (t === RULES_END_MARKER) {
2439
- return [begin, i + 1];
2440
- }
2457
+ if (isBeginMarker(line)) begin = i;
2458
+ else if (begin !== -1 && isEndMarker(line)) return [begin, i + 1];
2441
2459
  }
2442
2460
  return null;
2443
2461
  }
2444
- function findOrphanBeginLine(lines, mask) {
2462
+ function findBeginLines(lines, mask) {
2463
+ const found = [];
2445
2464
  for (let i = 0; i < lines.length; i++) {
2446
2465
  const line = lines[i];
2447
- if (line !== void 0 && !mask[i] && line.trim().startsWith(BEGIN_MARKER_PREFIX)) return i;
2466
+ if (line !== void 0 && !mask[i] && isBeginMarker(line)) found.push(i);
2448
2467
  }
2449
- return -1;
2468
+ return found;
2450
2469
  }
2451
2470
  function findLegacyPyxRange(lines, mask) {
2452
2471
  for (let i = 0; i < lines.length; i++) {
2453
2472
  const line = lines[i];
2454
2473
  if (line === void 0 || mask[i] || !PM_HEADING_RE.test(line)) continue;
2455
2474
  const end = nextH1OrH2(lines, mask, i + 1);
2456
- if (lines.slice(i, end).join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
2475
+ const own = lines.slice(i, end).filter((l, k) => !mask[i + k] && !isBeginMarker(l) && !isEndMarker(l));
2476
+ if (own.join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
2457
2477
  }
2458
2478
  return null;
2459
2479
  }
2460
- function hasPyxMemoryRules(content) {
2461
- const lines = content.replace(/\r\n/g, "\n").split("\n");
2462
- const mask = fenceMask(lines);
2463
- return findCompleteManagedRange(lines, mask) !== null || findOrphanBeginLine(lines, mask) !== -1 || findLegacyPyxRange(lines, mask) !== null;
2464
- }
2465
2480
  function spliceBlock(lines, from, toExclusive, blockLines) {
2466
2481
  const before = lines.slice(0, from);
2467
2482
  const after = lines.slice(toExclusive);
@@ -2478,7 +2493,13 @@ function joinWithTrailingNewline(lines, eol) {
2478
2493
  const body = lines.join(eol);
2479
2494
  return body.endsWith(eol) ? body : `${body}${eol}`;
2480
2495
  }
2496
+ var BOM = "\uFEFF";
2481
2497
  function mergeMemoryRules(existing, section) {
2498
+ const bom = existing.startsWith(BOM) ? BOM : "";
2499
+ const merged = mergeBody(existing.slice(bom.length), section);
2500
+ return { ...merged, content: bom + merged.content };
2501
+ }
2502
+ function mergeBody(existing, section) {
2482
2503
  if (section.includes(BEGIN_MARKER_PREFIX) || section.includes(RULES_END_MARKER)) {
2483
2504
  throw new Error("pyx-memory rules section must not contain the managed markers.");
2484
2505
  }
@@ -2495,9 +2516,9 @@ function mergeMemoryRules(existing, section) {
2495
2516
  const next2 = spliceBlock(lines, complete[0], complete[1], blockLines);
2496
2517
  return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
2497
2518
  }
2498
- const orphan = findOrphanBeginLine(lines, mask);
2499
- if (orphan !== -1) {
2500
- lines = [...lines.slice(0, orphan), ...lines.slice(orphan + 1)];
2519
+ const orphans = new Set(findBeginLines(lines, mask));
2520
+ if (orphans.size > 0) {
2521
+ lines = lines.filter((_, i) => !orphans.has(i));
2501
2522
  mask = fenceMask(lines);
2502
2523
  }
2503
2524
  const legacy = findLegacyPyxRange(lines, mask);
@@ -2508,12 +2529,53 @@ function mergeMemoryRules(existing, section) {
2508
2529
  const next = spliceBlock(lines, lines.length, lines.length, blockLines);
2509
2530
  return { content: joinWithTrailingNewline(next, eol), action: "appended" };
2510
2531
  }
2532
+ function refreshMemoryRules(file, section) {
2533
+ const bom = file.startsWith(BOM) ? BOM : "";
2534
+ const refreshed = refreshBody(file.slice(bom.length), section);
2535
+ return refreshed === null ? null : bom + refreshed;
2536
+ }
2537
+ function refreshBody(existing, section) {
2538
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
2539
+ const complete = findCompleteManagedRange(lines, fenceMask(lines));
2540
+ if (!complete) return null;
2541
+ const [first, last] = [complete[0], complete[1] - 1];
2542
+ const starts = [0];
2543
+ for (let i = 0; i < existing.length; i++) if (existing[i] === "\n") starts.push(i + 1);
2544
+ const from = starts[first] ?? 0;
2545
+ const lastStart = starts[last] ?? 0;
2546
+ const to = lastStart + (lines[last] ?? "").length;
2547
+ const eol = existing.slice(from).match(/\r?\n/)?.[0] ?? "\n";
2548
+ const block = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER].join(eol);
2549
+ return existing.slice(0, from) + block + existing.slice(to);
2550
+ }
2551
+ function changedDuringWrite(filePath) {
2552
+ return new Error(`${filePath} changed while updating the pyx-memory rules; retry.`);
2553
+ }
2554
+ function readUtf8Strict(path) {
2555
+ try {
2556
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFileSync2(path));
2557
+ } catch (err) {
2558
+ if (err instanceof TypeError) {
2559
+ throw new Error(`${path} is not valid UTF-8; the pyx-memory rules were not changed.`);
2560
+ }
2561
+ throw err;
2562
+ }
2563
+ }
2511
2564
  function writeMemoryRulesFile(opts) {
2512
2565
  const { filePath, section } = opts;
2513
2566
  const fileExists = existsSync2(filePath);
2514
- const existing = fileExists ? readFileSync2(filePath, "utf8") : "";
2515
- const { content, action } = mergeMemoryRules(existing, section);
2516
- if (content === existing) {
2567
+ if (!fileExists && lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
2568
+ throw new Error(`${filePath} is a broken link; fix or remove it, then retry.`);
2569
+ }
2570
+ const target = fileExists ? realpathSync(filePath) : filePath;
2571
+ const existing = fileExists ? readUtf8Strict(target) : "";
2572
+ let merged = null;
2573
+ if (!opts.refreshOnly) merged = mergeMemoryRules(existing, section);
2574
+ else {
2575
+ const content = refreshMemoryRules(existing, section);
2576
+ if (content !== null) merged = { content, action: "updated" };
2577
+ }
2578
+ if (merged === null || merged.content === existing) {
2517
2579
  return {
2518
2580
  action: "unchanged",
2519
2581
  filePath,
@@ -2521,15 +2583,32 @@ function writeMemoryRulesFile(opts) {
2521
2583
  `
2522
2584
  };
2523
2585
  }
2524
- const parent = dirname3(filePath);
2586
+ const parent = dirname3(target);
2525
2587
  if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
2526
- const mode = fileExists ? statSync(filePath).mode & 511 : 420;
2527
- const tmpPath = `${filePath}.tmp`;
2528
- writeFileSync2(tmpPath, content, { mode });
2529
- renameSync2(tmpPath, filePath);
2530
- const verb = action === "created" ? "Wrote" : action === "appended" ? "Added" : "Updated";
2588
+ const mode = fileExists ? statSync(target).mode & 511 : 420;
2589
+ const tmpPath = `${target}.${process.pid}.${randomUUID4()}.tmp`;
2590
+ try {
2591
+ writeFileSync2(tmpPath, merged.content, { mode });
2592
+ chmodSync(tmpPath, mode);
2593
+ if (fileExists) {
2594
+ if (readUtf8Strict(target) !== existing) throw changedDuringWrite(filePath);
2595
+ renameSync2(tmpPath, target);
2596
+ } else {
2597
+ try {
2598
+ linkSync(tmpPath, target);
2599
+ } catch (err) {
2600
+ if (err.code === "EEXIST") throw changedDuringWrite(filePath);
2601
+ throw err;
2602
+ }
2603
+ rmSync(tmpPath);
2604
+ }
2605
+ } catch (err) {
2606
+ rmSync(tmpPath, { force: true });
2607
+ throw err;
2608
+ }
2609
+ const verb = merged.action === "created" ? "Wrote" : merged.action === "appended" ? "Added" : "Updated";
2531
2610
  return {
2532
- action,
2611
+ action: merged.action,
2533
2612
  filePath,
2534
2613
  message: `${verb} the pyx-memory rules in ${filePath}.
2535
2614
  `
@@ -2894,8 +2973,7 @@ function reconcileMemoryRules(opts = {}) {
2894
2973
  const filePath = rulesFilePath(target, opts);
2895
2974
  try {
2896
2975
  if (!existsSync3(filePath)) continue;
2897
- if (!hasPyxMemoryRules(readFileSync3(filePath, "utf8"))) continue;
2898
- const result = writeMemoryRulesFile({ filePath, section });
2976
+ const result = writeMemoryRulesFile({ filePath, section, refreshOnly: true });
2899
2977
  if (result.action !== "unchanged") {
2900
2978
  process.stderr.write(
2901
2979
  `pyx-memory: refreshed the "## Persistent Memory" rules in ${filePath}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "1.23.0",
3
+ "version": "1.23.1",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",