claude-mpm 4.0.23__py3-none-any.whl → 4.0.28__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 (60) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
  4. claude_mpm/agents/BASE_PM.md +3 -0
  5. claude_mpm/agents/templates/code_analyzer.json +2 -2
  6. claude_mpm/cli/commands/agents.py +453 -113
  7. claude_mpm/cli/commands/aggregate.py +107 -15
  8. claude_mpm/cli/commands/cleanup.py +142 -10
  9. claude_mpm/cli/commands/config.py +358 -224
  10. claude_mpm/cli/commands/info.py +184 -75
  11. claude_mpm/cli/commands/mcp_command_router.py +5 -76
  12. claude_mpm/cli/commands/mcp_install_commands.py +68 -36
  13. claude_mpm/cli/commands/mcp_server_commands.py +30 -37
  14. claude_mpm/cli/commands/memory.py +331 -61
  15. claude_mpm/cli/commands/monitor.py +101 -7
  16. claude_mpm/cli/commands/run.py +368 -8
  17. claude_mpm/cli/commands/tickets.py +206 -24
  18. claude_mpm/cli/parsers/mcp_parser.py +3 -0
  19. claude_mpm/cli/shared/__init__.py +40 -0
  20. claude_mpm/cli/shared/argument_patterns.py +212 -0
  21. claude_mpm/cli/shared/command_base.py +234 -0
  22. claude_mpm/cli/shared/error_handling.py +238 -0
  23. claude_mpm/cli/shared/output_formatters.py +231 -0
  24. claude_mpm/config/agent_config.py +29 -8
  25. claude_mpm/core/container.py +6 -4
  26. claude_mpm/core/service_registry.py +4 -2
  27. claude_mpm/core/shared/__init__.py +17 -0
  28. claude_mpm/core/shared/config_loader.py +320 -0
  29. claude_mpm/core/shared/path_resolver.py +277 -0
  30. claude_mpm/core/shared/singleton_manager.py +208 -0
  31. claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
  32. claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
  33. claude_mpm/hooks/memory_integration_hook.py +11 -2
  34. claude_mpm/services/agents/deployment/agent_deployment.py +49 -23
  35. claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
  36. claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
  37. claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
  38. claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
  39. claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
  40. claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
  41. claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
  42. claude_mpm/services/event_aggregator.py +4 -2
  43. claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
  44. claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
  45. claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
  46. claude_mpm/services/memory/builder.py +6 -1
  47. claude_mpm/services/response_tracker.py +3 -1
  48. claude_mpm/services/runner_configuration_service.py +15 -6
  49. claude_mpm/services/shared/__init__.py +20 -0
  50. claude_mpm/services/shared/async_service_base.py +219 -0
  51. claude_mpm/services/shared/config_service_base.py +292 -0
  52. claude_mpm/services/shared/lifecycle_service_base.py +317 -0
  53. claude_mpm/services/shared/manager_base.py +303 -0
  54. claude_mpm/services/shared/service_factory.py +308 -0
  55. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/METADATA +19 -13
  56. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/RECORD +60 -44
  57. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/WHEEL +0 -0
  58. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-4.0.23.dist-info → claude_mpm-4.0.28.dist-info}/top_level.txt +0 -0
@@ -4,10 +4,17 @@ from pathlib import Path
4
4
 
5
5
  WHY: Provides command-line interface for managing the event aggregator service
6
6
  that captures Socket.IO events and saves them as structured session documents.
