claude-mpm 5.1.9__py3-none-any.whl → 5.4.22__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (176) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +4 -0
  3. claude_mpm/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +1 -1
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +290 -34
  5. claude_mpm/agents/agent_loader.py +13 -44
  6. claude_mpm/agents/templates/circuit-breakers.md +138 -1
  7. claude_mpm/cli/__main__.py +4 -0
  8. claude_mpm/cli/chrome_devtools_installer.py +175 -0
  9. claude_mpm/cli/commands/agent_state_manager.py +8 -17
  10. claude_mpm/cli/commands/agents.py +0 -31
  11. claude_mpm/cli/commands/auto_configure.py +210 -25
  12. claude_mpm/cli/commands/config.py +88 -2
  13. claude_mpm/cli/commands/configure.py +1097 -158
  14. claude_mpm/cli/commands/configure_agent_display.py +15 -6
  15. claude_mpm/cli/commands/mpm_init/core.py +160 -46
  16. claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
  17. claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
  18. claude_mpm/cli/commands/skills.py +214 -189
  19. claude_mpm/cli/commands/summarize.py +413 -0
  20. claude_mpm/cli/executor.py +11 -3
  21. claude_mpm/cli/parsers/agents_parser.py +0 -9
  22. claude_mpm/cli/parsers/auto_configure_parser.py +0 -138
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/config_parser.py +153 -83
  25. claude_mpm/cli/parsers/skills_parser.py +3 -2
  26. claude_mpm/cli/startup.py +550 -94
  27. claude_mpm/commands/mpm-config.md +265 -0
  28. claude_mpm/commands/mpm-help.md +14 -95
  29. claude_mpm/commands/mpm-organize.md +500 -0
  30. claude_mpm/config/agent_sources.py +27 -0
  31. claude_mpm/core/framework/formatters/content_formatter.py +3 -13
  32. claude_mpm/core/framework/loaders/agent_loader.py +8 -5
  33. claude_mpm/core/framework_loader.py +4 -2
  34. claude_mpm/core/logger.py +13 -0
  35. claude_mpm/core/socketio_pool.py +3 -3
  36. claude_mpm/core/unified_agent_registry.py +5 -15
  37. claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
  38. claude_mpm/hooks/claude_hooks/event_handlers.py +211 -78
  39. claude_mpm/hooks/claude_hooks/hook_handler.py +6 -0
  40. claude_mpm/hooks/claude_hooks/installer.py +33 -10
  41. claude_mpm/hooks/claude_hooks/memory_integration.py +26 -9
  42. claude_mpm/hooks/claude_hooks/response_tracking.py +2 -3
  43. claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
  44. claude_mpm/hooks/memory_integration_hook.py +46 -1
  45. claude_mpm/init.py +0 -19
  46. claude_mpm/scripts/claude-hook-handler.sh +58 -18
  47. claude_mpm/scripts/launch_monitor.py +93 -13
  48. claude_mpm/scripts/start_activity_logging.py +0 -0
  49. claude_mpm/services/agents/agent_recommendation_service.py +278 -0
  50. claude_mpm/services/agents/agent_review_service.py +280 -0
  51. claude_mpm/services/agents/deployment/agent_discovery_service.py +2 -3
  52. claude_mpm/services/agents/deployment/agent_template_builder.py +4 -2
  53. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +78 -9
  54. claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +335 -53
  55. claude_mpm/services/agents/git_source_manager.py +34 -0
  56. claude_mpm/services/agents/loading/base_agent_manager.py +1 -13
  57. claude_mpm/services/agents/sources/git_source_sync_service.py +8 -1
  58. claude_mpm/services/agents/toolchain_detector.py +10 -6
  59. claude_mpm/services/analysis/__init__.py +11 -1
  60. claude_mpm/services/analysis/clone_detector.py +1030 -0
  61. claude_mpm/services/command_deployment_service.py +81 -10
  62. claude_mpm/services/event_bus/config.py +3 -1
  63. claude_mpm/services/git/git_operations_service.py +93 -8
  64. claude_mpm/services/monitor/daemon.py +9 -2
  65. claude_mpm/services/monitor/daemon_manager.py +39 -3
  66. claude_mpm/services/monitor/server.py +225 -19
  67. claude_mpm/services/self_upgrade_service.py +120 -12
  68. claude_mpm/services/skills/__init__.py +3 -0
  69. claude_mpm/services/skills/git_skill_source_manager.py +32 -2
  70. claude_mpm/services/skills/selective_skill_deployer.py +704 -0
  71. claude_mpm/services/skills/skill_to_agent_mapper.py +406 -0
  72. claude_mpm/services/skills_deployer.py +126 -9
  73. claude_mpm/services/socketio/event_normalizer.py +15 -1
  74. claude_mpm/services/socketio/server/core.py +160 -21
  75. claude_mpm/services/version_control/git_operations.py +103 -0
  76. claude_mpm/utils/agent_filters.py +17 -44
  77. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/METADATA +47 -84
  78. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/RECORD +82 -161
  79. claude_mpm-5.4.22.dist-info/entry_points.txt +5 -0
  80. claude_mpm-5.4.22.dist-info/licenses/LICENSE +94 -0
  81. claude_mpm-5.4.22.dist-info/licenses/LICENSE-FAQ.md +153 -0
  82. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +0 -292
  83. claude_mpm/agents/BASE_DOCUMENTATION.md +0 -53
  84. claude_mpm/agents/BASE_ENGINEER.md +0 -658
  85. claude_mpm/agents/BASE_OPS.md +0 -219
  86. claude_mpm/agents/BASE_PM.md +0 -480
  87. claude_mpm/agents/BASE_PROMPT_ENGINEER.md +0 -787
  88. claude_mpm/agents/BASE_QA.md +0 -167
  89. claude_mpm/agents/BASE_RESEARCH.md +0 -53
  90. claude_mpm/agents/base_agent.json +0 -31
  91. claude_mpm/agents/base_agent_loader.py +0 -601
  92. claude_mpm/cli/commands/agents_detect.py +0 -380
  93. claude_mpm/cli/commands/agents_recommend.py +0 -309
  94. claude_mpm/cli/ticket_cli.py +0 -35
  95. claude_mpm/commands/mpm-agents-auto-configure.md +0 -278
  96. claude_mpm/commands/mpm-agents-detect.md +0 -177
  97. claude_mpm/commands/mpm-agents-list.md +0 -131
  98. claude_mpm/commands/mpm-agents-recommend.md +0 -223
  99. claude_mpm/commands/mpm-config-view.md +0 -150
  100. claude_mpm/commands/mpm-ticket-organize.md +0 -304
  101. claude_mpm/dashboard/analysis_runner.py +0 -455
  102. claude_mpm/dashboard/index.html +0 -13
  103. claude_mpm/dashboard/open_dashboard.py +0 -66
  104. claude_mpm/dashboard/static/css/activity.css +0 -1958
  105. claude_mpm/dashboard/static/css/connection-status.css +0 -370
  106. claude_mpm/dashboard/static/css/dashboard.css +0 -4701
  107. claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
  108. claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
  109. claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
  110. claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
  111. claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
  112. claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
  113. claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
  114. claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
  115. claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
  116. claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
  117. claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
  118. claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
  119. claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
  120. claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
  121. claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
  122. claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
  123. claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
  124. claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
  125. claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
  126. claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
  127. claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
  128. claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
  129. claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
  130. claude_mpm/dashboard/static/js/connection-manager.js +0 -536
  131. claude_mpm/dashboard/static/js/dashboard.js +0 -1914
  132. claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
  133. claude_mpm/dashboard/static/js/socket-client.js +0 -1474
  134. claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
  135. claude_mpm/dashboard/static/socket.io.min.js +0 -7
  136. claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
  137. claude_mpm/dashboard/templates/code_simple.html +0 -153
  138. claude_mpm/dashboard/templates/index.html +0 -606
  139. claude_mpm/dashboard/test_dashboard.html +0 -372
  140. claude_mpm/scripts/mcp_server.py +0 -75
  141. claude_mpm/scripts/mcp_wrapper.py +0 -39
  142. claude_mpm/services/mcp_gateway/__init__.py +0 -159
  143. claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
  144. claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
  145. claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
  146. claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
  147. claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
  148. claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
  149. claude_mpm/services/mcp_gateway/core/base.py +0 -312
  150. claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
  151. claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
  152. claude_mpm/services/mcp_gateway/core/process_pool.py +0 -977
  153. claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
  154. claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
  155. claude_mpm/services/mcp_gateway/main.py +0 -589
  156. claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
  157. claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
  158. claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
  159. claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
  160. claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
  161. claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
  162. claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
  163. claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
  164. claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
  165. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
  166. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
  167. claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
  168. claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
  169. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
  170. claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
  171. claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
  172. claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
  173. claude_mpm-5.1.9.dist-info/entry_points.txt +0 -10
  174. claude_mpm-5.1.9.dist-info/licenses/LICENSE +0 -21
  175. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.dist-info}/WHEEL +0 -0
  176. {claude_mpm-5.1.9.dist-info → claude_mpm-5.4.22.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;