superlocalmemory 3.0.16 → 3.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.0.16",
3
+ "version": "3.0.17",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.0.16"
3
+ version = "3.0.17"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -23,6 +23,7 @@ def dispatch(args: Namespace) -> None:
23
23
  "provider": cmd_provider,
24
24
  "connect": cmd_connect,
25
25
  "migrate": cmd_migrate,
26
+ "list": cmd_list,
26
27
  "remember": cmd_remember,
27
28
  "recall": cmd_recall,
28
29
  "forget": cmd_forget,
@@ -113,6 +114,34 @@ def cmd_migrate(args: Namespace) -> None:
113
114
  _migrate(args)
114
115
 
115
116
 
117
+ def cmd_list(args: Namespace) -> None:
118
+ """List recent memories chronologically."""
119
+ from superlocalmemory.core.config import SLMConfig
120
+ from superlocalmemory.core.engine import MemoryEngine
121
+
122
+ config = SLMConfig.load()
123
+ engine = MemoryEngine(config)
124
+ engine.initialize()
125
+
126
+ limit = getattr(args, "limit", 20)
127
+ facts = engine._db.get_all_facts(engine.profile_id)
128
+ # Sort by created_at descending, take limit
129
+ facts.sort(key=lambda f: f.created_at or "", reverse=True)
130
+ facts = facts[:limit]
131
+
132
+ if not facts:
133
+ print("No memories stored yet.")
134
+ return
135
+
136
+ print(f"Recent memories ({len(facts)}):\n")
137
+ for i, f in enumerate(facts, 1):
138
+ date = (f.created_at or "")[:19]
139
+ ftype_raw = getattr(f, "fact_type", "")
140
+ ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
141
+ content = f.content[:100] + ("..." if len(f.content) > 100 else "")
142
+ print(f" {i:3d}. [{date}] ({ftype}) {content}")
143
+
144
+
116
145
  def cmd_remember(args: Namespace) -> None:
117
146
  """Store a memory via the engine."""
118
147
  from superlocalmemory.core.config import SLMConfig
@@ -14,73 +14,127 @@ from __future__ import annotations
14
14
  import argparse
15
15
  import sys
16
16
 
17
+ _HELP_EPILOG = """\
18
+ operating modes:
19
+ Mode A Local Guardian — Zero cloud, zero LLM. All processing stays on
20
+ your machine. Full EU AI Act compliance. Best for privacy-first
21
+ use, air-gapped systems, and regulated environments.
22
+ Retrieval score: 74.8% on LoCoMo benchmark.
23
+
24
+ Mode B Smart Local — Uses a local Ollama LLM for summarization and
25
+ enrichment. Data never leaves your network. EU AI Act compliant.
26
+ Requires: ollama running locally with a model pulled.
27
+
28
+ Mode C Full Power — Uses a cloud LLM (OpenAI, Anthropic, etc.) for
29
+ maximum accuracy. Best retrieval quality, agentic multi-hop.
30
+ Retrieval score: 87.7% on LoCoMo benchmark.
31
+
32
+ quick start:
33
+ slm setup Interactive first-time setup
34
+ slm remember "some fact" Store a memory
35
+ slm recall "search query" Semantic search across memories
36
+ slm list -n 20 Show 20 most recent memories
37
+ slm dashboard Open web dashboard at localhost:8765
38
+
39
+ ide integration:
40
+ slm mcp Start MCP server (used by IDEs)
41
+ slm connect Auto-configure all detected IDEs
42
+ slm connect cursor Configure a specific IDE
43
+
44
+ examples:
45
+ slm remember "Project X uses PostgreSQL 16" --tags "project-x,db"
46
+ slm recall "which database does project X use"
47
+ slm list -n 50
48
+ slm mode a Switch to zero-LLM mode
49
+ slm trace "auth flow" Recall with per-channel score breakdown
50
+ slm health Check math layer status
51
+ slm dashboard --port 9000 Dashboard on custom port
52
+
53
+ documentation:
54
+ Website: https://superlocalmemory.com
55
+ GitHub: https://github.com/qualixar/superlocalmemory
56
+ Paper: https://arxiv.org/abs/2603.14588
57
+ """
58
+
17
59
 
18
60
  def main() -> None:
19
61
  """Parse CLI arguments and dispatch to command handlers."""
20
- parser = argparse.ArgumentParser(prog="slm", description="SuperLocalMemory V3")
21
- sub = parser.add_subparsers(dest="command")
62
+ try:
63
+ from importlib.metadata import version as _pkg_version
64
+ _ver = _pkg_version("superlocalmemory")
65
+ except Exception:
66
+ _ver = "unknown"
67
+
68
+ parser = argparse.ArgumentParser(
69
+ prog="slm",
70
+ description=f"SuperLocalMemory V3 ({_ver}) — AI agent memory with mathematical foundations",
71
+ epilog=_HELP_EPILOG,
72
+ formatter_class=argparse.RawDescriptionHelpFormatter,
73
+ )
74
+ parser.add_argument(
75
+ "-v", "--version", action="version", version=f"superlocalmemory {_ver}",
76
+ )
77
+ sub = parser.add_subparsers(dest="command", title="commands")
22
78
 
23
- # Setup
24
- sub.add_parser("setup", help="Run setup wizard")
79
+ # -- Setup & Config ------------------------------------------------
80
+ sub.add_parser("setup", help="Interactive first-time setup wizard")
25
81
 
26
- # Mode
27
- mode_p = sub.add_parser("mode", help="Get or set operating mode")
82
+ mode_p = sub.add_parser("mode", help="Get or set operating mode (a/b/c)")
28
83
  mode_p.add_argument(
29
84
  "value", nargs="?", choices=["a", "b", "c"], help="Mode to set",
30
85
  )
31
86
 
32
- # Provider
33
- provider_p = sub.add_parser("provider", help="Get or set LLM provider")
87
+ provider_p = sub.add_parser("provider", help="Get or set LLM provider for Mode B/C")
34
88
  provider_p.add_argument(
35
89
  "action", nargs="?", choices=["set"], help="Action",
36
90
  )