7
+
8
+ DESIGN DECISIONS:
9
+ - Use BaseCommand for consistent CLI patterns
10
+ - Leverage shared utilities for argument parsing and output formatting
11
+ - Maintain backward compatibility with existing event aggregator integration
12
+ - Support multiple output formats (json, yaml, table, text)
7
13
  """
8
14
 
9
15
  import json
10
16
  import sys
17
+ from typing import Optional
11
18
 
12
19
  from ...core.logger import get_logger
13
20
  from ...models.agent_session import AgentSession
@@ -17,36 +24,121 @@ from ...services.event_aggregator import (
17
24
  start_aggregator,
18
25
  stop_aggregator,
19
26
  )
27
+ from ..shared import BaseCommand, CommandResult
20
28
 
21
29
  logger = get_logger("cli.aggregate")
22
30
 
23
31
 
32
+ class AggregateCommand(BaseCommand):
33
+ """Aggregate command using shared utilities."""
34
+
35
+ def __init__(self):
36
+ super().__init__("aggregate")
37
+
38
+ def validate_args(self, args) -> Optional[str]:
39
+ """Validate command arguments."""
40
+ if not hasattr(args, 'aggregate_subcommand') or not args.aggregate_subcommand:
41
+ return "No aggregate subcommand specified"
42
+
43
+ valid_commands = ["start", "stop", "status", "sessions", "view", "export"]
44
+ if args.aggregate_subcommand not in valid_commands:
45
+ return f"Unknown aggregate command: {args.aggregate_subcommand}. Valid commands: {', '.join(valid_commands)}"
46
+
47
+ return None
48
+
49
+ def run(self, args) -> CommandResult:
50
+ """Execute the aggregate command."""
51
+ try:
52
+ # Route to specific subcommand handlers
53
+ command_map = {
54
+ "start": self._start_command,
55
+ "stop": self._stop_command,
56
+ "status": self._status_command,
57
+ "sessions": self._sessions_command,
58
+ "view": self._view_command,
59
+ "export": self._export_command,
60
+ }
61
+
62
+ if args.aggregate_subcommand in command_map:
63
+ exit_code = command_map[args.aggregate_subcommand](args)
64
+ if exit_code == 0:
65
+ return CommandResult.success_result(f"Aggregate {args.aggregate_subcommand} completed successfully")
66
+ else:
67
+ return CommandResult.error_result(f"Aggregate {args.aggregate_subcommand} failed", exit_code=exit_code)
68
+ else:
69
+ return CommandResult.error_result(f"Unknown aggregate command: {args.aggregate_subcommand}")
70
+
71
+ except Exception as e:
72
+ self.logger.error(f"Error executing aggregate command: {e}", exc_info=True)
73
+ return CommandResult.error_result(f"Error executing aggregate command: {e}")
74
+
75
+ def _start_command(self, args) -> int:
76
+ """Start the event aggregator service."""
77
+ return start_command_legacy(args)
78
+
79
+ def _stop_command(self, args) -> int:
80
+ """Stop the event aggregator service."""
81
+ return stop_command_legacy(args)
82
+
83
+ def _status_command(self, args) -> int:
84
+ """Show status of the event aggregator service."""
85
+ return status_command_legacy(args)
86
+
87
+ def _sessions_command(self, args) -> int:
88
+ """List captured sessions."""
89
+ return sessions_command_legacy(args)
90
+
91
+ def _view_command(self, args) -> int:
92
+ """View details of a specific session."""
93
+ return view_command_legacy(args)
94
+
95
+ def _export_command(self, args) -> int:
96
+ """Export a session to a file."""
97
+ return export_command_legacy(args)
98
+
99
+
24
100
  def aggregate_command(args):
25
- """Main entry point for aggregate commands.
101
+ """
102
+ Main entry point for aggregate command.
103
+
104
+ This function maintains backward compatibility while using the new BaseCommand pattern.
105
+ """
106
+ command = AggregateCommand()
107
+ result = command.execute(args)
108
+
109
+ # Print result if structured output format is requested
110
+ if hasattr(args, 'format') and args.format in ['json', 'yaml']:
111
+ command.print_result(result, args)
112
+
113
+ return result.exit_code
114
+
115
+
116
+ def aggregate_command_legacy(args):
117
+ """Legacy aggregate command dispatcher.
26
118
 
27
- WHY: Routes subcommands to appropriate handlers for managing the
28
- event aggregator service.
119
+ WHY: This contains the original aggregate_command logic, preserved during migration
120
+ to BaseCommand pattern. Will be gradually refactored into the AggregateCommand class.
29
121
  """
30
122
  subcommand = args.aggregate_subcommand
31
123
 
32
124
  if subcommand == "start":
33
- return start_command(args)
125
+ return start_command_legacy(args)
34
126
  elif subcommand == "stop":
35
- return stop_command(args)
127
+ return stop_command_legacy(args)
36
128
  elif subcommand == "status":
37
- return status_command(args)
129
+ return status_command_legacy(args)
38
130
  elif subcommand == "sessions":
39
- return sessions_command(args)
131
+ return sessions_command_legacy(args)
40
132
  elif subcommand == "view":
41
- return view_command(args)
133
+ return view_command_legacy(args)
42
134
  elif subcommand == "export":
43
- return export_command(args)
135
+ return export_command_legacy(args)
44
136
  else:
45
137
  print(f"Unknown subcommand: {subcommand}", file=sys.stderr)
46
138
  return 1
47
139
 
48
140
 
