claude-mpm 4.5.6__py3-none-any.whl → 4.5.11__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 (62) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/__init__.py +20 -5
  3. claude_mpm/agents/BASE_OPS.md +10 -0
  4. claude_mpm/agents/PM_INSTRUCTIONS.md +28 -4
  5. claude_mpm/agents/agent_loader.py +19 -2
  6. claude_mpm/agents/base_agent_loader.py +5 -5
  7. claude_mpm/agents/templates/agent-manager.json +3 -3
  8. claude_mpm/agents/templates/agentic-coder-optimizer.json +3 -3
  9. claude_mpm/agents/templates/api_qa.json +1 -1
  10. claude_mpm/agents/templates/clerk-ops.json +3 -3
  11. claude_mpm/agents/templates/code_analyzer.json +3 -3
  12. claude_mpm/agents/templates/dart_engineer.json +294 -0
  13. claude_mpm/agents/templates/data_engineer.json +3 -3
  14. claude_mpm/agents/templates/documentation.json +2 -2
  15. claude_mpm/agents/templates/engineer.json +2 -2
  16. claude_mpm/agents/templates/gcp_ops_agent.json +2 -2
  17. claude_mpm/agents/templates/imagemagick.json +1 -1
  18. claude_mpm/agents/templates/local_ops_agent.json +363 -49
  19. claude_mpm/agents/templates/memory_manager.json +2 -2
  20. claude_mpm/agents/templates/nextjs_engineer.json +2 -2
  21. claude_mpm/agents/templates/ops.json +2 -2
  22. claude_mpm/agents/templates/php-engineer.json +1 -1
  23. claude_mpm/agents/templates/project_organizer.json +1 -1
  24. claude_mpm/agents/templates/prompt-engineer.json +6 -4
  25. claude_mpm/agents/templates/python_engineer.json +2 -2
  26. claude_mpm/agents/templates/qa.json +1 -1
  27. claude_mpm/agents/templates/react_engineer.json +3 -3
  28. claude_mpm/agents/templates/refactoring_engineer.json +3 -3
  29. claude_mpm/agents/templates/research.json +2 -2
  30. claude_mpm/agents/templates/security.json +2 -2
  31. claude_mpm/agents/templates/ticketing.json +2 -2
  32. claude_mpm/agents/templates/typescript_engineer.json +2 -2
  33. claude_mpm/agents/templates/vercel_ops_agent.json +2 -2
  34. claude_mpm/agents/templates/version_control.json +2 -2
  35. claude_mpm/agents/templates/web_qa.json +6 -6
  36. claude_mpm/agents/templates/web_ui.json +3 -3
  37. claude_mpm/cli/__init__.py +49 -19
  38. claude_mpm/cli/commands/configure.py +591 -7
  39. claude_mpm/cli/parsers/configure_parser.py +5 -0
  40. claude_mpm/core/__init__.py +53 -17
  41. claude_mpm/core/config.py +1 -1
  42. claude_mpm/core/log_manager.py +7 -0
  43. claude_mpm/hooks/claude_hooks/response_tracking.py +16 -11
  44. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +9 -11
  45. claude_mpm/services/__init__.py +140 -156
  46. claude_mpm/services/agents/deployment/deployment_config_loader.py +21 -0
  47. claude_mpm/services/agents/loading/base_agent_manager.py +12 -2
  48. claude_mpm/services/async_session_logger.py +112 -96
  49. claude_mpm/services/claude_session_logger.py +63 -61
  50. claude_mpm/services/mcp_config_manager.py +328 -38
  51. claude_mpm/services/mcp_gateway/__init__.py +98 -94
  52. claude_mpm/services/monitor/event_emitter.py +1 -1
  53. claude_mpm/services/orphan_detection.py +791 -0
  54. claude_mpm/services/project_port_allocator.py +601 -0
  55. claude_mpm/services/response_tracker.py +17 -6
  56. claude_mpm/services/session_manager.py +176 -0
  57. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/METADATA +1 -1
  58. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/RECORD +62 -58
  59. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/WHEEL +0 -0
  60. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/entry_points.txt +0 -0
  61. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/licenses/LICENSE +0 -0
  62. {claude_mpm-4.5.6.dist-info → claude_mpm-4.5.11.dist-info}/top_level.txt +0 -0
@@ -26,6 +26,8 @@ from rich.syntax import Syntax
26
26
  from rich.table import Table
27
27
  from rich.text import Text
28
28
 
