@xultrax-web/agent-memory-mcp 0.10.2 → 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.
Files changed (3) hide show
  1. package/README.md +33 -16
  2. package/dist/index.js +364 -4
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -7,9 +7,37 @@
7
7
  [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
8
8
  [![MCP](https://img.shields.io/badge/MCP-server-blueviolet)](https://modelcontextprotocol.io)
9
9
 
10
- **The only MCP memory server that isn't a database.** Every other option asks you to trust a knowledge graph, a vector DB, Postgres + pgvector, DuckDB, or Neo4j. This one writes plain markdown files to a directory.
10
+ **Markdown memory for AI agents.** Plain files in a directory you control read them, edit them, grep them, commit them. Operator-grade storage primitives (atomic writes, file locking, soft-delete to `.trash/`, schema versioning, doctor command) wrap the files so nothing rots in the long tail.
11
11
 
12
- You can `cat` your memory. You can `grep` it. You can edit it in vim. You can commit it to git. You can move it between machines with `scp`. If the AI gets it wrong, you fix it in a text editor and save. No migration scripts. No vendor lock-in. No "just trust the embedding."
12
+ You can `cat` your memory. You can `grep` it. You can edit it in vim. You can commit it to git. You can move it between machines with `scp` or with the built-in `agent-memory sync` (git-backed). If the AI gets a memory wrong, you fix it in a text editor and save. No migration scripts. No vendor lock-in.
13
+
14
+ ---
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)
13
41
 
14
42
  ---
15
43
 
@@ -350,22 +378,11 @@ The importer walks `~/.claude/projects/*/memory/`, parses each memory's YAML fro
350
378
 
351
379
  ---
352
380
 
353
- ## How it compares
354
-
355
- The memory MCP landscape, as of May 2026:
356
-
357
- | Server | Backend | Hand-editable? | Greppable? | Git-friendly? |
358
- | ------------------------------------------------ | ---------------------- | -------------- | ---------- | -------------- |
359
- | **agent-memory-mcp (this)** | **Markdown files** | **Yes** | **Yes** | **Yes** |
360
- | `@modelcontextprotocol/server-memory` (official) | Knowledge graph (JSON) | No (raw JSON) | Limited | Painful merges |
361
- | memory-graph/memory-graph | Graph DB | No | No | No |
362
- | IzumiSy/mcp-duckdb-memory-server | DuckDB | No | No | No |
363
- | sdimitrov/mcp-memory | Postgres + pgvector | No | No | No |
364
- | JovanHsu/mcp-neo4j-memory-server | Neo4j | No | No | No |
381
+ ## Why files, not a database
365
382
 
366
- **The trade you're making:** you give up native semantic similarity search and structured entity-relation queries. You get a memory store that survives every tool change, every machine swap, every "wait, what was that AI telling me about this codebase six months ago?"
383
+ You give up native semantic similarity search and structured entity-relation queries. You get a memory store that survives every tool change, every machine swap, every "wait, what was that AI telling me about this codebase six months ago?" — and that you can still read after a power outage.
367
384
 
368
- For most workflows that's a good trade. For some it isn't. Pick the right tool.
385
+ The trade is real. For workflows that need vector recall or graph queries, a database-backed memory is the right tool. For workflows where memory is something you want to grep, edit, version-control, and audit by hand, this is.
369
386
 
370
387
  ---
371
388
 
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.10.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
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 (rules to follow), project (state/context), reference (external pointers)",
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,8 +1,8 @@
1
1
  {
2
2
  "name": "@xultrax-web/agent-memory-mcp",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.xultrax-web/agent-memory-mcp",
5
- "description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. The only MCP memory server that isn't a database.",
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",
7
7
  "bin": {
8
8
  "agent-memory-mcp": "dist/index.js",