claude-mpm 4.1.2__py3-none-any.whl → 4.1.3__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 (53) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/templates/engineer.json +33 -11
  3. claude_mpm/cli/commands/agents.py +556 -1009
  4. claude_mpm/cli/commands/memory.py +248 -927
  5. claude_mpm/cli/commands/run.py +139 -484
  6. claude_mpm/cli/startup_logging.py +76 -0
  7. claude_mpm/core/agent_registry.py +6 -10
  8. claude_mpm/core/framework_loader.py +114 -595
  9. claude_mpm/core/logging_config.py +2 -4
  10. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  11. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  12. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  13. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  14. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  15. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  16. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  17. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  18. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  19. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  20. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  21. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  22. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  23. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  24. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  25. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  26. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  28. claude_mpm/services/agents/registry/__init__.py +1 -1
  29. claude_mpm/services/cli/__init__.py +18 -0
  30. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  31. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  32. claude_mpm/services/cli/agent_listing_service.py +463 -0
  33. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  34. claude_mpm/services/cli/agent_validation_service.py +589 -0
  35. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  36. claude_mpm/services/cli/memory_crud_service.py +617 -0
  37. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  38. claude_mpm/services/cli/session_manager.py +513 -0
  39. claude_mpm/services/cli/socketio_manager.py +498 -0
  40. claude_mpm/services/cli/startup_checker.py +370 -0
  41. claude_mpm/services/core/cache_manager.py +311 -0
  42. claude_mpm/services/core/memory_manager.py +637 -0
  43. claude_mpm/services/core/path_resolver.py +498 -0
  44. claude_mpm/services/core/service_container.py +520 -0
  45. claude_mpm/services/core/service_interfaces.py +436 -0
  46. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  47. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/METADATA +1 -1
  48. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/RECORD +52 -22
  49. claude_mpm/cli/commands/run_config_checker.py +0 -159
  50. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/WHEEL +0 -0
  51. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/entry_points.txt +0 -0
  52. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/licenses/LICENSE +0 -0
  53. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,498 @@
