@xultrax-web/agent-memory-mcp 0.10.3 → 0.11.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/README.md +28 -0
- package/dist/index.js +364 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,34 @@ You can `cat` your memory. You can `grep` it. You can edit it in vim. You can co
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## New in v0.11 · rule memories + `AGENTS.md` emission
|
|
17
|
+
|
|
18
|
+
A new memory type — `rule` — captures constraints the agent should respect, not just facts to recall. Rules carry optional frontmatter fields: `severity` (hard / soft), `scope`, `applies_when`, `matches`, `enforce_on`, and `last_verified`.
|
|
19
|
+
|
|
20
|
+
When you save a rule (or run `agent-memory emit-companions`), the server projects every rule memory out to `AGENTS.md` — the cross-tool universal standard read natively by Claude Code, OpenAI Codex CLI, Cursor, Aider, Devin, GitHub Copilot, Gemini CLI, Windsurf, and Amazon Q. One source of truth in your memory store; every AI tool reads the same rules.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
agent-memory save-rule no-emojis-ever \
|
|
24
|
+
--description "Never use emojis in commits, comments, or chat output." \
|
|
25
|
+
--severity hard \
|
|
26
|
+
--scope global \
|
|
27
|
+
--enforce-on commits,chat_responses \
|
|
28
|
+
--content "No emojis. Anywhere. Ever."
|
|
29
|
+
|
|
30
|
+
agent-memory emit-companions # writes ./AGENTS.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Set `AGENT_MEMORY_AUTO_EMIT_DIR=/path/to/project` to auto-regenerate companions on every rule save.
|
|
34
|
+
|
|
35
|
+
Roadmap for the v0.11.x series:
|
|
36
|
+
|
|
37
|
+
- `CLAUDE.md` + `.cursor/rules/*.mdc` + `.gemini/instructions.md` emitters (per-tool native formats)
|
|
38
|
+
- Compliance Receipts (Macaroon-style HMAC tokens · protocol-level enforcement of our own destructive tools)
|
|
39
|
+
- `check_action` tool (deterministic rule matching · optional Sampling enrichment where clients support it)
|
|
40
|
+
- `audit` command (rule conflicts · staleness · receipt-denial log)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
16
44
|
## What you get
|
|
17
45
|
|
|
18
46
|
```text
|
package/dist/index.js
CHANGED
|
@@ -168,7 +168,8 @@ function withLock(fn) {
|
|
|
168
168
|
// -------------------------------------------------------------
|
|
169
169
|
// Types & validation
|
|
170
170
|
// -------------------------------------------------------------
|
|
171
|
-
const VALID_TYPES = new Set(["user", "feedback", "project", "reference"]);
|
|
171
|
+
const VALID_TYPES = new Set(["user", "feedback", "project", "reference", "rule"]);
|
|
172
|
+
const VALID_RULE_SEVERITIES = new Set(["hard", "soft"]);
|
|
172
173
|
// Slug rules: lowercase a-z + digits + hyphen + underscore, start with
|
|
173
174
|
// a letter or digit, 1-80 chars. Underscores are allowed because Claude
|
|
174
175
|
// Code's memory tree uses them; we want frictionless import.
|
|
@@ -182,6 +183,12 @@ const WIKI_LINK_PATTERN = /\[\[([a-z0-9][a-z0-9_-]{0,80})\]\]/g;
|
|
|
182
183
|
export function memoryFilePath(name) {
|
|
183
184
|
return join(MEMORY_DIR, `${name}.md`);
|
|
184
185
|
}
|
|
186
|
+
function parseStringArray(input) {
|
|
187
|
+
if (!Array.isArray(input))
|
|
188
|
+
return undefined;
|
|
189
|
+
const out = input.filter((x) => typeof x === "string" && x.length > 0);
|
|
190
|
+
return out.length > 0 ? out : undefined;
|
|
191
|
+
}
|
|
185
192
|
export function readMemory(name) {
|
|
186
193
|
const fp = memoryFilePath(name);
|
|
187
194
|
if (!existsSync(fp))
|
|
@@ -189,6 +196,7 @@ export function readMemory(name) {
|
|
|
189
196
|
const raw = readFileSync(fp, "utf8");
|
|
190
197
|
const parsed = matter(raw);
|
|
191
198
|
const fm = parsed.data;
|
|
199
|
+
const severity = fm.severity === "hard" || fm.severity === "soft" ? fm.severity : undefined;
|
|
192
200
|
return {
|
|
193
201
|
name: fm.name ?? name,
|
|
194
202
|
description: fm.description ?? "",
|
|
@@ -196,6 +204,14 @@ export function readMemory(name) {
|
|
|
196
204
|
tags: Array.isArray(fm.tags) ? fm.tags.filter((t) => typeof t === "string") : [],
|
|
197
205
|
body: parsed.content.trim(),
|
|
198
206
|
filePath: fp,
|
|
207
|
+
severity,
|
|
208
|
+
scope: parseStringArray(fm.scope),
|
|
209
|
+
applies_when: parseStringArray(fm.applies_when),
|
|
210
|
+
matches: parseStringArray(fm.matches),
|
|
211
|
+
enforce_on: parseStringArray(fm.enforce_on),
|
|
212
|
+
last_verified: typeof fm.last_verified === "string" && /^\d{4}-\d{2}-\d{2}$/.test(fm.last_verified)
|
|
213
|
+
? fm.last_verified
|
|
214
|
+
: undefined,
|
|
199
215
|
};
|
|
200
216
|
}
|
|
201
217
|
export function listMemoryFiles() {
|
|
@@ -344,6 +360,8 @@ function toolSaveMemory(args) {
|
|
|
344
360
|
conflicts: conflictWarning ? "warned" : undefined,
|
|
345
361
|
});
|
|
346
362
|
log("debug", "save_memory", { name, type, update: isUpdate });
|
|
363
|
+
if (type === "rule")
|
|
364
|
+
maybeAutoEmitCompanions();
|
|
347
365
|
return `${isUpdate ? "Updated" : "Saved"} memory "${name}" (${type}) at ${fp}${conflictWarning}`;
|
|
348
366
|
});
|
|
349
367
|
}
|
|
@@ -915,6 +933,218 @@ function toolFindRelated(args) {
|
|
|
915
933
|
return lines.join("\n");
|
|
916
934
|
}
|
|
917
935
|
// -------------------------------------------------------------
|
|
936
|
+
// Rule memories · the v0.11 "memory as constraint" wedge
|
|
937
|
+
// -------------------------------------------------------------
|
|
938
|
+
//
|
|
939
|
+
// Rules are first-class memories with type=rule. They differ from
|
|
940
|
+
// other types in that they're meant to constrain agent behavior,
|
|
941
|
+
// not just be retrievable facts. Three things differentiate them:
|
|
942
|
+
//
|
|
943
|
+
// 1. Frontmatter has severity / scope / applies_when / matches /
|
|
944
|
+
// enforce_on / last_verified · all optional, gracefully fall
|
|
945
|
+
// back when absent.
|
|
946
|
+
//
|
|
947
|
+
// 2. Companion-file emission · all type=rule memories project out
|
|
948
|
+
// to AGENTS.md (Linux-Foundation-stewarded universal standard
|
|
949
|
+
// read natively by Claude Code, Codex CLI, Cursor, Aider, Devin,
|
|
950
|
+
// Copilot, Gemini CLI, Windsurf, and Amazon Q as of late 2025).
|
|
951
|
+
// One source of truth, regenerated from the rule store.
|
|
952
|
+
//
|
|
953
|
+
// 3. `save_rule` is a convenience wrapper around save_memory that
|
|
954
|
+
// validates rule-specific fields, then auto-emits companions
|
|
955
|
+
// when AGENT_MEMORY_AUTO_EMIT_DIR is set in the environment
|
|
956
|
+
// (opt-in to avoid surprise file creation in arbitrary CWDs).
|
|
957
|
+
function loadAllRules() {
|
|
958
|
+
return listMemoryFiles()
|
|
959
|
+
.map((n) => readMemory(n))
|
|
960
|
+
.filter((m) => m !== null && m.type === "rule")
|
|
961
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
962
|
+
}
|
|
963
|
+
function formatRuleAsMarkdown(rule) {
|
|
964
|
+
const lines = [];
|
|
965
|
+
const sev = rule.severity ? ` _(${rule.severity})_` : "";
|
|
966
|
+
lines.push(`### ${rule.name}${sev}`);
|
|
967
|
+
lines.push("");
|
|
968
|
+
if (rule.description)
|
|
969
|
+
lines.push(rule.description);
|
|
970
|
+
if (rule.scope && rule.scope.length > 0) {
|
|
971
|
+
lines.push(`- **Scope:** ${rule.scope.join(", ")}`);
|
|
972
|
+
}
|
|
973
|
+
if (rule.enforce_on && rule.enforce_on.length > 0) {
|
|
974
|
+
lines.push(`- **Enforce on:** ${rule.enforce_on.join(", ")}`);
|
|
975
|
+
}
|
|
976
|
+
if (rule.applies_when && rule.applies_when.length > 0) {
|
|
977
|
+
lines.push(`- **Applies when:**`);
|
|
978
|
+
for (const a of rule.applies_when)
|
|
979
|
+
lines.push(` - ${a}`);
|
|
980
|
+
}
|
|
981
|
+
if (rule.matches && rule.matches.length > 0) {
|
|
982
|
+
lines.push(`- **Pattern matches:** \`${rule.matches.map((m) => m.replace(/`/g, "\\`")).join("` · `")}\``);
|
|
983
|
+
}
|
|
984
|
+
if (rule.last_verified)
|
|
985
|
+
lines.push(`- _Last verified: ${rule.last_verified}_`);
|
|
986
|
+
if (rule.body) {
|
|
987
|
+
lines.push("");
|
|
988
|
+
lines.push(rule.body);
|
|
989
|
+
}
|
|
990
|
+
lines.push("");
|
|
991
|
+
return lines.join("\n");
|
|
992
|
+
}
|
|
993
|
+
function buildAgentsMdContent(rules) {
|
|
994
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
995
|
+
const head = [
|
|
996
|
+
`# Operator rules`,
|
|
997
|
+
``,
|
|
998
|
+
`> Auto-generated by agent-memory-mcp from \`${MEMORY_DIR}\` on ${today}.`,
|
|
999
|
+
`> Edit the source memory files at that path — not this file — and rerun \`agent-memory emit-companions\` to refresh.`,
|
|
1000
|
+
`>`,
|
|
1001
|
+
`> ${rules.length} rule${rules.length === 1 ? "" : "s"} active.`,
|
|
1002
|
+
``,
|
|
1003
|
+
];
|
|
1004
|
+
if (rules.length === 0) {
|
|
1005
|
+
head.push(`No rules defined yet. Run \`agent-memory save-rule …\` to add the first one.`);
|
|
1006
|
+
return head.join("\n") + "\n";
|
|
1007
|
+
}
|
|
1008
|
+
const hard = rules.filter((r) => r.severity === "hard");
|
|
1009
|
+
const soft = rules.filter((r) => r.severity !== "hard");
|
|
1010
|
+
const parts = [...head];
|
|
1011
|
+
if (hard.length > 0) {
|
|
1012
|
+
parts.push(`## Hard rules · always obey`);
|
|
1013
|
+
parts.push(``);
|
|
1014
|
+
for (const r of hard)
|
|
1015
|
+
parts.push(formatRuleAsMarkdown(r));
|
|
1016
|
+
}
|
|
1017
|
+
if (soft.length > 0) {
|
|
1018
|
+
parts.push(`## Conventions · prefer to obey`);
|
|
1019
|
+
parts.push(``);
|
|
1020
|
+
for (const r of soft)
|
|
1021
|
+
parts.push(formatRuleAsMarkdown(r));
|
|
1022
|
+
}
|
|
1023
|
+
return parts.join("\n");
|
|
1024
|
+
}
|
|
1025
|
+
function resolveCompanionDir(explicit) {
|
|
1026
|
+
if (explicit && explicit.trim().length > 0)
|
|
1027
|
+
return explicit.trim();
|
|
1028
|
+
const envOverride = process.env.AGENT_MEMORY_COMPANION_DIR;
|
|
1029
|
+
if (envOverride && envOverride.trim().length > 0)
|
|
1030
|
+
return envOverride.trim();
|
|
1031
|
+
return process.cwd();
|
|
1032
|
+
}
|
|
1033
|
+
function emitCompanions(opts = {}) {
|
|
1034
|
+
const outDir = resolveCompanionDir(opts.outDir);
|
|
1035
|
+
const rules = loadAllRules();
|
|
1036
|
+
const agentsPath = join(outDir, "AGENTS.md");
|
|
1037
|
+
const content = buildAgentsMdContent(rules);
|
|
1038
|
+
// Best-effort directory creation (companion dir may not exist if user
|
|
1039
|
+
// passes a fresh path); mkdirSync is idempotent with recursive:true.
|
|
1040
|
+
try {
|
|
1041
|
+
mkdirSync(outDir, { recursive: true });
|
|
1042
|
+
}
|
|
1043
|
+
catch {
|
|
1044
|
+
// Ignore — atomicWriteFile will surface a clearer error if needed.
|
|
1045
|
+
}
|
|
1046
|
+
atomicWriteFile(agentsPath, content);
|
|
1047
|
+
logEvent("emit_companions", { outDir, rules_count: rules.length, files: ["AGENTS.md"] });
|
|
1048
|
+
return { outDir, emitted: [agentsPath], rules_count: rules.length };
|
|
1049
|
+
}
|
|
1050
|
+
function maybeAutoEmitCompanions() {
|
|
1051
|
+
const autoDir = process.env.AGENT_MEMORY_AUTO_EMIT_DIR;
|
|
1052
|
+
if (!autoDir || autoDir.trim().length === 0)
|
|
1053
|
+
return;
|
|
1054
|
+
try {
|
|
1055
|
+
emitCompanions({ outDir: autoDir });
|
|
1056
|
+
}
|
|
1057
|
+
catch (err) {
|
|
1058
|
+
log("warn", "auto_emit_failed", {
|
|
1059
|
+
outDir: autoDir,
|
|
1060
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function toolEmitCompanions(args) {
|
|
1065
|
+
const outDir = typeof args.out_dir === "string" ? args.out_dir : undefined;
|
|
1066
|
+
const r = emitCompanions({ outDir });
|
|
1067
|
+
if (r.rules_count === 0) {
|
|
1068
|
+
return `Emitted ${r.emitted[0]} with no rules yet · run save_rule to add the first one.`;
|
|
1069
|
+
}
|
|
1070
|
+
return `Emitted ${r.rules_count} rule${r.rules_count === 1 ? "" : "s"} to ${r.emitted.join(", ")}.`;
|
|
1071
|
+
}
|
|
1072
|
+
function toolListRules(_args) {
|
|
1073
|
+
const rules = loadAllRules();
|
|
1074
|
+
if (rules.length === 0)
|
|
1075
|
+
return "No rules defined yet. Use save_rule to add one.";
|
|
1076
|
+
const lines = [];
|
|
1077
|
+
lines.push(c(ANSI.bold, `${rules.length} rule${rules.length === 1 ? "" : "s"} active:`));
|
|
1078
|
+
lines.push("");
|
|
1079
|
+
for (const r of rules) {
|
|
1080
|
+
const sev = r.severity ? ` [${r.severity}]` : "";
|
|
1081
|
+
const scope = r.scope && r.scope.length > 0 ? ` · scope: ${r.scope.join(", ")}` : "";
|
|
1082
|
+
const stale = r.last_verified && Date.now() - new Date(r.last_verified).getTime() > 90 * 86400_000
|
|
1083
|
+
? c(ANSI.yellow, " · STALE >90d")
|
|
1084
|
+
: "";
|
|
1085
|
+
lines.push(` ${r.name}${sev}${scope}${stale}`);
|
|
1086
|
+
lines.push(` ${r.description}`);
|
|
1087
|
+
}
|
|
1088
|
+
return lines.join("\n");
|
|
1089
|
+
}
|
|
1090
|
+
function toolSaveRule(args) {
|
|
1091
|
+
const name = String(args.name ?? "").trim();
|
|
1092
|
+
const description = String(args.description ?? "").trim();
|
|
1093
|
+
const content = String(args.content ?? "").trim();
|
|
1094
|
+
const severity = String(args.severity ?? "soft").trim();
|
|
1095
|
+
if (!VALID_RULE_SEVERITIES.has(severity)) {
|
|
1096
|
+
throw new Error(`Invalid severity "${severity}". Must be one of: ${Array.from(VALID_RULE_SEVERITIES).join(", ")}.`);
|
|
1097
|
+
}
|
|
1098
|
+
const scope = parseStringArray(args.scope);
|
|
1099
|
+
const applies_when = parseStringArray(args.applies_when);
|
|
1100
|
+
const matches = parseStringArray(args.matches);
|
|
1101
|
+
const enforce_on = parseStringArray(args.enforce_on);
|
|
1102
|
+
const last_verified = typeof args.last_verified === "string" && /^\d{4}-\d{2}-\d{2}$/.test(args.last_verified)
|
|
1103
|
+
? args.last_verified
|
|
1104
|
+
: new Date().toISOString().slice(0, 10);
|
|
1105
|
+
if (!SLUG_PATTERN.test(name)) {
|
|
1106
|
+
throw new Error(`Invalid name "${name}". Use lowercase (a-z, 0-9, hyphen, underscore), 1-80 chars, must start with letter or digit.`);
|
|
1107
|
+
}
|
|
1108
|
+
if (!description)
|
|
1109
|
+
throw new Error("description is required");
|
|
1110
|
+
if (!content)
|
|
1111
|
+
throw new Error("content is required");
|
|
1112
|
+
ensureStorage();
|
|
1113
|
+
const extras = [];
|
|
1114
|
+
extras.push(`severity: ${severity}`);
|
|
1115
|
+
if (scope)
|
|
1116
|
+
extras.push(`scope: [${scope.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1117
|
+
if (applies_when)
|
|
1118
|
+
extras.push(`applies_when: [${applies_when.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1119
|
+
if (matches)
|
|
1120
|
+
extras.push(`matches: [${matches.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1121
|
+
if (enforce_on)
|
|
1122
|
+
extras.push(`enforce_on: [${enforce_on.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1123
|
+
extras.push(`last_verified: ${last_verified}`);
|
|
1124
|
+
const frontmatter = `---\n` +
|
|
1125
|
+
`name: ${name}\n` +
|
|
1126
|
+
`description: ${JSON.stringify(description)}\n` +
|
|
1127
|
+
`type: rule\n` +
|
|
1128
|
+
extras.map((e) => `${e}\n`).join("") +
|
|
1129
|
+
`schema: ${SCHEMA_VERSION}\n` +
|
|
1130
|
+
`---\n\n`;
|
|
1131
|
+
const fp = memoryFilePath(name);
|
|
1132
|
+
const isUpdate = existsSync(fp);
|
|
1133
|
+
return withLock(() => {
|
|
1134
|
+
atomicWriteFile(fp, frontmatter + content + "\n");
|
|
1135
|
+
upsertIndexEntryUnlocked(name, description);
|
|
1136
|
+
logEvent("save_rule", {
|
|
1137
|
+
name,
|
|
1138
|
+
severity,
|
|
1139
|
+
update: isUpdate,
|
|
1140
|
+
bytes: content.length,
|
|
1141
|
+
});
|
|
1142
|
+
log("debug", "save_rule", { name, severity, update: isUpdate });
|
|
1143
|
+
maybeAutoEmitCompanions();
|
|
1144
|
+
return `${isUpdate ? "Updated" : "Saved"} rule "${name}" (${severity}) at ${fp}`;
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
// -------------------------------------------------------------
|
|
918
1148
|
// Git sync · multi-machine memory via git remote
|
|
919
1149
|
// -------------------------------------------------------------
|
|
920
1150
|
//
|
|
@@ -1236,7 +1466,7 @@ function actionColor(action) {
|
|
|
1236
1466
|
// -------------------------------------------------------------
|
|
1237
1467
|
// Server wiring
|
|
1238
1468
|
// -------------------------------------------------------------
|
|
1239
|
-
const server = new Server({ name: "agent-memory", version: "0.
|
|
1469
|
+
const server = new Server({ name: "agent-memory", version: "0.11.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1240
1470
|
// -------------------------------------------------------------
|
|
1241
1471
|
// Resource URI scheme
|
|
1242
1472
|
// -------------------------------------------------------------
|
|
@@ -1479,8 +1709,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1479
1709
|
},
|
|
1480
1710
|
type: {
|
|
1481
1711
|
type: "string",
|
|
1482
|
-
enum: ["user", "feedback", "project", "reference"],
|
|
1483
|
-
description: "Memory type: user (about the person), feedback (
|
|
1712
|
+
enum: ["user", "feedback", "project", "reference", "rule"],
|
|
1713
|
+
description: "Memory type: user (about the person), feedback (lessons + corrections), project (state/context), reference (external pointers), rule (constraint enforced via companion files — prefer the save_rule tool which validates rule-specific fields)",
|
|
1484
1714
|
},
|
|
1485
1715
|
content: {
|
|
1486
1716
|
type: "string",
|
|
@@ -1671,6 +1901,80 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1671
1901
|
description: "Pull memory updates from the configured git remote (fast-forward only). Run at the start of a session to get memories saved on other machines. Refuses to pull if there are uncommitted local changes.",
|
|
1672
1902
|
inputSchema: { type: "object", properties: {} },
|
|
1673
1903
|
},
|
|
1904
|
+
{
|
|
1905
|
+
name: "save_rule",
|
|
1906
|
+
description: "Save (or update) a rule memory · the 'memory as constraint' wedge. " +
|
|
1907
|
+
"Rules constrain agent behavior, not just store facts. Severity 'hard' = " +
|
|
1908
|
+
"must obey; 'soft' = prefer to obey. Rules auto-project out to AGENTS.md " +
|
|
1909
|
+
"(read by Claude Code, Codex CLI, Cursor, Aider, Devin, Copilot, Gemini CLI, " +
|
|
1910
|
+
"Windsurf, and Amazon Q natively) when AGENT_MEMORY_AUTO_EMIT_DIR is set, or " +
|
|
1911
|
+
"via the emit_companions tool on demand.",
|
|
1912
|
+
inputSchema: {
|
|
1913
|
+
type: "object",
|
|
1914
|
+
properties: {
|
|
1915
|
+
name: {
|
|
1916
|
+
type: "string",
|
|
1917
|
+
description: "Short kebab-case slug, 1-80 chars (e.g. 'no-emojis-ever', 'tests-before-commit')",
|
|
1918
|
+
},
|
|
1919
|
+
description: {
|
|
1920
|
+
type: "string",
|
|
1921
|
+
description: "One-line summary of what the rule constrains",
|
|
1922
|
+
},
|
|
1923
|
+
content: {
|
|
1924
|
+
type: "string",
|
|
1925
|
+
description: "Markdown body. Lead with the rule itself, then **Why:** and **How to apply:** lines.",
|
|
1926
|
+
},
|
|
1927
|
+
severity: {
|
|
1928
|
+
type: "string",
|
|
1929
|
+
enum: ["hard", "soft"],
|
|
1930
|
+
description: "hard = must obey (rule violations are blocked when enforced); soft = prefer to obey (warned but allowed). Defaults to soft.",
|
|
1931
|
+
},
|
|
1932
|
+
scope: {
|
|
1933
|
+
type: "array",
|
|
1934
|
+
items: { type: "string" },
|
|
1935
|
+
description: "Where this rule applies. Examples: ['global'], ['project:prefixcheck'], ['tool:git'].",
|
|
1936
|
+
},
|
|
1937
|
+
applies_when: {
|
|
1938
|
+
type: "array",
|
|
1939
|
+
items: { type: "string" },
|
|
1940
|
+
description: "Natural-language conditions for when the rule triggers. Used by Sampling-enriched check_action on supporting clients.",
|
|
1941
|
+
},
|
|
1942
|
+
matches: {
|
|
1943
|
+
type: "array",
|
|
1944
|
+
items: { type: "string" },
|
|
1945
|
+
description: "Regex patterns that deterministically signal a violation. Used by Tier-1 check_action on every client.",
|
|
1946
|
+
},
|
|
1947
|
+
enforce_on: {
|
|
1948
|
+
type: "array",
|
|
1949
|
+
items: { type: "string" },
|
|
1950
|
+
description: "Action categories this rule constrains. Examples: 'file_writes', 'commits', 'pushes', 'chat_responses'.",
|
|
1951
|
+
},
|
|
1952
|
+
last_verified: {
|
|
1953
|
+
type: "string",
|
|
1954
|
+
description: "ISO date (YYYY-MM-DD) of last verification. Defaults to today.",
|
|
1955
|
+
},
|
|
1956
|
+
},
|
|
1957
|
+
required: ["name", "description", "content"],
|
|
1958
|
+
},
|
|
1959
|
+
},
|
|
1960
|
+
{
|
|
1961
|
+
name: "list_rules",
|
|
1962
|
+
description: "List every active rule memory with severity, scope, and staleness markers (>90 days since last_verified). Use this to audit which rules are currently constraining the agent.",
|
|
1963
|
+
inputSchema: { type: "object", properties: {} },
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
name: "emit_companions",
|
|
1967
|
+
description: "Regenerate companion rule files (AGENTS.md) from the current rule memories. Writes to the directory in `out_dir`, the AGENT_MEMORY_COMPANION_DIR env var, or the current working directory in that priority order. AGENTS.md is the universal cross-tool standard (Linux Foundation / Agentic AI Foundation).",
|
|
1968
|
+
inputSchema: {
|
|
1969
|
+
type: "object",
|
|
1970
|
+
properties: {
|
|
1971
|
+
out_dir: {
|
|
1972
|
+
type: "string",
|
|
1973
|
+
description: "Optional output directory. Defaults to AGENT_MEMORY_COMPANION_DIR env var, then process.cwd().",
|
|
1974
|
+
},
|
|
1975
|
+
},
|
|
1976
|
+
},
|
|
1977
|
+
},
|
|
1674
1978
|
],
|
|
1675
1979
|
}));
|
|
1676
1980
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1726,6 +2030,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1726
2030
|
case "sync_pull":
|
|
1727
2031
|
result = toolSyncPull(args);
|
|
1728
2032
|
break;
|
|
2033
|
+
case "save_rule":
|
|
2034
|
+
result = toolSaveRule(args);
|
|
2035
|
+
break;
|
|
2036
|
+
case "list_rules":
|
|
2037
|
+
result = toolListRules(args);
|
|
2038
|
+
break;
|
|
2039
|
+
case "emit_companions":
|
|
2040
|
+
result = toolEmitCompanions(args);
|
|
2041
|
+
break;
|
|
1729
2042
|
default:
|
|
1730
2043
|
throw new Error(`Unknown tool: ${name}`);
|
|
1731
2044
|
}
|
|
@@ -1762,6 +2075,9 @@ const CLI_COMMANDS = new Set([
|
|
|
1762
2075
|
"backlinks",
|
|
1763
2076
|
"related",
|
|
1764
2077
|
"sync",
|
|
2078
|
+
"save-rule",
|
|
2079
|
+
"list-rules",
|
|
2080
|
+
"emit-companions",
|
|
1765
2081
|
"ui",
|
|
1766
2082
|
"import-claude-code",
|
|
1767
2083
|
"help",
|
|
@@ -1955,6 +2271,50 @@ async function cliMain(command, rest) {
|
|
|
1955
2271
|
}) + "\n");
|
|
1956
2272
|
return 0;
|
|
1957
2273
|
}
|
|
2274
|
+
case "save-rule": {
|
|
2275
|
+
const name = positional[0];
|
|
2276
|
+
if (!name)
|
|
2277
|
+
throw new Error("Usage: agent-memory save-rule <name> --description <d> [--severity hard|soft] " +
|
|
2278
|
+
"[--scope a,b,c] [--applies-when a,b] [--matches a,b] [--enforce-on a,b] " +
|
|
2279
|
+
"[--content <c> | --content-file <path> | --stdin]");
|
|
2280
|
+
let content = String(flags.content ?? "");
|
|
2281
|
+
if (flags["content-file"]) {
|
|
2282
|
+
content = readFileSync(String(flags["content-file"]), "utf8");
|
|
2283
|
+
}
|
|
2284
|
+
else if (flags.stdin) {
|
|
2285
|
+
content = await readStdin();
|
|
2286
|
+
}
|
|
2287
|
+
const csv = (v) => {
|
|
2288
|
+
if (typeof v !== "string" || v.trim().length === 0)
|
|
2289
|
+
return undefined;
|
|
2290
|
+
return v
|
|
2291
|
+
.split(",")
|
|
2292
|
+
.map((x) => x.trim())
|
|
2293
|
+
.filter((x) => x.length > 0);
|
|
2294
|
+
};
|
|
2295
|
+
const result = toolSaveRule({
|
|
2296
|
+
name,
|
|
2297
|
+
description: String(flags.description ?? ""),
|
|
2298
|
+
content,
|
|
2299
|
+
severity: String(flags.severity ?? "soft"),
|
|
2300
|
+
scope: csv(flags.scope),
|
|
2301
|
+
applies_when: csv(flags["applies-when"]),
|
|
2302
|
+
matches: csv(flags.matches),
|
|
2303
|
+
enforce_on: csv(flags["enforce-on"]),
|
|
2304
|
+
last_verified: flags["last-verified"] ? String(flags["last-verified"]) : undefined,
|
|
2305
|
+
});
|
|
2306
|
+
process.stdout.write(result + "\n");
|
|
2307
|
+
return 0;
|
|
2308
|
+
}
|
|
2309
|
+
case "list-rules": {
|
|
2310
|
+
process.stdout.write(toolListRules({}) + "\n");
|
|
2311
|
+
return 0;
|
|
2312
|
+
}
|
|
2313
|
+
case "emit-companions": {
|
|
2314
|
+
const out = flags.out ? String(flags.out) : undefined;
|
|
2315
|
+
process.stdout.write(toolEmitCompanions({ out_dir: out }) + "\n");
|
|
2316
|
+
return 0;
|
|
2317
|
+
}
|
|
1958
2318
|
case "ui": {
|
|
1959
2319
|
// Dynamic import so Ink + React only load when the TUI runs,
|
|
1960
2320
|
// keeping cold-start fast for MCP server + every other CLI command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xultrax-web/agent-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"mcpName": "io.github.xultrax-web/agent-memory-mcp",
|
|
5
5
|
"description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. Operator-grade storage with atomic writes, file locking, tags, [[wiki-links]], find_related, git-backed multi-machine sync, and an Ink-based TUI.",
|
|
6
6
|
"type": "module",
|