claude-mpm 4.1.2__py3-none-any.whl → 4.1.4__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 (87) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +16 -19
  3. claude_mpm/agents/MEMORY.md +21 -49
  4. claude_mpm/agents/templates/OPTIMIZATION_REPORT.md +156 -0
  5. claude_mpm/agents/templates/api_qa.json +36 -116
  6. claude_mpm/agents/templates/backup/data_engineer_agent_20250726_234551.json +42 -9
  7. claude_mpm/agents/templates/backup/documentation_agent_20250726_234551.json +29 -6
  8. claude_mpm/agents/templates/backup/engineer_agent_20250726_234551.json +34 -6
  9. claude_mpm/agents/templates/backup/ops_agent_20250726_234551.json +41 -9
  10. claude_mpm/agents/templates/backup/qa_agent_20250726_234551.json +30 -8
  11. claude_mpm/agents/templates/backup/research_agent_2025011_234551.json +2 -2
  12. claude_mpm/agents/templates/backup/research_agent_20250726_234551.json +29 -6
  13. claude_mpm/agents/templates/backup/research_memory_efficient.json +2 -2
  14. claude_mpm/agents/templates/backup/security_agent_20250726_234551.json +41 -9
  15. claude_mpm/agents/templates/backup/version_control_agent_20250726_234551.json +23 -7
  16. claude_mpm/agents/templates/code_analyzer.json +18 -36
  17. claude_mpm/agents/templates/data_engineer.json +43 -14
  18. claude_mpm/agents/templates/documentation.json +55 -74
  19. claude_mpm/agents/templates/engineer.json +57 -40
  20. claude_mpm/agents/templates/imagemagick.json +7 -2
  21. claude_mpm/agents/templates/memory_manager.json +1 -1
  22. claude_mpm/agents/templates/ops.json +36 -4
  23. claude_mpm/agents/templates/project_organizer.json +23 -71
  24. claude_mpm/agents/templates/qa.json +34 -2
  25. claude_mpm/agents/templates/refactoring_engineer.json +9 -5
  26. claude_mpm/agents/templates/research.json +36 -4
  27. claude_mpm/agents/templates/security.json +29 -2
  28. claude_mpm/agents/templates/ticketing.json +3 -3
  29. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  30. claude_mpm/agents/templates/version_control.json +28 -2
  31. claude_mpm/agents/templates/web_qa.json +38 -151
  32. claude_mpm/agents/templates/web_ui.json +2 -2
  33. claude_mpm/cli/commands/agent_manager.py +221 -1
  34. claude_mpm/cli/commands/agents.py +556 -1009
  35. claude_mpm/cli/commands/memory.py +248 -927
  36. claude_mpm/cli/commands/run.py +139 -484
  37. claude_mpm/cli/parsers/agent_manager_parser.py +34 -0
  38. claude_mpm/cli/startup_logging.py +76 -0
  39. claude_mpm/core/agent_registry.py +6 -10
  40. claude_mpm/core/framework_loader.py +205 -595
  41. claude_mpm/core/log_manager.py +49 -1
  42. claude_mpm/core/logging_config.py +2 -4
  43. claude_mpm/hooks/claude_hooks/event_handlers.py +7 -117
  44. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -755
  45. claude_mpm/hooks/claude_hooks/hook_handler_original.py +1040 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler_refactored.py +347 -0
  47. claude_mpm/hooks/claude_hooks/services/__init__.py +13 -0
  48. claude_mpm/hooks/claude_hooks/services/connection_manager.py +190 -0
  49. claude_mpm/hooks/claude_hooks/services/duplicate_detector.py +106 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +282 -0
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +374 -0
  52. claude_mpm/services/agents/deployment/agent_deployment.py +42 -454
  53. claude_mpm/services/agents/deployment/base_agent_locator.py +132 -0
  54. claude_mpm/services/agents/deployment/deployment_results_manager.py +185 -0
  55. claude_mpm/services/agents/deployment/single_agent_deployer.py +315 -0
  56. claude_mpm/services/agents/memory/agent_memory_manager.py +42 -508
  57. claude_mpm/services/agents/memory/memory_categorization_service.py +165 -0
  58. claude_mpm/services/agents/memory/memory_file_service.py +103 -0
  59. claude_mpm/services/agents/memory/memory_format_service.py +201 -0
  60. claude_mpm/services/agents/memory/memory_limits_service.py +99 -0
  61. claude_mpm/services/agents/registry/__init__.py +1 -1
  62. claude_mpm/services/cli/__init__.py +18 -0
  63. claude_mpm/services/cli/agent_cleanup_service.py +407 -0
  64. claude_mpm/services/cli/agent_dependency_service.py +395 -0
  65. claude_mpm/services/cli/agent_listing_service.py +463 -0
  66. claude_mpm/services/cli/agent_output_formatter.py +605 -0
  67. claude_mpm/services/cli/agent_validation_service.py +589 -0
  68. claude_mpm/services/cli/dashboard_launcher.py +424 -0
  69. claude_mpm/services/cli/memory_crud_service.py +617 -0
  70. claude_mpm/services/cli/memory_output_formatter.py +604 -0
  71. claude_mpm/services/cli/session_manager.py +513 -0
  72. claude_mpm/services/cli/socketio_manager.py +498 -0
  73. claude_mpm/services/cli/startup_checker.py +370 -0
  74. claude_mpm/services/core/cache_manager.py +311 -0
  75. claude_mpm/services/core/memory_manager.py +637 -0
  76. claude_mpm/services/core/path_resolver.py +498 -0
  77. claude_mpm/services/core/service_container.py +520 -0
  78. claude_mpm/services/core/service_interfaces.py +436 -0
  79. claude_mpm/services/diagnostics/checks/agent_check.py +65 -19
  80. claude_mpm/services/memory/router.py +116 -10
  81. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/METADATA +1 -1
  82. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/RECORD +86 -55
  83. claude_mpm/cli/commands/run_config_checker.py +0 -159
  84. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/WHEEL +0 -0
  85. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/entry_points.txt +0 -0
  86. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/licenses/LICENSE +0 -0
  87. {claude_mpm-4.1.2.dist-info → claude_mpm-4.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,513 @@
1
+ """Session management service for CLI commands.
2
+
3
+ WHY: This service extracts session lifecycle management from run.py to improve
4
+ separation of concerns, testability, and reusability across CLI commands. It
5
+ handles session creation, loading, validation, and persistence.
6
+
7
+ DESIGN DECISIONS:
8
+ - Interface-based design for dependency injection
9
+ - Single responsibility: session lifecycle management
10
+ - Supports multiple session contexts (default, orchestration, etc.)
11
+ - Automatic session cleanup and archiving
12
+ - Thread-safe session operations
13
+ - Non-blocking validation with structured warnings
14
+ """
15
+
16
+ import gzip
17
+ import json
18
+ import uuid
19
+ from abc import ABC, abstractmethod
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ from claude_mpm.core.logger import get_logger
26
+
27
+
28
+ # Interface Definition
29
+ class ISessionManager(ABC):
30
+ """Interface for session management service."""
31
+
32
+ @abstractmethod
33
+ def create_session(
34
+ self, context: str = "default", options: Optional[Dict[str, Any]] = None
35
+ ) -> "SessionInfo":
36
+ """Create a new session with options.
37
+
38
+ Args:
39
+ context: Session context (e.g., 'default', 'orchestration')
40
+ options: Optional session configuration
41
+
42
+ Returns:
43
+ SessionInfo object with session details
44
+ """
45
+
46
+ @abstractmethod
47
+ def load_session(self, session_id: str) -> Optional["SessionInfo"]:
48
+ """Load an existing session by ID.
49
+
50
+ Args:
51
+ session_id: Session UUID
52
+
53
+ Returns:
54
+ SessionInfo if found, None otherwise
55
+ """
56
+
57
+ @abstractmethod
58
+ def save_session(self, session_info: "SessionInfo") -> bool:
59
+ """Persist session state to storage.
60
+
61
+ Args:
62
+ session_info: Session to save
63
+
64
+ Returns:
65
+ True if successful, False otherwise
66
+ """
67
+
68
+ @abstractmethod
69
+ def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
70
+ """Get session metadata.
71
+
72
+ Args:
73
+ session_id: Session UUID
74
+
75
+ Returns:
76
+ Session metadata dictionary or None
77
+ """
78
+
79
+ @abstractmethod
80
+ def validate_session(self, session_id: str) -> "SessionValidation":
81
+ """Validate session consistency and health.
82
+
83
+ Args:
84
+ session_id: Session UUID
85
+
86
+ Returns:
87
+ SessionValidation with results
88
+ """
89
+
90
+ @abstractmethod
91
+ def get_recent_sessions(
92
+ self, limit: int = 10, context: Optional[str] = None
93
+ ) -> List["SessionInfo"]:
94
+ """Get recent sessions sorted by last used.
95
+
96
+ Args:
97
+ limit: Maximum number of sessions
98
+ context: Filter by context (optional)
99
+
100
+ Returns:
101
+ List of SessionInfo objects
102
+ """
103
+
104
+ @abstractmethod
105
+ def get_last_interactive_session(self) -> Optional[str]:
106
+ """Get the most recently used interactive session ID.
107
+
108
+ Returns:
109
+ Session ID or None
110
+ """
111
+
112
+ @abstractmethod
113
+ def record_agent_use(self, session_id: str, agent: str, task: str) -> None:
114
+ """Record agent activity in session.
115
+
116
+ Args:
117
+ session_id: Session UUID
118
+ agent: Agent name
119
+ task: Task description
120
+ """
121
+
122
+ @abstractmethod
123
+ def cleanup_old_sessions(
124
+ self, max_age_hours: int = 24, archive: bool = True
125
+ ) -> int:
126
+ """Remove or archive old sessions.
127
+
128
+ Args:
129
+ max_age_hours: Maximum age in hours
130
+ archive: Whether to archive before removing
131
+
132
+ Returns:
133
+ Number of sessions cleaned up
134
+ """
135
+
136
+ @abstractmethod
137
+ def archive_sessions(self, session_ids: List[str]) -> bool:
138
+ """Archive specific sessions.
139
+
140
+ Args:
141
+ session_ids: List of session IDs to archive
142
+
143
+ Returns:
144
+ True if successful, False otherwise
145
+ """
146
+
147
+
148
+ @dataclass
149
+ class SessionInfo:
150
+ """Session information container."""
151
+
152
+ id: str
153
+ context: str = "default"
154
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
155
+ last_used: str = field(default_factory=lambda: datetime.now().isoformat())
156
+ use_count: int = 0
157
+ agents_run: List[Dict[str, Any]] = field(default_factory=list)
158
+ metadata: Dict[str, Any] = field(default_factory=dict)
159
+
160
+ def to_dict(self) -> Dict[str, Any]:
161
+ """Convert to dictionary for serialization."""
162
+ return {
163
+ "id": self.id,
164
+ "context": self.context,
165
+ "created_at": self.created_at,
166
+ "last_used": self.last_used,
167
+ "use_count": self.use_count,
168
+ "agents_run": self.agents_run,
169
+ "metadata": self.metadata,
170
+ }
171
+
172
+ @classmethod
173
+ def from_dict(cls, data: Dict[str, Any]) -> "SessionInfo":
174
+ """Create from dictionary."""
175
+ return cls(
176
+ id=data["id"],
177
+ context=data.get("context", "default"),
178
+ created_at=data.get("created_at", datetime.now().isoformat()),
179
+ last_used=data.get("last_used", datetime.now().isoformat()),
180
+ use_count=data.get("use_count", 0),
181
+ agents_run=data.get("agents_run", []),
182
+ metadata=data.get("metadata", {}),
183
+ )
184
+
185
+
186
+ @dataclass
187
+ class SessionValidation:
188
+ """Session validation results."""
189
+
190
+ valid: bool
191
+ session_id: str
192
+ errors: List[str] = field(default_factory=list)
193
+ warnings: List[str] = field(default_factory=list)
194
+
195
+ @property
196
+ def has_issues(self) -> bool:
197
+ """Check if there are any issues."""
198
+ return bool(self.errors or self.warnings)
199
+
200
+
201
+ class SessionManager(ISessionManager):
202
+ """Service for managing Claude session lifecycle."""
203
+
204
+ def __init__(self, session_dir: Optional[Path] = None, config_service=None):
205
+ """Initialize session manager.
206
+
207
+ Args:
208
+ session_dir: Directory to store session metadata
209
+ config_service: Optional configuration service
210
+ """
211
+ self.session_dir = session_dir or Path.home() / ".claude-mpm" / "sessions"
212
+ self.session_dir.mkdir(parents=True, exist_ok=True)
213
+ self.config_service = config_service
214
+ self.logger = get_logger("SessionManager")
215
+ self._sessions_cache: Dict[str, SessionInfo] = {}
216
+ self._load_sessions()
217
+
218
+ def create_session(
219
+ self, context: str = "default", options: Optional[Dict[str, Any]] = None
220
+ ) -> SessionInfo:
221
+ """Create a new session with options.
222
+
223
+ WHY: Creates a new session with unique ID and initializes metadata.
224
+ This enables session tracking and context preservation.
225
+ """
226
+ session_id = str(uuid.uuid4())
227
+
228
+ session = SessionInfo(id=session_id, context=context, metadata=options or {})
229
+
230
+ self._sessions_cache[session_id] = session
231
+ self._save_sessions()
232
+
233
+ self.logger.info(f"Created session {session_id} for context: {context}")
234
+ return session
235
+
236
+ def load_session(self, session_id: str) -> Optional[SessionInfo]:
237
+ """Load an existing session by ID.
238
+
239
+ WHY: Retrieves session state from cache or disk for resumption.
240
+ """
241
+ # Check cache first
242
+ if session_id in self._sessions_cache:
243
+ return self._sessions_cache[session_id]
244
+
245
+ # Try loading from disk
246
+ self._load_sessions()
247
+ return self._sessions_cache.get(session_id)
248
+
249
+ def save_session(self, session_info: SessionInfo) -> bool:
250
+ """Persist session state to storage.
251
+
252
+ WHY: Ensures session state is preserved across application restarts.
253
+ """
254
+ try:
255
+ self._sessions_cache[session_info.id] = session_info
256
+ self._save_sessions()
257
+ return True
258
+ except Exception as e:
259
+ self.logger.error(f"Failed to save session {session_info.id}: {e}")
260
+ return False
261
+
262
+ def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
263
+ """Get session metadata.
264
+
265
+ WHY: Provides session details for display and decision-making.
266
+ """
267
+ session = self.load_session(session_id)
268
+ return session.to_dict() if session else None
269
+
270
+ def validate_session(self, session_id: str) -> SessionValidation:
271
+ """Validate session consistency and health.
272
+
273
+ WHY: Ensures session data is consistent and usable before resumption.
274
+ Checks for corruption, missing data, and age constraints.
275
+ """
276
+ validation = SessionValidation(valid=True, session_id=session_id)
277
+
278
+ session = self.load_session(session_id)
279
+ if not session:
280
+ validation.valid = False
281
+ validation.errors.append(f"Session {session_id} not found")
282
+ return validation
283
+
284
+ # Check session age
285
+ try:
286
+ created = datetime.fromisoformat(session.created_at)
287
+ age = datetime.now() - created
288
+
289
+ if age > timedelta(days=7):
290
+ validation.warnings.append(f"Session is {age.days} days old")
291
+
292
+ if age > timedelta(days=30):
293
+ validation.valid = False
294
+ validation.errors.append("Session too old (>30 days)")
295
+ except (ValueError, TypeError) as e:
296
+ validation.errors.append(f"Invalid timestamp: {e}")
297
+ validation.valid = False
298
+
299
+ # Check for required fields
300
+ if not session.context:
301
+ validation.errors.append("Missing session context")
302
+ validation.valid = False
303
+
304
+ # Check session file integrity
305
+ session_file = self.session_dir / "active_sessions.json"
306
+ if not session_file.exists():
307
+ validation.warnings.append("Session file missing, will recreate")
308
+ elif not session_file.stat().st_size:
309
+ validation.errors.append("Session file is empty")
310
+ validation.valid = False
311
+
312
+ return validation
313
+
314
+ def get_recent_sessions(
315
+ self, limit: int = 10, context: Optional[str] = None
316
+ ) -> List[SessionInfo]:
317
+ """Get recent sessions sorted by last used.
318
+
319
+ WHY: Enables users to easily resume recent sessions.
320
+ """
321
+ sessions = list(self._sessions_cache.values())
322
+
323
+ # Filter by context if specified
324
+ if context:
325
+ sessions = [s for s in sessions if s.context == context]
326
+
327
+ # Sort by last_used descending
328
+ sessions.sort(key=lambda s: datetime.fromisoformat(s.last_used), reverse=True)
329
+
330
+ return sessions[:limit]
331
+
332
+ def get_last_interactive_session(self) -> Optional[str]:
333
+ """Get the most recently used interactive session ID.
334
+
335
+ WHY: For --resume without arguments, we want to resume the last
336
+ interactive session (context="default").
337
+ """
338
+ recent = self.get_recent_sessions(limit=1, context="default")
339
+ return recent[0].id if recent else None
340
+
341
+ def record_agent_use(self, session_id: str, agent: str, task: str) -> None:
342
+ """Record agent activity in session.
343
+
344
+ WHY: Tracks which agents were used in a session for context
345
+ preservation and debugging.
346
+ """
347
+ session = self.load_session(session_id)
348
+ if not session:
349
+ self.logger.warning(
350
+ f"Cannot record agent use: session {session_id} not found"
351
+ )
352
+ return
353
+
354
+ session.agents_run.append(
355
+ {
356
+ "agent": agent,
357
+ "task": task[:100], # Truncate long tasks
358
+ "timestamp": datetime.now().isoformat(),
359
+ }
360
+ )
361
+ session.last_used = datetime.now().isoformat()
362
+ session.use_count += 1
363
+
364
+ self.save_session(session)
365
+
366
+ def cleanup_old_sessions(
367
+ self, max_age_hours: int = 24, archive: bool = True
368
+ ) -> int:
369
+ """Remove or archive old sessions.
370
+
371
+ WHY: Prevents unbounded growth of session data and improves performance.
372
+ """
373
+ now = datetime.now()
374
+ max_age = timedelta(hours=max_age_hours)
375
+
376
+ expired_ids = []
377
+ for session_id, session in self._sessions_cache.items():
378
+ try:
379
+ created = datetime.fromisoformat(session.created_at)
380
+ if now - created > max_age:
381
+ expired_ids.append(session_id)
382
+ except (ValueError, TypeError):
383
+ # Invalid timestamp, mark for cleanup
384
+ expired_ids.append(session_id)
385
+
386
+ if archive and expired_ids:
387
+ self.archive_sessions(expired_ids)
388
+
389
+ # Remove from cache
390
+ for session_id in expired_ids:
391
+ del self._sessions_cache[session_id]
392
+ self.logger.info(f"Cleaned up expired session: {session_id}")
393
+
394
+ if expired_ids:
395
+ self._save_sessions()
396
+
397
+ return len(expired_ids)
398
+
399
+ def archive_sessions(self, session_ids: List[str]) -> bool:
400
+ """Archive specific sessions.
401
+
402
+ WHY: Preserves session history while reducing active memory usage.
403
+ """
404
+ if not session_ids:
405
+ return True
406
+
407
+ archive_dir = self.session_dir.parent / "archives" / "sessions"
408
+ archive_dir.mkdir(parents=True, exist_ok=True)
409
+
410
+ # Collect sessions to archive
411
+ sessions_to_archive = []
412
+ for sid in session_ids:
413
+ session = self.load_session(sid)
414
+ if session:
415
+ sessions_to_archive.append(session.to_dict())
416
+
417
+ if not sessions_to_archive:
418
+ return True
419
+
420
+ # Create timestamped archive file
421
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
422
+ archive_name = f"sessions_archive_{timestamp}.json.gz"
423
+ archive_path = archive_dir / archive_name
424
+
425
+ try:
426
+ # Compress and save sessions
427
+ with gzip.open(archive_path, "wt", encoding="utf-8") as f:
428
+ json.dump(sessions_to_archive, f, indent=2)
429
+
430
+ self.logger.info(
431
+ f"Archived {len(sessions_to_archive)} sessions to {archive_path}"
432
+ )
433
+ return True
434
+ except Exception as e:
435
+ self.logger.error(f"Failed to archive sessions: {e}")
436
+ return False
437
+
438
+ def _save_sessions(self) -> None:
439
+ """Save sessions to disk.
440
+
441
+ WHY: Persists session state for recovery after restart.
442
+ """
443
+ session_file = self.session_dir / "active_sessions.json"
444
+ try:
445
+ sessions_dict = {
446
+ sid: session.to_dict() for sid, session in self._sessions_cache.items()
447
+ }
448
+ with open(session_file, "w") as f:
449
+ json.dump(sessions_dict, f, indent=2)
450
+ except Exception as e:
451
+ self.logger.error(f"Failed to save sessions: {e}")
452
+
453
+ def _load_sessions(self) -> None:
454
+ """Load sessions from disk.
455
+
456
+ WHY: Restores session state from persistent storage.
457
+ """
458
+ session_file = self.session_dir / "active_sessions.json"
459
+ if not session_file.exists():
460
+ return
461
+
462
+ try:
463
+ with open(session_file) as f:
464
+ sessions_dict = json.load(f)
465
+
466
+ self._sessions_cache = {
467
+ sid: SessionInfo.from_dict(data) for sid, data in sessions_dict.items()
468
+ }
469
+
470
+ # Clean up old sessions on load
471
+ self.cleanup_old_sessions(archive=True)
472
+ except Exception as e:
473
+ self.logger.error(f"Failed to load sessions: {e}")
474
+ self._sessions_cache = {}
475
+
476
+
477
+ # Context manager for session management
478
+ class ManagedSession:
479
+ """Context manager for session lifecycle.
480
+
481
+ WHY: Provides a clean interface for session management with automatic
482
+ cleanup and error handling.
483
+ """
484
+
485
+ def __init__(
486
+ self,
487
+ manager: ISessionManager,
488
+ context: str = "default",
489
+ options: Optional[Dict[str, Any]] = None,
490
+ ):
491
+ """Initialize managed session.
492
+
493
+ Args:
494
+ manager: Session manager instance
495
+ context: Session context
496
+ options: Optional session configuration
497
+ """
498
+ self.manager = manager
499
+ self.context = context
500
+ self.options = options
501
+ self.session: Optional[SessionInfo] = None
502
+
503
+ def __enter__(self) -> SessionInfo:
504
+ """Enter session context, return session info."""
505
+ self.session = self.manager.create_session(self.context, self.options)
506
+ return self.session
507
+
508
+ def __exit__(self, exc_type, exc_val, exc_tb):
509
+ """Exit session context with cleanup."""
510
+ if self.session:
511
+ # Update last used time
512
+ self.session.last_used = datetime.now().isoformat()
513
+ self.manager.save_session(self.session)