37
91
 
38
- # Connect
39
- connect_p = sub.add_parser("connect", help="Configure IDE integrations")
92
+ connect_p = sub.add_parser("connect", help="Auto-configure IDE integrations (17+ IDEs)")
40
93
  connect_p.add_argument("ide", nargs="?", help="Specific IDE to configure")
41
94
  connect_p.add_argument(
42
95
  "--list", action="store_true", help="List all supported IDEs",
43
96
  )
44
97
 
45
- # Migrate
46
- migrate_p = sub.add_parser("migrate", help="Migrate from V2")
98
+ migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
47
99
  migrate_p.add_argument(
48
100
  "--rollback", action="store_true", help="Rollback migration",
49
101
  )
50
102
 
51
- # Memory ops
52
- remember_p = sub.add_parser("remember", help="Store a memory")
103
+ # -- Memory Operations ---------------------------------------------
104
+ remember_p = sub.add_parser("remember", help="Store a memory (extracts facts, builds graph)")
53
105
  remember_p.add_argument("content", help="Content to remember")
54
106
  remember_p.add_argument("--tags", default="", help="Comma-separated tags")
55
107
 
56
- recall_p = sub.add_parser("recall", help="Search memories")
108
+ recall_p = sub.add_parser("recall", help="Semantic search with 4-channel retrieval")
57
109
  recall_p.add_argument("query", help="Search query")
58
- recall_p.add_argument("--limit", type=int, default=10, help="Max results")
110
+ recall_p.add_argument("--limit", type=int, default=10, help="Max results (default 10)")
59
111
 
60
- forget_p = sub.add_parser("forget", help="Delete memories matching query")
61
- forget_p.add_argument("query", help="Query to match")
112
+ forget_p = sub.add_parser("forget", help="Delete memories matching a query")
113
+ forget_p.add_argument("query", help="Query to match for deletion")
62
114
 
63
- # Status & diagnostics
64
- sub.add_parser("status", help="System status")
65
- sub.add_parser("health", help="Math layer health")
115
+ list_p = sub.add_parser("list", help="List recent memories chronologically")
116
+ list_p.add_argument(
117
+ "--limit", "-n", type=int, default=20, help="Number of entries (default 20)",
118
+ )
119
+
120
+ # -- Diagnostics ---------------------------------------------------
121
+ sub.add_parser("status", help="System status (mode, profile, DB size)")
122
+ sub.add_parser("health", help="Math layer health (Fisher-Rao, Sheaf, Langevin)")
66
123
 
67
- trace_p = sub.add_parser("trace", help="Recall with channel breakdown")
124
+ trace_p = sub.add_parser("trace", help="Recall with per-channel score breakdown")
68
125
  trace_p.add_argument("query", help="Search query")
69
126
 
70
- # MCP server (for IDE integration)
127
+ # -- Services ------------------------------------------------------
71
128
  sub.add_parser("mcp", help="Start MCP server (stdio transport for IDE integration)")
129
+ sub.add_parser("warmup", help="Pre-download embedding model (~500MB, one-time)")
72
130
 
73
- # Warmup (pre-download model)
74
- sub.add_parser("warmup", help="Pre-download embedding model (~500MB)")
75
-
76
- # Dashboard
77
- dashboard_p = sub.add_parser("dashboard", help="Open web dashboard")
131
+ dashboard_p = sub.add_parser("dashboard", help="Open 17-tab web dashboard")
78
132
  dashboard_p.add_argument(
79
133
  "--port", type=int, default=8765, help="Port (default 8765)",
80
134
  )
81
135
 
82
- # Profiles
83
- profile_p = sub.add_parser("profile", help="Profile management")
136
+ # -- Profiles ------------------------------------------------------
137
+ profile_p = sub.add_parser("profile", help="Profile management (list/switch/create)")
84
138
  profile_p.add_argument(
85
139
  "action", choices=["list", "switch", "create"], help="Action",
86
140
  )
@@ -20,6 +20,27 @@ logger = logging.getLogger(__name__)
20
20
  def register_v3_tools(server, get_engine: Callable) -> None:
21
21
  """Register 5 V3-exclusive tools on *server*."""
22
22
 
23
+ # ------------------------------------------------------------------
24
+ # 0. get_version (so IDEs can check compatibility)
25
+ # ------------------------------------------------------------------
26
+ @server.tool()
27
+ async def get_version() -> dict:
28
+ """Get SuperLocalMemory version, Python version, and platform info."""
29
+ try:
30
+ from importlib.metadata import version as _pkg_version
31
+ slm_ver = _pkg_version("superlocalmemory")
32
+ except Exception:
33
+ slm_ver = "unknown"
34
+ import platform
35
+ import sys as _sys
36
+ return {
37
+ "success": True,
38
+ "version": slm_ver,
39
+ "python": _sys.version.split()[0],
40
+ "platform": platform.system(),
41
+ "arch": platform.machine(),
42
+ }
43
+
23
44
  # ------------------------------------------------------------------
24
45
  # 1. set_mode
25
46
  # ------------------------------------------------------------------
@@ -46,19 +46,61 @@ def _fetch_graph_data(
46
46
  ) -> tuple[list, list, list]:
47
47
  """Fetch graph nodes, links, clusters from V3 or V2 schema."""
48
48
  if use_v3:
49
+ # Graph-first: fetch edges, then get connected nodes, then fill slots
49
50
  cursor.execute("""
50
- SELECT f.fact_id as id, f.content, f.fact_type as category,
51
- f.confidence as importance, f.session_id as project_name,
52
- f.created_at
53
- FROM atomic_facts f WHERE f.profile_id = ? AND f.confidence >= ?
54
- ORDER BY f.confidence DESC, f.created_at DESC LIMIT ?
55
- """, (profile, min_importance / 10.0, max_nodes))
56
- nodes = cursor.fetchall()
51
+ SELECT source_id as source, target_id as target,
52
+ weight, edge_type as relationship_type
53
+ FROM graph_edges WHERE profile_id = ?
54
+ ORDER BY weight DESC
55
+ """, (profile,))
56
+ all_links = cursor.fetchall()
57
+
58
+ connected_ids = set()
59
+ for lk in all_links:
60
+ connected_ids.add(lk['source'])
61
+ connected_ids.add(lk['target'])
62
+
63
+ # Fetch connected nodes first (these have edges to display)
64
+ connected_nodes: list = []
65
+ if connected_ids:
66
+ ph = ','.join('?' * len(connected_ids))
67
+ cursor.execute(f"""
68
+ SELECT fact_id as id, content, fact_type as category,
69
+ confidence as importance, session_id as project_name,
70
+ created_at
71
+ FROM atomic_facts
72
+ WHERE profile_id = ? AND fact_id IN ({ph})
73
+ """, [profile] + list(connected_ids))
74
+ connected_nodes = cursor.fetchall()
75
+
76
+ # Fill remaining slots with top-confidence unconnected nodes
77
+ remaining = max_nodes - len(connected_nodes)
78
+ if remaining > 0:
79
+ existing = {n['id'] for n in connected_nodes}
80
+ cursor.execute("""
81
+ SELECT fact_id as id, content, fact_type as category,
82
+ confidence as importance, session_id as project_name,
83
+ created_at
84
+ FROM atomic_facts
85
+ WHERE profile_id = ? AND confidence >= ?
86
+ ORDER BY confidence DESC, created_at DESC
87
+ LIMIT ?
88
+ """, (profile, min_importance / 10.0, remaining + len(existing)))
89
+ for n in cursor.fetchall():
90
+ if n['id'] not in existing:
91
+ connected_nodes.append(n)
92
+ if len(connected_nodes) >= max_nodes:
93
+ break
94
+
95
+ nodes = connected_nodes[:max_nodes]
57
96
  for n in nodes:
58
97
  n['entities'] = []
59
98
  n['content_preview'] = _preview(n.get('content'))
60
- ids = [n['id'] for n in nodes]
61
- links = _fetch_edges_v3(cursor, profile, ids)
99
+
100
+ # Filter edges to only those between displayed nodes
101
+ node_ids = {n['id'] for n in nodes}
102
+ links = [lk for lk in all_links
103
+ if lk['source'] in node_ids and lk['target'] in node_ids]
62
104
  return nodes, links, []
63
105
 
64
106
  # V2 fallback
@@ -72,6 +72,17 @@ async def get_stats():
72
72
  total_clusters = cursor.fetchone()['total']
73
73
  except Exception:
74
74
  pass
75
+ # Fallback: V2-migrated clusters stored as cluster_id on memories
76
+ if total_clusters == 0:
77
+ try:
78
+ cursor.execute(
79
+ "SELECT COUNT(DISTINCT cluster_id) as total FROM memories "
80
+ "WHERE cluster_id IS NOT NULL AND profile = ?",
81
+ (active_profile,),
82
+ )
83
+ total_clusters = cursor.fetchone()['total']
84
+ except Exception:
85
+ pass
75
86
 
76
87
  # Fact type breakdown (replaces category in V3)
77
88
  cursor.execute("""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: superlocalmemory
3
- Version: 3.0.16
3
+ Version: 3.0.17
4
4
  Summary: Information-geometric agent memory with mathematical guarantees
5
5
  Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
6
6
  License: MIT
package/ui/index.html CHANGED
@@ -740,7 +740,7 @@
740
740
  </li>
741
741
  <li class="nav-item">
742
742
  <button class="nav-link" id="learning-tab" data-bs-toggle="tab" data-bs-target="#learning-pane">
743
- <i class="bi bi-mortarboard"></i> Learning <span class="badge bg-warning text-dark" style="font-size:0.6rem;vertical-align:top;">v2.7</span>
743
+ <i class="bi bi-mortarboard"></i> Learning
744
744
  </button>
745
745
  </li>
746
746
  <li class="nav-item">
@@ -760,17 +760,17 @@
760
760
  </li>
761
761
  <li class="nav-item" role="presentation">
762
762
  <button class="nav-link" id="lifecycle-tab" data-bs-toggle="tab" data-bs-target="#lifecycle-pane" type="button" role="tab">
763
- <i class="bi bi-hourglass-split"></i> Lifecycle <span class="badge bg-success ms-1">v2.8</span>
763
+ <i class="bi bi-hourglass-split"></i> Lifecycle
764
764
  </button>
765
765
  </li>
766
766
  <li class="nav-item" role="presentation">
767
767
  <button class="nav-link" id="behavioral-tab" data-bs-toggle="tab" data-bs-target="#behavioral-pane" type="button" role="tab">
768
- <i class="bi bi-lightbulb"></i> Behavioral <span class="badge bg-success ms-1">v2.8</span>
768
+ <i class="bi bi-lightbulb"></i> Behavioral
769
769
  </button>
770
770
  </li>
771
771
  <li class="nav-item" role="presentation">
772
772
  <button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance-pane" type="button" role="tab">
773
- <i class="bi bi-shield-lock"></i> Compliance <span class="badge bg-success ms-1">v2.8</span>
773
+ <i class="bi bi-shield-lock"></i> Compliance
774
774
  </button>
775
775
  </li>
776
776
  <li class="nav-item">
@@ -1023,8 +1023,13 @@
1023
1023
  <p class="text-muted">See how each retrieval channel contributes to results.</p>
1024
1024
  <div class="input-group mb-3">
1025
1025
  <input type="text" id="recall-lab-query" class="form-control form-control-lg" placeholder="Enter your query...">
1026
+ <select id="recall-lab-per-page" class="form-select" style="max-width:100px;">
1027
+ <option value="10" selected>10</option>
1028
+ <option value="25">25</option>
1029
+ <option value="50">50</option>
1030
+ </select>
1026
1031
  <button class="btn btn-primary btn-lg" id="recall-lab-search">
1027
- <i class="bi bi-search"></i> Search with Trace
1032
+ <i class="bi bi-search"></i> Search
1028
1033
  </button>
