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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
claude_mpm/core/claude_runner.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Claude runner with both exec and subprocess launch methods."""
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
3
5
|
import os
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Optional
|
|
@@ -211,9 +213,146 @@ class ClaudeRunner:
|
|
|
211
213
|
}
|
|
212
214
|
)
|
|
213
215
|
|
|
216
|
+
def _get_deployment_state_path(self) -> Path:
|
|
217
|
+
"""Get path to deployment state file.
|
|
218
|
+
|
|
219
|
+
CRITICAL: Must match path used by startup.py::_save_deployment_state_after_reconciliation()
|
|
220
|
+
Located at: .claude-mpm/cache/deployment_state.json
|
|
221
|
+
"""
|
|
222
|
+
return Path.cwd() / ".claude-mpm" / "cache" / "deployment_state.json"
|
|
223
|
+
|
|
224
|
+
def _calculate_deployment_hash(self, agents_dir: Path) -> str:
|
|
225
|
+
"""Calculate hash of all agent files for change detection.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
agents_dir: Directory containing agent .md files
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
SHA256 hash of agent file contents
|
|
232
|
+
"""
|
|
233
|
+
if not agents_dir.exists():
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
# Get all .md files sorted for consistent hashing
|
|
237
|
+
agent_files = sorted(agents_dir.glob("*.md"))
|
|
238
|
+
|
|
239
|
+
hash_obj = hashlib.sha256()
|
|
240
|
+
for agent_file in agent_files:
|
|
241
|
+
# Include filename and content in hash
|
|
242
|
+
hash_obj.update(agent_file.name.encode())
|
|
243
|
+
try:
|
|
244
|
+
hash_obj.update(agent_file.read_bytes())
|
|
245
|
+
except Exception as e:
|
|
246
|
+
self.logger.debug(f"Error reading {agent_file} for hash: {e}")
|
|
247
|
+
|
|
248
|
+
return hash_obj.hexdigest()
|
|
249
|
+
|
|
250
|
+
def _check_deployment_state(self) -> bool:
|
|
251
|
+
"""Check if agents are already deployed and up-to-date.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if agents are already deployed and match current version, False otherwise
|
|
255
|
+
"""
|
|
256
|
+
state_file = self._get_deployment_state_path()
|
|
257
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
258
|
+
|
|
259
|
+
# If state file doesn't exist, need to deploy
|
|
260
|
+
if not state_file.exists():
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
# If agents directory doesn't exist, need to deploy
|
|
264
|
+
if not agents_dir.exists():
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Load deployment state
|
|
269
|
+
state_data = json.loads(state_file.read_text())
|
|
270
|
+
|
|
271
|
+
# Get current version from package
|
|
272
|
+
from claude_mpm import __version__
|
|
273
|
+
|
|
274
|
+
# Check if version matches
|
|
275
|
+
if state_data.get("version") != __version__:
|
|
276
|
+
self.logger.debug(
|
|
277
|
+
f"Version mismatch: {state_data.get('version')} != {__version__}"
|
|
278
|
+
)
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Check if agent count and hash match
|
|
282
|
+
current_hash = self._calculate_deployment_hash(agents_dir)
|
|
283
|
+
stored_hash = state_data.get("deployment_hash", "")
|
|
284
|
+
|
|
285
|
+
if current_hash != stored_hash:
|
|
286
|
+
self.logger.debug("Agent deployment hash mismatch")
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
# All checks passed - agents are already deployed
|
|
290
|
+
agent_count = state_data.get("agent_count", 0)
|
|
291
|
+
self.logger.debug(
|
|
292
|
+
f"Agents already deployed: {agent_count} agents (v{__version__})"
|
|
293
|
+
)
|
|
294
|
+
return True
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
self.logger.debug(f"Error checking deployment state: {e}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
def _save_deployment_state(self, agent_count: int) -> None:
|
|
301
|
+
"""Save current deployment state.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
agent_count: Number of agents deployed
|
|
305
|
+
"""
|
|
306
|
+
state_file = self._get_deployment_state_path()
|
|
307
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
import time
|
|
311
|
+
|
|
312
|
+
from claude_mpm import __version__
|
|
313
|
+
|
|
314
|
+
# Calculate deployment hash
|
|
315
|
+
deployment_hash = self._calculate_deployment_hash(agents_dir)
|
|
316
|
+
|
|
317
|
+
# Create state data
|
|
318
|
+
state_data = {
|
|
319
|
+
"version": __version__,
|
|
320
|
+
"agent_count": agent_count,
|
|
321
|
+
"deployment_hash": deployment_hash,
|
|
322
|
+
"deployed_at": time.time(),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Ensure directory exists
|
|
326
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
327
|
+
|
|
328
|
+
# Write state file
|
|
329
|
+
state_file.write_text(json.dumps(state_data, indent=2))
|
|
330
|
+
self.logger.debug(f"Saved deployment state: {agent_count} agents")
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
self.logger.debug(f"Error saving deployment state: {e}")
|
|
334
|
+
|
|
214
335
|
def setup_agents(self) -> bool:
|
|
215
336
|
"""Deploy native agents to .claude/agents/."""
|
|
216
337
|
try:
|
|
338
|
+
# SIMPLE CHECK: If agents already exist from reconciliation, skip deployment
|
|
339
|
+
# This ensures reconciliation's user-configured agents are never overwritten
|
|
340
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
341
|
+
if agents_dir.exists():
|
|
342
|
+
existing_agents = list(agents_dir.glob("*.md"))
|
|
343
|
+
if len(existing_agents) > 0:
|
|
344
|
+
# Reconciliation already deployed agents - skip
|
|
345
|
+
self.logger.debug(
|
|
346
|
+
f"Skipping setup_agents: {len(existing_agents)} agents already deployed by reconciliation"
|
|
347
|
+
)
|
|
348
|
+
if self.project_logger:
|
|
349
|
+
self.project_logger.log_system(
|
|
350
|
+
f"Agents already deployed via reconciliation: {len(existing_agents)} agents",
|
|
351
|
+
level="DEBUG",
|
|
352
|
+
component="deployment",
|
|
353
|
+
)
|
|
354
|
+
return True
|
|
355
|
+
|
|
217
356
|
if self.project_logger:
|
|
218
357
|
self.project_logger.log_system(
|
|
219
358
|
"Starting agent deployment", level="INFO", component="deployment"
|
|
@@ -239,6 +378,12 @@ class ClaudeRunner:
|
|
|
239
378
|
|
|
240
379
|
# Set Claude environment
|
|
241
380
|
self.deployment_service.set_claude_environment()
|
|
381
|
+
|
|
382
|
+
# Save deployment state for future runs
|
|
383
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
384
|
+
total_agents = len(list(agents_dir.glob("*.md")))
|
|
385
|
+
self._save_deployment_state(total_agents)
|
|
386
|
+
|
|
242
387
|
return True
|
|
243
388
|
self.logger.info("All agents already up to date")
|
|
244
389
|
if self.project_logger:
|
|
@@ -247,6 +392,13 @@ class ClaudeRunner:
|
|
|
247
392
|
level="INFO",
|
|
248
393
|
component="deployment",
|
|
249
394
|
)
|
|
395
|
+
|
|
396
|
+
# Save deployment state even if no changes
|
|
397
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
398
|
+
if agents_dir.exists():
|
|
399
|
+
total_agents = len(list(agents_dir.glob("*.md")))
|
|
400
|
+
self._save_deployment_state(total_agents)
|
|
401
|
+
|
|
250
402
|
return True
|
|
251
403
|
|
|
252
404
|
except PermissionError as e:
|
claude_mpm/core/config.py
CHANGED
|
@@ -12,11 +12,10 @@ import threading
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
14
|
|
|
15
|
-
import yaml
|
|
16
|
-
|
|
17
15
|
from claude_mpm.core.logging_utils import get_logger
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
# Lazy import ConfigurationManager to avoid importing yaml at module level
|
|
18
|
+
# This prevents hook errors when yaml isn't available in the execution environment
|
|
20
19
|
from .exceptions import ConfigurationError, FileOperationError
|
|
21
20
|
from .unified_paths import get_path_manager
|
|
22
21
|
|
|
@@ -104,6 +103,9 @@ class Config:
|
|
|
104
103
|
Config._initialized = True
|
|
105
104
|
logger.debug("Initializing Config singleton for the first time")
|
|
106
105
|
|
|
106
|
+
# Lazy import ConfigurationManager at runtime to avoid yaml import at module level
|
|
107
|
+
from ..utils.config_manager import ConfigurationManager
|
|
108
|
+
|
|
107
109
|
# Initialize instance variables inside the lock to ensure thread safety
|
|
108
110
|
self._config: Dict[str, Any] = {}
|
|
109
111
|
self._env_prefix = env_prefix
|
|
@@ -224,21 +226,6 @@ class Config:
|
|
|
224
226
|
f"Response logging format: {response_logging.get('format', 'json')}"
|
|
225
227
|
)
|
|
226
228
|
|
|
227
|
-
except yaml.YAMLError as e:
|
|
228
|
-
logger.error(f"YAML syntax error in {file_path}: {e}")
|
|
229
|
-
if hasattr(e, "problem_mark"):
|
|
230
|
-
mark = e.problem_mark
|
|
231
|
-
logger.error(f"Error at line {mark.line + 1}, column {mark.column + 1}")
|
|
232
|
-
logger.info(
|
|
233
|
-
"TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
|
|
234
|
-
)
|
|
235
|
-
logger.info(
|
|
236
|
-
"TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
|
|
237
|
-
+ str(file_path)
|
|
238
|
-
)
|
|
239
|
-
# Store error for later retrieval
|
|
240
|
-
self._config["_load_error"] = str(e)
|
|
241
|
-
|
|
242
229
|
except json.JSONDecodeError as e:
|
|
243
230
|
logger.error(f"JSON syntax error in {file_path}: {e}")
|
|
244
231
|
logger.error(f"Error at line {e.lineno}, column {e.colno}")
|
|
@@ -255,7 +242,28 @@ class Config:
|
|
|
255
242
|
},
|
|
256
243
|
) from e
|
|
257
244
|
except Exception as e:
|
|
258
|
-
#
|
|
245
|
+
# Handle YAML errors without importing yaml at module level
|
|
246
|
+
# ConfigurationManager.load_yaml raises yaml.YAMLError, but we don't want to import yaml
|
|
247
|
+
# Check if it's a YAML error by class name to avoid import
|
|
248
|
+
if e.__class__.__name__ == "YAMLError":
|
|
249
|
+
logger.error(f"YAML syntax error in {file_path}: {e}")
|
|
250
|
+
if hasattr(e, "problem_mark"):
|
|
251
|
+
mark = e.problem_mark
|
|
252
|
+
logger.error(
|
|
253
|
+
f"Error at line {mark.line + 1}, column {mark.column + 1}"
|
|
254
|
+
)
|
|
255
|
+
logger.info(
|
|
256
|
+
"TIP: Validate your YAML at https://www.yamllint.com/ or run: python scripts/validate_configuration.py"
|
|
257
|
+
)
|
|
258
|
+
logger.info(
|
|
259
|
+
"TIP: Common issue - YAML requires spaces, not tabs. Fix with: sed -i '' 's/\t/ /g' "
|
|
260
|
+
+ str(file_path)
|
|
261
|
+
)
|
|
262
|
+
# Store error for later retrieval
|
|
263
|
+
self._config["_load_error"] = str(e)
|
|
264
|
+
return # Don't re-raise, we handled it
|
|
265
|
+
|
|
266
|
+
# Not a YAML error, wrap as configuration error
|
|
259
267
|
raise ConfigurationError(
|
|
260
268
|
f"Unexpected error loading configuration from {file_path}: {e}",
|
|
261
269
|
context={
|
|
@@ -491,7 +499,7 @@ class Config:
|
|
|
491
499
|
# Socket.IO server health and recovery configuration
|
|
492
500
|
"socketio_server": {
|
|
493
501
|
"host": "localhost",
|
|
494
|
-
"port":
|
|
502
|
+
"port": 8768, # Default SocketIO port (from network_config.NetworkPorts)
|
|
495
503
|
"enable_health_monitoring": True,
|
|
496
504
|
"enable_recovery": True,
|
|
497
505
|
"health_monitoring": {
|
|
@@ -532,7 +540,7 @@ class Config:
|
|
|
532
540
|
# Monitor server configuration (decoupled from dashboard)
|
|
533
541
|
"monitor_server": {
|
|
534
542
|
"host": "localhost",
|
|
535
|
-
"port": 8765, # Default monitor port (
|
|
543
|
+
"port": 8765, # Default monitor port (from network_config.NetworkPorts.MONITOR_DEFAULT)
|
|
536
544
|
"enable_health_monitoring": True,
|
|
537
545
|
"auto_start": False, # Don't auto-start with dashboard by default
|
|
538
546
|
"event_buffer_size": 2000, # Larger buffer for monitor server
|
|
@@ -541,7 +549,7 @@ class Config:
|
|
|
541
549
|
# Dashboard server configuration (connects to monitor)
|
|
542
550
|
"dashboard_server": {
|
|
543
551
|
"host": "localhost",
|
|
544
|
-
"port":
|
|
552
|
+
"port": 8767, # Dashboard UI port (from network_config.NetworkPorts.DASHBOARD_DEFAULT)
|
|
545
553
|
"monitor_host": "localhost", # Monitor server host to connect to
|
|
546
554
|
"monitor_port": 8765, # Monitor server port to connect to
|
|
547
555
|
"auto_connect_monitor": True, # Automatically connect to monitor
|
|
@@ -44,11 +44,14 @@ class ConfigConstants:
|
|
|
44
44
|
"startup": 60,
|
|
45
45
|
"graceful_shutdown": 30,
|
|
46
46
|
},
|
|
47
|
-
# Ports
|
|
47
|
+
# Ports (updated to use network_config.NetworkPorts defaults)
|
|
48
48
|
"ports": {
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
49
|
+
"monitor_default": 8765, # NetworkPorts.MONITOR_DEFAULT
|
|
50
|
+
"commander_default": 8766, # NetworkPorts.COMMANDER_DEFAULT
|
|
51
|
+
"dashboard_default": 8767, # NetworkPorts.DASHBOARD_DEFAULT
|
|
52
|
+
"socketio_default": 8768, # NetworkPorts.SOCKETIO_DEFAULT
|
|
53
|
+
"socketio_range_start": 8765, # NetworkPorts.PORT_RANGE_START
|
|
54
|
+
"socketio_range_end": 8785, # NetworkPorts.PORT_RANGE_END
|
|
52
55
|
},
|
|
53
56
|
# Cache settings
|
|
54
57
|
"cache": {
|
|
@@ -134,23 +137,70 @@ class ConfigConstants:
|
|
|
134
137
|
Get port value by type.
|
|
135
138
|
|
|
136
139
|
Args:
|
|
137
|
-
port_type: Type of port (e.g., 'socketio_default')
|
|
140
|
+
port_type: Type of port (e.g., 'socketio_default', 'monitor_default')
|
|
138
141
|
|
|
139
142
|
Returns:
|
|
140
143
|
Port number
|
|
141
144
|
"""
|
|
142
145
|
try:
|
|
146
|
+
# Try to get from unified config first
|
|
143
147
|
config = cls._get_config_service().config
|
|
144
148
|
|
|
149
|
+
if port_type == "monitor_default":
|
|
150
|
+
return (
|
|
151
|
+
config.network.monitor_port
|
|
152
|
+
if hasattr(config.network, "monitor_port")
|
|
153
|
+
else 8765
|
|
154
|
+
)
|
|
155
|
+
if port_type == "commander_default":
|
|
156
|
+
return (
|
|
157
|
+
config.network.commander_port
|
|
158
|
+
if hasattr(config.network, "commander_port")
|
|
159
|
+
else 8766
|
|
160
|
+
)
|
|
161
|
+
if port_type == "dashboard_default":
|
|
162
|
+
return (
|
|
163
|
+
config.network.dashboard_port
|
|
164
|
+
if hasattr(config.network, "dashboard_port")
|
|
165
|
+
else 8767
|
|
166
|
+
)
|
|
145
167
|
if port_type == "socketio_default":
|
|
146
|
-
return
|
|
168
|
+
return (
|
|
169
|
+
config.network.socketio_port
|
|
170
|
+
if hasattr(config.network, "socketio_port")
|
|
171
|
+
else 8768
|
|
172
|
+
)
|
|
147
173
|
if port_type == "socketio_range_start":
|
|
148
|
-
return
|
|
174
|
+
return (
|
|
175
|
+
config.network.socketio_port_range[0]
|
|
176
|
+
if hasattr(config.network, "socketio_port_range")
|
|
177
|
+
else 8765
|
|
178
|
+
)
|
|
149
179
|
if port_type == "socketio_range_end":
|
|
150
|
-
return
|
|
180
|
+
return (
|
|
181
|
+
config.network.socketio_port_range[1]
|
|
182
|
+
if hasattr(config.network, "socketio_port_range")
|
|
183
|
+
else 8785
|
|
184
|
+
)
|
|
151
185
|
return cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
|
|
152
186
|
except Exception:
|
|
153
|
-
|
|
187
|
+
# Fallback to network_config.NetworkPorts or DEFAULT_VALUES
|
|
188
|
+
try:
|
|
189
|
+
from .network_config import NetworkPorts
|
|
190
|
+
|
|
191
|
+
port_map = {
|
|
192
|
+
"monitor_default": NetworkPorts.MONITOR_DEFAULT,
|
|
193
|
+
"commander_default": NetworkPorts.COMMANDER_DEFAULT,
|
|
194
|
+
"dashboard_default": NetworkPorts.DASHBOARD_DEFAULT,
|
|
195
|
+
"socketio_default": NetworkPorts.SOCKETIO_DEFAULT,
|
|
196
|
+
"socketio_range_start": NetworkPorts.PORT_RANGE_START,
|
|
197
|
+
"socketio_range_end": NetworkPorts.PORT_RANGE_END,
|
|
198
|
+
}
|
|
199
|
+
return port_map.get(
|
|
200
|
+
port_type, cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
|
|
201
|
+
)
|
|
202
|
+
except Exception:
|
|
203
|
+
return cls.DEFAULT_VALUES["ports"].get(port_type, 8765)
|
|
154
204
|
|
|
155
205
|
@classmethod
|
|
156
206
|
def get_cache_setting(cls, setting_name: str) -> Any:
|
|
@@ -304,6 +354,21 @@ def get_socketio_port() -> int:
|
|
|
304
354
|
return ConfigConstants.get_port("socketio_default")
|
|
305
355
|
|
|
306
356
|
|
|
357
|
+
def get_monitor_port() -> int:
|
|
358
|
+
"""Get default monitor port."""
|
|
359
|
+
return ConfigConstants.get_port("monitor_default")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def get_commander_port() -> int:
|
|
363
|
+
"""Get default commander port."""
|
|
364
|
+
return ConfigConstants.get_port("commander_default")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def get_dashboard_port() -> int:
|
|
368
|
+
"""Get default dashboard port."""
|
|
369
|
+
return ConfigConstants.get_port("dashboard_default")
|
|
370
|
+
|
|
371
|
+
|
|
307
372
|
def get_cache_size() -> float:
|
|
308
373
|
"""Get default cache size in MB."""
|
|
309
374
|
return ConfigConstants.get_cache_setting("max_size_mb")
|
claude_mpm/core/constants.py
CHANGED
|
@@ -38,12 +38,36 @@ class SystemLimits:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class NetworkConfig:
|
|
41
|
-
"""Network-related configuration constants.
|
|
41
|
+
"""Network-related configuration constants.
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
NOTE: Port defaults are now centralized in network_config.NetworkPorts.
|
|
44
|
+
This class maintains backward compatibility but delegates to NetworkPorts.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Import from network_config for single source of truth
|
|
48
|
+
# Lazy import to avoid circular dependencies
|
|
49
|
+
@property
|
|
50
|
+
def SOCKETIO_PORT_RANGE(self) -> Tuple[int, int]:
|
|
51
|
+
from .network_config import NetworkPorts
|
|
52
|
+
|
|
53
|
+
return (NetworkPorts.PORT_RANGE_START, NetworkPorts.PORT_RANGE_END)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def DEFAULT_SOCKETIO_PORT(self) -> int:
|
|
57
|
+
from .network_config import NetworkPorts
|
|
58
|
+
|
|
59
|
+
return NetworkPorts.SOCKETIO_DEFAULT
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def DEFAULT_DASHBOARD_PORT(self) -> int:
|
|
63
|
+
from .network_config import NetworkPorts
|
|
64
|
+
|
|
65
|
+
return NetworkPorts.DASHBOARD_DEFAULT
|
|
66
|
+
|
|
67
|
+
# Port ranges (module-level for backward compatibility)
|
|
68
|
+
SOCKETIO_PORT_RANGE: Tuple[int, int] = (8765, 8785) # Will be updated at runtime
|
|
69
|
+
DEFAULT_SOCKETIO_PORT = 8768 # Updated to match new default
|
|
70
|
+
DEFAULT_DASHBOARD_PORT = 8767 # Updated to match new default
|
|
47
71
|
|
|
48
72
|
# Connection timeouts (seconds)
|
|
49
73
|
CONNECTION_TIMEOUT = 5.0
|
|
@@ -303,18 +327,38 @@ DEFAULT_TIMEOUT = TimeoutConfig.DEFAULT_TIMEOUT
|
|
|
303
327
|
|
|
304
328
|
|
|
305
329
|
class NetworkPorts:
|
|
306
|
-
"""Network port configuration.
|
|
330
|
+
"""Network port configuration.
|
|
331
|
+
|
|
332
|
+
DEPRECATED: Use claude_mpm.core.network_config.NetworkPorts instead.
|
|
333
|
+
This class is maintained for backward compatibility.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
# Import from network_config for single source of truth
|
|
337
|
+
@classmethod
|
|
338
|
+
def _get_config(cls):
|
|
339
|
+
from .network_config import NetworkPorts as NewNetworkPorts
|
|
340
|
+
|
|
341
|
+
return NewNetworkPorts
|
|
342
|
+
|
|
343
|
+
# Delegate to new NetworkPorts
|
|
344
|
+
@property
|
|
345
|
+
def DEFAULT_SOCKETIO(self) -> int:
|
|
346
|
+
return self._get_config().SOCKETIO_DEFAULT
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def DEFAULT_DASHBOARD(self) -> int:
|
|
350
|
+
return self._get_config().DASHBOARD_DEFAULT
|
|
307
351
|
|
|
308
|
-
#
|
|
309
|
-
DEFAULT_SOCKETIO =
|
|
310
|
-
DEFAULT_DASHBOARD =
|
|
311
|
-
PORT_RANGE_START =
|
|
312
|
-
PORT_RANGE_END =
|
|
352
|
+
# Keep class-level attributes for compatibility
|
|
353
|
+
DEFAULT_SOCKETIO = 8768 # Updated to match network_config
|
|
354
|
+
DEFAULT_DASHBOARD = 8767 # Updated to match network_config
|
|
355
|
+
PORT_RANGE_START = 8765
|
|
356
|
+
PORT_RANGE_END = 8785
|
|
313
357
|
|
|
314
358
|
@classmethod
|
|
315
359
|
def get_port_range(cls) -> range:
|
|
316
360
|
"""Get the valid port range."""
|
|
317
|
-
return
|
|
361
|
+
return cls._get_config().get_port_range()
|
|
318
362
|
|
|
319
363
|
|
|
320
364
|
class ProjectPaths:
|
claude_mpm/core/hook_manager.py
CHANGED
|
@@ -186,8 +186,9 @@ class HookManager:
|
|
|
186
186
|
env["CLAUDE_MPM_HOOK_DEBUG"] = "true"
|
|
187
187
|
|
|
188
188
|
# Execute with timeout in background thread
|
|
189
|
+
# Run as module to ensure proper package context for relative imports
|
|
189
190
|
result = subprocess.run( # nosec B603 B607
|
|
190
|
-
["python",
|
|
191
|
+
["python", "-m", "claude_mpm.hooks.claude_hooks.hook_handler"],
|
|
191
192
|
input=event_json,
|
|
192
193
|
text=True,
|
|
193
194
|
capture_output=True,
|
|
@@ -143,11 +143,12 @@ class InteractiveSession:
|
|
|
143
143
|
Tuple of (success, environment_dict)
|
|
144
144
|
"""
|
|
145
145
|
try:
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
# NOTE: System agents are deployed via reconciliation during startup.
|
|
147
|
+
# The reconciliation process respects user configuration and handles
|
|
148
|
+
# both native and custom mode deployment. No need to call setup_agents() here.
|
|
149
149
|
|
|
150
|
-
# Deploy project-specific agents
|
|
150
|
+
# Deploy project-specific agents from .claude-mpm/agents/
|
|
151
|
+
# This is separate from system agents and handles user-defined agents
|
|
151
152
|
self.runner.deploy_project_agents_to_claude()
|
|
152
153
|
|
|
153
154
|
# Build command
|
claude_mpm/core/logger.py
CHANGED
|
@@ -391,8 +391,22 @@ def cleanup_old_mpm_logs(
|
|
|
391
391
|
|
|
392
392
|
|
|
393
393
|
def get_logger(name: str) -> logging.Logger:
|
|
394
|
-
"""Get a logger instance.
|
|
395
|
-
|
|
394
|
+
"""Get a logger instance.
|
|
395
|
+
|
|
396
|
+
CRITICAL: Respects startup suppression mode (CRITICAL+1) to prevent
|
|
397
|
+
early INFO logs before setup_logging() is called.
|
|
398
|
+
"""
|
|
399
|
+
logger = logging.getLogger(f"claude_mpm.{name}")
|
|
400
|
+
|
|
401
|
+
# Check if root logger is suppressed (startup.py sets CRITICAL+1)
|
|
402
|
+
root_logger = logging.getLogger()
|
|
403
|
+
if root_logger.level > logging.CRITICAL:
|
|
404
|
+
# Suppression active - ensure this logger is also suppressed
|
|
405
|
+
logger.setLevel(logging.CRITICAL + 1)
|
|
406
|
+
logger.handlers = []
|
|
407
|
+
logger.propagate = False
|
|
408
|
+
|
|
409
|
+
return logger
|
|
396
410
|
|
|
397
411
|
|
|
398
412
|
def setup_streaming_logger(name: str, level: str = "INFO") -> logging.Logger:
|
claude_mpm/core/logging_utils.py
CHANGED
|
@@ -103,23 +103,47 @@ class LoggerFactory:
|
|
|
103
103
|
|
|
104
104
|
# Set up root logger
|
|
105
105
|
root_logger = logging.getLogger()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
date_format or LoggingConfig.DATE_FORMAT,
|
|
106
|
+
|
|
107
|
+
# CRITICAL FIX: Respect existing root logger suppression
|
|
108
|
+
# If root logger is already set to CRITICAL+1 (suppressed by startup.py),
|
|
109
|
+
# don't override it. This prevents logging from appearing during startup
|
|
110
|
+
# before the CLI's setup_logging() runs.
|
|
111
|
+
current_level = root_logger.level
|
|
112
|
+
desired_level = LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
|
|
113
|
+
|
|
114
|
+
# Only set level if current is unset (0) or lower than desired
|
|
115
|
+
# CRITICAL+1 is 51, so this check preserves suppression
|
|
116
|
+
should_configure_logging = current_level == 0 or (
|
|
117
|
+
current_level < desired_level and current_level <= logging.CRITICAL
|
|
119
118
|
)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
|
|
120
|
+
if should_configure_logging:
|
|
121
|
+
root_logger.setLevel(desired_level)
|
|
122
|
+
# else: root logger is suppressed (CRITICAL+1), keep it suppressed
|
|
123
|
+
|
|
124
|
+
# Preserve FileHandlers (e.g., hooks logging), only remove StreamHandlers
|
|
125
|
+
root_logger.handlers = [
|
|
126
|
+
h for h in root_logger.handlers if isinstance(h, logging.FileHandler)
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# CRITICAL FIX: Don't add handlers if logging is suppressed
|
|
130
|
+
# If root logger is at CRITICAL+1 (startup suppression), don't add any handlers
|
|
131
|
+
# This prevents early imports from logging before CLI setup_logging() runs
|
|
132
|
+
if should_configure_logging:
|
|
133
|
+
# Console handler - MUST use stderr to avoid corrupting hook JSON output
|
|
134
|
+
# WHY stderr: Hook handlers output JSON to stdout. Logging to stdout
|
|
135
|
+
# corrupts this JSON and causes "hook error" messages from Claude Code.
|
|
136
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
137
|
+
console_handler.setLevel(
|
|
138
|
+
LoggingConfig.LEVELS.get(cls._log_level, logging.INFO)
|
|
139
|
+
)
|
|
140
|
+
console_formatter = logging.Formatter(
|
|
141
|
+
log_format or LoggingConfig.DEFAULT_FORMAT,
|
|
142
|
+
date_format or LoggingConfig.DATE_FORMAT,
|
|
143
|
+
)
|
|
144
|
+
console_handler.setFormatter(console_formatter)
|
|
145
|
+
root_logger.addHandler(console_handler)
|
|
146
|
+
cls._handlers["console"] = console_handler
|
|
123
147
|
|
|
124
148
|
# File handler (optional)
|
|
125
149
|
if log_to_file and cls._log_dir:
|