1
+ """
2
+ Path Resolution Service
3
+ =======================
4
+
5
+ This module provides the PathResolver service for handling all path resolution logic
6
+ that was previously embedded in FrameworkLoader. It manages:
7
+ - Framework path detection (packaged vs development)
8
+ - NPM global path resolution
9
+ - Deployment context management
10
+ - Instruction file path resolution with precedence
11
+ - Cross-platform path handling
12
+
13
+ The service consolidates path management logic while maintaining backward compatibility.
14
+ """
15
+
16
+ import logging
17
+ import subprocess
18
+ from enum import Enum
19
+ from pathlib import Path
20
+ from typing import Dict, Optional, Tuple
21
+
22
+ from .service_interfaces import ICacheManager, IPathResolver
23
+
24
+
25
+ class DeploymentContext(Enum):
26
+ """Deployment context enumeration."""
27
+
28
+ DEVELOPMENT = "development"
29
+ EDITABLE_INSTALL = "editable_install"
30
+ PIP_INSTALL = "pip_install"
31
+ PIPX_INSTALL = "pipx_install"
32
+ SYSTEM_PACKAGE = "system_package"
33
+ UNKNOWN = "unknown"
34
+
35
+
36
+ class PathResolver(IPathResolver):
37
+ """
38
+ Service for resolving and managing paths in the claude-mpm framework.
39
+
40
+ This service extracts path resolution logic from FrameworkLoader to provide
41
+ a focused, reusable service for path management across the application.
42
+ """
43
+
44
+ def __init__(self, cache_manager: Optional[ICacheManager] = None):
45
+ """
46
+ Initialize the PathResolver service.
47
+
48
+ Args:
49
+ cache_manager: Optional cache manager for caching resolved paths
50
+ """
51
+ self.logger = logging.getLogger(__name__)
52
+ self.cache_manager = cache_manager
53
+ self._framework_path: Optional[Path] = None
54
+ self._deployment_context: Optional[DeploymentContext] = None
55
+ self._path_cache: Dict[str, str] = {} # Internal cache for paths
56
+
57
+ def resolve_path(self, path: str, base_dir: Optional[Path] = None) -> Path:
58
+ """
59
+ Resolve a path relative to a base directory.
60
+
61
+ Args:
62
+ path: The path to resolve (can be relative or absolute)
63
+ base_dir: Base directory for relative paths (defaults to cwd)
64
+
65
+ Returns:
66
+ The resolved absolute path
67
+ """
68
+ path_obj = Path(path)
69
+
70
+ if path_obj.is_absolute():
71
+ return path_obj
72
+
73
+ if base_dir is None:
74
+ base_dir = Path.cwd()
75
+
76
+ return (base_dir / path_obj).resolve()
77
+
78
+ def validate_path(self, path: Path, must_exist: bool = False) -> bool:
79
+ """
80
+ Validate a path for security and existence.
81
+
82
+ Args:
83
+ path: The path to validate
84
+ must_exist: Whether the path must exist
85
+
86
+ Returns:
87
+ True if path is valid, False otherwise
88
+ """
89
+ try:
90
+ # Resolve to absolute path to check for path traversal
91
+ resolved = path.resolve()
92
+
93
+ # Check if path exists if required
94
+ if must_exist and not resolved.exists():
95
+ return False
96
+
97
+ return True
98
+ except (OSError, ValueError):
99
+ return False
100
+
101
+ def ensure_directory(self, path: Path) -> Path:
102
+ """
103
+ Ensure a directory exists, creating it if necessary.
104
+
105
+ Args:
106
+ path: The directory path
107
+
108
+ Returns:
109
+ The directory path
110
+ """
111
+ path = path.resolve()
112
+ if not path.exists():
113
+ path.mkdir(parents=True, exist_ok=True)
114
+ self.logger.debug(f"Created directory: {path}")
115
+ elif not path.is_dir():
116
+ raise ValueError(f"Path exists but is not a directory: {path}")
117
+ return path
118
+
119
+ def find_project_root(self, start_path: Optional[Path] = None) -> Optional[Path]:
120
+ """
121
+ Find the project root directory.
122
+
123
+ Looks for common project indicators like .git, pyproject.toml, package.json, etc.
124
+
125
+ Args:
126
+ start_path: Starting path for search (defaults to cwd)
127
+
128
+ Returns:
129
+ Project root path or None if not found
130
+ """
131
+ if start_path is None:
132
+ start_path = Path.cwd()
133
+
134
+ start_path = start_path.resolve()
135
+
136
+ # If start_path is a file, use its parent directory
137
+ if start_path.is_file():
138
+ start_path = start_path.parent
139
+
140
+ # Common project root indicators
141
+ root_indicators = [
142
+ ".git",
143
+ "pyproject.toml",
144
+ "setup.py",
145
+ "setup.cfg",
146
+ "package.json",
147
+ "Cargo.toml",
148
+ "go.mod",
149
+ "pom.xml",
150
+ "build.gradle",
151
+ ".claude-mpm", # Claude MPM specific
152
+ "CLAUDE.md", # Claude project instructions
153
+ ]
154
+
155
+ current = start_path
156
+ while current != current.parent: # Stop at filesystem root
157
+ for indicator in root_indicators:
158
+ if (current / indicator).exists():
159
+ self.logger.debug(
160
+ f"Found project root at {current} (indicator: {indicator})"
161
+ )
162
+ return current
163
+ current = current.parent
164
+
165
+ # If no indicators found, return None
166
+ self.logger.debug(f"No project root found from {start_path}")
167
+ return None
168
+
169
+ def detect_framework_path(self) -> Optional[Path]:
170
+ """
171
+ Auto-detect claude-mpm framework using unified path management.
172
+
173
+ Returns:
174
+ Path to framework root or Path("__PACKAGED__") for packaged installations,
175
+ None if framework not found
176
+ """
177
+ # Check cache first
178
+ if self._framework_path is not None:
179
+ return self._framework_path
180
+
181
+ # Try to use internal cache if available
182
+ if "framework_path" in self._path_cache:
183
+ cached_path = self._path_cache["framework_path"]
184
+ self._framework_path = (
185
+ Path(cached_path)
186
+ if cached_path != "__PACKAGED__"
187
+ else Path("__PACKAGED__")
188
+ )
189
+ return self._framework_path
190
+
191
+ # Try unified path manager first
192
+ framework_path = self._detect_via_unified_paths()
193
+ if framework_path:
194
+ self._cache_framework_path(framework_path)
195
+ return framework_path
196
+
197
+ # Fallback to package detection
198
+ framework_path = self._detect_via_package()
199
+ if framework_path:
200
+ self._cache_framework_path(framework_path)
201
+ return framework_path
202
+
203
+ # Try development mode detection
204
+ framework_path = self._detect_development_mode()
205
+ if framework_path:
206
+ self._cache_framework_path(framework_path)
207
+ return framework_path
208
+
209
+ # Check common locations
210
+ framework_path = self._check_common_locations()
211
+ if framework_path:
212
+ self._cache_framework_path(framework_path)
213
+ return framework_path
214
+
215
+ self.logger.warning("Framework not found, will use minimal instructions")
216
+ return None
217
+
218
+ def get_npm_global_path(self) -> Optional[Path]:
219
+ """
220
+ Get npm global installation path for @bobmatnyc/claude-multiagent-pm.
221
+
222
+ Returns:
223
+ Path to npm global installation or None if not found
224
+ """
225
+ # Check internal cache first
226
+ if "npm_global_path" in self._path_cache:
227
+ cached_path = self._path_cache["npm_global_path"]
228
+ return Path(cached_path) if cached_path != "NOT_FOUND" else None
229
+
230
+ npm_path = self._detect_npm_global()
231
+
232
+ # Cache the result internally
233
+ cache_value = str(npm_path) if npm_path else "NOT_FOUND"
234
+ self._path_cache["npm_global_path"] = cache_value
235
+
236
+ return npm_path
237
+
238
+ def get_deployment_context(self) -> DeploymentContext:
239
+ """
240
+ Get the current deployment context.
241
+
242
+ Returns:
243
+ The detected deployment context
244
+ """
245
+ if self._deployment_context is None:
246
+ self._deployment_context = self._detect_deployment_context()
247
+ return self._deployment_context
248
+
249
+ def discover_agent_paths(
250
+ self, agents_dir: Optional[Path] = None, framework_path: Optional[Path] = None
251
+ ) -> Tuple[Optional[Path], Optional[Path], Optional[Path]]:
252
+ """
253
+ Discover agent directories based on priority.
254
+
255
+ Args:
256
+ agents_dir: Custom agents directory override
257
+ framework_path: Framework path to search in
258
+
259
+ Returns:
260
+ Tuple of (agents_dir, templates_dir, main_dir)
261
+ """
262
+ discovered_agents_dir = None
263
+ templates_dir = None
264
+ main_dir = None
265
+
266
+ if agents_dir and agents_dir.exists():
267
+ discovered_agents_dir = agents_dir
268
+ self.logger.info(f"Using custom agents directory: {discovered_agents_dir}")
269
+ elif framework_path and framework_path != Path("__PACKAGED__"):
270
+ # Prioritize templates directory over main agents directory
271
+ templates_dir = (
272
+ framework_path / "src" / "claude_mpm" / "agents" / "templates"
273
+ )
274
+ main_dir = framework_path / "src" / "claude_mpm" / "agents"
275
+
276
+ if templates_dir.exists() and any(templates_dir.glob("*.md")):
277
+ discovered_agents_dir = templates_dir
278
+ self.logger.info(
279
+ f"Using agents from templates directory: {discovered_agents_dir}"
280
+ )
281
+ elif main_dir.exists() and any(main_dir.glob("*.md")):
282
+ discovered_agents_dir = main_dir
283
+ self.logger.info(
284
+ f"Using agents from main directory: {discovered_agents_dir}"
285
+ )
286
+
287
+ return discovered_agents_dir, templates_dir, main_dir
288
+
289
+ def get_instruction_file_paths(self) -> Dict[str, Optional[Path]]:
290
+ """
291
+ Get paths for instruction files with precedence.
292
+
293
+ Returns:
294
+ Dictionary mapping instruction type to path:
295
+ - "project": Project-specific INSTRUCTIONS.md
296
+ - "user": User-specific INSTRUCTIONS.md
297
+ - "system": System-wide INSTRUCTIONS.md
298
+ """
299
+ paths = {"project": None, "user": None, "system": None}
300
+
301
+ # Project-specific instructions
302
+ project_path = Path.cwd() / ".claude-mpm" / "INSTRUCTIONS.md"
303
+ if project_path.exists():
304
+ paths["project"] = project_path
305
+
306
+ # User-specific instructions
307
+ user_path = Path.home() / ".claude-mpm" / "INSTRUCTIONS.md"
308
+ if user_path.exists():
309
+ paths["user"] = user_path
310
+
311
+ # System-wide instructions (if framework is detected)
312
+ framework_path = self.detect_framework_path()
313
+ if framework_path and framework_path != Path("__PACKAGED__"):
314
+ system_path = (
315
+ framework_path / "src" / "claude_mpm" / "agents" / "INSTRUCTIONS.md"
316
+ )
317
+ if system_path.exists():
318
+ paths["system"] = system_path
319
+
320
+ return paths
321
+
322
+ # Private helper methods
323
+
324
+ def _detect_via_unified_paths(self) -> Optional[Path]:
325
+ """Detect framework path using unified path management."""
326
+ try:
327
+ # Import here to avoid circular dependencies
328
+ from ...core.unified_paths import (
329
+ DeploymentContext as UnifiedContext,
330
+ get_path_manager,
331
+ )
332
+
333
+ path_manager = get_path_manager()
334
+ deployment_context = path_manager._deployment_context
335
+
336
+ # Map unified context to our context
337
+ context_map = {
338
+ UnifiedContext.PIP_INSTALL: DeploymentContext.PIP_INSTALL,
339
+ UnifiedContext.PIPX_INSTALL: DeploymentContext.PIPX_INSTALL,
340
+ UnifiedContext.SYSTEM_PACKAGE: DeploymentContext.SYSTEM_PACKAGE,
341
+ UnifiedContext.DEVELOPMENT: DeploymentContext.DEVELOPMENT,
342
+ UnifiedContext.EDITABLE_INSTALL: DeploymentContext.EDITABLE_INSTALL,
343
+ }
344
+
345
+ if deployment_context in context_map:
346
+ self._deployment_context = context_map[deployment_context]
347
+
348
+ # Check if we're in a packaged installation
349
+ if deployment_context in [
350
+ UnifiedContext.PIP_INSTALL,
351
+ UnifiedContext.PIPX_INSTALL,
352
+ UnifiedContext.SYSTEM_PACKAGE,
353
+ ]:
354
+ self.logger.info(
355
+ f"Running from packaged installation (context: {deployment_context})"
356
+ )
357
+ return Path("__PACKAGED__")
358
+
359
+ if deployment_context == UnifiedContext.DEVELOPMENT:
360
+ # Development mode - use framework root
361
+ framework_root = path_manager.framework_root
362
+ if (framework_root / "src" / "claude_mpm" / "agents").exists():
363
+ self.logger.info(
364
+ f"Using claude-mpm development installation at: {framework_root}"
365
+ )
366
+ return framework_root
367
+
368
+ elif deployment_context == UnifiedContext.EDITABLE_INSTALL:
369
+ # Editable install - similar to development
370
+ framework_root = path_manager.framework_root
371
+ if (framework_root / "src" / "claude_mpm" / "agents").exists():
372
+ self.logger.info(
373
+ f"Using claude-mpm editable installation at: {framework_root}"
374
+ )
375
+ return framework_root
376
+
377
+ except Exception as e:
378
+ self.logger.warning(
379
+ f"Failed to use unified path manager for framework detection: {e}"
380
+ )
381
+
382
+ return None
383
+
384
+ def _detect_via_package(self) -> Optional[Path]:
385
+ """Detect framework via package installation."""
386
+ try:
387
+ import claude_mpm
388
+
389
+ package_file = Path(claude_mpm.__file__)
390
+
391
+ # For packaged installations, we don't need a framework path
392
+ # since we'll use importlib.resources to load files
393
+ if "site-packages" in str(package_file) or "dist-packages" in str(
394
+ package_file
395
+ ):
396
+ self.logger.info(
397
+ f"Running from packaged installation at: {package_file.parent}"
398
+ )
399
+ self._deployment_context = DeploymentContext.PIP_INSTALL
400
+ return Path("__PACKAGED__")
401
+ except ImportError:
402
+ pass
403
+
404
+ return None
405
+
406
+ def _detect_development_mode(self) -> Optional[Path]:
407
+ """Detect if running in development mode."""
408
+ current_file = Path(__file__)
409
+
410
+ if "claude-mpm" in str(current_file):
411
+ # We're running from claude-mpm, use its agents
412
+ for parent in current_file.parents:
413
+ if parent.name == "claude-mpm":
414
+ if (parent / "src" / "claude_mpm" / "agents").exists():
415
+ self.logger.info(f"Using claude-mpm at: {parent}")
416
+ self._deployment_context = DeploymentContext.DEVELOPMENT
417
+ return parent
418
+ break
419
+
420
+ return None
421
+
422
+ def _check_common_locations(self) -> Optional[Path]:
423
+ """Check common locations for claude-mpm."""
424
+ candidates = [
425
+ # Current directory (if we're already in claude-mpm)
426
+ Path.cwd(),
427
+ # Development location
428
+ Path.home() / "Projects" / "claude-mpm",
429
+ # Current directory subdirectory
430
+ Path.cwd() / "claude-mpm",
431
+ ]
432
+
433
+ for candidate in candidates:
434
+ if candidate and candidate.exists():
435
+ # Check for claude-mpm agents directory
436
+ if (candidate / "src" / "claude_mpm" / "agents").exists():
437
+ self.logger.info(f"Found claude-mpm at: {candidate}")
438
+ return candidate
439
+
440
+ return None
441
+
442
+ def _detect_npm_global(self) -> Optional[Path]:
443
+ """Detect npm global installation path."""
444
+ try:
445
+ result = subprocess.run(
446
+ ["npm", "root", "-g"],
447
+ capture_output=True,
448
+ text=True,
449
+ timeout=5,
450
+ check=False,
451
+ )
452
+ if result.returncode == 0:
453
+ npm_root = Path(result.stdout.strip())
454
+ npm_path = npm_root / "@bobmatnyc" / "claude-multiagent-pm"
455
+ if npm_path.exists():
456
+ return npm_path
457
+ except (subprocess.SubprocessError, OSError, FileNotFoundError):
458
+ pass
459
+
460
+ return None
461
+
462
+ def _detect_deployment_context(self) -> DeploymentContext:
463
+ """Detect the current deployment context."""
464
+ # If already detected via framework path detection
465
+ if self._deployment_context:
466
+ return self._deployment_context
467
+
468
+ # Try to detect based on current environment
469
+ try:
470
+ import claude_mpm
471
+
472
+ package_file = Path(claude_mpm.__file__)
473
+ package_str = str(package_file)
474
+
475
+ # Check for pipx first (more specific)
476
+ if ".local" in package_str and "pipx" in package_str:
477
+ return DeploymentContext.PIPX_INSTALL
478
+ if "dist-packages" in package_str:
479
+ return DeploymentContext.SYSTEM_PACKAGE
480
+ if "site-packages" in package_str:
481
+ return DeploymentContext.PIP_INSTALL
482
+
483
+ except ImportError:
484
+ pass
485
+
486
+ # Check if we're in development
487
+ if (Path.cwd() / "pyproject.toml").exists():
488
+ return DeploymentContext.DEVELOPMENT
489
+
490
+ return DeploymentContext.UNKNOWN
491
+
492
+ def _cache_framework_path(self, path: Path) -> None:
493
+ """Cache the framework path."""
494
+ self._framework_path = path
495
+
496
+ # Cache internally
497
+ cache_value = str(path) if path != Path("__PACKAGED__") else "__PACKAGED__"
498
+ self._path_cache["framework_path"] = cache_value