hmem-mcp 6.6.0 → 6.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,6 +101,12 @@ export interface MemoryEntry {
101
101
  }[];
102
102
  /** True if the entry is pinned (super-favorite). Pinned entries show full L2 content in bulk reads. */
103
103
  pinned?: boolean;
104
+ /** FTS search: sub-nodes of this entry that matched the query. Empty/absent for root-only or tag-only matches. */
105
+ matchedNodes?: {
106
+ id: string;
107
+ title: string;
108
+ preview: string;
109
+ }[];
104
110
  }
105
111
  export interface MemoryNode {
106
112
  id: string;
@@ -636,7 +636,19 @@ export class HmemStore {
636
636
  return [];
637
637
  // FTS5 phrase match — all words must appear in the text
638
638
  const ftsMatch = `"${searchTerm}"`;
639
- const ftsRootIds = new Set(this.db.prepare("SELECT DISTINCT root_id FROM hmem_fts_rowid_map WHERE fts_rowid IN (SELECT rowid FROM hmem_fts WHERE hmem_fts MATCH ?)").all(ftsMatch).map(r => r.root_id));
639
+ const ftsRows = this.db.prepare("SELECT rm.root_id, rm.node_id FROM hmem_fts_rowid_map rm " +
640
+ "JOIN hmem_fts fts ON fts.rowid = rm.fts_rowid " +
641
+ "WHERE hmem_fts MATCH ?").all(ftsMatch);
642
+ const ftsRootIds = new Set();
643
+ const matchedNodesByRoot = new Map();
644
+ for (const r of ftsRows) {
645
+ ftsRootIds.add(r.root_id);
646
+ if (r.node_id) {
647
+ const arr = matchedNodesByRoot.get(r.root_id) ?? [];
648
+ arr.push(r.node_id);
649
+ matchedNodesByRoot.set(r.root_id, arr);
650
+ }
651
+ }
640
652
  // Also search tags (e.g. search="#hmem" matches tag "#hmem")
641
653
  const tagPattern = `%${opts.search}%`;
642
654
  const tagRows = this.db.prepare("SELECT entry_id FROM memory_tags WHERE tag LIKE ?").all(tagPattern);
@@ -654,9 +666,38 @@ export class HmemStore {
654
666
  if (limit !== undefined)
655
667
  ftsParams.push(limit);
656
668
  const rows = this.db.prepare(`SELECT * FROM memories ${where} ORDER BY created_at DESC${limitClause}`).all(...ftsParams);
669
+ // Batch-fetch matched sub-nodes across all roots (skip irrelevant)
670
+ const allMatchedNodeIds = [...new Set([...matchedNodesByRoot.values()].flat())];
671
+ const nodeInfo = new Map();
672
+ if (allMatchedNodeIds.length > 0) {
673
+ const nodePlaceholders = allMatchedNodeIds.map(() => "?").join(", ");
674
+ const nodeRows = this.db.prepare(`SELECT id, root_id, title, content FROM memory_nodes ` +
675
+ `WHERE id IN (${nodePlaceholders}) AND (irrelevant IS NULL OR irrelevant = 0)`).all(...allMatchedNodeIds);
676
+ for (const n of nodeRows) {
677
+ nodeInfo.set(n.id, { root_id: n.root_id, title: n.title ?? "", content: n.content ?? "" });
678
+ }
679
+ }
657
680
  for (const row of rows)
658
681
  this.bumpAccess(row.id);
659
- return rows.map(r => this.rowToEntry(r));
682
+ return rows.map(r => {
683
+ const entry = this.rowToEntry(r);
684
+ const matchedIds = matchedNodesByRoot.get(r.id);
685
+ if (matchedIds && matchedIds.length > 0) {
686
+ const matched = matchedIds
687
+ .map(nid => {
688
+ const info = nodeInfo.get(nid);
689
+ if (!info)
690
+ return null; // filtered (irrelevant) or missing
691
+ const text = (info.title || info.content).replace(/\s+/g, " ").trim();
692
+ const preview = text.length > 80 ? text.substring(0, 80) + "…" : text;
693
+ return { id: nid, title: info.title || info.content.substring(0, 50), preview };
694
+ })
695
+ .filter((x) => x !== null);
696
+ if (matched.length > 0)
697
+ entry.matchedNodes = matched;
698
+ }
699
+ return entry;
700
+ });
660
701
  }
661
702
  // Build filtered bulk query (exclude headers: seq > 0)
662
703
  const conditions = ["seq > 0"];