superlocalmemory 2.4.2 → 2.5.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/CHANGELOG.md +46 -0
- package/README.md +21 -0
- package/docs/ARCHITECTURE-V2.5.md +190 -0
- package/mcp_server.py +115 -14
- package/package.json +1 -1
- package/src/agent_registry.py +385 -0
- package/src/db_connection_manager.py +532 -0
- package/src/event_bus.py +555 -0
- package/src/memory_store_v2.py +626 -471
- package/src/provenance_tracker.py +322 -0
- package/src/subscription_manager.py +399 -0
- package/src/trust_scorer.py +456 -0
- package/src/webhook_dispatcher.py +229 -0
- package/ui/app.js +425 -0
- package/ui/index.html +147 -1
- package/ui/js/agents.js +192 -0
- package/ui/js/clusters.js +80 -0
- package/ui/js/core.js +230 -0
- package/ui/js/events.js +178 -0
- package/ui/js/graph.js +32 -0
- package/ui/js/init.js +31 -0
- package/ui/js/memories.js +149 -0
- package/ui/js/modal.js +139 -0
- package/ui/js/patterns.js +93 -0
- package/ui/js/profiles.js +202 -0
- package/ui/js/search.js +59 -0
- package/ui/js/settings.js +167 -0
- package/ui/js/timeline.js +32 -0
- package/ui_server.py +69 -1665
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SuperLocalMemory V2 - Agent Registry
|
|
4
|
+
Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
5
|
+
Licensed under MIT License
|
|
6
|
+
|
|
7
|
+
Repository: https://github.com/varun369/SuperLocalMemoryV2
|
|
8
|
+
Author: Varun Pratap Bhardwaj (Solution Architect)
|
|
9
|
+
|
|
10
|
+
NOTICE: This software is protected by MIT License.
|
|
11
|
+
Attribution must be preserved in all copies or derivatives.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
AgentRegistry — Tracks which AI agents connect to SuperLocalMemory,
|
|
16
|
+
what they write, when, and via which protocol.
|
|
17
|
+
|
|
18
|
+
Every MCP client (Claude, Cursor, Windsurf), CLI call, REST API request,
|
|
19
|
+
and future A2A agent gets registered here. This powers:
|
|
20
|
+
- Dashboard "Connected Agents" panel
|
|
21
|
+
- Trust scoring input (v2.5 silent collection)
|
|
22
|
+
- Provenance tracking (who created which memory)
|
|
23
|
+
- Usage analytics
|
|
24
|
+
|
|
25
|
+
Agent Identity:
|
|
26
|
+
Each agent gets a unique agent_id derived from its protocol + name.
|
|
27
|
+
Example: "mcp:claude-desktop", "cli:terminal", "rest:api-client"
|
|
28
|
+
|
|
29
|
+
Protocols:
|
|
30
|
+
mcp — Model Context Protocol (Claude Desktop, Cursor, Windsurf, etc.)
|
|
31
|
+
cli — Command-line interface (slm command, bin/ scripts)
|
|
32
|
+
rest — REST API (api_server.py)
|
|
33
|
+
python — Direct Python import
|
|
34
|
+
a2a — Agent-to-Agent Protocol (v2.6+)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import logging
|
|
39
|
+
import threading
|
|
40
|
+
from datetime import datetime
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Optional, List, Dict, Any
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("superlocalmemory.agents")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentRegistry:
|
|
48
|
+
"""
|
|
49
|
+
Registry of all agents that interact with SuperLocalMemory.
|
|
50
|
+
|
|
51
|
+
Singleton per database path. Thread-safe.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
_instances: Dict[str, "AgentRegistry"] = {}
|
|
55
|
+
_instances_lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def get_instance(cls, db_path: Optional[Path] = None) -> "AgentRegistry":
|
|
59
|
+
"""Get or create the singleton AgentRegistry."""
|
|
60
|
+
if db_path is None:
|
|
61
|
+
db_path = Path.home() / ".claude-memory" / "memory.db"
|
|
62
|
+
key = str(db_path)
|
|
63
|
+
with cls._instances_lock:
|
|
64
|
+
if key not in cls._instances:
|
|
65
|
+
cls._instances[key] = cls(db_path)
|
|
66
|
+
return cls._instances[key]
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def reset_instance(cls, db_path: Optional[Path] = None):
|
|
70
|
+
"""Remove singleton. Used for testing."""
|
|
71
|
+
with cls._instances_lock:
|
|
72
|
+
if db_path is None:
|
|
73
|
+
cls._instances.clear()
|
|
74
|
+
else:
|
|
75
|
+
key = str(db_path)
|
|
76
|
+
if key in cls._instances:
|
|
77
|
+
del cls._instances[key]
|
|
78
|
+
|
|
79
|
+
def __init__(self, db_path: Path):
|
|
80
|
+
self.db_path = Path(db_path)
|
|
81
|
+
self._init_schema()
|
|
82
|
+
logger.info("AgentRegistry initialized: db=%s", self.db_path)
|
|
83
|
+
|
|
84
|
+
def _init_schema(self):
|
|
85
|
+
"""Create agent_registry table if it doesn't exist."""
|
|
86
|
+
try:
|
|
87
|
+
from db_connection_manager import DbConnectionManager
|
|
88
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
89
|
+
|
|
90
|
+
def _create(conn):
|
|
91
|
+
conn.execute('''
|
|
92
|
+
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
95
|
+
agent_name TEXT,
|
|
96
|
+
protocol TEXT NOT NULL,
|
|
97
|
+
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
98
|
+
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
99
|
+
memories_written INTEGER DEFAULT 0,
|
|
100
|
+
memories_recalled INTEGER DEFAULT 0,
|
|
101
|
+
trust_score REAL DEFAULT 1.0,
|
|
102
|
+
metadata TEXT DEFAULT '{}'
|
|
103
|
+
)
|
|
104
|
+
''')
|
|
105
|
+
conn.execute('''
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_agent_protocol
|
|
107
|
+
ON agent_registry(protocol)
|
|
108
|
+
''')
|
|
109
|
+
conn.execute('''
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_agent_last_seen
|
|
111
|
+
ON agent_registry(last_seen)
|
|
112
|
+
''')
|
|
113
|
+
conn.commit()
|
|
114
|
+
|
|
115
|
+
mgr.execute_write(_create)
|
|
116
|
+
except ImportError:
|
|
117
|
+
import sqlite3
|
|
118
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
119
|
+
conn.execute('''
|
|
120
|
+
CREATE TABLE IF NOT EXISTS agent_registry (
|
|
121
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
122
|
+
agent_id TEXT NOT NULL UNIQUE,
|
|
123
|
+
agent_name TEXT,
|
|
124
|
+
protocol TEXT NOT NULL,
|
|
125
|
+
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
126
|
+
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
127
|
+
memories_written INTEGER DEFAULT 0,
|
|
128
|
+
memories_recalled INTEGER DEFAULT 0,
|
|
129
|
+
trust_score REAL DEFAULT 1.0,
|
|
130
|
+
metadata TEXT DEFAULT '{}'
|
|
131
|
+
)
|
|
132
|
+
''')
|
|
133
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_agent_protocol ON agent_registry(protocol)')
|
|
134
|
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_agent_last_seen ON agent_registry(last_seen)')
|
|
135
|
+
conn.commit()
|
|
136
|
+
conn.close()
|
|
137
|
+
|
|
138
|
+
# =========================================================================
|
|
139
|
+
# Agent Registration
|
|
140
|
+
# =========================================================================
|
|
141
|
+
|
|
142
|
+
def register_agent(
|
|
143
|
+
self,
|
|
144
|
+
agent_id: str,
|
|
145
|
+
agent_name: Optional[str] = None,
|
|
146
|
+
protocol: str = "cli",
|
|
147
|
+
metadata: Optional[dict] = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""
|
|
150
|
+
Register or update an agent in the registry.
|
|
151
|
+
|
|
152
|
+
If the agent already exists, updates last_seen and metadata.
|
|
153
|
+
If new, creates the entry with trust_score=1.0.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
agent_id: Unique identifier (e.g., "mcp:claude-desktop")
|
|
157
|
+
agent_name: Human-readable name (e.g., "Claude Desktop")
|
|
158
|
+
protocol: Connection protocol (mcp, cli, rest, python, a2a)
|
|
159
|
+
metadata: Additional agent info (version, capabilities, etc.)
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Agent record dict
|
|
163
|
+
"""
|
|
164
|
+
if not agent_id or not isinstance(agent_id, str):
|
|
165
|
+
raise ValueError("agent_id must be a non-empty string")
|
|
166
|
+
|
|
167
|
+
valid_protocols = ("mcp", "cli", "rest", "python", "a2a")
|
|
168
|
+
if protocol not in valid_protocols:
|
|
169
|
+
raise ValueError(f"Invalid protocol: {protocol}. Must be one of {valid_protocols}")
|
|
170
|
+
|
|
171
|
+
now = datetime.now().isoformat()
|
|
172
|
+
meta_json = json.dumps(metadata or {})
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
from db_connection_manager import DbConnectionManager
|
|
176
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
177
|
+
|
|
178
|
+
def _upsert(conn):
|
|
179
|
+
conn.execute('''
|
|
180
|
+
INSERT INTO agent_registry (agent_id, agent_name, protocol, first_seen, last_seen, metadata)
|
|
181
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
182
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
183
|
+
last_seen = excluded.last_seen,
|
|
184
|
+
metadata = excluded.metadata,
|
|
185
|
+
agent_name = COALESCE(excluded.agent_name, agent_registry.agent_name)
|
|
186
|
+
''', (agent_id, agent_name, protocol, now, now, meta_json))
|
|
187
|
+
conn.commit()
|
|
188
|
+
|
|
189
|
+
mgr.execute_write(_upsert)
|
|
190
|
+
except ImportError:
|
|
191
|
+
import sqlite3
|
|
192
|
+
conn = sqlite3.connect(str(self.db_path))
|
|
193
|
+
conn.execute('''
|
|
194
|
+
INSERT INTO agent_registry (agent_id, agent_name, protocol, first_seen, last_seen, metadata)
|
|
195
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
196
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
197
|
+
last_seen = excluded.last_seen,
|
|
198
|
+
metadata = excluded.metadata,
|
|
199
|
+
agent_name = COALESCE(excluded.agent_name, agent_registry.agent_name)
|
|
200
|
+
''', (agent_id, agent_name, protocol, now, now, meta_json))
|
|
201
|
+
conn.commit()
|
|
202
|
+
conn.close()
|
|
203
|
+
|
|
204
|
+
# Emit agent.connected event
|
|
205
|
+
try:
|
|
206
|
+
from event_bus import EventBus
|
|
207
|
+
bus = EventBus.get_instance(self.db_path)
|
|
208
|
+
bus.emit("agent.connected", payload={
|
|
209
|
+
"agent_id": agent_id,
|
|
210
|
+
"agent_name": agent_name,
|
|
211
|
+
"protocol": protocol,
|
|
212
|
+
})
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
logger.info("Agent registered: id=%s, protocol=%s", agent_id, protocol)
|
|
217
|
+
return self.get_agent(agent_id) or {"agent_id": agent_id}
|
|
218
|
+
|
|
219
|
+
def record_write(self, agent_id: str):
|
|
220
|
+
"""Increment memories_written counter and update last_seen."""
|
|
221
|
+
self._increment_counter(agent_id, "memories_written")
|
|
222
|
+
|
|
223
|
+
def record_recall(self, agent_id: str):
|
|
224
|
+
"""Increment memories_recalled counter and update last_seen."""
|
|
225
|
+
self._increment_counter(agent_id, "memories_recalled")
|
|
226
|
+
|
|
227
|
+
def _increment_counter(self, agent_id: str, column: str):
|
|
228
|
+
"""Increment a counter column for an agent."""
|
|
229
|
+
if column not in ("memories_written", "memories_recalled"):
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
now = datetime.now().isoformat()
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
from db_connection_manager import DbConnectionManager
|
|
236
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
237
|
+
|
|
238
|
+
def _inc(conn):
|
|
239
|
+
conn.execute(
|
|
240
|
+
f"UPDATE agent_registry SET {column} = {column} + 1, last_seen = ? WHERE agent_id = ?",
|
|
241
|
+
(now, agent_id)
|
|
242
|
+
)
|
|
243
|
+
conn.commit()
|
|
244
|
+
|
|
245
|
+
mgr.execute_write(_inc)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error("Failed to increment %s for %s: %s", column, agent_id, e)
|
|
248
|
+
|
|
249
|
+
# =========================================================================
|
|
250
|
+
# Query Agents
|
|
251
|
+
# =========================================================================
|
|
252
|
+
|
|
253
|
+
def get_agent(self, agent_id: str) -> Optional[dict]:
|
|
254
|
+
"""Get a specific agent by ID."""
|
|
255
|
+
try:
|
|
256
|
+
from db_connection_manager import DbConnectionManager
|
|
257
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
258
|
+
|
|
259
|
+
with mgr.read_connection() as conn:
|
|
260
|
+
cursor = conn.cursor()
|
|
261
|
+
cursor.execute("""
|
|
262
|
+
SELECT agent_id, agent_name, protocol, first_seen, last_seen,
|
|
263
|
+
memories_written, memories_recalled, trust_score, metadata
|
|
264
|
+
FROM agent_registry WHERE agent_id = ?
|
|
265
|
+
""", (agent_id,))
|
|
266
|
+
row = cursor.fetchone()
|
|
267
|
+
|
|
268
|
+
if not row:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
return self._row_to_dict(row)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error("Failed to get agent %s: %s", agent_id, e)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def list_agents(
|
|
277
|
+
self,
|
|
278
|
+
protocol: Optional[str] = None,
|
|
279
|
+
limit: int = 50,
|
|
280
|
+
active_since_hours: Optional[int] = None,
|
|
281
|
+
) -> List[dict]:
|
|
282
|
+
"""
|
|
283
|
+
List registered agents with optional filtering.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
protocol: Filter by protocol (mcp, cli, rest, python, a2a)
|
|
287
|
+
limit: Max agents to return
|
|
288
|
+
active_since_hours: Only agents seen within N hours
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
List of agent dicts, ordered by last_seen descending
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
from db_connection_manager import DbConnectionManager
|
|
295
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
296
|
+
|
|
297
|
+
with mgr.read_connection() as conn:
|
|
298
|
+
cursor = conn.cursor()
|
|
299
|
+
|
|
300
|
+
query = """
|
|
301
|
+
SELECT agent_id, agent_name, protocol, first_seen, last_seen,
|
|
302
|
+
memories_written, memories_recalled, trust_score, metadata
|
|
303
|
+
FROM agent_registry WHERE 1=1
|
|
304
|
+
"""
|
|
305
|
+
params = []
|
|
306
|
+
|
|
307
|
+
if protocol:
|
|
308
|
+
query += " AND protocol = ?"
|
|
309
|
+
params.append(protocol)
|
|
310
|
+
|
|
311
|
+
if active_since_hours:
|
|
312
|
+
query += " AND last_seen >= datetime('now', '-' || ? || ' hours')"
|
|
313
|
+
params.append(active_since_hours)
|
|
314
|
+
|
|
315
|
+
query += " ORDER BY last_seen DESC LIMIT ?"
|
|
316
|
+
params.append(limit)
|
|
317
|
+
|
|
318
|
+
cursor.execute(query, params)
|
|
319
|
+
rows = cursor.fetchall()
|
|
320
|
+
|
|
321
|
+
return [self._row_to_dict(row) for row in rows]
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.error("Failed to list agents: %s", e)
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
def get_stats(self) -> dict:
|
|
327
|
+
"""Get agent registry statistics."""
|
|
328
|
+
try:
|
|
329
|
+
from db_connection_manager import DbConnectionManager
|
|
330
|
+
mgr = DbConnectionManager.get_instance(self.db_path)
|
|
331
|
+
|
|
332
|
+
with mgr.read_connection() as conn:
|
|
333
|
+
cursor = conn.cursor()
|
|
334
|
+
|
|
335
|
+
cursor.execute("SELECT COUNT(*) FROM agent_registry")
|
|
336
|
+
total = cursor.fetchone()[0]
|
|
337
|
+
|
|
338
|
+
cursor.execute("""
|
|
339
|
+
SELECT protocol, COUNT(*) FROM agent_registry
|
|
340
|
+
GROUP BY protocol ORDER BY COUNT(*) DESC
|
|
341
|
+
""")
|
|
342
|
+
by_protocol = dict(cursor.fetchall())
|
|
343
|
+
|
|
344
|
+
cursor.execute("""
|
|
345
|
+
SELECT SUM(memories_written), SUM(memories_recalled)
|
|
346
|
+
FROM agent_registry
|
|
347
|
+
""")
|
|
348
|
+
sums = cursor.fetchone()
|
|
349
|
+
|
|
350
|
+
cursor.execute("""
|
|
351
|
+
SELECT COUNT(*) FROM agent_registry
|
|
352
|
+
WHERE last_seen >= datetime('now', '-24 hours')
|
|
353
|
+
""")
|
|
354
|
+
active_24h = cursor.fetchone()[0]
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
"total_agents": total,
|
|
358
|
+
"active_last_24h": active_24h,
|
|
359
|
+
"by_protocol": by_protocol,
|
|
360
|
+
"total_writes": sums[0] or 0,
|
|
361
|
+
"total_recalls": sums[1] or 0,
|
|
362
|
+
}
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error("Failed to get agent stats: %s", e)
|
|
365
|
+
return {"total_agents": 0, "error": str(e)}
|
|
366
|
+
|
|
367
|
+
def _row_to_dict(self, row: tuple) -> dict:
|
|
368
|
+
"""Convert a database row to an agent dict."""
|
|
369
|
+
metadata = {}
|
|
370
|
+
try:
|
|
371
|
+
metadata = json.loads(row[8]) if row[8] else {}
|
|
372
|
+
except (json.JSONDecodeError, TypeError):
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
"agent_id": row[0],
|
|
377
|
+
"agent_name": row[1],
|
|
378
|
+
"protocol": row[2],
|
|
379
|
+
"first_seen": row[3],
|
|
380
|
+
"last_seen": row[4],
|
|
381
|
+
"memories_written": row[5],
|
|
382
|
+
"memories_recalled": row[6],
|
|
383
|
+
"trust_score": row[7],
|
|
384
|
+
"metadata": metadata,
|
|
385
|
+
}
|