claude-mpm 4.2.1__py3-none-any.whl → 4.2.3__py3-none-any.whl
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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/agent-manager.json +1 -1
- claude_mpm/agents/templates/agentic_coder_optimizer.json +1 -1
- claude_mpm/agents/templates/api_qa.json +1 -1
- claude_mpm/agents/templates/code_analyzer.json +1 -1
- claude_mpm/agents/templates/data_engineer.json +1 -1
- claude_mpm/agents/templates/documentation.json +1 -1
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/gcp_ops_agent.json +14 -9
- claude_mpm/agents/templates/imagemagick.json +1 -1
- claude_mpm/agents/templates/memory_manager.json +1 -1
- claude_mpm/agents/templates/ops.json +1 -1
- claude_mpm/agents/templates/project_organizer.json +1 -1
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +1 -1
- claude_mpm/agents/templates/research.json +3 -3
- claude_mpm/agents/templates/security.json +1 -1
- claude_mpm/agents/templates/test-non-mpm.json +20 -0
- claude_mpm/agents/templates/ticketing.json +1 -1
- claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
- claude_mpm/agents/templates/version_control.json +1 -1
- claude_mpm/agents/templates/web_qa.json +3 -8
- claude_mpm/agents/templates/web_ui.json +1 -1
- claude_mpm/cli/commands/agents.py +3 -0
- claude_mpm/cli/commands/dashboard.py +3 -3
- claude_mpm/cli/commands/monitor.py +227 -64
- claude_mpm/core/config.py +25 -0
- claude_mpm/core/unified_agent_registry.py +2 -2
- claude_mpm/dashboard/static/css/code-tree.css +220 -1
- claude_mpm/dashboard/static/css/dashboard.css +286 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +1 -1
- claude_mpm/dashboard/static/js/components/code-simple.js +507 -15
- claude_mpm/dashboard/static/js/components/code-tree.js +2044 -124
- claude_mpm/dashboard/static/js/socket-client.js +5 -2
- claude_mpm/dashboard/templates/code_simple.html +79 -0
- claude_mpm/dashboard/templates/index.html +42 -41
- claude_mpm/services/agents/deployment/agent_deployment.py +4 -1
- claude_mpm/services/agents/deployment/agent_discovery_service.py +101 -2
- claude_mpm/services/agents/deployment/agent_format_converter.py +53 -9
- claude_mpm/services/agents/deployment/agent_template_builder.py +355 -25
- claude_mpm/services/agents/deployment/agent_validator.py +11 -6
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +83 -15
- claude_mpm/services/agents/deployment/validation/template_validator.py +51 -40
- claude_mpm/services/cli/agent_listing_service.py +2 -2
- claude_mpm/services/dashboard/stable_server.py +389 -0
- claude_mpm/services/socketio/client_proxy.py +16 -0
- claude_mpm/services/socketio/dashboard_server.py +360 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +27 -5
- claude_mpm/services/socketio/monitor_client.py +366 -0
- claude_mpm/services/socketio/monitor_server.py +505 -0
- claude_mpm/tools/code_tree_analyzer.py +95 -17
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/RECORD +57 -52
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.2.1.dist-info → claude_mpm-4.2.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight MonitorServer for claude-mpm.
|
|
3
|
+
|
|
4
|
+
WHY: This module provides a minimal, independent monitoring service that:
|
|
5
|
+
- Runs as a stable background service on port 8765
|
|
6
|
+
- Only handles event collection and relay (no UI components)
|
|
7
|
+
- Has minimal dependencies and resource usage
|
|
8
|
+
- Can run as always-on background service
|
|
9
|
+
- Includes event buffering capabilities
|
|
10
|
+
- Acts as a bridge between hooks and dashboard(s)
|
|
11
|
+
|
|
12
|
+
DESIGN DECISIONS:
|
|
13
|
+
- Minimal Socket.IO server with only essential features
|
|
14
|
+
- Event buffering for reliable delivery to dashboard clients
|
|
15
|
+
- Independent lifecycle from dashboard service
|
|
16
|
+
- Configurable port with sensible defaults
|
|
17
|
+
- Health monitoring and status endpoints
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from collections import deque
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from typing import Any, Dict, List, Optional, Set
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import socketio
|
|
29
|
+
from aiohttp import web
|
|
30
|
+
|
|
31
|
+
SOCKETIO_AVAILABLE = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
SOCKETIO_AVAILABLE = False
|
|
34
|
+
socketio = None
|
|
35
|
+
web = None
|
|
36
|
+
|
|
37
|
+
from ...core.config import Config
|
|
38
|
+
from ...core.constants import SystemLimits
|
|
39
|
+
from ...core.logging_config import get_logger
|
|
40
|
+
from ..core.interfaces.communication import SocketIOServiceInterface
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MonitorServer(SocketIOServiceInterface):
|
|
44
|
+
"""Lightweight Socket.IO server for monitoring and event relay.
|
|
45
|
+
|
|
46
|
+
WHY: This server acts as a stable, lightweight background service that:
|
|
47
|
+
- Collects events from hooks and other system components
|
|
48
|
+
- Buffers events for reliable delivery
|
|
49
|
+
- Relays events to connected dashboard clients
|
|
50
|
+
- Maintains minimal resource footprint
|
|
51
|
+
- Can run independently of dashboard services
|
|
52
|
+
|
|
53
|
+
This separation allows the monitor to be a stable always-on service
|
|
54
|
+
while dashboards can come and go without affecting event collection.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, host: str = None, port: int = None):
|
|
58
|
+
# Load configuration
|
|
59
|
+
config = Config()
|
|
60
|
+
monitor_config = config.get("monitor_server", {})
|
|
61
|
+
|
|
62
|
+
self.host = host or monitor_config.get("host", "localhost")
|
|
63
|
+
self.port = port or monitor_config.get("port", 8765)
|
|
64
|
+
self.logger = get_logger(__name__ + ".MonitorServer")
|
|
65
|
+
|
|
66
|
+
# Configuration-based settings
|
|
67
|
+
self.event_buffer_size = monitor_config.get(
|
|
68
|
+
"event_buffer_size", SystemLimits.MAX_EVENTS_BUFFER * 2
|
|
69
|
+
)
|
|
70
|
+
self.client_timeout = monitor_config.get("client_timeout", 60)
|
|
71
|
+
self.health_monitoring_enabled = monitor_config.get(
|
|
72
|
+
"enable_health_monitoring", True
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Server state
|
|
76
|
+
self.running = False
|
|
77
|
+
self.sio = None
|
|
78
|
+
self.app = None
|
|
79
|
+
self.runner = None
|
|
80
|
+
self.site = None
|
|
81
|
+
self.thread = None
|
|
82
|
+
self.loop = None
|
|
83
|
+
|
|
84
|
+
# Client management
|
|
85
|
+
self.connected_clients: Set[str] = set()
|
|
86
|
+
self.client_info: Dict[str, Dict[str, Any]] = {}
|
|
87
|
+
|
|
88
|
+
# Event buffering - configurable buffer size for monitor server
|
|
89
|
+
self.event_buffer = deque(maxlen=self.event_buffer_size)
|
|
90
|
+
self.buffer_lock = threading.Lock()
|
|
91
|
+
|
|
92
|
+
# Statistics
|
|
93
|
+
self.stats = {
|
|
94
|
+
"events_received": 0,
|
|
95
|
+
"events_relayed": 0,
|
|
96
|
+
"events_buffered": 0,
|
|
97
|
+
"connections_total": 0,
|
|
98
|
+
"start_time": None,
|
|
99
|
+
"clients_connected": 0,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Session tracking for compatibility
|
|
103
|
+
self.session_id = None
|
|
104
|
+
self.claude_status = "unknown"
|
|
105
|
+
self.claude_pid = None
|
|
106
|
+
self.active_sessions: Dict[str, Dict[str, Any]] = {}
|
|
107
|
+
|
|
108
|
+
def start_sync(self):
|
|
109
|
+
"""Start the monitor server in a background thread."""
|
|
110
|
+
if not SOCKETIO_AVAILABLE:
|
|
111
|
+
self.logger.error("Socket.IO not available - monitor server cannot start")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
if self.running:
|
|
115
|
+
self.logger.info("Monitor server already running")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
self.logger.info(
|
|
119
|
+
f"Starting lightweight monitor server on {self.host}:{self.port}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Start server in background thread
|
|
123
|
+
self.thread = threading.Thread(target=self._run_server, daemon=True)
|
|
124
|
+
self.thread.start()
|
|
125
|
+
|
|
126
|
+
# Wait for server to start
|
|
127
|
+
max_wait = 10.0
|
|
128
|
+
wait_interval = 0.1
|
|
129
|
+
waited = 0.0
|
|
130
|
+
|
|
131
|
+
while not self.running and waited < max_wait:
|
|
132
|
+
time.sleep(wait_interval)
|
|
133
|
+
waited += wait_interval
|
|
134
|
+
|
|
135
|
+
if self.running:
|
|
136
|
+
self.stats["start_time"] = datetime.now().isoformat()
|
|
137
|
+
self.logger.info(
|
|
138
|
+
f"Monitor server started successfully on {self.host}:{self.port}"
|
|
139
|
+
)
|
|
140
|
+
return True
|
|
141
|
+
self.logger.error(f"Monitor server failed to start within {max_wait}s")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def stop_sync(self):
|
|
145
|
+
"""Stop the monitor server."""
|
|
146
|
+
if not self.running:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self.logger.info("Stopping monitor server...")
|
|
150
|
+
self.running = False
|
|
151
|
+
|
|
152
|
+
# Stop the server
|
|
153
|
+
if self.loop and self.runner:
|
|
154
|
+
try:
|
|
155
|
+
# Schedule cleanup in the event loop
|
|
156
|
+
asyncio.run_coroutine_threadsafe(self._stop_server(), self.loop)
|
|
157
|
+
|
|
158
|
+
# Wait for thread to finish
|
|
159
|
+
if self.thread and self.thread.is_alive():
|
|
160
|
+
self.thread.join(timeout=5)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
self.logger.error(f"Error stopping monitor server: {e}")
|
|
164
|
+
|
|
165
|
+
self.logger.info("Monitor server stopped")
|
|
166
|
+
|
|
167
|
+
def _run_server(self):
|
|
168
|
+
"""Run the server in its own event loop."""
|
|
169
|
+
try:
|
|
170
|
+
# Create new event loop for this thread
|
|
171
|
+
self.loop = asyncio.new_event_loop()
|
|
172
|
+
asyncio.set_event_loop(self.loop)
|
|
173
|
+
|
|
174
|
+
# Start the server
|
|
175
|
+
self.loop.run_until_complete(self._start_server())
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self.logger.error(f"Error in monitor server thread: {e}")
|
|
179
|
+
self.running = False
|
|
180
|
+
|
|
181
|
+
async def _start_server(self):
|
|
182
|
+
"""Start the Socket.IO server with minimal configuration."""
|
|
183
|
+
try:
|
|
184
|
+
# Create Socket.IO server with minimal configuration
|
|
185
|
+
self.sio = socketio.AsyncServer(
|
|
186
|
+
cors_allowed_origins="*",
|
|
187
|
+
logger=False, # Minimize logging overhead
|
|
188
|
+
engineio_logger=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Create minimal aiohttp application
|
|
192
|
+
self.app = web.Application()
|
|
193
|
+
self.sio.attach(self.app)
|
|
194
|
+
|
|
195
|
+
# Register minimal event handlers
|
|
196
|
+
self._register_events()
|
|
197
|
+
|
|
198
|
+
# Add health check endpoint
|
|
199
|
+
self.app.router.add_get("/health", self._health_check)
|
|
200
|
+
self.app.router.add_get("/status", self._status_check)
|
|
201
|
+
|
|
202
|
+
# Start the HTTP server
|
|
203
|
+
self.runner = web.AppRunner(self.app)
|
|
204
|
+
await self.runner.setup()
|
|
205
|
+
|
|
206
|
+
self.site = web.TCPSite(
|
|
207
|
+
self.runner, self.host, self.port, shutdown_timeout=1.0 # Fast shutdown
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
await self.site.start()
|
|
211
|
+
self.running = True
|
|
212
|
+
|
|
213
|
+
self.logger.info(f"Monitor server running on {self.host}:{self.port}")
|
|
214
|
+
|
|
215
|
+
# Keep the server running
|
|
216
|
+
while self.running:
|
|
217
|
+
await asyncio.sleep(1)
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
self.logger.error(f"Failed to start monitor server: {e}")
|
|
221
|
+
self.running = False
|
|
222
|
+
|
|
223
|
+
async def _stop_server(self):
|
|
224
|
+
"""Stop the server components."""
|
|
225
|
+
try:
|
|
226
|
+
if self.site:
|
|
227
|
+
await self.site.stop()
|
|
228
|
+
if self.runner:
|
|
229
|
+
await self.runner.cleanup()
|
|
230
|
+
except Exception as e:
|
|
231
|
+
self.logger.error(f"Error stopping server components: {e}")
|
|
232
|
+
|
|
233
|
+
def _register_events(self):
|
|
234
|
+
"""Register minimal Socket.IO events for monitoring."""
|
|
235
|
+
|
|
236
|
+
@self.sio.event
|
|
237
|
+
async def connect(sid, environ):
|
|
238
|
+
"""Handle client connection."""
|
|
239
|
+
self.connected_clients.add(sid)
|
|
240
|
+
self.client_info[sid] = {
|
|
241
|
+
"connected_at": datetime.now().isoformat(),
|
|
242
|
+
"client_type": "dashboard", # Assume dashboard clients
|
|
243
|
+
}
|
|
244
|
+
self.stats["connections_total"] += 1
|
|
245
|
+
self.stats["clients_connected"] = len(self.connected_clients)
|
|
246
|
+
|
|
247
|
+
self.logger.info(f"Dashboard client connected: {sid}")
|
|
248
|
+
|
|
249
|
+
# Send buffered events to new client
|
|
250
|
+
await self._send_buffered_events(sid)
|
|
251
|
+
|
|
252
|
+
@self.sio.event
|
|
253
|
+
async def disconnect(sid):
|
|
254
|
+
"""Handle client disconnection."""
|
|
255
|
+
self.connected_clients.discard(sid)
|
|
256
|
+
self.client_info.pop(sid, None)
|
|
257
|
+
self.stats["clients_connected"] = len(self.connected_clients)
|
|
258
|
+
|
|
259
|
+
self.logger.info(f"Dashboard client disconnected: {sid}")
|
|
260
|
+
|
|
261
|
+
@self.sio.event
|
|
262
|
+
async def get_status(sid):
|
|
263
|
+
"""Handle status request from client."""
|
|
264
|
+
status_data = {
|
|
265
|
+
"server_type": "monitor",
|
|
266
|
+
"running": self.running,
|
|
267
|
+
"port": self.port,
|
|
268
|
+
"connected_clients": len(self.connected_clients),
|
|
269
|
+
"stats": self.stats,
|
|
270
|
+
"active_sessions": list(self.active_sessions.values()),
|
|
271
|
+
}
|
|
272
|
+
await self.sio.emit("status_response", status_data, room=sid)
|
|
273
|
+
|
|
274
|
+
async def _send_buffered_events(self, client_id: str):
|
|
275
|
+
"""Send buffered events to a newly connected client."""
|
|
276
|
+
with self.buffer_lock:
|
|
277
|
+
if self.event_buffer:
|
|
278
|
+
self.logger.info(
|
|
279
|
+
f"Sending {len(self.event_buffer)} buffered events to {client_id}"
|
|
280
|
+
)
|
|
281
|
+
for event in list(self.event_buffer):
|
|
282
|
+
try:
|
|
283
|
+
await self.sio.emit(
|
|
284
|
+
event["type"], event["data"], room=client_id
|
|
285
|
+
)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
self.logger.error(
|
|
288
|
+
f"Error sending buffered event to {client_id}: {e}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def _health_check(self, request):
|
|
292
|
+
"""Health check endpoint."""
|
|
293
|
+
return web.json_response(
|
|
294
|
+
{
|
|
295
|
+
"status": "healthy" if self.running else "unhealthy",
|
|
296
|
+
"service": "monitor-server",
|
|
297
|
+
"port": self.port,
|
|
298
|
+
"clients": len(self.connected_clients),
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
async def _status_check(self, request):
|
|
303
|
+
"""Detailed status endpoint."""
|
|
304
|
+
return web.json_response(
|
|
305
|
+
{
|
|
306
|
+
"running": self.running,
|
|
307
|
+
"port": self.port,
|
|
308
|
+
"host": self.host,
|
|
309
|
+
"clients_connected": len(self.connected_clients),
|
|
310
|
+
"stats": self.stats,
|
|
311
|
+
"active_sessions": list(self.active_sessions.values()),
|
|
312
|
+
"buffer_size": len(self.event_buffer),
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# SocketIOServiceInterface implementation
|
|
317
|
+
def broadcast_event(self, event_type: str, data: Dict[str, Any]):
|
|
318
|
+
"""Broadcast an event to all connected dashboard clients."""
|
|
319
|
+
self.stats["events_received"] += 1
|
|
320
|
+
|
|
321
|
+
# Buffer the event
|
|
322
|
+
event_data = {
|
|
323
|
+
"type": event_type,
|
|
324
|
+
"data": data,
|
|
325
|
+
"timestamp": datetime.now().isoformat(),
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
with self.buffer_lock:
|
|
329
|
+
self.event_buffer.append(event_data)
|
|
330
|
+
self.stats["events_buffered"] += 1
|
|
331
|
+
|
|
332
|
+
# Relay to connected clients if server is running
|
|
333
|
+
if self.loop and self.sio and self.connected_clients:
|
|
334
|
+
asyncio.run_coroutine_threadsafe(
|
|
335
|
+
self._relay_event(event_type, data), self.loop
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
async def _relay_event(self, event_type: str, data: Dict[str, Any]):
|
|
339
|
+
"""Relay event to all connected dashboard clients."""
|
|
340
|
+
if not self.connected_clients:
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
await self.sio.emit(event_type, data)
|
|
345
|
+
self.stats["events_relayed"] += 1
|
|
346
|
+
except Exception as e:
|
|
347
|
+
self.logger.error(f"Error relaying event {event_type}: {e}")
|
|
348
|
+
|
|
349
|
+
def send_to_client(
|
|
350
|
+
self, client_id: str, event_type: str, data: Dict[str, Any]
|
|
351
|
+
) -> bool:
|
|
352
|
+
"""Send an event to a specific dashboard client."""
|
|
353
|
+
if not self.loop or client_id not in self.connected_clients:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
asyncio.run_coroutine_threadsafe(
|
|
358
|
+
self.sio.emit(event_type, data, room=client_id), self.loop
|
|
359
|
+
)
|
|
360
|
+
return True
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self.logger.error(f"Error sending to client {client_id}: {e}")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def get_connection_count(self) -> int:
|
|
366
|
+
"""Get number of connected dashboard clients."""
|
|
367
|
+
return len(self.connected_clients)
|
|
368
|
+
|
|
369
|
+
def is_running(self) -> bool:
|
|
370
|
+
"""Check if monitor server is running."""
|
|
371
|
+
return self.running
|
|
372
|
+
|
|
373
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
374
|
+
"""Get monitor server statistics."""
|
|
375
|
+
return {
|
|
376
|
+
**self.stats,
|
|
377
|
+
"clients_connected": len(self.connected_clients),
|
|
378
|
+
"buffer_size": len(self.event_buffer),
|
|
379
|
+
"uptime": (
|
|
380
|
+
(
|
|
381
|
+
datetime.now() - datetime.fromisoformat(self.stats["start_time"])
|
|
382
|
+
).total_seconds()
|
|
383
|
+
if self.stats["start_time"]
|
|
384
|
+
else 0
|
|
385
|
+
),
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Session tracking methods for compatibility with existing hooks
|
|
389
|
+
def session_started(self, session_id: str, launch_method: str, working_dir: str):
|
|
390
|
+
"""Track session start."""
|
|
391
|
+
self.session_id = session_id
|
|
392
|
+
self.active_sessions[session_id] = {
|
|
393
|
+
"session_id": session_id,
|
|
394
|
+
"start_time": datetime.now().isoformat(),
|
|
395
|
+
"agent": "pm",
|
|
396
|
+
"status": "active",
|
|
397
|
+
"launch_method": launch_method,
|
|
398
|
+
"working_dir": working_dir,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
self.broadcast_event(
|
|
402
|
+
"session_started",
|
|
403
|
+
{
|
|
404
|
+
"session_id": session_id,
|
|
405
|
+
"launch_method": launch_method,
|
|
406
|
+
"working_dir": working_dir,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def session_ended(self):
|
|
411
|
+
"""Track session end."""
|
|
412
|
+
if self.session_id and self.session_id in self.active_sessions:
|
|
413
|
+
session_data = self.active_sessions.pop(self.session_id)
|
|
414
|
+
self.broadcast_event("session_ended", {"session_id": self.session_id})
|
|
415
|
+
|
|
416
|
+
def claude_status_changed(
|
|
417
|
+
self, status: str, pid: Optional[int] = None, message: str = ""
|
|
418
|
+
):
|
|
419
|
+
"""Track Claude status changes."""
|
|
420
|
+
self.claude_status = status
|
|
421
|
+
self.claude_pid = pid
|
|
422
|
+
self.broadcast_event(
|
|
423
|
+
"claude_status", {"status": status, "pid": pid, "message": message}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def claude_output(self, content: str, stream: str = "stdout"):
|
|
427
|
+
"""Relay Claude output."""
|
|
428
|
+
self.broadcast_event("claude_output", {"content": content, "stream": stream})
|
|
429
|
+
|
|
430
|
+
def agent_delegated(self, agent: str, task: str, status: str = "started"):
|
|
431
|
+
"""Track agent delegation."""
|
|
432
|
+
if self.session_id and self.session_id in self.active_sessions:
|
|
433
|
+
self.active_sessions[self.session_id]["agent"] = agent
|
|
434
|
+
self.active_sessions[self.session_id]["status"] = status
|
|
435
|
+
|
|
436
|
+
self.broadcast_event(
|
|
437
|
+
"agent_delegated", {"agent": agent, "task": task, "status": status}
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
def todo_updated(self, todos: List[Dict[str, Any]]):
|
|
441
|
+
"""Relay todo updates."""
|
|
442
|
+
self.broadcast_event("todos_updated", {"todos": todos})
|
|
443
|
+
|
|
444
|
+
def ticket_created(self, ticket_id: str, title: str, priority: str = "medium"):
|
|
445
|
+
"""Relay ticket creation."""
|
|
446
|
+
self.broadcast_event(
|
|
447
|
+
"ticket_created",
|
|
448
|
+
{"ticket_id": ticket_id, "title": title, "priority": priority},
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def memory_loaded(self, agent_id: str, memory_size: int, sections_count: int):
|
|
452
|
+
"""Relay memory loaded event."""
|
|
453
|
+
self.broadcast_event(
|
|
454
|
+
"memory_loaded",
|
|
455
|
+
{
|
|
456
|
+
"agent_id": agent_id,
|
|
457
|
+
"memory_size": memory_size,
|
|
458
|
+
"sections_count": sections_count,
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def memory_created(self, agent_id: str, template_type: str):
|
|
463
|
+
"""Relay memory created event."""
|
|
464
|
+
self.broadcast_event(
|
|
465
|
+
"memory_created", {"agent_id": agent_id, "template_type": template_type}
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
def memory_updated(
|
|
469
|
+
self, agent_id: str, learning_type: str, content: str, section: str
|
|
470
|
+
):
|
|
471
|
+
"""Relay memory update event."""
|
|
472
|
+
self.broadcast_event(
|
|
473
|
+
"memory_updated",
|
|
474
|
+
{
|
|
475
|
+
"agent_id": agent_id,
|
|
476
|
+
"learning_type": learning_type,
|
|
477
|
+
"content": content,
|
|
478
|
+
"section": section,
|
|
479
|
+
},
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def memory_injected(self, agent_id: str, context_size: int):
|
|
483
|
+
"""Relay memory injection event."""
|
|
484
|
+
self.broadcast_event(
|
|
485
|
+
"memory_injected", {"agent_id": agent_id, "context_size": context_size}
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def get_active_sessions(self) -> List[Dict[str, Any]]:
|
|
489
|
+
"""Get list of active sessions."""
|
|
490
|
+
# Clean up old sessions (older than 1 hour)
|
|
491
|
+
cutoff_time = datetime.now().timestamp() - 3600
|
|
492
|
+
sessions_to_remove = []
|
|
493
|
+
|
|
494
|
+
for session_id, session_data in self.active_sessions.items():
|
|
495
|
+
try:
|
|
496
|
+
start_time = datetime.fromisoformat(session_data["start_time"])
|
|
497
|
+
if start_time.timestamp() < cutoff_time:
|
|
498
|
+
sessions_to_remove.append(session_id)
|
|
499
|
+
except:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
for session_id in sessions_to_remove:
|
|
503
|
+
del self.active_sessions[session_id]
|
|
504
|
+
|
|
505
|
+
return list(self.active_sessions.values())
|
|
@@ -500,6 +500,36 @@ class PythonAnalyzer:
|
|
|
500
500
|
)
|
|
501
501
|
)
|
|
502
502
|
|
|
503
|
+
def visit_Assign(self, node):
|
|
504
|
+
# Handle module-level variable assignments
|
|
505
|
+
if self.current_class is None: # Only module-level assignments
|
|
506
|
+
for target in node.targets:
|
|
507
|
+
if isinstance(target, ast.Name):
|
|
508
|
+
var_node = CodeNode(
|
|
509
|
+
file_path=str(file_path),
|
|
510
|
+
node_type="variable",
|
|
511
|
+
name=target.id,
|
|
512
|
+
line_start=node.lineno,
|
|
513
|
+
line_end=node.end_lineno or node.lineno,
|
|
514
|
+
parent=self.parent_name,
|
|
515
|
+
complexity=0,
|
|
516
|
+
signature=f"{target.id} = ...",
|
|
517
|
+
)
|
|
518
|
+
nodes.append(var_node)
|
|
519
|
+
|
|
520
|
+
# Emit event if emitter is available
|
|
521
|
+
if self.emitter:
|
|
522
|
+
self.emitter.emit_node(
|
|
523
|
+
CodeNodeEvent(
|
|
524
|
+
file_path=str(file_path),
|
|
525
|
+
node_type="variable",
|
|
526
|
+
name=target.id,
|
|
527
|
+
line_start=node.lineno,
|
|
528
|
+
line_end=node.end_lineno or node.lineno,
|
|
529
|
+
parent=self.parent_name,
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
503
533
|
def visit_AsyncFunctionDef(self, node):
|
|
504
534
|
self.visit_FunctionDef(node)
|
|
505
535
|
|
|
@@ -578,6 +608,24 @@ class PythonAnalyzer:
|
|
|
578
608
|
|
|
579
609
|
return nodes
|
|
580
610
|
|
|
611
|
+
def _get_assignment_signature(self, node: ast.Assign, var_name: str) -> str:
|
|
612
|
+
"""Get assignment signature string."""
|
|
613
|
+
try:
|
|
614
|
+
# Try to get a simple representation of the value
|
|
615
|
+
if isinstance(node.value, ast.Constant):
|
|
616
|
+
if isinstance(node.value.value, str):
|
|
617
|
+
return f'{var_name} = "{node.value.value}"'
|
|
618
|
+
return f"{var_name} = {node.value.value}"
|
|
619
|
+
if isinstance(node.value, ast.Name):
|
|
620
|
+
return f"{var_name} = {node.value.id}"
|
|
621
|
+
if isinstance(node.value, ast.List):
|
|
622
|
+
return f"{var_name} = [...]"
|
|
623
|
+
if isinstance(node.value, ast.Dict):
|
|
624
|
+
return f"{var_name} = {{...}}"
|
|
625
|
+
return f"{var_name} = ..."
|
|
626
|
+
except:
|
|
627
|
+
return f"{var_name} = ..."
|
|
628
|
+
|
|
581
629
|
|
|
582
630
|
class MultiLanguageAnalyzer:
|
|
583
631
|
"""Analyzes multiple programming languages using tree-sitter.
|
|
@@ -1625,26 +1673,56 @@ class CodeTreeAnalyzer:
|
|
|
1625
1673
|
|
|
1626
1674
|
self.emitter.emit_file_analyzed(file_path, filtered_nodes, duration)
|
|
1627
1675
|
|
|
1676
|
+
# Prepare the nodes data
|
|
1677
|
+
final_nodes = (
|
|
1678
|
+
filtered_nodes
|
|
1679
|
+
if "filtered_nodes" in locals()
|
|
1680
|
+
else [
|
|
1681
|
+
{
|
|
1682
|
+
"name": n.name,
|
|
1683
|
+
"type": n.node_type,
|
|
1684
|
+
"line_start": n.line_start,
|
|
1685
|
+
"line_end": n.line_end,
|
|
1686
|
+
"complexity": n.complexity,
|
|
1687
|
+
"has_docstring": n.has_docstring,
|
|
1688
|
+
"signature": n.signature,
|
|
1689
|
+
}
|
|
1690
|
+
for n in nodes
|
|
1691
|
+
if not self._is_internal_node(n)
|
|
1692
|
+
]
|
|
1693
|
+
)
|
|
1694
|
+
|
|
1695
|
+
# Convert nodes to elements format for dashboard compatibility
|
|
1696
|
+
elements = []
|
|
1697
|
+
for node in final_nodes:
|
|
1698
|
+
element = {
|
|
1699
|
+
"name": node["name"],
|
|
1700
|
+
"type": node["type"],
|
|
1701
|
+
"line": node["line_start"], # Dashboard expects 'line' not 'line_start'
|
|
1702
|
+
"complexity": node["complexity"],
|
|
1703
|
+
"signature": node.get("signature", ""),
|
|
1704
|
+
"has_docstring": node.get("has_docstring", False),
|
|
1705
|
+
}
|
|
1706
|
+
# Add methods if it's a class (for expandable tree)
|
|
1707
|
+
if node["type"] == "class":
|
|
1708
|
+
element["methods"] = [] # Could be populated with class methods
|
|
1709
|
+
elements.append(element)
|
|
1710
|
+
|
|
1628
1711
|
return {
|
|
1629
1712
|
"path": file_path,
|
|
1630
1713
|
"language": language,
|
|
1631
|
-
"nodes":
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
}
|
|
1644
|
-
for n in nodes
|
|
1645
|
-
if not self._is_internal_node(n)
|
|
1646
|
-
]
|
|
1647
|
-
),
|
|
1714
|
+
"nodes": final_nodes, # Keep for backward compatibility
|
|
1715
|
+
"elements": elements, # Add for dashboard compatibility
|
|
1716
|
+
"complexity": sum(e["complexity"] for e in elements),
|
|
1717
|
+
"lines": len(elements), # Simple line count approximation
|
|
1718
|
+
"stats": {
|
|
1719
|
+
"classes": len([e for e in elements if e["type"] == "class"]),
|
|
1720
|
+
"functions": len([e for e in elements if e["type"] == "function"]),
|
|
1721
|
+
"methods": len([e for e in elements if e["type"] == "method"]),
|
|
1722
|
+
"variables": len([e for e in elements if e["type"] == "variable"]),
|
|
1723
|
+
"imports": len([e for e in elements if e["type"] == "import"]),
|
|
1724
|
+
"total": len(elements),
|
|
1725
|
+
},
|
|
1648
1726
|
}
|
|
1649
1727
|
|
|
1650
1728
|
def _is_internal_node(self, node: CodeNode) -> bool:
|