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.
Files changed (97) hide show
  1. package/README.md +206 -206
  2. package/agent_card.py +186 -0
  3. package/bin/cli.js +327 -185
  4. package/bin/lib/banner.js +39 -0
  5. package/bin/lib/environment.js +166 -0
  6. package/bin/lib/installer.js +291 -0
  7. package/bin/lib/models.js +95 -0
  8. package/bin/lib/steps/advanced.js +101 -0
  9. package/bin/lib/steps/confirm.js +87 -0
  10. package/bin/lib/steps/model.js +57 -0
  11. package/bin/lib/steps/provider.js +65 -0
  12. package/bin/lib/steps/scope.js +59 -0
  13. package/bin/lib/steps/server.js +74 -0
  14. package/bin/lib/ui.js +75 -0
  15. package/bin/onboarding.js +164 -0
  16. package/bin/postinstall.js +35 -270
  17. package/config.py +103 -4
  18. package/dashboard.html +4902 -2689
  19. package/hooks/extract_memories.py +439 -0
  20. package/hooks/grounding-hook.py +422 -348
  21. package/hooks/pre_compact_hook.py +76 -0
  22. package/hooks/session_end.py +293 -192
  23. package/hooks/session_end_hook.py +149 -0
  24. package/hooks/session_start.py +227 -227
  25. package/hooks/stop_hook.py +372 -0
  26. package/install.py +972 -902
  27. package/main.py +5240 -2859
  28. package/mcp_server.py +451 -0
  29. package/package.json +58 -47
  30. package/requirements.txt +12 -8
  31. package/services/__init__.py +50 -50
  32. package/services/adaptive_ranker.py +272 -0
  33. package/services/agent_catalog.json +153 -0
  34. package/services/agent_registry.py +245 -730
  35. package/services/claude_md_sync.py +320 -4
  36. package/services/consolidation.py +417 -0
  37. package/services/curator.py +1606 -0
  38. package/services/database.py +4118 -2485
  39. package/services/embedding_pipeline.py +262 -0
  40. package/services/embeddings.py +493 -85
  41. package/services/memory_decay.py +408 -0
  42. package/services/native_memory_paths.py +86 -0
  43. package/services/native_memory_sync.py +496 -0
  44. package/services/response_manager.py +183 -0
  45. package/services/terminal_ui.py +199 -0
  46. package/services/tier_manager.py +235 -0
  47. package/services/websocket.py +26 -6
  48. package/skills/__init__.py +21 -1
  49. package/skills/confidence_tracker.py +441 -0
  50. package/skills/context.py +675 -0
  51. package/skills/curator.py +348 -0
  52. package/skills/search.py +444 -213
  53. package/skills/session_review.py +605 -0
  54. package/skills/store.py +484 -179
  55. package/terminal_dashboard.py +474 -0
  56. package/update_system.py +829 -817
  57. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  58. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  59. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  60. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  61. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  62. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  63. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  64. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  65. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  66. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  67. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  68. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  69. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  70. package/services/__pycache__/database.cpython-312.pyc +0 -0
  71. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  72. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  74. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  75. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  76. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  77. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  78. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  81. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  82. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  83. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  84. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  85. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  86. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  87. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  88. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  89. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  90. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  91. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  92. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  93. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  94. package/test_automation.py +0 -221
  95. package/test_complete.py +0 -338
  96. package/test_full.py +0 -322
  97. 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()