29
+ from ...core.config import Config
30
+ from ...services.mcp_config_manager import MCPConfigManager
29
31
  from ...services.version_service import VersionService
30
32
  from ...utils.console import console as default_console
31
33
  from ..shared import BaseCommand, CommandResult
@@ -54,6 +56,10 @@ class SimpleAgentManager:
54
56
  self.templates_dir = (
55
57
  Path(__file__).parent.parent.parent / "agents" / "templates"
56
58
  )
59
+ # Add logger for error reporting
60
+ import logging
61
+
62
+ self.logger = logging.getLogger(__name__)
57
63
 
58
64
  def _load_states(self):
59
65
  """Load agent states from file."""
@@ -115,6 +121,9 @@ class SimpleAgentManager:
115
121
  # Extract capabilities/tools as dependencies for display
116
122
  capabilities = template_data.get("capabilities", {})
117
123
  tools = capabilities.get("tools", [])
124
+ # Ensure tools is a list before slicing
125
+ if not isinstance(tools, list):
126
+ tools = []
118
127
  # Show first few tools as "dependencies" for UI purposes
119
128
  display_tools = tools[:3] if len(tools) > 3 else tools
120
129
 
@@ -133,14 +142,24 @@ class SimpleAgentManager:
133
142
  )
134
143
  )
135
144
 
136
- except (json.JSONDecodeError, KeyError):
137
- # Skip malformed templates
145
+ except (json.JSONDecodeError, KeyError) as e:
146
+ # Log malformed templates but continue
147
+ self.logger.debug(
148
+ f"Skipping malformed template {template_file.name}: {e}"
149
+ )
150
+ continue
151
+ except Exception as e:
152
+ # Log unexpected errors but continue processing other templates
153
+ self.logger.debug(
154
+ f"Error processing template {template_file.name}: {e}"
155
+ )
138
156
  continue
139
157
 
140
158
  except Exception as e:
141
- # If there's an error reading templates, return a minimal set
159
+ # If there's a catastrophic error reading templates directory
160
+ self.logger.error(f"Failed to read templates directory: {e}")
142
161
  return [
143
- AgentConfig("engineer", f"Error loading templates: {e!s}", []),
162
+ AgentConfig("engineer", f"Error accessing templates: {e!s}", []),
144
163
  AgentConfig("research", "Research agent", []),
145
164
  ]
146
165
 
@@ -174,6 +193,7 @@ class ConfigureCommand(BaseCommand):
174
193
  getattr(args, "agents", False),
175
194
  getattr(args, "templates", False),
176
195
  getattr(args, "behaviors", False),
196
+ getattr(args, "startup", False),
177
197
  getattr(args, "version_info", False),
178
198
  ]
179
199
  if sum(nav_options) > 1:
@@ -242,6 +262,9 @@ class ConfigureCommand(BaseCommand):
242
262
  if getattr(args, "behaviors", False):
243
263
  return self._run_behavior_management()
244
264
 
265
+ if getattr(args, "startup", False):
266
+ return self._run_startup_configuration()
267
+
245
268
  # Launch interactive TUI
246
269
  return self._run_interactive_tui(args)
247
270
 
@@ -263,8 +286,15 @@ class ConfigureCommand(BaseCommand):
263
286
  elif choice == "3":
264
287
  self._manage_behaviors()
265
288
  elif choice == "4":
266
- self._switch_scope()
289
+ # If user saves and wants to proceed to startup, exit the configurator
290
+ if self._manage_startup_configuration():
291
+ self.console.print(
292
+ "\n[green]Configuration saved. Exiting configurator...[/green]"
293
+ )
294
+ break
267
295
  elif choice == "5":
296
+ self._switch_scope()
297
+ elif choice == "6":
268
298
  self._show_version_info_interactive()
269
299
  elif choice == "q":
