claude-mpm 3.1.3__py3-none-any.whl → 3.3.0__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.
Files changed (80) hide show
  1. claude_mpm/__init__.py +3 -3
  2. claude_mpm/__main__.py +0 -17
  3. claude_mpm/agents/INSTRUCTIONS.md +149 -17
  4. claude_mpm/agents/backups/INSTRUCTIONS.md +238 -0
  5. claude_mpm/agents/base_agent.json +1 -1
  6. claude_mpm/agents/templates/pm.json +25 -0
  7. claude_mpm/agents/templates/research.json +2 -1
  8. claude_mpm/cli/__init__.py +19 -23
  9. claude_mpm/cli/commands/__init__.py +3 -1
  10. claude_mpm/cli/commands/agents.py +7 -18
  11. claude_mpm/cli/commands/info.py +5 -10
  12. claude_mpm/cli/commands/memory.py +232 -0
  13. claude_mpm/cli/commands/run.py +501 -28
  14. claude_mpm/cli/commands/tickets.py +10 -17
  15. claude_mpm/cli/commands/ui.py +15 -37
  16. claude_mpm/cli/parser.py +91 -1
  17. claude_mpm/cli/utils.py +9 -28
  18. claude_mpm/config/socketio_config.py +256 -0
  19. claude_mpm/constants.py +9 -0
  20. claude_mpm/core/__init__.py +2 -2
  21. claude_mpm/core/agent_registry.py +4 -4
  22. claude_mpm/core/claude_runner.py +919 -0
  23. claude_mpm/core/config.py +21 -1
  24. claude_mpm/core/factories.py +1 -1
  25. claude_mpm/core/hook_manager.py +196 -0
  26. claude_mpm/core/pm_hook_interceptor.py +205 -0
  27. claude_mpm/core/service_registry.py +1 -1
  28. claude_mpm/core/simple_runner.py +323 -33
  29. claude_mpm/core/socketio_pool.py +582 -0
  30. claude_mpm/core/websocket_handler.py +233 -0
  31. claude_mpm/deployment_paths.py +261 -0
  32. claude_mpm/hooks/builtin/memory_hooks_example.py +67 -0
  33. claude_mpm/hooks/claude_hooks/hook_handler.py +667 -679
  34. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +9 -4
  35. claude_mpm/hooks/memory_integration_hook.py +312 -0
  36. claude_mpm/models/__init__.py +9 -91
  37. claude_mpm/orchestration/__init__.py +1 -1
  38. claude_mpm/scripts/claude-mpm-socketio +32 -0
  39. claude_mpm/scripts/claude_mpm_monitor.html +567 -0
  40. claude_mpm/scripts/install_socketio_server.py +407 -0
  41. claude_mpm/scripts/launch_monitor.py +132 -0
  42. claude_mpm/scripts/launch_socketio_dashboard.py +261 -0
  43. claude_mpm/scripts/manage_version.py +479 -0
  44. claude_mpm/scripts/socketio_daemon.py +181 -0
  45. claude_mpm/scripts/socketio_server_manager.py +428 -0
  46. claude_mpm/services/__init__.py +5 -0
  47. claude_mpm/services/agent_lifecycle_manager.py +76 -25
  48. claude_mpm/services/agent_memory_manager.py +684 -0
  49. claude_mpm/services/agent_modification_tracker.py +98 -17
  50. claude_mpm/services/agent_persistence_service.py +33 -13
  51. claude_mpm/services/agent_registry.py +82 -43
  52. claude_mpm/services/hook_service.py +362 -0
  53. claude_mpm/services/socketio_client_manager.py +474 -0
  54. claude_mpm/services/socketio_server.py +922 -0
  55. claude_mpm/services/standalone_socketio_server.py +631 -0
  56. claude_mpm/services/ticket_manager.py +4 -5
  57. claude_mpm/services/{ticket_manager_dependency_injection.py → ticket_manager_di.py} +12 -39
  58. claude_mpm/services/{legacy_ticketing_service.py → ticketing_service_original.py} +9 -16
  59. claude_mpm/services/version_control/semantic_versioning.py +9 -10
  60. claude_mpm/services/websocket_server.py +376 -0
  61. claude_mpm/utils/dependency_manager.py +211 -0
  62. claude_mpm/utils/import_migration_example.py +80 -0
  63. claude_mpm/utils/path_operations.py +0 -20
  64. claude_mpm/web/open_dashboard.py +34 -0
  65. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/METADATA +20 -9
  66. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/RECORD +71 -50
  67. claude_mpm-3.3.0.dist-info/entry_points.txt +7 -0
  68. claude_mpm/cli_old.py +0 -728
  69. claude_mpm/models/common.py +0 -41
  70. claude_mpm/models/lifecycle.py +0 -97
  71. claude_mpm/models/modification.py +0 -126
  72. claude_mpm/models/persistence.py +0 -57
  73. claude_mpm/models/registry.py +0 -91
  74. claude_mpm/security/__init__.py +0 -8
  75. claude_mpm/security/bash_validator.py +0 -393
  76. claude_mpm-3.1.3.dist-info/entry_points.txt +0 -4
  77. /claude_mpm/{cli_enhancements.py → experimental/cli_enhancements.py} +0 -0
  78. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/WHEEL +0 -0
  79. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/licenses/LICENSE +0 -0
  80. {claude_mpm-3.1.3.dist-info → claude_mpm-3.3.0.dist-info}/top_level.txt +0 -0
