@pyxmate/memory 1.23.0 → 1.23.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.
package/README.md CHANGED
@@ -34,10 +34,15 @@ pyx-mem mcp install claude-code --scope user
34
34
 
35
35
  Restart Claude Code. The MCP tools are auto-discovered via MCP Tool Search:
36
36
  `search_memories`, `store_memory`, `get_memory`, `list_memories`,
37
- `delete_memory`, `ingest_memory_file`, `summarize_memory_entity`, `status`,
37
+ `delete_memory`, `summarize_memory_entity`, `status`,
38
38
  `get_user_profile`, `upsert_user_profile`, `record_correction`, and
39
39
  `fetch_applicable_corrections`.
40
40
 
41
+ `ingest_memory_file` is currently supported only through `pyx-mem mcp` connected
42
+ to an upload-enabled self-hosted server (`DATA_PLANE_OPERATION_AUTHORITY_MODE=off`).
43
+ Run `pyx-mem login --endpoint <your-server-url>` first; file ingest is
44
+ unavailable at `https://memory.api.pyxmate.com`.
45
+
41
46
  **That's the whole setup.** Graph is a relational retrieval dimension, not a
42
47
  search-score boost. When content names people, organizations, tools, places,
43
48
  events, or key concepts, pass both `entities` and `relationships` when you can
@@ -907,7 +907,19 @@ function createHttpClient(credentials, fetchImpl = fetch) {
907
907
  if (res.status === 401 || res.status === 403) {
908
908
  return { ok: false, status: res.status, result: mcpAuthFailed(res.status) };
909
909
  }
910
- const text = await res.text().catch(() => "");
910
+ let text;
911
+ try {
912
+ text = await res.text();
913
+ } catch (err) {
914
+ const reason = err instanceof Error ? err.message : String(err);
915
+ return {
916
+ ok: false,
917
+ status: res.status,
918
+ result: mcpEnvelopeError(
919
+ `Failed to read pyx-memory response (HTTP ${res.status}): ${reason}`
920
+ )
921
+ };
922
+ }
911
923
  return parseHttpResponse(res, text);
912
924
  }
913
925
  return {
@@ -933,6 +945,9 @@ function parseHttpResponse(res, text) {
933
945
  if (res.status >= 500) {
934
946
  return { ok: false, status: res.status, result: mcpServerError(res.status, text) };
935
947
  }
948
+ if (res.ok && res.headers.get("content-type")?.includes("application/x-ndjson")) {
949
+ return parseNdjsonOutcome(res.status, text);
950
+ }
936
951
  const parsed = parseJsonText(text);
937
952
  if (!parsed.ok) {
938
953
  if (!res.ok) {
@@ -954,6 +969,31 @@ function parseHttpResponse(res, text) {
954
969
  }
955
970
  return { ok: true, status: res.status, data: parsed.value };
956
971
  }
972
+ function parseNdjsonOutcome(status, text) {
973
+ for (const line of text.split("\n")) {
974
+ if (!line.trim()) continue;
975
+ let frame;
976
+ try {
977
+ frame = JSON.parse(line);
978
+ } catch (err) {
979
+ const reason = err instanceof Error ? err.message : String(err);
980
+ return {
981
+ ok: false,
982
+ status,
983
+ result: mcpEnvelopeError(`pyx-memory sent an invalid NDJSON line: ${reason}`)
984
+ };
985
+ }
986
+ if (frame?.type === "result") return { ok: true, status, data: frame };
987
+ if (frame?.type === "error") {
988
+ return { ok: false, status, result: mcpEnvelopeError(JSON.stringify(frame)) };
989
+ }
990
+ }
991
+ return {
992
+ ok: false,
993
+ status,
994
+ result: mcpEnvelopeError("File ingest stream ended without a server result")
995
+ };
996
+ }
957
997
  function parseJsonText(text) {
958
998
  if (text.length === 0) return { ok: true, value: void 0 };
959
999
  try {
@@ -2175,27 +2215,29 @@ function createProxyServer(client, version, uploadLocalFile) {
2175
2215
  });
2176
2216
  const localUploads = /* @__PURE__ */ new Map();
2177
2217
  let listedOnce = false;
2178
- const refreshFromList = (result) => {
2179
- const tools = result.tools;
2180
- if (Array.isArray(tools)) {
2181
- listedOnce = true;
2182
- for (const tool of tools) {
2183
- const descriptor = uploadLocalFile ? extractLocalUpload(tool._meta) : void 0;
2184
- if (descriptor) localUploads.set(tool.name, descriptor);
2185
- else localUploads.delete(tool.name);
2186
- }
2218
+ let listingsStarted = 0;
2219
+ let listingApplied = 0;
2220
+ const listAndRefresh = async (params) => {
2221
+ const listing = ++listingsStarted;
2222
+ const result = await client.listTools(params);
2223
+ const { tools, nextCursor } = result;
2224
+ if (!Array.isArray(tools) || listing < listingApplied) return result;
2225
+ listingApplied = listing;
2226
+ listedOnce = true;
2227
+ if (params?.cursor === void 0 && nextCursor === void 0) localUploads.clear();
2228
+ for (const tool of tools) {
2229
+ const descriptor = uploadLocalFile ? extractLocalUpload(tool._meta) : void 0;
2230
+ if (descriptor) localUploads.set(tool.name, descriptor);
2231
+ else localUploads.delete(tool.name);
2187
2232
  }
2188
2233
  return result;
2189
2234
  };
2190
2235
  if (caps?.tools) {
2191
- server.setRequestHandler(
2192
- ListToolsRequestSchema,
2193
- async (req) => refreshFromList(await client.listTools(req.params))
2194
- );
2236
+ server.setRequestHandler(ListToolsRequestSchema, (req) => listAndRefresh(req.params));
2195
2237
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
2196
2238
  if (uploadLocalFile && !listedOnce) {
2197
2239
  try {
2198
- refreshFromList(await client.listTools());
2240
+ await listAndRefresh();
2199
2241
  } catch {
2200
2242
  }
2201
2243
  }
@@ -2223,7 +2265,7 @@ function createProxyServer(client, version, uploadLocalFile) {
2223
2265
  return server;
2224
2266
  }
2225
2267
  async function runMcpProxyServer(opts) {
2226
- const version = opts.version ?? (true ? "1.23.0" : "0.0.0-dev");
2268
+ const version = opts.version ?? (true ? "1.23.2" : "0.0.0-dev");
2227
2269
  const read = await opts.readCredentials();
2228
2270
  if (!read.ok) {
2229
2271
  throw new Error(
@@ -2269,7 +2311,7 @@ async function runMcpProxyServer(opts) {
2269
2311
 
2270
2312
  // src/cli/commands/mcp-install.ts
2271
2313
  import { spawnSync as nodeSpawnSync } from "child_process";
2272
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2314
+ import { existsSync as existsSync3 } from "fs";
2273
2315
  import { homedir as homedir2 } from "os";
2274
2316
  import { join as join2 } from "path";
2275
2317
 
@@ -2389,7 +2431,20 @@ function indent(text) {
2389
2431
  }
2390
2432
 
2391
2433
  // 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";
2434
+ import { randomUUID as randomUUID4 } from "crypto";
2435
+ import {
2436
+ chmodSync,
2437
+ existsSync as existsSync2,
2438
+ linkSync,
2439
+ lstatSync,
2440
+ mkdirSync as mkdirSync2,
2441
+ readFileSync as readFileSync2,
2442
+ realpathSync,
2443
+ renameSync as renameSync2,
2444
+ rmSync,
2445
+ statSync,
2446
+ writeFileSync as writeFileSync2
2447
+ } from "fs";
2393
2448
  import { dirname as dirname3 } from "path";
2394
2449
  var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
2395
2450
  var RULES_END_MARKER = "<!-- pyx-memory:end -->";
@@ -2397,6 +2452,15 @@ var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
2397
2452
  var PM_HEADING_RE = /^ {0,3}##[ \t]+Persistent Memory[ \t]*$/;
2398
2453
  var H1_OR_H2_RE = /^ {0,3}#{1,2}[ \t]+\S/;
2399
2454
  var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
2455
+ var CLOSING_FENCE_RE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
2456
+ var BEGIN_MARKER_RE = /^ {0,3}<!-- pyx-memory:begin(?:[ \t][^>]*)?-->[ \t]*$/;
2457
+ var END_MARKER_RE = /^ {0,3}<!-- pyx-memory:end -->[ \t]*$/;
2458
+ function isBeginMarker(line) {
2459
+ return BEGIN_MARKER_RE.test(line);
2460
+ }
2461
+ function isEndMarker(line) {
2462
+ return END_MARKER_RE.test(line);
2463
+ }
2400
2464
  function fenceMask(lines) {
2401
2465
  const mask = new Array(lines.length).fill(false);
2402
2466
  let openIdx = -1;
@@ -2411,7 +2475,7 @@ function fenceMask(lines) {
2411
2475
  openLen = fence.length;
2412
2476
  continue;
2413
2477
  }
2414
- if (fence.charAt(0) === delim && fence.length >= openLen) {
2478
+ if (CLOSING_FENCE_RE.test(lines[i] ?? "") && fence.charAt(0) === delim && fence.length >= openLen) {
2415
2479
  for (let j = openIdx; j <= i; j++) mask[j] = true;
2416
2480
  openIdx = -1;
2417
2481
  delim = "";
@@ -2432,36 +2496,29 @@ function findCompleteManagedRange(lines, mask) {
2432
2496
  for (let i = 0; i < lines.length; i++) {
2433
2497
  const line = lines[i];
2434
2498
  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
- }
2499
+ if (isBeginMarker(line)) begin = i;
2500
+ else if (begin !== -1 && isEndMarker(line)) return [begin, i + 1];
2441
2501
  }
2442
2502
  return null;
2443
2503
  }
2444
- function findOrphanBeginLine(lines, mask) {
2504
+ function findBeginLines(lines, mask) {
2505
+ const found = [];
2445
2506
  for (let i = 0; i < lines.length; i++) {
2446
2507
  const line = lines[i];
2447
- if (line !== void 0 && !mask[i] && line.trim().startsWith(BEGIN_MARKER_PREFIX)) return i;
2508
+ if (line !== void 0 && !mask[i] && isBeginMarker(line)) found.push(i);
2448
2509
  }
2449
- return -1;
2510
+ return found;
2450
2511
  }
2451
2512
  function findLegacyPyxRange(lines, mask) {
2452
2513
  for (let i = 0; i < lines.length; i++) {
2453
2514
  const line = lines[i];
2454
2515
  if (line === void 0 || mask[i] || !PM_HEADING_RE.test(line)) continue;
2455
2516
  const end = nextH1OrH2(lines, mask, i + 1);
2456
- if (lines.slice(i, end).join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
2517
+ const own = lines.slice(i, end).filter((l, k) => !mask[i + k] && !isBeginMarker(l) && !isEndMarker(l));
2518
+ if (own.join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
2457
2519
  }
2458
2520
  return null;
2459
2521
  }
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
2522
  function spliceBlock(lines, from, toExclusive, blockLines) {
2466
2523
  const before = lines.slice(0, from);
2467
2524
  const after = lines.slice(toExclusive);
@@ -2478,7 +2535,13 @@ function joinWithTrailingNewline(lines, eol) {
2478
2535
  const body = lines.join(eol);
2479
2536
  return body.endsWith(eol) ? body : `${body}${eol}`;
2480
2537
  }
2538
+ var BOM = "\uFEFF";
2481
2539
  function mergeMemoryRules(existing, section) {
2540
+ const bom = existing.startsWith(BOM) ? BOM : "";
2541
+ const merged = mergeBody(existing.slice(bom.length), section);
2542
+ return { ...merged, content: bom + merged.content };
2543
+ }
2544
+ function mergeBody(existing, section) {
2482
2545
  if (section.includes(BEGIN_MARKER_PREFIX) || section.includes(RULES_END_MARKER)) {
2483
2546
  throw new Error("pyx-memory rules section must not contain the managed markers.");
2484
2547
  }
@@ -2495,9 +2558,9 @@ function mergeMemoryRules(existing, section) {
2495
2558
  const next2 = spliceBlock(lines, complete[0], complete[1], blockLines);
2496
2559
  return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
2497
2560
  }
2498
- const orphan = findOrphanBeginLine(lines, mask);
2499
- if (orphan !== -1) {
2500
- lines = [...lines.slice(0, orphan), ...lines.slice(orphan + 1)];
2561
+ const orphans = new Set(findBeginLines(lines, mask));
2562
+ if (orphans.size > 0) {
2563
+ lines = lines.filter((_, i) => !orphans.has(i));
2501
2564
  mask = fenceMask(lines);
2502
2565
  }
2503
2566
  const legacy = findLegacyPyxRange(lines, mask);
@@ -2508,12 +2571,53 @@ function mergeMemoryRules(existing, section) {
2508
2571
  const next = spliceBlock(lines, lines.length, lines.length, blockLines);
2509
2572
  return { content: joinWithTrailingNewline(next, eol), action: "appended" };
2510
2573
  }
2574
+ function refreshMemoryRules(file, section) {
2575
+ const bom = file.startsWith(BOM) ? BOM : "";
2576
+ const refreshed = refreshBody(file.slice(bom.length), section);
2577
+ return refreshed === null ? null : bom + refreshed;
2578
+ }
2579
+ function refreshBody(existing, section) {
2580
+ const lines = existing.replace(/\r\n/g, "\n").split("\n");
2581
+ const complete = findCompleteManagedRange(lines, fenceMask(lines));
2582
+ if (!complete) return null;
2583
+ const [first, last] = [complete[0], complete[1] - 1];
2584
+ const starts = [0];
2585
+ for (let i = 0; i < existing.length; i++) if (existing[i] === "\n") starts.push(i + 1);
2586
+ const from = starts[first] ?? 0;
2587
+ const lastStart = starts[last] ?? 0;
2588
+ const to = lastStart + (lines[last] ?? "").length;
2589
+ const eol = existing.slice(from).match(/\r?\n/)?.[0] ?? "\n";
2590
+ const block = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER].join(eol);
2591
+ return existing.slice(0, from) + block + existing.slice(to);
2592
+ }
2593
+ function changedDuringWrite(filePath) {
2594
+ return new Error(`${filePath} changed while updating the pyx-memory rules; retry.`);
2595
+ }
2596
+ function readUtf8Strict(path) {
2597
+ try {
2598
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFileSync2(path));
2599
+ } catch (err) {
2600
+ if (err instanceof TypeError) {
2601
+ throw new Error(`${path} is not valid UTF-8; the pyx-memory rules were not changed.`);
2602
+ }
2603
+ throw err;
2604
+ }
2605
+ }
2511
2606
  function writeMemoryRulesFile(opts) {
2512
2607
  const { filePath, section } = opts;
2513
2608
  const fileExists = existsSync2(filePath);
2514
- const existing = fileExists ? readFileSync2(filePath, "utf8") : "";
2515
- const { content, action } = mergeMemoryRules(existing, section);
2516
- if (content === existing) {
2609
+ if (!fileExists && lstatSync(filePath, { throwIfNoEntry: false })?.isSymbolicLink()) {
2610
+ throw new Error(`${filePath} is a broken link; fix or remove it, then retry.`);
2611
+ }
2612
+ const target = fileExists ? realpathSync(filePath) : filePath;
2613
+ const existing = fileExists ? readUtf8Strict(target) : "";
2614
+ let merged = null;
2615
+ if (!opts.refreshOnly) merged = mergeMemoryRules(existing, section);
2616
+ else {
2617
+ const content = refreshMemoryRules(existing, section);
2618
+ if (content !== null) merged = { content, action: "updated" };
2619
+ }
2620
+ if (merged === null || merged.content === existing) {
2517
2621
  return {
2518
2622
  action: "unchanged",
2519
2623
  filePath,
@@ -2521,15 +2625,32 @@ function writeMemoryRulesFile(opts) {
2521
2625
  `
2522
2626
  };
2523
2627
  }
2524
- const parent = dirname3(filePath);
2628
+ const parent = dirname3(target);
2525
2629
  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";
2630
+ const mode = fileExists ? statSync(target).mode & 511 : 420;
2631
+ const tmpPath = `${target}.${process.pid}.${randomUUID4()}.tmp`;
2632
+ try {
2633
+ writeFileSync2(tmpPath, merged.content, { mode });
2634
+ chmodSync(tmpPath, mode);
2635
+ if (fileExists) {
2636
+ if (readUtf8Strict(target) !== existing) throw changedDuringWrite(filePath);
2637
+ renameSync2(tmpPath, target);
2638
+ } else {
2639
+ try {
2640
+ linkSync(tmpPath, target);
2641
+ } catch (err) {
2642
+ if (err.code === "EEXIST") throw changedDuringWrite(filePath);
2643
+ throw err;
2644
+ }
2645
+ rmSync(tmpPath);
2646
+ }
2647
+ } catch (err) {
2648
+ rmSync(tmpPath, { force: true });
2649
+ throw err;
2650
+ }
2651
+ const verb = merged.action === "created" ? "Wrote" : merged.action === "appended" ? "Added" : "Updated";
2531
2652
  return {
2532
- action,
2653
+ action: merged.action,
2533
2654
  filePath,
2534
2655
  message: `${verb} the pyx-memory rules in ${filePath}.
2535
2656
  `
@@ -2894,8 +3015,7 @@ function reconcileMemoryRules(opts = {}) {
2894
3015
  const filePath = rulesFilePath(target, opts);
2895
3016
  try {
2896
3017
  if (!existsSync3(filePath)) continue;
2897
- if (!hasPyxMemoryRules(readFileSync3(filePath, "utf8"))) continue;
2898
- const result = writeMemoryRulesFile({ filePath, section });
3018
+ const result = writeMemoryRulesFile({ filePath, section, refreshOnly: true });
2899
3019
  if (result.action !== "unchanged") {
2900
3020
  process.stderr.write(
2901
3021
  `pyx-memory: refreshed the "## Persistent Memory" rules in ${filePath}
package/dist/index.d.ts CHANGED
@@ -1650,7 +1650,11 @@ interface EnrichmentPendingV1 {
1650
1650
  interface EnrichmentTextWindowsTruncation {
1651
1651
  /** Total characters seen by the parser, including content beyond the cap. */
1652
1652
  originalChars: number;
1653
- /** Sum of emitted window lengths. <= originalChars. */
1653
+ /**
1654
+ * Sum of emitted window lengths. Overlapping regions between adjacent
1655
+ * windows are counted once per window, so this can exceed originalChars
1656
+ * once windowCount >= 2; bounded by windowCount * originalChars.
1657
+ */
1654
1658
  includedChars: number;
1655
1659
  windowCount: number;
1656
1660
  truncated: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "1.23.0",
3
+ "version": "1.23.2",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",