@xultrax-web/agent-memory-mcp 0.11.3 → 0.11.5
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 +27 -2
- package/dist/index.js +236 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,10 +97,35 @@ if (!v.valid) throw new Error(v.reason);
|
|
|
97
97
|
|
|
98
98
|
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.
|
|
99
99
|
|
|
100
|
+
### `audit` command (v0.11.4)
|
|
101
|
+
|
|
102
|
+
Daily operational health report for the rule store:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
agent-memory audit # pretty colored terminal output
|
|
106
|
+
agent-memory audit --json # structured JSON for tooling
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Surfaces:
|
|
110
|
+
|
|
111
|
+
- Rule count broken down by severity (hard / soft / unspecified)
|
|
112
|
+
- **Stale rules** · `last_verified` > 90 days ago, or never verified
|
|
113
|
+
- **Pattern conflicts** · two rules sharing an `enforce_on` category AND an identical regex in their `matches` arrays
|
|
114
|
+
- **Recent denials** · `check_action` calls that blocked an action (helps spot over-aggressive rules)
|
|
115
|
+
- **Unreceipted destructive ops** · `delete_memory` calls that bypassed the receipt path (back-compat in v0.11.x · v0.12 will remove the path)
|
|
116
|
+
|
|
117
|
+
The `healthy` flag is true iff no stale rules, no conflicts, no unreceipted ops in the recent log.
|
|
118
|
+
|
|
119
|
+
### Compliance Receipt Protocol 1.0 spec (v0.11.5)
|
|
120
|
+
|
|
121
|
+
The CRP enforcement primitive is documented as a standalone spec at [docs/compliance-receipt-protocol-1.0.md](docs/compliance-receipt-protocol-1.0.md). Other MCP servers can adopt the same receipt format + validation rules to interoperate · `agent-memory-mcp` is the reference implementation.
|
|
122
|
+
|
|
123
|
+
The spec covers: receipt structure, canonical encoding, signing (HMAC-SHA256), validation order, rules-version hashing, reserved caveat types, MCP integration patterns, security considerations, cross-server adoption, and test vectors.
|
|
124
|
+
|
|
100
125
|
### Roadmap for the v0.11.x series:
|
|
101
126
|
|
|
102
|
-
- `check_action`
|
|
103
|
-
-
|
|
127
|
+
- Sampling-enriched Tier-2 `check_action` for clients that support it (Claude Desktop, VS Code Copilot)
|
|
128
|
+
- Repositioning · README hero rewrite, /labs/agent-memory/ page, npm description (v0.11.6)
|
|
104
129
|
|
|
105
130
|
---
|
|
106
131
|
|
package/dist/index.js
CHANGED
|
@@ -190,6 +190,20 @@ function parseStringArray(input) {
|
|
|
190
190
|
const out = input.filter((x) => typeof x === "string" && x.length > 0);
|
|
191
191
|
return out.length > 0 ? out : undefined;
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* js-yaml (gray-matter's parser) auto-coerces ISO-8601 date strings into
|
|
195
|
+
* JavaScript Date objects unless quoted. Normalize to YYYY-MM-DD string
|
|
196
|
+
* regardless of whether the YAML gave us a string or a Date.
|
|
197
|
+
*/
|
|
198
|
+
function parseLastVerified(input) {
|
|
199
|
+
if (input instanceof Date) {
|
|
200
|
+
return input.toISOString().slice(0, 10);
|
|
201
|
+
}
|
|
202
|
+
if (typeof input === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
203
|
+
return input;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
193
207
|
export function readMemory(name) {
|
|
194
208
|
const fp = memoryFilePath(name);
|
|
195
209
|
if (!existsSync(fp))
|
|
@@ -210,9 +224,7 @@ export function readMemory(name) {
|
|
|
210
224
|
applies_when: parseStringArray(fm.applies_when),
|
|
211
225
|
matches: parseStringArray(fm.matches),
|
|
212
226
|
enforce_on: parseStringArray(fm.enforce_on),
|
|
213
|
-
last_verified:
|
|
214
|
-
? fm.last_verified
|
|
215
|
-
: undefined,
|
|
227
|
+
last_verified: parseLastVerified(fm.last_verified),
|
|
216
228
|
};
|
|
217
229
|
}
|
|
218
230
|
export function listMemoryFiles() {
|
|
@@ -1509,7 +1521,7 @@ function toolCheckAction(args) {
|
|
|
1509
1521
|
rules_evaluated,
|
|
1510
1522
|
};
|
|
1511
1523
|
logEvent("check_action_denied", {
|
|
1512
|
-
action,
|
|
1524
|
+
proposed_action: action,
|
|
1513
1525
|
action_type: actionType,
|
|
1514
1526
|
hard_count: hard.length,
|
|
1515
1527
|
soft_count: soft.length,
|
|
@@ -1532,7 +1544,7 @@ function toolCheckAction(args) {
|
|
|
1532
1544
|
rules_evaluated,
|
|
1533
1545
|
};
|
|
1534
1546
|
logEvent("check_action_approved", {
|
|
1535
|
-
action,
|
|
1547
|
+
proposed_action: action,
|
|
1536
1548
|
action_type: actionType,
|
|
1537
1549
|
receipt_id: receipt.id,
|
|
1538
1550
|
soft_count: soft.length,
|
|
@@ -1560,6 +1572,200 @@ function parseReceiptArg(input) {
|
|
|
1560
1572
|
return null;
|
|
1561
1573
|
}
|
|
1562
1574
|
// -------------------------------------------------------------
|
|
1575
|
+
// audit · v0.11.4 · daily-rhythm operational health command
|
|
1576
|
+
// -------------------------------------------------------------
|
|
1577
|
+
//
|
|
1578
|
+
// Surfaces what's working and what's drifting in the rule store:
|
|
1579
|
+
// - Rule count, broken down by severity
|
|
1580
|
+
// - Stale rules (last_verified > 90 days ago, or never verified)
|
|
1581
|
+
// - Pattern conflicts (two rules sharing an enforce_on category AND
|
|
1582
|
+
// an identical regex in their matches arrays · likely contradiction)
|
|
1583
|
+
// - Recent check_action denials (the system blocked something — was
|
|
1584
|
+
// that the right call? is a rule too aggressive?)
|
|
1585
|
+
// - Recent unreceipted destructive ops (back-compat path · v0.12 will
|
|
1586
|
+
// remove it but for v0.11.x audit just flags them)
|
|
1587
|
+
const STALE_THRESHOLD_DAYS = 90;
|
|
1588
|
+
const AUDIT_EVENT_TAIL = 50;
|
|
1589
|
+
function summarizeRules(rules) {
|
|
1590
|
+
let hard = 0;
|
|
1591
|
+
let soft = 0;
|
|
1592
|
+
let noSev = 0;
|
|
1593
|
+
for (const r of rules) {
|
|
1594
|
+
if (r.severity === "hard")
|
|
1595
|
+
hard++;
|
|
1596
|
+
else if (r.severity === "soft")
|
|
1597
|
+
soft++;
|
|
1598
|
+
else
|
|
1599
|
+
noSev++;
|
|
1600
|
+
}
|
|
1601
|
+
return { total: rules.length, hard, soft, no_severity: noSev };
|
|
1602
|
+
}
|
|
1603
|
+
function findStaleRules(rules, thresholdDays) {
|
|
1604
|
+
const now = Date.now();
|
|
1605
|
+
const stale = [];
|
|
1606
|
+
for (const r of rules) {
|
|
1607
|
+
if (!r.last_verified) {
|
|
1608
|
+
stale.push({
|
|
1609
|
+
name: r.name,
|
|
1610
|
+
last_verified: null,
|
|
1611
|
+
age_days: null,
|
|
1612
|
+
description: r.description,
|
|
1613
|
+
});
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const verifiedAt = new Date(r.last_verified).getTime();
|
|
1617
|
+
if (Number.isNaN(verifiedAt)) {
|
|
1618
|
+
stale.push({
|
|
1619
|
+
name: r.name,
|
|
1620
|
+
last_verified: r.last_verified,
|
|
1621
|
+
age_days: null,
|
|
1622
|
+
description: r.description,
|
|
1623
|
+
});
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const ageDays = Math.floor((now - verifiedAt) / 86400_000);
|
|
1627
|
+
if (ageDays > thresholdDays) {
|
|
1628
|
+
stale.push({
|
|
1629
|
+
name: r.name,
|
|
1630
|
+
last_verified: r.last_verified,
|
|
1631
|
+
age_days: ageDays,
|
|
1632
|
+
description: r.description,
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return stale;
|
|
1637
|
+
}
|
|
1638
|
+
function findPatternConflicts(rules) {
|
|
1639
|
+
const conflicts = [];
|
|
1640
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1641
|
+
const a = rules[i];
|
|
1642
|
+
if (!a.matches || a.matches.length === 0)
|
|
1643
|
+
continue;
|
|
1644
|
+
for (let j = i + 1; j < rules.length; j++) {
|
|
1645
|
+
const b = rules[j];
|
|
1646
|
+
if (!b.matches || b.matches.length === 0)
|
|
1647
|
+
continue;
|
|
1648
|
+
// Need overlap in enforce_on (or both empty → universal)
|
|
1649
|
+
const aCategories = a.enforce_on ?? [];
|
|
1650
|
+
const bCategories = b.enforce_on ?? [];
|
|
1651
|
+
const sharedCategories = aCategories.length === 0 || bCategories.length === 0
|
|
1652
|
+
? ["(universal)"]
|
|
1653
|
+
: aCategories.filter((c) => bCategories.includes(c));
|
|
1654
|
+
if (sharedCategories.length === 0)
|
|
1655
|
+
continue;
|
|
1656
|
+
// Need at least one identical pattern · low-false-positive heuristic
|
|
1657
|
+
const sharedPatterns = a.matches.filter((p) => b.matches.includes(p));
|
|
1658
|
+
if (sharedPatterns.length === 0)
|
|
1659
|
+
continue;
|
|
1660
|
+
for (const cat of sharedCategories) {
|
|
1661
|
+
for (const pat of sharedPatterns) {
|
|
1662
|
+
conflicts.push({
|
|
1663
|
+
rule_a: a.name,
|
|
1664
|
+
rule_b: b.name,
|
|
1665
|
+
shared_pattern: pat,
|
|
1666
|
+
shared_enforce_on: cat,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return conflicts;
|
|
1673
|
+
}
|
|
1674
|
+
function recentDenials() {
|
|
1675
|
+
const records = readEventLog({ tail: AUDIT_EVENT_TAIL, action: "check_action_denied" });
|
|
1676
|
+
return records.map((r) => ({
|
|
1677
|
+
ts: String(r.ts),
|
|
1678
|
+
action: String(r.proposed_action ?? ""),
|
|
1679
|
+
action_type: String(r.action_type ?? ""),
|
|
1680
|
+
hard_count: typeof r.hard_count === "number" ? r.hard_count : 0,
|
|
1681
|
+
}));
|
|
1682
|
+
}
|
|
1683
|
+
function recentUnreceiptedDeletes() {
|
|
1684
|
+
const records = readEventLog({ tail: AUDIT_EVENT_TAIL, action: "delete_without_receipt" });
|
|
1685
|
+
return records.map((r) => ({
|
|
1686
|
+
ts: String(r.ts),
|
|
1687
|
+
name: String(r.name ?? ""),
|
|
1688
|
+
}));
|
|
1689
|
+
}
|
|
1690
|
+
export function runAudit() {
|
|
1691
|
+
const rules = loadAllRules();
|
|
1692
|
+
const stale = findStaleRules(rules, STALE_THRESHOLD_DAYS);
|
|
1693
|
+
const conflicts = findPatternConflicts(rules);
|
|
1694
|
+
const denials = recentDenials();
|
|
1695
|
+
const unreceipted = recentUnreceiptedDeletes();
|
|
1696
|
+
return {
|
|
1697
|
+
rules: summarizeRules(rules),
|
|
1698
|
+
stale_rules: stale,
|
|
1699
|
+
pattern_conflicts: conflicts,
|
|
1700
|
+
recent_denials: denials,
|
|
1701
|
+
recent_unreceipted_deletes: unreceipted,
|
|
1702
|
+
healthy: stale.length === 0 && conflicts.length === 0 && unreceipted.length === 0,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
function formatAuditPretty(r) {
|
|
1706
|
+
const lines = [];
|
|
1707
|
+
lines.push(c(ANSI.bold, "agent-memory audit"));
|
|
1708
|
+
lines.push("");
|
|
1709
|
+
lines.push(` Rules: ${c(ANSI.bold, String(r.rules.total))} ` +
|
|
1710
|
+
`(${c(ANSI.red, String(r.rules.hard) + " hard")} · ` +
|
|
1711
|
+
`${c(ANSI.yellow, String(r.rules.soft) + " soft")} · ` +
|
|
1712
|
+
`${r.rules.no_severity} unspecified)`);
|
|
1713
|
+
lines.push("");
|
|
1714
|
+
if (r.stale_rules.length > 0) {
|
|
1715
|
+
lines.push(c(ANSI.yellow, ` ${r.stale_rules.length} stale rule(s):`));
|
|
1716
|
+
for (const s of r.stale_rules) {
|
|
1717
|
+
const age = s.age_days === null
|
|
1718
|
+
? c(ANSI.dim, "(never verified)")
|
|
1719
|
+
: c(ANSI.dim, `(${s.age_days}d since last_verified)`);
|
|
1720
|
+
lines.push(` ${s.name} ${age}`);
|
|
1721
|
+
lines.push(` ${c(ANSI.dim, s.description)}`);
|
|
1722
|
+
}
|
|
1723
|
+
lines.push("");
|
|
1724
|
+
}
|
|
1725
|
+
else {
|
|
1726
|
+
lines.push(c(ANSI.green, " All rules verified within 90 days."));
|
|
1727
|
+
lines.push("");
|
|
1728
|
+
}
|
|
1729
|
+
if (r.pattern_conflicts.length > 0) {
|
|
1730
|
+
lines.push(c(ANSI.red, ` ${r.pattern_conflicts.length} pattern conflict(s):`));
|
|
1731
|
+
for (const conf of r.pattern_conflicts) {
|
|
1732
|
+
lines.push(` ${conf.rule_a} <-> ${conf.rule_b} ` +
|
|
1733
|
+
c(ANSI.dim, `(share pattern '${conf.shared_pattern}' on '${conf.shared_enforce_on}')`));
|
|
1734
|
+
}
|
|
1735
|
+
lines.push("");
|
|
1736
|
+
}
|
|
1737
|
+
else {
|
|
1738
|
+
lines.push(c(ANSI.green, " No pattern conflicts detected."));
|
|
1739
|
+
lines.push("");
|
|
1740
|
+
}
|
|
1741
|
+
if (r.recent_denials.length > 0) {
|
|
1742
|
+
lines.push(` Recent denials (${r.recent_denials.length}):`);
|
|
1743
|
+
for (const d of r.recent_denials.slice(-5)) {
|
|
1744
|
+
lines.push(` ${c(ANSI.dim, d.ts.slice(0, 19))} ${d.action_type} ` +
|
|
1745
|
+
c(ANSI.red, `(${d.hard_count} hard violation${d.hard_count === 1 ? "" : "s"})`));
|
|
1746
|
+
lines.push(` ${c(ANSI.dim, d.action)}`);
|
|
1747
|
+
}
|
|
1748
|
+
lines.push("");
|
|
1749
|
+
}
|
|
1750
|
+
if (r.recent_unreceipted_deletes.length > 0) {
|
|
1751
|
+
lines.push(c(ANSI.yellow, ` ${r.recent_unreceipted_deletes.length} unreceipted delete(s):`));
|
|
1752
|
+
for (const u of r.recent_unreceipted_deletes.slice(-5)) {
|
|
1753
|
+
lines.push(` ${c(ANSI.dim, u.ts.slice(0, 19))} ${u.name}`);
|
|
1754
|
+
}
|
|
1755
|
+
lines.push(c(ANSI.dim, ` (v0.11.x back-compat path; v0.12 will require receipts for delete_memory)`));
|
|
1756
|
+
lines.push("");
|
|
1757
|
+
}
|
|
1758
|
+
lines.push(r.healthy
|
|
1759
|
+
? c(ANSI.green, " HEALTHY · no action required")
|
|
1760
|
+
: c(ANSI.yellow, " ATTENTION · review the items above"));
|
|
1761
|
+
return lines.join("\n");
|
|
1762
|
+
}
|
|
1763
|
+
function toolAudit(args) {
|
|
1764
|
+
const json = args.json === true || args.format === "json";
|
|
1765
|
+
const report = runAudit();
|
|
1766
|
+
return json ? JSON.stringify(report, null, 2) : formatAuditPretty(report);
|
|
1767
|
+
}
|
|
1768
|
+
// -------------------------------------------------------------
|
|
1563
1769
|
// Git sync · multi-machine memory via git remote
|
|
1564
1770
|
// -------------------------------------------------------------
|
|
1565
1771
|
//
|
|
@@ -1881,7 +2087,7 @@ function actionColor(action) {
|
|
|
1881
2087
|
// -------------------------------------------------------------
|
|
1882
2088
|
// Server wiring
|
|
1883
2089
|
// -------------------------------------------------------------
|
|
1884
|
-
const server = new Server({ name: "agent-memory", version: "0.11.
|
|
2090
|
+
const server = new Server({ name: "agent-memory", version: "0.11.4" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1885
2091
|
// -------------------------------------------------------------
|
|
1886
2092
|
// Resource URI scheme
|
|
1887
2093
|
// -------------------------------------------------------------
|
|
@@ -2383,6 +2589,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2383
2589
|
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.",
|
|
2384
2590
|
inputSchema: { type: "object", properties: {} },
|
|
2385
2591
|
},
|
|
2592
|
+
{
|
|
2593
|
+
name: "audit",
|
|
2594
|
+
description: "v0.11.4 · operational health report for the rule store. Surfaces: rule count by severity, stale rules (last_verified > 90 days or null), pattern conflicts (two rules sharing an enforce_on category AND an identical regex pattern), recent check_action denials, and recent unreceipted destructive ops. " +
|
|
2595
|
+
"Default returns pretty-printed text; pass {format: 'json'} for structured output. " +
|
|
2596
|
+
"Run daily-ish · the report is fast and gives the operator a single view of what's drifting.",
|
|
2597
|
+
inputSchema: {
|
|
2598
|
+
type: "object",
|
|
2599
|
+
properties: {
|
|
2600
|
+
format: {
|
|
2601
|
+
type: "string",
|
|
2602
|
+
enum: ["pretty", "json"],
|
|
2603
|
+
description: "Output format. Default 'pretty' (human-readable colored text).",
|
|
2604
|
+
},
|
|
2605
|
+
},
|
|
2606
|
+
},
|
|
2607
|
+
},
|
|
2386
2608
|
{
|
|
2387
2609
|
name: "check_action",
|
|
2388
2610
|
description: "v0.11.3 · the protocol enforcement point. Pass a proposed action description + its category; the server matches against rule memories (type=rule) and either:\n" +
|
|
@@ -2501,6 +2723,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2501
2723
|
case "check_action":
|
|
2502
2724
|
result = toolCheckAction(args);
|
|
2503
2725
|
break;
|
|
2726
|
+
case "audit":
|
|
2727
|
+
result = toolAudit(args);
|
|
2728
|
+
break;
|
|
2504
2729
|
default:
|
|
2505
2730
|
throw new Error(`Unknown tool: ${name}`);
|
|
2506
2731
|
}
|
|
@@ -2541,6 +2766,7 @@ const CLI_COMMANDS = new Set([
|
|
|
2541
2766
|
"list-rules",
|
|
2542
2767
|
"emit-companions",
|
|
2543
2768
|
"check-action",
|
|
2769
|
+
"audit",
|
|
2544
2770
|
"ui",
|
|
2545
2771
|
"import-claude-code",
|
|
2546
2772
|
"help",
|
|
@@ -2799,6 +3025,10 @@ async function cliMain(command, rest) {
|
|
|
2799
3025
|
}) + "\n");
|
|
2800
3026
|
return 0;
|
|
2801
3027
|
}
|
|
3028
|
+
case "audit": {
|
|
3029
|
+
process.stdout.write(toolAudit({ format: flags.json ? "json" : "pretty" }) + "\n");
|
|
3030
|
+
return 0;
|
|
3031
|
+
}
|
|
2802
3032
|
case "ui": {
|
|
2803
3033
|
// Dynamic import so Ink + React only load when the TUI runs,
|
|
2804
3034
|
// 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.11.
|
|
3
|
+
"version": "0.11.5",
|
|
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",
|