1029
1034
  </div>
1030
1035
  <div id="recall-lab-meta" class="mb-3 text-muted small"></div>
@@ -1070,8 +1075,19 @@
1070
1075
  </div>
1071
1076
  </div>
1072
1077
 
1073
- <!-- Learning System (v2.7) -->
1078
+ <!-- Learning System -->
1074
1079
  <div class="tab-pane fade" id="learning-pane">
1080
+ <!-- Getting Started Guide -->
1081
+ <div class="alert alert-light border mb-3" id="learning-getting-started">
1082
+ <h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> How Learning Works</h6>
1083
+ <p class="mb-1 small">SuperLocalMemory learns from your usage patterns to improve retrieval ranking over time. This happens automatically as you use <code>slm recall</code> or search via MCP.</p>
1084
+ <ul class="small mb-0">
1085
+ <li><strong>0-20 signals:</strong> Baseline phase (collecting data)</li>
1086
+ <li><strong>20+ signals:</strong> Rule-based ranking adjustments</li>
1087
+ <li><strong>200+ signals:</strong> ML model trained on your patterns</li>
1088
+ </ul>
1089
+ <p class="mb-0 mt-1 small text-muted">Each recall query generates a signal. Keep using SLM and this tab will populate automatically.</p>
1090
+ </div>
1075
1091
  <!-- Ranking Phase & Engagement -->
1076
1092
  <div class="row g-3 mb-3">
1077
1093
  <div class="col-md-4">
@@ -1424,12 +1440,18 @@
1424
1440
  </div>
1425
1441
  </div>
1426
1442
 
1427
- <!-- Behavioral Learning (v2.8) -->
1443
+ <!-- Behavioral Learning -->
1428
1444
  <div class="tab-pane fade" id="behavioral-pane" role="tabpanel">
1429
1445
  <div class="d-flex justify-content-between align-items-center mb-3">
1430
1446
  <h5 class="mb-0"><i class="bi bi-lightbulb text-info"></i> Behavioral Learning</h5>
1431
1447
  <span class="badge bg-secondary" id="behavioral-profile-badge">default</span>
1432
1448
  </div>
1449
+ <!-- Getting Started Guide -->
1450
+ <div class="alert alert-light border mb-3">
1451
+ <h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> How Behavioral Learning Works</h6>
1452
+ <p class="mb-1 small">This tab tracks how memories are used in practice — which recalls led to successful outcomes (code written, decisions made, bugs fixed) and which didn't.</p>
1453
+ <p class="mb-0 small text-muted">Report outcomes using the form below, or via MCP: <code>report_outcome</code>. Patterns emerge after 10+ outcomes.</p>
1454
+ </div>
1433
1455
  <!-- Stats Row -->
1434
1456
  <div class="row g-3 mb-4">
1435
1457
  <div class="col-md-3"><div class="card p-3 text-center"><div class="fw-bold fs-3 text-success" id="bh-success-count">-</div><small class="text-muted">Successes</small></div></div>
@@ -1465,12 +1487,18 @@
1465
1487
  </div>
1466
1488
  </div>
1467
1489
 
1468
- <!-- Compliance & Audit (v2.8) -->
1490
+ <!-- Compliance & Audit -->
1469
1491
  <div class="tab-pane fade" id="compliance-pane" role="tabpanel">
1470
1492
  <div class="d-flex justify-content-between align-items-center mb-3">
1471
1493
  <h5 class="mb-0"><i class="bi bi-shield-lock text-success"></i> Compliance &amp; Audit</h5>
1472
1494
  <span class="badge bg-secondary" id="compliance-profile-badge">default</span>
1473
1495
  </div>
1496
+ <!-- Getting Started Guide -->
1497
+ <div class="alert alert-light border mb-3">
1498
+ <h6 class="mb-2"><i class="bi bi-info-circle text-primary"></i> Compliance Overview</h6>
1499
+ <p class="mb-1 small">Set retention policies and access controls for your memories. In Mode A, all data stays on your device — EU AI Act compliant by default.</p>
1500
+ <p class="mb-0 small text-muted">Create a retention policy below to start managing memory lifecycle automatically.</p>
1501
+ </div>
1474
1502
  <!-- Stats Row -->
1475
1503
  <div class="row g-3 mb-4">
1476
1504
  <div class="col-md-4"><div class="card p-3 text-center"><div class="fw-bold fs-3" id="cp-audit-count">-</div><small class="text-muted">Audit Events</small></div></div>
package/ui/js/clusters.js CHANGED
@@ -1,7 +1,5 @@
1
- // SuperLocalMemory V2 - Clusters View
2
- // Depends on: core.js
3
- //
4
- // Security: All dynamic values escaped via escapeHtml(). Data from local DB only.
1
+ // SuperLocalMemory V3 - Clusters View
2
+ // Part of Qualixar | https://superlocalmemory.com
5
3
 
