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.
- package/.env.example +107 -0
- package/README.md +200 -0
- package/agent_card.py +512 -0
- package/bin/cli.js +181 -0
- package/bin/postinstall.js +216 -0
- package/config.py +104 -0
- package/dashboard.html +2689 -0
- package/hooks/README.md +196 -0
- 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/hooks/auto-detect-response.py +348 -0
- package/hooks/auto_capture.py +255 -0
- package/hooks/detect-correction.py +173 -0
- package/hooks/grounding-hook.py +348 -0
- package/hooks/log-tool-use.py +234 -0
- package/hooks/log-user-request.py +208 -0
- package/hooks/pre-tool-decision.py +218 -0
- package/hooks/problem-detector.py +343 -0
- package/hooks/session_end.py +192 -0
- package/hooks/session_start.py +227 -0
- package/install.py +887 -0
- package/main.py +2859 -0
- package/manager.py +997 -0
- package/package.json +55 -0
- package/requirements.txt +8 -0
- package/run_server.py +136 -0
- package/services/__init__.py +50 -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/services/agent_registry.py +753 -0
- package/services/auth.py +331 -0
- package/services/auto_inject.py +250 -0
- package/services/claude_md_sync.py +275 -0
- package/services/cleanup.py +667 -0
- package/services/compaction_flush.py +447 -0
- package/services/confidence.py +301 -0
- package/services/daily_log.py +333 -0
- package/services/database.py +2485 -0
- package/services/embeddings.py +358 -0
- package/services/insights.py +632 -0
- package/services/llm_analyzer.py +595 -0
- package/services/memory_md_sync.py +409 -0
- package/services/retry_queue.py +453 -0
- package/services/timeline.py +579 -0
- package/services/vector_index.py +398 -0
- package/services/websocket.py +257 -0
- package/skills/__init__.py +6 -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/skills/admin.py +469 -0
- package/skills/checkpoint.py +198 -0
- package/skills/claude_md.py +363 -0
- package/skills/cleanup.py +241 -0
- package/skills/grounding.py +801 -0
- package/skills/insights.py +231 -0
- package/skills/natural_language.py +277 -0
- package/skills/retrieve.py +67 -0
- package/skills/search.py +213 -0
- package/skills/state.py +182 -0
- package/skills/store.py +179 -0
- package/skills/summarize.py +588 -0
- package/skills/timeline.py +387 -0
- package/skills/verification.py +391 -0
- package/start_daemon.py +155 -0
- package/test_automation.py +221 -0
- package/test_complete.py +338 -0
- package/test_full.py +322 -0
- package/update_system.py +817 -0
- 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()
|