49
- def start_command(args):
141
+ def start_command_legacy(args):
50
142
  """Start the event aggregator service.
51
143
 
52
144
  WHY: Starts capturing events from the Socket.IO dashboard server
@@ -105,7 +197,7 @@ def start_command(args):
105
197
  return 1
106
198
 
107
199
 
108
- def stop_command(args):
200
+ def stop_command_legacy(args):
109
201
  """Stop the event aggregator service.
110
202
 
111
203
  WHY: Gracefully stops the aggregator and saves any active sessions.
@@ -136,7 +228,7 @@ def stop_command(args):
136
228
  return 0
137
229
 
138
230
 
139
- def status_command(args):
231
+ def status_command_legacy(args):
140
232
  """Show status of the event aggregator service.
141
233
 
142
234
  WHY: Provides visibility into what the aggregator is doing and
@@ -169,7 +261,7 @@ def status_command(args):
169
261
  return 0
170
262
 
171
263
 
172
- def sessions_command(args):
264
+ def sessions_command_legacy(args):
173
265
  """List captured sessions.
174
266
 
175
267
  WHY: Shows what sessions have been captured for analysis.
@@ -198,7 +290,7 @@ def sessions_command(args):
198
290
  return 0
199
291
 
200
292
 
201
- def view_command(args):
293
+ def view_command_legacy(args):
202
294
  """View details of a specific session.
203
295
 
204
296
  WHY: Allows detailed inspection of what happened during a session.
@@ -293,7 +385,7 @@ def view_command(args):
293
385
  return 0
294
386
 
295
387
 
296
- def export_command(args):
388
+ def export_command_legacy(args):
297
389
  """Export a session to a file.
298
390
 
299
391
  WHY: Allows sessions to be exported for external analysis or sharing.
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  """
4
2
  Memory cleanup command implementation for claude-mpm.
5
3
 
@@ -8,19 +6,22 @@ Claude Desktop loads the entire conversation history into memory, leading to 2GB
8
6
  consumption. This command helps users manage and clean up their conversation history.
9
7
 
10
8
  DESIGN DECISIONS:
9
+ - Use BaseCommand for consistent CLI patterns
11
10
  - Archive old conversations instead of deleting them
12
11
  - Provide clear feedback about space savings
13
12
  - Default to safe operations with confirmation prompts
14
13
  - Keep recent conversations (30 days by default) in active memory
14
+ - Support multiple output formats (json, yaml, table, text)
15
15
  """
16
16
 
17
17
  import json
18
18
  import shutil
19
19
  import sys
20
20
  from datetime import datetime, timedelta
21
+ from pathlib import Path
21
22
  from typing import Any, Dict, List, Tuple
22
23
 
23
- from ...core.logger import get_logger
24
+ from ..shared import BaseCommand, CommandResult
24
25
 
25
26
 
26
27
  def add_cleanup_parser(subparsers):
