@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 +6 -1
- package/dist/cli/pyx-mem.mjs +169 -49
- package/dist/index.d.ts +5 -1
- package/package.json +1 -1
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`, `
|
|
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
|
package/dist/cli/pyx-mem.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
2436
|
-
if (begin
|
|
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
|
|
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
|
|
2508
|
+
if (line !== void 0 && !mask[i] && isBeginMarker(line)) found.push(i);
|
|
2448
2509
|
}
|
|
2449
|
-
return
|
|
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
|
-
|
|
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
|
|
2499
|
-
if (
|
|
2500
|
-
lines =
|
|
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
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
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(
|
|
2628
|
+
const parent = dirname3(target);
|
|
2525
2629
|
if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
|
|
2526
|
-
const mode = fileExists ? statSync(
|
|
2527
|
-
const tmpPath = `${
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|