@pyxmate/memory 1.3.0 → 1.4.0
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/dist/agent-contract.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const PYX_MEMORY_INSTRUCTIONS = "Use pyx-memory
|
|
1
|
+
declare const PYX_MEMORY_INSTRUCTIONS = "Use pyx-memory for durable memory across sessions, proactively \u2014 do not wait to be told. SEARCH before assuming: at the start of a task, and before proposing an approach or re-deriving a past decision, search memory for the project and topic; resolve relative times (\"last year\", \"\uB450 \uB2EC \uC804\") to an absolute ISO-8601 anchorTime so results rank by proximity to that time. STORE once a fact is settled: corrections, bug fixes (root cause + fix), design/architecture decisions (with reasoning), integration/API/auth/endpoint details, gotchas, explicit preferences, and \"remember this\" requests \u2014 concise facts not deliberation, each with topic and project. Pass eventTime (ISO-8601, when the fact took effect) for anything that can change or go stale; recency ordering, \"as of\" queries, and stale-vs-current resolution all key off it. After a memory informs your work, call reinforce so it stays in the quick/medium tiers (idle never revives it). When the user corrects you, call record_correction; before a task, call fetch_applicable_corrections (pyx never auto-applies them). When content names people, organizations, tools, places, events, or key concepts, pass entities and relationships \u2014 you build the graph, the server does not extract it; a multi-entity store with no connecting edge is refused. Match search effort to need: quick (default, strongest) for routine recall, deep for full/archived history; use lineage to trace how a fact changed. userId/teamId/agentId and callerAccessLevel are attribution filters and sensitivity redaction, not a security isolation boundary \u2014 for tenant/namespace/ReBAC detail see the installed Persistent Memory rule.";
|
|
2
2
|
declare const PERSISTENT_MEMORY_SECTION: string;
|
|
3
3
|
declare function buildDesignGuide({ appName }: {
|
|
4
4
|
appName: string;
|
package/dist/agent-contract.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/contract/index.ts
|
|
2
|
-
var PYX_MEMORY_INSTRUCTIONS = `Use pyx-memory
|
|
2
|
+
var PYX_MEMORY_INSTRUCTIONS = `Use pyx-memory for durable memory across sessions, proactively \u2014 do not wait to be told. SEARCH before assuming: at the start of a task, and before proposing an approach or re-deriving a past decision, search memory for the project and topic; resolve relative times ("last year", "\uB450 \uB2EC \uC804") to an absolute ISO-8601 anchorTime so results rank by proximity to that time. STORE once a fact is settled: corrections, bug fixes (root cause + fix), design/architecture decisions (with reasoning), integration/API/auth/endpoint details, gotchas, explicit preferences, and "remember this" requests \u2014 concise facts not deliberation, each with topic and project. Pass eventTime (ISO-8601, when the fact took effect) for anything that can change or go stale; recency ordering, "as of" queries, and stale-vs-current resolution all key off it. After a memory informs your work, call reinforce so it stays in the quick/medium tiers (idle never revives it). When the user corrects you, call record_correction; before a task, call fetch_applicable_corrections (pyx never auto-applies them). When content names people, organizations, tools, places, events, or key concepts, pass entities and relationships \u2014 you build the graph, the server does not extract it; a multi-entity store with no connecting edge is refused. Match search effort to need: quick (default, strongest) for routine recall, deep for full/archived history; use lineage to trace how a fact changed. userId/teamId/agentId and callerAccessLevel are attribution filters and sensitivity redaction, not a security isolation boundary \u2014 for tenant/namespace/ReBAC detail see the installed Persistent Memory rule.`;
|
|
3
3
|
var PERSISTENT_MEMORY_SECTION = [
|
|
4
4
|
"## Persistent Memory",
|
|
5
5
|
"",
|
|
@@ -142,7 +142,8 @@ function buildAgentSnippet() {
|
|
|
142
142
|
...targetLines,
|
|
143
143
|
"",
|
|
144
144
|
"Conflict policy:",
|
|
145
|
-
"- If
|
|
145
|
+
"- If a pyx-memory managed block (between `<!-- pyx-memory:begin \u2026 -->` and `<!-- pyx-memory:end -->`) is already present \u2014 `pyx-mem mcp install` may have written it \u2014 leave it exactly as-is and do NOT add a second copy.",
|
|
146
|
+
"- Else if an existing `Persistent Memory` section references the `pyx-memory` MCP tools, REPLACE it with the version below \u2014 this is the newer canonical rule set for the same system, not a duplicate.",
|
|
146
147
|
"- If an existing `Persistent Memory` section uses a different memory tool, append this one underneath instead. Do not delete, rewrite, or summarize the other tool's instructions.",
|
|
147
148
|
"",
|
|
148
149
|
"Install only the section below, verbatim. After writing, tell the user which file you updated.",
|
package/dist/cli/pyx-mem.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
PERSISTENT_MEMORY_SECTION,
|
|
3
4
|
buildDesignGuide
|
|
4
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-ML6VKYAL.mjs";
|
|
5
6
|
|
|
6
7
|
// src/cli/exit-codes.ts
|
|
7
8
|
var EXIT = {
|
|
@@ -797,7 +798,7 @@ function createProxyServer(client, version, uploadLocalFile) {
|
|
|
797
798
|
return server;
|
|
798
799
|
}
|
|
799
800
|
async function runMcpProxyServer(opts) {
|
|
800
|
-
const version = opts.version ?? (true ? "1.
|
|
801
|
+
const version = opts.version ?? (true ? "1.4.0" : "0.0.0-dev");
|
|
801
802
|
const read = await opts.readCredentials();
|
|
802
803
|
if (!read.ok) {
|
|
803
804
|
const text = read.result.content.map((c) => c.type === "text" ? c.text : "").join(" ").trim();
|
|
@@ -977,6 +978,149 @@ function indent(text) {
|
|
|
977
978
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
978
979
|
}
|
|
979
980
|
|
|
981
|
+
// src/cli/commands/memory-rules-merge.ts
|
|
982
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
983
|
+
import { dirname as dirname2 } from "path";
|
|
984
|
+
var RULES_BEGIN_MARKER = "<!-- pyx-memory:begin (managed by `pyx-mem mcp install`; edit outside these markers) -->";
|
|
985
|
+
var RULES_END_MARKER = "<!-- pyx-memory:end -->";
|
|
986
|
+
var BEGIN_MARKER_PREFIX = "<!-- pyx-memory:begin";
|
|
987
|
+
var PM_HEADING_RE = /^ {0,3}##[ \t]+Persistent Memory[ \t]*$/;
|
|
988
|
+
var H1_OR_H2_RE = /^ {0,3}#{1,2}[ \t]+\S/;
|
|
989
|
+
var FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
990
|
+
function fenceMask(lines) {
|
|
991
|
+
const mask = new Array(lines.length).fill(false);
|
|
992
|
+
let openIdx = -1;
|
|
993
|
+
let delim = "";
|
|
994
|
+
let openLen = 0;
|
|
995
|
+
for (let i = 0; i < lines.length; i++) {
|
|
996
|
+
const fence = FENCE_RE.exec(lines[i] ?? "")?.[1];
|
|
997
|
+
if (!fence) continue;
|
|
998
|
+
if (openIdx === -1) {
|
|
999
|
+
openIdx = i;
|
|
1000
|
+
delim = fence.charAt(0);
|
|
1001
|
+
openLen = fence.length;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (fence.charAt(0) === delim && fence.length >= openLen) {
|
|
1005
|
+
for (let j = openIdx; j <= i; j++) mask[j] = true;
|
|
1006
|
+
openIdx = -1;
|
|
1007
|
+
delim = "";
|
|
1008
|
+
openLen = 0;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return mask;
|
|
1012
|
+
}
|
|
1013
|
+
function nextH1OrH2(lines, mask, from) {
|
|
1014
|
+
for (let i = from; i < lines.length; i++) {
|
|
1015
|
+
const line = lines[i];
|
|
1016
|
+
if (line !== void 0 && !mask[i] && H1_OR_H2_RE.test(line)) return i;
|
|
1017
|
+
}
|
|
1018
|
+
return lines.length;
|
|
1019
|
+
}
|
|
1020
|
+
function findCompleteManagedRange(lines, mask) {
|
|
1021
|
+
let begin = -1;
|
|
1022
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1023
|
+
const line = lines[i];
|
|
1024
|
+
if (line === void 0 || mask[i]) continue;
|
|
1025
|
+
const t = line.trim();
|
|
1026
|
+
if (begin === -1) {
|
|
1027
|
+
if (t.startsWith(BEGIN_MARKER_PREFIX)) begin = i;
|
|
1028
|
+
} else if (t === RULES_END_MARKER) {
|
|
1029
|
+
return [begin, i + 1];
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
function findOrphanBeginLine(lines, mask) {
|
|
1035
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1036
|
+
const line = lines[i];
|
|
1037
|
+
if (line !== void 0 && !mask[i] && line.trim().startsWith(BEGIN_MARKER_PREFIX)) return i;
|
|
1038
|
+
}
|
|
1039
|
+
return -1;
|
|
1040
|
+
}
|
|
1041
|
+
function findLegacyPyxRange(lines, mask) {
|
|
1042
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1043
|
+
const line = lines[i];
|
|
1044
|
+
if (line === void 0 || mask[i] || !PM_HEADING_RE.test(line)) continue;
|
|
1045
|
+
const end = nextH1OrH2(lines, mask, i + 1);
|
|
1046
|
+
if (lines.slice(i, end).join("\n").toLowerCase().includes("pyx-memory")) return [i, end];
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function spliceBlock(lines, from, toExclusive, blockLines) {
|
|
1051
|
+
const before = lines.slice(0, from);
|
|
1052
|
+
const after = lines.slice(toExclusive);
|
|
1053
|
+
while (before.length && (before[before.length - 1] ?? "").trim() === "") before.pop();
|
|
1054
|
+
while (after.length && (after[0] ?? "").trim() === "") after.shift();
|
|
1055
|
+
const out = [...before];
|
|
1056
|
+
if (out.length) out.push("");
|
|
1057
|
+
out.push(...blockLines);
|
|
1058
|
+
if (after.length) out.push("");
|
|
1059
|
+
out.push(...after);
|
|
1060
|
+
return out;
|
|
1061
|
+
}
|
|
1062
|
+
function joinWithTrailingNewline(lines, eol) {
|
|
1063
|
+
const body = lines.join(eol);
|
|
1064
|
+
return body.endsWith(eol) ? body : `${body}${eol}`;
|
|
1065
|
+
}
|
|
1066
|
+
function mergeMemoryRules(existing, section) {
|
|
1067
|
+
if (section.includes(BEGIN_MARKER_PREFIX) || section.includes(RULES_END_MARKER)) {
|
|
1068
|
+
throw new Error("pyx-memory rules section must not contain the managed markers.");
|
|
1069
|
+
}
|
|
1070
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
1071
|
+
const blockLines = [RULES_BEGIN_MARKER, ...section.split(/\r?\n/), RULES_END_MARKER];
|
|
1072
|
+
const normalized = existing.replace(/\r\n/g, "\n");
|
|
1073
|
+
if (normalized.trim().length === 0) {
|
|
1074
|
+
return { content: joinWithTrailingNewline(blockLines, eol), action: "created" };
|
|
1075
|
+
}
|
|
1076
|
+
let lines = normalized.split("\n");
|
|
1077
|
+
let mask = fenceMask(lines);
|
|
1078
|
+
const complete = findCompleteManagedRange(lines, mask);
|
|
1079
|
+
if (complete) {
|
|
1080
|
+
const next2 = spliceBlock(lines, complete[0], complete[1], blockLines);
|
|
1081
|
+
return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
|
|
1082
|
+
}
|
|
1083
|
+
const orphan = findOrphanBeginLine(lines, mask);
|
|
1084
|
+
if (orphan !== -1) {
|
|
1085
|
+
lines = [...lines.slice(0, orphan), ...lines.slice(orphan + 1)];
|
|
1086
|
+
mask = fenceMask(lines);
|
|
1087
|
+
}
|
|
1088
|
+
const legacy = findLegacyPyxRange(lines, mask);
|
|
1089
|
+
if (legacy) {
|
|
1090
|
+
const next2 = spliceBlock(lines, legacy[0], legacy[1], blockLines);
|
|
1091
|
+
return { content: joinWithTrailingNewline(next2, eol), action: "updated" };
|
|
1092
|
+
}
|
|
1093
|
+
const next = spliceBlock(lines, lines.length, lines.length, blockLines);
|
|
1094
|
+
return { content: joinWithTrailingNewline(next, eol), action: "appended" };
|
|
1095
|
+
}
|
|
1096
|
+
function writeMemoryRulesFile(opts) {
|
|
1097
|
+
const { filePath, section } = opts;
|
|
1098
|
+
const fileExists = existsSync2(filePath);
|
|
1099
|
+
const existing = fileExists ? readFileSync2(filePath, "utf8") : "";
|
|
1100
|
+
const { content, action } = mergeMemoryRules(existing, section);
|
|
1101
|
+
if (content === existing) {
|
|
1102
|
+
return {
|
|
1103
|
+
action: "unchanged",
|
|
1104
|
+
filePath,
|
|
1105
|
+
message: `pyx-memory rules already current in ${filePath} (no changes written).
|
|
1106
|
+
`
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
const parent = dirname2(filePath);
|
|
1110
|
+
if (!existsSync2(parent)) mkdirSync2(parent, { recursive: true });
|
|
1111
|
+
const mode = fileExists ? statSync(filePath).mode & 511 : 420;
|
|
1112
|
+
const tmpPath = `${filePath}.tmp`;
|
|
1113
|
+
writeFileSync2(tmpPath, content, { mode });
|
|
1114
|
+
renameSync2(tmpPath, filePath);
|
|
1115
|
+
const verb = action === "created" ? "Wrote" : action === "appended" ? "Added" : "Updated";
|
|
1116
|
+
return {
|
|
1117
|
+
action,
|
|
1118
|
+
filePath,
|
|
1119
|
+
message: `${verb} the pyx-memory rules in ${filePath}.
|
|
1120
|
+
`
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
980
1124
|
// src/cli/commands/mcp-install.ts
|
|
981
1125
|
var VALID_SCOPES = /* @__PURE__ */ new Set(["local", "user", "project"]);
|
|
982
1126
|
var SERVER_NAME = "pyx-memory";
|
|
@@ -1283,6 +1427,28 @@ function printHermesManualInstructions(remote) {
|
|
|
1283
1427
|
].join("\n")
|
|
1284
1428
|
);
|
|
1285
1429
|
}
|
|
1430
|
+
var RULES_FILE_TARGETS = {
|
|
1431
|
+
"claude-code": [".claude", "CLAUDE.md"],
|
|
1432
|
+
codex: [".codex", "AGENTS.md"],
|
|
1433
|
+
"gemini-cli": [".gemini", "GEMINI.md"],
|
|
1434
|
+
"oh-my-pi": [".omp", "agent", "RULES.md"]
|
|
1435
|
+
};
|
|
1436
|
+
function installMemoryRules(opts) {
|
|
1437
|
+
const segs = RULES_FILE_TARGETS[opts.target];
|
|
1438
|
+
if (!segs) return;
|
|
1439
|
+
if (opts.noRules) {
|
|
1440
|
+
process.stdout.write(
|
|
1441
|
+
'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'
|
|
1442
|
+
);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const home = opts._homeDir ?? homedir();
|
|
1446
|
+
const result = writeMemoryRulesFile({
|
|
1447
|
+
filePath: join(home, ...segs),
|
|
1448
|
+
section: PERSISTENT_MEMORY_SECTION
|
|
1449
|
+
});
|
|
1450
|
+
process.stdout.write(result.message);
|
|
1451
|
+
}
|
|
1286
1452
|
function validateScope(scope) {
|
|
1287
1453
|
if (VALID_SCOPES.has(scope)) return true;
|
|
1288
1454
|
process.stderr.write(`Error: invalid --scope \`${scope}\`. Expected: local | user | project.
|
|
@@ -1311,7 +1477,7 @@ function writeJsonAndReport(filePath, agentLabel, entry, opts = {}) {
|
|
|
1311
1477
|
}
|
|
1312
1478
|
|
|
1313
1479
|
// src/cli/commands/scaffold.ts
|
|
1314
|
-
import { existsSync as
|
|
1480
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1315
1481
|
import { basename as basename2, join as join2, resolve as resolve2 } from "path";
|
|
1316
1482
|
var SERVER_IMAGE = "ghcr.io/pyx-corp/pyx-memory-v1:latest";
|
|
1317
1483
|
var DOCKER_COMPOSE = `services:
|
|
@@ -1453,21 +1619,21 @@ function buildFiles(targetDir, appName) {
|
|
|
1453
1619
|
function scaffoldCommand(args = {}) {
|
|
1454
1620
|
const target = resolveTarget(args);
|
|
1455
1621
|
if (target === null) return EXIT.USAGE;
|
|
1456
|
-
if (
|
|
1622
|
+
if (existsSync3(target.targetDir) && !statSync2(target.targetDir).isDirectory()) {
|
|
1457
1623
|
process.stderr.write(`Error: target exists and is not a directory: ${target.targetDir}
|
|
1458
1624
|
`);
|
|
1459
1625
|
return EXIT.USAGE;
|
|
1460
1626
|
}
|
|
1461
|
-
|
|
1627
|
+
mkdirSync3(target.targetDir, { recursive: true });
|
|
1462
1628
|
const created = [];
|
|
1463
1629
|
const skipped = [];
|
|
1464
1630
|
for (const file of buildFiles(target.targetDir, target.appName)) {
|
|
1465
1631
|
const relativePath = basename2(file.path);
|
|
1466
|
-
if (
|
|
1632
|
+
if (existsSync3(file.path)) {
|
|
1467
1633
|
skipped.push(relativePath);
|
|
1468
1634
|
continue;
|
|
1469
1635
|
}
|
|
1470
|
-
|
|
1636
|
+
writeFileSync3(file.path, file.contents, "utf8");
|
|
1471
1637
|
created.push(relativePath);
|
|
1472
1638
|
}
|
|
1473
1639
|
process.stdout.write(
|
|
@@ -1547,11 +1713,16 @@ Commands:
|
|
|
1547
1713
|
updates; key stays in the OS keychain). Self-host:
|
|
1548
1714
|
run 'login --endpoint <url>' first to point it at
|
|
1549
1715
|
your own server.
|
|
1550
|
-
mcp install <target> [--scope user|local|project] [--remote]
|
|
1716
|
+
mcp install <target> [--scope user|local|project] [--remote] [--no-rules]
|
|
1551
1717
|
Install pyx-memory MCP config for your AI agent. The
|
|
1552
1718
|
config launches the hosted zero-touch thin proxy;
|
|
1553
1719
|
--remote is an accepted no-op alias (the bundled
|
|
1554
1720
|
stdio server was removed in v1.3.0).
|
|
1721
|
+
For claude-code, codex, gemini-cli, and oh-my-pi it
|
|
1722
|
+
also writes the always-on "## Persistent Memory" rules
|
|
1723
|
+
into that tool's global instructions file (idempotent;
|
|
1724
|
+
your other content is preserved) so memory fires by
|
|
1725
|
+
default. Pass --no-rules to skip that write.
|
|
1555
1726
|
Targets: claude-code, codex, cursor, cline, continue, windsurf, gemini-cli, pi, oh-my-pi, openclaw, hermes.
|
|
1556
1727
|
|
|
1557
1728
|
Notes:
|
|
@@ -1620,7 +1791,7 @@ var INSTALL_TARGETS = {
|
|
|
1620
1791
|
hermes: mcpInstallHermesCommand
|
|
1621
1792
|
};
|
|
1622
1793
|
var VALID_TARGETS = Object.keys(INSTALL_TARGETS);
|
|
1623
|
-
function runMcpInstall(target, scope, remote) {
|
|
1794
|
+
function runMcpInstall(target, scope, remote, noRules) {
|
|
1624
1795
|
if (target === void 0 || !Object.hasOwn(INSTALL_TARGETS, target)) {
|
|
1625
1796
|
process.stderr.write(
|
|
1626
1797
|
`Error: unknown install target \`${target ?? ""}\`. Expected: ${VALID_TARGETS.join(", ")}.
|
|
@@ -1629,7 +1800,9 @@ function runMcpInstall(target, scope, remote) {
|
|
|
1629
1800
|
return EXIT.USAGE;
|
|
1630
1801
|
}
|
|
1631
1802
|
const handler = INSTALL_TARGETS[target];
|
|
1632
|
-
|
|
1803
|
+
const code = handler({ scope, remote });
|
|
1804
|
+
if (code === EXIT.OK) installMemoryRules({ target, noRules });
|
|
1805
|
+
return code;
|
|
1633
1806
|
}
|
|
1634
1807
|
function runMcpCommand(parsed) {
|
|
1635
1808
|
if (parsed.subcommand === void 0) {
|
|
@@ -1647,7 +1820,12 @@ function runMcpCommand(parsed) {
|
|
|
1647
1820
|
if (parsed.subcommand === "install") {
|
|
1648
1821
|
const target = parsed.positional[0];
|
|
1649
1822
|
const scope = typeof parsed.flags.scope === "string" ? parsed.flags.scope : void 0;
|
|
1650
|
-
return runMcpInstall(
|
|
1823
|
+
return runMcpInstall(
|
|
1824
|
+
target,
|
|
1825
|
+
scope,
|
|
1826
|
+
parsed.flags.remote === true,
|
|
1827
|
+
parsed.flags["no-rules"] === true
|
|
1828
|
+
);
|
|
1651
1829
|
}
|
|
1652
1830
|
process.stderr.write(
|
|
1653
1831
|
`Error: unknown subcommand \`mcp ${parsed.subcommand}\`. Run \`pyx-mem --help\`.
|