claude-mpm 4.1.8__py3-none-any.whl → 4.1.11__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/INSTRUCTIONS.md +26 -1
- claude_mpm/agents/agents_metadata.py +57 -0
- claude_mpm/agents/templates/.claude-mpm/memories/README.md +17 -0
- claude_mpm/agents/templates/.claude-mpm/memories/engineer_memories.md +3 -0
- claude_mpm/agents/templates/agent-manager.json +263 -17
- claude_mpm/agents/templates/agentic_coder_optimizer.json +222 -0
- claude_mpm/agents/templates/code_analyzer.json +18 -8
- claude_mpm/agents/templates/engineer.json +1 -1
- claude_mpm/agents/templates/logs/prompts/agent_engineer_20250826_014258_728.md +39 -0
- claude_mpm/agents/templates/qa.json +1 -1
- claude_mpm/agents/templates/research.json +1 -1
- claude_mpm/cli/__init__.py +15 -0
- claude_mpm/cli/commands/__init__.py +6 -0
- claude_mpm/cli/commands/analyze.py +548 -0
- claude_mpm/cli/commands/analyze_code.py +524 -0
- claude_mpm/cli/commands/configure.py +78 -28
- claude_mpm/cli/commands/configure_tui.py +62 -60
- claude_mpm/cli/commands/dashboard.py +288 -0
- claude_mpm/cli/commands/debug.py +1386 -0
- claude_mpm/cli/commands/mpm_init.py +427 -0
- claude_mpm/cli/commands/mpm_init_handler.py +83 -0
- claude_mpm/cli/parsers/analyze_code_parser.py +170 -0
- claude_mpm/cli/parsers/analyze_parser.py +135 -0
- claude_mpm/cli/parsers/base_parser.py +44 -0
- claude_mpm/cli/parsers/dashboard_parser.py +113 -0
- claude_mpm/cli/parsers/debug_parser.py +319 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +122 -0
- claude_mpm/constants.py +13 -1
- claude_mpm/core/framework_loader.py +148 -6
- claude_mpm/core/log_manager.py +16 -13
- claude_mpm/core/logger.py +1 -1
- claude_mpm/core/unified_agent_registry.py +1 -1
- claude_mpm/dashboard/.claude-mpm/socketio-instances.json +1 -0
- claude_mpm/dashboard/analysis_runner.py +455 -0
- claude_mpm/dashboard/static/built/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/agent-inference.js +1 -1
- claude_mpm/dashboard/static/built/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/built/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/file-tool-tracker.js +1 -1
- claude_mpm/dashboard/static/built/components/module-viewer.js +1 -1
- claude_mpm/dashboard/static/built/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/built/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/built/dashboard.js +1 -1
- claude_mpm/dashboard/static/built/socket-client.js +1 -1
- claude_mpm/dashboard/static/css/activity.css +549 -0
- claude_mpm/dashboard/static/css/code-tree.css +1175 -0
- claude_mpm/dashboard/static/css/dashboard.css +245 -0
- claude_mpm/dashboard/static/dist/components/activity-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-tree.js +2 -0
- claude_mpm/dashboard/static/dist/components/code-viewer.js +2 -0
- claude_mpm/dashboard/static/dist/components/event-viewer.js +1 -1
- claude_mpm/dashboard/static/dist/components/session-manager.js +1 -1
- claude_mpm/dashboard/static/dist/components/working-directory.js +1 -1
- claude_mpm/dashboard/static/dist/dashboard.js +1 -1
- claude_mpm/dashboard/static/dist/socket-client.js +1 -1
- claude_mpm/dashboard/static/js/components/activity-tree.js +1338 -0
- claude_mpm/dashboard/static/js/components/code-tree.js +2535 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +480 -0
- claude_mpm/dashboard/static/js/components/event-viewer.js +59 -9
- claude_mpm/dashboard/static/js/components/session-manager.js +40 -4
- claude_mpm/dashboard/static/js/components/socket-manager.js +12 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +4 -0
- claude_mpm/dashboard/static/js/components/working-directory.js +17 -1
- claude_mpm/dashboard/static/js/dashboard.js +51 -0
- claude_mpm/dashboard/static/js/socket-client.js +465 -29
- claude_mpm/dashboard/templates/index.html +182 -4
- claude_mpm/hooks/claude_hooks/hook_handler.py +182 -5
- claude_mpm/hooks/claude_hooks/installer.py +386 -113
- claude_mpm/scripts/claude-hook-handler.sh +161 -0
- claude_mpm/scripts/socketio_daemon.py +121 -8
- claude_mpm/services/agents/deployment/agent_lifecycle_manager_refactored.py +2 -2
- claude_mpm/services/agents/deployment/agent_record_service.py +1 -2
- claude_mpm/services/agents/memory/memory_format_service.py +1 -3
- claude_mpm/services/cli/agent_cleanup_service.py +1 -5
- claude_mpm/services/cli/agent_dependency_service.py +1 -1
- claude_mpm/services/cli/agent_validation_service.py +3 -4
- claude_mpm/services/cli/dashboard_launcher.py +2 -3
- claude_mpm/services/cli/startup_checker.py +0 -11
- claude_mpm/services/core/cache_manager.py +1 -3
- claude_mpm/services/core/path_resolver.py +1 -4
- claude_mpm/services/core/service_container.py +2 -2
- claude_mpm/services/diagnostics/checks/instructions_check.py +1 -2
- claude_mpm/services/infrastructure/monitoring/__init__.py +11 -11
- claude_mpm/services/infrastructure/monitoring.py +11 -11
- claude_mpm/services/project/architecture_analyzer.py +1 -1
- claude_mpm/services/project/dependency_analyzer.py +4 -4
- claude_mpm/services/project/language_analyzer.py +3 -3
- claude_mpm/services/project/metrics_collector.py +3 -6
- claude_mpm/services/socketio/event_normalizer.py +64 -0
- claude_mpm/services/socketio/handlers/__init__.py +2 -0
- claude_mpm/services/socketio/handlers/code_analysis.py +672 -0
- claude_mpm/services/socketio/handlers/registry.py +2 -0
- claude_mpm/services/socketio/server/connection_manager.py +6 -4
- claude_mpm/services/socketio/server/core.py +100 -11
- claude_mpm/services/socketio/server/main.py +8 -2
- claude_mpm/services/visualization/__init__.py +19 -0
- claude_mpm/services/visualization/mermaid_generator.py +938 -0
- claude_mpm/tools/__main__.py +208 -0
- claude_mpm/tools/code_tree_analyzer.py +1596 -0
- claude_mpm/tools/code_tree_builder.py +631 -0
- claude_mpm/tools/code_tree_events.py +416 -0
- claude_mpm/tools/socketio_debug.py +671 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/METADATA +2 -1
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/RECORD +110 -74
- claude_mpm/agents/schema/agent_schema.json +0 -314
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/WHEEL +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.1.8.dist-info → claude_mpm-4.1.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Code Tree Event Emitter
|
|
4
|
+
=======================
|
|
5
|
+
|
|
6
|
+
WHY: Provides incremental event emission for real-time code tree visualization
|
|
7
|
+
in the dashboard. Uses Socket.IO to stream events as code is analyzed.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Support lazy loading with directory discovery
|
|
11
|
+
- Emit events only for structural elements (directories, files, main functions)
|
|
12
|
+
- Filter out internal functions and handlers
|
|
13
|
+
- Batch events for performance (emit every 10 nodes or 100ms)
|
|
14
|
+
- Use clean event subtypes without colons
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
from collections import deque
|
|
21
|
+
from dataclasses import asdict, dataclass
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import socketio
|
|
27
|
+
|
|
28
|
+
SOCKETIO_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
SOCKETIO_AVAILABLE = False
|
|
31
|
+
socketio = None
|
|
32
|
+
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from ..core.logging_config import get_logger
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CodeNodeEvent:
|
|
40
|
+
"""Represents a code node discovery event."""
|
|
41
|
+
|
|
42
|
+
file_path: str
|
|
43
|
+
node_type: str # 'class', 'function', 'method', 'module', 'import'
|
|
44
|
+
name: str
|
|
45
|
+
line_start: int
|
|
46
|
+
line_end: int
|
|
47
|
+
complexity: int = 0
|
|
48
|
+
has_docstring: bool = False
|
|
49
|
+
decorators: List[str] = None
|
|
50
|
+
parent: Optional[str] = None
|
|
51
|
+
children_count: int = 0
|
|
52
|
+
language: str = "python"
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
55
|
+
"""Convert to dictionary for JSON serialization."""
|
|
56
|
+
data = asdict(self)
|
|
57
|
+
data["timestamp"] = datetime.utcnow().isoformat()
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CodeTreeEventEmitter:
|
|
62
|
+
"""Emits code analysis events to dashboard via Socket.IO.
|
|
63
|
+
|
|
64
|
+
WHY: Real-time streaming of analysis progress allows users to see
|
|
65
|
+
the tree building incrementally, providing feedback for large codebases.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# Event types - using underscores for clean subtypes
|
|
69
|
+
EVENT_DIRECTORY_DISCOVERED = "code:directory:discovered"
|
|
70
|
+
EVENT_FILE_DISCOVERED = "code:file:discovered"
|
|
71
|
+
EVENT_FILE_ANALYZED = "code:file:analyzed"
|
|
72
|
+
EVENT_NODE_FOUND = "code:node:found"
|
|
73
|
+
EVENT_ANALYSIS_START = "code:analysis:start"
|
|
74
|
+
EVENT_ANALYSIS_COMPLETE = "code:analysis:complete"
|
|
75
|
+
EVENT_PROGRESS = "code:analysis:progress"
|
|
76
|
+
EVENT_ERROR = "code:analysis:error"
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
socketio_url: str = "http://localhost:8765",
|
|
81
|
+
batch_size: int = 10,
|
|
82
|
+
batch_timeout: float = 0.1,
|
|
83
|
+
use_stdout: bool = False,
|
|
84
|
+
):
|
|
85
|
+
"""Initialize event emitter.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
socketio_url: Socket.IO server URL
|
|
89
|
+
batch_size: Number of events to batch before emitting
|
|
90
|
+
batch_timeout: Maximum time to wait before emitting batch (seconds)
|
|
91
|
+
use_stdout: If True, emit to stdout instead of Socket.IO
|
|
92
|
+
"""
|
|
93
|
+
self.logger = get_logger(__name__)
|
|
94
|
+
self.socketio_url = socketio_url
|
|
95
|
+
self.batch_size = batch_size
|
|
96
|
+
self.batch_timeout = batch_timeout
|
|
97
|
+
self.use_stdout = use_stdout
|
|
98
|
+
|
|
99
|
+
# Event buffer for batching
|
|
100
|
+
self.event_buffer = deque()
|
|
101
|
+
self.buffer_lock = threading.Lock()
|
|
102
|
+
self.last_emit_time = time.time()
|
|
103
|
+
|
|
104
|
+
# Socket.IO client
|
|
105
|
+
self.sio = None
|
|
106
|
+
self.connected = False
|
|
107
|
+
if not use_stdout:
|
|
108
|
+
self._init_socketio()
|
|
109
|
+
|
|
110
|
+
# Statistics
|
|
111
|
+
self.stats = {
|
|
112
|
+
"events_sent": 0,
|
|
113
|
+
"events_buffered": 0,
|
|
114
|
+
"files_processed": 0,
|
|
115
|
+
"nodes_found": 0,
|
|
116
|
+
"errors": 0,
|
|
117
|
+
"start_time": None,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Background task for periodic emission
|
|
121
|
+
self._emit_task = None
|
|
122
|
+
self._stop_event = threading.Event()
|
|
123
|
+
|
|
124
|
+
def _init_socketio(self):
|
|
125
|
+
"""Initialize Socket.IO client connection."""
|
|
126
|
+
if not SOCKETIO_AVAILABLE:
|
|
127
|
+
self.logger.warning("Socket.IO not available - events will be logged only")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
self.sio = socketio.Client(
|
|
132
|
+
reconnection=True,
|
|
133
|
+
reconnection_attempts=3,
|
|
134
|
+
reconnection_delay=1,
|
|
135
|
+
logger=False,
|
|
136
|
+
engineio_logger=False,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@self.sio.event
|
|
140
|
+
def connect():
|
|
141
|
+
self.connected = True
|
|
142
|
+
self.logger.info(
|
|
143
|
+
f"Connected to Socket.IO server at {self.socketio_url}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@self.sio.event
|
|
147
|
+
def disconnect():
|
|
148
|
+
self.connected = False
|
|
149
|
+
self.logger.info("Disconnected from Socket.IO server")
|
|
150
|
+
|
|
151
|
+
# Attempt connection
|
|
152
|
+
self.sio.connect(self.socketio_url, wait_timeout=2)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self.logger.warning(f"Failed to connect to Socket.IO: {e}")
|
|
156
|
+
self.sio = None
|
|
157
|
+
|
|
158
|
+
def start(self):
|
|
159
|
+
"""Start the event emitter and background tasks."""
|
|
160
|
+
self.stats["start_time"] = datetime.utcnow()
|
|
161
|
+
self._stop_event.clear()
|
|
162
|
+
|
|
163
|
+
# Start background emit task
|
|
164
|
+
self._emit_task = threading.Thread(target=self._emit_loop, daemon=True)
|
|
165
|
+
self._emit_task.start()
|
|
166
|
+
|
|
167
|
+
# Emit analysis start event
|
|
168
|
+
self.emit(
|
|
169
|
+
self.EVENT_ANALYSIS_START,
|
|
170
|
+
{
|
|
171
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
172
|
+
"batch_size": self.batch_size,
|
|
173
|
+
"batch_timeout": self.batch_timeout,
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def stop(self):
|
|
178
|
+
"""Stop the event emitter and flush remaining events."""
|
|
179
|
+
# Flush remaining events
|
|
180
|
+
self._flush_events()
|
|
181
|
+
|
|
182
|
+
# Emit analysis complete event with statistics
|
|
183
|
+
self.emit(
|
|
184
|
+
self.EVENT_ANALYSIS_COMPLETE,
|
|
185
|
+
{
|
|
186
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
187
|
+
"duration": (
|
|
188
|
+
(datetime.utcnow() - self.stats["start_time"]).total_seconds()
|
|
189
|
+
if self.stats["start_time"]
|
|
190
|
+
else 0
|
|
191
|
+
),
|
|
192
|
+
"stats": self.stats,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Stop background task
|
|
197
|
+
self._stop_event.set()
|
|
198
|
+
if self._emit_task:
|
|
199
|
+
self._emit_task.join(timeout=1)
|
|
200
|
+
|
|
201
|
+
# Disconnect Socket.IO
|
|
202
|
+
if self.sio and self.connected:
|
|
203
|
+
self.sio.disconnect()
|
|
204
|
+
|
|
205
|
+
def emit(self, event_type: str, data: Dict[str, Any], batch: bool = False):
|
|
206
|
+
"""Emit an event, either immediately or batched.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
event_type: Type of event to emit
|
|
210
|
+
data: Event data
|
|
211
|
+
batch: Whether to batch this event
|
|
212
|
+
"""
|
|
213
|
+
event = {
|
|
214
|
+
"type": event_type,
|
|
215
|
+
"data": data,
|
|
216
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if batch:
|
|
220
|
+
with self.buffer_lock:
|
|
221
|
+
self.event_buffer.append(event)
|
|
222
|
+
self.stats["events_buffered"] += 1
|
|
223
|
+
|
|
224
|
+
# Check if we should flush
|
|
225
|
+
if len(self.event_buffer) >= self.batch_size:
|
|
226
|
+
self._flush_events()
|
|
227
|
+
else:
|
|
228
|
+
self._emit_event(event)
|
|
229
|
+
|
|
230
|
+
def emit_directory_discovered(self, dir_path: str, children: List[Dict[str, Any]]):
|
|
231
|
+
"""Emit directory discovery event."""
|
|
232
|
+
self.emit(
|
|
233
|
+
self.EVENT_DIRECTORY_DISCOVERED,
|
|
234
|
+
{
|
|
235
|
+
"path": dir_path,
|
|
236
|
+
"name": Path(dir_path).name,
|
|
237
|
+
"children": children,
|
|
238
|
+
"type": "directory",
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def emit_file_discovered(
|
|
243
|
+
self, file_path: str, language: Optional[str] = None, size: int = 0
|
|
244
|
+
):
|
|
245
|
+
"""Emit file discovery event."""
|
|
246
|
+
self.emit(
|
|
247
|
+
self.EVENT_FILE_DISCOVERED,
|
|
248
|
+
{
|
|
249
|
+
"path": file_path,
|
|
250
|
+
"name": Path(file_path).name,
|
|
251
|
+
"language": language or "unknown",
|
|
252
|
+
"size": size,
|
|
253
|
+
"type": "file",
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def emit_file_start(self, file_path: str, language: Optional[str] = None):
|
|
258
|
+
"""Emit file analysis start event.
|
|
259
|
+
|
|
260
|
+
WHY: Signals the beginning of file analysis for progress tracking.
|
|
261
|
+
"""
|
|
262
|
+
self.emit(
|
|
263
|
+
"code:file_start",
|
|
264
|
+
{
|
|
265
|
+
"path": file_path,
|
|
266
|
+
"name": Path(file_path).name,
|
|
267
|
+
"language": language or "unknown",
|
|
268
|
+
"type": "file",
|
|
269
|
+
"status": "analyzing",
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def emit_file_complete(
|
|
274
|
+
self, file_path: str, nodes_count: int = 0, duration: float = 0
|
|
275
|
+
):
|
|
276
|
+
"""Emit file analysis complete event.
|
|
277
|
+
|
|
278
|
+
WHY: Signals completion of file analysis with summary statistics.
|
|
279
|
+
"""
|
|
280
|
+
self.emit(
|
|
281
|
+
"code:file_complete",
|
|
282
|
+
{
|
|
283
|
+
"path": file_path,
|
|
284
|
+
"name": Path(file_path).name,
|
|
285
|
+
"nodes_count": nodes_count,
|
|
286
|
+
"duration": duration,
|
|
287
|
+
"type": "file",
|
|
288
|
+
"status": "complete",
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def emit_file_analyzed(
|
|
293
|
+
self, file_path: str, nodes: List[Dict[str, Any]], duration: float = 0
|
|
294
|
+
):
|
|
295
|
+
"""Emit file analysis complete event."""
|
|
296
|
+
self.stats["files_processed"] += 1
|
|
297
|
+
self.emit(
|
|
298
|
+
self.EVENT_FILE_ANALYZED,
|
|
299
|
+
{
|
|
300
|
+
"path": file_path,
|
|
301
|
+
"nodes": nodes,
|
|
302
|
+
"nodes_count": len(nodes),
|
|
303
|
+
"duration": duration,
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def emit_node(self, node: CodeNodeEvent):
|
|
308
|
+
"""Emit code node discovery event (batched).
|
|
309
|
+
|
|
310
|
+
Filters out internal functions and handlers.
|
|
311
|
+
"""
|
|
312
|
+
# Filter out internal handler functions
|
|
313
|
+
if self._is_internal_function(node.name):
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
self.stats["nodes_found"] += 1
|
|
317
|
+
# In stdout mode, don't batch - emit immediately for real-time updates
|
|
318
|
+
batch_mode = not self.use_stdout
|
|
319
|
+
self.emit(self.EVENT_NODE_FOUND, node.to_dict(), batch=batch_mode)
|
|
320
|
+
|
|
321
|
+
def _is_internal_function(self, name: str) -> bool:
|
|
322
|
+
"""Check if function is an internal handler that should be filtered."""
|
|
323
|
+
internal_patterns = [
|
|
324
|
+
"handle", # Event handlers
|
|
325
|
+
"on_", # Event callbacks
|
|
326
|
+
"_", # Private methods
|
|
327
|
+
"get_", # Simple getters
|
|
328
|
+
"set_", # Simple setters
|
|
329
|
+
"__", # Python magic methods
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
name_lower = name.lower()
|
|
333
|
+
return any(name_lower.startswith(pattern) for pattern in internal_patterns)
|
|
334
|
+
|
|
335
|
+
def emit_progress(self, current: int, total: int, message: str = ""):
|
|
336
|
+
"""Emit progress update event."""
|
|
337
|
+
self.emit(
|
|
338
|
+
self.EVENT_PROGRESS,
|
|
339
|
+
{
|
|
340
|
+
"current": current,
|
|
341
|
+
"total": total,
|
|
342
|
+
"percentage": (current / total * 100) if total > 0 else 0,
|
|
343
|
+
"message": message,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def emit_error(self, file_path: str, error: str):
|
|
348
|
+
"""Emit error event."""
|
|
349
|
+
self.stats["errors"] += 1
|
|
350
|
+
self.emit(self.EVENT_ERROR, {"file": file_path, "error": str(error)})
|
|
351
|
+
|
|
352
|
+
def _emit_event(self, event: Dict[str, Any]):
|
|
353
|
+
"""Emit a single event."""
|
|
354
|
+
|
|
355
|
+
# Convert datetime objects to ISO strings for JSON serialization
|
|
356
|
+
def convert_datetime(obj):
|
|
357
|
+
if isinstance(obj, datetime):
|
|
358
|
+
return obj.isoformat()
|
|
359
|
+
if isinstance(obj, dict):
|
|
360
|
+
return {k: convert_datetime(v) for k, v in obj.items()}
|
|
361
|
+
if isinstance(obj, list):
|
|
362
|
+
return [convert_datetime(item) for item in obj]
|
|
363
|
+
return obj
|
|
364
|
+
|
|
365
|
+
event = convert_datetime(event)
|
|
366
|
+
|
|
367
|
+
if self.use_stdout:
|
|
368
|
+
# Emit to stdout as JSON for subprocess communication
|
|
369
|
+
print(json.dumps(event), flush=True)
|
|
370
|
+
self.stats["events_sent"] += 1
|
|
371
|
+
elif self.sio and self.connected:
|
|
372
|
+
try:
|
|
373
|
+
self.sio.emit("code_tree_event", event)
|
|
374
|
+
self.stats["events_sent"] += 1
|
|
375
|
+
except Exception as e:
|
|
376
|
+
self.logger.error(f"Failed to emit event: {e}")
|
|
377
|
+
else:
|
|
378
|
+
# Fallback to logging
|
|
379
|
+
self.logger.debug(
|
|
380
|
+
f"Event: {event['type']} - {json.dumps(event['data'])[:100]}"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def _flush_events(self):
|
|
384
|
+
"""Flush all buffered events."""
|
|
385
|
+
with self.buffer_lock:
|
|
386
|
+
if not self.event_buffer:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
# Emit as batch
|
|
390
|
+
batch_event = {
|
|
391
|
+
"type": "code:batch",
|
|
392
|
+
"data": {
|
|
393
|
+
"events": list(self.event_buffer),
|
|
394
|
+
"count": len(self.event_buffer),
|
|
395
|
+
},
|
|
396
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
self._emit_event(batch_event)
|
|
400
|
+
self.event_buffer.clear()
|
|
401
|
+
self.last_emit_time = time.time()
|
|
402
|
+
|
|
403
|
+
def _emit_loop(self):
|
|
404
|
+
"""Background loop for periodic event emission."""
|
|
405
|
+
while not self._stop_event.is_set():
|
|
406
|
+
time.sleep(self.batch_timeout)
|
|
407
|
+
|
|
408
|
+
# Check if enough time has passed since last emit
|
|
409
|
+
if time.time() - self.last_emit_time >= self.batch_timeout:
|
|
410
|
+
with self.buffer_lock:
|
|
411
|
+
if self.event_buffer:
|
|
412
|
+
self._flush_events()
|
|
413
|
+
|
|
414
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
415
|
+
"""Get current statistics."""
|
|
416
|
+
return self.stats.copy()
|