claude-mpm 3.5.6__py3-none-any.whl → 3.6.2__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 (46) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +96 -23
  3. claude_mpm/agents/BASE_PM.md +273 -0
  4. claude_mpm/agents/INSTRUCTIONS.md +114 -103
  5. claude_mpm/agents/agent_loader.py +36 -1
  6. claude_mpm/agents/async_agent_loader.py +421 -0
  7. claude_mpm/agents/templates/code_analyzer.json +81 -0
  8. claude_mpm/agents/templates/data_engineer.json +18 -3
  9. claude_mpm/agents/templates/documentation.json +18 -3
  10. claude_mpm/agents/templates/engineer.json +19 -4
  11. claude_mpm/agents/templates/ops.json +18 -3
  12. claude_mpm/agents/templates/qa.json +20 -4
  13. claude_mpm/agents/templates/research.json +20 -4
  14. claude_mpm/agents/templates/security.json +18 -3
  15. claude_mpm/agents/templates/version_control.json +16 -3
  16. claude_mpm/cli/__init__.py +5 -1
  17. claude_mpm/cli/commands/__init__.py +5 -1
  18. claude_mpm/cli/commands/agents.py +212 -3
  19. claude_mpm/cli/commands/aggregate.py +462 -0
  20. claude_mpm/cli/commands/config.py +277 -0
  21. claude_mpm/cli/commands/run.py +224 -36
  22. claude_mpm/cli/parser.py +176 -1
  23. claude_mpm/constants.py +19 -0
  24. claude_mpm/core/claude_runner.py +320 -44
  25. claude_mpm/core/config.py +161 -4
  26. claude_mpm/core/framework_loader.py +81 -0
  27. claude_mpm/hooks/claude_hooks/hook_handler.py +391 -9
  28. claude_mpm/init.py +40 -5
  29. claude_mpm/models/agent_session.py +511 -0
  30. claude_mpm/scripts/__init__.py +15 -0
  31. claude_mpm/scripts/start_activity_logging.py +86 -0
  32. claude_mpm/services/agents/deployment/agent_deployment.py +165 -19
  33. claude_mpm/services/agents/deployment/async_agent_deployment.py +461 -0
  34. claude_mpm/services/event_aggregator.py +547 -0
  35. claude_mpm/utils/agent_dependency_loader.py +655 -0
  36. claude_mpm/utils/console.py +11 -0
  37. claude_mpm/utils/dependency_cache.py +376 -0
  38. claude_mpm/utils/dependency_strategies.py +343 -0
  39. claude_mpm/utils/environment_context.py +310 -0
  40. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/METADATA +47 -3
  41. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/RECORD +45 -31
  42. claude_mpm/agents/templates/pm.json +0 -122
  43. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/WHEEL +0 -0
  44. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/entry_points.txt +0 -0
  45. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/licenses/LICENSE +0 -0
  46. {claude_mpm-3.5.6.dist-info → claude_mpm-3.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,462 @@
1
+ """CLI commands for the Event Aggregator service.
2
+
3
+ WHY: Provides command-line interface for managing the event aggregator service
4
+ that captures Socket.IO events and saves them as structured session documents.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from ...services.event_aggregator import (
13
+ get_aggregator,
14
+ start_aggregator,
15
+ stop_aggregator,
16
+ aggregator_status
17
+ )
18
+ from ...models.agent_session import AgentSession
19
+ from ...core.logger import get_logger
20
+
21
+
22
+ logger = get_logger("cli.aggregate")
23
+
24
+
25
+ def aggregate_command(args):
26
+ """Main entry point for aggregate commands.
27
+
28
+ WHY: Routes subcommands to appropriate handlers for managing the
29
+ event aggregator service.
30
+ """
31
+ subcommand = args.aggregate_subcommand
32
+
33
+ if subcommand == 'start':
34
+ return start_command(args)
35
+ elif subcommand == 'stop':
36
+ return stop_command(args)
37
+ elif subcommand == 'status':
38
+ return status_command(args)
39
+ elif subcommand == 'sessions':
40
+ return sessions_command(args)
41
+ elif subcommand == 'view':
42
+ return view_command(args)
43
+ elif subcommand == 'export':
44
+ return export_command(args)
45
+ else:
46
+ print(f"Unknown subcommand: {subcommand}", file=sys.stderr)
47
+ return 1
48
+
49
+
50
+ def start_command(args):
51
+ """Start the event aggregator service.
52
+
53
+ WHY: Starts capturing events from the Socket.IO dashboard server
54
+ for building complete session documents.
55
+ """
56
+ print("Starting Event Aggregator service...")
57
+
58
+ # Check if Socket.IO server is running
59
+ import socket
60
+ try:
61
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
62
+ s.settimeout(1)
63
+ result = s.connect_ex(('127.0.0.1', 8765))
64
+ if result != 0:
65
+ print("Warning: Socket.IO server not detected on port 8765")
66
+ print("The aggregator requires the dashboard server to be running.")
67
+ print("Start it with: claude-mpm monitor")
68
+ if not args.force:
69
+ return 1
70
+ except Exception as e:
71
+ logger.error(f"Error checking server status: {e}")
72
+
73
+ # Start the aggregator
74
+ if start_aggregator():
75
+ print("✅ Event Aggregator started successfully")
76
+ print(f"Capturing events from localhost:8765")
77
+ print(f"Sessions will be saved to: .claude-mpm/sessions/")
78
+
79
+ # Show initial status
80
+ status = aggregator_status()
81
+ print(f"\nStatus:")
82
+ print(f" Connected: {status['connected']}")
83
+ print(f" Active sessions: {status['active_sessions']}")
84
+
85
+ if args.daemon:
86
+ print("\nAggregator running in background. Use 'claude-mpm aggregate stop' to stop it.")
87
+ else:
88
+ print("\nPress Ctrl+C to stop the aggregator...")
89
+ try:
90
+ # Keep running until interrupted
91
+ import time
92
+ while True:
93
+ time.sleep(1)
94
+ except KeyboardInterrupt:
95
+ print("\nStopping aggregator...")
96
+ stop_aggregator()
97
+
98
+ return 0
99
+ else:
100
+ print("❌ Failed to start Event Aggregator")
101
+ print("Check that python-socketio is installed: pip install python-socketio")
102
+ return 1
103
+
104
+
105
+ def stop_command(args):
106
+ """Stop the event aggregator service.
107
+
108
+ WHY: Gracefully stops the aggregator and saves any active sessions.
109
+ """
110
+ print("Stopping Event Aggregator service...")
111
+
112
+ # Get status before stopping
113
+ status = aggregator_status()
114
+ active_sessions = status.get('active_sessions', 0)
115
+
116
+ if active_sessions > 0:
117
+ print(f"Saving {active_sessions} active session(s)...")
118
+
119
+ stop_aggregator()
120
+ print("✅ Event Aggregator stopped")
121
+
122
+ # Show final statistics
123
+ if status['total_events'] > 0:
124
+ print(f"\nStatistics:")
125
+ print(f" Total events captured: {status['total_events']}")
126
+ print(f" Sessions completed: {status['sessions_completed']}")
127
+ print(f" Events by type:")
128
+ for event_type, count in sorted(status['events_by_type'].items(),
129
+ key=lambda x: x[1], reverse=True)[:5]:
130
+ print(f" {event_type}: {count}")
131
+
132
+ return 0
133
+
134
+
135
+ def status_command(args):
136
+ """Show status of the event aggregator service.
137
+
138
+ WHY: Provides visibility into what the aggregator is doing and
139
+ what it has captured.
140
+ """
141
+ status = aggregator_status()
142
+
143
+ print("Event Aggregator Status")
144
+ print("=" * 50)
145
+ print(f"Running: {status['running']}")
146
+ print(f"Connected: {status['connected']}")
147
+ print(f"Server: {status['server']}")
148
+ print(f"Save directory: {status['save_directory']}")
149
+ print()
150
+ print(f"Active sessions: {status['active_sessions']}")
151
+ if status['active_session_ids']:
152
+ for sid in status['active_session_ids']:
153
+ print(f" - {sid}")
154
+ print()
155
+ print(f"Sessions completed: {status['sessions_completed']}")
156
+ print(f"Total events captured: {status['total_events']}")
157
+
158
+ if status['events_by_type']:
159
+ print("\nTop event types:")
160
+ for event_type, count in sorted(status['events_by_type'].items(),
161
+ key=lambda x: x[1], reverse=True)[:10]:
162
+ print(f" {event_type:30s} {count:6d}")
163
+
164
+ return 0
165
+
166
+
167
+ def sessions_command(args):
168
+ """List captured sessions.
169
+
170
+ WHY: Shows what sessions have been captured for analysis.
171
+ """
172
+ aggregator = get_aggregator()
173
+ sessions = aggregator.list_sessions(limit=args.limit)
174
+
175
+ if not sessions:
176
+ print("No sessions found")
177
+ return 0
178
+
179
+ print(f"Recent Sessions (showing {len(sessions)} of {args.limit} max)")
180
+ print("=" * 80)
181
+
182
+ for session in sessions:
183
+ print(f"\n📁 {session['file']}")
184
+ print(f" Session ID: {session['session_id']}")
185
+ print(f" Start: {session['start_time']}")
186
+ print(f" End: {session['end_time']}")
187
+ print(f" Events: {session['events']}")
188
+ print(f" Delegations: {session['delegations']}")
189
+ print(f" Prompt: {session['initial_prompt']}")
190
+
191
+ print(f"\nUse 'claude-mpm aggregate view <session_id>' to view details")
192
+
193
+ return 0
194
+
195
+
196
+ def view_command(args):
197
+ """View details of a specific session.
198
+
199
+ WHY: Allows detailed inspection of what happened during a session.
200
+ """
201
+ aggregator = get_aggregator()
202
+
203
+ # Load the session
204
+ session = aggregator.load_session(args.session_id)
205
+
206
+ if not session:
207
+ print(f"Session not found: {args.session_id}")
208
+ print("Use 'claude-mpm aggregate sessions' to list available sessions")
209
+ return 1
210
+
211
+ print(f"Session: {session.session_id}")
212
+ print("=" * 80)
213
+ print(f"Start: {session.start_time}")
214
+ print(f"End: {session.end_time or 'In progress'}")
215
+ print(f"Working directory: {session.working_directory}")
216
+ print(f"Launch method: {session.launch_method}")
217
+
218
+ if session.git_branch:
219
+ print(f"Git branch: {session.git_branch}")
220
+
221
+ print(f"\nInitial prompt:")
222
+ print("-" * 40)
223
+ if session.initial_prompt:
224
+ print(session.initial_prompt[:500])
225
+ if len(session.initial_prompt) > 500:
226
+ print("...")
227
+ else:
228
+ print("(No prompt captured)")
229
+
230
+ print(f"\nMetrics:")
231
+ print("-" * 40)
232
+ metrics = session.metrics
233
+ print(f"Total events: {metrics.total_events}")
234
+ print(f"Delegations: {metrics.total_delegations}")
235
+ print(f"Tool calls: {metrics.total_tool_calls}")
236
+ print(f"File operations: {metrics.total_file_operations}")
237
+
238
+ if metrics.session_duration_ms:
239
+ duration_sec = metrics.session_duration_ms / 1000
240
+ print(f"Duration: {duration_sec:.1f} seconds")
241
+
242
+ if metrics.agents_used:
243
+ print(f"\nAgents used: {', '.join(sorted(metrics.agents_used))}")
244
+
245
+ if metrics.tools_used:
246
+ print(f"\nTools used: {', '.join(sorted(metrics.tools_used))}")
247
+
248
+ if metrics.files_modified:
249
+ print(f"\nFiles modified ({len(metrics.files_modified)}):")
250
+ for filepath in sorted(metrics.files_modified)[:10]:
251
+ print(f" - {filepath}")
252
+ if len(metrics.files_modified) > 10:
253
+ print(f" ... and {len(metrics.files_modified) - 10} more")
254
+
255
+ if session.delegations:
256
+ print(f"\nDelegations ({len(session.delegations)}):")
257
+ print("-" * 40)
258
+ for i, delegation in enumerate(session.delegations, 1):
259
+ print(f"\n{i}. {delegation.agent_type}")
260
+ print(f" Task: {delegation.task_description[:100]}")
261
+ if delegation.prompt:
262
+ print(f" Prompt: {delegation.prompt[:100]}...")
263
+ print(f" Tools: {len(delegation.tool_operations)}")
264
+ print(f" Files: {len(delegation.file_changes)}")
265
+ if delegation.duration_ms:
266
+ print(f" Duration: {delegation.duration_ms/1000:.1f}s")
267
+ print(f" Status: {'✅ Success' if delegation.success else '❌ Failed'}")
268
+ if delegation.error:
269
+ print(f" Error: {delegation.error}")
270
+
271
+ if args.show_events:
272
+ print(f"\nEvents ({len(session.events)}):")
273
+ print("-" * 40)
274
+ for event in session.events[:args.event_limit]:
275
+ print(f"{event.timestamp} [{event.category.value:10s}] {event.event_type}")
276
+ if args.verbose:
277
+ print(f" Agent: {event.agent_context or 'N/A'}")
278
+ if event.correlation_id:
279
+ print(f" Correlation: {event.correlation_id}")
280
+
281
+ if session.final_response and not args.no_response:
282
+ print(f"\nFinal response:")
283
+ print("-" * 40)
284
+ print(session.final_response[:1000])
285
+ if len(session.final_response) > 1000:
286
+ print("...")
287
+
288
+ return 0
289
+
290
+
291
+ def export_command(args):
292
+ """Export a session to a file.
293
+
294
+ WHY: Allows sessions to be exported for external analysis or sharing.
295
+ """
296
+ aggregator = get_aggregator()
297
+
298
+ # Load the session
299
+ session = aggregator.load_session(args.session_id)
300
+
301
+ if not session:
302
+ print(f"Session not found: {args.session_id}")
303
+ return 1
304
+
305
+ # Determine output file
306
+ if args.output:
307
+ output_path = Path(args.output)
308
+ else:
309
+ output_path = Path(f"session_{session.session_id[:8]}_export.json")
310
+
311
+ # Export based on format
312
+ if args.format == 'json':
313
+ # Full JSON export
314
+ with open(output_path, 'w') as f:
315
+ json.dump(session.to_dict(), f, indent=2)
316
+ print(f"✅ Exported session to {output_path}")
317
+
318
+ elif args.format == 'summary':
319
+ # Summary export
320
+ summary = {
321
+ 'session_id': session.session_id,
322
+ 'start_time': session.start_time,
323
+ 'end_time': session.end_time,
324
+ 'working_directory': session.working_directory,
325
+ 'initial_prompt': session.initial_prompt,
326
+ 'final_response': session.final_response,
327
+ 'metrics': session.metrics.to_dict(),
328
+ 'delegations_summary': [
329
+ {
330
+ 'agent': d.agent_type,
331
+ 'task': d.task_description,
332
+ 'duration_ms': d.duration_ms,
333
+ 'success': d.success,
334
+ 'tools_used': len(d.tool_operations),
335
+ 'files_changed': len(d.file_changes)
336
+ }
337
+ for d in session.delegations
338
+ ]
339
+ }
340
+
341
+ with open(output_path, 'w') as f:
342
+ json.dump(summary, f, indent=2)
343
+ print(f"✅ Exported session summary to {output_path}")
344
+
345
+ elif args.format == 'events':
346
+ # Events-only export
347
+ events_data = [e.to_dict() for e in session.events]
348
+
349
+ with open(output_path, 'w') as f:
350
+ json.dump(events_data, f, indent=2)
351
+ print(f"✅ Exported {len(events_data)} events to {output_path}")
352
+
353
+ return 0
354
+
355
+
356
+ def add_aggregate_parser(subparsers):
357
+ """Add the aggregate command parser.
358
+
359
+ WHY: Integrates the aggregator commands into the main CLI system.
360
+ """
361
+ aggregate_parser = subparsers.add_parser(
362
+ 'aggregate',
363
+ help='Manage event aggregator for capturing agent sessions'
364
+ )
365
+
366
+ aggregate_subparsers = aggregate_parser.add_subparsers(
367
+ dest='aggregate_subcommand',
368
+ help='Aggregator subcommands'
369
+ )
370
+
371
+ # Start command
372
+ start_parser = aggregate_subparsers.add_parser(
373
+ 'start',
374
+ help='Start the event aggregator service'
375
+ )
376
+ start_parser.add_argument(
377
+ '--daemon', '-d',
378
+ action='store_true',
379
+ help='Run in background as daemon'
380
+ )
381
+ start_parser.add_argument(
382
+ '--force', '-f',
383
+ action='store_true',
384
+ help='Start even if Socket.IO server not detected'
385
+ )
386
+
387
+ # Stop command
388
+ aggregate_subparsers.add_parser(
389
+ 'stop',
390
+ help='Stop the event aggregator service'
391
+ )
392
+
393
+ # Status command
394
+ aggregate_subparsers.add_parser(
395
+ 'status',
396
+ help='Show aggregator status and statistics'
397
+ )
398
+
399
+ # Sessions command
400
+ sessions_parser = aggregate_subparsers.add_parser(
401
+ 'sessions',
402
+ help='List captured sessions'
403
+ )
404
+ sessions_parser.add_argument(
405
+ '--limit', '-l',
406
+ type=int,
407
+ default=10,
408
+ help='Maximum number of sessions to show (default: 10)'
409
+ )
410
+
411
+ # View command
412
+ view_parser = aggregate_subparsers.add_parser(
413
+ 'view',
414
+ help='View details of a specific session'
415
+ )
416
+ view_parser.add_argument(
417
+ 'session_id',
418
+ help='Session ID or prefix to view'
419
+ )
420
+ view_parser.add_argument(
421
+ '--show-events', '-e',
422
+ action='store_true',
423
+ help='Show event list'
424
+ )
425
+ view_parser.add_argument(
426
+ '--event-limit',
427
+ type=int,
428
+ default=50,
429
+ help='Maximum events to show (default: 50)'
430
+ )
431
+ view_parser.add_argument(
432
+ '--verbose', '-v',
433
+ action='store_true',
434
+ help='Show verbose event details'
435
+ )
436
+ view_parser.add_argument(
437
+ '--no-response',
438
+ action='store_true',
439
+ help='Don\'t show final response'
440
+ )
441
+
442
+ # Export command
443
+ export_parser = aggregate_subparsers.add_parser(
444
+ 'export',
445
+ help='Export a session to file'
446
+ )
447
+ export_parser.add_argument(
448
+ 'session_id',
449
+ help='Session ID or prefix to export'
450
+ )
451
+ export_parser.add_argument(
452
+ '--output', '-o',
453
+ help='Output file path'
454
+ )
455
+ export_parser.add_argument(
456
+ '--format', '-f',
457
+ choices=['json', 'summary', 'events'],
458
+ default='json',
459
+ help='Export format (default: json)'
460
+ )
461
+
462
+ aggregate_parser.set_defaults(func=aggregate_command)