@xultrax-web/agent-memory-mcp 0.10.3 → 0.11.1
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 +38 -0
- package/dist/index.js +509 -4
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -13,6 +13,44 @@ 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
|
|
31
|
+
# writes AGENTS.md + CLAUDE.md + .cursor/rules/*.mdc + .gemini/instructions.md
|
|
32
|
+
# (v0.11.1 — all four targets · use --target agents,claude to filter)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Companion file targets (v0.11.1):
|
|
36
|
+
|
|
37
|
+
| Target | Path | Auto-loaded by |
|
|
38
|
+
| -------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
39
|
+
| `agents` | `AGENTS.md` | Claude Code, Codex CLI, Cursor, Aider, Devin, Copilot, Gemini CLI, Windsurf, Amazon Q |
|
|
40
|
+
| `claude` | `CLAUDE.md` | Claude Code (5-level hierarchy · managed/global/project/local/subdir) |
|
|
41
|
+
| `cursor` | `.cursor/rules/operator-hard.mdc` (`alwaysApply: true`) + `operator-conventions.mdc` (agent-requested) | Cursor (MDC format) |
|
|
42
|
+
| `gemini` | `.gemini/instructions.md` | Gemini CLI |
|
|
43
|
+
|
|
44
|
+
Set `AGENT_MEMORY_AUTO_EMIT_DIR=/path/to/project` to auto-regenerate all companions on every rule save.
|
|
45
|
+
|
|
46
|
+
Roadmap for the v0.11.x series:
|
|
47
|
+
|
|
48
|
+
- Compliance Receipts (Macaroon-style HMAC tokens · protocol-level enforcement of our own destructive tools)
|
|
49
|
+
- `check_action` tool (deterministic rule matching · optional Sampling enrichment where clients support it)
|
|
50
|
+
- `audit` command (rule conflicts · staleness · receipt-denial log)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
16
54
|
## What you get
|
|
17
55
|
|
|
18
56
|
```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,343 @@ 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
|
+
export const ALL_COMPANION_TARGETS = ["agents", "claude", "cursor", "gemini"];
|
|
1026
|
+
function resolveCompanionDir(explicit) {
|
|
1027
|
+
if (explicit && explicit.trim().length > 0)
|
|
1028
|
+
return explicit.trim();
|
|
1029
|
+
const envOverride = process.env.AGENT_MEMORY_COMPANION_DIR;
|
|
1030
|
+
if (envOverride && envOverride.trim().length > 0)
|
|
1031
|
+
return envOverride.trim();
|
|
1032
|
+
return process.cwd();
|
|
1033
|
+
}
|
|
1034
|
+
// CLAUDE.md content · same body as AGENTS.md but with a Claude-Code-specific
|
|
1035
|
+
// header. Claude Code's 5-level hierarchy (managed/global/project/local/subdir)
|
|
1036
|
+
// reads any CLAUDE.md it finds; we generate the project-root file by default.
|
|
1037
|
+
function buildClaudeMdContent(rules) {
|
|
1038
|
+
const body = buildAgentsMdContent(rules);
|
|
1039
|
+
// Replace the AGENTS.md-specific header sentence with a CLAUDE.md one
|
|
1040
|
+
return body.replace(/^# Operator rules\n/, `# Operator rules · Claude Code\n\n> This is your CLAUDE.md — Claude Code reads it on session start.\n\n`);
|
|
1041
|
+
}
|
|
1042
|
+
// .gemini/instructions.md content · same body as AGENTS.md, slightly different
|
|
1043
|
+
// header.
|
|
1044
|
+
function buildGeminiInstructionsContent(rules) {
|
|
1045
|
+
const body = buildAgentsMdContent(rules);
|
|
1046
|
+
return body.replace(/^# Operator rules\n/, `# Operator rules · Gemini CLI\n\n> Loaded by Gemini CLI from .gemini/instructions.md on session start.\n\n`);
|
|
1047
|
+
}
|
|
1048
|
+
// Cursor consumes .cursor/rules/*.mdc files with their own YAML frontmatter.
|
|
1049
|
+
// Per spec: each file <150 lines, alwaysApply file <50 lines, dir total <500.
|
|
1050
|
+
// Strategy: one file per severity (hard / soft) — hard is alwaysApply, soft
|
|
1051
|
+
// is description-driven so the agent pulls it in when relevant.
|
|
1052
|
+
function buildCursorMdcFiles(rules) {
|
|
1053
|
+
const hard = rules.filter((r) => r.severity === "hard");
|
|
1054
|
+
const soft = rules.filter((r) => r.severity !== "hard");
|
|
1055
|
+
const files = [];
|
|
1056
|
+
if (hard.length > 0) {
|
|
1057
|
+
const fm = [
|
|
1058
|
+
"---",
|
|
1059
|
+
`description: "Operator hard rules · always obey · auto-generated from agent-memory-mcp"`,
|
|
1060
|
+
"alwaysApply: true",
|
|
1061
|
+
"---",
|
|
1062
|
+
"",
|
|
1063
|
+
"# Operator hard rules",
|
|
1064
|
+
"",
|
|
1065
|
+
"These rules MUST be obeyed. Violations should be flagged and blocked.",
|
|
1066
|
+
"",
|
|
1067
|
+
].join("\n");
|
|
1068
|
+
files.push({
|
|
1069
|
+
filename: "operator-hard.mdc",
|
|
1070
|
+
content: fm + hard.map((r) => formatRuleAsMarkdown(r)).join("\n"),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (soft.length > 0) {
|
|
1074
|
+
const fm = [
|
|
1075
|
+
"---",
|
|
1076
|
+
`description: "Operator conventions · prefer to obey · pulled in by agent on relevance"`,
|
|
1077
|
+
"alwaysApply: false",
|
|
1078
|
+
"---",
|
|
1079
|
+
"",
|
|
1080
|
+
"# Operator conventions",
|
|
1081
|
+
"",
|
|
1082
|
+
"Soft rules · prefer to obey. The agent may consult these when the context warrants.",
|
|
1083
|
+
"",
|
|
1084
|
+
].join("\n");
|
|
1085
|
+
files.push({
|
|
1086
|
+
filename: "operator-conventions.mdc",
|
|
1087
|
+
content: fm + soft.map((r) => formatRuleAsMarkdown(r)).join("\n"),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
return files;
|
|
1091
|
+
}
|
|
1092
|
+
function emitCompanions(opts = {}) {
|
|
1093
|
+
const outDir = resolveCompanionDir(opts.outDir);
|
|
1094
|
+
const targets = opts.targets && opts.targets.length > 0 ? opts.targets : ALL_COMPANION_TARGETS;
|
|
1095
|
+
const rules = loadAllRules();
|
|
1096
|
+
// Best-effort directory creation; mkdirSync recursive is idempotent.
|
|
1097
|
+
try {
|
|
1098
|
+
mkdirSync(outDir, { recursive: true });
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
// Ignore — atomicWriteFile surfaces a clearer error if needed.
|
|
1102
|
+
}
|
|
1103
|
+
const emitted = [];
|
|
1104
|
+
if (targets.includes("agents")) {
|
|
1105
|
+
const fp = join(outDir, "AGENTS.md");
|
|
1106
|
+
atomicWriteFile(fp, buildAgentsMdContent(rules));
|
|
1107
|
+
emitted.push(fp);
|
|
1108
|
+
}
|
|
1109
|
+
if (targets.includes("claude")) {
|
|
1110
|
+
const fp = join(outDir, "CLAUDE.md");
|
|
1111
|
+
atomicWriteFile(fp, buildClaudeMdContent(rules));
|
|
1112
|
+
emitted.push(fp);
|
|
1113
|
+
}
|
|
1114
|
+
if (targets.includes("cursor")) {
|
|
1115
|
+
const cursorDir = join(outDir, ".cursor", "rules");
|
|
1116
|
+
try {
|
|
1117
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
// Ignore — atomicWriteFile surfaces clearer error if needed.
|
|
1121
|
+
}
|
|
1122
|
+
const files = buildCursorMdcFiles(rules);
|
|
1123
|
+
if (files.length === 0) {
|
|
1124
|
+
// No rules yet — drop a placeholder so the tool knows where to put them
|
|
1125
|
+
const placeholderPath = join(cursorDir, "operator-rules.mdc");
|
|
1126
|
+
const placeholder = [
|
|
1127
|
+
"---",
|
|
1128
|
+
`description: "Operator rules · auto-generated · no rules defined yet"`,
|
|
1129
|
+
"alwaysApply: false",
|
|
1130
|
+
"---",
|
|
1131
|
+
"",
|
|
1132
|
+
"No rules defined yet. Run `agent-memory save-rule` to add the first one.",
|
|
1133
|
+
"",
|
|
1134
|
+
].join("\n");
|
|
1135
|
+
atomicWriteFile(placeholderPath, placeholder);
|
|
1136
|
+
emitted.push(placeholderPath);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
for (const f of files) {
|
|
1140
|
+
const fp = join(cursorDir, f.filename);
|
|
1141
|
+
atomicWriteFile(fp, f.content);
|
|
1142
|
+
emitted.push(fp);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (targets.includes("gemini")) {
|
|
1147
|
+
const geminiDir = join(outDir, ".gemini");
|
|
1148
|
+
try {
|
|
1149
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
// Ignore — atomicWriteFile surfaces clearer error if needed.
|
|
1153
|
+
}
|
|
1154
|
+
const fp = join(geminiDir, "instructions.md");
|
|
1155
|
+
atomicWriteFile(fp, buildGeminiInstructionsContent(rules));
|
|
1156
|
+
emitted.push(fp);
|
|
1157
|
+
}
|
|
1158
|
+
logEvent("emit_companions", {
|
|
1159
|
+
outDir,
|
|
1160
|
+
rules_count: rules.length,
|
|
1161
|
+
targets,
|
|
1162
|
+
files: emitted.map((p) => p.replace(outDir, "").replace(/^[\\/]/, "")),
|
|
1163
|
+
});
|
|
1164
|
+
return { outDir, emitted, rules_count: rules.length, targets };
|
|
1165
|
+
}
|
|
1166
|
+
function maybeAutoEmitCompanions() {
|
|
1167
|
+
const autoDir = process.env.AGENT_MEMORY_AUTO_EMIT_DIR;
|
|
1168
|
+
if (!autoDir || autoDir.trim().length === 0)
|
|
1169
|
+
return;
|
|
1170
|
+
try {
|
|
1171
|
+
emitCompanions({ outDir: autoDir });
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
log("warn", "auto_emit_failed", {
|
|
1175
|
+
outDir: autoDir,
|
|
1176
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function toolEmitCompanions(args) {
|
|
1181
|
+
const outDir = typeof args.out_dir === "string" ? args.out_dir : undefined;
|
|
1182
|
+
let targets;
|
|
1183
|
+
if (Array.isArray(args.targets)) {
|
|
1184
|
+
targets = args.targets
|
|
1185
|
+
.filter((t) => typeof t === "string")
|
|
1186
|
+
.filter((t) => ALL_COMPANION_TARGETS.includes(t));
|
|
1187
|
+
if (targets.length === 0)
|
|
1188
|
+
targets = undefined;
|
|
1189
|
+
}
|
|
1190
|
+
const r = emitCompanions({ outDir, targets });
|
|
1191
|
+
const files = r.emitted.map((p) => p.replace(r.outDir, "").replace(/^[\\/]/, "")).join(", ");
|
|
1192
|
+
if (r.rules_count === 0) {
|
|
1193
|
+
return `Emitted ${r.emitted.length} placeholder file(s) to ${r.outDir} (${files}) · no rules yet · run save_rule to add the first one.`;
|
|
1194
|
+
}
|
|
1195
|
+
return `Emitted ${r.rules_count} rule${r.rules_count === 1 ? "" : "s"} across ${r.targets.length} target${r.targets.length === 1 ? "" : "s"} (${r.targets.join(", ")}) → ${r.emitted.length} file${r.emitted.length === 1 ? "" : "s"} at ${r.outDir}: ${files}`;
|
|
1196
|
+
}
|
|
1197
|
+
function toolListRules(_args) {
|
|
1198
|
+
const rules = loadAllRules();
|
|
1199
|
+
if (rules.length === 0)
|
|
1200
|
+
return "No rules defined yet. Use save_rule to add one.";
|
|
1201
|
+
const lines = [];
|
|
1202
|
+
lines.push(c(ANSI.bold, `${rules.length} rule${rules.length === 1 ? "" : "s"} active:`));
|
|
1203
|
+
lines.push("");
|
|
1204
|
+
for (const r of rules) {
|
|
1205
|
+
const sev = r.severity ? ` [${r.severity}]` : "";
|
|
1206
|
+
const scope = r.scope && r.scope.length > 0 ? ` · scope: ${r.scope.join(", ")}` : "";
|
|
1207
|
+
const stale = r.last_verified && Date.now() - new Date(r.last_verified).getTime() > 90 * 86400_000
|
|
1208
|
+
? c(ANSI.yellow, " · STALE >90d")
|
|
1209
|
+
: "";
|
|
1210
|
+
lines.push(` ${r.name}${sev}${scope}${stale}`);
|
|
1211
|
+
lines.push(` ${r.description}`);
|
|
1212
|
+
}
|
|
1213
|
+
return lines.join("\n");
|
|
1214
|
+
}
|
|
1215
|
+
function toolSaveRule(args) {
|
|
1216
|
+
const name = String(args.name ?? "").trim();
|
|
1217
|
+
const description = String(args.description ?? "").trim();
|
|
1218
|
+
const content = String(args.content ?? "").trim();
|
|
1219
|
+
const severity = String(args.severity ?? "soft").trim();
|
|
1220
|
+
if (!VALID_RULE_SEVERITIES.has(severity)) {
|
|
1221
|
+
throw new Error(`Invalid severity "${severity}". Must be one of: ${Array.from(VALID_RULE_SEVERITIES).join(", ")}.`);
|
|
1222
|
+
}
|
|
1223
|
+
const scope = parseStringArray(args.scope);
|
|
1224
|
+
const applies_when = parseStringArray(args.applies_when);
|
|
1225
|
+
const matches = parseStringArray(args.matches);
|
|
1226
|
+
const enforce_on = parseStringArray(args.enforce_on);
|
|
1227
|
+
const last_verified = typeof args.last_verified === "string" && /^\d{4}-\d{2}-\d{2}$/.test(args.last_verified)
|
|
1228
|
+
? args.last_verified
|
|
1229
|
+
: new Date().toISOString().slice(0, 10);
|
|
1230
|
+
if (!SLUG_PATTERN.test(name)) {
|
|
1231
|
+
throw new Error(`Invalid name "${name}". Use lowercase (a-z, 0-9, hyphen, underscore), 1-80 chars, must start with letter or digit.`);
|
|
1232
|
+
}
|
|
1233
|
+
if (!description)
|
|
1234
|
+
throw new Error("description is required");
|
|
1235
|
+
if (!content)
|
|
1236
|
+
throw new Error("content is required");
|
|
1237
|
+
ensureStorage();
|
|
1238
|
+
const extras = [];
|
|
1239
|
+
extras.push(`severity: ${severity}`);
|
|
1240
|
+
if (scope)
|
|
1241
|
+
extras.push(`scope: [${scope.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1242
|
+
if (applies_when)
|
|
1243
|
+
extras.push(`applies_when: [${applies_when.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1244
|
+
if (matches)
|
|
1245
|
+
extras.push(`matches: [${matches.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1246
|
+
if (enforce_on)
|
|
1247
|
+
extras.push(`enforce_on: [${enforce_on.map((s) => JSON.stringify(s)).join(", ")}]`);
|
|
1248
|
+
extras.push(`last_verified: ${last_verified}`);
|
|
1249
|
+
const frontmatter = `---\n` +
|
|
1250
|
+
`name: ${name}\n` +
|
|
1251
|
+
`description: ${JSON.stringify(description)}\n` +
|
|
1252
|
+
`type: rule\n` +
|
|
1253
|
+
extras.map((e) => `${e}\n`).join("") +
|
|
1254
|
+
`schema: ${SCHEMA_VERSION}\n` +
|
|
1255
|
+
`---\n\n`;
|
|
1256
|
+
const fp = memoryFilePath(name);
|
|
1257
|
+
const isUpdate = existsSync(fp);
|
|
1258
|
+
return withLock(() => {
|
|
1259
|
+
atomicWriteFile(fp, frontmatter + content + "\n");
|
|
1260
|
+
upsertIndexEntryUnlocked(name, description);
|
|
1261
|
+
logEvent("save_rule", {
|
|
1262
|
+
name,
|
|
1263
|
+
severity,
|
|
1264
|
+
update: isUpdate,
|
|
1265
|
+
bytes: content.length,
|
|
1266
|
+
});
|
|
1267
|
+
log("debug", "save_rule", { name, severity, update: isUpdate });
|
|
1268
|
+
maybeAutoEmitCompanions();
|
|
1269
|
+
return `${isUpdate ? "Updated" : "Saved"} rule "${name}" (${severity}) at ${fp}`;
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
// -------------------------------------------------------------
|
|
918
1273
|
// Git sync · multi-machine memory via git remote
|
|
919
1274
|
// -------------------------------------------------------------
|
|
920
1275
|
//
|
|
@@ -1236,7 +1591,7 @@ function actionColor(action) {
|
|
|
1236
1591
|
// -------------------------------------------------------------
|
|
1237
1592
|
// Server wiring
|
|
1238
1593
|
// -------------------------------------------------------------
|
|
1239
|
-
const server = new Server({ name: "agent-memory", version: "0.
|
|
1594
|
+
const server = new Server({ name: "agent-memory", version: "0.11.1" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1240
1595
|
// -------------------------------------------------------------
|
|
1241
1596
|
// Resource URI scheme
|
|
1242
1597
|
// -------------------------------------------------------------
|
|
@@ -1479,8 +1834,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1479
1834
|
},
|
|
1480
1835
|
type: {
|
|
1481
1836
|
type: "string",
|
|
1482
|
-
enum: ["user", "feedback", "project", "reference"],
|
|
1483
|
-
description: "Memory type: user (about the person), feedback (
|
|
1837
|
+
enum: ["user", "feedback", "project", "reference", "rule"],
|
|
1838
|
+
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
1839
|
},
|
|
1485
1840
|
content: {
|
|
1486
1841
|
type: "string",
|
|
@@ -1671,6 +2026,92 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1671
2026
|
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
2027
|
inputSchema: { type: "object", properties: {} },
|
|
1673
2028
|
},
|
|
2029
|
+
{
|
|
2030
|
+
name: "save_rule",
|
|
2031
|
+
description: "Save (or update) a rule memory · the 'memory as constraint' wedge. " +
|
|
2032
|
+
"Rules constrain agent behavior, not just store facts. Severity 'hard' = " +
|
|
2033
|
+
"must obey; 'soft' = prefer to obey. Rules auto-project out to AGENTS.md " +
|
|
2034
|
+
"(read by Claude Code, Codex CLI, Cursor, Aider, Devin, Copilot, Gemini CLI, " +
|
|
2035
|
+
"Windsurf, and Amazon Q natively) when AGENT_MEMORY_AUTO_EMIT_DIR is set, or " +
|
|
2036
|
+
"via the emit_companions tool on demand.",
|
|
2037
|
+
inputSchema: {
|
|
2038
|
+
type: "object",
|
|
2039
|
+
properties: {
|
|
2040
|
+
name: {
|
|
2041
|
+
type: "string",
|
|
2042
|
+
description: "Short kebab-case slug, 1-80 chars (e.g. 'no-emojis-ever', 'tests-before-commit')",
|
|
2043
|
+
},
|
|
2044
|
+
description: {
|
|
2045
|
+
type: "string",
|
|
2046
|
+
description: "One-line summary of what the rule constrains",
|
|
2047
|
+
},
|
|
2048
|
+
content: {
|
|
2049
|
+
type: "string",
|
|
2050
|
+
description: "Markdown body. Lead with the rule itself, then **Why:** and **How to apply:** lines.",
|
|
2051
|
+
},
|
|
2052
|
+
severity: {
|
|
2053
|
+
type: "string",
|
|
2054
|
+
enum: ["hard", "soft"],
|
|
2055
|
+
description: "hard = must obey (rule violations are blocked when enforced); soft = prefer to obey (warned but allowed). Defaults to soft.",
|
|
2056
|
+
},
|
|
2057
|
+
scope: {
|
|
2058
|
+
type: "array",
|
|
2059
|
+
items: { type: "string" },
|
|
2060
|
+
description: "Where this rule applies. Examples: ['global'], ['project:prefixcheck'], ['tool:git'].",
|
|
2061
|
+
},
|
|
2062
|
+
applies_when: {
|
|
2063
|
+
type: "array",
|
|
2064
|
+
items: { type: "string" },
|
|
2065
|
+
description: "Natural-language conditions for when the rule triggers. Used by Sampling-enriched check_action on supporting clients.",
|
|
2066
|
+
},
|
|
2067
|
+
matches: {
|
|
2068
|
+
type: "array",
|
|
2069
|
+
items: { type: "string" },
|
|
2070
|
+
description: "Regex patterns that deterministically signal a violation. Used by Tier-1 check_action on every client.",
|
|
2071
|
+
},
|
|
2072
|
+
enforce_on: {
|
|
2073
|
+
type: "array",
|
|
2074
|
+
items: { type: "string" },
|
|
2075
|
+
description: "Action categories this rule constrains. Examples: 'file_writes', 'commits', 'pushes', 'chat_responses'.",
|
|
2076
|
+
},
|
|
2077
|
+
last_verified: {
|
|
2078
|
+
type: "string",
|
|
2079
|
+
description: "ISO date (YYYY-MM-DD) of last verification. Defaults to today.",
|
|
2080
|
+
},
|
|
2081
|
+
},
|
|
2082
|
+
required: ["name", "description", "content"],
|
|
2083
|
+
},
|
|
2084
|
+
},
|
|
2085
|
+
{
|
|
2086
|
+
name: "list_rules",
|
|
2087
|
+
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.",
|
|
2088
|
+
inputSchema: { type: "object", properties: {} },
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
name: "emit_companions",
|
|
2092
|
+
description: "Regenerate companion rule files from the current rule memories. " +
|
|
2093
|
+
"Writes one or more of: AGENTS.md (universal cross-tool standard, Linux Foundation), " +
|
|
2094
|
+
"CLAUDE.md (Claude Code's 5-level hierarchy), .cursor/rules/*.mdc (Cursor's MDC format · hard rules get alwaysApply:true, soft rules become description-driven), " +
|
|
2095
|
+
".gemini/instructions.md (Gemini CLI). " +
|
|
2096
|
+
"Default writes ALL four targets. Use `targets` to filter. Output dir resolves from `out_dir`, then AGENT_MEMORY_COMPANION_DIR env, then process.cwd().",
|
|
2097
|
+
inputSchema: {
|
|
2098
|
+
type: "object",
|
|
2099
|
+
properties: {
|
|
2100
|
+
out_dir: {
|
|
2101
|
+
type: "string",
|
|
2102
|
+
description: "Optional output directory. Defaults to AGENT_MEMORY_COMPANION_DIR env, then process.cwd().",
|
|
2103
|
+
},
|
|
2104
|
+
targets: {
|
|
2105
|
+
type: "array",
|
|
2106
|
+
items: {
|
|
2107
|
+
type: "string",
|
|
2108
|
+
enum: ["agents", "claude", "cursor", "gemini"],
|
|
2109
|
+
},
|
|
2110
|
+
description: "Which companion files to emit. Omit (or pass empty) to emit all four. Examples: ['agents'] for AGENTS.md only, ['claude','cursor'] for Claude Code + Cursor.",
|
|
2111
|
+
},
|
|
2112
|
+
},
|
|
2113
|
+
},
|
|
2114
|
+
},
|
|
1674
2115
|
],
|
|
1675
2116
|
}));
|
|
1676
2117
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1726,6 +2167,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1726
2167
|
case "sync_pull":
|
|
1727
2168
|
result = toolSyncPull(args);
|
|
1728
2169
|
break;
|
|
2170
|
+
case "save_rule":
|
|
2171
|
+
result = toolSaveRule(args);
|
|
2172
|
+
break;
|
|
2173
|
+
case "list_rules":
|
|
2174
|
+
result = toolListRules(args);
|
|
2175
|
+
break;
|
|
2176
|
+
case "emit_companions":
|
|
2177
|
+
result = toolEmitCompanions(args);
|
|
2178
|
+
break;
|
|
1729
2179
|
default:
|
|
1730
2180
|
throw new Error(`Unknown tool: ${name}`);
|
|
1731
2181
|
}
|
|
@@ -1762,6 +2212,9 @@ const CLI_COMMANDS = new Set([
|
|
|
1762
2212
|
"backlinks",
|
|
1763
2213
|
"related",
|
|
1764
2214
|
"sync",
|
|
2215
|
+
"save-rule",
|
|
2216
|
+
"list-rules",
|
|
2217
|
+
"emit-companions",
|
|
1765
2218
|
"ui",
|
|
1766
2219
|
"import-claude-code",
|
|
1767
2220
|
"help",
|
|
@@ -1955,6 +2408,58 @@ async function cliMain(command, rest) {
|
|
|
1955
2408
|
}) + "\n");
|
|
1956
2409
|
return 0;
|
|
1957
2410
|
}
|
|
2411
|
+
case "save-rule": {
|
|
2412
|
+
const name = positional[0];
|
|
2413
|
+
if (!name)
|
|
2414
|
+
throw new Error("Usage: agent-memory save-rule <name> --description <d> [--severity hard|soft] " +
|
|
2415
|
+
"[--scope a,b,c] [--applies-when a,b] [--matches a,b] [--enforce-on a,b] " +
|
|
2416
|
+
"[--content <c> | --content-file <path> | --stdin]");
|
|
2417
|
+
let content = String(flags.content ?? "");
|
|
2418
|
+
if (flags["content-file"]) {
|
|
2419
|
+
content = readFileSync(String(flags["content-file"]), "utf8");
|
|
2420
|
+
}
|
|
2421
|
+
else if (flags.stdin) {
|
|
2422
|
+
content = await readStdin();
|
|
2423
|
+
}
|
|
2424
|
+
const csv = (v) => {
|
|
2425
|
+
if (typeof v !== "string" || v.trim().length === 0)
|
|
2426
|
+
return undefined;
|
|
2427
|
+
return v
|
|
2428
|
+
.split(",")
|
|
2429
|
+
.map((x) => x.trim())
|
|
2430
|
+
.filter((x) => x.length > 0);
|
|
2431
|
+
};
|
|
2432
|
+
const result = toolSaveRule({
|
|
2433
|
+
name,
|
|
2434
|
+
description: String(flags.description ?? ""),
|
|
2435
|
+
content,
|
|
2436
|
+
severity: String(flags.severity ?? "soft"),
|
|
2437
|
+
scope: csv(flags.scope),
|
|
2438
|
+
applies_when: csv(flags["applies-when"]),
|
|
2439
|
+
matches: csv(flags.matches),
|
|
2440
|
+
enforce_on: csv(flags["enforce-on"]),
|
|
2441
|
+
last_verified: flags["last-verified"] ? String(flags["last-verified"]) : undefined,
|
|
2442
|
+
});
|
|
2443
|
+
process.stdout.write(result + "\n");
|
|
2444
|
+
return 0;
|
|
2445
|
+
}
|
|
2446
|
+
case "list-rules": {
|
|
2447
|
+
process.stdout.write(toolListRules({}) + "\n");
|
|
2448
|
+
return 0;
|
|
2449
|
+
}
|
|
2450
|
+
case "emit-companions": {
|
|
2451
|
+
const out = flags.out ? String(flags.out) : undefined;
|
|
2452
|
+
const target = flags.target;
|
|
2453
|
+
let targets;
|
|
2454
|
+
if (typeof target === "string" && target.length > 0) {
|
|
2455
|
+
targets = target
|
|
2456
|
+
.split(",")
|
|
2457
|
+
.map((t) => t.trim())
|
|
2458
|
+
.filter((t) => t.length > 0);
|
|
2459
|
+
}
|
|
2460
|
+
process.stdout.write(toolEmitCompanions({ out_dir: out, targets }) + "\n");
|
|
2461
|
+
return 0;
|
|
2462
|
+
}
|
|
1958
2463
|
case "ui": {
|
|
1959
2464
|
// Dynamic import so Ink + React only load when the TUI runs,
|
|
1960
2465
|
// 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.1",
|
|
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",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"format:check": "prettier --check .",
|
|
22
22
|
"test": "vitest run",
|
|
23
23
|
"test:watch": "vitest",
|
|
24
|
-
"prepublishOnly": "npm run build"
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"prepare": "husky"
|
|
25
26
|
},
|
|
26
27
|
"keywords": [
|
|
27
28
|
"mcp",
|
|
@@ -65,11 +66,18 @@
|
|
|
65
66
|
"@types/node": "^22.10.2",
|
|
66
67
|
"@types/proper-lockfile": "^4.1.4",
|
|
67
68
|
"@types/react": "^19.2.15",
|
|
69
|
+
"husky": "^9.1.7",
|
|
70
|
+
"lint-staged": "^17.0.5",
|
|
68
71
|
"prettier": "^3.8.3",
|
|
69
72
|
"typescript": "^5.7.2",
|
|
70
73
|
"vitest": "^4.1.7"
|
|
71
74
|
},
|
|
72
75
|
"engines": {
|
|
73
76
|
"node": ">=20"
|
|
77
|
+
},
|
|
78
|
+
"lint-staged": {
|
|
79
|
+
"*.{ts,tsx,js,mjs,cjs,json,jsonc,md,yml,yaml}": [
|
|
80
|
+
"prettier --write"
|
|
81
|
+
]
|
|
74
82
|
}
|
|
75
83
|
}
|