@@ -5,20 +5,91 @@ WHY: This module handles the main 'run' command which starts Claude sessions.
5
5
  It's the most commonly used command and handles both interactive and non-interactive modes.
6
6
  """
7
7
 
8
+ import os
8
9
  import subprocess
9
10
  import sys
11
+ import time
12
+ import webbrowser
10
13
  from pathlib import Path
11
14
 
12
- from claude_mpm.utils.imports import safe_import
15
+ from ...core.logger import get_logger
16
+ from ...constants import LogLevel
17
+ from ..utils import get_user_input, list_agent_versions_at_startup
18
+ from ...utils.dependency_manager import ensure_socketio_dependencies
19
+ from ...deployment_paths import get_monitor_html_path, get_scripts_dir, get_package_root
13
20
 
14
- # Import with safe_import pattern for better error handling
15
- get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
16
- LogLevel = safe_import('claude_mpm.constants', None, ['LogLevel'])
17
- get_user_input, list_agent_versions_at_startup = safe_import(
18
- 'claude_mpm.cli.utils',
19
- None,
20
- ['get_user_input', 'list_agent_versions_at_startup']
21
- )
21
+
22
+ def filter_claude_mpm_args(claude_args):
23
+ """
24
+ Filter out claude-mpm specific arguments from claude_args before passing to Claude CLI.
25
+
26
+ WHY: The argparse.REMAINDER captures ALL remaining arguments, including claude-mpm
27
+ specific flags like --monitor, etc. Claude CLI doesn't understand these
28
+ flags and will error if they're passed through.
29
+
30
+ DESIGN DECISION: We maintain a list of known claude-mpm flags to filter out,
31
+ ensuring only genuine Claude CLI arguments are passed through.
32
+
33
+ Args:
34
+ claude_args: List of arguments captured by argparse.REMAINDER
35
+
36
+ Returns:
37
+ Filtered list of arguments safe to pass to Claude CLI
38
+ """
39
+ if not claude_args:
40
+ return []
41
+
42
+ # Known claude-mpm specific flags that should NOT be passed to Claude CLI
43
+ # This includes all MPM-specific arguments from the parser
44
+ mpm_flags = {
45
+ # Run-specific flags
46
+ '--monitor',
47
+ '--websocket-port',
48
+ '--no-hooks',
49
+ '--no-tickets',
50
+ '--intercept-commands',
51
+ '--no-native-agents',
52
+ '--launch-method',
53
+ '--resume',
54
+ # Input/output flags (these are MPM-specific, not Claude CLI flags)
55
+ '--input',
56
+ '--non-interactive',
57
+ # Common logging flags (these are MPM-specific, not Claude CLI flags)
58
+ '--debug',
59
+ '--logging',
60
+ '--log-dir',
61
+ # Framework flags (these are MPM-specific)
62
+ '--framework-path',
63
+ '--agents-dir',
64
+ # Version flag (handled by MPM)
65
+ '--version',
66
+ # Short flags (MPM-specific equivalents)
67
+ '-i', # --input (MPM-specific, not Claude CLI)
68
+ '-d' # --debug (MPM-specific, not Claude CLI)
69
+ }
70
+
71
+ filtered_args = []
72
+ i = 0
73
+ while i < len(claude_args):
74
+ arg = claude_args[i]
75
+
76
+ # Check if this is a claude-mpm flag
77
+ if arg in mpm_flags:
78
+ # Skip this flag
79
+ i += 1
80
+ # Also skip the next argument if this flag expects a value
81
+ value_expecting_flags = {
82
+ '--websocket-port', '--launch-method', '--logging', '--log-dir',
83
+ '--framework-path', '--agents-dir', '-i', '--input', '--resume'
84
+ }
85
+ if arg in value_expecting_flags and i < len(claude_args):
86
+ i += 1 # Skip the value too
87
+ else:
88
+ # This is not a claude-mpm flag, keep it
89
+ filtered_args.append(arg)
90
+ i += 1
91
+
92
+ return filtered_args
22
93
 
23
94
 
24
95
  def run_session(args):
@@ -28,29 +99,21 @@ def run_session(args):
28
99
  WHY: This is the primary command that users interact with. It sets up the
29
100
  environment, optionally deploys agents, and launches Claude with the MPM framework.
30
101
 
31
- DESIGN DECISION: We use SimpleClaudeRunner to handle the complexity of
102
+ DESIGN DECISION: We use ClaudeRunner to handle the complexity of
32
103
  subprocess management and hook integration, keeping this function focused
33
104
  on high-level orchestration.
34
105
 
35
106
  Args:
36
107
  args: Parsed command line arguments
37
108
  """