@@ -269,16 +270,147 @@ def clean_claude_json(
269
270
  return original_size, original_size
270
271
 
271
272
 
272
- def cleanup_memory(args):
273
- """Clean up Claude conversation history to reduce memory usage.
273
+ class CleanupCommand(BaseCommand):
274
+ """Memory cleanup command using shared utilities."""
274
275
 
275
- WHY: This command addresses the 2GB memory leak issue when using --resume
276
- with large .claude.json files. It provides users with tools to manage
277
- their conversation history and prevent memory issues.
276
+ def __init__(self):
277
+ super().__init__("cleanup")
278
278
 
279
- Args:
280
- args: Parsed command line arguments
279
+ def validate_args(self, args) -> str:
280
+ """Validate command arguments."""
281
+ # Validate max_size format
282
+ max_size = getattr(args, 'max_size', '500KB')
283
+ try:
284
+ parse_size(max_size)
285
+ except ValueError as e:
286
+ return str(e)
287
+
288
+ # Validate days
289
+ days = getattr(args, 'days', 30)
290
+ if days < 0:
291
+ return "Days must be a positive number"
292
+
293
+ return None
294
+
295
+ def run(self, args) -> CommandResult:
296
+ """Execute the cleanup command."""
297
+ try:
298
+ # Gather cleanup information
299
+ cleanup_data = self._analyze_cleanup_needs(args)
300
+
301
+ output_format = getattr(args, 'format', 'text')
302
+
303
+ if output_format in ['json', 'yaml']:
304
+ # Structured output
305
+ if getattr(args, 'dry_run', False):
306
+ return CommandResult.success_result("Cleanup analysis completed (dry run)", data=cleanup_data)
307
+ else:
308
+ # Perform actual cleanup
309
+ result_data = self._perform_cleanup(args, cleanup_data)
310
+ return CommandResult.success_result("Cleanup completed", data=result_data)
311
+ else:
312
+ # Text output using existing function
313
+ cleanup_memory(args)
314
+ return CommandResult.success_result("Cleanup completed")
315
+
316
+ except Exception as e:
317
+ self.logger.error(f"Error during cleanup: {e}", exc_info=True)
318
+ return CommandResult.error_result(f"Error during cleanup: {e}")
319
+
320
+ def _analyze_cleanup_needs(self, args) -> Dict[str, Any]:
321
+ """Analyze what needs to be cleaned up."""
322
+ claude_json = Path.home() / ".claude.json"
323
+ archive_dir = Path.home() / ".claude-mpm" / "archives"
324
+
325
+ if not claude_json.exists():
326
+ return {
327
+ "file_exists": False,
328
+ "file_path": str(claude_json),
329
+ "needs_cleanup": False,
330
+ "message": "No .claude.json file found - nothing to clean up"
331
+ }
332
+
333
+ # Analyze current state
334
+ stats, issues = analyze_claude_json(claude_json)
335
+
336
+ # Check if cleanup is needed
337
+ max_size = parse_size(getattr(args, 'max_size', '500KB'))
338
+ needs_cleanup = stats["file_size"] > max_size
339
+
340
+ return {
341
+ "file_exists": True,
342
+ "file_path": str(claude_json),
343
+ "archive_dir": str(archive_dir),
344
+ "stats": stats,
345
+ "issues": issues,
346
+ "needs_cleanup": needs_cleanup,
347
+ "max_size_bytes": max_size,
348
+ "max_size_formatted": format_size(max_size),
349
+ "current_size_formatted": format_size(stats["file_size"]),
350
+ "settings": {
351
+ "days": getattr(args, 'days', 30),
352
+ "archive": getattr(args, 'archive', True),
353
+ "force": getattr(args, 'force', False),
354
+ "dry_run": getattr(args, 'dry_run', False)
355
+ }
356
+ }
357
+
358
+ def _perform_cleanup(self, args, cleanup_data: Dict[str, Any]) -> Dict[str, Any]:
359
+ """Perform the actual cleanup operation."""
360
+ claude_json = Path(cleanup_data["file_path"])
361
+ archive_dir = Path(cleanup_data["archive_dir"])
362
+
363
+ result = {
364
+ "archive_created": False,
365
+ "archive_path": None,
366
+ "original_size": cleanup_data["stats"]["file_size"],
367
+ "new_size": cleanup_data["stats"]["file_size"],
368
+ "savings": 0,
369
+ "old_archives_removed": 0
370
+ }
371
+
372
+ # Create archive if requested
373
+ if cleanup_data["settings"]["archive"] and not cleanup_data["settings"]["dry_run"]:
374
+ try:
375
+ archive_path = create_archive(claude_json, archive_dir)
376
+ result["archive_created"] = True
377
+ result["archive_path"] = str(archive_path)
378
+ except Exception as e:
379
+ raise Exception(f"Failed to create archive: {e}")
380
+
381
+ # Perform cleanup
382
+ original_size, new_size = clean_claude_json(
383
+ claude_json,
384
+ keep_days=cleanup_data["settings"]["days"],
385
+ dry_run=cleanup_data["settings"]["dry_run"]
386
+ )
387
+
388
+ result["original_size"] = original_size
389
+ result["new_size"] = new_size
390
+ result["savings"] = original_size - new_size
391
+
392
+ # Clean up old archives
393
+ if cleanup_data["settings"]["archive"] and not cleanup_data["settings"]["dry_run"]:
394
+ old_archives = clean_old_archives(archive_dir, keep_days=90)
395
+ result["old_archives_removed"] = len(old_archives)
396
+
397
+ return result
398
+
399
+
400
+ def cleanup_memory(args):
401
+ """
402
+ Main entry point for cleanup command.
403
+
404
+ This function maintains backward compatibility while using the new BaseCommand pattern.
281
405
  """
406
+ # For complex interactive commands like this, we'll delegate to the original implementation
407
+ # but could be refactored to use the new pattern in the future
408
+ _cleanup_memory_original(args)
409
+
410
+
411
+ def _cleanup_memory_original(args):
412
+ """Original cleanup implementation for backward compatibility."""
413
+ from ...core.logger import get_logger
282
414
  logger = get_logger("cleanup")
283
415
 
284
416
  # File paths