claude-mpm 4.1.6__py3-none-any.whl → 4.1.8__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.
@@ -222,6 +222,16 @@ class ConfigureCommand(BaseCommand):
222
222
  if getattr(args, "version_info", False):
223
223
  return self._show_version_info()
224
224
 
225
+ # Handle hook installation
226
+ if getattr(args, "install_hooks", False):
227
+ return self._install_hooks(force=getattr(args, "force", False))
228
+
229
+ if getattr(args, "verify_hooks", False):
230
+ return self._verify_hooks()
231
+
232
+ if getattr(args, "uninstall_hooks", False):
233
+ return self._uninstall_hooks()
234
+
225
235
  # Handle direct navigation options
226
236
  if getattr(args, "agents", False):
227
237
  return self._run_agent_management()
@@ -1018,7 +1028,11 @@ class ConfigureCommand(BaseCommand):
1018
1028
  import subprocess
1019
1029
 
1020
1030
  result = subprocess.run(
1021
- ["claude", "--version"], capture_output=True, text=True, timeout=5, check=False
1031
+ ["claude", "--version"],
1032
+ capture_output=True,
1033
+ text=True,
1034
+ timeout=5,
1035
+ check=False,
1022
1036
  )
1023
1037
  if result.returncode == 0:
1024
1038
  claude_version = result.stdout.strip()
@@ -1157,7 +1171,11 @@ Directory: {self.project_dir}
1157
1171
  import subprocess
1158
1172
 
1159
1173
  result = subprocess.run(
1160
- ["claude", "--version"], capture_output=True, text=True, timeout=5, check=False
1174
+ ["claude", "--version"],
1175
+ capture_output=True,
1176
+ text=True,
1177
+ timeout=5,
1178
+ check=False,
1161
1179
  )
1162
1180
  if result.returncode == 0:
1163
1181
  data["claude_version"] = result.stdout.strip()
@@ -1175,6 +1193,137 @@ Directory: {self.project_dir}
1175
1193
 
1176
1194
  return CommandResult.success_result("Version information displayed", data=data)
1177
1195
 
