claude-memory-agent 2.0.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 (100) hide show
  1. package/.env.example +107 -0
  2. package/README.md +200 -0
  3. package/agent_card.py +512 -0
  4. package/bin/cli.js +181 -0
  5. package/bin/postinstall.js +216 -0
  6. package/config.py +104 -0
  7. package/dashboard.html +2689 -0
  8. package/hooks/README.md +196 -0
  9. package/hooks/__pycache__/auto-detect-response.cpython-312.pyc +0 -0
  10. package/hooks/__pycache__/auto_capture.cpython-312.pyc +0 -0
  11. package/hooks/__pycache__/session_end.cpython-312.pyc +0 -0
  12. package/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  13. package/hooks/auto-detect-response.py +348 -0
  14. package/hooks/auto_capture.py +255 -0
  15. package/hooks/detect-correction.py +173 -0
  16. package/hooks/grounding-hook.py +348 -0
  17. package/hooks/log-tool-use.py +234 -0
  18. package/hooks/log-user-request.py +208 -0
  19. package/hooks/pre-tool-decision.py +218 -0
  20. package/hooks/problem-detector.py +343 -0
  21. package/hooks/session_end.py +192 -0
  22. package/hooks/session_start.py +227 -0
  23. package/install.py +887 -0
  24. package/main.py +2859 -0
  25. package/manager.py +997 -0
  26. package/package.json +55 -0
  27. package/requirements.txt +8 -0
  28. package/run_server.py +136 -0
  29. package/services/__init__.py +50 -0
  30. package/services/__pycache__/__init__.cpython-312.pyc +0 -0
  31. package/services/__pycache__/agent_registry.cpython-312.pyc +0 -0
  32. package/services/__pycache__/auth.cpython-312.pyc +0 -0
  33. package/services/__pycache__/auto_inject.cpython-312.pyc +0 -0
  34. package/services/__pycache__/claude_md_sync.cpython-312.pyc +0 -0
  35. package/services/__pycache__/cleanup.cpython-312.pyc +0 -0
  36. package/services/__pycache__/compaction_flush.cpython-312.pyc +0 -0
  37. package/services/__pycache__/confidence.cpython-312.pyc +0 -0
  38. package/services/__pycache__/daily_log.cpython-312.pyc +0 -0
  39. package/services/__pycache__/database.cpython-312.pyc +0 -0
  40. package/services/__pycache__/embeddings.cpython-312.pyc +0 -0
  41. package/services/__pycache__/insights.cpython-312.pyc +0 -0
  42. package/services/__pycache__/llm_analyzer.cpython-312.pyc +0 -0
  43. package/services/__pycache__/memory_md_sync.cpython-312.pyc +0 -0
  44. package/services/__pycache__/retry_queue.cpython-312.pyc +0 -0
  45. package/services/__pycache__/timeline.cpython-312.pyc +0 -0
  46. package/services/__pycache__/vector_index.cpython-312.pyc +0 -0
  47. package/services/__pycache__/websocket.cpython-312.pyc +0 -0
  48. package/services/agent_registry.py +753 -0
  49. package/services/auth.py +331 -0
  50. package/services/auto_inject.py +250 -0
  51. package/services/claude_md_sync.py +275 -0
  52. package/services/cleanup.py +667 -0
  53. package/services/compaction_flush.py +447 -0
  54. package/services/confidence.py +301 -0
  55. package/services/daily_log.py +333 -0
  56. package/services/database.py +2485 -0
  57. package/services/embeddings.py +358 -0
  58. package/services/insights.py +632 -0
  59. package/services/llm_analyzer.py +595 -0
  60. package/services/memory_md_sync.py +409 -0
  61. package/services/retry_queue.py +453 -0
  62. package/services/timeline.py +579 -0
  63. package/services/vector_index.py +398 -0
  64. package/services/websocket.py +257 -0
  65. package/skills/__init__.py +6 -0
  66. package/skills/__pycache__/__init__.cpython-312.pyc +0 -0
  67. package/skills/__pycache__/admin.cpython-312.pyc +0 -0
  68. package/skills/__pycache__/checkpoint.cpython-312.pyc +0 -0
  69. package/skills/__pycache__/claude_md.cpython-312.pyc +0 -0
  70. package/skills/__pycache__/cleanup.cpython-312.pyc +0 -0
  71. package/skills/__pycache__/grounding.cpython-312.pyc +0 -0
  72. package/skills/__pycache__/insights.cpython-312.pyc +0 -0
  73. package/skills/__pycache__/natural_language.cpython-312.pyc +0 -0
  74. package/skills/__pycache__/retrieve.cpython-312.pyc +0 -0
  75. package/skills/__pycache__/search.cpython-312.pyc +0 -0
  76. package/skills/__pycache__/state.cpython-312.pyc +0 -0
  77. package/skills/__pycache__/store.cpython-312.pyc +0 -0
  78. package/skills/__pycache__/summarize.cpython-312.pyc +0 -0
  79. package/skills/__pycache__/timeline.cpython-312.pyc +0 -0
  80. package/skills/__pycache__/verification.cpython-312.pyc +0 -0
  81. package/skills/admin.py +469 -0
  82. package/skills/checkpoint.py +198 -0
  83. package/skills/claude_md.py +363 -0
  84. package/skills/cleanup.py +241 -0
  85. package/skills/grounding.py +801 -0
  86. package/skills/insights.py +231 -0
  87. package/skills/natural_language.py +277 -0
  88. package/skills/retrieve.py +67 -0
  89. package/skills/search.py +213 -0
  90. package/skills/state.py +182 -0
  91. package/skills/store.py +179 -0
  92. package/skills/summarize.py +588 -0
  93. package/skills/timeline.py +387 -0
  94. package/skills/verification.py +391 -0
  95. package/start_daemon.py +155 -0
  96. package/test_automation.py +221 -0
  97. package/test_complete.py +338 -0
  98. package/test_full.py +322 -0
  99. package/update_system.py +817 -0
  100. package/verify_db.py +134 -0
