@xultrax-web/agent-memory-mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/dist/index.js +275 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,6 +198,8 @@ Custom path:
|
|
|
198
198
|
| `stats` | Dashboard: counts per type, total size, largest memory, audit-log size, trash count. |
|
|
199
199
|
| `log_events` | Read recent entries from the audit event log. Optional `tail` (default 20) + `action` filter. |
|
|
200
200
|
| `verify_memory` | Re-evaluate a memory's claims. Extracts URLs/dates/file refs, flags stale-date signals, returns type-specific verification heuristics. Pairs with the `audit_stale` prompt. |
|
|
201
|
+
| `find_backlinks` | List memories that link to the given memory via `[[wiki-link]]` syntax in their bodies. Useful for "what references this" views. |
|
|
202
|
+
| `find_related` | Surface memories related to one by combining outbound links, inbound backlinks, shared tags, type match, and content similarity. Navigates the memory graph by association. |
|
|
201
203
|
|
|
202
204
|
### Prompts
|
|
203
205
|
|
|
@@ -219,6 +221,23 @@ Four built-in types, matching the Claude Code convention:
|
|
|
219
221
|
- **project** — current-state context that isn't in the code (deadlines, in-flight work)
|
|
220
222
|
- **reference** — pointers to external systems (Linear board URL, monitoring dashboard)
|
|
221
223
|
|
|
224
|
+
### Tags + wiki-links
|
|
225
|
+
|
|
226
|
+
Beyond types, two cross-cutting organization features:
|
|
227
|
+
|
|
228
|
+
**Tags** — optional `tags: [a, b, c]` array in frontmatter. Queryable via `list_memories({tags: [...]})` and the `agent-memory list --tags "a,b"` CLI. Filter is intersection — memories must have all listed tags. Tag names are lowercase a-z + digits + hyphen/underscore, max 40 chars.
|
|
229
|
+
|
|
230
|
+
```markdown
|
|
231
|
+
---
|
|
232
|
+
name: deploy-process
|
|
233
|
+
description: Blue-green prod deployment
|
|
234
|
+
type: project
|
|
235
|
+
tags: [deployment, production, critical]
|
|
236
|
+
---
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Wiki-links** — write `[[memory-name]]` anywhere in a memory body and it becomes a link. `find_backlinks` returns memories that reference a given one; `find_related` ranks the full graph (outbound links, inbound backlinks, shared tags, content similarity) for discovery navigation.
|
|
240
|
+
|
|
222
241
|
---
|
|
223
242
|
|
|
224
243
|
## CLI
|
|
@@ -242,6 +261,10 @@ agent-memory stats # dashboard: counts, sizes, audit
|
|
|
242
261
|
agent-memory log # last 20 entries from the audit log
|
|
243
262
|
agent-memory log --tail 50 --action delete # filter by action, tail size
|
|
244
263
|
agent-memory verify deploy-process # extract URLs/dates/file refs + staleness heuristics
|
|
264
|
+
agent-memory save my-mem --type project --description "X" --content "Body" --tags "production,critical"
|
|
265
|
+
agent-memory list --tags "production" # filter by tag (intersection)
|
|
266
|
+
agent-memory backlinks deploy-process # memories that link to deploy-process
|
|
267
|
+
agent-memory related deploy-process # ranked discovery: links + tags + similarity
|
|
245
268
|
```
|
|
246
269
|
|
|
247
270
|
### Audit log + structured logging
|
|
@@ -349,9 +372,16 @@ This server is built to be used daily, not to demo well once.
|
|
|
349
372
|
- **`verify_memory` tool** — static analysis of a memory's URLs/dates/file refs with type-specific staleness heuristics. Plus the matching `agent-memory verify <name>` CLI.
|
|
350
373
|
- **Conflict detection on save** — fuzzy-matches new memories against existing ones; warns on near-duplicates without blocking the save (so the LLM can decide whether to merge, rename, or proceed).
|
|
351
374
|
|
|
352
|
-
**
|
|
375
|
+
**Shipped in v0.8 · organization at scale:**
|
|
376
|
+
|
|
377
|
+
- **Tags** — optional `tags: [...]` array in frontmatter. Queryable via `list_memories` and `agent-memory list --tags "a,b"`. Intersection filter.
|
|
378
|
+
- **`[[wiki-links]]`** — write `[[memory-name]]` in any memory body, auto-detected.
|
|
379
|
+
- **`find_backlinks`** tool + `agent-memory backlinks <name>` CLI — "what links to this".
|
|
380
|
+
- **`find_related`** tool + `agent-memory related <name>` CLI — combines outbound + inbound links, shared tags, type match, and content similarity into a ranked discovery view.
|
|
381
|
+
|
|
382
|
+
**Landing in v0.9+:**
|
|
353
383
|
|
|
354
|
-
-
|
|
384
|
+
- Folder support (`.agent-memory/work/`, `.agent-memory/personal/`)
|
|
355
385
|
- TUI / web UI for browsing + editing memories in a clean interface
|
|
356
386
|
- `agent-memory sync` for git-backed multi-machine memory (the moat)
|
|
357
387
|
- Memory packs for shareable curated bundles
|
package/dist/index.js
CHANGED
|
@@ -171,6 +171,12 @@ const VALID_TYPES = new Set(["user", "feedback", "project", "reference"]);
|
|
|
171
171
|
// a letter or digit, 1-80 chars. Underscores are allowed because Claude
|
|
172
172
|
// Code's memory tree uses them; we want frictionless import.
|
|
173
173
|
const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,80}$/;
|
|
174
|
+
// Tags: lowercase, digits, hyphen/underscore. Max 40 chars per tag.
|
|
175
|
+
// Same alphabet as slugs but shorter — meant for "container-industry",
|
|
176
|
+
// "weekly", "deprecated", etc.
|
|
177
|
+
const TAG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,40}$/;
|
|
178
|
+
// Wiki-links: [[memory-name]] · names follow SLUG_PATTERN rules
|
|
179
|
+
const WIKI_LINK_PATTERN = /\[\[([a-z0-9][a-z0-9_-]{0,80})\]\]/g;
|
|
174
180
|
function memoryFilePath(name) {
|
|
175
181
|
return join(MEMORY_DIR, `${name}.md`);
|
|
176
182
|
}
|
|
@@ -185,6 +191,7 @@ function readMemory(name) {
|
|
|
185
191
|
name: fm.name ?? name,
|
|
186
192
|
description: fm.description ?? "",
|
|
187
193
|
type: fm.type ?? "project",
|
|
194
|
+
tags: Array.isArray(fm.tags) ? fm.tags.filter((t) => typeof t === "string") : [],
|
|
188
195
|
body: parsed.content.trim(),
|
|
189
196
|
filePath: fp,
|
|
190
197
|
};
|
|
@@ -196,6 +203,50 @@ function listMemoryFiles() {
|
|
|
196
203
|
.filter((f) => f.endsWith(".md") && f !== "MEMORY.md")
|
|
197
204
|
.map((f) => f.replace(/\.md$/, ""));
|
|
198
205
|
}
|
|
206
|
+
// Tags can arrive as a string array (MCP/CLI args) or a comma-separated
|
|
207
|
+
// string (CLI flag). Normalize to a deduped lowercase string[] preserving
|
|
208
|
+
// order of first appearance.
|
|
209
|
+
function normalizeTags(input) {
|
|
210
|
+
if (!input)
|
|
211
|
+
return [];
|
|
212
|
+
let raw;
|
|
213
|
+
if (Array.isArray(input)) {
|
|
214
|
+
raw = input.map((t) => String(t).trim()).filter((t) => t.length > 0);
|
|
215
|
+
}
|
|
216
|
+
else if (typeof input === "string") {
|
|
217
|
+
raw = input
|
|
218
|
+
.split(",")
|
|
219
|
+
.map((t) => t.trim())
|
|
220
|
+
.filter((t) => t.length > 0);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const t of raw) {
|
|
228
|
+
const lower = t.toLowerCase();
|
|
229
|
+
if (!seen.has(lower)) {
|
|
230
|
+
seen.add(lower);
|
|
231
|
+
out.push(lower);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
// Extract [[wiki-link]] targets from a memory body. Returns the set of
|
|
237
|
+
// referenced memory names (deduped, lowercase). Self-references stripped.
|
|
238
|
+
function extractWikiLinks(body, selfName) {
|
|
239
|
+
const found = new Set();
|
|
240
|
+
let m;
|
|
241
|
+
// RegExp with /g flag needs reset of lastIndex per call
|
|
242
|
+
WIKI_LINK_PATTERN.lastIndex = 0;
|
|
243
|
+
while ((m = WIKI_LINK_PATTERN.exec(body)) !== null) {
|
|
244
|
+
const target = m[1].toLowerCase();
|
|
245
|
+
if (target !== selfName)
|
|
246
|
+
found.add(target);
|
|
247
|
+
}
|
|
248
|
+
return Array.from(found);
|
|
249
|
+
}
|
|
199
250
|
// -------------------------------------------------------------
|
|
200
251
|
// Index management
|
|
201
252
|
// -------------------------------------------------------------
|
|
@@ -236,21 +287,29 @@ function toolSaveMemory(args) {
|
|
|
236
287
|
const description = String(args.description ?? "").trim();
|
|
237
288
|
const type = String(args.type ?? "project").trim();
|
|
238
289
|
const content = String(args.content ?? "").trim();
|
|
290
|
+
const tags = normalizeTags(args.tags);
|
|
239
291
|
if (!SLUG_PATTERN.test(name)) {
|
|
240
292
|
throw new Error(`Invalid name "${name}". Use lowercase (a-z, 0-9, hyphen, underscore), 1-80 chars, must start with letter or digit.`);
|
|
241
293
|
}
|
|
242
294
|
if (!VALID_TYPES.has(type)) {
|
|
243
295
|
throw new Error(`Invalid type "${type}". Must be one of: ${Array.from(VALID_TYPES).join(", ")}.`);
|
|
244
296
|
}
|
|
297
|
+
for (const tag of tags) {
|
|
298
|
+
if (!TAG_PATTERN.test(tag)) {
|
|
299
|
+
throw new Error(`Invalid tag "${tag}". Tags use lowercase (a-z, 0-9, hyphen, underscore), 1-40 chars.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
245
302
|
if (!description)
|
|
246
303
|
throw new Error("description is required");
|
|
247
304
|
if (!content)
|
|
248
305
|
throw new Error("content is required");
|
|
249
306
|
ensureStorage();
|
|
307
|
+
const tagsLine = tags.length > 0 ? `tags: [${tags.map((t) => JSON.stringify(t)).join(", ")}]\n` : "";
|
|
250
308
|
const frontmatter = `---\n` +
|
|
251
309
|
`name: ${name}\n` +
|
|
252
310
|
`description: ${JSON.stringify(description)}\n` +
|
|
253
311
|
`type: ${type}\n` +
|
|
312
|
+
tagsLine +
|
|
254
313
|
`schema: ${SCHEMA_VERSION}\n` +
|
|
255
314
|
`---\n\n`;
|
|
256
315
|
const fp = memoryFilePath(name);
|
|
@@ -344,16 +403,21 @@ function toolGetMemory(args) {
|
|
|
344
403
|
const mem = readMemory(name);
|
|
345
404
|
if (!mem)
|
|
346
405
|
return `Memory "${name}" not found.`;
|
|
406
|
+
const tagsLine = mem.tags.length > 0 ? `tags : ${mem.tags.join(", ")}` : "";
|
|
347
407
|
return [
|
|
348
408
|
`# ${mem.name}`,
|
|
349
|
-
`type: ${mem.type}`,
|
|
350
|
-
|
|
409
|
+
`type : ${mem.type}`,
|
|
410
|
+
tagsLine,
|
|
411
|
+
`description : ${mem.description}`,
|
|
351
412
|
"",
|
|
352
413
|
mem.body,
|
|
353
|
-
]
|
|
414
|
+
]
|
|
415
|
+
.filter((l) => l !== "")
|
|
416
|
+
.join("\n");
|
|
354
417
|
}
|
|
355
418
|
function toolListMemories(args) {
|
|
356
419
|
const typeFilter = args.type ? String(args.type) : null;
|
|
420
|
+
const tagFilter = normalizeTags(args.tags);
|
|
357
421
|
const offset = args.offset ? Math.max(0, Number(args.offset)) : 0;
|
|
358
422
|
const limit = args.limit ? Math.max(1, Number(args.limit)) : 50;
|
|
359
423
|
const names = listMemoryFiles();
|
|
@@ -361,21 +425,34 @@ function toolListMemories(args) {
|
|
|
361
425
|
.map((n) => readMemory(n))
|
|
362
426
|
.filter((m) => m !== null)
|
|
363
427
|
.filter((m) => !typeFilter || m.type === typeFilter)
|
|
428
|
+
.filter((m) => tagFilter.length === 0 || tagFilter.every((t) => m.tags.includes(t)))
|
|
364
429
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
365
430
|
if (all.length === 0) {
|
|
366
|
-
|
|
367
|
-
|
|
431
|
+
const parts = [];
|
|
432
|
+
if (typeFilter)
|
|
433
|
+
parts.push(`type "${typeFilter}"`);
|
|
434
|
+
if (tagFilter.length > 0)
|
|
435
|
+
parts.push(`tags [${tagFilter.join(", ")}]`);
|
|
436
|
+
return parts.length > 0
|
|
437
|
+
? `No memories matching ${parts.join(" and ")}.`
|
|
368
438
|
: "No memories yet. Use save_memory to create one.";
|
|
369
439
|
}
|
|
370
440
|
const page = all.slice(offset, offset + limit);
|
|
371
441
|
const lines = [];
|
|
442
|
+
const filterDesc = [
|
|
443
|
+
typeFilter ? `type=${typeFilter}` : null,
|
|
444
|
+
tagFilter.length > 0 ? `tags=[${tagFilter.join(",")}]` : null,
|
|
445
|
+
]
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.join(" ");
|
|
372
448
|
const showing = offset === 0 && page.length === all.length
|
|
373
|
-
? `Found ${all.length} memor${all.length === 1 ? "y" : "ies"}:`
|
|
374
|
-
: `Showing ${offset + 1}-${offset + page.length} of ${all.length}:`;
|
|
449
|
+
? `Found ${all.length} memor${all.length === 1 ? "y" : "ies"}${filterDesc ? ` (${filterDesc})` : ""}:`
|
|
450
|
+
: `Showing ${offset + 1}-${offset + page.length} of ${all.length}${filterDesc ? ` (${filterDesc})` : ""}:`;
|
|
375
451
|
lines.push(showing);
|
|
376
452
|
lines.push("");
|
|
377
453
|
for (const m of page) {
|
|
378
|
-
|
|
454
|
+
const tagSuffix = m.tags.length > 0 ? ` ${c(ANSI.dim, `· ${m.tags.join(" · ")}`)}` : "";
|
|
455
|
+
lines.push(` ${m.name} [${m.type}]${tagSuffix}`);
|
|
379
456
|
lines.push(` ${m.description}`);
|
|
380
457
|
}
|
|
381
458
|
if (offset + page.length < all.length) {
|
|
@@ -718,6 +795,124 @@ function toolVerifyMemory(args) {
|
|
|
718
795
|
return lines.join("\n");
|
|
719
796
|
}
|
|
720
797
|
// -------------------------------------------------------------
|
|
798
|
+
// Backlinks · which memories link to this one via [[wiki-link]]
|
|
799
|
+
// -------------------------------------------------------------
|
|
800
|
+
function toolFindBacklinks(args) {
|
|
801
|
+
const name = String(args.name ?? "").trim();
|
|
802
|
+
if (!SLUG_PATTERN.test(name))
|
|
803
|
+
throw new Error(`Invalid name "${name}".`);
|
|
804
|
+
const target = name.toLowerCase();
|
|
805
|
+
const all = listMemoryFiles()
|
|
806
|
+
.map((n) => readMemory(n))
|
|
807
|
+
.filter((m) => m !== null);
|
|
808
|
+
const backlinks = [];
|
|
809
|
+
for (const m of all) {
|
|
810
|
+
if (m.name === name)
|
|
811
|
+
continue;
|
|
812
|
+
const links = extractWikiLinks(m.body, m.name);
|
|
813
|
+
if (links.includes(target))
|
|
814
|
+
backlinks.push(m);
|
|
815
|
+
}
|
|
816
|
+
if (backlinks.length === 0)
|
|
817
|
+
return `No memories link to [[${name}]].`;
|
|
818
|
+
const lines = [];
|
|
819
|
+
lines.push(c(ANSI.bold, `Found ${backlinks.length} memor${backlinks.length === 1 ? "y" : "ies"} linking to [[${name}]]:`));
|
|
820
|
+
lines.push("");
|
|
821
|
+
for (const m of backlinks.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
822
|
+
lines.push(` ${m.name} [${m.type}]`);
|
|
823
|
+
lines.push(` ${m.description}`);
|
|
824
|
+
}
|
|
825
|
+
return lines.join("\n");
|
|
826
|
+
}
|
|
827
|
+
function toolFindRelated(args) {
|
|
828
|
+
const name = String(args.name ?? "").trim();
|
|
829
|
+
if (!SLUG_PATTERN.test(name))
|
|
830
|
+
throw new Error(`Invalid name "${name}".`);
|
|
831
|
+
const mem = readMemory(name);
|
|
832
|
+
if (!mem)
|
|
833
|
+
return `Memory "${name}" not found.`;
|
|
834
|
+
const max = args.max ? Math.max(1, Math.min(20, Number(args.max))) : 8;
|
|
835
|
+
const others = listMemoryFiles()
|
|
836
|
+
.filter((n) => n !== name)
|
|
837
|
+
.map((n) => readMemory(n))
|
|
838
|
+
.filter((m) => m !== null);
|
|
839
|
+
if (others.length === 0)
|
|
840
|
+
return `No other memories to compare against.`;
|
|
841
|
+
const outbound = new Set(extractWikiLinks(mem.body, mem.name));
|
|
842
|
+
const myTags = new Set(mem.tags);
|
|
843
|
+
// Fuzzy similarity against name + description only (body content is
|
|
844
|
+
// too noisy for relatedness; we already cover semantic overlap via
|
|
845
|
+
// search_memories).
|
|
846
|
+
const fuse = new Fuse(others, {
|
|
847
|
+
includeScore: true,
|
|
848
|
+
threshold: 0.7,
|
|
849
|
+
ignoreLocation: true,
|
|
850
|
+
minMatchCharLength: 3,
|
|
851
|
+
keys: [
|
|
852
|
+
{ name: "name", weight: 2 },
|
|
853
|
+
{ name: "description", weight: 1 },
|
|
854
|
+
],
|
|
855
|
+
});
|
|
856
|
+
const fuzzy = new Map();
|
|
857
|
+
const query = `${mem.name} ${mem.description}`;
|
|
858
|
+
for (const r of fuse.search(query, { limit: 20 })) {
|
|
859
|
+
fuzzy.set(r.item.name, 1 - (r.score ?? 1));
|
|
860
|
+
}
|
|
861
|
+
const scored = new Map();
|
|
862
|
+
for (const other of others) {
|
|
863
|
+
let score = 0;
|
|
864
|
+
const reasons = [];
|
|
865
|
+
if (outbound.has(other.name)) {
|
|
866
|
+
score += 5;
|
|
867
|
+
reasons.push("linked from this memory");
|
|
868
|
+
}
|
|
869
|
+
const otherOutbound = extractWikiLinks(other.body, other.name);
|
|
870
|
+
if (otherOutbound.includes(mem.name)) {
|
|
871
|
+
score += 5;
|
|
872
|
+
reasons.push("links to this memory");
|
|
873
|
+
}
|
|
874
|
+
const sharedTags = other.tags.filter((t) => myTags.has(t));
|
|
875
|
+
if (sharedTags.length > 0) {
|
|
876
|
+
score += sharedTags.length * 3;
|
|
877
|
+
reasons.push(`shared tags: ${sharedTags.join(", ")}`);
|
|
878
|
+
}
|
|
879
|
+
if (other.type === mem.type) {
|
|
880
|
+
score += 1;
|
|
881
|
+
}
|
|
882
|
+
const sim = fuzzy.get(other.name);
|
|
883
|
+
if (sim && sim > 0.3) {
|
|
884
|
+
score += Math.round(sim * 4);
|
|
885
|
+
reasons.push(`content similarity ${Math.round(sim * 100)}%`);
|
|
886
|
+
}
|
|
887
|
+
if (score > 0) {
|
|
888
|
+
scored.set(other.name, {
|
|
889
|
+
name: other.name,
|
|
890
|
+
type: other.type,
|
|
891
|
+
tags: other.tags,
|
|
892
|
+
description: other.description,
|
|
893
|
+
score,
|
|
894
|
+
reasons,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const ranked = Array.from(scored.values())
|
|
899
|
+
.sort((a, b) => b.score - a.score)
|
|
900
|
+
.slice(0, max);
|
|
901
|
+
if (ranked.length === 0)
|
|
902
|
+
return `No memories related to "${name}" found.`;
|
|
903
|
+
const lines = [];
|
|
904
|
+
lines.push(c(ANSI.bold, `Memories related to ${name}:`));
|
|
905
|
+
lines.push("");
|
|
906
|
+
for (const r of ranked) {
|
|
907
|
+
lines.push(` ${r.name} [${r.type}] ${c(ANSI.dim, `· score ${r.score}`)}`);
|
|
908
|
+
lines.push(` ${r.description}`);
|
|
909
|
+
for (const reason of r.reasons) {
|
|
910
|
+
lines.push(` ${c(ANSI.dim, `· ${reason}`)}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return lines.join("\n");
|
|
914
|
+
}
|
|
915
|
+
// -------------------------------------------------------------
|
|
721
916
|
// Stats · operator dashboard
|
|
722
917
|
// -------------------------------------------------------------
|
|
723
918
|
function toolStats(_args) {
|
|
@@ -827,7 +1022,7 @@ function actionColor(action) {
|
|
|
827
1022
|
// -------------------------------------------------------------
|
|
828
1023
|
// Server wiring
|
|
829
1024
|
// -------------------------------------------------------------
|
|
830
|
-
const server = new Server({ name: "agent-memory", version: "0.
|
|
1025
|
+
const server = new Server({ name: "agent-memory", version: "0.8.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
831
1026
|
// -------------------------------------------------------------
|
|
832
1027
|
// Resource URI scheme
|
|
833
1028
|
// -------------------------------------------------------------
|
|
@@ -1077,6 +1272,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1077
1272
|
type: "string",
|
|
1078
1273
|
description: "Markdown body. For feedback/project, include **Why:** and **How to apply:** lines.",
|
|
1079
1274
|
},
|
|
1275
|
+
tags: {
|
|
1276
|
+
type: "array",
|
|
1277
|
+
items: { type: "string" },
|
|
1278
|
+
description: "Optional tags for cross-cutting categorization. Lowercase, kebab/underscore, max 40 chars each. Queryable in list_memories + search_memories.",
|
|
1279
|
+
},
|
|
1080
1280
|
},
|
|
1081
1281
|
required: ["name", "description", "type", "content"],
|
|
1082
1282
|
},
|
|
@@ -1124,7 +1324,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1124
1324
|
},
|
|
1125
1325
|
{
|
|
1126
1326
|
name: "list_memories",
|
|
1127
|
-
description: "List stored memories, optionally filtered by type. Paginated.",
|
|
1327
|
+
description: "List stored memories, optionally filtered by type and/or tags. Paginated.",
|
|
1128
1328
|
inputSchema: {
|
|
1129
1329
|
type: "object",
|
|
1130
1330
|
properties: {
|
|
@@ -1133,6 +1333,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1133
1333
|
enum: ["user", "feedback", "project", "reference"],
|
|
1134
1334
|
description: "Optional filter — only list memories of this type",
|
|
1135
1335
|
},
|
|
1336
|
+
tags: {
|
|
1337
|
+
type: "array",
|
|
1338
|
+
items: { type: "string" },
|
|
1339
|
+
description: "Optional tag filter — memories must have ALL listed tags (intersection). Can also be passed as a comma-separated string.",
|
|
1340
|
+
},
|
|
1136
1341
|
offset: { type: "number", description: "Skip this many results (default 0)." },
|
|
1137
1342
|
limit: { type: "number", description: "Max results per page (default 50)." },
|
|
1138
1343
|
},
|
|
@@ -1203,6 +1408,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1203
1408
|
required: ["name"],
|
|
1204
1409
|
},
|
|
1205
1410
|
},
|
|
1411
|
+
{
|
|
1412
|
+
name: "find_backlinks",
|
|
1413
|
+
description: "List memories that link to the given memory via [[wiki-link]] syntax in their bodies. Useful for building a 'what references this' view.",
|
|
1414
|
+
inputSchema: {
|
|
1415
|
+
type: "object",
|
|
1416
|
+
properties: {
|
|
1417
|
+
name: { type: "string", description: "The memory's name slug" },
|
|
1418
|
+
},
|
|
1419
|
+
required: ["name"],
|
|
1420
|
+
},
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
name: "find_related",
|
|
1424
|
+
description: "Surface memories related to a given one. Ranks by combining: outbound [[wiki-links]], inbound backlinks, shared tags, same type, and content similarity (name + description). Use this to navigate the memory graph by association rather than by exact lookup.",
|
|
1425
|
+
inputSchema: {
|
|
1426
|
+
type: "object",
|
|
1427
|
+
properties: {
|
|
1428
|
+
name: { type: "string", description: "The starting memory's name slug" },
|
|
1429
|
+
max: {
|
|
1430
|
+
type: "number",
|
|
1431
|
+
description: "Max related memories to return (default 8, capped at 20).",
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
required: ["name"],
|
|
1435
|
+
},
|
|
1436
|
+
},
|
|
1206
1437
|
],
|
|
1207
1438
|
}));
|
|
1208
1439
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1243,6 +1474,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1243
1474
|
case "verify_memory":
|
|
1244
1475
|
result = toolVerifyMemory(args);
|
|
1245
1476
|
break;
|
|
1477
|
+
case "find_backlinks":
|
|
1478
|
+
result = toolFindBacklinks(args);
|
|
1479
|
+
break;
|
|
1480
|
+
case "find_related":
|
|
1481
|
+
result = toolFindRelated(args);
|
|
1482
|
+
break;
|
|
1246
1483
|
default:
|
|
1247
1484
|
throw new Error(`Unknown tool: ${name}`);
|
|
1248
1485
|
}
|
|
@@ -1276,6 +1513,8 @@ const CLI_COMMANDS = new Set([
|
|
|
1276
1513
|
"stats",
|
|
1277
1514
|
"log",
|
|
1278
1515
|
"verify",
|
|
1516
|
+
"backlinks",
|
|
1517
|
+
"related",
|
|
1279
1518
|
"import-claude-code",
|
|
1280
1519
|
"help",
|
|
1281
1520
|
"--help",
|
|
@@ -1344,6 +1583,7 @@ async function cliMain(command, rest) {
|
|
|
1344
1583
|
type: String(flags.type ?? "project"),
|
|
1345
1584
|
description: String(flags.description ?? ""),
|
|
1346
1585
|
content,
|
|
1586
|
+
tags: flags.tags,
|
|
1347
1587
|
});
|
|
1348
1588
|
process.stdout.write(result + "\n");
|
|
1349
1589
|
return 0;
|
|
@@ -1378,6 +1618,7 @@ async function cliMain(command, rest) {
|
|
|
1378
1618
|
case "list": {
|
|
1379
1619
|
process.stdout.write(toolListMemories({
|
|
1380
1620
|
type: flags.type,
|
|
1621
|
+
tags: flags.tags,
|
|
1381
1622
|
offset: flags.offset ? Number(flags.offset) : undefined,
|
|
1382
1623
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
1383
1624
|
}) + "\n");
|
|
@@ -1412,6 +1653,23 @@ async function cliMain(command, rest) {
|
|
|
1412
1653
|
process.stdout.write(toolVerifyMemory({ name }) + "\n");
|
|
1413
1654
|
return 0;
|
|
1414
1655
|
}
|
|
1656
|
+
case "backlinks": {
|
|
1657
|
+
const name = positional[0];
|
|
1658
|
+
if (!name)
|
|
1659
|
+
throw new Error("Usage: agent-memory backlinks <name>");
|
|
1660
|
+
process.stdout.write(toolFindBacklinks({ name }) + "\n");
|
|
1661
|
+
return 0;
|
|
1662
|
+
}
|
|
1663
|
+
case "related": {
|
|
1664
|
+
const name = positional[0];
|
|
1665
|
+
if (!name)
|
|
1666
|
+
throw new Error("Usage: agent-memory related <name> [--max N]");
|
|
1667
|
+
process.stdout.write(toolFindRelated({
|
|
1668
|
+
name,
|
|
1669
|
+
max: flags.max ? Number(flags.max) : undefined,
|
|
1670
|
+
}) + "\n");
|
|
1671
|
+
return 0;
|
|
1672
|
+
}
|
|
1415
1673
|
case "log": {
|
|
1416
1674
|
process.stdout.write(toolLogEvents({
|
|
1417
1675
|
tail: flags.tail ? Number(flags.tail) : undefined,
|
|
@@ -1445,15 +1703,17 @@ USAGE
|
|
|
1445
1703
|
agent-memory-mcp MCP server mode (default when no args)
|
|
1446
1704
|
|
|
1447
1705
|
COMMANDS
|
|
1448
|
-
save <name> --type <t> --description <d> --content <c>
|
|
1706
|
+
save <name> --type <t> --description <d> --content <c> [--tags "a,b,c"]
|
|
1449
1707
|
Save or update a memory.
|
|
1450
1708
|
Type: user | feedback | project | reference
|
|
1451
1709
|
Content sources: --content "..." | --content-file <path> | --stdin
|
|
1710
|
+
Tags: comma-separated, lowercase, max 40 chars each.
|
|
1452
1711
|
search <query> [--limit N] Fuzzy search (typo-tolerant), top N (default 10)
|
|
1453
1712
|
relevant <query> [--max N] Top N matches as full markdown for LLM ingestion
|
|
1454
1713
|
get <name> Print one memory's full contents
|
|
1455
|
-
list [--type <t>] [--offset N] [--limit N]
|
|
1714
|
+
list [--type <t>] [--tags "a,b"] [--offset N] [--limit N]
|
|
1456
1715
|
List memories (paginated, default limit 50)
|
|
1716
|
+
Tag filter requires ALL listed tags (intersection).
|
|
1457
1717
|
delete <name> Soft-delete: move to .trash/, removable later
|
|
1458
1718
|
restore <name> Restore the most recent trash entry for <name>
|
|
1459
1719
|
doctor [--rebuild-index] Check storage integrity (orphans, dangling
|
|
@@ -1465,6 +1725,9 @@ COMMANDS
|
|
|
1465
1725
|
verify <name> Re-evaluate a memory's claims (URLs, dates, file refs,
|
|
1466
1726
|
type-specific staleness heuristics). Static analysis;
|
|
1467
1727
|
no network calls.
|
|
1728
|
+
backlinks <name> List memories that link to <name> via [[wiki-links]].
|
|
1729
|
+
related <name> [--max N] Surface related memories via outbound + inbound links,
|
|
1730
|
+
shared tags, type match, content similarity.
|
|
1468
1731
|
import-claude-code [--source <path>] [--project <pat>] [--overwrite] [--dry-run]
|
|
1469
1732
|
Walk ~/.claude/projects/*/memory/ and
|
|
1470
1733
|
import each memory into the current store.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xultrax-web/agent-memory-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"mcpName": "io.github.xultrax-web/agent-memory-mcp",
|
|
5
5
|
"description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. The only MCP memory server that isn't a database.",
|
|
6
6
|
"type": "module",
|