270
300
  self.console.print(
@@ -287,10 +317,14 @@ class ConfigureCommand(BaseCommand):
287
317
  """Display the TUI header."""
288
318
  self.console.clear()
289
319
 
320
+ # Get version for display
321
+ from claude_mpm import __version__
322
+
290
323
  # Create header panel
291
324
  header_text = Text()
292
325
  header_text.append("Claude MPM ", style="bold cyan")
293
326
  header_text.append("Configuration Interface", style="bold white")
327
+ header_text.append(f"\nv{__version__}", style="dim cyan")
294
328
 
295
329
  scope_text = Text(f"Scope: {self.current_scope.upper()}", style="yellow")
296
330
  dir_text = Text(f"Directory: {self.project_dir}", style="dim")
@@ -315,8 +349,13 @@ class ConfigureCommand(BaseCommand):
315
349
  ("1", "Agent Management", "Enable/disable agents and customize settings"),
316
350
  ("2", "Template Editing", "Edit agent JSON templates"),
317
351
  ("3", "Behavior Files", "Manage identity and workflow configurations"),
318
- ("4", "Switch Scope", f"Current: {self.current_scope}"),
319
- ("5", "Version Info", "Display MPM and Claude versions"),
352
+ (
353
+ "4",
354
+ "Startup Configuration",
355
+ "Configure MCP services and agents to start",
356
+ ),
357
+ ("5", "Switch Scope", f"Current: {self.current_scope}"),
358
+ ("6", "Version Info", "Display MPM and Claude versions"),
320
359
  ("q", "Quit", "Exit configuration interface"),
321
360
  ]
322
361
 
@@ -979,6 +1018,537 @@ class ConfigureCommand(BaseCommand):
979
1018
  self.console.print("[yellow]Behavior file export - Coming soon![/yellow]")
980
1019
  Prompt.ask("Press Enter to continue")
981
1020
 