1196
+ def _install_hooks(self, force: bool = False) -> CommandResult:
1197
+ """Install Claude MPM hooks for Claude Code integration."""
1198
+ try:
1199
+ from ...hooks.claude_hooks.installer import HookInstaller
1200
+
1201
+ installer = HookInstaller()
1202
+
1203
+ # Check current status first
1204
+ status = installer.get_status()
1205
+ if status["installed"] and not force:
1206
+ self.console.print("[yellow]Hooks are already installed.[/yellow]")
1207
+ self.console.print("Use --force to reinstall.")
1208
+
1209
+ if not status["valid"]:
1210
+ self.console.print("\n[red]However, there are issues:[/red]")
1211
+ for issue in status["issues"]:
1212
+ self.console.print(f" - {issue}")
1213
+
1214
+ return CommandResult.success_result(
1215
+ "Hooks already installed", data=status
1216
+ )
1217
+
1218
+ # Install hooks
1219
+ self.console.print("[cyan]Installing Claude MPM hooks...[/cyan]")
1220
+ success = installer.install_hooks(force=force)
1221
+
1222
+ if success:
1223
+ self.console.print("[green]✓ Hooks installed successfully![/green]")
1224
+ self.console.print("\nYou can now use /mpm commands in Claude Code:")
1225
+ self.console.print(" /mpm - Show help")
1226
+ self.console.print(" /mpm status - Show claude-mpm status")
1227
+
1228
+ # Verify installation
1229
+ is_valid, issues = installer.verify_hooks()
1230
+ if not is_valid:
1231
+ self.console.print(
1232
+ "\n[yellow]Warning: Installation completed but verification found issues:[/yellow]"
1233
+ )
1234
+ for issue in issues:
1235
+ self.console.print(f" - {issue}")
1236
+
1237
+ return CommandResult.success_result("Hooks installed successfully")
1238
+ self.console.print("[red]✗ Hook installation failed[/red]")
1239
+ return CommandResult.error_result("Hook installation failed")
1240
+
1241
+ except ImportError:
1242
+ self.console.print("[red]Error: HookInstaller module not found[/red]")
1243
+ self.console.print("Please ensure claude-mpm is properly installed.")
1244
+ return CommandResult.error_result("HookInstaller module not found")
1245
+ except Exception as e:
1246
+ self.logger.error(f"Hook installation error: {e}", exc_info=True)
1247
+ return CommandResult.error_result(f"Hook installation failed: {e}")
1248
+
1249
+ def _verify_hooks(self) -> CommandResult:
1250
+ """Verify that Claude MPM hooks are properly installed."""
1251
+ try:
1252
+ from ...hooks.claude_hooks.installer import HookInstaller
1253
+
1254
+ installer = HookInstaller()
1255
+ status = installer.get_status()
1256
+
1257
+ self.console.print("[bold]Hook Installation Status[/bold]\n")
1258
+
1259
+ if status["installed"]:
1260
+ self.console.print(
1261
+ f"[green]✓[/green] Hooks installed at: {status['hook_script']}"
1262
+ )
1263
+ else:
1264
+ self.console.print("[red]✗[/red] Hooks not installed")
1265
+
1266
+ if status["settings_file"]:
1267
+ self.console.print(
1268
+ f"[green]✓[/green] Settings file: {status['settings_file']}"
1269
+ )
1270
+ else:
1271
+ self.console.print("[red]✗[/red] Settings file not found")
1272
+
1273
+ if status.get("configured_events"):
1274
+ self.console.print(
1275
+ f"[green]✓[/green] Configured events: {', '.join(status['configured_events'])}"
1276
+ )
1277
+ else:
1278
+ self.console.print("[red]✗[/red] No events configured")
1279
+
1280
+ if status["valid"]:
1281
+ self.console.print("\n[green]All checks passed![/green]")
1282
+ else:
1283
+ self.console.print("\n[red]Issues found:[/red]")
1284
+ for issue in status["issues"]:
1285
+ self.console.print(f" - {issue}")
1286
+
1287
+ return CommandResult.success_result(
1288
+ "Hook verification complete", data=status
1289
+ )
1290
+
1291
+ except ImportError:
1292
+ self.console.print("[red]Error: HookInstaller module not found[/red]")
1293
+ return CommandResult.error_result("HookInstaller module not found")
1294
+ except Exception as e:
1295
+ self.logger.error(f"Hook verification error: {e}", exc_info=True)
1296
+ return CommandResult.error_result(f"Hook verification failed: {e}")
1297
+
1298
+ def _uninstall_hooks(self) -> CommandResult:
1299
+ """Uninstall Claude MPM hooks."""
1300
+ try:
1301
+ from ...hooks.claude_hooks.installer import HookInstaller
1302
+
1303
+ installer = HookInstaller()
1304
+
1305
+ # Confirm uninstallation
1306
+ if not Confirm.ask(
1307
+ "[yellow]Are you sure you want to uninstall Claude MPM hooks?[/yellow]"
1308
+ ):
1309
+ return CommandResult.success_result("Uninstallation cancelled")
1310
+
1311
+ self.console.print("[cyan]Uninstalling Claude MPM hooks...[/cyan]")
1312
+ success = installer.uninstall_hooks()
1313
+
1314
+ if success:
1315
+ self.console.print("[green]✓ Hooks uninstalled successfully![/green]")
1316
+ return CommandResult.success_result("Hooks uninstalled successfully")
1317
+ self.console.print("[red]✗ Hook uninstallation failed[/red]")
1318
+ return CommandResult.error_result("Hook uninstallation failed")
1319
+
1320
+ except ImportError:
1321
+ self.console.print("[red]Error: HookInstaller module not found[/red]")
1322
+ return CommandResult.error_result("HookInstaller module not found")
1323
+ except Exception as e:
1324
+ self.logger.error(f"Hook uninstallation error: {e}", exc_info=True)
1325
+ return CommandResult.error_result(f"Hook uninstallation failed: {e}")
1326
+
1178
1327
  def _run_agent_management(self) -> CommandResult:
1179
1328
  """Jump directly to agent management."""
1180
1329
  try:
@@ -1200,7 +1200,11 @@ class SettingsScreen(Container):
1200
1200
  import subprocess
1201
1201
 
1202
1202
  result = subprocess.run(
1203
- ["claude", "--version"], capture_output=True, text=True, timeout=5, check=False
1203
+ ["claude", "--version"],
1204
+ capture_output=True,
1205
+ text=True,
1206
+ timeout=5,
1207
+ check=False,
1204
1208
  )
1205
1209
  if result.returncode == 0:
1206
1210
  claude_version = result.stdout.strip()
@@ -96,6 +96,29 @@ def add_configure_subparser(subparsers) -> argparse.ArgumentParser:
96
96
  help="Import configuration from a file",
97
97
  )
98
98
 