6
4
  async function loadClusters() {
7
5
  showLoading('clusters-list', 'Loading clusters...');
@@ -18,152 +16,180 @@ async function loadClusters() {
18
16
  function renderClusters(clusters) {
19
17
  var container = document.getElementById('clusters-list');
20
18
  if (!clusters || clusters.length === 0) {
21
- showEmpty('clusters-list', 'collection', 'No clusters found. Run "slm build-graph" to generate clusters.');
19
+ showEmpty('clusters-list', 'collection', 'No clusters found yet. Clusters form automatically as you store related memories.');
22
20
  return;
23
21
  }
24
22
 
25
- var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a'];
23
+ var colors = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#30cfd0', '#764ba2', '#f5576c'];
26
24
  container.textContent = '';
27
25
 
28
26
  clusters.forEach(function(cluster, idx) {
29
27
  var color = colors[idx % colors.length];
30
28
 
31
29
  var card = document.createElement('div');
32
- card.className = 'card cluster-card';
33
- card.style.cssText = 'border-color:' + color + '; cursor:pointer;';
34
- card.setAttribute('data-cluster-id', cluster.cluster_id);
35
- card.title = 'Click to filter graph to this cluster';
30
+ card.className = 'card mb-2';
31
+ card.style.borderLeft = '4px solid ' + color;
36
32
 
37
33
  var body = document.createElement('div');
38
- body.className = 'card-body';
34
+ body.className = 'card-body py-2 px-3';
35
+ body.style.cursor = 'pointer';
36
+
37
+ // Header row
38
+ var headerRow = document.createElement('div');
39
+ headerRow.className = 'd-flex justify-content-between align-items-center';
39
40
 
40
41
  var title = document.createElement('h6');
41
- title.className = 'card-title';
42
- title.textContent = 'Cluster ' + cluster.cluster_id + ' ';
42
+ title.className = 'mb-0';
43
+ title.textContent = 'Cluster ' + cluster.cluster_id;
44
+
45
+ var badges = document.createElement('div');
43
46
  var countBadge = document.createElement('span');
44
- countBadge.className = 'badge bg-secondary float-end';
47
+ countBadge.className = 'badge bg-secondary me-1';
45
48
  countBadge.textContent = cluster.member_count + ' memories';
46
- title.appendChild(countBadge);
47
- body.appendChild(title);
48
-
49
- var imp = document.createElement('p');
50
- imp.className = 'mb-2';
51
- imp.textContent = 'Avg Importance: ' + parseFloat(cluster.avg_importance).toFixed(1);
52
- body.appendChild(imp);
53
-
54
- var cats = document.createElement('p');
55
- cats.className = 'mb-2';
56
- cats.textContent = 'Categories: ' + (cluster.categories || 'None');
57
- body.appendChild(cats);
58
-
59
- var entLabel = document.createElement('strong');
60
- entLabel.textContent = 'Top Entities:';
61
- body.appendChild(entLabel);
62
- body.appendChild(document.createElement('br'));
63
-
64
- if (cluster.top_entities && cluster.top_entities.length > 0) {
65
- cluster.top_entities.forEach(function(e) {
66
- var badge = document.createElement('span');
67
- badge.className = 'badge bg-info entity-badge';
68
- badge.textContent = e.entity + ' (' + e.count + ')';
69
- body.appendChild(badge);
70
- body.appendChild(document.createTextNode(' '));
71
- });
72
- } else {
73
- var none = document.createElement('span');
74
- none.className = 'text-muted';
75
- none.textContent = 'No entities';
76
- body.appendChild(none);
49
+ badges.appendChild(countBadge);
50
+
51
+ if (cluster.avg_importance) {
52
+ var impBadge = document.createElement('span');
53
+ impBadge.className = 'badge bg-outline-primary';
54
+ impBadge.style.cssText = 'border:1px solid #667eea; color:#667eea;';
55
+ impBadge.textContent = 'imp: ' + parseFloat(cluster.avg_importance).toFixed(1);
56
+ badges.appendChild(impBadge);
57
+ }
58
+
59
+ var expandIcon = document.createElement('i');
60
+ expandIcon.className = 'bi bi-chevron-down ms-2';
61
+ expandIcon.style.transition = 'transform 0.2s';
62
+ badges.appendChild(expandIcon);
63
+
64
+ headerRow.appendChild(title);
65
+ headerRow.appendChild(badges);
66
+ body.appendChild(headerRow);
67
+
68
+ // Summary line (categories if available)
69
+ if (cluster.categories) {
70
+ var catLine = document.createElement('small');
71
+ catLine.className = 'text-muted';
72
+ catLine.textContent = cluster.categories;
73
+ body.appendChild(catLine);
77
74
  }
78
75
 
76
+ // Expandable member area (hidden by default)
77
+ var memberArea = document.createElement('div');
78
+ memberArea.className = 'mt-2';
79
+ memberArea.style.display = 'none';
80
+ memberArea.id = 'cluster-members-' + cluster.cluster_id;
81
+
82
+ var loadingText = document.createElement('div');
83
+ loadingText.className = 'text-center text-muted small py-2';
84
+ loadingText.textContent = 'Loading members...';
85
+ memberArea.appendChild(loadingText);
86
+
87
+ body.appendChild(memberArea);
79
88
  card.appendChild(body);
80
89
  container.appendChild(card);
81
90
 
82
- // v2.6.5: Click card → filter graph to this cluster
83
- card.addEventListener('click', function(e) {
84
- // Don't trigger if clicking on badge or entity
85
- if (e.target.classList.contains('entity-badge') || e.target.classList.contains('badge')) {
86
- return;
87
- }
91
+ // Click to expand/collapse
92
+ var expanded = false;
93
+ body.addEventListener('click', function(e) {
94
+ expanded = !expanded;
95
+ memberArea.style.display = expanded ? 'block' : 'none';
96
+ expandIcon.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0)';
88
97
 
89
- const clusterId = parseInt(card.getAttribute('data-cluster-id'));
90
- filterGraphToCluster(clusterId);
98
+ if (expanded && memberArea.children.length === 1 && memberArea.children[0] === loadingText) {
99
+ loadClusterMembers(cluster.cluster_id, memberArea);
100
+ }
91
101
  });
102
+ });
103
+ }
92
104
 
93
- // v2.6.5: Click entity badge → filter graph by entity
94
- if (cluster.top_entities && cluster.top_entities.length > 0) {
95
- const entityBadges = body.querySelectorAll('.entity-badge');
96
- entityBadges.forEach(function(badge) {
97
- badge.style.cursor = 'pointer';
98
- badge.title = 'Click to show memories with this entity';
99
- badge.addEventListener('click', function(e) {
100
- e.stopPropagation(); // Don't trigger card click
101
- const entityText = badge.textContent.split(' (')[0]; // Extract entity name
102
- filterGraphByEntity(entityText);
103
- });
104
- });
105
+ async function loadClusterMembers(clusterId, container) {
106
+ try {
107
+ var response = await fetch('/api/clusters/' + clusterId + '?limit=10');
108
+ var data = await response.json();
109
+ container.textContent = '';
110
+
111
+ if (!data.members || data.members.length === 0) {
112
+ var empty = document.createElement('div');
113
+ empty.className = 'text-muted small';
114
+ empty.textContent = 'No members found.';
115
+ container.appendChild(empty);
116
+ return;
105
117
  }
106
118
 
107
- // v2.6.5: Click "X memories" badge → show list in sidebar (future feature)
108
- countBadge.style.cursor = 'pointer';
109
- countBadge.title = 'Click to view memories in this cluster';
110
- countBadge.addEventListener('click', function(e) {
111
- e.stopPropagation(); // Don't trigger card click
112
- showClusterMemories(cluster.cluster_id);
119
+ data.members.forEach(function(m, i) {
120
+ var row = document.createElement('div');
121
+ row.className = 'border-bottom py-1';
122
+ if (i === data.members.length - 1) row.className = 'py-1';
123
+
124
+ var content = document.createElement('div');
125
+ content.className = 'small';
126
+ var text = m.content || m.summary || '';
127
+ content.textContent = (i + 1) + '. ' + (text.length > 150 ? text.substring(0, 150) + '...' : text);
128
+ row.appendChild(content);
129
+
130
+ var meta = document.createElement('div');
131
+ meta.className = 'text-muted';
132
+ meta.style.fontSize = '0.7rem';
133
+ var parts = [];
134
+ if (m.category) parts.push(m.category);
135
+ if (m.importance) parts.push('imp: ' + m.importance);
136
+ if (m.created_at) parts.push(m.created_at.substring(0, 10));
137
+ meta.textContent = parts.join(' | ');
138
+ row.appendChild(meta);
139
+
140
+ container.appendChild(row);
113
141
  });
114
- });
142
+
143
+ // View in graph button
144
+ var graphBtn = document.createElement('button');
145
+ graphBtn.className = 'btn btn-sm btn-outline-primary mt-2';
146
+ graphBtn.textContent = 'View in Knowledge Graph';
147
+ graphBtn.addEventListener('click', function(e) {
148
+ e.stopPropagation();
149
+ filterGraphToCluster(clusterId);
150
+ });
151
+ container.appendChild(graphBtn);
152
+
153
+ } catch (error) {
154
+ container.textContent = '';
155
+ var errDiv = document.createElement('div');
156
+ errDiv.className = 'text-danger small';
157
+ errDiv.textContent = 'Failed to load: ' + error.message;
158
+ container.appendChild(errDiv);
159
+ }
115
160
  }
116
161
 
117
- // v2.6.5: Filter graph to a specific cluster
118
162
  function filterGraphToCluster(clusterId) {
119
- // Switch to Graph tab
120
- const graphTab = document.querySelector('a[href="#graph"]');
121
- if (graphTab) {
122
- graphTab.click();
123
- }
163
+ var graphTab = document.querySelector('a[href="#graph"]');
164
+ if (graphTab) graphTab.click();
124
165
 
125
- // Apply filter after a delay (for tab to load)
126
166
  setTimeout(function() {
127
167
  if (typeof filterState !== 'undefined' && typeof filterByCluster === 'function' && typeof renderGraph === 'function') {
128
168
  filterState.cluster_id = clusterId;
129
- const filtered = filterByCluster(originalGraphData, clusterId);
169
+ var filtered = filterByCluster(originalGraphData, clusterId);
130
170
  renderGraph(filtered);
131
-
132
- // Update URL
133
- const url = new URL(window.location);
171
+ var url = new URL(window.location);
134
172
  url.searchParams.set('cluster_id', clusterId);
135
173
  window.history.replaceState({}, '', url);
136
174
  }
137
175
  }, 300);
138
176
  }
139
177
 
140
- // v2.6.5: Filter graph by entity
141
178
  function filterGraphByEntity(entity) {
142
- // Switch to Graph tab
143
- const graphTab = document.querySelector('a[href="#graph"]');
144
- if (graphTab) {
145
- graphTab.click();
146
- }
179
+ var graphTab = document.querySelector('a[href="#graph"]');
180
+ if (graphTab) graphTab.click();
147
181
 
148
- // Apply filter after a delay
149
182
  setTimeout(function() {
150
183
  if (typeof filterState !== 'undefined' && typeof filterByEntity === 'function' && typeof renderGraph === 'function') {
151
184
  filterState.entity = entity;
152
- const filtered = filterByEntity(originalGraphData, entity);
185
+ var filtered = filterByEntity(originalGraphData, entity);
153
186
  renderGraph(filtered);
154
187
  }
155
188
  }, 300);
156
189
  }
157
190
 
158
- // v2.6.5: Show memories in a cluster (future: sidebar list)
159
191
  function showClusterMemories(clusterId) {
160
- // For now, just filter Memories tab
161
- const memoriesTab = document.querySelector('a[href="#memories"]');
162
- if (memoriesTab) {
163
- memoriesTab.click();
164
- }
165
-
166
- // TODO: Implement sidebar memory list view
167
- console.log('Show memories for cluster', clusterId);
168
- showToast('Filtering memories for cluster ' + clusterId);
192
+ var memoriesTab = document.querySelector('a[href="#memories"]');
193
+ if (memoriesTab) memoriesTab.click();
194
+ if (typeof showToast === 'function') showToast('Filtering memories for cluster ' + clusterId);
169
195
  }
@@ -98,8 +98,10 @@ function transformDataForCytoscape(data) {
98
98
 
99
99
  // Add nodes
100
100
  data.nodes.forEach(node => {
101
- const label = node.category || node.project_name || `Memory #${node.id}`;
102
101
  const contentPreview = node.content_preview || node.summary || node.content || '';
102
+ // Label: first 4 words of content (readable on node), fallback to category
103
+ const contentWords = contentPreview.split(/\s+/).slice(0, 4).join(' ');
104
+ const label = contentWords || node.category || `Memory #${node.id}`;
103
105
  const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
104
106
 
105
107
  elements.push({
@@ -1,10 +1,23 @@
1
- // SuperLocalMemory V3 — Recall Lab
1
+ // SuperLocalMemory V3 — Recall Lab with Pagination
2
2
  // Part of Qualixar | https://superlocalmemory.com
3
3
 
4
+ var recallLabState = {
5
+ allResults: [],
6
+ page: 0,
7
+ perPage: 10,
8
+ query: '',
9
+ };
10
+
4
11
  document.getElementById('recall-lab-search')?.addEventListener('click', function() {
5
12
  var query = document.getElementById('recall-lab-query').value.trim();
6
13
  if (!query) return;
7
14
 
15
+ recallLabState.query = query;
16
+ recallLabState.page = 0;
17
+ var perPageEl = document.getElementById('recall-lab-per-page');
18
+ recallLabState.perPage = perPageEl ? parseInt(perPageEl.value) : 10;
19
+ var fetchLimit = Math.max(recallLabState.perPage * 5, 50); // Fetch up to 5 pages
20
+
8
21
  var resultsDiv = document.getElementById('recall-lab-results');
9
22
  var metaDiv = document.getElementById('recall-lab-meta');
10
23
  resultsDiv.textContent = '';
@@ -18,7 +31,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
18
31
  fetch('/api/v3/recall/trace', {
19
32
  method: 'POST',
20
33
  headers: {'Content-Type': 'application/json'},
21
- body: JSON.stringify({query: query, limit: 10})
34
+ body: JSON.stringify({query: query, limit: fetchLimit})
22
35
  }).then(function(r) { return r.json(); }).then(function(data) {
23
36
  if (data.error) {
24
37
  resultsDiv.textContent = '';
@@ -32,11 +45,13 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
32
45
  metaDiv.textContent = '';
33
46
  appendMetaField(metaDiv, 'Query type: ', data.query_type || 'unknown');
34
47
  metaDiv.appendChild(document.createTextNode(' | '));
35
- appendMetaField(metaDiv, 'Results: ', String(data.result_count));
48
+ appendMetaField(metaDiv, 'Results: ', String((data.results || []).length));
36
49
  metaDiv.appendChild(document.createTextNode(' | '));
37
50
  appendMetaField(metaDiv, 'Time: ', (data.retrieval_time_ms || 0).toFixed(0) + 'ms');
38
51
 
39
- if (!data.results || data.results.length === 0) {
52
+ recallLabState.allResults = data.results || [];
53
+
54
+ if (recallLabState.allResults.length === 0) {
40
55
  resultsDiv.textContent = '';
41
56
  var infoDiv = document.createElement('div');
42
57
  infoDiv.className = 'alert alert-info';
@@ -45,57 +60,7 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
45
60
  return;
46
61
  }
47
62
 
48
- resultsDiv.textContent = '';
49
- var listGroup = document.createElement('div');
50
- listGroup.className = 'list-group';
51
-
52
- data.results.forEach(function(r, i) {
53
- var channels = r.channel_scores || {};
54
- var maxChannel = Math.max(channels.semantic || 0, channels.bm25 || 0, channels.entity_graph || 0, channels.temporal || 0) || 1;
55
-
56
- var item = document.createElement('div');
57
- item.className = 'list-group-item list-group-item-action';
58
- item.style.cursor = 'pointer';
59
- item.title = 'Click to view full memory';
60
- (function(result) {
61
- item.addEventListener('click', function() {
62
- if (typeof openMemoryDetail === 'function') {
63
- openMemoryDetail({
64
- id: result.fact_id,
65
- content: result.content,
66
- score: result.score,
67
- importance: Math.round((result.confidence || 0.5) * 10),
68
- category: 'recall',
69
- tags: Object.keys(result.channel_scores || {}).join(', '),
70
- created_at: null,
71
- trust_score: result.trust_score,
72
- channel_scores: result.channel_scores
73
- });
74
- }
75
- });
76
- })(r);
77
-
78
- var header = document.createElement('h6');
79
- header.className = 'mb-1';
80
- header.textContent = (i + 1) + '. ' + (r.content || '').substring(0, 200);
81
- item.appendChild(header);
82
-
83
- var meta = document.createElement('small');
84
- meta.className = 'text-muted';
85
- meta.textContent = 'Score: ' + r.score + ' | Trust: ' + r.trust_score + ' | Confidence: ' + r.confidence;
86
- item.appendChild(meta);
87
-
88
- var barsDiv = document.createElement('div');
89
- barsDiv.className = 'mt-2';
90
- barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
91
- barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
92
- barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
93
- barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
94
- item.appendChild(barsDiv);
95
-
96
- listGroup.appendChild(item);
97
- });
98
- resultsDiv.appendChild(listGroup);
63
+ renderRecallPage();
99
64
  }).catch(function(e) {
100
65
  resultsDiv.textContent = '';
101
66
  var errDiv = document.createElement('div');
@@ -105,6 +70,140 @@ document.getElementById('recall-lab-search')?.addEventListener('click', function
105
70
  });
106
71
  });
107
72
 
73
+ function renderRecallPage() {
74
+ var resultsDiv = document.getElementById('recall-lab-results');
75
+ resultsDiv.textContent = '';
76
+
77
+ var results = recallLabState.allResults;
78
+ var start = recallLabState.page * recallLabState.perPage;
79
+ var end = Math.min(start + recallLabState.perPage, results.length);
80
+ var pageResults = results.slice(start, end);
81
+ var totalPages = Math.ceil(results.length / recallLabState.perPage);
82
+
83
+ var listGroup = document.createElement('div');
84
+ listGroup.className = 'list-group';
85
+
86
+ pageResults.forEach(function(r, i) {
87
+ var globalIndex = start + i;
88
+ var channels = r.channel_scores || {};
89
+ var maxChannel = Math.max(channels.semantic || 0, channels.bm25 || 0, channels.entity_graph || 0, channels.temporal || 0) || 1;
90
+
91
+ var item = document.createElement('div');
92
+ item.className = 'list-group-item list-group-item-action';
93
+ item.style.cursor = 'pointer';
94
+ item.title = 'Click to view full memory';
95
+ (function(result) {
96
+ item.addEventListener('click', function() {
97
+ if (typeof openMemoryDetail === 'function') {
98
+ openMemoryDetail({
99
+ id: result.fact_id,
100
+ content: result.content,
101
+ score: result.score,
102
+ importance: Math.round((result.confidence || 0.5) * 10),
103
+ category: 'recall',
104
+ tags: Object.keys(result.channel_scores || {}).join(', '),
105
+ created_at: null,
106
+ trust_score: result.trust_score,
107
+ channel_scores: result.channel_scores
108
+ });
109
+ }
110
+ });
111
+ })(r);
112
+
113
+ var header = document.createElement('h6');
114
+ header.className = 'mb-1';
115
+ header.textContent = (globalIndex + 1) + '. ' + (r.content || '').substring(0, 200);
116
+ item.appendChild(header);
117
+
118
+ var meta = document.createElement('small');
119
+ meta.className = 'text-muted';
120
+ meta.textContent = 'Score: ' + r.score + ' | Trust: ' + r.trust_score + ' | Confidence: ' + r.confidence;
121
+ item.appendChild(meta);
122
+
123
+ var barsDiv = document.createElement('div');
124
+ barsDiv.className = 'mt-2';
125
+ barsDiv.appendChild(buildChannelBar('Semantic', channels.semantic || 0, maxChannel, 'primary'));
126
+ barsDiv.appendChild(buildChannelBar('BM25', channels.bm25 || 0, maxChannel, 'success'));
127
+ barsDiv.appendChild(buildChannelBar('Entity', channels.entity_graph || 0, maxChannel, 'info'));
128
+ barsDiv.appendChild(buildChannelBar('Temporal', channels.temporal || 0, maxChannel, 'warning'));
129
+ item.appendChild(barsDiv);
130
+
131
+ listGroup.appendChild(item);
132
+ });
133
+ resultsDiv.appendChild(listGroup);
134
+
135
+ // Pagination controls
136
+ if (totalPages > 1) {
137
+ var nav = document.createElement('nav');
138
+ nav.className = 'mt-3';
139
+ nav.setAttribute('aria-label', 'Recall results pagination');
140
+ var ul = document.createElement('ul');
141
+ ul.className = 'pagination justify-content-center';
142
+
143
+ // Prev
144
+ var prevLi = document.createElement('li');
145
+ prevLi.className = 'page-item' + (recallLabState.page === 0 ? ' disabled' : '');
146
+ var prevA = document.createElement('a');
147
+ prevA.className = 'page-link';
148
+ prevA.href = '#';
149
+ prevA.textContent = 'Previous';
150
+ prevA.addEventListener('click', function(e) {
151
+ e.preventDefault();
152
+ if (recallLabState.page > 0) {
153
+ recallLabState.page--;
154
+ renderRecallPage();
155
+ }
156
+ });
157
+ prevLi.appendChild(prevA);
158
+ ul.appendChild(prevLi);
159
+
160
+ // Page numbers
161
+ for (var p = 0; p < totalPages; p++) {
162
+ var li = document.createElement('li');
163
+ li.className = 'page-item' + (p === recallLabState.page ? ' active' : '');
164
+ var a = document.createElement('a');
165
+ a.className = 'page-link';
166
+ a.href = '#';
167
+ a.textContent = String(p + 1);
168
+ (function(pageNum) {
169
+ a.addEventListener('click', function(e) {
170
+ e.preventDefault();
171
+ recallLabState.page = pageNum;
172
+ renderRecallPage();
173
+ });
174
+ })(p);
175
+ li.appendChild(a);
176
+ ul.appendChild(li);
177
+ }
178
+
179
+ // Next
180
+ var nextLi = document.createElement('li');
181
+ nextLi.className = 'page-item' + (recallLabState.page >= totalPages - 1 ? ' disabled' : '');
182
+ var nextA = document.createElement('a');
183
+ nextA.className = 'page-link';
184
+ nextA.href = '#';
185
+ nextA.textContent = 'Next';
186
+ nextA.addEventListener('click', function(e) {
187
+ e.preventDefault();
188
+ if (recallLabState.page < totalPages - 1) {
189
+ recallLabState.page++;
190
+ renderRecallPage();
191
+ }
192
+ });
193
+ nextLi.appendChild(nextA);
194
+ ul.appendChild(nextLi);
195
+
196
+ nav.appendChild(ul);
197
+ resultsDiv.appendChild(nav);
198
+
199
+ // Page info
200
+ var info = document.createElement('div');
201
+ info.className = 'text-center text-muted small';
202
+ info.textContent = 'Showing ' + (start + 1) + '-' + end + ' of ' + results.length + ' results';
203
+ resultsDiv.appendChild(info);
204
+ }
205
+ }
206
+
108
207
  function appendMetaField(parent, label, value) {
109
208
  var text = document.createTextNode(label);
110
209
  parent.appendChild(text);
@@ -115,27 +214,22 @@ function appendMetaField(parent, label, value) {
115
214
 
116
215
  function buildChannelBar(name, score, max, color) {
117
216
  var pct = max > 0 ? Math.round((score / max) * 100) : 0;
118
-
119
217
  var row = document.createElement('div');
120
218
  row.className = 'd-flex align-items-center mb-1';
121
-
122
219
  var label = document.createElement('span');
123
220
  label.className = 'me-2';
124
221
  label.style.width = '70px';
125
222
  label.style.fontSize = '0.75rem';
126
223
  label.textContent = name;
127
224
  row.appendChild(label);
128
-
129
225
  var progressWrap = document.createElement('div');
130
226
  progressWrap.className = 'progress flex-grow-1';
131
227
  progressWrap.style.height = '14px';
132
-
133
228
  var bar = document.createElement('div');
134
229
  bar.className = 'progress-bar bg-' + color;
135
230
  bar.style.width = pct + '%';
136
231
  bar.textContent = score.toFixed(3);
137
232
  progressWrap.appendChild(bar);
138
-
139
233
  row.appendChild(progressWrap);
140
234
  return row;
141
235
  }