package/manager.py ADDED
@@ -0,0 +1,997 @@
1
+ """
2
+ Claude Memory Manager - Windows GUI Application
3
+ A system tray application to manage the Claude Memory Agent server.
4
+
5
+ Features:
6
+ - Start/Stop/Restart the Memory Agent
7
+ - Live log viewer with auto-scroll
8
+ - System tray integration (minimize to tray)
9
+ - Windows startup registration
10
+ - Ollama status monitoring
11
+ - Dark themed UI
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import socket
18
+ import subprocess
19
+ import threading
20
+ import webbrowser
21
+ import winreg
22
+ import tkinter as tk
23
+ from tkinter import ttk, scrolledtext, messagebox
24
+ from pathlib import Path
25
+ from typing import Optional
26
+ import queue
27
+ import time
28
+
29
+ # Configuration
30
+ APP_NAME = "Claude Memory Manager"
31
+ APP_VERSION = "1.0.0"
32
+ AGENT_DIR = Path(__file__).parent.absolute()
33
+ VENV_DIR = AGENT_DIR / "venv"
34
+ PYTHON_EXE = VENV_DIR / "Scripts" / "python.exe"
35
+ MAIN_SCRIPT = AGENT_DIR / "main.py"
36
+ SETTINGS_FILE = AGENT_DIR / "manager_settings.json"
37
+
38
+ # Load port from environment or use default
39
+ from dotenv import load_dotenv
40
+ load_dotenv(AGENT_DIR / ".env")
41
+ PORT = int(os.getenv("PORT", "8102"))
42
+ DASHBOARD_URL = os.getenv("MEMORY_AGENT_URL", f"http://localhost:{PORT}") + "/dashboard"
43
+
44
+ # Color scheme (Dark theme matching dashboard)
45
+ COLORS = {
46
+ "bg": "#1a1a2e",
47
+ "panel": "#16213e",
48
+ "accent": "#0f3460",
49
+ "text": "#e6e6e6",
50
+ "text_dim": "#8888aa",
51
+ "success": "#00ff88",
52
+ "error": "#ff4444",
53
+ "warning": "#ffaa00",
54
+ "button_hover": "#1f4068",
55
+ "button_bg": "#0f3460",
56
+ "border": "#2a2a4e",
57
+ "disabled_fg": "#666688",
58
+ "disabled_bg": "#0a0a1e",
59
+ }
60
+
61
+
62
+ class Settings:
63
+ """Manages persistent settings."""
64
+
65
+ def __init__(self):
66
+ self.run_on_startup = False
67
+ self.auto_start_agent = True
68
+ self.minimize_to_tray = True
69
+ self.start_minimized = False
70
+ self.load()
71
+
72
+ def load(self):
73
+ """Load settings from file."""
74
+ try:
75
+ if SETTINGS_FILE.exists():
76
+ with open(SETTINGS_FILE, "r") as f:
77
+ data = json.load(f)
78
+ self.run_on_startup = data.get("run_on_startup", False)
79
+ self.auto_start_agent = data.get("auto_start_agent", True)
80
+ self.minimize_to_tray = data.get("minimize_to_tray", True)
81
+ self.start_minimized = data.get("start_minimized", False)
82
+ except Exception as e:
83
+ print(f"Error loading settings: {e}")
84
+
85
+ def save(self):
86
+ """Save settings to file."""
87
+ try:
88
+ with open(SETTINGS_FILE, "w") as f:
89
+ json.dump({
90
+ "run_on_startup": self.run_on_startup,
91
+ "auto_start_agent": self.auto_start_agent,
92
+ "minimize_to_tray": self.minimize_to_tray,
93
+ "start_minimized": self.start_minimized,
94
+ }, f, indent=2)
95
+ except Exception as e:
96
+ print(f"Error saving settings: {e}")
97
+
98
+
99
+ class StartupManager:
100
+ """Manages Windows startup registry entries."""
101
+
102
+ REGISTRY_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
103
+ APP_KEY_NAME = "ClaudeMemoryManager"
104
+
105
+ @classmethod
106
+ def is_registered(cls) -> bool:
107
+ """Check if app is registered for startup."""
108
+ try:
109
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, cls.REGISTRY_KEY, 0, winreg.KEY_READ)
110
+ try:
111
+ winreg.QueryValueEx(key, cls.APP_KEY_NAME)
112
+ return True
113
+ except FileNotFoundError:
114
+ return False
115
+ finally:
116
+ winreg.CloseKey(key)
117
+ except Exception:
118
+ return False
119
+
120
+ @classmethod
121
+ def register(cls):
122
+ """Add app to Windows startup."""
123
+ try:
124
+ # Get the path to pythonw.exe (no console) and manager.py
125
+ pythonw = VENV_DIR / "Scripts" / "pythonw.exe"
126
+ if not pythonw.exists():
127
+ pythonw = PYTHON_EXE # Fallback to python.exe
128
+
129
+ startup_cmd = f'"{pythonw}" "{AGENT_DIR / "manager.py"}"'
130
+
131
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, cls.REGISTRY_KEY, 0, winreg.KEY_WRITE)
132
+ winreg.SetValueEx(key, cls.APP_KEY_NAME, 0, winreg.REG_SZ, startup_cmd)
133
+ winreg.CloseKey(key)
134
+ return True
135
+ except Exception as e:
136
+ print(f"Error registering startup: {e}")
137
+ return False
138
+
139
+ @classmethod
140
+ def unregister(cls):
141
+ """Remove app from Windows startup."""
142
+ try:
143
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, cls.REGISTRY_KEY, 0, winreg.KEY_WRITE)
144
+ try:
145
+ winreg.DeleteValue(key, cls.APP_KEY_NAME)
146
+ except FileNotFoundError:
147
+ pass # Already not registered
148
+ winreg.CloseKey(key)
149
+ return True
150
+ except Exception as e:
151
+ print(f"Error unregistering startup: {e}")
152
+ return False
153
+
154
+
155
+ class ProcessManager:
156
+ """Manages the Memory Agent process."""
157
+
158
+ def __init__(self, log_callback=None):
159
+ self.process: Optional[subprocess.Popen] = None
160
+ self.log_callback = log_callback
161
+ self.log_queue = queue.Queue()
162
+ self.reader_thread: Optional[threading.Thread] = None
163
+ self.running = False
164
+
165
+ def is_port_in_use(self) -> bool:
166
+ """Check if the port is already in use."""
167
+ try:
168
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
169
+ return s.connect_ex(("localhost", PORT)) == 0
170
+ except Exception:
171
+ return False
172
+
173
+ def is_running(self) -> bool:
174
+ """Check if the Memory Agent is actually running (not just port in use)."""
175
+ if self.process and self.process.poll() is None:
176
+ return True
177
+
178
+ # Actually check if Memory Agent API responds, not just port
179
+ try:
180
+ import urllib.request
181
+ req = urllib.request.Request(
182
+ f"http://localhost:{PORT}/api/stats",
183
+ headers={"User-Agent": "MemoryManager/1.0"}
184
+ )
185
+ with urllib.request.urlopen(req, timeout=2) as response:
186
+ if response.status == 200:
187
+ return True
188
+ except:
189
+ pass
190
+
191
+ return False
192
+
193
+ def _kill_port_processes(self):
194
+ """Kill all processes using our port."""
195
+ try:
196
+ result = subprocess.run(
197
+ ["netstat", "-ano"],
198
+ capture_output=True, text=True,
199
+ creationflags=subprocess.CREATE_NO_WINDOW
200
+ )
201
+ for line in result.stdout.split("\n"):
202
+ if f":{PORT}" in line and "LISTENING" in line:
203
+ parts = line.split()
204
+ if parts:
205
+ pid = parts[-1]
206
+ if pid.isdigit():
207
+ subprocess.run(
208
+ ["taskkill", "/F", "/PID", pid],
209
+ capture_output=True,
210
+ creationflags=subprocess.CREATE_NO_WINDOW
211
+ )
212
+ except:
213
+ pass
214
+
215
+ def start(self) -> tuple[bool, str]:
216
+ """Start the Memory Agent."""
217
+ if self.is_running():
218
+ return False, "Agent is already running"
219
+
220
+ # If port is in use but agent isn't responding, kill zombie processes first
221
+ if self.is_port_in_use():
222
+ self._kill_port_processes()
223
+ time.sleep(1)
224
+
225
+ if not PYTHON_EXE.exists():
226
+ return False, f"Python not found at {PYTHON_EXE}"
227
+
228
+ if not MAIN_SCRIPT.exists():
229
+ return False, f"Main script not found at {MAIN_SCRIPT}"
230
+
231
+ try:
232
+ # Create the process with pipes for output
233
+ self.process = subprocess.Popen(
234
+ [str(PYTHON_EXE), str(MAIN_SCRIPT)],
235
+ cwd=str(AGENT_DIR),
236
+ stdout=subprocess.PIPE,
237
+ stderr=subprocess.STDOUT,
238
+ text=True,
239
+ bufsize=1,
240
+ creationflags=subprocess.CREATE_NO_WINDOW
241
+ )
242
+
243
+ self.running = True
244
+
245
+ # Start log reader thread
246
+ self.reader_thread = threading.Thread(target=self._read_logs, daemon=True)
247
+ self.reader_thread.start()
248
+
249
+ # Wait a bit and check if it started
250
+ time.sleep(1.5)
251
+ if self.process.poll() is not None:
252
+ # Process exited, read any error output
253
+ return False, "Process exited immediately - check logs for details"
254
+
255
+ return True, f"Agent started successfully on port {PORT}"
256
+
257
+ except Exception as e:
258
+ return False, f"Failed to start: {str(e)}"
259
+
260
+ def stop(self) -> tuple[bool, str]:
261
+ """Stop the Memory Agent."""
262
+ self.running = False
263
+
264
+ if self.process and self.process.poll() is None:
265
+ try:
266
+ self.process.terminate()
267
+ self.process.wait(timeout=5)
268
+ self.process = None
269
+ return True, "Agent stopped gracefully"
270
+ except subprocess.TimeoutExpired:
271
+ self.process.kill()
272
+ self.process = None
273
+ return True, "Agent killed (forced termination)"
274
+ except Exception as e:
275
+ return False, f"Error stopping: {str(e)}"
276
+
277
+ # Kill ALL processes on the port (not just one)
278
+ killed_pids = []
279
+ try:
280
+ # Find ALL processes using the port
281
+ result = subprocess.run(
282
+ ["netstat", "-ano"],
283
+ capture_output=True, text=True,
284
+ creationflags=subprocess.CREATE_NO_WINDOW
285
+ )
286
+ pids_to_kill = set()
287
+ for line in result.stdout.split("\n"):
288
+ if f":{PORT}" in line and "LISTENING" in line:
289
+ parts = line.split()
290
+ if parts:
291
+ pid = parts[-1]
292
+ if pid.isdigit():
293
+ pids_to_kill.add(pid)
294
+
295
+ # Kill ALL of them at once
296
+ for pid in pids_to_kill:
297
+ try:
298
+ subprocess.run(
299
+ ["taskkill", "/F", "/PID", pid],
300
+ capture_output=True,
301
+ creationflags=subprocess.CREATE_NO_WINDOW
302
+ )
303
+ killed_pids.append(pid)
304
+ except:
305
+ pass
306
+ except Exception as e:
307
+ pass
308
+
309
+ # Also kill any stray python processes running memory-agent as fallback
310
+ try:
311
+ subprocess.run(
312
+ ["powershell", "-Command",
313
+ "Get-Process python* -ErrorAction SilentlyContinue | Stop-Process -Force"],
314
+ capture_output=True,
315
+ creationflags=subprocess.CREATE_NO_WINDOW,
316
+ timeout=5
317
+ )
318
+ except:
319
+ pass
320
+
321
+ if killed_pids:
322
+ return True, f"Killed {len(killed_pids)} process(es) (PIDs: {', '.join(killed_pids)})"
323
+ return True, "Agent stopped"
324
+
325
+ def restart(self) -> tuple[bool, str]:
326
+ """Restart the Memory Agent."""
327
+ stop_success, stop_msg = self.stop()
328
+
329
+ # Wait until port is actually free (max 10 seconds)
330
+ for _ in range(20):
331
+ if not self.is_port_in_use():
332
+ break
333
+ time.sleep(0.5)
334
+ else:
335
+ # Force kill everything one more time
336
+ self.stop()
337
+ time.sleep(1)
338
+
339
+ start_success, start_msg = self.start()
340
+
341
+ if start_success:
342
+ return True, f"Agent restarted: {start_msg}"
343
+ else:
344
+ return False, f"Restart failed: {start_msg}"
345
+
346
+ def _read_logs(self):
347
+ """Read logs from the process output."""
348
+ try:
349
+ while self.running and self.process and self.process.stdout:
350
+ try:
351
+ line = self.process.stdout.readline()
352
+ if line:
353
+ self.log_queue.put(line.rstrip())
354
+ elif self.process.poll() is not None:
355
+ break
356
+ except Exception:
357
+ break
358
+ except Exception as e:
359
+ self.log_queue.put(f"[Log reader error: {e}]")
360
+
361
+ def get_logs(self) -> list[str]:
362
+ """Get all pending log lines."""
363
+ logs = []
364
+ while not self.log_queue.empty():
365
+ try:
366
+ logs.append(self.log_queue.get_nowait())
367
+ except queue.Empty:
368
+ break
369
+ return logs
370
+
371
+
372
+ def check_ollama_running() -> bool:
373
+ """Check if Ollama is running."""
374
+ try:
375
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
376
+ s.settimeout(1)
377
+ return s.connect_ex(("localhost", 11434)) == 0
378
+ except Exception:
379
+ return False
380
+
381
+
382
+ class ClaudeMemoryManagerApp:
383
+ """Main application window."""
384
+
385
+ def __init__(self):
386
+ self.settings = Settings()
387
+ self.process_manager = ProcessManager()
388
+ self.root = tk.Tk()
389
+ self.is_minimized = False
390
+
391
+ self.setup_window()
392
+ self.create_widgets()
393
+ self.start_log_updater()
394
+ self.update_status()
395
+ self.sync_startup_checkbox()
396
+
397
+ # Initial log message
398
+ self.log(f"Claude Memory Manager v{APP_VERSION}", "info")
399
+ self.log(f"Agent directory: {AGENT_DIR}", "info")
400
+
401
+ # Check Ollama on startup
402
+ if not check_ollama_running():
403
+ self.log("WARNING: Ollama is not running.", "warning")
404
+ self.log("The Memory Agent requires Ollama for embeddings.", "warning")
405
+ self.log("Please start Ollama before starting the agent.", "warning")
406
+ else:
407
+ self.log("Ollama is running and ready.", "success")
408
+
409
+ # Auto-start if configured
410
+ if self.settings.auto_start_agent and not self.process_manager.is_running():
411
+ self.root.after(1000, self.start_agent)
412
+
413
+ # Start minimized if configured
414
+ if self.settings.start_minimized:
415
+ self.root.after(100, self.minimize_window)
416
+
417
+ def setup_window(self):
418
+ """Configure the main window."""
419
+ self.root.title(APP_NAME)
420
+ self.root.geometry("580x650")
421
+ self.root.minsize(480, 550)
422
+ self.root.configure(bg=COLORS["bg"])
423
+
424
+ # Center the window
425
+ self.root.update_idletasks()
426
+ width = self.root.winfo_width()
427
+ height = self.root.winfo_height()
428
+ x = (self.root.winfo_screenwidth() // 2) - (width // 2)
429
+ y = (self.root.winfo_screenheight() // 2) - (height // 2)
430
+ self.root.geometry(f"+{x}+{y}")
431
+
432
+ # Configure styles
433
+ style = ttk.Style()
434
+ style.theme_use("clam")
435
+
436
+ # Configure ttk styles
437
+ style.configure("TFrame", background=COLORS["bg"])
438
+ style.configure("Panel.TFrame", background=COLORS["panel"])
439
+ style.configure("TLabel", background=COLORS["bg"], foreground=COLORS["text"])
440
+ style.configure("Panel.TLabel", background=COLORS["panel"], foreground=COLORS["text"])
441
+ style.configure("TCheckbutton", background=COLORS["bg"], foreground=COLORS["text"])
442
+
443
+ # Handle window close and minimize
444
+ self.root.protocol("WM_DELETE_WINDOW", self.on_close)
445
+ self.root.bind("<Unmap>", self.on_minimize)
446
+
447
+ def create_widgets(self):
448
+ """Create all UI widgets."""
449
+ # Main container with padding
450
+ main_frame = ttk.Frame(self.root, style="TFrame")
451
+ main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
452
+
453
+ # Header with title and version
454
+ header_frame = tk.Frame(main_frame, bg=COLORS["bg"])
455
+ header_frame.pack(fill=tk.X, pady=(0, 15))
456
+
457
+ title_label = tk.Label(
458
+ header_frame,
459
+ text="Claude Memory Manager",
460
+ font=("Segoe UI", 20, "bold"),
461
+ bg=COLORS["bg"],
462
+ fg=COLORS["text"]
463
+ )
464
+ title_label.pack(side=tk.LEFT)
465
+
466
+ version_label = tk.Label(
467
+ header_frame,
468
+ text=f"v{APP_VERSION}",
469
+ font=("Segoe UI", 10),
470
+ bg=COLORS["bg"],
471
+ fg=COLORS["text_dim"]
472
+ )
473
+ version_label.pack(side=tk.LEFT, padx=(10, 0), pady=(8, 0))
474
+
475
+ # Status panel
476
+ status_frame = tk.Frame(main_frame, bg=COLORS["panel"], padx=20, pady=15)
477
+ status_frame.pack(fill=tk.X, pady=(0, 15))
478
+
479
+ # Left side - Agent status
480
+ agent_status_frame = tk.Frame(status_frame, bg=COLORS["panel"])
481
+ agent_status_frame.pack(side=tk.LEFT)
482
+
483
+ status_label = tk.Label(
484
+ agent_status_frame,
485
+ text="Agent Status:",
486
+ font=("Segoe UI", 11),
487
+ bg=COLORS["panel"],
488
+ fg=COLORS["text_dim"]
489
+ )
490
+ status_label.pack(side=tk.LEFT)
491
+
492
+ self.status_indicator = tk.Label(
493
+ agent_status_frame,
494
+ text="\u25cf", # Filled circle
495
+ font=("Segoe UI", 16),
496
+ bg=COLORS["panel"],
497
+ fg=COLORS["error"]
498
+ )
499
+ self.status_indicator.pack(side=tk.LEFT, padx=(8, 5))
500
+
501
+ self.status_text = tk.Label(
502
+ agent_status_frame,
503
+ text="Stopped",
504
+ font=("Segoe UI", 12, "bold"),
505
+ bg=COLORS["panel"],
506
+ fg=COLORS["text"]
507
+ )
508
+ self.status_text.pack(side=tk.LEFT)
509
+
510
+ # Right side - Ollama status
511
+ self.ollama_frame = tk.Frame(status_frame, bg=COLORS["panel"])
512
+ self.ollama_frame.pack(side=tk.RIGHT)
513
+
514
+ ollama_label = tk.Label(
515
+ self.ollama_frame,
516
+ text="Ollama:",
517
+ font=("Segoe UI", 10),
518
+ bg=COLORS["panel"],
519
+ fg=COLORS["text_dim"]
520
+ )
521
+ ollama_label.pack(side=tk.LEFT)
522
+
523
+ self.ollama_indicator = tk.Label(
524
+ self.ollama_frame,
525
+ text="\u25cf",
526
+ font=("Segoe UI", 12),
527
+ bg=COLORS["panel"],
528
+ fg=COLORS["error"]
529
+ )
530
+ self.ollama_indicator.pack(side=tk.LEFT, padx=(5, 3))
531
+
532
+ self.ollama_status = tk.Label(
533
+ self.ollama_frame,
534
+ text="Stopped",
535
+ font=("Segoe UI", 10),
536
+ bg=COLORS["panel"],
537
+ fg=COLORS["text_dim"]
538
+ )
539
+ self.ollama_status.pack(side=tk.LEFT)
540
+
541
+ # Control buttons panel
542
+ buttons_frame = tk.Frame(main_frame, bg=COLORS["bg"])
543
+ buttons_frame.pack(fill=tk.X, pady=(0, 15))
544
+
545
+ # Create styled buttons
546
+ button_config = {
547
+ "font": ("Segoe UI", 11),
548
+ "width": 11,
549
+ "cursor": "hand2",
550
+ "relief": tk.FLAT,
551
+ "bd": 0,
552
+ }
553
+
554
+ self.start_btn = tk.Button(
555
+ buttons_frame,
556
+ text="\u25b6 Start",
557
+ bg=COLORS["button_bg"],
558
+ fg=COLORS["text"],
559
+ activebackground=COLORS["button_hover"],
560
+ activeforeground=COLORS["text"],
561
+ disabledforeground=COLORS["disabled_fg"],
562
+ command=self.start_agent,
563
+ **button_config
564
+ )
565
+ self.start_btn.pack(side=tk.LEFT, padx=(0, 10), ipady=8)
566
+ self._add_hover_effect(self.start_btn)
567
+
568
+ self.stop_btn = tk.Button(
569
+ buttons_frame,
570
+ text="\u25a0 Stop",
571
+ bg=COLORS["button_bg"],
572
+ fg=COLORS["text"],
573
+ activebackground=COLORS["button_hover"],
574
+ activeforeground=COLORS["text"],
575
+ disabledforeground=COLORS["disabled_fg"],
576
+ command=self.stop_agent,
577
+ **button_config
578
+ )
579
+ self.stop_btn.pack(side=tk.LEFT, padx=(0, 10), ipady=8)
580
+ self._add_hover_effect(self.stop_btn)
581
+
582
+ self.restart_btn = tk.Button(
583
+ buttons_frame,
584
+ text="\u21bb Restart",
585
+ bg=COLORS["button_bg"],
586
+ fg=COLORS["text"],
587
+ activebackground=COLORS["button_hover"],
588
+ activeforeground=COLORS["text"],
589
+ disabledforeground=COLORS["disabled_fg"],
590
+ command=self.restart_agent,
591
+ **button_config
592
+ )
593
+ self.restart_btn.pack(side=tk.LEFT, ipady=8)
594
+ self._add_hover_effect(self.restart_btn)
595
+
596
+ # Dashboard button
597
+ dashboard_frame = tk.Frame(main_frame, bg=COLORS["bg"])
598
+ dashboard_frame.pack(fill=tk.X, pady=(0, 15))
599
+
600
+ self.dashboard_btn = tk.Button(
601
+ dashboard_frame,
602
+ text="\U0001F4CA Open Dashboard",
603
+ font=("Segoe UI", 12),
604
+ bg=COLORS["accent"],
605
+ fg=COLORS["text"],
606
+ activebackground=COLORS["button_hover"],
607
+ activeforeground=COLORS["text"],
608
+ cursor="hand2",
609
+ relief=tk.FLAT,
610
+ bd=0,
611
+ command=self.open_dashboard
612
+ )
613
+ self.dashboard_btn.pack(fill=tk.X, ipady=12)
614
+ self._add_hover_effect(self.dashboard_btn)
615
+
616
+ # Settings panel
617
+ settings_frame = tk.Frame(main_frame, bg=COLORS["panel"], padx=15, pady=15)
618
+ settings_frame.pack(fill=tk.X, pady=(0, 15))
619
+
620
+ settings_title = tk.Label(
621
+ settings_frame,
622
+ text="Settings",
623
+ font=("Segoe UI", 11, "bold"),
624
+ bg=COLORS["panel"],
625
+ fg=COLORS["text"]
626
+ )
627
+ settings_title.pack(anchor=tk.W, pady=(0, 10))
628
+
629
+ # Checkbox styling
630
+ checkbox_config = {
631
+ "font": ("Segoe UI", 10),
632
+ "bg": COLORS["panel"],
633
+ "fg": COLORS["text"],
634
+ "selectcolor": COLORS["accent"],
635
+ "activebackground": COLORS["panel"],
636
+ "activeforeground": COLORS["text"],
637
+ "cursor": "hand2",
638
+ }
639
+
640
+ # Run on startup checkbox
641
+ self.startup_var = tk.BooleanVar(value=self.settings.run_on_startup)
642
+ self.startup_check = tk.Checkbutton(
643
+ settings_frame,
644
+ text="Run on Windows Startup",
645
+ variable=self.startup_var,
646
+ command=self.toggle_startup,
647
+ **checkbox_config
648
+ )
649
+ self.startup_check.pack(anchor=tk.W)
650
+
651
+ # Auto-start agent checkbox
652
+ self.autostart_var = tk.BooleanVar(value=self.settings.auto_start_agent)
653
+ self.autostart_check = tk.Checkbutton(
654
+ settings_frame,
655
+ text="Auto-start agent when manager launches",
656
+ variable=self.autostart_var,
657
+ command=self.toggle_autostart,
658
+ **checkbox_config
659
+ )
660
+ self.autostart_check.pack(anchor=tk.W, pady=(5, 0))
661
+
662
+ # Start minimized checkbox
663
+ self.start_minimized_var = tk.BooleanVar(value=self.settings.start_minimized)
664
+ self.start_minimized_check = tk.Checkbutton(
665
+ settings_frame,
666
+ text="Start minimized to taskbar",
667
+ variable=self.start_minimized_var,
668
+ command=self.toggle_start_minimized,
669
+ **checkbox_config
670
+ )
671
+ self.start_minimized_check.pack(anchor=tk.W, pady=(5, 0))
672
+
673
+ # Minimize to tray checkbox
674
+ self.minimize_tray_var = tk.BooleanVar(value=self.settings.minimize_to_tray)
675
+ self.minimize_tray_check = tk.Checkbutton(
676
+ settings_frame,
677
+ text="Keep agent running when window is closed",
678
+ variable=self.minimize_tray_var,
679
+ command=self.toggle_minimize_tray,
680
+ **checkbox_config
681
+ )
682
+ self.minimize_tray_check.pack(anchor=tk.W, pady=(5, 0))
683
+
684
+ # Logs section
685
+ logs_header = tk.Frame(main_frame, bg=COLORS["bg"])
686
+ logs_header.pack(fill=tk.X, pady=(0, 5))
687
+
688
+ logs_label = tk.Label(
689
+ logs_header,
690
+ text="Logs",
691
+ font=("Segoe UI", 11, "bold"),
692
+ bg=COLORS["bg"],
693
+ fg=COLORS["text"]
694
+ )
695
+ logs_label.pack(side=tk.LEFT)
696
+
697
+ # Clear logs button (smaller, next to title)
698
+ clear_btn = tk.Button(
699
+ logs_header,
700
+ text="Clear",
701
+ font=("Segoe UI", 9),
702
+ bg=COLORS["accent"],
703
+ fg=COLORS["text"],
704
+ activebackground=COLORS["button_hover"],
705
+ activeforeground=COLORS["text"],
706
+ cursor="hand2",
707
+ relief=tk.FLAT,
708
+ bd=0,
709
+ command=self.clear_logs
710
+ )
711
+ clear_btn.pack(side=tk.RIGHT, ipadx=8, ipady=2)
712
+ self._add_hover_effect(clear_btn)
713
+
714
+ # Log viewer with border
715
+ log_frame = tk.Frame(main_frame, bg=COLORS["border"], padx=1, pady=1)
716
+ log_frame.pack(fill=tk.BOTH, expand=True)
717
+
718
+ self.log_text = scrolledtext.ScrolledText(
719
+ log_frame,
720
+ font=("Consolas", 9),
721
+ bg=COLORS["panel"],
722
+ fg=COLORS["text"],
723
+ insertbackground=COLORS["text"],
724
+ selectbackground=COLORS["accent"],
725
+ wrap=tk.WORD,
726
+ state=tk.DISABLED,
727
+ padx=10,
728
+ pady=10
729
+ )
730
+ self.log_text.pack(fill=tk.BOTH, expand=True)
731
+
732
+ # Configure log text tags for coloring
733
+ self.log_text.tag_configure("info", foreground=COLORS["text"])
734
+ self.log_text.tag_configure("warning", foreground=COLORS["warning"])
735
+ self.log_text.tag_configure("error", foreground=COLORS["error"])
736
+ self.log_text.tag_configure("success", foreground=COLORS["success"])
737
+ self.log_text.tag_configure("dim", foreground=COLORS["text_dim"])
738
+
739
+ # Footer with info
740
+ footer = tk.Label(
741
+ main_frame,
742
+ text=f"Port: {PORT} | Dashboard: {DASHBOARD_URL}",
743
+ font=("Segoe UI", 9),
744
+ bg=COLORS["bg"],
745
+ fg=COLORS["text_dim"]
746
+ )
747
+ footer.pack(pady=(10, 0))
748
+
749
+ def _add_hover_effect(self, button):
750
+ """Add hover effect to a button."""
751
+ original_bg = button.cget("bg")
752
+
753
+ def on_enter(e):
754
+ if str(button.cget("state")) != "disabled":
755
+ button.configure(bg=COLORS["button_hover"])
756
+
757
+ def on_leave(e):
758
+ if str(button.cget("state")) != "disabled":
759
+ button.configure(bg=original_bg)
760
+
761
+ button.bind("<Enter>", on_enter)
762
+ button.bind("<Leave>", on_leave)
763
+
764
+ def log(self, message: str, tag: str = "info"):
765
+ """Add a message to the log viewer."""
766
+ timestamp = time.strftime("%H:%M:%S")
767
+ self.log_text.configure(state=tk.NORMAL)
768
+ self.log_text.insert(tk.END, f"[{timestamp}] {message}\n", tag)
769
+ self.log_text.see(tk.END)
770
+ self.log_text.configure(state=tk.DISABLED)
771
+
772
+ def clear_logs(self):
773
+ """Clear the log viewer."""
774
+ self.log_text.configure(state=tk.NORMAL)
775
+ self.log_text.delete(1.0, tk.END)
776
+ self.log_text.configure(state=tk.DISABLED)
777
+ self.log("Logs cleared", "dim")
778
+
779
+ def update_status(self):
780
+ """Update the status indicators."""
781
+ is_running = self.process_manager.is_running()
782
+ ollama_running = check_ollama_running()
783
+
784
+ # Update agent status
785
+ if is_running:
786
+ self.status_indicator.configure(fg=COLORS["success"])
787
+ self.status_text.configure(text="Running", fg=COLORS["success"])
788
+ self.start_btn.configure(state=tk.DISABLED)
789
+ self.stop_btn.configure(state=tk.NORMAL)
790
+ self.restart_btn.configure(state=tk.NORMAL)
791
+ else:
792
+ self.status_indicator.configure(fg=COLORS["error"])
793
+ self.status_text.configure(text="Stopped", fg=COLORS["error"])
794
+ self.start_btn.configure(state=tk.NORMAL)
795
+ self.stop_btn.configure(state=tk.DISABLED)
796
+ self.restart_btn.configure(state=tk.DISABLED)
797
+
798
+ # Update Ollama status
799
+ if ollama_running:
800
+ self.ollama_indicator.configure(fg=COLORS["success"])
801
+ self.ollama_status.configure(text="Running", fg=COLORS["success"])
802
+ else:
803
+ self.ollama_indicator.configure(fg=COLORS["warning"])
804
+ self.ollama_status.configure(text="Stopped", fg=COLORS["warning"])
805
+
806
+ # Schedule next update
807
+ self.root.after(2000, self.update_status)
808
+
809
+ def start_log_updater(self):
810
+ """Start the log updater loop."""
811
+ def update_logs():
812
+ logs = self.process_manager.get_logs()
813
+ for line in logs:
814
+ # Determine log level for coloring
815
+ tag = "info"
816
+ line_lower = line.lower()
817
+ if "error" in line_lower or "exception" in line_lower or "traceback" in line_lower:
818
+ tag = "error"
819
+ elif "warning" in line_lower or "warn" in line_lower:
820
+ tag = "warning"
821
+ elif "started" in line_lower or "success" in line_lower or "ready" in line_lower:
822
+ tag = "success"
823
+ elif "info:" in line_lower or "debug:" in line_lower:
824
+ tag = "dim"
825
+
826
+ self.log(line, tag)
827
+
828
+ self.root.after(100, update_logs)
829
+
830
+ update_logs()
831
+
832
+ def sync_startup_checkbox(self):
833
+ """Sync the startup checkbox with actual registry state."""
834
+ is_registered = StartupManager.is_registered()
835
+ self.startup_var.set(is_registered)
836
+ self.settings.run_on_startup = is_registered
837
+
838
+ def start_agent(self):
839
+ """Start the Memory Agent."""
840
+ if not check_ollama_running():
841
+ result = messagebox.askyesno(
842
+ "Ollama Not Running",
843
+ "Ollama is not running. The Memory Agent requires Ollama for embeddings.\n\n"
844
+ "Do you want to start the agent anyway?",
845
+ parent=self.root
846
+ )
847
+ if not result:
848
+ return
849
+
850
+ self.log("Starting Memory Agent...", "info")
851
+
852
+ # Run in thread to avoid blocking UI
853
+ def start_thread():
854
+ success, message = self.process_manager.start()
855
+ self.root.after(0, lambda: self.log(message, "success" if success else "error"))
856
+
857
+ threading.Thread(target=start_thread, daemon=True).start()
858
+
859
+ def stop_agent(self):
860
+ """Stop the Memory Agent."""
861
+ self.log("Stopping Memory Agent...", "info")
862
+
863
+ def stop_thread():
864
+ success, message = self.process_manager.stop()
865
+ self.root.after(0, lambda: self.log(message, "success" if success else "error"))
866
+
867
+ threading.Thread(target=stop_thread, daemon=True).start()
868
+
869
+ def restart_agent(self):
870
+ """Restart the Memory Agent."""
871
+ self.log("Restarting Memory Agent...", "info")
872
+
873
+ def restart_thread():
874
+ success, message = self.process_manager.restart()
875
+ self.root.after(0, lambda: self.log(message, "success" if success else "error"))
876
+
877
+ threading.Thread(target=restart_thread, daemon=True).start()
878
+
879
+ def open_dashboard(self):
880
+ """Open the dashboard in the default browser."""
881
+ if not self.process_manager.is_running():
882
+ result = messagebox.askyesno(
883
+ "Agent Not Running",
884
+ "The Memory Agent is not running.\n\nWould you like to start it first?",
885
+ parent=self.root
886
+ )
887
+ if result:
888
+ self.start_agent()
889
+ # Wait for agent to start, then open dashboard
890
+ self.root.after(3000, lambda: webbrowser.open(DASHBOARD_URL))
891
+ self.log("Will open dashboard after agent starts...", "info")
892
+ return
893
+ else:
894
+ # Open anyway (might show error page)
895
+ pass
896
+
897
+ webbrowser.open(DASHBOARD_URL)
898
+ self.log(f"Opened dashboard in browser", "info")
899
+
900
+ def toggle_startup(self):
901
+ """Toggle run on Windows startup."""
902
+ enabled = self.startup_var.get()
903
+
904
+ if enabled:
905
+ if StartupManager.register():
906
+ self.settings.run_on_startup = True
907
+ self.log("Added to Windows startup", "success")
908
+ else:
909
+ self.startup_var.set(False)
910
+ self.log("Failed to add to Windows startup", "error")
911
+ else:
912
+ if StartupManager.unregister():
913
+ self.settings.run_on_startup = False
914
+ self.log("Removed from Windows startup", "info")
915
+ else:
916
+ self.startup_var.set(True)
917
+ self.log("Failed to remove from Windows startup", "error")
918
+
919
+ self.settings.save()
920
+
921
+ def toggle_autostart(self):
922
+ """Toggle auto-start agent on launch."""
923
+ self.settings.auto_start_agent = self.autostart_var.get()
924
+ self.settings.save()
925
+ status = "enabled" if self.settings.auto_start_agent else "disabled"
926
+ self.log(f"Auto-start agent {status}", "info")
927
+
928
+ def toggle_start_minimized(self):
929
+ """Toggle start minimized setting."""
930
+ self.settings.start_minimized = self.start_minimized_var.get()
931
+ self.settings.save()
932
+ status = "enabled" if self.settings.start_minimized else "disabled"
933
+ self.log(f"Start minimized {status}", "info")
934
+
935
+ def toggle_minimize_tray(self):
936
+ """Toggle minimize to tray setting."""
937
+ self.settings.minimize_to_tray = self.minimize_tray_var.get()
938
+ self.settings.save()
939
+ status = "enabled" if self.settings.minimize_to_tray else "disabled"
940
+ self.log(f"Keep running on close {status}", "info")
941
+
942
+ def minimize_window(self):
943
+ """Minimize the window."""
944
+ self.root.iconify()
945
+ self.is_minimized = True
946
+
947
+ def on_minimize(self, event):
948
+ """Handle window minimize event."""
949
+ if event.widget == self.root:
950
+ self.is_minimized = True
951
+
952
+ def on_close(self):
953
+ """Handle window close."""
954
+ if self.settings.minimize_to_tray and self.process_manager.is_running():
955
+ # Just hide the window, keep agent running
956
+ self.root.iconify()
957
+ self.is_minimized = True
958
+ self.log("Window minimized. Agent continues running.", "dim")
959
+ else:
960
+ # Actually close
961
+ if self.process_manager.is_running():
962
+ result = messagebox.askyesnocancel(
963
+ "Exit Confirmation",
964
+ "The Memory Agent is still running.\n\n"
965
+ "Yes = Stop agent and exit\n"
966
+ "No = Exit but keep agent running\n"
967
+ "Cancel = Don't exit",
968
+ parent=self.root
969
+ )
970
+ if result is True:
971
+ self.log("Stopping agent and exiting...", "info")
972
+ self.process_manager.stop()
973
+ self.root.destroy()
974
+ elif result is False:
975
+ self.log("Exiting manager (agent will continue running)", "info")
976
+ self.root.destroy()
977
+ # If result is None (Cancel), do nothing
978
+ else:
979
+ self.root.destroy()
980
+
981
+ def run(self):
982
+ """Run the application."""
983
+ self.root.mainloop()
984
+
985
+
986
+ def main():
987
+ """Main entry point."""
988
+ # Ensure we're in the right directory
989
+ os.chdir(AGENT_DIR)
990
+
991
+ # Create and run the application
992
+ app = ClaudeMemoryManagerApp()
993
+ app.run()
994
+
995
+
996
+ if __name__ == "__main__":
997
+ main()