38
- import os
39
109
  logger = get_logger("cli")
40
110
  if args.logging != LogLevel.OFF.value:
41
111
  logger.info("Starting Claude MPM session")
42
- # Log working directory context
43
- logger.info(f"Working directory: {os.getcwd()}")
44
- if args.debug:
45
- logger.debug(f"User PWD (from env): {os.environ.get('CLAUDE_MPM_USER_PWD', 'Not set')}")
46
- logger.debug(f"Framework path: {os.environ.get('CLAUDE_MPM_FRAMEWORK_PATH', 'Not set')}")
47
-
48
- # Import SimpleClaudeRunner using safe_import pattern
49
- SimpleClaudeRunner, create_simple_context = safe_import(
50
- 'claude_mpm.core.simple_runner',
51
- None,
52
- ['SimpleClaudeRunner', 'create_simple_context']
53
- )
112
+
113
+ try:
114
+ from ...core.claude_runner import ClaudeRunner, create_simple_context
115
+ except ImportError:
116
+ from claude_mpm.core.claude_runner import ClaudeRunner, create_simple_context
54
117
 
55
118
  # Skip native agents if disabled
56
119
  if getattr(args, 'no_native_agents', False):
@@ -61,16 +124,81 @@ def run_session(args):
61
124
 
62
125
  # Create simple runner
63
126
  enable_tickets = not args.no_tickets