1021
+ def _manage_startup_configuration(self) -> bool:
1022
+ """Manage startup configuration for MCP services and agents.
1023
+
1024
+ Returns:
1025
+ bool: True if user saved and wants to proceed to startup, False otherwise
1026
+ """
1027
+ # Temporarily suppress INFO logging during Config initialization
1028
+ import logging
1029
+
1030
+ root_logger = logging.getLogger("claude_mpm")
1031
+ original_level = root_logger.level
1032
+ root_logger.setLevel(logging.WARNING)
1033
+
1034
+ try:
1035
+ # Load current configuration ONCE at the start
1036
+ config = Config()
1037
+ startup_config = self._load_startup_configuration(config)
1038
+ finally:
1039
+ # Restore original logging level
1040
+ root_logger.setLevel(original_level)
1041
+
1042
+ proceed_to_startup = False
1043
+ while True:
1044
+ self.console.clear()
1045
+ self._display_header()
1046
+
1047
+ self.console.print("[bold]Startup Configuration Management[/bold]\n")
1048
+ self.console.print(
1049
+ "[dim]Configure which MCP services, hook services, and system agents "
1050
+ "are enabled when Claude MPM starts.[/dim]\n"
1051
+ )
1052
+
1053
+ # Display current configuration (using in-memory state)
1054
+ self._display_startup_configuration(startup_config)
1055
+
1056
+ # Show menu options
1057
+ self.console.print("\n[bold]Options:[/bold]")
1058
+ self.console.print(" [cyan]1[/cyan] - Configure MCP Services")
1059
+ self.console.print(" [cyan]2[/cyan] - Configure Hook Services")
1060
+ self.console.print(" [cyan]3[/cyan] - Configure System Agents")
1061
+ self.console.print(" [cyan]4[/cyan] - Enable All")
1062
+ self.console.print(" [cyan]5[/cyan] - Disable All")
1063
+ self.console.print(" [cyan]6[/cyan] - Reset to Defaults")
1064
+ self.console.print(
1065
+ " [cyan]s[/cyan] - Save configuration and start claude-mpm"
1066
+ )
1067
+ self.console.print(" [cyan]b[/cyan] - Cancel and return without saving")
1068
+ self.console.print()
1069
+
1070
+ choice = Prompt.ask("[bold cyan]Select an option[/bold cyan]", default="s")
1071
+
1072
+ if choice == "b":
1073
+ break
1074
+ if choice == "1":
1075
+ self._configure_mcp_services(startup_config, config)
1076
+ elif choice == "2":
1077
+ self._configure_hook_services(startup_config, config)
1078
+ elif choice == "3":
1079
+ self._configure_system_agents(startup_config, config)
1080
+ elif choice == "4":
1081
+ self._enable_all_services(startup_config, config)
1082
+ elif choice == "5":
1083
+ self._disable_all_services(startup_config, config)
1084
+ elif choice == "6":
1085
+ self._reset_to_defaults(startup_config, config)
1086
+ elif choice == "s":
1087
+ # Save and exit if successful
1088
+ if self._save_startup_configuration(startup_config, config):
1089
+ proceed_to_startup = True
1090
+ break
1091
+ else:
1092
+ self.console.print("[red]Invalid choice.[/red]")
1093
+ Prompt.ask("Press Enter to continue")
1094
+
1095
+ return proceed_to_startup
1096
+
1097
+ def _load_startup_configuration(self, config: Config) -> Dict:
1098
+ """Load current startup configuration from config."""
1099
+ startup_config = config.get("startup", {})
1100
+
1101
+ # Ensure all required sections exist
1102
+ if "enabled_mcp_services" not in startup_config:
1103
+ # Get available MCP services from MCPConfigManager
1104
+ mcp_manager = MCPConfigManager()
1105
+ available_services = list(mcp_manager.STATIC_MCP_CONFIGS.keys())
1106
+ startup_config["enabled_mcp_services"] = available_services.copy()
1107
+
1108
+ if "enabled_hook_services" not in startup_config:
1109
+ # Default hook services (health-monitor enabled by default)
1110
+ startup_config["enabled_hook_services"] = [
1111
+ "monitor",
1112
+ "dashboard",
1113
+ "response-logger",
1114
+ "health-monitor",
1115
+ ]
1116
+
1117
+ if "disabled_agents" not in startup_config:
1118
+ # NEW LOGIC: Track DISABLED agents instead of enabled
1119
+ # By default, NO agents are disabled (all agents enabled)
1120
+ startup_config["disabled_agents"] = []
1121
+
1122
+ return startup_config
1123
+
1124
+ def _display_startup_configuration(self, startup_config: Dict) -> None:
1125
+ """Display current startup configuration in a table."""
1126
+ table = Table(
1127
+ title="Current Startup Configuration", box=ROUNDED, show_lines=True
1128
+ )
1129
+
1130
+ table.add_column("Category", style="cyan", width=20)
1131
+ table.add_column("Enabled Services", style="white", width=50)
1132
+ table.add_column("Count", style="dim", width=10)
1133
+
1134
+ # MCP Services
1135
+ mcp_services = startup_config.get("enabled_mcp_services", [])
1136
+ mcp_display = ", ".join(mcp_services[:3]) + (
1137
+ "..." if len(mcp_services) > 3 else ""
1138
+ )
1139
+ table.add_row(
1140
+ "MCP Services",
1141
+ mcp_display if mcp_services else "[dim]None[/dim]",
1142
+ str(len(mcp_services)),
1143
+ )
1144
+
1145
+ # Hook Services
1146
+ hook_services = startup_config.get("enabled_hook_services", [])
1147
+ hook_display = ", ".join(hook_services[:3]) + (
1148
+ "..." if len(hook_services) > 3 else ""
1149
+ )
1150
+ table.add_row(
1151
+ "Hook Services",
1152
+ hook_display if hook_services else "[dim]None[/dim]",
1153
+ str(len(hook_services)),
1154
+ )
1155
+
1156
+ # System Agents - show count of ENABLED agents (total - disabled)
1157
+ all_agents = self.agent_manager.discover_agents() if self.agent_manager else []
1158
+ disabled_agents = startup_config.get("disabled_agents", [])
1159
+ enabled_count = len(all_agents) - len(disabled_agents)
1160
+
1161
+ # Show first few enabled agent names
1162
+ enabled_names = [a.name for a in all_agents if a.name not in disabled_agents]
1163
+ agent_display = ", ".join(enabled_names[:3]) + (
1164
+ "..." if len(enabled_names) > 3 else ""
1165
+ )
1166
+ table.add_row(
1167
+ "System Agents",
1168
+ agent_display if enabled_names else "[dim]All Disabled[/dim]",
1169
+ f"{enabled_count}/{len(all_agents)}",
1170
+ )
1171
+
1172
+ self.console.print(table)
1173
+
1174
+ def _configure_mcp_services(self, startup_config: Dict, config: Config) -> None:
1175
+ """Configure which MCP services to enable at startup."""
1176
+ self.console.clear()
1177
+ self._display_header()
1178
+ self.console.print("[bold]Configure MCP Services[/bold]\n")
1179
+
1180
+ # Get available MCP services
1181
+ mcp_manager = MCPConfigManager()
1182
+ available_services = list(mcp_manager.STATIC_MCP_CONFIGS.keys())
1183
+ enabled_services = set(startup_config.get("enabled_mcp_services", []))
1184
+
1185
+ # Display services with checkboxes
1186
+ table = Table(box=ROUNDED, show_lines=True)
1187
+ table.add_column("ID", style="dim", width=5)
1188
+ table.add_column("Service", style="cyan", width=25)
1189
+ table.add_column("Status", width=15)
1190
+ table.add_column("Description", style="white", width=45)
1191
+
1192
+ service_descriptions = {
1193
+ "kuzu-memory": "Graph-based memory system for agents",
1194
+ "mcp-ticketer": "Ticket and issue tracking integration",
1195
+ "mcp-browser": "Browser automation and web scraping",
1196
+ "mcp-vector-search": "Semantic code search capabilities",
1197
+ }
1198
+
1199
+ for idx, service in enumerate(available_services, 1):
1200
+ status = (
1201
+ "[green]✓ Enabled[/green]"
1202
+ if service in enabled_services
1203
+ else "[red]✗ Disabled[/red]"
1204
+ )
1205
+ description = service_descriptions.get(service, "MCP service")
1206
+ table.add_row(str(idx), service, status, description)
1207
+
1208
+ self.console.print(table)
1209
+ self.console.print("\n[bold]Commands:[/bold]")
1210
+ self.console.print(" Enter service IDs to toggle (e.g., '1,3' or '1-4')")
1211
+ self.console.print(" [cyan][a][/cyan] Enable all")
1212
+ self.console.print(" [cyan][n][/cyan] Disable all")
1213
+ self.console.print(" [cyan][b][/cyan] Back to previous menu")
1214
+ self.console.print()
1215
+
1216
+ choice = Prompt.ask("[bold cyan]Toggle services[/bold cyan]", default="b")
1217
+
1218
+ if choice == "b":
1219
+ return
1220
+ if choice == "a":
1221
+ startup_config["enabled_mcp_services"] = available_services.copy()
1222
+ self.console.print("[green]All MCP services enabled![/green]")
1223
+ elif choice == "n":
1224
+ startup_config["enabled_mcp_services"] = []
1225
+ self.console.print("[green]All MCP services disabled![/green]")
1226
+ else:
1227
+ # Parse service IDs
1228
+ try:
1229
+ selected_ids = self._parse_id_selection(choice, len(available_services))
1230
+ for idx in selected_ids:
1231
+ service = available_services[idx - 1]
1232
+ if service in enabled_services:
1233
+ enabled_services.remove(service)
1234
+ self.console.print(f"[red]Disabled {service}[/red]")
1235
+ else:
1236
+ enabled_services.add(service)
1237
+ self.console.print(f"[green]Enabled {service}[/green]")
1238
+ startup_config["enabled_mcp_services"] = list(enabled_services)
1239
+ except (ValueError, IndexError) as e:
1240
+ self.console.print(f"[red]Invalid selection: {e}[/red]")
1241
+
1242
+ Prompt.ask("Press Enter to continue")
1243
+
1244
+ def _configure_hook_services(self, startup_config: Dict, config: Config) -> None:
1245
+ """Configure which hook services to enable at startup."""
1246
+ self.console.clear()
1247
+ self._display_header()
1248
+ self.console.print("[bold]Configure Hook Services[/bold]\n")
1249
+
1250
+ # Available hook services
1251
+ available_services = [
1252
+ ("monitor", "Real-time event monitoring server (SocketIO)"),
1253
+ ("dashboard", "Web-based dashboard interface"),
1254
+ ("response-logger", "Agent response logging"),
1255
+ ("health-monitor", "Service health and recovery monitoring"),
1256
+ ]
1257
+
1258
+ enabled_services = set(startup_config.get("enabled_hook_services", []))
1259
+
1260
+ # Display services with checkboxes
1261
+ table = Table(box=ROUNDED, show_lines=True)
1262
+ table.add_column("ID", style="dim", width=5)
1263
+ table.add_column("Service", style="cyan", width=25)
1264
+ table.add_column("Status", width=15)
1265
+ table.add_column("Description", style="white", width=45)
1266
+
1267
+ for idx, (service, description) in enumerate(available_services, 1):
1268
+ status = (
1269
+ "[green]✓ Enabled[/green]"
1270
+ if service in enabled_services
1271
+ else "[red]✗ Disabled[/red]"
1272
+ )
1273
+ table.add_row(str(idx), service, status, description)
1274
+
1275
+ self.console.print(table)
1276
+ self.console.print("\n[bold]Commands:[/bold]")
1277
+ self.console.print(" Enter service IDs to toggle (e.g., '1,3' or '1-4')")
1278
+ self.console.print(" [cyan][a][/cyan] Enable all")
1279
+ self.console.print(" [cyan][n][/cyan] Disable all")
1280
+ self.console.print(" [cyan][b][/cyan] Back to previous menu")
1281
+ self.console.print()
1282
+
1283
+ choice = Prompt.ask("[bold cyan]Toggle services[/bold cyan]", default="b")
1284
+
1285
+ if choice == "b":
1286
+ return
1287
+ if choice == "a":
1288
+ startup_config["enabled_hook_services"] = [s[0] for s in available_services]
1289
+ self.console.print("[green]All hook services enabled![/green]")
1290
+ elif choice == "n":
1291
+ startup_config["enabled_hook_services"] = []
1292
+ self.console.print("[green]All hook services disabled![/green]")
1293
+ else:
1294
+ # Parse service IDs
1295
+ try:
1296
+ selected_ids = self._parse_id_selection(choice, len(available_services))
1297
+ for idx in selected_ids:
1298
+ service = available_services[idx - 1][0]
1299
+ if service in enabled_services:
1300
+ enabled_services.remove(service)
1301
+ self.console.print(f"[red]Disabled {service}[/red]")
1302
+ else:
1303
+ enabled_services.add(service)
1304
+ self.console.print(f"[green]Enabled {service}[/green]")
1305
+ startup_config["enabled_hook_services"] = list(enabled_services)
1306
+ except (ValueError, IndexError) as e:
1307
+ self.console.print(f"[red]Invalid selection: {e}[/red]")
1308
+
1309
+ Prompt.ask("Press Enter to continue")
1310
+
1311
+ def _configure_system_agents(self, startup_config: Dict, config: Config) -> None:
1312
+ """Configure which system agents to deploy at startup.
1313
+
1314
+ NEW LOGIC: Uses disabled_agents list. All agents from templates are enabled by default.
1315
+ """
1316
+ while True:
1317
+ self.console.clear()
1318
+ self._display_header()
1319
+ self.console.print("[bold]Configure System Agents[/bold]\n")
1320
+ self.console.print(
1321
+ "[dim]All agents discovered from templates are enabled by default. "
1322
+ "Mark agents as disabled to prevent deployment.[/dim]\n"
1323
+ )
1324
+
1325
+ # Discover available agents from template files
1326
+ agents = self.agent_manager.discover_agents()
1327
+ disabled_agents = set(startup_config.get("disabled_agents", []))
1328
+
1329
+ # Display agents with checkboxes
1330
+ table = Table(box=ROUNDED, show_lines=True)
1331
+ table.add_column("ID", style="dim", width=5)
1332
+ table.add_column("Agent", style="cyan", width=25)
1333
+ table.add_column("Status", width=15)
1334
+ table.add_column("Description", style="white", width=45)
1335
+
1336
+ for idx, agent in enumerate(agents, 1):
1337
+ # Agent is ENABLED if NOT in disabled list
1338
+ is_enabled = agent.name not in disabled_agents
1339
+ status = (
1340
+ "[green]✓ Enabled[/green]"
1341
+ if is_enabled
1342
+ else "[red]✗ Disabled[/red]"
1343
+ )
1344
+ desc_display = (
1345
+ agent.description[:42] + "..."
1346
+ if len(agent.description) > 42
1347
+ else agent.description
1348
+ )
1349
+ table.add_row(str(idx), agent.name, status, desc_display)
1350
+
1351
+ self.console.print(table)
1352
+ self.console.print("\n[bold]Commands:[/bold]")
1353
+ self.console.print(" Enter agent IDs to toggle (e.g., '1,3' or '1-4')")
1354
+ self.console.print(" [cyan]a[/cyan] - Enable all (clear disabled list)")
1355
+ self.console.print(" [cyan]n[/cyan] - Disable all")
1356
+ self.console.print(" [cyan]b[/cyan] - Back to previous menu")
1357
+ self.console.print()
1358
+
1359
+ choice = Prompt.ask("[bold cyan]Select option[/bold cyan]", default="b")
1360
+
1361
+ if choice == "b":
1362
+ return
1363
+ if choice == "a":
1364
+ # Enable all = empty disabled list
1365
+ startup_config["disabled_agents"] = []
1366
+ self.console.print("[green]All agents enabled![/green]")
1367
+ Prompt.ask("Press Enter to continue")
1368
+ elif choice == "n":
1369
+ # Disable all = all agents in disabled list
1370
+ startup_config["disabled_agents"] = [agent.name for agent in agents]
1371
+ self.console.print("[green]All agents disabled![/green]")
1372
+ Prompt.ask("Press Enter to continue")
1373
+ else:
1374
+ # Parse agent IDs
1375
+ try:
1376
+ selected_ids = self._parse_id_selection(choice, len(agents))
1377
+ for idx in selected_ids:
1378
+ agent = agents[idx - 1]
1379
+ if agent.name in disabled_agents:
1380
+ # Currently disabled, enable it (remove from disabled list)
1381
+ disabled_agents.remove(agent.name)
1382
+ self.console.print(f"[green]Enabled {agent.name}[/green]")
1383
+ else:
1384
+ # Currently enabled, disable it (add to disabled list)
1385
+ disabled_agents.add(agent.name)
1386
+ self.console.print(f"[red]Disabled {agent.name}[/red]")
1387
+ startup_config["disabled_agents"] = list(disabled_agents)
1388
+ # Refresh the display to show updated status immediately
1389
+ except (ValueError, IndexError) as e:
1390
+ self.console.print(f"[red]Invalid selection: {e}[/red]")
1391
+ Prompt.ask("Press Enter to continue")
1392
+
1393
+ def _parse_id_selection(self, selection: str, max_id: int) -> List[int]:
1394
+ """Parse ID selection string (e.g., '1,3,5' or '1-4')."""
1395
+ ids = set()
1396
+ parts = selection.split(",")
1397
+
1398
+ for part in parts:
1399
+ part = part.strip()
1400
+ if "-" in part:
1401
+ # Range selection
1402
+ start, end = part.split("-")
1403
+ start_id = int(start.strip())
1404
+ end_id = int(end.strip())
1405
+ if start_id < 1 or end_id > max_id or start_id > end_id:
1406
+ raise ValueError(f"Invalid range: {part}")
1407
+ ids.update(range(start_id, end_id + 1))
1408
+ else:
1409
+ # Single ID
1410
+ id_num = int(part)
1411
+ if id_num < 1 or id_num > max_id:
1412
+ raise ValueError(f"Invalid ID: {id_num}")
1413
+ ids.add(id_num)
1414
+
1415
+ return sorted(ids)
1416
+
1417
+ def _enable_all_services(self, startup_config: Dict, config: Config) -> None:
1418
+ """Enable all services and agents."""
1419
+ if Confirm.ask("[yellow]Enable ALL services and agents?[/yellow]"):
1420
+ # Enable all MCP services
1421
+ mcp_manager = MCPConfigManager()
1422
+ startup_config["enabled_mcp_services"] = list(
1423
+ mcp_manager.STATIC_MCP_CONFIGS.keys()
1424
+ )
1425
+
1426
+ # Enable all hook services
1427
+ startup_config["enabled_hook_services"] = [
1428
+ "monitor",
1429
+ "dashboard",
1430
+ "response-logger",
1431
+ "health-monitor",
1432
+ ]
1433
+
1434
+ # Enable all agents (empty disabled list)
1435
+ startup_config["disabled_agents"] = []
1436
+
1437
+ self.console.print("[green]All services and agents enabled![/green]")
1438
+ Prompt.ask("Press Enter to continue")
1439
+
1440
+ def _disable_all_services(self, startup_config: Dict, config: Config) -> None:
1441
+ """Disable all services and agents."""
1442
+ if Confirm.ask("[yellow]Disable ALL services and agents?[/yellow]"):
1443
+ startup_config["enabled_mcp_services"] = []
1444
+ startup_config["enabled_hook_services"] = []
1445
+ # Disable all agents = add all to disabled list
1446
+ agents = self.agent_manager.discover_agents()
1447
+ startup_config["disabled_agents"] = [agent.name for agent in agents]
1448
+
1449
+ self.console.print("[green]All services and agents disabled![/green]")
1450
+ self.console.print(
1451
+ "[yellow]Note: You may need to enable at least some services for Claude MPM to function properly.[/yellow]"
1452
+ )
1453
+ Prompt.ask("Press Enter to continue")
1454
+
1455
+ def _reset_to_defaults(self, startup_config: Dict, config: Config) -> None:
1456
+ """Reset startup configuration to defaults."""
1457
+ if Confirm.ask("[yellow]Reset startup configuration to defaults?[/yellow]"):
1458
+ # Reset to default values
1459
+ mcp_manager = MCPConfigManager()
1460
+ startup_config["enabled_mcp_services"] = list(
1461
+ mcp_manager.STATIC_MCP_CONFIGS.keys()
1462
+ )
1463
+ startup_config["enabled_hook_services"] = [
1464
+ "monitor",
1465
+ "dashboard",
1466
+ "response-logger",
1467
+ "health-monitor",
1468
+ ]
1469
+ # Default: All agents enabled (empty disabled list)
1470
+ startup_config["disabled_agents"] = []
1471
+
1472
+ self.console.print(
1473
+ "[green]Startup configuration reset to defaults![/green]"
1474
+ )
1475
+ Prompt.ask("Press Enter to continue")
1476
+
1477
+ def _save_startup_configuration(self, startup_config: Dict, config: Config) -> bool:
1478
+ """Save startup configuration to config file and return whether to proceed to startup.
1479
+
1480
+ Returns:
1481
+ bool: True if should proceed to startup, False to continue in menu
1482
+ """
1483
+ try:
1484
+ # Update the startup configuration
1485
+ config.set("startup", startup_config)
1486
+
1487
+ # IMPORTANT: Also update agent_deployment.disabled_agents so the deployment
1488
+ # system actually uses the configured disabled agents list
1489
+ config.set(
1490
+ "agent_deployment.disabled_agents",
1491
+ startup_config.get("disabled_agents", []),
1492
+ )
1493
+
1494
+ # Determine config file path
1495
+ if self.current_scope == "project":
1496
+ config_file = self.project_dir / ".claude-mpm" / "configuration.yaml"
1497
+ else:
1498
+ config_file = Path.home() / ".claude-mpm" / "configuration.yaml"
1499
+
1500
+ # Ensure directory exists
1501
+ config_file.parent.mkdir(parents=True, exist_ok=True)
1502
+
1503
+ # Temporarily suppress INFO logging to avoid duplicate save messages
1504
+ import logging
1505
+
1506
+ root_logger = logging.getLogger("claude_mpm")
1507
+ original_level = root_logger.level
1508
+ root_logger.setLevel(logging.WARNING)
1509
+
1510
+ try:
1511
+ # Save configuration (this will log at INFO level which we've suppressed)
1512
+ config.save(config_file, format="yaml")
1513
+ finally:
1514
+ # Restore original logging level
1515
+ root_logger.setLevel(original_level)
1516
+
1517
+ self.console.print(
1518
+ f"[green]✓ Startup configuration saved to {config_file}[/green]"
1519
+ )
1520
+ self.console.print(
1521
+ "\n[cyan]Applying configuration and launching Claude MPM...[/cyan]\n"
1522
+ )
1523
+
1524
+ # Launch claude-mpm run command to get full startup cycle
1525
+ # This ensures:
1526
+ # 1. Configuration is loaded
1527
+ # 2. Enabled agents are deployed
1528
+ # 3. Disabled agents are removed from .claude/agents/
1529
+ # 4. MCP services and hooks are started
1530
+ try:
1531
+ # Use execvp to replace the current process with claude-mpm run
1532
+ # This ensures a clean transition from configurator to Claude MPM
1533
+ os.execvp("claude-mpm", ["claude-mpm", "run"])
1534
+ except Exception as e:
1535
+ self.console.print(
1536
+ f"[yellow]Could not launch Claude MPM automatically: {e}[/yellow]"
1537
+ )
1538
+ self.console.print(
1539
+ "[cyan]Please run 'claude-mpm' manually to start.[/cyan]"
1540
+ )
1541
+ Prompt.ask("Press Enter to continue")
1542
+ return True
1543
+
1544
+ # This line will never be reached if execvp succeeds
1545
+ return True
1546
+
1547
+ except Exception as e:
1548
+ self.console.print(f"[red]Error saving configuration: {e}[/red]")
1549
+ Prompt.ask("Press Enter to continue")
1550
+ return False
1551
+
982
1552
  def _switch_scope(self) -> None:
983
1553
  """Switch between project and user scope."""
984
1554
  self.current_scope = "user" if self.current_scope == "project" else "project"
@@ -1377,6 +1947,20 @@ Directory: {self.project_dir}
1377
1947
  except Exception as e:
1378
1948
  return CommandResult.error_result(f"Behavior management failed: {e}")
1379
1949
 
1950
+ def _run_startup_configuration(self) -> CommandResult:
1951
+ """Jump directly to startup configuration."""
1952
+ try:
1953
+ proceed = self._manage_startup_configuration()
1954
+ if proceed:
1955
+ return CommandResult.success_result(
1956
+ "Startup configuration saved, proceeding to startup"
1957
+ )
1958
+ return CommandResult.success_result("Startup configuration completed")
1959
+ except KeyboardInterrupt:
1960
+ return CommandResult.success_result("Startup configuration cancelled")
1961
+ except Exception as e:
1962
+ return CommandResult.error_result(f"Startup configuration failed: {e}")
1963
+
1380
1964
 
1381
1965
  def manage_configure(args) -> int:
1382
1966
  """Main entry point for configuration management command.