@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.
Files changed (3) hide show
  1. package/README.md +32 -2
  2. package/dist/index.js +275 -12
  3. 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
- **Landing in v0.8 - v0.9:**
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
- - Tags + wiki-links + folder support + `find_related` (organization-at-scale)
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
- `description: ${mem.description}`,
409
+ `type : ${mem.type}`,
410
+ tagsLine,
411
+ `description : ${mem.description}`,
351
412
  "",
352
413
  mem.body,
353
- ].join("\n");
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
- return typeFilter
367
- ? `No memories of type "${typeFilter}".`
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
- lines.push(` ${m.name} [${m.type}]`);
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.7.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
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.7.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",