64
- claude_args = getattr(args, 'claude_args', []) or []
65
- runner = SimpleClaudeRunner(
127
+ raw_claude_args = getattr(args, 'claude_args', []) or []
128
+ # Filter out claude-mpm specific flags before passing to Claude CLI
129
+ claude_args = filter_claude_mpm_args(raw_claude_args)
130
+ monitor_mode = getattr(args, 'monitor', False)
131
+
132
+ # Debug logging for argument filtering
133
+ if raw_claude_args != claude_args:
134
+ logger.debug(f"Filtered claude-mpm args: {set(raw_claude_args) - set(claude_args)}")
135
+ logger.debug(f"Passing to Claude CLI: {claude_args}")
136
+
137
+ # Use the specified launch method (default: exec)
138
+ launch_method = getattr(args, 'launch_method', 'exec')
139
+
140
+ enable_websocket = getattr(args, 'monitor', False) or monitor_mode
141
+ websocket_port = getattr(args, 'websocket_port', 8765)
142
+
143
+ # Display Socket.IO server info if enabled
144
+ if enable_websocket:
145
+ # Auto-install Socket.IO dependencies if needed
146
+ print("🔧 Checking Socket.IO dependencies...")
147
+ dependencies_ok, error_msg = ensure_socketio_dependencies(logger)
148
+
149
+ if not dependencies_ok:
150
+ print(f"❌ Failed to install Socket.IO dependencies: {error_msg}")
151
+ print(" Please install manually: pip install python-socketio aiohttp python-engineio")
152
+ print(" Or install with extras: pip install claude-mpm[monitor]")
153
+ # Continue anyway - some functionality might still work
154
+ else:
155
+ print("✓ Socket.IO dependencies ready")
156
+
157
+ try:
158
+ import socketio
159
+ print(f"✓ Socket.IO server enabled at http://localhost:{websocket_port}")
160
+ if launch_method == "exec":
161
+ print(" Note: Socket.IO monitoring using exec mode with Claude Code hooks")
162
+
163
+ # Launch Socket.IO dashboard if in monitor mode
164
+ if monitor_mode:
165
+ success, browser_opened = launch_socketio_monitor(websocket_port, logger)
166
+ if not success:
167
+ print(f"⚠️ Failed to launch Socket.IO monitor")
168
+ print(f" You can manually run: python scripts/launch_socketio_dashboard.py --port {websocket_port}")
169
+ # Store whether browser was opened by CLI for coordination with ClaudeRunner
170
+ args._browser_opened_by_cli = browser_opened
171
+ except ImportError as e:
172
+ print(f"⚠️ Socket.IO still not available after installation attempt: {e}")
173
+ print(" This might be a virtual environment issue.")
174
+ print(" Try: pip install python-socketio aiohttp python-engineio")
175
+ print(" Or: pip install claude-mpm[monitor]")
176
+
177
+ runner = ClaudeRunner(
66
178
  enable_tickets=enable_tickets,
67
179
  log_level=args.logging,
68
- claude_args=claude_args
180
+ claude_args=claude_args,
181
+ launch_method=launch_method,
182
+ enable_websocket=enable_websocket,
183
+ websocket_port=websocket_port
69
184
  )
70
185
 
186
+ # Set browser opening flag for monitor mode
187
+ if monitor_mode:
188
+ runner._should_open_monitor_browser = True
189
+ # Pass information about whether we already opened the browser in run.py
190
+ runner._browser_opened_by_cli = getattr(args, '_browser_opened_by_cli', False)
191
+
71
192
  # Create basic context
72
193
  context = create_simple_context()
73
194
 
195
+ # For monitor mode, we handled everything in launch_socketio_monitor
196
+ # No need for ClaudeRunner browser delegation
197
+ if monitor_mode:
198
+ # Clear any browser opening flags since we handled it completely
199
+ runner._should_open_monitor_browser = False
200
+ runner._browser_opened_by_cli = True # Prevent duplicate opening
201
+
74
202
  # Run session based on mode
75
203
  if args.non_interactive or args.input:
76
204
  # Non-interactive mode
@@ -84,7 +212,7 @@ def run_session(args):
84
212
  # Use the interactive wrapper for command interception
85
213
  # WHY: Command interception requires special handling of stdin/stdout
86
214
  # which is better done in a separate Python script
87
- wrapper_path = Path(__file__).parent.parent.parent.parent.parent / "scripts" / "interactive_wrapper.py"
215
+ wrapper_path = get_scripts_dir() / "interactive_wrapper.py"
88
216
  if wrapper_path.exists():
89
217
  print("Starting interactive session with command interception...")
90
218
  subprocess.run([sys.executable, str(wrapper_path)])
@@ -92,4 +220,349 @@ def run_session(args):
92
220
  logger.warning("Interactive wrapper not found, falling back to normal mode")
93
221
  runner.run_interactive(context)
94
222
  else:
95
- runner.run_interactive(context)
223
+ runner.run_interactive(context)
224
+
225
+
226
+ def launch_socketio_monitor(port, logger):
227
+ """
228
+ Launch the Socket.IO monitoring dashboard using static HTML file.
229
+
230
+ WHY: This function opens a static HTML file that connects to the Socket.IO server.
231
+ This approach is simpler and more reliable than serving the dashboard from the server.
232
+ The HTML file connects to whatever Socket.IO server is running on the specified port.
233
+
234
+ DESIGN DECISION: Use file:// protocol to open static HTML file directly from filesystem.
235
+ Pass the server port as a URL parameter so the dashboard knows which port to connect to.
236
+ This decouples the dashboard from the server serving and makes it more robust.
237
+
238
+ Args:
239
+ port: Port number for the Socket.IO server
240
+ logger: Logger instance for output
241
+
242
+ Returns:
243
+ tuple: (success: bool, browser_opened: bool) - success status and whether browser was opened
244
+ """
245
+ try:
246
+ # Verify Socket.IO dependencies are available
247
+ try:
248
+ import socketio
249
+ import aiohttp
250
+ import engineio
251
+ logger.debug("Socket.IO dependencies verified")
252
+ except ImportError as e:
253
+ logger.error(f"Socket.IO dependencies not available: {e}")
254
+ print(f"❌ Socket.IO dependencies missing: {e}")
255
+ print(" This is unexpected - dependency installation may have failed.")
256
+ return False, False
257
+
258
+ print(f"🚀 Setting up Socket.IO monitor on port {port}...")
259
+ logger.info(f"Launching Socket.IO monitor on port {port}")
260
+
261
+ socketio_port = port
262
+
263
+ # Get path to monitor HTML using deployment paths
264
+ html_file_path = get_monitor_html_path()
265
+
266
+ if not html_file_path.exists():
267
+ logger.error(f"Monitor HTML file not found: {html_file_path}")
268
+ print(f"❌ Monitor HTML file not found: {html_file_path}")
269
+ return False, False
270
+
271
+ # Create file:// URL with port parameter
272
+ dashboard_url = f'file://{html_file_path.absolute()}?port={socketio_port}'
273
+
274
+ # Check if Socket.IO server is already running
275
+ server_running = _check_socketio_server_running(socketio_port, logger)
276
+
277
+ if server_running:
278
+ print(f"✅ Socket.IO server already running on port {socketio_port}")
279
+
280
+ # Check if it's managed by our daemon
281
+ daemon_script = get_package_root() / "scripts" / "socketio_daemon.py"
282
+ if daemon_script.exists():
283
+ status_result = subprocess.run(
284
+ [sys.executable, str(daemon_script), "status"],
285
+ capture_output=True,
286
+ text=True
287
+ )
288
+ if "is running" in status_result.stdout:
289
+ print(f" (Managed by Python daemon)")
290
+
291
+ print(f"📊 Dashboard: {dashboard_url}")
292
+
293
+ # Open browser with static HTML file
294
+ try:
295
+ # Check if we should suppress browser opening (for tests)
296
+ if os.environ.get('CLAUDE_MPM_NO_BROWSER') != '1':
297
+ print(f"🌐 Opening dashboard in browser...")
298
+ open_in_browser_tab(dashboard_url, logger)
299
+ logger.info(f"Socket.IO dashboard opened: {dashboard_url}")
300
+ else:
301
+ print(f"🌐 Browser opening suppressed (CLAUDE_MPM_NO_BROWSER=1)")
302
+ logger.info(f"Browser opening suppressed by environment variable")
303
+ return True, True
304
+ except Exception as e:
305
+ logger.warning(f"Failed to open browser: {e}")
306
+ print(f"⚠️ Could not open browser automatically")
307
+ print(f"📊 Please open manually: {dashboard_url}")
308
+ return True, False
309
+ else:
310
+ # Start standalone Socket.IO server
311
+ print(f"🔧 Starting Socket.IO server on port {socketio_port}...")
312
+ server_started = _start_standalone_socketio_server(socketio_port, logger)
313
+
314
+ if server_started:
315
+ print(f"✅ Socket.IO server started successfully")
316
+ print(f"📊 Dashboard: {dashboard_url}")
317
+
318
+ # Final verification that server is responsive
319
+ final_check_passed = False
320
+ for i in range(3):
321
+ if _check_socketio_server_running(socketio_port, logger):
322
+ final_check_passed = True
323
+ break
324
+ time.sleep(1)
325
+
326
+ if not final_check_passed:
327
+ logger.warning("Server started but final connectivity check failed")
328
+ print(f"⚠️ Server may still be initializing. Dashboard should work once fully ready.")
329
+
330
+ # Open browser with static HTML file
331
+ try:
332
+ # Check if we should suppress browser opening (for tests)
333
+ if os.environ.get('CLAUDE_MPM_NO_BROWSER') != '1':
334
+ print(f"🌐 Opening dashboard in browser...")
335
+ open_in_browser_tab(dashboard_url, logger)
336
+ logger.info(f"Socket.IO dashboard opened: {dashboard_url}")
337
+ else:
338
+ print(f"🌐 Browser opening suppressed (CLAUDE_MPM_NO_BROWSER=1)")
339
+ logger.info(f"Browser opening suppressed by environment variable")
340
+ return True, True
341
+ except Exception as e:
342
+ logger.warning(f"Failed to open browser: {e}")
343
+ print(f"⚠️ Could not open browser automatically")
344
+ print(f"📊 Please open manually: {dashboard_url}")
345
+ return True, False
346
+ else:
347
+ print(f"❌ Failed to start Socket.IO server")
348
+ print(f"💡 Troubleshooting tips:")
349
+ print(f" - Check if port {socketio_port} is already in use")
350
+ print(f" - Verify Socket.IO dependencies: pip install python-socketio aiohttp")
351
+ print(f" - Try a different port with --websocket-port")
352
+ return False, False
353
+
354
+ except Exception as e:
355
+ logger.error(f"Failed to launch Socket.IO monitor: {e}")
356
+ print(f"❌ Failed to launch Socket.IO monitor: {e}")
357
+ return False, False
358
+
359
+
360
+ def _check_socketio_server_running(port, logger):
361
+ """
362
+ Check if a Socket.IO server is running on the specified port.
363
+
364
+ WHY: We need to detect existing servers to avoid conflicts and provide
365
+ seamless experience regardless of whether server is already running.
366
+
367
+ DESIGN DECISION: We try multiple endpoints and connection methods to ensure
368
+ robust detection. Some servers may be starting up and only partially ready.
369
+
370
+ Args:
371
+ port: Port number to check
372
+ logger: Logger instance for output
373
+
374
+ Returns:
375
+ bool: True if server is running and responding, False otherwise
376
+ """
377
+ try:
378
+ import urllib.request
379
+ import urllib.error
380
+ import socket
381
+
382
+ # First, do a basic TCP connection check
383
+ try:
384
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
385
+ s.settimeout(1.0)
386
+ result = s.connect_ex(('127.0.0.1', port))
387
+ if result != 0:
388
+ logger.debug(f"TCP connection to port {port} failed (not listening)")
389
+ return False
390
+ except Exception as e:
391
+ logger.debug(f"TCP socket check failed for port {port}: {e}")
392
+ return False
393
+
394
+ # If TCP connection succeeds, try HTTP health check
395
+ try:
396
+ response = urllib.request.urlopen(f'http://localhost:{port}/status', timeout=5)
397
+
398
+ if response.getcode() == 200:
399
+ content = response.read().decode()
400
+ logger.debug(f"✅ Socket.IO server health check passed on port {port}")
401
+ logger.debug(f"📄 Server response: {content[:100]}...")
402
+ return True
403
+ else:
404
+ logger.debug(f"⚠️ HTTP response code {response.getcode()} from port {port}")
405
+ return False
406
+
407
+ except urllib.error.HTTPError as e:
408
+ logger.debug(f"⚠️ HTTP error {e.code} from server on port {port}")
409
+ return False
410
+ except urllib.error.URLError as e:
411
+ logger.debug(f"⚠️ URL error connecting to port {port}: {e.reason}")
412
+ return False
413
+
414
+ except (ConnectionError, OSError) as e:
415
+ logger.debug(f"🔌 Connection error checking port {port}: {e}")
416
+ except Exception as e:
417
+ logger.debug(f"❌ Unexpected error checking Socket.IO server on port {port}: {e}")
418
+
419
+ return False
420
+
421
+
422
+ def _start_standalone_socketio_server(port, logger):
423
+ """
424
+ Start a standalone Socket.IO server using the Python daemon.
425
+
426
+ WHY: For monitor mode, we want a persistent server that runs independently
427
+ of the Claude session. This allows users to monitor multiple sessions and
428
+ keeps the dashboard available even when Claude isn't running.
429
+
430
+ DESIGN DECISION: We use a pure Python daemon script to manage the server
431
+ process. This avoids Node.js dependencies (like PM2) and provides proper
432
+ process management with PID tracking.
433
+
434
+ Args:
435
+ port: Port number for the server
436
+ logger: Logger instance for output
437
+
438
+ Returns:
439
+ bool: True if server started successfully, False otherwise
440
+ """
441
+ try:
442
+ from ...deployment_paths import get_scripts_dir
443
+ import subprocess
444
+
445
+ # Get path to daemon script in package
446
+ daemon_script = get_package_root() / "scripts" / "socketio_daemon.py"
447
+
448
+ if not daemon_script.exists():
449
+ logger.error(f"Socket.IO daemon script not found: {daemon_script}")
450
+ return False
451
+
452
+ logger.info(f"Starting Socket.IO server daemon on port {port}")
453
+
454
+ # Start the daemon
455
+ result = subprocess.run(
456
+ [sys.executable, str(daemon_script), "start"],
457
+ capture_output=True,
458
+ text=True
459
+ )
460
+
461
+ if result.returncode != 0:
462
+ logger.error(f"Failed to start Socket.IO daemon: {result.stderr}")
463
+ return False
464
+
465
+ # Wait for server to be ready with longer timeouts and progressive delays
466
+ # WHY: Socket.IO server startup involves complex async initialization:
467
+ # 1. Thread creation (~0.1s)
468
+ # 2. Event loop setup (~1s)
469
+ # 3. aiohttp server binding (~2-5s)
470
+ # 4. Socket.IO service initialization (~1-3s)
471
+ # Total: up to 10 seconds for full readiness
472
+ max_attempts = 20 # Increased from 10
473
+ initial_delay = 0.5 # seconds
474
+ max_delay = 2.0 # seconds
475
+
476
+ logger.info(f"Waiting up to {max_attempts * max_delay} seconds for server to be fully ready...")
477
+
478
+ for attempt in range(max_attempts):
479
+ # Progressive delay - start fast, then slow down for socket binding
480
+ if attempt < 5:
481
+ delay = initial_delay
482
+ else:
483
+ delay = min(max_delay, initial_delay + (attempt - 5) * 0.2)
484
+
485
+ logger.debug(f"Checking server readiness (attempt {attempt + 1}/{max_attempts}, waiting {delay}s)")
486
+
487
+ # Check if thread is alive first
488
+ if hasattr(server, 'thread') and server.thread and server.thread.is_alive():
489
+ logger.debug("Server thread is alive, checking connectivity...")
490
+
491
+ # Give it time for socket binding (progressive delay)
492
+ time.sleep(delay)
493
+
494
+ # Verify it's actually accepting connections
495
+ if _check_socketio_server_running(port, logger):
496
+ logger.info(f"✅ Standalone Socket.IO server started successfully on port {port}")
497
+ logger.info(f"🕐 Server ready after {attempt + 1} attempts ({(attempt + 1) * delay:.1f}s)")
498
+ return True
499
+ else:
500
+ logger.debug(f"Server not yet accepting connections on attempt {attempt + 1}")
501
+ else:
502
+ logger.warning(f"Server thread not alive or not created on attempt {attempt + 1}")
503
+ # Give thread more time to start
504
+ time.sleep(delay)
505
+
506
+ logger.error(f"❌ Socket.IO server failed to start properly on port {port} after {max_attempts} attempts")
507
+ logger.error(f"💡 This may indicate a port conflict or dependency issue")
508
+ logger.error(f"🔧 Try a different port with --websocket-port or check for conflicts")
509
+ return False
510
+
511
+ except Exception as e:
512
+ logger.error(f"❌ Failed to start standalone Socket.IO server: {e}")
513
+ import traceback
514
+ logger.error(f"📋 Stack trace: {traceback.format_exc()}")
515
+ logger.error(f"💡 This may be a dependency issue - try: pip install python-socketio aiohttp")
516
+ return False
517
+
518
+
519
+
520
+ def open_in_browser_tab(url, logger):
521
+ """
522
+ Open URL in browser, attempting to reuse existing tabs when possible.
523
+
524
+ WHY: Users prefer reusing browser tabs instead of opening new ones constantly.
525
+ This function attempts platform-specific solutions for tab reuse.
526
+
527
+ DESIGN DECISION: We try different methods based on platform capabilities,
528
+ falling back to standard webbrowser.open() if needed.
529
+
530
+ Args:
531
+ url: URL to open
532
+ logger: Logger instance for output
533
+ """
534
+ try:
535
+ # Platform-specific optimizations for tab reuse
536
+ import platform
537
+ system = platform.system().lower()
538
+
539
+ if system == "darwin": # macOS
540
+ # Just use the standard webbrowser module on macOS
541
+ # The AppleScript approach is too unreliable
542
+ webbrowser.open(url, new=0, autoraise=True) # new=0 tries to reuse window
543
+ logger.info("Opened browser on macOS")
544
+
545
+ elif system == "linux":
546
+ # On Linux, try to use existing browser session
547
+ try:
548
+ # This is a best-effort approach for common browsers
549
+ webbrowser.get().open(url, new=0) # new=0 tries to reuse existing window
550
+ logger.info("Attempted Linux browser tab reuse")
551
+ except Exception:
552
+ webbrowser.open(url, autoraise=True)
553
+
554
+ elif system == "windows":
555
+ # On Windows, try to use existing browser
556
+ try:
557
+ webbrowser.get().open(url, new=0) # new=0 tries to reuse existing window
558
+ logger.info("Attempted Windows browser tab reuse")
559
+ except Exception:
560
+ webbrowser.open(url, autoraise=True)
561
+ else:
562
+ # Unknown platform, use standard opening
563
+ webbrowser.open(url, autoraise=True)
564
+
565
+ except Exception as e:
566
+ logger.warning(f"Browser opening failed: {e}")
567
+ # Final fallback
568
+ webbrowser.open(url)
@@ -5,10 +5,7 @@ WHY: This module handles ticket listing functionality, allowing users to view
5
5
  recent tickets created during Claude sessions.
6
6
  """
7
7
 
8
- from claude_mpm.utils.imports import safe_import
9
-
10
- # Import logger using safe_import pattern
11
- get_logger = safe_import('claude_mpm.core.logger', None, ['get_logger'])
8
+ from ...core.logger import get_logger
12
9
 
13
10
 
14
11
  def list_tickets(args):
@@ -27,20 +24,12 @@ def list_tickets(args):
27
24
  """
28
25
  logger = get_logger("cli")
29
26
 
30
- # Import TicketManager using safe_import pattern
31
- TicketManager = safe_import(
32
- '...services.ticket_manager',
33
- 'claude_mpm.services.ticket_manager',
34
- ['TicketManager']
35
- )
36
-
37
- if not TicketManager:
38
- logger.error("ai-trackdown-pytools not installed")
39
- print("Error: ai-trackdown-pytools not installed")
40
- print("Install with: pip install ai-trackdown-pytools")
41
- return
42
-
43
27
  try:
28
+ try:
29
+ from ...services.ticket_manager import TicketManager
30
+ except ImportError:
31
+ from claude_mpm.services.ticket_manager import TicketManager
32
+
44
33
  ticket_manager = TicketManager()
45
34
  tickets = ticket_manager.list_recent_tickets(limit=args.limit)
46
35
 
@@ -65,6 +54,10 @@ def list_tickets(args):
65
54
  print(f" Created: {ticket['created_at']}")
66
55
  print()
67
56
 
57
+ except ImportError:
58
+ logger.error("ai-trackdown-pytools not installed")
59
+ print("Error: ai-trackdown-pytools not installed")
60
+ print("Install with: pip install ai-trackdown-pytools")
68
61
  except Exception as e:
69
62
  logger.error(f"Error listing tickets: {e}")
70
63
  print(f"Error: {e}")