@xultrax-web/agent-memory-mcp 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/index.js +309 -15
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -27,17 +27,51 @@ agent-memory save-rule no-emojis-ever \
|
|
|
27
27
|
--enforce-on commits,chat_responses \
|
|
28
28
|
--content "No emojis. Anywhere. Ever."
|
|
29
29
|
|
|
30
|
-
agent-memory emit-companions
|
|
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)
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Companion file targets (v0.11.1):
|
|
34
36
|
|
|
35
|
-
|
|
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 |
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
Set `AGENT_MEMORY_AUTO_EMIT_DIR=/path/to/project` to auto-regenerate all companions on every rule save.
|
|
45
|
+
|
|
46
|
+
### Compliance Receipts (v0.11.2 · primitive · tool wiring in v0.11.3)
|
|
47
|
+
|
|
48
|
+
Receipts are short-lived, HMAC-signed bearer tokens with caveats (Macaroon pattern · [Birgisson et al., NDSS 2014](https://research.google/pubs/pub41892/)). The novel protocol primitive in agent-memory-mcp: server-issued tokens that bind to action + session + rules-version-hash + expiry. Tampering breaks the HMAC. Rule changes invalidate stale receipts (because `rules_version` is part of the signed payload).
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { issueReceipt, validateReceipt } from "@xultrax-web/agent-memory-mcp";
|
|
52
|
+
|
|
53
|
+
// Server-internal: issue a receipt for a destructive action
|
|
54
|
+
const r = issueReceipt({
|
|
55
|
+
caveats: [
|
|
56
|
+
{ type: "action", value: "delete_memory" },
|
|
57
|
+
{ type: "session", value: "sess_abc123" },
|
|
58
|
+
],
|
|
59
|
+
ttl_seconds: 60,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Later: validate before executing the destructive op
|
|
63
|
+
const v = validateReceipt(r, {
|
|
64
|
+
required_caveats: [{ type: "action", value: "delete_memory" }],
|
|
65
|
+
});
|
|
66
|
+
if (!v.valid) throw new Error(v.reason);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
HMAC key lives at `<MEMORY_DIR>/.keyring/hmac-key` · 32 random bytes · mode `0600`. v0.11.3 wires receipts into `delete_memory` + other destructive tools and adds the `check_action` MCP tool.
|
|
70
|
+
|
|
71
|
+
### Roadmap for the v0.11.x series:
|
|
72
|
+
|
|
73
|
+
- `check_action` tool · deterministic rule matching · optional Sampling enrichment where clients support it · issues Compliance Receipts when proposed action passes
|
|
74
|
+
- `audit` command · rule conflicts · staleness · receipt-denial log
|
|
41
75
|
|
|
42
76
|
---
|
|
43
77
|
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema
|
|
|
27
27
|
import Fuse from "fuse.js";
|
|
28
28
|
import matter from "gray-matter";
|
|
29
29
|
import { spawnSync } from "node:child_process";
|
|
30
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
30
31
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
|
|
31
32
|
import { homedir } from "node:os";
|
|
32
33
|
import { join, resolve } from "node:path";
|
|
@@ -1022,6 +1023,7 @@ function buildAgentsMdContent(rules) {
|
|
|
1022
1023
|
}
|
|
1023
1024
|
return parts.join("\n");
|
|
1024
1025
|
}
|
|
1026
|
+
export const ALL_COMPANION_TARGETS = ["agents", "claude", "cursor", "gemini"];
|
|
1025
1027
|
function resolveCompanionDir(explicit) {
|
|
1026
1028
|
if (explicit && explicit.trim().length > 0)
|
|
1027
1029
|
return explicit.trim();
|
|
@@ -1030,22 +1032,137 @@ function resolveCompanionDir(explicit) {
|
|
|
1030
1032
|
return envOverride.trim();
|
|
1031
1033
|
return process.cwd();
|
|
1032
1034
|
}
|
|
1035
|
+
// CLAUDE.md content · same body as AGENTS.md but with a Claude-Code-specific
|
|
1036
|
+
// header. Claude Code's 5-level hierarchy (managed/global/project/local/subdir)
|
|
1037
|
+
// reads any CLAUDE.md it finds; we generate the project-root file by default.
|
|
1038
|
+
function buildClaudeMdContent(rules) {
|
|
1039
|
+
const body = buildAgentsMdContent(rules);
|
|
1040
|
+
// Replace the AGENTS.md-specific header sentence with a CLAUDE.md one
|
|
1041
|
+
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`);
|
|
1042
|
+
}
|
|
1043
|
+
// .gemini/instructions.md content · same body as AGENTS.md, slightly different
|
|
1044
|
+
// header.
|
|
1045
|
+
function buildGeminiInstructionsContent(rules) {
|
|
1046
|
+
const body = buildAgentsMdContent(rules);
|
|
1047
|
+
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`);
|
|
1048
|
+
}
|
|
1049
|
+
// Cursor consumes .cursor/rules/*.mdc files with their own YAML frontmatter.
|
|
1050
|
+
// Per spec: each file <150 lines, alwaysApply file <50 lines, dir total <500.
|
|
1051
|
+
// Strategy: one file per severity (hard / soft) — hard is alwaysApply, soft
|
|
1052
|
+
// is description-driven so the agent pulls it in when relevant.
|
|
1053
|
+
function buildCursorMdcFiles(rules) {
|
|
1054
|
+
const hard = rules.filter((r) => r.severity === "hard");
|
|
1055
|
+
const soft = rules.filter((r) => r.severity !== "hard");
|
|
1056
|
+
const files = [];
|
|
1057
|
+
if (hard.length > 0) {
|
|
1058
|
+
const fm = [
|
|
1059
|
+
"---",
|
|
1060
|
+
`description: "Operator hard rules · always obey · auto-generated from agent-memory-mcp"`,
|
|
1061
|
+
"alwaysApply: true",
|
|
1062
|
+
"---",
|
|
1063
|
+
"",
|
|
1064
|
+
"# Operator hard rules",
|
|
1065
|
+
"",
|
|
1066
|
+
"These rules MUST be obeyed. Violations should be flagged and blocked.",
|
|
1067
|
+
"",
|
|
1068
|
+
].join("\n");
|
|
1069
|
+
files.push({
|
|
1070
|
+
filename: "operator-hard.mdc",
|
|
1071
|
+
content: fm + hard.map((r) => formatRuleAsMarkdown(r)).join("\n"),
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (soft.length > 0) {
|
|
1075
|
+
const fm = [
|
|
1076
|
+
"---",
|
|
1077
|
+
`description: "Operator conventions · prefer to obey · pulled in by agent on relevance"`,
|
|
1078
|
+
"alwaysApply: false",
|
|
1079
|
+
"---",
|
|
1080
|
+
"",
|
|
1081
|
+
"# Operator conventions",
|
|
1082
|
+
"",
|
|
1083
|
+
"Soft rules · prefer to obey. The agent may consult these when the context warrants.",
|
|
1084
|
+
"",
|
|
1085
|
+
].join("\n");
|
|
1086
|
+
files.push({
|
|
1087
|
+
filename: "operator-conventions.mdc",
|
|
1088
|
+
content: fm + soft.map((r) => formatRuleAsMarkdown(r)).join("\n"),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
return files;
|
|
1092
|
+
}
|
|
1033
1093
|
function emitCompanions(opts = {}) {
|
|
1034
1094
|
const outDir = resolveCompanionDir(opts.outDir);
|
|
1095
|
+
const targets = opts.targets && opts.targets.length > 0 ? opts.targets : ALL_COMPANION_TARGETS;
|
|
1035
1096
|
const rules = loadAllRules();
|
|
1036
|
-
|
|
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.
|
|
1097
|
+
// Best-effort directory creation; mkdirSync recursive is idempotent.
|
|
1040
1098
|
try {
|
|
1041
1099
|
mkdirSync(outDir, { recursive: true });
|
|
1042
1100
|
}
|
|
1043
1101
|
catch {
|
|
1044
|
-
// Ignore — atomicWriteFile
|
|
1102
|
+
// Ignore — atomicWriteFile surfaces a clearer error if needed.
|
|
1103
|
+
}
|
|
1104
|
+
const emitted = [];
|
|
1105
|
+
if (targets.includes("agents")) {
|
|
1106
|
+
const fp = join(outDir, "AGENTS.md");
|
|
1107
|
+
atomicWriteFile(fp, buildAgentsMdContent(rules));
|
|
1108
|
+
emitted.push(fp);
|
|
1109
|
+
}
|
|
1110
|
+
if (targets.includes("claude")) {
|
|
1111
|
+
const fp = join(outDir, "CLAUDE.md");
|
|
1112
|
+
atomicWriteFile(fp, buildClaudeMdContent(rules));
|
|
1113
|
+
emitted.push(fp);
|
|
1114
|
+
}
|
|
1115
|
+
if (targets.includes("cursor")) {
|
|
1116
|
+
const cursorDir = join(outDir, ".cursor", "rules");
|
|
1117
|
+
try {
|
|
1118
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
// Ignore — atomicWriteFile surfaces clearer error if needed.
|
|
1122
|
+
}
|
|
1123
|
+
const files = buildCursorMdcFiles(rules);
|
|
1124
|
+
if (files.length === 0) {
|
|
1125
|
+
// No rules yet — drop a placeholder so the tool knows where to put them
|
|
1126
|
+
const placeholderPath = join(cursorDir, "operator-rules.mdc");
|
|
1127
|
+
const placeholder = [
|
|
1128
|
+
"---",
|
|
1129
|
+
`description: "Operator rules · auto-generated · no rules defined yet"`,
|
|
1130
|
+
"alwaysApply: false",
|
|
1131
|
+
"---",
|
|
1132
|
+
"",
|
|
1133
|
+
"No rules defined yet. Run `agent-memory save-rule` to add the first one.",
|
|
1134
|
+
"",
|
|
1135
|
+
].join("\n");
|
|
1136
|
+
atomicWriteFile(placeholderPath, placeholder);
|
|
1137
|
+
emitted.push(placeholderPath);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
for (const f of files) {
|
|
1141
|
+
const fp = join(cursorDir, f.filename);
|
|
1142
|
+
atomicWriteFile(fp, f.content);
|
|
1143
|
+
emitted.push(fp);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1045
1146
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1147
|
+
if (targets.includes("gemini")) {
|
|
1148
|
+
const geminiDir = join(outDir, ".gemini");
|
|
1149
|
+
try {
|
|
1150
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// Ignore — atomicWriteFile surfaces clearer error if needed.
|
|
1154
|
+
}
|
|
1155
|
+
const fp = join(geminiDir, "instructions.md");
|
|
1156
|
+
atomicWriteFile(fp, buildGeminiInstructionsContent(rules));
|
|
1157
|
+
emitted.push(fp);
|
|
1158
|
+
}
|
|
1159
|
+
logEvent("emit_companions", {
|
|
1160
|
+
outDir,
|
|
1161
|
+
rules_count: rules.length,
|
|
1162
|
+
targets,
|
|
1163
|
+
files: emitted.map((p) => p.replace(outDir, "").replace(/^[\\/]/, "")),
|
|
1164
|
+
});
|
|
1165
|
+
return { outDir, emitted, rules_count: rules.length, targets };
|
|
1049
1166
|
}
|
|
1050
1167
|
function maybeAutoEmitCompanions() {
|
|
1051
1168
|
const autoDir = process.env.AGENT_MEMORY_AUTO_EMIT_DIR;
|
|
@@ -1063,11 +1180,20 @@ function maybeAutoEmitCompanions() {
|
|
|
1063
1180
|
}
|
|
1064
1181
|
function toolEmitCompanions(args) {
|
|
1065
1182
|
const outDir = typeof args.out_dir === "string" ? args.out_dir : undefined;
|
|
1066
|
-
|
|
1183
|
+
let targets;
|
|
1184
|
+
if (Array.isArray(args.targets)) {
|
|
1185
|
+
targets = args.targets
|
|
1186
|
+
.filter((t) => typeof t === "string")
|
|
1187
|
+
.filter((t) => ALL_COMPANION_TARGETS.includes(t));
|
|
1188
|
+
if (targets.length === 0)
|
|
1189
|
+
targets = undefined;
|
|
1190
|
+
}
|
|
1191
|
+
const r = emitCompanions({ outDir, targets });
|
|
1192
|
+
const files = r.emitted.map((p) => p.replace(r.outDir, "").replace(/^[\\/]/, "")).join(", ");
|
|
1067
1193
|
if (r.rules_count === 0) {
|
|
1068
|
-
return `Emitted ${r.emitted
|
|
1194
|
+
return `Emitted ${r.emitted.length} placeholder file(s) to ${r.outDir} (${files}) · no rules yet · run save_rule to add the first one.`;
|
|
1069
1195
|
}
|
|
1070
|
-
return `Emitted ${r.rules_count} rule${r.rules_count === 1 ? "" : "s"}
|
|
1196
|
+
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}`;
|
|
1071
1197
|
}
|
|
1072
1198
|
function toolListRules(_args) {
|
|
1073
1199
|
const rules = loadAllRules();
|
|
@@ -1145,6 +1271,154 @@ function toolSaveRule(args) {
|
|
|
1145
1271
|
});
|
|
1146
1272
|
}
|
|
1147
1273
|
// -------------------------------------------------------------
|
|
1274
|
+
// Compliance Receipts · v0.11.2 · the novel protocol primitive
|
|
1275
|
+
// -------------------------------------------------------------
|
|
1276
|
+
//
|
|
1277
|
+
// Receipts are short-lived, HMAC-signed bearer tokens with caveats
|
|
1278
|
+
// (attenuations). Macaroon-style. Issued by `check_action` (v0.11.3),
|
|
1279
|
+
// validated before our own destructive tools execute. Prior art:
|
|
1280
|
+
//
|
|
1281
|
+
// Birgisson et al · "Macaroons: Cookies with Contextual Caveats for
|
|
1282
|
+
// Decentralized Authorization in the Cloud" · Google Research,
|
|
1283
|
+
// NDSS 2014 · https://research.google/pubs/pub41892/
|
|
1284
|
+
//
|
|
1285
|
+
// Why receipts work where MCP Sampling doesn't:
|
|
1286
|
+
// - MCP Sampling is unsupported on Claude Code / Cursor / Cline /
|
|
1287
|
+
// Codex CLI (the primary coding clients) per the MCP client matrix.
|
|
1288
|
+
// - Receipts are server-issued protocol artifacts — they work on every
|
|
1289
|
+
// client because the server controls both ends (issue + validate).
|
|
1290
|
+
// - Receipts bind to: action + session + rules-version-hash + expiry.
|
|
1291
|
+
// Tampering breaks the HMAC. Rule changes invalidate stale receipts.
|
|
1292
|
+
//
|
|
1293
|
+
// Storage:
|
|
1294
|
+
// HMAC key lives at <MEMORY_DIR>/.keyring/hmac-key · 32 random bytes
|
|
1295
|
+
// created on first use with mode 0o600 (owner read/write only).
|
|
1296
|
+
// Caller-rotatable via `agent-memory rotate-key` (a v0.11.x follow-up).
|
|
1297
|
+
//
|
|
1298
|
+
// v0.11.2 ships the PRIMITIVE only — issuance + validation +
|
|
1299
|
+
// canonicalization. Tool wiring (delete_memory + check_action) lands
|
|
1300
|
+
// in v0.11.3.
|
|
1301
|
+
const KEYRING_DIR = join(MEMORY_DIR, ".keyring");
|
|
1302
|
+
const HMAC_KEY_FILE = join(KEYRING_DIR, "hmac-key");
|
|
1303
|
+
const RECEIPT_DEFAULT_TTL_SECONDS = 60;
|
|
1304
|
+
function loadOrCreateHmacKey() {
|
|
1305
|
+
if (existsSync(HMAC_KEY_FILE)) {
|
|
1306
|
+
return readFileSync(HMAC_KEY_FILE);
|
|
1307
|
+
}
|
|
1308
|
+
if (!existsSync(KEYRING_DIR)) {
|
|
1309
|
+
mkdirSync(KEYRING_DIR, { recursive: true });
|
|
1310
|
+
}
|
|
1311
|
+
const key = randomBytes(32); // 256 bits · plenty for HMAC-SHA256
|
|
1312
|
+
// mode 0o600 is owner-only on POSIX; Windows ignores mode but ACLs
|
|
1313
|
+
// default to the user, so practically equivalent for our threat model.
|
|
1314
|
+
writeFileSync(HMAC_KEY_FILE, key, { mode: 0o600 });
|
|
1315
|
+
return key;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Compute the rules-version hash · first 16 hex chars of SHA-256 over
|
|
1319
|
+
* the concatenated bytes of every type=rule memory file, in sorted
|
|
1320
|
+
* filename order. Any rule add / edit / remove changes this hash,
|
|
1321
|
+
* which invalidates outstanding receipts (they were issued against a
|
|
1322
|
+
* different rule set).
|
|
1323
|
+
*/
|
|
1324
|
+
function computeRulesVersion() {
|
|
1325
|
+
const rules = loadAllRules();
|
|
1326
|
+
const sortedPaths = rules.map((r) => r.filePath).sort();
|
|
1327
|
+
const hash = createHash("sha256");
|
|
1328
|
+
for (const fp of sortedPaths) {
|
|
1329
|
+
try {
|
|
1330
|
+
hash.update(readFileSync(fp));
|
|
1331
|
+
}
|
|
1332
|
+
catch {
|
|
1333
|
+
// File disappeared between listMemoryFiles + read; skip it.
|
|
1334
|
+
// Next computation will reflect the change.
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return hash.digest("hex").slice(0, 16);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Deterministic canonical form for HMAC input · caveats sorted by
|
|
1341
|
+
* (type, value) so the order in which the caller listed them doesn't
|
|
1342
|
+
* change the signature. JSON with no whitespace (single line) so the
|
|
1343
|
+
* exact byte sequence is reproducible across platforms.
|
|
1344
|
+
*/
|
|
1345
|
+
function canonicalizeReceipt(r) {
|
|
1346
|
+
const sortedCaveats = [...r.caveats].sort((a, b) => a.type === b.type ? a.value.localeCompare(b.value) : a.type.localeCompare(b.type));
|
|
1347
|
+
return JSON.stringify({
|
|
1348
|
+
id: r.id,
|
|
1349
|
+
issued_at: r.issued_at,
|
|
1350
|
+
expires_at: r.expires_at,
|
|
1351
|
+
rules_version: r.rules_version,
|
|
1352
|
+
caveats: sortedCaveats,
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
function signReceipt(r) {
|
|
1356
|
+
const key = loadOrCreateHmacKey();
|
|
1357
|
+
return createHmac("sha256", key).update(canonicalizeReceipt(r)).digest("hex");
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Issue a fresh Compliance Receipt with the given caveats. The receipt
|
|
1361
|
+
* is bound to the current rule-store hash; any rule edit invalidates it.
|
|
1362
|
+
*/
|
|
1363
|
+
export function issueReceipt(opts) {
|
|
1364
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1365
|
+
const ttl = Math.max(1, opts.ttl_seconds ?? RECEIPT_DEFAULT_TTL_SECONDS);
|
|
1366
|
+
const base = {
|
|
1367
|
+
id: "rcpt_" + randomBytes(8).toString("hex"),
|
|
1368
|
+
issued_at: now,
|
|
1369
|
+
expires_at: now + ttl,
|
|
1370
|
+
rules_version: computeRulesVersion(),
|
|
1371
|
+
caveats: opts.caveats,
|
|
1372
|
+
};
|
|
1373
|
+
return { ...base, signature: signReceipt(base) };
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Validate a Compliance Receipt against the current rule store + caller's
|
|
1377
|
+
* required caveats. Returns {valid: true} on success, otherwise
|
|
1378
|
+
* {valid: false, reason: <human-readable>}.
|
|
1379
|
+
*/
|
|
1380
|
+
export function validateReceipt(receipt, opts = {}) {
|
|
1381
|
+
// 1. HMAC verification · constant-time compare to avoid timing leaks.
|
|
1382
|
+
const expected = signReceipt({
|
|
1383
|
+
id: receipt.id,
|
|
1384
|
+
issued_at: receipt.issued_at,
|
|
1385
|
+
expires_at: receipt.expires_at,
|
|
1386
|
+
rules_version: receipt.rules_version,
|
|
1387
|
+
caveats: receipt.caveats,
|
|
1388
|
+
});
|
|
1389
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
1390
|
+
const actualBuf = Buffer.from(receipt.signature, "hex");
|
|
1391
|
+
if (expectedBuf.length !== actualBuf.length || !timingSafeEqual(expectedBuf, actualBuf)) {
|
|
1392
|
+
return { valid: false, reason: "invalid signature" };
|
|
1393
|
+
}
|
|
1394
|
+
// 2. Expiry · receipts past their expires_at are dead.
|
|
1395
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1396
|
+
if (now > receipt.expires_at) {
|
|
1397
|
+
return { valid: false, reason: "receipt expired" };
|
|
1398
|
+
}
|
|
1399
|
+
// 3. Rules-version binding · any rule edit since issuance invalidates.
|
|
1400
|
+
const currentRulesVersion = opts.current_rules_version ?? computeRulesVersion();
|
|
1401
|
+
if (receipt.rules_version !== currentRulesVersion) {
|
|
1402
|
+
return {
|
|
1403
|
+
valid: false,
|
|
1404
|
+
reason: `rules changed since receipt issued (was ${receipt.rules_version}, now ${currentRulesVersion})`,
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
// 4. Required-caveat check · each required pair must appear on the receipt.
|
|
1408
|
+
if (opts.required_caveats) {
|
|
1409
|
+
for (const required of opts.required_caveats) {
|
|
1410
|
+
const found = receipt.caveats.find((c) => c.type === required.type && c.value === required.value);
|
|
1411
|
+
if (!found) {
|
|
1412
|
+
return {
|
|
1413
|
+
valid: false,
|
|
1414
|
+
reason: `missing required caveat: ${required.type}=${required.value}`,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return { valid: true };
|
|
1420
|
+
}
|
|
1421
|
+
// -------------------------------------------------------------
|
|
1148
1422
|
// Git sync · multi-machine memory via git remote
|
|
1149
1423
|
// -------------------------------------------------------------
|
|
1150
1424
|
//
|
|
@@ -1466,7 +1740,7 @@ function actionColor(action) {
|
|
|
1466
1740
|
// -------------------------------------------------------------
|
|
1467
1741
|
// Server wiring
|
|
1468
1742
|
// -------------------------------------------------------------
|
|
1469
|
-
const server = new Server({ name: "agent-memory", version: "0.11.
|
|
1743
|
+
const server = new Server({ name: "agent-memory", version: "0.11.2" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1470
1744
|
// -------------------------------------------------------------
|
|
1471
1745
|
// Resource URI scheme
|
|
1472
1746
|
// -------------------------------------------------------------
|
|
@@ -1964,13 +2238,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1964
2238
|
},
|
|
1965
2239
|
{
|
|
1966
2240
|
name: "emit_companions",
|
|
1967
|
-
description: "Regenerate companion rule files
|
|
2241
|
+
description: "Regenerate companion rule files from the current rule memories. " +
|
|
2242
|
+
"Writes one or more of: AGENTS.md (universal cross-tool standard, Linux Foundation), " +
|
|
2243
|
+
"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), " +
|
|
2244
|
+
".gemini/instructions.md (Gemini CLI). " +
|
|
2245
|
+
"Default writes ALL four targets. Use `targets` to filter. Output dir resolves from `out_dir`, then AGENT_MEMORY_COMPANION_DIR env, then process.cwd().",
|
|
1968
2246
|
inputSchema: {
|
|
1969
2247
|
type: "object",
|
|
1970
2248
|
properties: {
|
|
1971
2249
|
out_dir: {
|
|
1972
2250
|
type: "string",
|
|
1973
|
-
description: "Optional output directory. Defaults to AGENT_MEMORY_COMPANION_DIR env
|
|
2251
|
+
description: "Optional output directory. Defaults to AGENT_MEMORY_COMPANION_DIR env, then process.cwd().",
|
|
2252
|
+
},
|
|
2253
|
+
targets: {
|
|
2254
|
+
type: "array",
|
|
2255
|
+
items: {
|
|
2256
|
+
type: "string",
|
|
2257
|
+
enum: ["agents", "claude", "cursor", "gemini"],
|
|
2258
|
+
},
|
|
2259
|
+
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.",
|
|
1974
2260
|
},
|
|
1975
2261
|
},
|
|
1976
2262
|
},
|
|
@@ -2312,7 +2598,15 @@ async function cliMain(command, rest) {
|
|
|
2312
2598
|
}
|
|
2313
2599
|
case "emit-companions": {
|
|
2314
2600
|
const out = flags.out ? String(flags.out) : undefined;
|
|
2315
|
-
|
|
2601
|
+
const target = flags.target;
|
|
2602
|
+
let targets;
|
|
2603
|
+
if (typeof target === "string" && target.length > 0) {
|
|
2604
|
+
targets = target
|
|
2605
|
+
.split(",")
|
|
2606
|
+
.map((t) => t.trim())
|
|
2607
|
+
.filter((t) => t.length > 0);
|
|
2608
|
+
}
|
|
2609
|
+
process.stdout.write(toolEmitCompanions({ out_dir: out, targets }) + "\n");
|
|
2316
2610
|
return 0;
|
|
2317
2611
|
}
|
|
2318
2612
|
case "ui": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xultrax-web/agent-memory-mcp",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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
|
}
|