claude-memory-agent 2.0.1 → 2.2.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 +206 -206
- package/agent_card.py +186 -0
- package/bin/cli.js +327 -185
- package/bin/lib/banner.js +39 -0
- package/bin/lib/environment.js +166 -0
- package/bin/lib/installer.js +291 -0
- package/bin/lib/models.js +95 -0
- package/bin/lib/steps/advanced.js +101 -0
- package/bin/lib/steps/confirm.js +87 -0
- package/bin/lib/steps/model.js +57 -0
- package/bin/lib/steps/provider.js +65 -0
- package/bin/lib/steps/scope.js +59 -0
- package/bin/lib/steps/server.js +74 -0
- package/bin/lib/ui.js +75 -0
- package/bin/onboarding.js +164 -0
- package/bin/postinstall.js +35 -270
- package/config.py +103 -4
- package/dashboard.html +4902 -2689
- package/hooks/extract_memories.py +439 -0
- package/hooks/grounding-hook.py +422 -348
- package/hooks/pre_compact_hook.py +76 -0
- package/hooks/session_end.py +293 -192
- package/hooks/session_end_hook.py +149 -0
- package/hooks/session_start.py +227 -227
- package/hooks/stop_hook.py +372 -0
- package/install.py +972 -902
- package/main.py +5240 -2859
- package/mcp_server.py +451 -0
- package/package.json +58 -47
- package/requirements.txt +12 -8
- package/services/__init__.py +50 -50
- package/services/adaptive_ranker.py +272 -0
- package/services/agent_catalog.json +153 -0
- package/services/agent_registry.py +245 -730
- package/services/claude_md_sync.py +320 -4
- package/services/consolidation.py +417 -0
- package/services/curator.py +1606 -0
- package/services/database.py +4118 -2485
- package/services/embedding_pipeline.py +262 -0
- package/services/embeddings.py +493 -85
- package/services/memory_decay.py +408 -0
- package/services/native_memory_paths.py +86 -0
- package/services/native_memory_sync.py +496 -0
- package/services/response_manager.py +183 -0
- package/services/terminal_ui.py +199 -0
- package/services/tier_manager.py +235 -0
- package/services/websocket.py +26 -6
- package/skills/__init__.py +21 -1
- package/skills/confidence_tracker.py +441 -0
- package/skills/context.py +675 -0
- package/skills/curator.py +348 -0
- package/skills/search.py +444 -213
- package/skills/session_review.py +605 -0
- package/skills/store.py +484 -179
- package/terminal_dashboard.py +474 -0
- package/update_system.py +829 -817
- package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
- package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
- package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
- package/services/__pycache__/__init__.cpython-312.pyc +0 -0
- package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
- package/services/__pycache__/auth.cpython-312.pyc +0 -0
- package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
- package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
- package/services/__pycache__/confidence.cpython-312.pyc +0 -0
- package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
- package/services/__pycache__/database.cpython-312.pyc +0 -0
- package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
- package/services/__pycache__/insights.cpython-312.pyc +0 -0
- package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
- package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
- package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
- package/services/__pycache__/timeline.cpython-312.pyc +0 -0
- package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
- package/services/__pycache__/websocket.cpython-312.pyc +0 -0
- package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
- package/skills/__pycache__/admin.cpython-312.pyc +0 -0
- package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
- package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
- package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
- package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
- package/skills/__pycache__/insights.cpython-312.pyc +0 -0
- package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
- package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
- package/skills/__pycache__/search.cpython-312.pyc +0 -0
- package/skills/__pycache__/state.cpython-312.pyc +0 -0
- package/skills/__pycache__/store.cpython-312.pyc +0 -0
- package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
- package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
- package/skills/__pycache__/verification.cpython-312.pyc +0 -0
- package/test_automation.py +0 -221
- package/test_complete.py +0 -338
- package/test_full.py +0 -322
- package/verify_db.py +0 -134
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live Terminal Dashboard for Claude Memory Agent.
|
|
3
|
+
|
|
4
|
+
A standalone real-time dashboard that polls the running server and displays
|
|
5
|
+
live statistics using Rich's Live display.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python terminal_dashboard.py # Default: http://localhost:8102
|
|
9
|
+
python terminal_dashboard.py --port 8103 # Custom port
|
|
10
|
+
python terminal_dashboard.py --refresh 3 # Refresh every 3 seconds
|
|
11
|
+
|
|
12
|
+
Press Ctrl+C to exit.
|
|
13
|
+
"""
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import argparse
|
|
17
|
+
import httpx
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from typing import Dict, Any, Optional
|
|
20
|
+
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
from rich.text import Text
|
|
25
|
+
from rich.layout import Layout
|
|
26
|
+
from rich.live import Live
|
|
27
|
+
from rich.align import Align
|
|
28
|
+
from rich.columns import Columns
|
|
29
|
+
from rich.progress_bar import ProgressBar
|
|
30
|
+
from rich.spinner import Spinner
|
|
31
|
+
from rich.theme import Theme
|
|
32
|
+
|
|
33
|
+
THEME = Theme({
|
|
34
|
+
"info": "cyan",
|
|
35
|
+
"warning": "yellow",
|
|
36
|
+
"error.style": "bold red",
|
|
37
|
+
"success": "bold green",
|
|
38
|
+
"hot": "bold red",
|
|
39
|
+
"warm": "bold yellow",
|
|
40
|
+
"cold": "bold blue",
|
|
41
|
+
"header": "bold magenta",
|
|
42
|
+
"muted": "dim white",
|
|
43
|
+
"accent": "bold cyan",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
console = Console(theme=THEME)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DashboardState:
|
|
51
|
+
"""Tracks dashboard state and history."""
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.health: Dict = {}
|
|
55
|
+
self.stats: Dict = {}
|
|
56
|
+
self.tier_stats: Dict = {}
|
|
57
|
+
self.consolidation_stats: Dict = {}
|
|
58
|
+
self.pipeline_stats: Dict = {}
|
|
59
|
+
self.index_stats: Dict = {}
|
|
60
|
+
self.decay_stats: Dict = {}
|
|
61
|
+
self.recent_activity: list = []
|
|
62
|
+
self.last_update: Optional[datetime] = None
|
|
63
|
+
self.error: Optional[str] = None
|
|
64
|
+
self.uptime_start: Optional[datetime] = None
|
|
65
|
+
self.refresh_count: int = 0
|
|
66
|
+
self.connection_errors: int = 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fetch_data(base_url: str, state: DashboardState):
|
|
70
|
+
"""Fetch all dashboard data from the server."""
|
|
71
|
+
client = httpx.Client(timeout=5.0)
|
|
72
|
+
state.error = None
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Health check
|
|
76
|
+
r = client.get(f"{base_url}/health")
|
|
77
|
+
state.health = r.json()
|
|
78
|
+
if state.uptime_start is None:
|
|
79
|
+
state.uptime_start = datetime.now()
|
|
80
|
+
|
|
81
|
+
# Stats
|
|
82
|
+
try:
|
|
83
|
+
r = client.get(f"{base_url}/api/stats")
|
|
84
|
+
state.stats = r.json()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# Tier stats
|
|
89
|
+
try:
|
|
90
|
+
r = client.get(f"{base_url}/api/tiers/stats")
|
|
91
|
+
state.tier_stats = r.json()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Consolidation stats
|
|
96
|
+
try:
|
|
97
|
+
r = client.get(f"{base_url}/api/consolidation/stats")
|
|
98
|
+
state.consolidation_stats = r.json()
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
# Embedding pipeline stats
|
|
103
|
+
try:
|
|
104
|
+
r = client.get(f"{base_url}/api/embedding-pipeline/stats")
|
|
105
|
+
state.pipeline_stats = r.json()
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
# Index stats
|
|
110
|
+
try:
|
|
111
|
+
r = client.get(f"{base_url}/api/index-stats")
|
|
112
|
+
state.index_stats = r.json()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# Decay stats
|
|
117
|
+
try:
|
|
118
|
+
r = client.get(f"{base_url}/api/decay/stats")
|
|
119
|
+
state.decay_stats = r.json()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
state.last_update = datetime.now()
|
|
124
|
+
state.refresh_count += 1
|
|
125
|
+
state.connection_errors = 0
|
|
126
|
+
|
|
127
|
+
except httpx.ConnectError:
|
|
128
|
+
state.error = "Cannot connect to server"
|
|
129
|
+
state.connection_errors += 1
|
|
130
|
+
except Exception as e:
|
|
131
|
+
state.error = str(e)
|
|
132
|
+
state.connection_errors += 1
|
|
133
|
+
finally:
|
|
134
|
+
client.close()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def make_header(state: DashboardState, base_url: str) -> Panel:
|
|
138
|
+
"""Create the header panel."""
|
|
139
|
+
text = Text()
|
|
140
|
+
text.append(" Claude", style="bold cyan")
|
|
141
|
+
text.append("Memory", style="bold white")
|
|
142
|
+
text.append(" ", style="")
|
|
143
|
+
|
|
144
|
+
# Connection status
|
|
145
|
+
if state.error:
|
|
146
|
+
text.append(" DISCONNECTED ", style="bold white on red")
|
|
147
|
+
else:
|
|
148
|
+
text.append(" LIVE ", style="bold white on green")
|
|
149
|
+
|
|
150
|
+
text.append(f" {base_url}", style="muted")
|
|
151
|
+
|
|
152
|
+
# Uptime
|
|
153
|
+
if state.uptime_start:
|
|
154
|
+
uptime = datetime.now() - state.uptime_start
|
|
155
|
+
hours = int(uptime.total_seconds() // 3600)
|
|
156
|
+
mins = int((uptime.total_seconds() % 3600) // 60)
|
|
157
|
+
text.append(f" | Uptime: {hours}h{mins:02d}m", style="muted")
|
|
158
|
+
|
|
159
|
+
text.append(f" | Refresh #{state.refresh_count}", style="muted")
|
|
160
|
+
|
|
161
|
+
return Panel(Align.center(text), style="cyan", height=3)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def make_memory_panel(state: DashboardState) -> Panel:
|
|
165
|
+
"""Create the memory statistics panel."""
|
|
166
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
167
|
+
table.add_column("label", style="dim cyan", width=16)
|
|
168
|
+
table.add_column("value", style="bold white")
|
|
169
|
+
|
|
170
|
+
stats = state.stats
|
|
171
|
+
total = stats.get('total_memories', 0)
|
|
172
|
+
table.add_row("Total Memories", f"[bold]{total}[/bold]")
|
|
173
|
+
|
|
174
|
+
# Type breakdown
|
|
175
|
+
type_counts = stats.get('type_counts', {})
|
|
176
|
+
if type_counts:
|
|
177
|
+
for mtype, count in sorted(type_counts.items(), key=lambda x: -x[1]):
|
|
178
|
+
bar_width = min(int(count / max(total, 1) * 20), 20)
|
|
179
|
+
bar = "[green]" + "=" * bar_width + "[/green]" + "[muted]" + "-" * (20 - bar_width) + "[/muted]"
|
|
180
|
+
table.add_row(f" {mtype}", f"{count:>4} {bar}")
|
|
181
|
+
|
|
182
|
+
table.add_row("", "")
|
|
183
|
+
table.add_row("Patterns", str(stats.get('total_patterns', 0)))
|
|
184
|
+
table.add_row("Timeline Events", str(stats.get('total_timeline_events', 0)))
|
|
185
|
+
|
|
186
|
+
return Panel(table, title="[bold]Memories[/bold]", border_style="green")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def make_tier_panel(state: DashboardState) -> Panel:
|
|
190
|
+
"""Create the tier distribution panel."""
|
|
191
|
+
table = Table(show_header=True, box=None, padding=(0, 1), expand=True)
|
|
192
|
+
table.add_column("Tier", style="dim cyan", width=8)
|
|
193
|
+
table.add_column("Count", style="bold white", width=8, justify="right")
|
|
194
|
+
table.add_column("Avg Imp", style="dim white", width=8, justify="right")
|
|
195
|
+
table.add_column("", width=20)
|
|
196
|
+
|
|
197
|
+
tiers = state.tier_stats.get('tiers', {})
|
|
198
|
+
total = state.tier_stats.get('total_memories', 1) or 1
|
|
199
|
+
|
|
200
|
+
tier_styles = {'hot': 'hot', 'warm': 'warm', 'cold': 'cold'}
|
|
201
|
+
|
|
202
|
+
for tier_name in ['hot', 'warm', 'cold']:
|
|
203
|
+
info = tiers.get(tier_name, {'count': 0, 'avg_importance': 0})
|
|
204
|
+
count = info.get('count', 0)
|
|
205
|
+
avg_imp = info.get('avg_importance', 0)
|
|
206
|
+
pct = count / total * 100
|
|
207
|
+
|
|
208
|
+
bar_width = min(int(pct / 5), 20)
|
|
209
|
+
style = tier_styles.get(tier_name, 'muted')
|
|
210
|
+
bar = f"[{style}]" + "|" * bar_width + f"[/{style}]" + " " * (20 - bar_width)
|
|
211
|
+
|
|
212
|
+
icon = {"hot": "[hot]***[/hot]", "warm": "[warm] ** [/warm]", "cold": "[cold] * [/cold]"}.get(tier_name, "")
|
|
213
|
+
table.add_row(
|
|
214
|
+
f"[{style}]{tier_name.upper()}[/{style}] {icon}",
|
|
215
|
+
str(count),
|
|
216
|
+
f"{avg_imp:.1f}",
|
|
217
|
+
f"{bar} {pct:.0f}%"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return Panel(table, title="[bold]Memory Tiers[/bold]", border_style="yellow")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def make_health_panel(state: DashboardState) -> Panel:
|
|
224
|
+
"""Create the system health panel."""
|
|
225
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
226
|
+
table.add_column("key", style="dim cyan", width=16)
|
|
227
|
+
table.add_column("value", style="bold white")
|
|
228
|
+
|
|
229
|
+
health = state.health
|
|
230
|
+
|
|
231
|
+
# Server status
|
|
232
|
+
status = health.get('status', 'unknown')
|
|
233
|
+
if status == 'healthy':
|
|
234
|
+
table.add_row("Server", "[green]Healthy[/green]")
|
|
235
|
+
elif status == 'degraded':
|
|
236
|
+
table.add_row("Server", "[yellow]Degraded[/yellow]")
|
|
237
|
+
else:
|
|
238
|
+
table.add_row("Server", f"[red]{status}[/red]")
|
|
239
|
+
|
|
240
|
+
# Ollama
|
|
241
|
+
ollama = health.get('ollama', {})
|
|
242
|
+
if isinstance(ollama, dict):
|
|
243
|
+
if ollama.get('healthy'):
|
|
244
|
+
model = ollama.get('model', '?')
|
|
245
|
+
table.add_row("Ollama", f"[green]OK[/green] ({model})")
|
|
246
|
+
else:
|
|
247
|
+
table.add_row("Ollama", "[red]Down[/red]")
|
|
248
|
+
else:
|
|
249
|
+
table.add_row("Ollama", str(ollama))
|
|
250
|
+
|
|
251
|
+
# Database
|
|
252
|
+
db_info = health.get('database', {})
|
|
253
|
+
if isinstance(db_info, dict):
|
|
254
|
+
if db_info.get('connected'):
|
|
255
|
+
table.add_row("Database", "[green]Connected[/green]")
|
|
256
|
+
else:
|
|
257
|
+
table.add_row("Database", "[red]Disconnected[/red]")
|
|
258
|
+
|
|
259
|
+
# Vector index
|
|
260
|
+
index = health.get('vector_index', {})
|
|
261
|
+
if isinstance(index, dict):
|
|
262
|
+
faiss = "[green]FAISS[/green]" if index.get('faiss_available') else "[yellow]NumPy[/yellow]"
|
|
263
|
+
table.add_row("Vector Index", faiss)
|
|
264
|
+
|
|
265
|
+
return Panel(table, title="[bold]Health[/bold]", border_style="green")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def make_pipeline_panel(state: DashboardState) -> Panel:
|
|
269
|
+
"""Create the embedding pipeline panel."""
|
|
270
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
271
|
+
table.add_column("key", style="dim cyan", width=16)
|
|
272
|
+
table.add_column("value", style="bold white")
|
|
273
|
+
|
|
274
|
+
pipeline = state.pipeline_stats
|
|
275
|
+
cache = pipeline.get('cache', {})
|
|
276
|
+
|
|
277
|
+
if cache:
|
|
278
|
+
size = cache.get('size', 0)
|
|
279
|
+
max_size = cache.get('max_size', 0)
|
|
280
|
+
hits = cache.get('hits', 0)
|
|
281
|
+
misses = cache.get('misses', 0)
|
|
282
|
+
hit_rate = cache.get('hit_rate', 0)
|
|
283
|
+
mem_mb = cache.get('estimated_memory_mb', 0)
|
|
284
|
+
|
|
285
|
+
# Cache fill bar
|
|
286
|
+
fill_pct = size / max(max_size, 1) * 100
|
|
287
|
+
fill_bar_w = min(int(fill_pct / 5), 20)
|
|
288
|
+
fill_bar = "[cyan]" + "|" * fill_bar_w + "[/cyan]" + "[muted]" + "-" * (20 - fill_bar_w) + "[/muted]"
|
|
289
|
+
|
|
290
|
+
table.add_row("Cache Fill", f"{size}/{max_size} {fill_bar}")
|
|
291
|
+
table.add_row("Hit Rate", f"[{'green' if hit_rate > 0.5 else 'yellow'}]{hit_rate:.1%}[/{'green' if hit_rate > 0.5 else 'yellow'}]")
|
|
292
|
+
table.add_row("Hits / Misses", f"[green]{hits}[/green] / [yellow]{misses}[/yellow]")
|
|
293
|
+
table.add_row("Memory", f"{mem_mb:.2f} MB")
|
|
294
|
+
else:
|
|
295
|
+
table.add_row("Status", "[muted]Not initialized[/muted]")
|
|
296
|
+
|
|
297
|
+
precomputing = pipeline.get('precompute_running', False)
|
|
298
|
+
if precomputing:
|
|
299
|
+
table.add_row("Precompute", "[green]Running[/green]")
|
|
300
|
+
else:
|
|
301
|
+
table.add_row("Precompute", "[muted]Idle[/muted]")
|
|
302
|
+
|
|
303
|
+
degraded = pipeline.get('service_degraded', False)
|
|
304
|
+
if degraded:
|
|
305
|
+
table.add_row("Ollama", "[red]Degraded[/red]")
|
|
306
|
+
|
|
307
|
+
return Panel(table, title="[bold]Embedding Pipeline[/bold]", border_style="cyan")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def make_consolidation_panel(state: DashboardState) -> Panel:
|
|
311
|
+
"""Create the consolidation stats panel."""
|
|
312
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
313
|
+
table.add_column("key", style="dim cyan", width=16)
|
|
314
|
+
table.add_column("value", style="bold white")
|
|
315
|
+
|
|
316
|
+
cs = state.consolidation_stats
|
|
317
|
+
|
|
318
|
+
consolidated = cs.get('consolidated_memories', 0)
|
|
319
|
+
archived = cs.get('archived_originals', 0)
|
|
320
|
+
avg_group = cs.get('avg_group_size', 0)
|
|
321
|
+
|
|
322
|
+
table.add_row("Consolidated", str(consolidated))
|
|
323
|
+
table.add_row("Archived", str(archived))
|
|
324
|
+
table.add_row("Avg Group Size", str(avg_group))
|
|
325
|
+
table.add_row("Space Saved", cs.get('space_savings_estimate', 'N/A'))
|
|
326
|
+
|
|
327
|
+
return Panel(table, title="[bold]Consolidation[/bold]", border_style="magenta")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def make_decay_panel(state: DashboardState) -> Panel:
|
|
331
|
+
"""Create the memory decay stats panel."""
|
|
332
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
333
|
+
table.add_column("key", style="dim cyan", width=16)
|
|
334
|
+
table.add_column("value", style="bold white")
|
|
335
|
+
|
|
336
|
+
ds = state.decay_stats
|
|
337
|
+
|
|
338
|
+
permanent = ds.get('permanent_count', 0)
|
|
339
|
+
decayable = ds.get('decayable_count', 0)
|
|
340
|
+
at_risk = ds.get('at_risk_count', 0)
|
|
341
|
+
healthy = ds.get('healthy_count', 0)
|
|
342
|
+
archived = ds.get('archived_by_decay', 0)
|
|
343
|
+
|
|
344
|
+
table.add_row("Permanent", f"[green]{permanent}[/green]")
|
|
345
|
+
table.add_row("Decayable", str(decayable))
|
|
346
|
+
|
|
347
|
+
if decayable > 0:
|
|
348
|
+
health_pct = healthy / max(decayable, 1) * 100
|
|
349
|
+
risk_bar_w = min(int(at_risk / max(decayable, 1) * 20), 20)
|
|
350
|
+
health_bar_w = 20 - risk_bar_w
|
|
351
|
+
bar = "[green]" + "|" * health_bar_w + "[/green]" + "[red]" + "|" * risk_bar_w + "[/red]"
|
|
352
|
+
table.add_row("Health", f"{bar} {health_pct:.0f}%")
|
|
353
|
+
|
|
354
|
+
table.add_row("At Risk", f"[{'red' if at_risk > 0 else 'green'}]{at_risk}[/{'red' if at_risk > 0 else 'green'}]")
|
|
355
|
+
table.add_row("Archived", str(archived))
|
|
356
|
+
|
|
357
|
+
return Panel(table, title="[bold]Memory Decay[/bold]", border_style="yellow")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def make_index_panel(state: DashboardState) -> Panel:
|
|
361
|
+
"""Create the vector index stats panel."""
|
|
362
|
+
table = Table(show_header=False, box=None, padding=(0, 1), expand=True)
|
|
363
|
+
table.add_column("key", style="dim cyan", width=16)
|
|
364
|
+
table.add_column("value", style="bold white")
|
|
365
|
+
|
|
366
|
+
ix = state.index_stats
|
|
367
|
+
|
|
368
|
+
faiss = ix.get('faiss_available', False)
|
|
369
|
+
table.add_row("Backend", "[green]FAISS[/green]" if faiss else "[yellow]NumPy[/yellow]")
|
|
370
|
+
|
|
371
|
+
for idx_name in ['memories', 'patterns', 'timeline']:
|
|
372
|
+
idx = ix.get(idx_name, {})
|
|
373
|
+
if idx:
|
|
374
|
+
size = idx.get('size', 0)
|
|
375
|
+
searches = idx.get('search_count', 0)
|
|
376
|
+
table.add_row(f" {idx_name}", f"{size} vectors, {searches} searches")
|
|
377
|
+
|
|
378
|
+
return Panel(table, title="[bold]Vector Index[/bold]", border_style="blue")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def make_error_panel(state: DashboardState) -> Panel:
|
|
382
|
+
"""Create an error panel when server is unreachable."""
|
|
383
|
+
text = Text()
|
|
384
|
+
text.append("\n Cannot connect to server\n\n", style="bold red")
|
|
385
|
+
text.append(f" {state.error}\n\n", style="muted")
|
|
386
|
+
text.append(f" Consecutive errors: {state.connection_errors}\n", style="yellow")
|
|
387
|
+
text.append(" Retrying...\n", style="muted")
|
|
388
|
+
return Panel(text, title="[bold red]Connection Error[/bold red]", border_style="red")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def build_layout(state: DashboardState, base_url: str) -> Layout:
|
|
392
|
+
"""Build the full dashboard layout."""
|
|
393
|
+
layout = Layout()
|
|
394
|
+
|
|
395
|
+
layout.split_column(
|
|
396
|
+
Layout(name="header", size=3),
|
|
397
|
+
Layout(name="body"),
|
|
398
|
+
Layout(name="footer", size=3),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Header
|
|
402
|
+
layout["header"].update(make_header(state, base_url))
|
|
403
|
+
|
|
404
|
+
if state.error and state.connection_errors > 2:
|
|
405
|
+
layout["body"].update(make_error_panel(state))
|
|
406
|
+
else:
|
|
407
|
+
# Body: two rows of panels
|
|
408
|
+
layout["body"].split_column(
|
|
409
|
+
Layout(name="top_row", size=14),
|
|
410
|
+
Layout(name="bottom_row"),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
layout["top_row"].split_row(
|
|
414
|
+
Layout(make_memory_panel(state), name="memories"),
|
|
415
|
+
Layout(make_tier_panel(state), name="tiers"),
|
|
416
|
+
Layout(make_health_panel(state), name="health"),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
layout["bottom_row"].split_row(
|
|
420
|
+
Layout(make_pipeline_panel(state), name="pipeline"),
|
|
421
|
+
Layout(make_consolidation_panel(state), name="consolidation"),
|
|
422
|
+
Layout(make_decay_panel(state), name="decay"),
|
|
423
|
+
Layout(make_index_panel(state), name="index"),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Footer
|
|
427
|
+
footer_text = Text()
|
|
428
|
+
footer_text.append(" Press ", style="muted")
|
|
429
|
+
footer_text.append("Ctrl+C", style="bold")
|
|
430
|
+
footer_text.append(" to exit", style="muted")
|
|
431
|
+
if state.last_update:
|
|
432
|
+
footer_text.append(f" | Last updated: {state.last_update.strftime('%H:%M:%S')}", style="muted")
|
|
433
|
+
layout["footer"].update(Panel(footer_text, style="muted"))
|
|
434
|
+
|
|
435
|
+
return layout
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def main():
|
|
439
|
+
parser = argparse.ArgumentParser(description="Claude Memory Agent - Live Dashboard")
|
|
440
|
+
parser.add_argument("--port", type=int, default=8102, help="Server port (default: 8102)")
|
|
441
|
+
parser.add_argument("--host", type=str, default="localhost", help="Server host (default: localhost)")
|
|
442
|
+
parser.add_argument("--refresh", type=float, default=2.0, help="Refresh interval in seconds (default: 2)")
|
|
443
|
+
args = parser.parse_args()
|
|
444
|
+
|
|
445
|
+
base_url = f"http://{args.host}:{args.port}"
|
|
446
|
+
state = DashboardState()
|
|
447
|
+
|
|
448
|
+
console.print(f"\n[bold cyan]Connecting to {base_url}...[/bold cyan]\n")
|
|
449
|
+
|
|
450
|
+
# Initial fetch
|
|
451
|
+
fetch_data(base_url, state)
|
|
452
|
+
|
|
453
|
+
if state.error:
|
|
454
|
+
console.print(f"[red]Warning: {state.error}[/red]")
|
|
455
|
+
console.print("[muted]Dashboard will retry automatically...[/muted]\n")
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
with Live(
|
|
459
|
+
build_layout(state, base_url),
|
|
460
|
+
console=console,
|
|
461
|
+
refresh_per_second=1,
|
|
462
|
+
screen=True,
|
|
463
|
+
) as live:
|
|
464
|
+
while True:
|
|
465
|
+
time.sleep(args.refresh)
|
|
466
|
+
fetch_data(base_url, state)
|
|
467
|
+
live.update(build_layout(state, base_url))
|
|
468
|
+
|
|
469
|
+
except KeyboardInterrupt:
|
|
470
|
+
console.print("\n[muted]Dashboard stopped.[/muted]")
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
if __name__ == "__main__":
|
|
474
|
+
main()
|