99
+ # Hook management options
100
+ hooks_group = configure_parser.add_argument_group("hook management")
101
+ hooks_group.add_argument(
102
+ "--install-hooks",
103
+ action="store_true",
104
+ help="Install Claude MPM hooks for Claude Code integration",
105
+ )
106
+ hooks_group.add_argument(
107
+ "--verify-hooks",
108
+ action="store_true",
109
+ help="Verify that Claude MPM hooks are properly installed",
110
+ )
111
+ hooks_group.add_argument(
112
+ "--uninstall-hooks",
113
+ action="store_true",
114
+ help="Uninstall Claude MPM hooks",
115
+ )
116
+ hooks_group.add_argument(
117
+ "--force",
118
+ action="store_true",
119
+ help="Force reinstallation of hooks even if they already exist",
120
+ )
121
+
99
122
  # Display options
100
123
  display_group = configure_parser.add_argument_group("display options")
101
124
  display_group.add_argument(
@@ -10,6 +10,8 @@ WHY configuration management:
10
10
  - Supports multiple deployment scenarios (local, PyPI, Docker, etc.)
11
11
  - Provides environment-specific defaults
12
12
  - Allows runtime configuration overrides
13
+
14
+ CRITICAL: Ping/pong settings MUST match between client and server to prevent disconnections!
13
15
  """
14
16
 
15
17
  import os
@@ -19,6 +21,30 @@ from typing import Any, Dict, List, Optional
19
21
  # Import constants for default values
20
22
  from claude_mpm.core.constants import NetworkConfig, RetryConfig, SystemLimits
21
23
 
24
+ # Connection stability settings - MUST be consistent between client and server
25
+ CONNECTION_CONFIG = {
26
+ # Ping/pong intervals (milliseconds for client, seconds for server)
27
+ "ping_interval_ms": 25000, # 25 seconds (for client JavaScript)
28
+ "ping_interval": 25, # 25 seconds (for server Python) - reduced for stability
29
+ "ping_timeout_ms": 20000, # 20 seconds (for client JavaScript)
30
+ "ping_timeout": 20, # 20 seconds (for server Python)
31
+ # Connection management
32
+ "stale_timeout": 90, # 90 seconds before considering connection stale (was 180)
33
+ "health_check_interval": 30, # Health check every 30 seconds
34
+ "event_ttl": 300, # Keep events for 5 minutes for replay
35
+ "connection_timeout": 10, # 10 seconds for initial connection timeout
36
+ # Client reconnection settings
37
+ "reconnection_attempts": 5, # Number of reconnection attempts
38
+ "reconnection_delay": 1000, # Initial delay in ms
39
+ "reconnection_delay_max": 5000, # Maximum delay in ms
40
+ # Feature flags
41
+ "enable_extra_heartbeat": False, # Disable redundant heartbeats
42
+ "enable_health_monitoring": True, # Enable connection health monitoring
43
+ # Buffer settings
44
+ "max_events_buffer": 1000, # Maximum events to buffer per client
45
+ "max_http_buffer_size": 1e8, # 100MB max buffer for large payloads
46
+ }
47
+
22
48
 
23
49
  @dataclass
24
50
  class SocketIOConfig:
@@ -29,11 +55,14 @@ class SocketIOConfig:
29
55
  port: int = NetworkConfig.DEFAULT_DASHBOARD_PORT
30
56
  server_id: Optional[str] = None
31
57
 
32
- # Connection settings
58
+ # Connection settings - Use centralized config for consistency
33
59
  cors_allowed_origins: str = "*" # Configure properly for production
34
- ping_timeout: int = NetworkConfig.PING_TIMEOUT_STANDARD
35
- ping_interval: int = NetworkConfig.PING_INTERVAL_STANDARD
36
- max_http_buffer_size: int = 1000000
60
+ ping_timeout: int = CONNECTION_CONFIG["ping_timeout"] # 20 seconds
61
+ ping_interval: int = CONNECTION_CONFIG["ping_interval"] # 25 seconds (was 45)
62
+ max_http_buffer_size: int = int(CONNECTION_CONFIG["max_http_buffer_size"])
63
+ connection_timeout: int = CONNECTION_CONFIG.get(
64
+ "connection_timeout", 10
65
+ ) # 10 seconds
37
66
 
38
67
  # Compatibility settings
39
68
  min_client_version: str = "0.7.0"
@@ -38,10 +38,10 @@ class SocketClient {
38
38
  this.eventQueue = [];
39
39
  this.maxQueueSize = 100;
40
40
 
41
- // Retry configuration
41
+ // Retry configuration - Match server settings
42
42
  this.retryAttempts = 0;
43
- this.maxRetryAttempts = 3;
44
- this.retryDelays = [1000, 2000, 4000]; // Exponential backoff
43
+ this.maxRetryAttempts = 5; // Increased from 3 to 5 for better stability
44
+ this.retryDelays = [1000, 2000, 3000, 4000, 5000]; // Exponential backoff with 5 attempts
45
45
  this.pendingEmissions = new Map(); // Track pending emissions for retry
46
46
 
47
47
  // Health monitoring
@@ -98,12 +98,12 @@ class SocketClient {
98
98
  reconnection: true,
99
99
  reconnectionDelay: 1000,
100
100
  reconnectionDelayMax: 5000,
101
- reconnectionAttempts: Infinity, // Keep trying indefinitely
102
- timeout: 20000, // Increase connection timeout
101
+ reconnectionAttempts: 5, // Try 5 times then stop (was Infinity which can cause issues)
102
+ timeout: 20000, // Connection timeout
103
103
  forceNew: true,
104
104
  transports: ['websocket', 'polling'],
105
- pingInterval: 25000, // Match server setting
106
- pingTimeout: 60000 // Match server setting
105
+ pingInterval: 45000, // CRITICAL: Must match server's 45 seconds
106
+ pingTimeout: 20000 // CRITICAL: Must match server's 20 seconds
107
107
  });
108
108
 
109
109
  this.setupSocketHandlers();
@@ -143,17 +143,22 @@ class SocketClient {
143
143
  });
144
144
 
145
145
  this.socket.on('disconnect', (reason) => {
146
- console.log('Disconnected from server:', reason);
146
+ // Enhanced logging for debugging disconnection issues
147
+ const disconnectInfo = {
148
+ reason: reason,
149
+ timestamp: new Date().toISOString(),
150
+ wasConnected: this.isConnected,
151
+ uptimeSeconds: this.lastConnectTime ? ((Date.now() - this.lastConnectTime) / 1000).toFixed(1) : 0,
152
+ lastPing: this.lastPingTime ? ((Date.now() - this.lastPingTime) / 1000).toFixed(1) + 's ago' : 'never',
153
+ lastPong: this.lastPongTime ? ((Date.now() - this.lastPongTime) / 1000).toFixed(1) + 's ago' : 'never'
154
+ };
155
+
156
+ console.log('Disconnected from server:', disconnectInfo);
157
+
147
158
  this.isConnected = false;
148
159
  this.isConnecting = false;
149
160
  this.disconnectTime = Date.now();
150
161
 
151
- // Calculate uptime
152
- if (this.lastConnectTime) {
153
- const uptime = (Date.now() - this.lastConnectTime) / 1000;
154
- console.log(`Connection uptime was ${uptime.toFixed(1)}s`);
155
- }
156
-
157
162
  this.notifyConnectionStatus(`Disconnected: ${reason}`, 'disconnected');
158
163
 
159
164
  // Emit disconnect callback
@@ -161,8 +166,21 @@ class SocketClient {
161
166
  callback(reason)
162
167
  );
163
168
 
164
- // Start auto-reconnect if it was an unexpected disconnect
165
- if (reason === 'transport close' || reason === 'ping timeout') {
169
+ // Detailed reason analysis for auto-reconnect decision
170
+ const reconnectReasons = [
171
+ 'transport close', // Network issue
172
+ 'ping timeout', // Server not responding
173
+ 'transport error', // Connection error
174
+ 'io server disconnect', // Server initiated disconnect (might be restart)
175
+ ];
176
+
177
+ if (reconnectReasons.includes(reason)) {
178
+ console.log(`Auto-reconnect triggered for reason: ${reason}`);
179
+ this.scheduleReconnect();
180
+ } else if (reason === 'io client disconnect') {
181
+ console.log('Client-initiated disconnect, not auto-reconnecting');
182
+ } else {
183
+ console.log(`Unknown disconnect reason: ${reason}, attempting reconnect anyway`);
166
184
  this.scheduleReconnect();
167
185
  }
168
186
  });
@@ -222,6 +240,12 @@ class SocketClient {
222
240
  });
223
241
  });
224
242
 
243
+ // Track pong responses from server
244
+ this.socket.on('pong', (data) => {
245
+ this.lastPongTime = Date.now();
246
+ // console.log('Received pong from server');
247
+ });
248
+
225
249
  // Session and event handlers (legacy/fallback)
226
250
  this.socket.on('session.started', (data) => {
227
251
  this.addEvent({ type: 'session', subtype: 'started', timestamp: new Date().toISOString(), data });