claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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 (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,148 @@
1
+ """Centralized network port configuration for Claude MPM.
2
+
3
+ This module provides the single source of truth for all network port defaults
4
+ and environment variable names used throughout the MPM system.
5
+
6
+ WHY: Previously, port defaults were hardcoded in multiple locations (config.py,
7
+ constants.py, commander/config.py, CLI parsers), leading to inconsistencies and
8
+ difficulty maintaining different defaults per service.
9
+
10
+ USAGE:
11
+ from claude_mpm.core.network_config import NetworkPorts
12
+
13
+ # Get default ports
14
+ monitor_port = NetworkPorts.MONITOR_DEFAULT
15
+ commander_port = NetworkPorts.COMMANDER_DEFAULT
16
+
17
+ # Get from environment with fallback
18
+ port = NetworkPorts.get_monitor_port()
19
+ """
20
+
21
+ import os
22
+ from typing import Optional
23
+
24
+
25
+ class NetworkPorts:
26
+ """Network port configuration with different defaults for each service.
27
+
28
+ Service Default Ports:
29
+ - Monitor: 8765 (user's preferred default)
30
+ - Commander: 8766
31
+ - Dashboard: 8767
32
+ - SocketIO: 8768
33
+
34
+ Port Range: 8765-8785 (21 ports available)
35
+
36
+ Environment Variables:
37
+ - CLAUDE_MPM_MONITOR_PORT: Override monitor port
38
+ - CLAUDE_MPM_COMMANDER_PORT: Override commander port
39
+ - CLAUDE_MPM_DASHBOARD_PORT: Override dashboard port
40
+ - CLAUDE_MPM_SOCKETIO_PORT: Override socketio port
41
+ - CLAUDE_MPM_DEFAULT_HOST: Override default host (default: 127.0.0.1)
42
+ """
43
+
44
+ # Default ports for each service
45
+ MONITOR_DEFAULT = 8765
46
+ COMMANDER_DEFAULT = 8766
47
+ DASHBOARD_DEFAULT = 8767
48
+ SOCKETIO_DEFAULT = 8768
49
+
50
+ # Port range configuration
51
+ PORT_RANGE_START = 8765
52
+ PORT_RANGE_END = 8785
53
+
54
+ # Default host
55
+ DEFAULT_HOST = "127.0.0.1"
56
+
57
+ # Environment variable names
58
+ ENV_MONITOR_PORT = "CLAUDE_MPM_MONITOR_PORT"
59
+ ENV_COMMANDER_PORT = "CLAUDE_MPM_COMMANDER_PORT"
60
+ ENV_DASHBOARD_PORT = "CLAUDE_MPM_DASHBOARD_PORT"
61
+ ENV_SOCKETIO_PORT = "CLAUDE_MPM_SOCKETIO_PORT"
62
+ ENV_DEFAULT_HOST = "CLAUDE_MPM_DEFAULT_HOST"
63
+
64
+ @classmethod
65
+ def get_monitor_port(cls, default: Optional[int] = None) -> int:
66
+ """Get monitor port from environment or default.
67
+
68
+ Args:
69
+ default: Optional override default (if not provided, uses MONITOR_DEFAULT)
70
+
71
+ Returns:
72
+ Port number from environment or default
73
+ """
74
+ if default is None:
75
+ default = cls.MONITOR_DEFAULT
76
+ return int(os.getenv(cls.ENV_MONITOR_PORT, default))
77
+
78
+ @classmethod
79
+ def get_commander_port(cls, default: Optional[int] = None) -> int:
80
+ """Get commander port from environment or default.
81
+
82
+ Args:
83
+ default: Optional override default (if not provided, uses COMMANDER_DEFAULT)
84
+
85
+ Returns:
86
+ Port number from environment or default
87
+ """
88
+ if default is None:
89
+ default = cls.COMMANDER_DEFAULT
90
+ return int(os.getenv(cls.ENV_COMMANDER_PORT, default))
91
+
92
+ @classmethod
93
+ def get_dashboard_port(cls, default: Optional[int] = None) -> int:
94
+ """Get dashboard port from environment or default.
95
+
96
+ Args:
97
+ default: Optional override default (if not provided, uses DASHBOARD_DEFAULT)
98
+
99
+ Returns:
100
+ Port number from environment or default
101
+ """
102
+ if default is None:
103
+ default = cls.DASHBOARD_DEFAULT
104
+ return int(os.getenv(cls.ENV_DASHBOARD_PORT, default))
105
+
106
+ @classmethod
107
+ def get_socketio_port(cls, default: Optional[int] = None) -> int:
108
+ """Get socketio port from environment or default.
109
+
110
+ Args:
111
+ default: Optional override default (if not provided, uses SOCKETIO_DEFAULT)
112
+
113
+ Returns:
114
+ Port number from environment or default
115
+ """
116
+ if default is None:
117
+ default = cls.SOCKETIO_DEFAULT
118
+ return int(os.getenv(cls.ENV_SOCKETIO_PORT, default))
119
+
120
+ @classmethod
121
+ def get_default_host(cls) -> str:
122
+ """Get default host from environment or default.
123
+
124
+ Returns:
125
+ Host address from environment or DEFAULT_HOST
126
+ """
127
+ return os.getenv(cls.ENV_DEFAULT_HOST, cls.DEFAULT_HOST)
128
+
129
+ @classmethod
130
+ def get_port_range(cls) -> range:
131
+ """Get the valid port range.
132
+
133
+ Returns:
134
+ Range object from PORT_RANGE_START to PORT_RANGE_END (inclusive)
135
+ """
136
+ return range(cls.PORT_RANGE_START, cls.PORT_RANGE_END + 1)
137
+
138
+ @classmethod
139
+ def is_port_in_range(cls, port: int) -> bool:
140
+ """Check if port is within valid range.
141
+
142
+ Args:
143
+ port: Port number to check
144
+
145
+ Returns:
146
+ True if port is in valid range, False otherwise
147
+ """
148
+ return cls.PORT_RANGE_START <= port <= cls.PORT_RANGE_END
@@ -11,7 +11,7 @@ defines the interface it needs.
11
11
 
12
12
  import contextlib
13
13
  import os
14
- import subprocess
14
+ import subprocess # nosec B404
15
15
  import tempfile
16
16
  import time
17
17
  import uuid
@@ -86,11 +86,12 @@ class OneshotSession:
86
86
  Returns:
87
87
  True if successful, False otherwise
88
88
  """
89
- # Deploy system agents
90
- if not self.runner.setup_agents():
91
- print("Continuing without native agents...")
89
+ # NOTE: System agents are deployed via reconciliation during startup.
90
+ # The reconciliation process respects user configuration and handles
91
+ # both native and custom mode deployment. No need to call setup_agents() here.
92
92
 
93
- # Deploy project-specific agents
93
+ # Deploy project-specific agents from .claude-mpm/agents/
94
+ # This is separate from system agents and handles user-defined agents
94
95
  self.runner.deploy_project_agents_to_claude()
95
96
 
96
97
  return True
@@ -225,7 +226,7 @@ class OneshotSession:
225
226
  if len(cmd) > 5:
226
227
  self.logger.debug(f"Command has {len(cmd)} arguments total")
227
228
 
228
- result = subprocess.run(
229
+ result = subprocess.run( # nosec B603
229
230
  cmd, capture_output=True, text=True, env=env, check=False
230
231
  )
231
232
 
@@ -297,6 +297,9 @@ class OutputStyleManager:
297
297
  target_path = style_config["target"]
298
298
  style_name = style_config["name"]
299
299
 
300
+ # Check if this is a fresh install (file doesn't exist yet)
301
+ is_fresh_install = not target_path.exists()
302
+
300
303
  # If content not provided, read from source
301
304
  if content is None:
302
305
  content = self.extract_output_style_content(style=style)
@@ -310,7 +313,9 @@ class OutputStyleManager:
310
313
 
311
314
  # Activate the style if requested
312
315
  if activate:
313
- self._activate_output_style(style_name)
316
+ self._activate_output_style(
317
+ style_name, is_fresh_install=is_fresh_install
318
+ )
314
319
 
315
320
  return True
316
321
 
@@ -318,12 +323,21 @@ class OutputStyleManager:
318
323
  self.logger.error(f"Failed to deploy {style} style: {e}")
319
324
  return False
320
325
 
321
- def _activate_output_style(self, style_name: str = "Claude MPM") -> bool:
326
+ def _activate_output_style(
327
+ self, style_name: str = "Claude MPM", is_fresh_install: bool = False
328
+ ) -> bool:
322
329
  """
323
330
  Update Claude Code settings to activate a specific output style.
324
331
 
332
+ Only activates the style if:
333
+ 1. No active style is currently set (first deployment), OR
334
+ 2. This is a fresh install (style file didn't exist before deployment)
335
+
336
+ This preserves user preferences if they've manually changed their active style.
337
+
325
338
  Args:
326
339
  style_name: Name of the style to activate (e.g., "Claude MPM", "Claude MPM Teacher")
340
+ is_fresh_install: Whether this is a fresh install (style file didn't exist before)
327
341
 
328
342
  Returns:
329
343
  True if activated successfully, False otherwise
@@ -342,8 +356,15 @@ class OutputStyleManager:
342
356
  # Check current active style
343
357
  current_style = settings.get("activeOutputStyle")
344
358
 
345
- # Update active output style if different
346
- if current_style != style_name:
359
+ # Only set activeOutputStyle if:
360
+ # 1. No active style is set (first deployment), OR
361
+ # 2. Current style is "default" (not a real user preference), OR
362
+ # 3. This is a fresh install (file didn't exist before deployment)
363
+ should_activate = (
364
+ current_style is None or current_style == "default" or is_fresh_install
365
+ )
366
+
367
+ if should_activate and current_style != style_name:
347
368
  settings["activeOutputStyle"] = style_name
348
369
 
349
370
  # Ensure settings directory exists
@@ -358,7 +379,10 @@ class OutputStyleManager:
358
379
  f"✅ Activated {style_name} output style (was: {current_style or 'none'})"
359
380
  )
360
381
  else:
361
- self.logger.debug(f"{style_name} output style already active")
382
+ self.logger.debug(
383
+ f"Preserving user preference: {current_style or 'none'} "
384
+ f"(skipping activation of {style_name})"
385
+ )
362
386
 
363
387
  return True
364
388
 
@@ -452,6 +476,10 @@ class OutputStyleManager:
452
476
  """
453
477
  results: Dict[str, bool] = {}
454
478
 
479
+ # Check if professional style exists BEFORE deployment
480
+ # This determines if this is a fresh install
481
+ professional_style_existed = self.styles["professional"]["target"].exists()
482
+
455
483
  for style_type_key in self.styles:
456
484
  # Deploy without activation
457
485
  # Cast is safe because we know self.styles keys are OutputStyleType
@@ -459,9 +487,11 @@ class OutputStyleManager:
459
487
  success = self.deploy_output_style(style=style_type, activate=False)
460
488
  results[style_type] = success
461
489
 
462
- # Activate the default style if requested
490
+ # Activate the default style if requested AND this is first deployment
463
491
  if activate_default and results.get("professional", False):
464
- self._activate_output_style("Claude MPM")
492
+ self._activate_output_style(
493
+ "Claude MPM", is_fresh_install=not professional_style_existed
494
+ )
465
495
 
466
496
  return results
467
497
 
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env python3
2
1
  """Socket.IO connection pool for efficient client connection management.
3
2
 
4
3
  This module provides a connection pool to reuse Socket.IO client connections,
@@ -31,12 +30,21 @@ except ImportError:
31
30
  # Import constants for configuration
32
31
  try:
33
32
  from claude_mpm.core.constants import NetworkConfig
33
+ from claude_mpm.core.network_config import NetworkPorts
34
34
  except ImportError:
35
35
  # Fallback if constants module not available
36
+ class NetworkPorts:
37
+ MONITOR_DEFAULT = 8765
38
+ COMMANDER_DEFAULT = 8766
39
+ DASHBOARD_DEFAULT = 8767
40
+ SOCKETIO_DEFAULT = 8768
41
+ PORT_RANGE_START = 8765
42
+ PORT_RANGE_END = 8785
43
+
36
44
  class NetworkConfig:
37
- DEFAULT_DASHBOARD_PORT = 8765
45
+ DEFAULT_DASHBOARD_PORT = 8767
38
46
  SOCKETIO_PORT_RANGE = (8765, 8785)
39
- DEFAULT_SOCKETIO_PORT = 8765
47
+ DEFAULT_SOCKETIO_PORT = 8768
40
48
 
41
49
  socketio = None
42
50
 
@@ -184,9 +192,14 @@ class SocketIOConnectionPool:
184
192
  self.health_running = False
185
193
  self.last_health_check = datetime.now(timezone.utc)
186
194
 
187
- # Server configuration
188
- self.server_url = None
189
- self.server_port = None
195
+ # Server configuration - use default immediately, update async
196
+ self.server_port = int(
197
+ os.environ.get(
198
+ "CLAUDE_MPM_SOCKETIO_PORT", str(NetworkConfig.DEFAULT_SOCKETIO_PORT)
199
+ )
200
+ )
201
+ self.server_url = f"http://localhost:{self.server_port}"
202
+ self._port_detection_complete = False
190
203
 
191
204
  # Pool lifecycle
192
205
  self._running = False
@@ -200,7 +213,10 @@ class SocketIOConnectionPool:
200
213
  return
201
214
 
202
215
  self._running = True
203
- self._detect_server()
216
+
217
+ # Start async port detection in background (non-blocking)
218
+ # Default port is already set in __init__, this just updates if a better one is found
219
+ self._detect_server_async()
204
220
 
205
221
  # Start batch processing thread
206
222
  self.batch_running = True
@@ -266,14 +282,29 @@ class SocketIOConnectionPool:
266
282
 
267
283
  self.logger.info("Socket.IO connection pool stopped")
268
284
 
285
+ def _detect_server_async(self):
286
+ """Start server detection in background thread.
287
+
288
+ This runs port scanning asynchronously to avoid blocking the main thread.
289
+ The default port is already set in __init__, so this just updates if a better one is found.
290
+ """
291
+ threading.Thread(
292
+ target=self._detect_server, daemon=True, name="port-detect"
293
+ ).start()
294
+
269
295
  def _detect_server(self):
270
- """Detect Socket.IO server configuration."""
271
- # Check environment variable first
296
+ """Detect Socket.IO server configuration.
297
+
298
+ This method scans ports to find a running Socket.IO server.
299
+ It's designed to be run in a background thread to avoid blocking.
300
+ """
301
+ # Check environment variable first - if set, use it and skip detection
272
302
  env_port = os.environ.get("CLAUDE_MPM_SOCKETIO_PORT")
273
303
  if env_port:
274
304
  try:
275
305
  self.server_port = int(env_port)
276
306
  self.server_url = f"http://localhost:{self.server_port}"
307
+ self._port_detection_complete = True
277
308
  self.logger.debug(
278
309
  f"Using Socket.IO server from environment: {self.server_url}"
279
310
  )
@@ -302,19 +333,20 @@ class SocketIOConnectionPool:
302
333
  for port in common_ports:
303
334
  try:
304
335
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
305
- s.settimeout(0.05)
336
+ # Use 10ms timeout (reduced from 50ms) for faster scanning
337
+ s.settimeout(0.01)
306
338
  result = s.connect_ex(("localhost", port))
307
339
  if result == 0:
308
340
  self.server_port = port
309
341
  self.server_url = f"http://localhost:{port}"
342
+ self._port_detection_complete = True
310
343
  self.logger.debug(f"Detected Socket.IO server on port {port}")
311
344
  return
312
- except Exception:
345
+ except Exception: # nosec B112 - intentional: skip ports that fail
313
346
  continue
314
347
 
315
- # Fall back to default
316
- self.server_port = NetworkConfig.DEFAULT_DASHBOARD_PORT
317
- self.server_url = f"http://localhost:{self.server_port}"
348
+ # Keep default port set in __init__, mark detection complete
349
+ self._port_detection_complete = True
318
350
  self.logger.debug(f"Using default Socket.IO server: {self.server_url}")
319
351
 
320
352
  def _create_client(self) -> Optional[socketio.AsyncClient]:
@@ -579,7 +611,7 @@ class SocketIOConnectionPool:
579
611
  loop.stop()
580
612
  loop.run_until_complete(loop.shutdown_asyncgens())
581
613
  loop.close()
582
- except Exception:
614
+ except Exception: # nosec B110 - intentional: cleanup best-effort
583
615
  pass
584
616
 
585
617
  async def _connect_client(self, client: socketio.AsyncClient):
@@ -76,6 +76,7 @@ class DeploymentContext(Enum):
76
76
  EDITABLE_INSTALL = "editable_install"
77
77
  PIP_INSTALL = "pip_install"
78
78
  PIPX_INSTALL = "pipx_install"
79
+ UV_TOOLS = "uv_tools"
79
80
  SYSTEM_PACKAGE = "system_package"
80
81
 
81
82
 
@@ -190,113 +191,100 @@ class PathContext:
190
191
 
191
192
  Priority order:
192
193
  1. Environment variable override (CLAUDE_MPM_DEV_MODE)
193
- 2. Current working directory is a claude-mpm development project
194
- 3. Editable installation detection
195
- 4. Path-based detection (development, pipx, system, pip)
194
+ 2. Package installation path (uv tools, pipx, site-packages, editable)
195
+ 3. Current working directory (opt-in with CLAUDE_MPM_PREFER_LOCAL_SOURCE)
196
+
197
+ This ensures installed packages use their installation paths rather than
198
+ accidentally picking up development paths from CWD.
196
199
  """
197
- # Check for environment variable override
200
+ # 1. Explicit environment variable override
198
201
  if os.environ.get("CLAUDE_MPM_DEV_MODE", "").lower() in ("1", "true", "yes"):
199
202
  logger.debug(
200
203
  "Development mode forced via CLAUDE_MPM_DEV_MODE environment variable"
201
204
  )
202
205
  return DeploymentContext.DEVELOPMENT
203
206
 
204
- # Check if current working directory is a claude-mpm development project
205
- # This handles the case where pipx claude-mpm is run from within the dev directory
206
- cwd = _safe_cwd()
207
- current = cwd
208
- for _ in range(5): # Check up to 5 levels up from current directory
209
- if (current / "pyproject.toml").exists() and (
210
- current / "src" / "claude_mpm"
211
- ).exists():
212
- # Check if this is the claude-mpm project
213
- try:
214
- pyproject_content = (current / "pyproject.toml").read_text()
215
- if (
216
- 'name = "claude-mpm"' in pyproject_content
217
- or '"claude-mpm"' in pyproject_content
218
- ):
219
- logger.debug(
220
- f"Detected claude-mpm development directory at {current}"
221
- )
222
- logger.debug(
223
- "Using development mode for local source preference"
224
- )
225
- return DeploymentContext.DEVELOPMENT
226
- except Exception: # nosec B110
227
- pass
228
- if current == current.parent:
229
- break
230
- current = current.parent
231
-
207
+ # 2. Check where the actual package is installed
232
208
  try:
233
209
  import claude_mpm
234
210
 
235
211
  module_path = Path(claude_mpm.__file__).parent
212
+ package_str = str(module_path)
236
213
 
237
- # First check if this is an editable install, regardless of path
238
- # This is important for cases where pipx points to a development installation
239
- if PathContext._is_editable_install():
240
- logger.debug("Detected editable/development installation")
241
- # Check if we should use development paths
242
- # This could be because we're in a src/ directory or running from dev directory
243
- if module_path.parent.name == "src":
244
- return DeploymentContext.DEVELOPMENT
245
- if "pipx" in str(module_path):
246
- # Running via pipx but from within a development directory
247
- # Use development mode to prefer local source over pipx installation
248
- cwd = _safe_cwd()
249
- current = cwd
250
- for _ in range(5):
251
- if (current / "src" / "claude_mpm").exists() and (
252
- current / "pyproject.toml"
253
- ).exists():
254
- logger.debug(
255
- "Running pipx from development directory, using development mode"
256
- )
257
- return DeploymentContext.DEVELOPMENT
258
- if current == current.parent:
259
- break
260
- current = current.parent
261
- return DeploymentContext.EDITABLE_INSTALL
262
- return DeploymentContext.EDITABLE_INSTALL
214
+ # UV tools installation (~/.local/share/uv/tools/)
215
+ if "/.local/share/uv/tools/" in package_str:
216
+ logger.debug(f"Detected uv tools installation at {module_path}")
217
+ return DeploymentContext.UV_TOOLS
263
218
 
264
- # Check for development mode based on directory structure
265
- # module_path is typically /path/to/project/src/claude_mpm
266
- if (
267
- module_path.parent.name == "src"
268
- and (module_path.parent.parent / "src" / "claude_mpm").exists()
269
- ):
219
+ # pipx installation (~/.local/pipx/venvs/)
220
+ if "/.local/pipx/venvs/" in package_str or "/pipx/" in package_str:
221
+ logger.debug(f"Detected pipx installation at {module_path}")
222
+ return DeploymentContext.PIPX_INSTALL
223
+
224
+ # site-packages (pip install) - but not editable
225
+ if "/site-packages/" in package_str and "/src/" not in package_str:
226
+ logger.debug(f"Detected pip installation at {module_path}")
227
+ return DeploymentContext.PIP_INSTALL
228
+
229
+ # Editable install (pip install -e) - module in src/
230
+ if module_path.parent.name == "src":
231
+ # Check if this is truly an editable install
232
+ if PathContext._is_editable_install():
233
+ logger.debug(f"Detected editable installation at {module_path}")
234
+ return DeploymentContext.EDITABLE_INSTALL
235
+ # Module in src/ but not editable - development mode
270
236
  logger.debug(
271
237
  f"Detected development mode via directory structure at {module_path}"
272
238
  )
273
239
  return DeploymentContext.DEVELOPMENT
274
240
 
275
- # Check for pipx install
276
- if "pipx" in str(module_path):
277
- logger.debug(f"Detected pipx installation at {module_path}")
278
- return DeploymentContext.PIPX_INSTALL
279
-
280
- # Check for system package
281
- if "dist-packages" in str(module_path):
241
+ # dist-packages (system package manager)
242
+ if "dist-packages" in package_str:
282
243
  logger.debug(f"Detected system package installation at {module_path}")
283
244
  return DeploymentContext.SYSTEM_PACKAGE
284
245
 
285
- # Check for site-packages (could be pip or editable)
286
- if "site-packages" in str(module_path):
287
- # Already checked for editable above, so this is a regular pip install
288
- logger.debug(f"Detected pip installation at {module_path}")
289
- return DeploymentContext.PIP_INSTALL
290
-
291
- # Default to pip install
246
+ # Default to pip install for any other installation
292
247
  logger.debug(f"Defaulting to pip installation for {module_path}")
293
248
  return DeploymentContext.PIP_INSTALL
294
249
 
295
250
  except ImportError:
296
251
  logger.debug(
297
- "ImportError during context detection, defaulting to development"
252
+ "ImportError during module path detection, checking CWD as fallback"
298
253
  )
299
- return DeploymentContext.DEVELOPMENT
254
+
255
+ # 3. CWD-based detection (OPT-IN ONLY for explicit development work)
256
+ # Only use CWD if explicitly requested or no package installation found
257
+ if os.environ.get("CLAUDE_MPM_PREFER_LOCAL_SOURCE", "").lower() in (
258
+ "1",
259
+ "true",
260
+ "yes",
261
+ ):
262
+ cwd = _safe_cwd()
263
+ current = cwd
264
+ for _ in range(5): # Check up to 5 levels up from current directory
265
+ if (current / "pyproject.toml").exists() and (
266
+ current / "src" / "claude_mpm"
267
+ ).exists():
268
+ # Check if this is the claude-mpm project
269
+ try:
270
+ pyproject_content = (current / "pyproject.toml").read_text()
271
+ if (
272
+ 'name = "claude-mpm"' in pyproject_content
273
+ or '"claude-mpm"' in pyproject_content
274
+ ):
275
+ logger.debug(
276
+ f"CLAUDE_MPM_PREFER_LOCAL_SOURCE: Using development directory at {current}"
277
+ )
278
+ return DeploymentContext.DEVELOPMENT
279
+ except Exception: # nosec B110
280
+ pass
281
+ if current == current.parent:
282
+ break
283
+ current = current.parent
284
+
285
+ # Final fallback: assume development mode
286
+ logger.debug("No installation detected, defaulting to development mode")
287
+ return DeploymentContext.DEVELOPMENT
300
288
 
301
289
 
302
290
  class UnifiedPathManager: