claude-mpm 5.1.9__py3-none-any.whl → 5.4.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/PM_INSTRUCTIONS.md +46 -0
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/templates/circuit-breakers.md +138 -1
- claude_mpm/cli/commands/agent_state_manager.py +8 -17
- claude_mpm/cli/commands/configure.py +1046 -149
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/parsers/base_parser.py +5 -0
- claude_mpm/cli/startup.py +60 -53
- claude_mpm/commands/{mpm-ticket-organize.md → mpm-organize.md} +4 -5
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/unified_agent_registry.py +5 -15
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +322 -53
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +11 -1
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/command_deployment_service.py +0 -2
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/monitor/daemon.py +9 -2
- claude_mpm/services/monitor/daemon_manager.py +39 -3
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +17 -44
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/METADATA +1 -77
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/RECORD +59 -114
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -1,1474 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Socket.IO Client for Claude MPM Dashboard
|
|
3
|
-
*
|
|
4
|
-
* This module provides real-time WebSocket communication between the Claude MPM dashboard
|
|
5
|
-
* and the backend Socket.IO server. It handles connection management, event processing,
|
|
6
|
-
* retry logic, and health monitoring.
|
|
7
|
-
*
|
|
8
|
-
* Architecture:
|
|
9
|
-
* - Maintains persistent WebSocket connection to Claude MPM backend
|
|
10
|
-
* - Implements robust retry logic with exponential backoff
|
|
11
|
-
* - Provides event queuing during disconnections
|
|
12
|
-
* - Validates event schemas for data integrity
|
|
13
|
-
* - Monitors connection health with ping/pong mechanisms
|
|
14
|
-
*
|
|
15
|
-
* Event Flow:
|
|
16
|
-
* 1. Events from Claude Code hooks → Socket.IO server → Dashboard client
|
|
17
|
-
* 2. Dashboard requests → Socket.IO server → Backend services
|
|
18
|
-
* 3. Status updates → Socket.IO server → All connected clients
|
|
19
|
-
*
|
|
20
|
-
* Thread Safety:
|
|
21
|
-
* - Single-threaded JavaScript execution model ensures safety
|
|
22
|
-
* - Event callbacks are queued and executed sequentially
|
|
23
|
-
* - Connection state changes are atomic
|
|
24
|
-
*
|
|
25
|
-
* Performance Considerations:
|
|
26
|
-
* - Event queue limited to 100 items to prevent memory leaks
|
|
27
|
-
* - Health checks run every 45s to match server ping interval
|
|
28
|
-
* - Exponential backoff prevents connection spam
|
|
29
|
-
* - Lazy event validation reduces overhead
|
|
30
|
-
*
|
|
31
|
-
* Security:
|
|
32
|
-
* - Connects only to localhost to prevent external access
|
|
33
|
-
* - Event schema validation prevents malformed data processing
|
|
34
|
-
* - Connection timeout prevents hanging connections
|
|
35
|
-
*
|
|
36
|
-
* @author Claude MPM Team
|
|
37
|
-
* @version 1.0
|
|
38
|
-
* @since v4.0.25
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
// Access the global io from window object in ES6 module context
|
|
42
|
-
// WHY: Socket.IO is loaded via CDN in HTML, available as window.io
|
|
43
|
-
const io = window.io;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Primary Socket.IO client for dashboard communication.
|
|
47
|
-
*
|
|
48
|
-
* Manages WebSocket connection lifecycle, event processing, and error handling.
|
|
49
|
-
* Implements connection resilience with automatic retry and health monitoring.
|
|
50
|
-
*
|
|
51
|
-
* Key Features:
|
|
52
|
-
* - Automatic connection retry with exponential backoff
|
|
53
|
-
* - Event queue management during disconnections
|
|
54
|
-
* - Schema validation for incoming events
|
|
55
|
-
* - Health monitoring with ping/pong
|
|
56
|
-
* - Session management and event history
|
|
57
|
-
*
|
|
58
|
-
* Connection States:
|
|
59
|
-
* - isConnected: Currently connected to server
|
|
60
|
-
* - isConnecting: Connection attempt in progress
|
|
61
|
-
* - disconnectTime: Timestamp of last disconnection
|
|
62
|
-
*
|
|
63
|
-
* Event Processing:
|
|
64
|
-
* - Validates against schema before processing
|
|
65
|
-
* - Queues events during disconnection (max 100)
|
|
66
|
-
* - Maintains event history and session tracking
|
|
67
|
-
*
|
|
68
|
-
* @class SocketClient
|
|
69
|
-
*/
|
|
70
|
-
class SocketClient {
|
|
71
|
-
/**
|
|
72
|
-
* Initialize Socket.IO client with default configuration.
|
|
73
|
-
*
|
|
74
|
-
* Sets up connection management, event processing, and health monitoring.
|
|
75
|
-
* Configures retry logic and event queue management.
|
|
76
|
-
*
|
|
77
|
-
* WHY this initialization approach:
|
|
78
|
-
* - Lazy socket creation allows for port specification
|
|
79
|
-
* - Event queue prevents data loss during reconnections
|
|
80
|
-
* - Health monitoring detects server issues early
|
|
81
|
-
* - Schema validation ensures data integrity
|
|
82
|
-
*
|
|
83
|
-
* @constructor
|
|
84
|
-
*/
|
|
85
|
-
constructor() {
|
|
86
|
-
/**
|
|
87
|
-
* Socket.IO connection instance.
|
|
88
|
-
* @type {Socket|null}
|
|
89
|
-
* @private
|
|
90
|
-
*/
|
|
91
|
-
this.socket = null;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Current connection port.
|
|
95
|
-
* @type {string|null}
|
|
96
|
-
* @private
|
|
97
|
-
*/
|
|
98
|
-
this.port = null; // Store the current port
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Event callback registry for connection lifecycle events.
|
|
102
|
-
* WHY: Allows multiple components to register for connection events.
|
|
103
|
-
* @type {Object.<string, Function[]>}
|
|
104
|
-
* @private
|
|
105
|
-
*/
|
|
106
|
-
this.connectionCallbacks = {
|
|
107
|
-
connect: [], // Called on successful connection
|
|
108
|
-
disconnect: [], // Called on disconnection
|
|
109
|
-
error: [], // Called on connection errors
|
|
110
|
-
event: [] // Called on incoming events
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Event schema definition for validation.
|
|
115
|
-
* WHY: Ensures data integrity and prevents processing malformed events.
|
|
116
|
-
* @type {Object}
|
|
117
|
-
* @private
|
|
118
|
-
*/
|
|
119
|
-
this.eventSchema = {
|
|
120
|
-
required: ['source', 'type', 'subtype', 'timestamp', 'data'],
|
|
121
|
-
optional: ['event', 'session_id']
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Current connection state.
|
|
126
|
-
* @type {boolean}
|
|
127
|
-
* @private
|
|
128
|
-
*/
|
|
129
|
-
this.isConnected = false;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Connection attempt in progress flag.
|
|
133
|
-
* WHY: Prevents multiple simultaneous connection attempts.
|
|
134
|
-
* @type {boolean}
|
|
135
|
-
* @private
|
|
136
|
-
*/
|
|
137
|
-
this.isConnecting = false;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Timestamp of last successful connection.
|
|
141
|
-
* @type {number|null}
|
|
142
|
-
* @private
|
|
143
|
-
*/
|
|
144
|
-
this.lastConnectTime = null;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Timestamp of last disconnection.
|
|
148
|
-
* WHY: Used to calculate downtime and trigger reconnection logic.
|
|
149
|
-
* @type {number|null}
|
|
150
|
-
* @private
|
|
151
|
-
*/
|
|
152
|
-
this.disconnectTime = null;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Event history storage.
|
|
156
|
-
* WHY: Maintains event history for dashboard display and analysis.
|
|
157
|
-
* @type {Array.<Object>}
|
|
158
|
-
* @private
|
|
159
|
-
*/
|
|
160
|
-
this.events = [];
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Session tracking map.
|
|
164
|
-
* WHY: Groups events by session for better organization.
|
|
165
|
-
* @type {Map<string, Object>}
|
|
166
|
-
* @private
|
|
167
|
-
*/
|
|
168
|
-
this.sessions = new Map();
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Current active session identifier.
|
|
172
|
-
* @type {string|null}
|
|
173
|
-
* @private
|
|
174
|
-
*/
|
|
175
|
-
this.currentSessionId = null;
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Event queue for disconnection periods.
|
|
179
|
-
* WHY: Prevents event loss during temporary disconnections.
|
|
180
|
-
* @type {Array.<Object>}
|
|
181
|
-
* @private
|
|
182
|
-
*/
|
|
183
|
-
this.eventQueue = [];
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Maximum queue size to prevent memory leaks.
|
|
187
|
-
* WHY: Limits memory usage during extended disconnections.
|
|
188
|
-
* @type {number}
|
|
189
|
-
* @private
|
|
190
|
-
* @const
|
|
191
|
-
*/
|
|
192
|
-
this.maxQueueSize = 100;
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Current retry attempt counter.
|
|
196
|
-
* WHY: Tracks retry attempts for exponential backoff logic.
|
|
197
|
-
* @type {number}
|
|
198
|
-
* @private
|
|
199
|
-
*/
|
|
200
|
-
this.retryAttempts = 0;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Maximum retry attempts before giving up.
|
|
204
|
-
* WHY: Prevents infinite retry loops that could impact performance.
|
|
205
|
-
* @type {number}
|
|
206
|
-
* @private
|
|
207
|
-
* @const
|
|
208
|
-
*/
|
|
209
|
-
this.maxRetryAttempts = 5; // Increased from 3 to 5 for better stability
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Retry delay intervals in milliseconds (exponential backoff).
|
|
213
|
-
* WHY: Prevents server overload during connection issues.
|
|
214
|
-
* @type {number[]}
|
|
215
|
-
* @private
|
|
216
|
-
* @const
|
|
217
|
-
*/
|
|
218
|
-
this.retryDelays = [1000, 2000, 3000, 4000, 5000]; // Exponential backoff with 5 attempts
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Map of pending emissions for retry logic.
|
|
222
|
-
* WHY: Tracks failed emissions that need to be retried.
|
|
223
|
-
* @type {Map<string, Object>}
|
|
224
|
-
* @private
|
|
225
|
-
*/
|
|
226
|
-
this.pendingEmissions = new Map(); // Track pending emissions for retry
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Timestamp of last ping sent to server.
|
|
230
|
-
* WHY: Used for health monitoring and connection validation.
|
|
231
|
-
* @type {number|null}
|
|
232
|
-
* @private
|
|
233
|
-
*/
|
|
234
|
-
this.lastPingTime = null;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Timestamp of last pong received from server.
|
|
238
|
-
* WHY: Confirms server is responsive and connection is healthy.
|
|
239
|
-
* @type {number|null}
|
|
240
|
-
* @private
|
|
241
|
-
*/
|
|
242
|
-
this.lastPongTime = null;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Health check timeout in milliseconds.
|
|
246
|
-
* WHY: More lenient than Socket.IO timeout to prevent false positives.
|
|
247
|
-
* @type {number}
|
|
248
|
-
* @private
|
|
249
|
-
* @const
|
|
250
|
-
*/
|
|
251
|
-
this.pingTimeout = 120000; // 120 seconds for health check (more lenient for stability)
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Health check interval timer.
|
|
255
|
-
* @type {number|null}
|
|
256
|
-
* @private
|
|
257
|
-
*/
|
|
258
|
-
this.healthCheckInterval = null;
|
|
259
|
-
|
|
260
|
-
// Initialize background monitoring
|
|
261
|
-
this.startStatusCheckFallback();
|
|
262
|
-
this.startHealthMonitoring();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Connect to Socket.IO server on specified port.
|
|
267
|
-
*
|
|
268
|
-
* Initiates WebSocket connection to the Claude MPM Socket.IO server.
|
|
269
|
-
* Handles connection conflicts and ensures clean state transitions.
|
|
270
|
-
*
|
|
271
|
-
* Connection Process:
|
|
272
|
-
* 1. Validates port and constructs localhost URL
|
|
273
|
-
* 2. Checks for existing connections and cleans up if needed
|
|
274
|
-
* 3. Delegates to doConnect() for actual connection logic
|
|
275
|
-
*
|
|
276
|
-
* Thread Safety:
|
|
277
|
-
* - Uses setTimeout for async cleanup to prevent race conditions
|
|
278
|
-
* - Connection state flags prevent multiple simultaneous attempts
|
|
279
|
-
*
|
|
280
|
-
* @param {string} [port='8765'] - Port number to connect to (defaults to 8765)
|
|
281
|
-
*
|
|
282
|
-
* @throws {Error} If Socket.IO library is not loaded
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* // Connect to default port
|
|
286
|
-
* socketClient.connect();
|
|
287
|
-
*
|
|
288
|
-
* // Connect to specific port
|
|
289
|
-
* socketClient.connect('8766');
|
|
290
|
-
*/
|
|
291
|
-
connect(port = '8765') {
|
|
292
|
-
// Store the port for later use in reconnections
|
|
293
|
-
this.port = port;
|
|
294
|
-
const url = `http://localhost:${port}`;
|
|
295
|
-
|
|
296
|
-
// WHY this check: Prevents connection conflicts that can cause memory leaks
|
|
297
|
-
if (this.socket && (this.socket.connected || this.socket.connecting)) {
|
|
298
|
-
console.log('Already connected or connecting, disconnecting first...');
|
|
299
|
-
this.socket.disconnect();
|
|
300
|
-
// WHY 100ms delay: Allows cleanup to complete before new connection
|
|
301
|
-
setTimeout(() => this.doConnect(url), 100);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
this.doConnect(url);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Execute the actual Socket.IO connection with full configuration.
|
|
310
|
-
*
|
|
311
|
-
* Creates and configures Socket.IO client with appropriate timeouts,
|
|
312
|
-
* retry logic, and transport settings. Sets up event handlers for
|
|
313
|
-
* connection lifecycle management.
|
|
314
|
-
*
|
|
315
|
-
* Configuration Details:
|
|
316
|
-
* - autoConnect: true - Immediate connection attempt
|
|
317
|
-
* - reconnection: true - Built-in reconnection enabled
|
|
318
|
-
* - pingInterval: 25000ms - Matches server configuration
|
|
319
|
-
* - pingTimeout: 20000ms - Health check timeout
|
|
320
|
-
* - transports: ['websocket', 'polling'] - Fallback options
|
|
321
|
-
*
|
|
322
|
-
* WHY these settings:
|
|
323
|
-
* - Ping intervals must match server to prevent timeouts
|
|
324
|
-
* - Limited reconnection attempts prevent infinite loops
|
|
325
|
-
* - forceNew prevents socket reuse issues
|
|
326
|
-
*
|
|
327
|
-
* @param {string} url - Complete Socket.IO server URL (http://localhost:port)
|
|
328
|
-
* @private
|
|
329
|
-
*
|
|
330
|
-
* @throws {Error} If Socket.IO library is not available
|
|
331
|
-
*/
|
|
332
|
-
doConnect(url) {
|
|
333
|
-
console.log(`Connecting to Socket.IO server at ${url}`);
|
|
334
|
-
|
|
335
|
-
// Check if io is available
|
|
336
|
-
if (typeof io === 'undefined') {
|
|
337
|
-
console.error('Socket.IO library not loaded! Make sure socket.io.min.js is loaded before this script.');
|
|
338
|
-
this.notifyConnectionStatus('Socket.IO library not loaded', 'error');
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
this.isConnecting = true;
|
|
343
|
-
this.notifyConnectionStatus('Connecting...', 'connecting');
|
|
344
|
-
|
|
345
|
-
this.socket = io(url, {
|
|
346
|
-
autoConnect: true,
|
|
347
|
-
reconnection: true,
|
|
348
|
-
reconnectionDelay: 1000,
|
|
349
|
-
reconnectionDelayMax: 10000, // Increased max delay for stability
|
|
350
|
-
reconnectionAttempts: 10, // Increased attempts for better resilience
|
|
351
|
-
timeout: 30000, // Increased connection timeout to 30 seconds
|
|
352
|
-
forceNew: true,
|
|
353
|
-
transports: ['websocket', 'polling'],
|
|
354
|
-
// Remove client-side ping configuration - let server control this
|
|
355
|
-
// The server now properly configures: ping_interval=30s, ping_timeout=60s
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
this.setupSocketHandlers();
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Setup Socket.IO event handlers
|
|
363
|
-
*/
|
|
364
|
-
setupSocketHandlers() {
|
|
365
|
-
this.socket.on('connect', () => {
|
|
366
|
-
console.log('Connected to Socket.IO server');
|
|
367
|
-
const previouslyConnected = this.isConnected;
|
|
368
|
-
this.isConnected = true;
|
|
369
|
-
this.isConnecting = false;
|
|
370
|
-
this.lastConnectTime = Date.now();
|
|
371
|
-
this.retryAttempts = 0; // Reset retry counter on successful connect
|
|
372
|
-
|
|
373
|
-
// Calculate downtime if this is a reconnection
|
|
374
|
-
if (this.disconnectTime && previouslyConnected === false) {
|
|
375
|
-
const downtime = (Date.now() - this.disconnectTime) / 1000;
|
|
376
|
-
console.log(`Reconnected after ${downtime.toFixed(1)}s downtime`);
|
|
377
|
-
|
|
378
|
-
// Flush queued events after reconnection
|
|
379
|
-
this.flushEventQueue();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this.notifyConnectionStatus('Connected', 'connected');
|
|
383
|
-
|
|
384
|
-
// Expose socket globally for components that need direct access
|
|
385
|
-
window.socket = this.socket;
|
|
386
|
-
console.log('SocketClient: Exposed socket globally as window.socket');
|
|
387
|
-
|
|
388
|
-
// Emit connect callback
|
|
389
|
-
this.connectionCallbacks.connect.forEach(callback =>
|
|
390
|
-
callback(this.socket.id)
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
this.requestStatus();
|
|
394
|
-
// History is now automatically sent by server on connection
|
|
395
|
-
// No need to explicitly request it
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
this.socket.on('disconnect', (reason) => {
|
|
399
|
-
// Enhanced logging for debugging disconnection issues
|
|
400
|
-
const disconnectInfo = {
|
|
401
|
-
reason: reason,
|
|
402
|
-
timestamp: new Date().toISOString(),
|
|
403
|
-
wasConnected: this.isConnected,
|
|
404
|
-
uptimeSeconds: this.lastConnectTime ? ((Date.now() - this.lastConnectTime) / 1000).toFixed(1) : 0,
|
|
405
|
-
lastPing: this.lastPingTime ? ((Date.now() - this.lastPingTime) / 1000).toFixed(1) + 's ago' : 'never',
|
|
406
|
-
lastPong: this.lastPongTime ? ((Date.now() - this.lastPongTime) / 1000).toFixed(1) + 's ago' : 'never'
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
console.log('Disconnected from server:', disconnectInfo);
|
|
410
|
-
|
|
411
|
-
this.isConnected = false;
|
|
412
|
-
this.isConnecting = false;
|
|
413
|
-
this.disconnectTime = Date.now();
|
|
414
|
-
|
|
415
|
-
this.notifyConnectionStatus(`Disconnected: ${reason}`, 'disconnected');
|
|
416
|
-
|
|
417
|
-
// Emit disconnect callback
|
|
418
|
-
this.connectionCallbacks.disconnect.forEach(callback =>
|
|
419
|
-
callback(reason)
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
// Detailed reason analysis for auto-reconnect decision
|
|
423
|
-
const reconnectReasons = [
|
|
424
|
-
'transport close', // Network issue
|
|
425
|
-
'ping timeout', // Server not responding
|
|
426
|
-
'transport error', // Connection error
|
|
427
|
-
'io server disconnect', // Server initiated disconnect (might be restart)
|
|
428
|
-
];
|
|
429
|
-
|
|
430
|
-
if (reconnectReasons.includes(reason)) {
|
|
431
|
-
console.log(`Auto-reconnect triggered for reason: ${reason}`);
|
|
432
|
-
this.scheduleReconnect();
|
|
433
|
-
} else if (reason === 'io client disconnect') {
|
|
434
|
-
console.log('Client-initiated disconnect, not auto-reconnecting');
|
|
435
|
-
} else {
|
|
436
|
-
console.log(`Unknown disconnect reason: ${reason}, attempting reconnect anyway`);
|
|
437
|
-
this.scheduleReconnect();
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
this.socket.on('connect_error', (error) => {
|
|
442
|
-
console.error('Connection error:', error);
|
|
443
|
-
this.isConnecting = false;
|
|
444
|
-
const errorMsg = error.message || error.description || 'Unknown error';
|
|
445
|
-
this.notifyConnectionStatus(`Connection Error: ${errorMsg}`, 'disconnected');
|
|
446
|
-
|
|
447
|
-
// Add error event
|
|
448
|
-
this.addEvent({
|
|
449
|
-
type: 'connection.error',
|
|
450
|
-
timestamp: new Date().toISOString(),
|
|
451
|
-
data: {
|
|
452
|
-
error: errorMsg,
|
|
453
|
-
url: this.socket.io.uri,
|
|
454
|
-
retry_attempt: this.retryAttempts
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Emit error callback
|
|
459
|
-
this.connectionCallbacks.error.forEach(callback =>
|
|
460
|
-
callback(errorMsg)
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
// Schedule reconnect with backoff
|
|
464
|
-
this.scheduleReconnect();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// Primary event handler - this is what the server actually emits
|
|
468
|
-
this.socket.on('claude_event', (data) => {
|
|
469
|
-
console.log('Received claude_event:', data);
|
|
470
|
-
|
|
471
|
-
// Validate event schema
|
|
472
|
-
const validatedEvent = this.validateEventSchema(data);
|
|
473
|
-
if (!validatedEvent) {
|
|
474
|
-
console.warn('Invalid event schema received:', data);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Code analysis events are now allowed to flow through to the events list for troubleshooting
|
|
479
|
-
// They will appear in both the Events tab and the Code tab
|
|
480
|
-
if (validatedEvent.type && validatedEvent.type.startsWith('code:')) {
|
|
481
|
-
console.log('Code analysis event received via claude_event, adding to events list for troubleshooting:', validatedEvent.type);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Transform event to match expected format (for backward compatibility)
|
|
485
|
-
const transformedEvent = this.transformEvent(validatedEvent);
|
|
486
|
-
console.log('Transformed event:', transformedEvent);
|
|
487
|
-
this.addEvent(transformedEvent);
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Add ping/pong handlers for health monitoring
|
|
491
|
-
this.socket.on('ping', (data) => {
|
|
492
|
-
// console.log('Received ping from server');
|
|
493
|
-
this.lastPingTime = Date.now();
|
|
494
|
-
|
|
495
|
-
// Send pong response immediately
|
|
496
|
-
this.socket.emit('pong', {
|
|
497
|
-
timestamp: data.timestamp,
|
|
498
|
-
client_time: Date.now()
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// Track pong responses from server
|
|
503
|
-
this.socket.on('pong', (data) => {
|
|
504
|
-
this.lastPongTime = Date.now();
|
|
505
|
-
// console.log('Received pong from server');
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// Listen for heartbeat events from server (every 3 minutes)
|
|
509
|
-
this.socket.on('heartbeat', (data) => {
|
|
510
|
-
console.log('🫀 Received server heartbeat:', data);
|
|
511
|
-
// Add heartbeat to event list for visibility
|
|
512
|
-
this.addEvent({
|
|
513
|
-
type: 'system',
|
|
514
|
-
subtype: 'heartbeat',
|
|
515
|
-
timestamp: data.timestamp || new Date().toISOString(),
|
|
516
|
-
data: data
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// Update last ping time to indicate server is alive
|
|
520
|
-
this.lastPingTime = Date.now();
|
|
521
|
-
|
|
522
|
-
// Log to console for debugging
|
|
523
|
-
console.log(`Server heartbeat #${data.heartbeat_number}: ${data.server_uptime_formatted} uptime, ${data.connected_clients} clients connected`);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// Session and event handlers (legacy/fallback)
|
|
527
|
-
this.socket.on('session.started', (data) => {
|
|
528
|
-
this.addEvent({ type: 'session', subtype: 'started', timestamp: new Date().toISOString(), data });
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
this.socket.on('session.ended', (data) => {
|
|
532
|
-
this.addEvent({ type: 'session', subtype: 'ended', timestamp: new Date().toISOString(), data });
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
this.socket.on('claude.request', (data) => {
|
|
536
|
-
this.addEvent({ type: 'claude', subtype: 'request', timestamp: new Date().toISOString(), data });
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
this.socket.on('claude.response', (data) => {
|
|
540
|
-
this.addEvent({ type: 'claude', subtype: 'response', timestamp: new Date().toISOString(), data });
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
this.socket.on('agent.loaded', (data) => {
|
|
544
|
-
this.addEvent({ type: 'agent', subtype: 'loaded', timestamp: new Date().toISOString(), data });
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
this.socket.on('agent.executed', (data) => {
|
|
548
|
-
this.addEvent({ type: 'agent', subtype: 'executed', timestamp: new Date().toISOString(), data });
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// DISABLED: Legacy hook handlers - events now come through claude_event pathway
|
|
552
|
-
// to prevent duplication. Hook events are processed by the claude_event handler above.
|
|
553
|
-
// this.socket.on('hook.pre', (data) => {
|
|
554
|
-
// this.addEvent({ type: 'hook', subtype: 'pre', timestamp: new Date().toISOString(), data });
|
|
555
|
-
// });
|
|
556
|
-
|
|
557
|
-
// this.socket.on('hook.post', (data) => {
|
|
558
|
-
// this.addEvent({ type: 'hook', subtype: 'post', timestamp: new Date().toISOString(), data });
|
|
559
|
-
// });
|
|
560
|
-
|
|
561
|
-
this.socket.on('todo.updated', (data) => {
|
|
562
|
-
this.addEvent({ type: 'todo', subtype: 'updated', timestamp: new Date().toISOString(), data });
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
this.socket.on('memory.operation', (data) => {
|
|
566
|
-
this.addEvent({ type: 'memory', subtype: 'operation', timestamp: new Date().toISOString(), data });
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
this.socket.on('log.entry', (data) => {
|
|
570
|
-
this.addEvent({ type: 'log', subtype: 'entry', timestamp: new Date().toISOString(), data });
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Code analysis events - now allowed to flow through for troubleshooting
|
|
574
|
-
// These are ALSO handled by the code-tree component and shown in the footer
|
|
575
|
-
// They will appear in both places: Events tab (for troubleshooting) and Code tab (for visualization)
|
|
576
|
-
this.socket.on('code:analysis:queued', (data) => {
|
|
577
|
-
// Add to events list for troubleshooting
|
|
578
|
-
console.log('Code analysis queued event received, adding to events list for troubleshooting');
|
|
579
|
-
this.addEvent({ type: 'code', subtype: 'analysis:queued', timestamp: new Date().toISOString(), data });
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
this.socket.on('code:analysis:accepted', (data) => {
|
|
583
|
-
// Add to events list for troubleshooting
|
|
584
|
-
console.log('Code analysis accepted event received, adding to events list for troubleshooting');
|
|
585
|
-
this.addEvent({ type: 'code', subtype: 'analysis:accepted', timestamp: new Date().toISOString(), data });
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
this.socket.on('code:analysis:start', (data) => {
|
|
589
|
-
// Add to events list for troubleshooting
|
|
590
|
-
console.log('Code analysis start event received, adding to events list for troubleshooting');
|
|
591
|
-
this.addEvent({ type: 'code', subtype: 'analysis:start', timestamp: new Date().toISOString(), data });
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
this.socket.on('code:analysis:complete', (data) => {
|
|
595
|
-
// Add to events list for troubleshooting
|
|
596
|
-
console.log('Code analysis complete event received, adding to events list for troubleshooting');
|
|
597
|
-
this.addEvent({ type: 'code', subtype: 'analysis:complete', timestamp: new Date().toISOString(), data });
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
this.socket.on('code:analysis:error', (data) => {
|
|
601
|
-
// Add to events list for troubleshooting
|
|
602
|
-
console.log('Code analysis error event received, adding to events list for troubleshooting');
|
|
603
|
-
this.addEvent({ type: 'code', subtype: 'analysis:error', timestamp: new Date().toISOString(), data });
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
this.socket.on('code:file:start', (data) => {
|
|
607
|
-
// Add to events list for troubleshooting
|
|
608
|
-
console.log('Code file start event received, adding to events list for troubleshooting');
|
|
609
|
-
this.addEvent({ type: 'code', subtype: 'file:start', timestamp: new Date().toISOString(), data });
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
this.socket.on('code:node:found', (data) => {
|
|
613
|
-
// Add to events list for troubleshooting
|
|
614
|
-
console.log('Code node found event received, adding to events list for troubleshooting');
|
|
615
|
-
this.addEvent({ type: 'code', subtype: 'node:found', timestamp: new Date().toISOString(), data });
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
this.socket.on('code:analysis:progress', (data) => {
|
|
619
|
-
// Add to events list for troubleshooting
|
|
620
|
-
console.log('Code analysis progress event received, adding to events list for troubleshooting');
|
|
621
|
-
this.addEvent({ type: 'code', subtype: 'analysis:progress', timestamp: new Date().toISOString(), data });
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
this.socket.on('history', (data) => {
|
|
625
|
-
console.log('Received event history:', data);
|
|
626
|
-
if (data && Array.isArray(data.events)) {
|
|
627
|
-
console.log(`Processing ${data.events.length} historical events (${data.count} sent, ${data.total_available} total available)`);
|
|
628
|
-
// Add events in the order received (should already be chronological - oldest first)
|
|
629
|
-
// Transform each historical event to match expected format
|
|
630
|
-
data.events.forEach(event => {
|
|
631
|
-
const transformedEvent = this.transformEvent(event);
|
|
632
|
-
this.addEvent(transformedEvent, false);
|
|
633
|
-
});
|
|
634
|
-
this.notifyEventUpdate();
|
|
635
|
-
console.log(`Event history loaded: ${data.events.length} events added to dashboard`);
|
|
636
|
-
|
|
637
|
-
// FIX: Dispatch custom event after history is loaded
|
|
638
|
-
// WHY: Allows dashboard to render panes with initial data
|
|
639
|
-
document.dispatchEvent(new CustomEvent('historyLoaded', {
|
|
640
|
-
detail: {
|
|
641
|
-
eventCount: data.events.length,
|
|
642
|
-
totalAvailable: data.total_available
|
|
643
|
-
}
|
|
644
|
-
}));
|
|
645
|
-
} else if (Array.isArray(data)) {
|
|
646
|
-
// Handle legacy format for backward compatibility
|
|
647
|
-
console.log('Received legacy event history format:', data.length, 'events');
|
|
648
|
-
data.forEach(event => {
|
|
649
|
-
const transformedEvent = this.transformEvent(event);
|
|
650
|
-
this.addEvent(transformedEvent, false);
|
|
651
|
-
});
|
|
652
|
-
this.notifyEventUpdate();
|
|
653
|
-
|
|
654
|
-
// FIX: Dispatch custom event for legacy format too
|
|
655
|
-
document.dispatchEvent(new CustomEvent('historyLoaded', {
|
|
656
|
-
detail: {
|
|
657
|
-
eventCount: data.length,
|
|
658
|
-
totalAvailable: data.length
|
|
659
|
-
}
|
|
660
|
-
}));
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
this.socket.on('system.status', (data) => {
|
|
665
|
-
console.log('Received system status:', data);
|
|
666
|
-
if (data.sessions) {
|
|
667
|
-
this.updateSessions(data.sessions);
|
|
668
|
-
}
|
|
669
|
-
if (data.current_session) {
|
|
670
|
-
this.currentSessionId = data.current_session;
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Disconnect from Socket.IO server
|
|
677
|
-
*/
|
|
678
|
-
disconnect() {
|
|
679
|
-
if (this.socket) {
|
|
680
|
-
this.socket.disconnect();
|
|
681
|
-
this.socket = null;
|
|
682
|
-
}
|
|
683
|
-
this.port = null; // Clear the stored port
|
|
684
|
-
this.isConnected = false;
|
|
685
|
-
this.isConnecting = false;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* Emit an event with retry support
|
|
690
|
-
* @param {string} event - Event name
|
|
691
|
-
* @param {any} data - Event data
|
|
692
|
-
* @param {Object} options - Options for retry behavior
|
|
693
|
-
*/
|
|
694
|
-
emitWithRetry(event, data = null, options = {}) {
|
|
695
|
-
const {
|
|
696
|
-
maxRetries = 3,
|
|
697
|
-
retryDelays = [1000, 2000, 4000],
|
|
698
|
-
onSuccess = null,
|
|
699
|
-
onFailure = null
|
|
700
|
-
} = options;
|
|
701
|
-
|
|
702
|
-
const emissionId = `${event}_${Date.now()}_${Math.random()}`;
|
|
703
|
-
|
|
704
|
-
const attemptEmission = (attemptNum = 0) => {
|
|
705
|
-
if (!this.socket || !this.socket.connected) {
|
|
706
|
-
// Queue for later if disconnected
|
|
707
|
-
if (attemptNum === 0) {
|
|
708
|
-
this.queueEvent(event, data);
|
|
709
|
-
console.log(`Queued ${event} for later emission (disconnected)`);
|
|
710
|
-
if (onFailure) onFailure('disconnected');
|
|
711
|
-
}
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
try {
|
|
716
|
-
// Attempt emission
|
|
717
|
-
this.socket.emit(event, data);
|
|
718
|
-
console.log(`Emitted ${event} successfully`);
|
|
719
|
-
|
|
720
|
-
// Remove from pending
|
|
721
|
-
this.pendingEmissions.delete(emissionId);
|
|
722
|
-
|
|
723
|
-
if (onSuccess) onSuccess();
|
|
724
|
-
|
|
725
|
-
} catch (error) {
|
|
726
|
-
console.error(`Failed to emit ${event} (attempt ${attemptNum + 1}):`, error);
|
|
727
|
-
|
|
728
|
-
if (attemptNum < maxRetries - 1) {
|
|
729
|
-
const delay = retryDelays[attemptNum] || retryDelays[retryDelays.length - 1];
|
|
730
|
-
console.log(`Retrying ${event} in ${delay}ms...`);
|
|
731
|
-
|
|
732
|
-
// Store pending emission
|
|
733
|
-
this.pendingEmissions.set(emissionId, {
|
|
734
|
-
event,
|
|
735
|
-
data,
|
|
736
|
-
attemptNum: attemptNum + 1,
|
|
737
|
-
scheduledTime: Date.now() + delay
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
setTimeout(() => attemptEmission(attemptNum + 1), delay);
|
|
741
|
-
} else {
|
|
742
|
-
console.error(`Failed to emit ${event} after ${maxRetries} attempts`);
|
|
743
|
-
this.pendingEmissions.delete(emissionId);
|
|
744
|
-
if (onFailure) onFailure('max_retries_exceeded');
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
attemptEmission();
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Queue an event for later emission
|
|
754
|
-
* @param {string} event - Event name
|
|
755
|
-
* @param {any} data - Event data
|
|
756
|
-
*/
|
|
757
|
-
queueEvent(event, data) {
|
|
758
|
-
if (this.eventQueue.length >= this.maxQueueSize) {
|
|
759
|
-
// Remove oldest event if queue is full
|
|
760
|
-
const removed = this.eventQueue.shift();
|
|
761
|
-
console.warn(`Event queue full, dropped oldest event: ${removed.event}`);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
this.eventQueue.push({
|
|
765
|
-
event,
|
|
766
|
-
data,
|
|
767
|
-
timestamp: Date.now()
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* Flush queued events after reconnection
|
|
773
|
-
*/
|
|
774
|
-
flushEventQueue() {
|
|
775
|
-
if (this.eventQueue.length === 0) return;
|
|
776
|
-
|
|
777
|
-
console.log(`Flushing ${this.eventQueue.length} queued events...`);
|
|
778
|
-
const events = [...this.eventQueue];
|
|
779
|
-
this.eventQueue = [];
|
|
780
|
-
|
|
781
|
-
// Emit each queued event with a small delay between them
|
|
782
|
-
events.forEach((item, index) => {
|
|
783
|
-
setTimeout(() => {
|
|
784
|
-
if (this.socket && this.socket.connected) {
|
|
785
|
-
this.socket.emit(item.event, item.data);
|
|
786
|
-
console.log(`Flushed queued event: ${item.event}`);
|
|
787
|
-
}
|
|
788
|
-
}, index * 100); // 100ms between each event
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Schedule a reconnection attempt with exponential backoff
|
|
794
|
-
*/
|
|
795
|
-
scheduleReconnect() {
|
|
796
|
-
if (this.retryAttempts >= this.maxRetryAttempts) {
|
|
797
|
-
console.log('Max reconnection attempts reached, stopping auto-reconnect');
|
|
798
|
-
this.notifyConnectionStatus('Reconnection failed', 'disconnected');
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const delay = this.retryDelays[this.retryAttempts] || this.retryDelays[this.retryDelays.length - 1];
|
|
803
|
-
this.retryAttempts++;
|
|
804
|
-
|
|
805
|
-
console.log(`Scheduling reconnect attempt ${this.retryAttempts}/${this.maxRetryAttempts} in ${delay}ms...`);
|
|
806
|
-
this.notifyConnectionStatus(`Reconnecting in ${delay/1000}s...`, 'connecting');
|
|
807
|
-
|
|
808
|
-
setTimeout(() => {
|
|
809
|
-
if (!this.isConnected && this.port) {
|
|
810
|
-
console.log(`Attempting reconnection ${this.retryAttempts}/${this.maxRetryAttempts}...`);
|
|
811
|
-
this.connect(this.port);
|
|
812
|
-
}
|
|
813
|
-
}, delay);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Request server status
|
|
818
|
-
*/
|
|
819
|
-
requestStatus() {
|
|
820
|
-
if (this.socket && this.socket.connected) {
|
|
821
|
-
console.log('Requesting server status...');
|
|
822
|
-
this.emitWithRetry('request.status', null, {
|
|
823
|
-
maxRetries: 2,
|
|
824
|
-
retryDelays: [500, 1000]
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Request event history from server
|
|
831
|
-
* @param {Object} options - History request options
|
|
832
|
-
* @param {number} options.limit - Maximum number of events to retrieve (default: 50)
|
|
833
|
-
* @param {Array<string>} options.event_types - Optional filter by event types
|
|
834
|
-
*/
|
|
835
|
-
requestHistory(options = {}) {
|
|
836
|
-
if (this.socket && this.socket.connected) {
|
|
837
|
-
const params = {
|
|
838
|
-
limit: options.limit || 50,
|
|
839
|
-
event_types: options.event_types || []
|
|
840
|
-
};
|
|
841
|
-
console.log('Requesting event history...', params);
|
|
842
|
-
this.emitWithRetry('get_history', params, {
|
|
843
|
-
maxRetries: 3,
|
|
844
|
-
retryDelays: [1000, 2000, 3000],
|
|
845
|
-
onFailure: (reason) => {
|
|
846
|
-
console.error(`Failed to request history: ${reason}`);
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
} else {
|
|
850
|
-
console.warn('Cannot request history: not connected to server');
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Add event to local storage and notify listeners
|
|
856
|
-
* @param {Object} eventData - Event data
|
|
857
|
-
* @param {boolean} notify - Whether to notify listeners (default: true)
|
|
858
|
-
*/
|
|
859
|
-
addEvent(eventData, notify = true) {
|
|
860
|
-
// Ensure event has required fields
|
|
861
|
-
if (!eventData.timestamp) {
|
|
862
|
-
eventData.timestamp = new Date().toISOString();
|
|
863
|
-
}
|
|
864
|
-
if (!eventData.id) {
|
|
865
|
-
eventData.id = Date.now() + Math.random();
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
this.events.push(eventData);
|
|
869
|
-
|
|
870
|
-
// Update session tracking
|
|
871
|
-
if (eventData.data && eventData.data.session_id) {
|
|
872
|
-
const sessionId = eventData.data.session_id;
|
|
873
|
-
if (!this.sessions.has(sessionId)) {
|
|
874
|
-
this.sessions.set(sessionId, {
|
|
875
|
-
id: sessionId,
|
|
876
|
-
startTime: eventData.timestamp,
|
|
877
|
-
lastActivity: eventData.timestamp,
|
|
878
|
-
eventCount: 0,
|
|
879
|
-
working_directory: null,
|
|
880
|
-
git_branch: null
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
const session = this.sessions.get(sessionId);
|
|
884
|
-
session.lastActivity = eventData.timestamp;
|
|
885
|
-
session.eventCount++;
|
|
886
|
-
|
|
887
|
-
// Extract working directory from event data if available (prioritize newer data)
|
|
888
|
-
// Check multiple possible locations for working directory
|
|
889
|
-
const possiblePaths = [
|
|
890
|
-
eventData.data.cwd,
|
|
891
|
-
eventData.data.working_directory,
|
|
892
|
-
eventData.data.working_dir,
|
|
893
|
-
eventData.data.workingDirectory,
|
|
894
|
-
eventData.data.instance_info?.working_dir,
|
|
895
|
-
eventData.data.instance_info?.working_directory,
|
|
896
|
-
eventData.data.instance_info?.cwd,
|
|
897
|
-
eventData.cwd,
|
|
898
|
-
eventData.working_directory,
|
|
899
|
-
eventData.working_dir
|
|
900
|
-
];
|
|
901
|
-
|
|
902
|
-
for (const path of possiblePaths) {
|
|
903
|
-
if (path && typeof path === 'string' && path.trim()) {
|
|
904
|
-
session.working_directory = path;
|
|
905
|
-
console.log(`[SOCKET-CLIENT] Found working directory for session ${sessionId}:`, path);
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Extract git branch if available
|
|
911
|
-
if (eventData.data.git_branch) {
|
|
912
|
-
session.git_branch = eventData.data.git_branch;
|
|
913
|
-
} else if (eventData.data.instance_info && eventData.data.instance_info.git_branch) {
|
|
914
|
-
session.git_branch = eventData.data.instance_info.git_branch;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
if (notify) {
|
|
919
|
-
this.notifyEventUpdate();
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Update sessions from server data
|
|
925
|
-
* @param {Array} sessionsData - Sessions data from server
|
|
926
|
-
*/
|
|
927
|
-
updateSessions(sessionsData) {
|
|
928
|
-
if (Array.isArray(sessionsData)) {
|
|
929
|
-
sessionsData.forEach(session => {
|
|
930
|
-
this.sessions.set(session.id, session);
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/**
|
|
936
|
-
* Clear all events
|
|
937
|
-
*/
|
|
938
|
-
clearEvents() {
|
|
939
|
-
this.events = [];
|
|
940
|
-
this.sessions.clear();
|
|
941
|
-
this.notifyEventUpdate();
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* Clear events and request fresh history from server
|
|
946
|
-
* @param {Object} options - History request options (same as requestHistory)
|
|
947
|
-
*/
|
|
948
|
-
refreshHistory(options = {}) {
|
|
949
|
-
this.clearEvents();
|
|
950
|
-
this.requestHistory(options);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/**
|
|
954
|
-
* Get filtered events by session
|
|
955
|
-
* @param {string} sessionId - Session ID to filter by (null for all)
|
|
956
|
-
* @returns {Array} Filtered events
|
|
957
|
-
*/
|
|
958
|
-
getEventsBySession(sessionId = null) {
|
|
959
|
-
if (!sessionId) {
|
|
960
|
-
return this.events;
|
|
961
|
-
}
|
|
962
|
-
return this.events.filter(event =>
|
|
963
|
-
event.data && event.data.session_id === sessionId
|
|
964
|
-
);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
/**
|
|
968
|
-
* Register callback for connection events
|
|
969
|
-
* @param {string} eventType - Type of event (connect, disconnect, error)
|
|
970
|
-
* @param {Function} callback - Callback function
|
|
971
|
-
*/
|
|
972
|
-
onConnection(eventType, callback) {
|
|
973
|
-
if (this.connectionCallbacks[eventType]) {
|
|
974
|
-
this.connectionCallbacks[eventType].push(callback);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Register callback for event updates
|
|
980
|
-
* @param {Function} callback - Callback function
|
|
981
|
-
*/
|
|
982
|
-
onEventUpdate(callback) {
|
|
983
|
-
this.connectionCallbacks.event.push(callback);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Subscribe to socket events (proxy to underlying socket)
|
|
988
|
-
* @param {string} event - Event name
|
|
989
|
-
* @param {Function} callback - Callback function
|
|
990
|
-
*/
|
|
991
|
-
on(event, callback) {
|
|
992
|
-
if (this.socket) {
|
|
993
|
-
return this.socket.on(event, callback);
|
|
994
|
-
} else {
|
|
995
|
-
console.warn(`Cannot subscribe to '${event}': socket not initialized`);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Unsubscribe from socket events (proxy to underlying socket)
|
|
1001
|
-
* @param {string} event - Event name
|
|
1002
|
-
* @param {Function} callback - Callback function (optional)
|
|
1003
|
-
*/
|
|
1004
|
-
off(event, callback) {
|
|
1005
|
-
if (this.socket) {
|
|
1006
|
-
return this.socket.off(event, callback);
|
|
1007
|
-
} else {
|
|
1008
|
-
console.warn(`Cannot unsubscribe from '${event}': socket not initialized`);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Notify connection status change
|
|
1014
|
-
* @param {string} status - Status message
|
|
1015
|
-
* @param {string} type - Status type (connected, disconnected, connecting)
|
|
1016
|
-
*/
|
|
1017
|
-
notifyConnectionStatus(status, type) {
|
|
1018
|
-
console.log(`SocketClient: Connection status changed to '${status}' (${type})`);
|
|
1019
|
-
|
|
1020
|
-
// Direct DOM update - immediate and reliable
|
|
1021
|
-
this.updateConnectionStatusDOM(status, type);
|
|
1022
|
-
|
|
1023
|
-
// Also dispatch custom event for other modules
|
|
1024
|
-
document.dispatchEvent(new CustomEvent('socketConnectionStatus', {
|
|
1025
|
-
detail: { status, type }
|
|
1026
|
-
}));
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
/**
|
|
1030
|
-
* Directly update the connection status DOM element
|
|
1031
|
-
* @param {string} status - Status message
|
|
1032
|
-
* @param {string} type - Status type (connected, disconnected, connecting)
|
|
1033
|
-
*/
|
|
1034
|
-
updateConnectionStatusDOM(status, type) {
|
|
1035
|
-
const statusElement = document.getElementById('connection-status');
|
|
1036
|
-
if (statusElement) {
|
|
1037
|
-
// Update the text content while preserving the indicator span
|
|
1038
|
-
statusElement.innerHTML = `<span>●</span> ${status}`;
|
|
1039
|
-
|
|
1040
|
-
// Update the CSS class for styling
|
|
1041
|
-
statusElement.className = `status-badge status-${type}`;
|
|
1042
|
-
|
|
1043
|
-
console.log(`SocketClient: Direct DOM update - status: '${status}' (${type})`);
|
|
1044
|
-
} else {
|
|
1045
|
-
console.warn('SocketClient: Could not find connection-status element in DOM');
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Notify event update
|
|
1051
|
-
*/
|
|
1052
|
-
notifyEventUpdate() {
|
|
1053
|
-
this.connectionCallbacks.event.forEach(callback =>
|
|
1054
|
-
callback(this.events, this.sessions)
|
|
1055
|
-
);
|
|
1056
|
-
|
|
1057
|
-
// Also dispatch custom event
|
|
1058
|
-
document.dispatchEvent(new CustomEvent('socketEventUpdate', {
|
|
1059
|
-
detail: { events: this.events, sessions: this.sessions }
|
|
1060
|
-
}));
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
/**
|
|
1064
|
-
* Get connection state
|
|
1065
|
-
* @returns {Object} Connection state
|
|
1066
|
-
*/
|
|
1067
|
-
getConnectionState() {
|
|
1068
|
-
return {
|
|
1069
|
-
isConnected: this.isConnected,
|
|
1070
|
-
isConnecting: this.isConnecting,
|
|
1071
|
-
socketId: this.socket ? this.socket.id : null
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Validate event against expected schema
|
|
1077
|
-
* @param {Object} eventData - Raw event data
|
|
1078
|
-
* @returns {Object|null} Validated event or null if invalid
|
|
1079
|
-
*/
|
|
1080
|
-
validateEventSchema(eventData) {
|
|
1081
|
-
if (!eventData || typeof eventData !== 'object') {
|
|
1082
|
-
console.warn('Event data is not an object:', eventData);
|
|
1083
|
-
return null;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Make a copy to avoid modifying the original
|
|
1087
|
-
const validated = { ...eventData };
|
|
1088
|
-
|
|
1089
|
-
// Check and provide defaults for required fields
|
|
1090
|
-
if (!validated.source) {
|
|
1091
|
-
validated.source = 'system'; // Default source for backward compatibility
|
|
1092
|
-
}
|
|
1093
|
-
if (!validated.type) {
|
|
1094
|
-
// If there's an event field, use it as the type
|
|
1095
|
-
if (validated.event) {
|
|
1096
|
-
validated.type = validated.event;
|
|
1097
|
-
} else {
|
|
1098
|
-
validated.type = 'unknown';
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
if (!validated.subtype) {
|
|
1102
|
-
validated.subtype = 'generic';
|
|
1103
|
-
}
|
|
1104
|
-
if (!validated.timestamp) {
|
|
1105
|
-
validated.timestamp = new Date().toISOString();
|
|
1106
|
-
}
|
|
1107
|
-
if (!validated.data) {
|
|
1108
|
-
validated.data = {};
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Ensure data field is an object
|
|
1112
|
-
if (validated.data && typeof validated.data !== 'object') {
|
|
1113
|
-
validated.data = { value: validated.data };
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
console.log('Validated event:', validated);
|
|
1117
|
-
return validated;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
/**
|
|
1121
|
-
* Transform received event to match expected dashboard format
|
|
1122
|
-
* @param {Object} eventData - Raw event data from server
|
|
1123
|
-
* @returns {Object} Transformed event
|
|
1124
|
-
*/
|
|
1125
|
-
transformEvent(eventData) {
|
|
1126
|
-
// Handle multiple event structures:
|
|
1127
|
-
// 1. Hook events: { type: 'hook.pre_tool', timestamp: '...', data: {...} }
|
|
1128
|
-
// 2. Legacy events: { event: 'TestStart', timestamp: '...', ... }
|
|
1129
|
-
// 3. Standard events: { type: 'session', subtype: 'started', ... }
|
|
1130
|
-
// 4. Normalized events: { type: 'code', subtype: 'progress', ... } - already normalized, keep as-is
|
|
1131
|
-
|
|
1132
|
-
if (!eventData) {
|
|
1133
|
-
return eventData; // Return as-is if null/undefined
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
let transformedEvent = { ...eventData };
|
|
1137
|
-
|
|
1138
|
-
// Check if event is already normalized (has both type and subtype as separate fields)
|
|
1139
|
-
// This prevents double-transformation of events that were normalized on the backend
|
|
1140
|
-
const isAlreadyNormalized = eventData.type && eventData.subtype &&
|
|
1141
|
-
!eventData.type.includes('.') &&
|
|
1142
|
-
!eventData.type.includes(':');
|
|
1143
|
-
|
|
1144
|
-
if (isAlreadyNormalized) {
|
|
1145
|
-
// Event is already properly normalized from backend, just preserve it
|
|
1146
|
-
// Store a composite originalEventName for display if needed
|
|
1147
|
-
if (!transformedEvent.originalEventName) {
|
|
1148
|
-
if (eventData.subtype === 'generic' || eventData.type === eventData.subtype) {
|
|
1149
|
-
transformedEvent.originalEventName = eventData.type;
|
|
1150
|
-
} else {
|
|
1151
|
-
transformedEvent.originalEventName = `${eventData.type}.${eventData.subtype}`;
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
// Return early to avoid further transformation
|
|
1155
|
-
}
|
|
1156
|
-
// Handle legacy format with 'event' field but no 'type'
|
|
1157
|
-
else if (!eventData.type && eventData.event) {
|
|
1158
|
-
// Map common event names to proper type/subtype
|
|
1159
|
-
const eventName = eventData.event;
|
|
1160
|
-
|
|
1161
|
-
// Check for known event patterns
|
|
1162
|
-
if (eventName === 'TestStart' || eventName === 'TestEnd') {
|
|
1163
|
-
transformedEvent.type = 'test';
|
|
1164
|
-
transformedEvent.subtype = eventName.toLowerCase().replace('test', '');
|
|
1165
|
-
} else if (eventName === 'SubagentStart' || eventName === 'SubagentStop') {
|
|
1166
|
-
transformedEvent.type = 'subagent';
|
|
1167
|
-
transformedEvent.subtype = eventName.toLowerCase().replace('subagent', '');
|
|
1168
|
-
} else if (eventName === 'ToolCall') {
|
|
1169
|
-
transformedEvent.type = 'tool';
|
|
1170
|
-
transformedEvent.subtype = 'call';
|
|
1171
|
-
} else if (eventName === 'UserPrompt') {
|
|
1172
|
-
transformedEvent.type = 'hook';
|
|
1173
|
-
transformedEvent.subtype = 'user_prompt';
|
|
1174
|
-
} else {
|
|
1175
|
-
// Generic fallback for unknown event names
|
|
1176
|
-
// Use 'unknown' for type and the actual eventName for subtype
|
|
1177
|
-
transformedEvent.type = 'unknown';
|
|
1178
|
-
transformedEvent.subtype = eventName.toLowerCase();
|
|
1179
|
-
|
|
1180
|
-
// Prevent duplicate type/subtype values
|
|
1181
|
-
if (transformedEvent.type === transformedEvent.subtype) {
|
|
1182
|
-
transformedEvent.subtype = 'event';
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// Remove the 'event' field to avoid confusion
|
|
1187
|
-
delete transformedEvent.event;
|
|
1188
|
-
// Store original event name for display purposes
|
|
1189
|
-
transformedEvent.originalEventName = eventName;
|
|
1190
|
-
}
|
|
1191
|
-
// Handle standard format with 'type' field that needs transformation
|
|
1192
|
-
else if (eventData.type) {
|
|
1193
|
-
const type = eventData.type;
|
|
1194
|
-
|
|
1195
|
-
// Transform 'hook.subtype' format to separate type and subtype
|
|
1196
|
-
if (type.startsWith('hook.')) {
|
|
1197
|
-
const subtype = type.substring(5); // Remove 'hook.' prefix
|
|
1198
|
-
transformedEvent.type = 'hook';
|
|
1199
|
-
transformedEvent.subtype = subtype;
|
|
1200
|
-
transformedEvent.originalEventName = type;
|
|
1201
|
-
}
|
|
1202
|
-
// Transform 'code:*' events to proper code type
|
|
1203
|
-
// Handle multi-level subtypes like 'code:analysis:queued'
|
|
1204
|
-
else if (type.startsWith('code:')) {
|
|
1205
|
-
transformedEvent.type = 'code';
|
|
1206
|
-
// Replace colons with underscores in subtype for consistency
|
|
1207
|
-
const subtypePart = type.substring(5); // Remove 'code:' prefix
|
|
1208
|
-
transformedEvent.subtype = subtypePart.replace(/:/g, '_');
|
|
1209
|
-
transformedEvent.originalEventName = type;
|
|
1210
|
-
}
|
|
1211
|
-
// Transform other dotted types like 'session.started' -> type: 'session', subtype: 'started'
|
|
1212
|
-
else if (type.includes('.')) {
|
|
1213
|
-
const [mainType, ...subtypeParts] = type.split('.');
|
|
1214
|
-
transformedEvent.type = mainType;
|
|
1215
|
-
transformedEvent.subtype = subtypeParts.join('.');
|
|
1216
|
-
transformedEvent.originalEventName = type;
|
|
1217
|
-
}
|
|
1218
|
-
// Transform any remaining colon-separated types generically
|
|
1219
|
-
else if (type.includes(':')) {
|
|
1220
|
-
const parts = type.split(':', 2); // Split into max 2 parts
|
|
1221
|
-
transformedEvent.type = parts[0];
|
|
1222
|
-
// Replace any remaining colons with underscores in subtype
|
|
1223
|
-
transformedEvent.subtype = parts.length > 1 ? parts[1].replace(/:/g, '_') : 'generic';
|
|
1224
|
-
transformedEvent.originalEventName = type;
|
|
1225
|
-
}
|
|
1226
|
-
// If type doesn't need transformation but has no subtype, set a default
|
|
1227
|
-
else if (!eventData.subtype) {
|
|
1228
|
-
transformedEvent.subtype = 'generic';
|
|
1229
|
-
transformedEvent.originalEventName = type;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
// If no type and no event field, mark as unknown
|
|
1233
|
-
else {
|
|
1234
|
-
transformedEvent.type = 'unknown';
|
|
1235
|
-
transformedEvent.subtype = '';
|
|
1236
|
-
transformedEvent.originalEventName = 'unknown';
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// Extract and flatten data fields to top level for dashboard compatibility
|
|
1240
|
-
// The dashboard expects fields like tool_name, agent_type, etc. at the top level
|
|
1241
|
-
if (eventData.data && typeof eventData.data === 'object') {
|
|
1242
|
-
// Protected fields that should never be overwritten by data fields
|
|
1243
|
-
const protectedFields = ['type', 'subtype', 'timestamp', 'id', 'event', 'event_type', 'originalEventName'];
|
|
1244
|
-
|
|
1245
|
-
// Copy all data fields to the top level, except protected ones
|
|
1246
|
-
Object.keys(eventData.data).forEach(key => {
|
|
1247
|
-
// Only copy if not a protected field
|
|
1248
|
-
if (!protectedFields.includes(key)) {
|
|
1249
|
-
// Special handling for tool_parameters to ensure it's properly preserved
|
|
1250
|
-
// This is critical for file path extraction in file-tool-tracker
|
|
1251
|
-
if (key === 'tool_parameters' && typeof eventData.data[key] === 'object') {
|
|
1252
|
-
// Deep copy the tool_parameters object to preserve all nested fields
|
|
1253
|
-
transformedEvent[key] = JSON.parse(JSON.stringify(eventData.data[key]));
|
|
1254
|
-
} else {
|
|
1255
|
-
transformedEvent[key] = eventData.data[key];
|
|
1256
|
-
}
|
|
1257
|
-
} else {
|
|
1258
|
-
// Log debug info if data field would overwrite a protected field
|
|
1259
|
-
// Only log for non-timestamp fields to reduce noise
|
|
1260
|
-
if (key !== 'timestamp') {
|
|
1261
|
-
console.debug(`Protected field '${key}' in data object was not copied to top level to preserve event structure`);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
// Keep the original data object for backward compatibility
|
|
1267
|
-
transformedEvent.data = eventData.data;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// Add hook_event_name for ActivityTree compatibility
|
|
1271
|
-
// Map the type/subtype structure to the expected hook_event_name format
|
|
1272
|
-
if (transformedEvent.type === 'hook') {
|
|
1273
|
-
if (transformedEvent.subtype === 'pre_tool') {
|
|
1274
|
-
transformedEvent.hook_event_name = 'PreToolUse';
|
|
1275
|
-
} else if (transformedEvent.subtype === 'post_tool') {
|
|
1276
|
-
transformedEvent.hook_event_name = 'PostToolUse';
|
|
1277
|
-
} else if (transformedEvent.subtype === 'subagent_start') {
|
|
1278
|
-
transformedEvent.hook_event_name = 'SubagentStart';
|
|
1279
|
-
} else if (transformedEvent.subtype === 'subagent_stop') {
|
|
1280
|
-
transformedEvent.hook_event_name = 'SubagentStop';
|
|
1281
|
-
} else if (transformedEvent.subtype === 'todo_write') {
|
|
1282
|
-
transformedEvent.hook_event_name = 'TodoWrite';
|
|
1283
|
-
} else if (transformedEvent.subtype === 'start') {
|
|
1284
|
-
transformedEvent.hook_event_name = 'Start';
|
|
1285
|
-
} else if (transformedEvent.subtype === 'stop') {
|
|
1286
|
-
transformedEvent.hook_event_name = 'Stop';
|
|
1287
|
-
}
|
|
1288
|
-
} else if (transformedEvent.type === 'subagent') {
|
|
1289
|
-
if (transformedEvent.subtype === 'start') {
|
|
1290
|
-
transformedEvent.hook_event_name = 'SubagentStart';
|
|
1291
|
-
} else if (transformedEvent.subtype === 'stop') {
|
|
1292
|
-
transformedEvent.hook_event_name = 'SubagentStop';
|
|
1293
|
-
}
|
|
1294
|
-
} else if (transformedEvent.type === 'todo' && transformedEvent.subtype === 'updated') {
|
|
1295
|
-
transformedEvent.hook_event_name = 'TodoWrite';
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// Debug logging for tool events
|
|
1299
|
-
if (transformedEvent.type === 'hook' && (transformedEvent.subtype === 'pre_tool' || transformedEvent.subtype === 'post_tool')) {
|
|
1300
|
-
console.log('Transformed tool event:', {
|
|
1301
|
-
type: transformedEvent.type,
|
|
1302
|
-
subtype: transformedEvent.subtype,
|
|
1303
|
-
hook_event_name: transformedEvent.hook_event_name,
|
|
1304
|
-
tool_name: transformedEvent.tool_name,
|
|
1305
|
-
has_tool_parameters: !!transformedEvent.tool_parameters,
|
|
1306
|
-
tool_parameters: transformedEvent.tool_parameters,
|
|
1307
|
-
has_data: !!transformedEvent.data,
|
|
1308
|
-
keys: Object.keys(transformedEvent).filter(k => k !== 'data')
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
|
-
// Extra debug logging for file-related tools
|
|
1312
|
-
const fileTools = ['Read', 'Write', 'Edit', 'MultiEdit', 'NotebookEdit'];
|
|
1313
|
-
if (fileTools.includes(transformedEvent.tool_name)) {
|
|
1314
|
-
console.log('File tool event details:', {
|
|
1315
|
-
tool_name: transformedEvent.tool_name,
|
|
1316
|
-
file_path: transformedEvent.tool_parameters?.file_path,
|
|
1317
|
-
path: transformedEvent.tool_parameters?.path,
|
|
1318
|
-
notebook_path: transformedEvent.tool_parameters?.notebook_path,
|
|
1319
|
-
full_parameters: transformedEvent.tool_parameters
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
return transformedEvent;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* Get current events and sessions
|
|
1329
|
-
* @returns {Object} Current state
|
|
1330
|
-
*/
|
|
1331
|
-
getState() {
|
|
1332
|
-
return {
|
|
1333
|
-
events: this.events,
|
|
1334
|
-
sessions: this.sessions,
|
|
1335
|
-
currentSessionId: this.currentSessionId
|
|
1336
|
-
};
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
/**
|
|
1340
|
-
* Start health monitoring
|
|
1341
|
-
* Detects stale connections and triggers reconnection
|
|
1342
|
-
*/
|
|
1343
|
-
startHealthMonitoring() {
|
|
1344
|
-
this.healthCheckInterval = setInterval(() => {
|
|
1345
|
-
if (this.isConnected && this.lastPingTime) {
|
|
1346
|
-
const timeSinceLastPing = Date.now() - this.lastPingTime;
|
|
1347
|
-
|
|
1348
|
-
if (timeSinceLastPing > this.pingTimeout) {
|
|
1349
|
-
console.warn(`No ping from server for ${timeSinceLastPing/1000}s, connection may be stale`);
|
|
1350
|
-
|
|
1351
|
-
// Force reconnection
|
|
1352
|
-
if (this.socket) {
|
|
1353
|
-
console.log('Forcing reconnection due to stale connection...');
|
|
1354
|
-
this.socket.disconnect();
|
|
1355
|
-
setTimeout(() => {
|
|
1356
|
-
if (this.port) {
|
|
1357
|
-
this.connect(this.port);
|
|
1358
|
-
}
|
|
1359
|
-
}, 1000);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}, 10000); // Check every 10 seconds
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
/**
|
|
1367
|
-
* Stop health monitoring
|
|
1368
|
-
*/
|
|
1369
|
-
stopHealthMonitoring() {
|
|
1370
|
-
if (this.healthCheckInterval) {
|
|
1371
|
-
clearInterval(this.healthCheckInterval);
|
|
1372
|
-
this.healthCheckInterval = null;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
/**
|
|
1377
|
-
* Start periodic status check as fallback mechanism
|
|
1378
|
-
* This ensures the UI stays in sync with actual socket state
|
|
1379
|
-
*/
|
|
1380
|
-
startStatusCheckFallback() {
|
|
1381
|
-
// Check status every 2 seconds
|
|
1382
|
-
setInterval(() => {
|
|
1383
|
-
this.checkAndUpdateStatus();
|
|
1384
|
-
}, 2000);
|
|
1385
|
-
|
|
1386
|
-
// Initial check after DOM is ready
|
|
1387
|
-
if (document.readyState === 'loading') {
|
|
1388
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1389
|
-
setTimeout(() => this.checkAndUpdateStatus(), 100);
|
|
1390
|
-
});
|
|
1391
|
-
} else {
|
|
1392
|
-
setTimeout(() => this.checkAndUpdateStatus(), 100);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Check actual socket state and update UI if necessary
|
|
1398
|
-
*/
|
|
1399
|
-
checkAndUpdateStatus() {
|
|
1400
|
-
let actualStatus = 'Disconnected';
|
|
1401
|
-
let actualType = 'disconnected';
|
|
1402
|
-
|
|
1403
|
-
if (this.socket) {
|
|
1404
|
-
if (this.socket.connected) {
|
|
1405
|
-
actualStatus = 'Connected';
|
|
1406
|
-
actualType = 'connected';
|
|
1407
|
-
this.isConnected = true;
|
|
1408
|
-
this.isConnecting = false;
|
|
1409
|
-
|
|
1410
|
-
// Expose socket globally when connected
|
|
1411
|
-
if (!window.socket) {
|
|
1412
|
-
window.socket = this.socket;
|
|
1413
|
-
console.log('SocketClient: Exposed socket globally as window.socket');
|
|
1414
|
-
}
|
|
1415
|
-
} else if (this.socket.connecting || this.isConnecting) {
|
|
1416
|
-
actualStatus = 'Connecting...';
|
|
1417
|
-
actualType = 'connecting';
|
|
1418
|
-
this.isConnected = false;
|
|
1419
|
-
} else {
|
|
1420
|
-
actualStatus = 'Disconnected';
|
|
1421
|
-
actualType = 'disconnected';
|
|
1422
|
-
this.isConnected = false;
|
|
1423
|
-
this.isConnecting = false;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
// Always update status to ensure consistency
|
|
1428
|
-
this.updateConnectionStatusDOM(actualStatus, actualType);
|
|
1429
|
-
|
|
1430
|
-
// Also ensure state is consistent
|
|
1431
|
-
const statusElement = document.getElementById('connection-status');
|
|
1432
|
-
if (statusElement) {
|
|
1433
|
-
const currentText = statusElement.textContent.replace('●', '').trim();
|
|
1434
|
-
if (currentText !== actualStatus) {
|
|
1435
|
-
console.log(`SocketClient: Status sync - updating from '${currentText}' to '${actualStatus}'`);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
/**
|
|
1441
|
-
* Clean up resources
|
|
1442
|
-
*/
|
|
1443
|
-
destroy() {
|
|
1444
|
-
this.stopHealthMonitoring();
|
|
1445
|
-
if (this.socket) {
|
|
1446
|
-
this.socket.disconnect();
|
|
1447
|
-
this.socket = null;
|
|
1448
|
-
}
|
|
1449
|
-
this.eventQueue = [];
|
|
1450
|
-
this.pendingEmissions.clear();
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
/**
|
|
1454
|
-
* Get connection metrics
|
|
1455
|
-
* @returns {Object} Connection metrics
|
|
1456
|
-
*/
|
|
1457
|
-
getConnectionMetrics() {
|
|
1458
|
-
return {
|
|
1459
|
-
isConnected: this.isConnected,
|
|
1460
|
-
uptime: this.lastConnectTime ? (Date.now() - this.lastConnectTime) / 1000 : 0,
|
|
1461
|
-
lastPing: this.lastPingTime ? (Date.now() - this.lastPingTime) / 1000 : null,
|
|
1462
|
-
queuedEvents: this.eventQueue.length,
|
|
1463
|
-
pendingEmissions: this.pendingEmissions.size,
|
|
1464
|
-
retryAttempts: this.retryAttempts
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// ES6 Module export
|
|
1470
|
-
export { SocketClient };
|
|
1471
|
-
export default SocketClient;
|
|
1472
|
-
|
|
1473
|
-
// Backward compatibility - keep window export for non-module usage
|
|
1474
|
-
window.SocketClient = SocketClient;
|