claude-mpm 5.4.96__py3-none-any.whl → 5.6.17__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (214) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/{CLAUDE_MPM_FOUNDERS_OUTPUT_STYLE.md → CLAUDE_MPM_RESEARCH_OUTPUT_STYLE.md} +14 -6
  3. claude_mpm/agents/PM_INSTRUCTIONS.md +44 -10
  4. claude_mpm/agents/WORKFLOW.md +2 -0
  5. claude_mpm/agents/templates/circuit-breakers.md +26 -17
  6. claude_mpm/cli/commands/autotodos.py +45 -5
  7. claude_mpm/cli/commands/commander.py +216 -0
  8. claude_mpm/cli/commands/hook_errors.py +60 -60
  9. claude_mpm/cli/commands/run.py +35 -3
  10. claude_mpm/cli/commands/skill_source.py +51 -2
  11. claude_mpm/cli/commands/skills.py +5 -3
  12. claude_mpm/cli/executor.py +32 -17
  13. claude_mpm/cli/parsers/base_parser.py +17 -0
  14. claude_mpm/cli/parsers/commander_parser.py +116 -0
  15. claude_mpm/cli/parsers/run_parser.py +10 -0
  16. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  17. claude_mpm/cli/parsers/skills_parser.py +5 -0
  18. claude_mpm/cli/startup.py +124 -3
  19. claude_mpm/cli/startup_display.py +2 -1
  20. claude_mpm/cli/utils.py +7 -3
  21. claude_mpm/commander/__init__.py +78 -0
  22. claude_mpm/commander/adapters/__init__.py +60 -0
  23. claude_mpm/commander/adapters/auggie.py +260 -0
  24. claude_mpm/commander/adapters/base.py +288 -0
  25. claude_mpm/commander/adapters/claude_code.py +392 -0
  26. claude_mpm/commander/adapters/codex.py +237 -0
  27. claude_mpm/commander/adapters/communication.py +366 -0
  28. claude_mpm/commander/adapters/example_usage.py +310 -0
  29. claude_mpm/commander/adapters/mpm.py +389 -0
  30. claude_mpm/commander/adapters/registry.py +204 -0
  31. claude_mpm/commander/api/__init__.py +16 -0
  32. claude_mpm/commander/api/app.py +121 -0
  33. claude_mpm/commander/api/errors.py +133 -0
  34. claude_mpm/commander/api/routes/__init__.py +8 -0
  35. claude_mpm/commander/api/routes/events.py +184 -0
  36. claude_mpm/commander/api/routes/inbox.py +171 -0
  37. claude_mpm/commander/api/routes/messages.py +148 -0
  38. claude_mpm/commander/api/routes/projects.py +271 -0
  39. claude_mpm/commander/api/routes/sessions.py +226 -0
  40. claude_mpm/commander/api/routes/work.py +296 -0
  41. claude_mpm/commander/api/schemas.py +186 -0
  42. claude_mpm/commander/chat/__init__.py +7 -0
  43. claude_mpm/commander/chat/cli.py +111 -0
  44. claude_mpm/commander/chat/commands.py +96 -0
  45. claude_mpm/commander/chat/repl.py +310 -0
  46. claude_mpm/commander/config.py +49 -0
  47. claude_mpm/commander/config_loader.py +115 -0
  48. claude_mpm/commander/core/__init__.py +10 -0
  49. claude_mpm/commander/core/block_manager.py +325 -0
  50. claude_mpm/commander/core/response_manager.py +323 -0
  51. claude_mpm/commander/daemon.py +594 -0
  52. claude_mpm/commander/env_loader.py +59 -0
  53. claude_mpm/commander/events/__init__.py +26 -0
  54. claude_mpm/commander/events/manager.py +332 -0
  55. claude_mpm/commander/frameworks/__init__.py +12 -0
  56. claude_mpm/commander/frameworks/base.py +143 -0
  57. claude_mpm/commander/frameworks/claude_code.py +58 -0
  58. claude_mpm/commander/frameworks/mpm.py +62 -0
  59. claude_mpm/commander/inbox/__init__.py +16 -0
  60. claude_mpm/commander/inbox/dedup.py +128 -0
  61. claude_mpm/commander/inbox/inbox.py +224 -0
  62. claude_mpm/commander/inbox/models.py +70 -0
  63. claude_mpm/commander/instance_manager.py +337 -0
  64. claude_mpm/commander/llm/__init__.py +6 -0
  65. claude_mpm/commander/llm/openrouter_client.py +167 -0
  66. claude_mpm/commander/llm/summarizer.py +70 -0
  67. claude_mpm/commander/memory/__init__.py +45 -0
  68. claude_mpm/commander/memory/compression.py +347 -0
  69. claude_mpm/commander/memory/embeddings.py +230 -0
  70. claude_mpm/commander/memory/entities.py +310 -0
  71. claude_mpm/commander/memory/example_usage.py +290 -0
  72. claude_mpm/commander/memory/integration.py +325 -0
  73. claude_mpm/commander/memory/search.py +381 -0
  74. claude_mpm/commander/memory/store.py +657 -0
  75. claude_mpm/commander/models/__init__.py +18 -0
  76. claude_mpm/commander/models/events.py +121 -0
  77. claude_mpm/commander/models/project.py +162 -0
  78. claude_mpm/commander/models/work.py +214 -0
  79. claude_mpm/commander/parsing/__init__.py +20 -0
  80. claude_mpm/commander/parsing/extractor.py +132 -0
  81. claude_mpm/commander/parsing/output_parser.py +270 -0
  82. claude_mpm/commander/parsing/patterns.py +100 -0
  83. claude_mpm/commander/persistence/__init__.py +11 -0
  84. claude_mpm/commander/persistence/event_store.py +274 -0
  85. claude_mpm/commander/persistence/state_store.py +309 -0
  86. claude_mpm/commander/persistence/work_store.py +164 -0
  87. claude_mpm/commander/polling/__init__.py +13 -0
  88. claude_mpm/commander/polling/event_detector.py +104 -0
  89. claude_mpm/commander/polling/output_buffer.py +49 -0
  90. claude_mpm/commander/polling/output_poller.py +153 -0
  91. claude_mpm/commander/project_session.py +268 -0
  92. claude_mpm/commander/proxy/__init__.py +12 -0
  93. claude_mpm/commander/proxy/formatter.py +89 -0
  94. claude_mpm/commander/proxy/output_handler.py +191 -0
  95. claude_mpm/commander/proxy/relay.py +155 -0
  96. claude_mpm/commander/registry.py +410 -0
  97. claude_mpm/commander/runtime/__init__.py +10 -0
  98. claude_mpm/commander/runtime/executor.py +191 -0
  99. claude_mpm/commander/runtime/monitor.py +346 -0
  100. claude_mpm/commander/session/__init__.py +6 -0
  101. claude_mpm/commander/session/context.py +81 -0
  102. claude_mpm/commander/session/manager.py +59 -0
  103. claude_mpm/commander/tmux_orchestrator.py +361 -0
  104. claude_mpm/commander/web/__init__.py +1 -0
  105. claude_mpm/commander/work/__init__.py +30 -0
  106. claude_mpm/commander/work/executor.py +207 -0
  107. claude_mpm/commander/work/queue.py +405 -0
  108. claude_mpm/commander/workflow/__init__.py +27 -0
  109. claude_mpm/commander/workflow/event_handler.py +241 -0
  110. claude_mpm/commander/workflow/notifier.py +146 -0
  111. claude_mpm/commands/mpm-config.md +8 -0
  112. claude_mpm/commands/mpm-doctor.md +8 -0
  113. claude_mpm/commands/mpm-help.md +8 -0
  114. claude_mpm/commands/mpm-init.md +8 -0
  115. claude_mpm/commands/mpm-monitor.md +8 -0
  116. claude_mpm/commands/mpm-organize.md +8 -0
  117. claude_mpm/commands/mpm-postmortem.md +8 -0
  118. claude_mpm/commands/mpm-session-resume.md +8 -0
  119. claude_mpm/commands/mpm-status.md +8 -0
  120. claude_mpm/commands/mpm-ticket-view.md +8 -0
  121. claude_mpm/commands/mpm-version.md +8 -0
  122. claude_mpm/commands/mpm.md +8 -0
  123. claude_mpm/config/agent_presets.py +8 -7
  124. claude_mpm/config/skill_sources.py +16 -0
  125. claude_mpm/core/claude_runner.py +143 -0
  126. claude_mpm/core/config.py +32 -19
  127. claude_mpm/core/logger.py +26 -9
  128. claude_mpm/core/logging_utils.py +35 -11
  129. claude_mpm/core/output_style_manager.py +49 -12
  130. claude_mpm/core/unified_config.py +10 -6
  131. claude_mpm/core/unified_paths.py +68 -80
  132. claude_mpm/experimental/cli_enhancements.py +2 -1
  133. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
  134. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  135. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  136. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
  137. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
  138. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  139. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
  140. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  141. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  142. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
  143. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
  144. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  145. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
  146. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  147. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
  148. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
  149. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  150. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
  151. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
  152. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
  153. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
  154. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +29 -30
  155. claude_mpm/hooks/claude_hooks/event_handlers.py +112 -99
  156. claude_mpm/hooks/claude_hooks/hook_handler.py +81 -88
  157. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +6 -11
  158. claude_mpm/hooks/claude_hooks/installer.py +116 -8
  159. claude_mpm/hooks/claude_hooks/memory_integration.py +51 -31
  160. claude_mpm/hooks/claude_hooks/response_tracking.py +39 -58
  161. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
  162. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
  163. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  164. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  165. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
  166. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
  167. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
  168. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
  169. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  170. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
  171. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
  172. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  173. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
  174. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
  175. claude_mpm/hooks/claude_hooks/services/connection_manager.py +23 -28
  176. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +36 -103
  177. claude_mpm/hooks/claude_hooks/services/state_manager.py +23 -36
  178. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +47 -73
  179. claude_mpm/hooks/session_resume_hook.py +22 -18
  180. claude_mpm/hooks/templates/pre_tool_use_template.py +10 -2
  181. claude_mpm/scripts/claude-hook-handler.sh +43 -16
  182. claude_mpm/scripts/start_activity_logging.py +0 -0
  183. claude_mpm/services/agents/agent_recommendation_service.py +8 -8
  184. claude_mpm/services/agents/agent_selection_service.py +2 -2
  185. claude_mpm/services/agents/loading/framework_agent_loader.py +75 -2
  186. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  187. claude_mpm/services/event_log.py +8 -0
  188. claude_mpm/services/pm_skills_deployer.py +84 -6
  189. claude_mpm/services/skills/git_skill_source_manager.py +130 -10
  190. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  191. claude_mpm/services/skills/skill_discovery_service.py +74 -4
  192. claude_mpm/services/skills_deployer.py +31 -5
  193. claude_mpm/skills/__init__.py +2 -1
  194. claude_mpm/skills/bundled/pm/mpm/SKILL.md +38 -0
  195. claude_mpm/skills/bundled/pm/mpm-config/SKILL.md +29 -0
  196. claude_mpm/skills/bundled/pm/mpm-doctor/SKILL.md +53 -0
  197. claude_mpm/skills/bundled/pm/mpm-help/SKILL.md +35 -0
  198. claude_mpm/skills/bundled/pm/mpm-init/SKILL.md +125 -0
  199. claude_mpm/skills/bundled/pm/mpm-monitor/SKILL.md +32 -0
  200. claude_mpm/skills/bundled/pm/mpm-organize/SKILL.md +121 -0
  201. claude_mpm/skills/bundled/pm/mpm-postmortem/SKILL.md +22 -0
  202. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  203. claude_mpm/skills/bundled/pm/mpm-session-resume/SKILL.md +31 -0
  204. claude_mpm/skills/bundled/pm/mpm-status/SKILL.md +37 -0
  205. claude_mpm/skills/bundled/pm/mpm-ticket-view/SKILL.md +110 -0
  206. claude_mpm/skills/bundled/pm/mpm-version/SKILL.md +21 -0
  207. claude_mpm/skills/registry.py +295 -90
  208. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +22 -6
  209. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +213 -83
  210. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  211. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  212. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  213. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  214. {claude_mpm-5.4.96.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,111 @@
1
+ """Commander CLI entry point."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from claude_mpm.commander.env_loader import load_env
9
+ from claude_mpm.commander.instance_manager import InstanceManager
10
+ from claude_mpm.commander.llm.openrouter_client import (
11
+ OpenRouterClient,
12
+ OpenRouterConfig,
13
+ )
14
+ from claude_mpm.commander.llm.summarizer import OutputSummarizer
15
+ from claude_mpm.commander.proxy.formatter import OutputFormatter
16
+ from claude_mpm.commander.proxy.output_handler import OutputHandler
17
+ from claude_mpm.commander.proxy.relay import OutputRelay
18
+ from claude_mpm.commander.session.manager import SessionManager
19
+ from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
20
+
21
+ from .repl import CommanderREPL
22
+
23
+ # Load environment variables at module import
24
+ load_env()
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ async def run_commander(
30
+ port: int = 8765,
31
+ state_dir: Optional[Path] = None,
32
+ ) -> None:
33
+ """Run Commander in interactive mode.
34
+
35
+ Args:
36
+ port: Port for internal services (unused currently).
37
+ state_dir: Directory for state persistence (optional).
38
+
39
+ Example:
40
+ >>> asyncio.run(run_commander())
41
+ # Starts interactive Commander REPL
42
+ """
43
+ # Setup logging
44
+ logging.basicConfig(
45
+ level=logging.INFO,
46
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
47
+ )
48
+
49
+ # Initialize components
50
+ logger.info("Initializing Commander...")
51
+
52
+ # Create tmux orchestrator
53
+ orchestrator = TmuxOrchestrator()
54
+
55
+ # Create instance manager
56
+ instance_manager = InstanceManager(orchestrator)
57
+
58
+ # Create session manager
59
+ session_manager = SessionManager()
60
+
61
+ # Try to initialize LLM client (optional)
62
+ llm_client: Optional[OpenRouterClient] = None
63
+ try:
64
+ config = OpenRouterConfig()
65
+ llm_client = OpenRouterClient(config)
66
+ logger.info("LLM client initialized")
67
+ except ValueError as e:
68
+ logger.warning(f"LLM client not available: {e}")
69
+ logger.warning("Output summarization will be disabled")
70
+
71
+ # Create output relay (optional)
72
+ output_relay: Optional[OutputRelay] = None
73
+ if llm_client:
74
+ try:
75
+ summarizer = OutputSummarizer(llm_client)
76
+ handler = OutputHandler(orchestrator, summarizer)
77
+ formatter = OutputFormatter()
78
+ output_relay = OutputRelay(handler, formatter)
79
+ logger.info("Output relay initialized")
80
+ except Exception as e:
81
+ logger.warning(f"Output relay setup failed: {e}")
82
+
83
+ # Create REPL
84
+ repl = CommanderREPL(
85
+ instance_manager=instance_manager,
86
+ session_manager=session_manager,
87
+ output_relay=output_relay,
88
+ llm_client=llm_client,
89
+ )
90
+
91
+ # Run REPL
92
+ try:
93
+ await repl.run()
94
+ except KeyboardInterrupt:
95
+ logger.info("Commander interrupted by user")
96
+ except Exception as e:
97
+ logger.error(f"Commander error: {e}", exc_info=True)
98
+ finally:
99
+ # Cleanup
100
+ logger.info("Shutting down Commander...")
101
+ if output_relay:
102
+ await output_relay.stop_all()
103
+
104
+
105
+ def main() -> None:
106
+ """Entry point for command-line execution."""
107
+ asyncio.run(run_commander())
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,96 @@
1
+ """Built-in Commander chat commands."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+
8
+ class CommandType(Enum):
9
+ """Built-in command types."""
10
+
11
+ LIST = "list"
12
+ START = "start"
13
+ STOP = "stop"
14
+ CONNECT = "connect"
15
+ DISCONNECT = "disconnect"
16
+ STATUS = "status"
17
+ HELP = "help"
18
+ EXIT = "exit"
19
+ INSTANCES = "instances" # alias for list
20
+
21
+
22
+ @dataclass
23
+ class Command:
24
+ """Parsed command with args."""
25
+
26
+ type: CommandType
27
+ args: list[str]
28
+ raw: str
29
+
30
+
31
+ class CommandParser:
32
+ """Parses user input into commands."""
33
+
34
+ ALIASES = {
35
+ "ls": CommandType.LIST,
36
+ "instances": CommandType.LIST,
37
+ "quit": CommandType.EXIT,
38
+ "q": CommandType.EXIT,
39
+ }
40
+
41
+ def parse(self, input_text: str) -> Optional[Command]:
42
+ """Parse input into a Command.
43
+
44
+ Returns None if input is not a built-in command (natural language).
45
+
46
+ Args:
47
+ input_text: Raw user input.
48
+
49
+ Returns:
50
+ Command if input is a built-in command, None otherwise.
51
+
52
+ Example:
53
+ >>> parser = CommandParser()
54
+ >>> cmd = parser.parse("list")
55
+ >>> cmd.type
56
+ <CommandType.LIST: 'list'>
57
+ >>> parser.parse("tell me about the code")
58
+ None
59
+ """
60
+ if not input_text:
61
+ return None
62
+
63
+ parts = input_text.split()
64
+ command_str = parts[0].lower()
65
+ args = parts[1:] if len(parts) > 1 else []
66
+
67
+ # Check if it's an alias
68
+ if command_str in self.ALIASES:
69
+ cmd_type = self.ALIASES[command_str]
70
+ return Command(type=cmd_type, args=args, raw=input_text)
71
+
72
+ # Check if it's a direct command
73
+ try:
74
+ cmd_type = CommandType(command_str)
75
+ return Command(type=cmd_type, args=args, raw=input_text)
76
+ except ValueError:
77
+ # Not a built-in command
78
+ return None
79
+
80
+ def is_command(self, input_text: str) -> bool:
81
+ """Check if input is a built-in command.
82
+
83
+ Args:
84
+ input_text: Raw user input.
85
+
86
+ Returns:
87
+ True if input is a built-in command, False otherwise.
88
+
89
+ Example:
90
+ >>> parser = CommandParser()
91
+ >>> parser.is_command("list")
92
+ True
93
+ >>> parser.is_command("tell me about the code")
94
+ False
95
+ """
96
+ return self.parse(input_text) is not None
@@ -0,0 +1,310 @@
1
+ """Commander chat REPL interface."""
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.history import FileHistory
9
+
10
+ from claude_mpm.commander.instance_manager import InstanceManager
11
+ from claude_mpm.commander.llm.openrouter_client import OpenRouterClient
12
+ from claude_mpm.commander.proxy.relay import OutputRelay
13
+ from claude_mpm.commander.session.manager import SessionManager
14
+
15
+ from .commands import Command, CommandParser, CommandType
16
+
17
+
18
+ class CommanderREPL:
19
+ """Interactive REPL for Commander mode."""
20
+
21
+ def __init__(
22
+ self,
23
+ instance_manager: InstanceManager,
24
+ session_manager: SessionManager,
25
+ output_relay: Optional[OutputRelay] = None,
26
+ llm_client: Optional[OpenRouterClient] = None,
27
+ ):
28
+ """Initialize REPL.
29
+
30
+ Args:
31
+ instance_manager: Manages Claude instances.
32
+ session_manager: Manages chat session state.
33
+ output_relay: Optional relay for instance output.
34
+ llm_client: Optional OpenRouter client for chat.
35
+ """
36
+ self.instances = instance_manager
37
+ self.session = session_manager
38
+ self.relay = output_relay
39
+ self.llm = llm_client
40
+ self.parser = CommandParser()
41
+ self._running = False
42
+
43
+ async def run(self) -> None:
44
+ """Start the REPL loop."""
45
+ self._running = True
46
+ self._print_welcome()
47
+
48
+ # Setup history file
49
+ history_path = Path.home() / ".claude-mpm" / "commander_history"
50
+ history_path.parent.mkdir(parents=True, exist_ok=True)
51
+
52
+ prompt = PromptSession(history=FileHistory(str(history_path)))
53
+
54
+ while self._running:
55
+ try:
56
+ user_input = await asyncio.to_thread(prompt.prompt, self._get_prompt())
57
+ await self._handle_input(user_input.strip())
58
+ except KeyboardInterrupt:
59
+ continue
60
+ except EOFError:
61
+ break
62
+
63
+ self._print("\nGoodbye!")
64
+
65
+ async def _handle_input(self, input_text: str) -> None:
66
+ """Handle user input - command or natural language.
67
+
68
+ Args:
69
+ input_text: User input string.
70
+ """
71
+ if not input_text:
72
+ return
73
+
74
+ # Check if it's a built-in command
75
+ command = self.parser.parse(input_text)
76
+ if command:
77
+ await self._execute_command(command)
78
+ else:
79
+ # Natural language - send to connected instance
80
+ await self._send_to_instance(input_text)
81
+
82
+ async def _execute_command(self, cmd: Command) -> None:
83
+ """Execute a built-in command.
84
+
85
+ Args:
86
+ cmd: Parsed command.
87
+ """
88
+ handlers = {
89
+ CommandType.LIST: self._cmd_list,
90
+ CommandType.START: self._cmd_start,
91
+ CommandType.STOP: self._cmd_stop,
92
+ CommandType.CONNECT: self._cmd_connect,
93
+ CommandType.DISCONNECT: self._cmd_disconnect,
94
+ CommandType.STATUS: self._cmd_status,
95
+ CommandType.HELP: self._cmd_help,
96
+ CommandType.EXIT: self._cmd_exit,
97
+ }
98
+ handler = handlers.get(cmd.type)
99
+ if handler:
100
+ await handler(cmd.args)
101
+
102
+ async def _cmd_list(self, args: list[str]) -> None:
103
+ """List active instances."""
104
+ instances = self.instances.list_instances()
105
+ if not instances:
106
+ self._print("No active instances.")
107
+ else:
108
+ self._print("Active instances:")
109
+ for inst in instances:
110
+ status = (
111
+ "→" if inst.name == self.session.context.connected_instance else " "
112
+ )
113
+ git_info = f" [{inst.git_branch}]" if inst.git_branch else ""
114
+ self._print(
115
+ f" {status} {inst.name} ({inst.framework}){git_info} - {inst.project_path}"
116
+ )
117
+
118
+ async def _cmd_start(self, args: list[str]) -> None:
119
+ """Start a new instance: start <path> [--framework cc|mpm] [--name name]."""
120
+ if not args:
121
+ self._print("Usage: start <path> [--framework cc|mpm] [--name name]")
122
+ return
123
+
124
+ # Parse arguments
125
+ project_path = Path(args[0]).expanduser().resolve()
126
+ framework = "cc" # default
127
+ name = project_path.name # default
128
+
129
+ # Parse optional flags
130
+ i = 1
131
+ while i < len(args):
132
+ if args[i] == "--framework" and i + 1 < len(args):
133
+ framework = args[i + 1]
134
+ i += 2
135
+ elif args[i] == "--name" and i + 1 < len(args):
136
+ name = args[i + 1]
137
+ i += 2
138
+ else:
139
+ i += 1
140
+
141
+ # Validate path
142
+ if not project_path.exists():
143
+ self._print(f"Error: Path does not exist: {project_path}")
144
+ return
145
+
146
+ if not project_path.is_dir():
147
+ self._print(f"Error: Path is not a directory: {project_path}")
148
+ return
149
+
150
+ # Start instance
151
+ try:
152
+ instance = await self.instances.start_instance(
153
+ name, project_path, framework
154
+ )
155
+ self._print(f"Started instance '{name}' ({framework}) at {project_path}")
156
+ self._print(f"Tmux: {instance.tmux_session}:{instance.pane_target}")
157
+ except Exception as e:
158
+ self._print(f"Error starting instance: {e}")
159
+
160
+ async def _cmd_stop(self, args: list[str]) -> None:
161
+ """Stop an instance: stop <name>."""
162
+ if not args:
163
+ self._print("Usage: stop <instance-name>")
164
+ return
165
+
166
+ name = args[0]
167
+
168
+ try:
169
+ await self.instances.stop_instance(name)
170
+ self._print(f"Stopped instance '{name}'")
171
+
172
+ # Disconnect if we were connected
173
+ if self.session.context.connected_instance == name:
174
+ self.session.disconnect()
175
+ except Exception as e:
176
+ self._print(f"Error stopping instance: {e}")
177
+
178
+ async def _cmd_connect(self, args: list[str]) -> None:
179
+ """Connect to an instance: connect <name>."""
180
+ if not args:
181
+ self._print("Usage: connect <instance-name>")
182
+ return
183
+
184
+ name = args[0]
185
+ inst = self.instances.get_instance(name)
186
+ if not inst:
187
+ self._print(f"Instance '{name}' not found")
188
+ return
189
+
190
+ self.session.connect_to(name)
191
+ self._print(f"Connected to {name}")
192
+
193
+ async def _cmd_disconnect(self, args: list[str]) -> None:
194
+ """Disconnect from current instance."""
195
+ if not self.session.context.is_connected:
196
+ self._print("Not connected to any instance")
197
+ return
198
+
199
+ name = self.session.context.connected_instance
200
+ self.session.disconnect()
201
+ self._print(f"Disconnected from {name}")
202
+
203
+ async def _cmd_status(self, args: list[str]) -> None:
204
+ """Show status of current session."""
205
+ if self.session.context.is_connected:
206
+ name = self.session.context.connected_instance
207
+ inst = self.instances.get_instance(name)
208
+ if inst:
209
+ self._print(f"Connected to: {name}")
210
+ self._print(f" Framework: {inst.framework}")
211
+ self._print(f" Project: {inst.project_path}")
212
+ if inst.git_branch:
213
+ self._print(f" Git: {inst.git_branch} ({inst.git_status})")
214
+ self._print(f" Tmux: {inst.tmux_session}:{inst.pane_target}")
215
+ else:
216
+ self._print(f"Connected to: {name} (instance no longer exists)")
217
+ else:
218
+ self._print("Not connected to any instance")
219
+
220
+ self._print(f"Messages in history: {len(self.session.context.messages)}")
221
+
222
+ async def _cmd_help(self, args: list[str]) -> None:
223
+ """Show help message."""
224
+ help_text = """
225
+ Commander Commands:
226
+ list, ls, instances List active instances
227
+ start <path> Start new instance at path
228
+ --framework <cc|mpm> Specify framework (default: cc)
229
+ --name <name> Specify instance name (default: dir name)
230
+ stop <name> Stop an instance
231
+ connect <name> Connect to an instance
232
+ disconnect Disconnect from current instance
233
+ status Show current session status
234
+ help Show this help message
235
+ exit, quit, q Exit Commander
236
+
237
+ Natural Language:
238
+ When connected to an instance, any input that is not a built-in
239
+ command will be sent to the connected instance as a message.
240
+
241
+ Examples:
242
+ start ~/myproject --framework cc --name myapp
243
+ connect myapp
244
+ Fix the authentication bug in login.py
245
+ disconnect
246
+ exit
247
+ """
248
+ self._print(help_text)
249
+
250
+ async def _cmd_exit(self, args: list[str]) -> None:
251
+ """Exit the REPL."""
252
+ self._running = False
253
+
254
+ async def _send_to_instance(self, message: str) -> None:
255
+ """Send natural language to connected instance.
256
+
257
+ Args:
258
+ message: User message to send.
259
+ """
260
+ if not self.session.context.is_connected:
261
+ self._print("Not connected to any instance. Use 'connect <name>' first.")
262
+ return
263
+
264
+ name = self.session.context.connected_instance
265
+ inst = self.instances.get_instance(name)
266
+ if not inst:
267
+ self._print(f"Instance '{name}' no longer exists")
268
+ self.session.disconnect()
269
+ return
270
+
271
+ self._print(f"[Sending to {name}...]")
272
+ await self.instances.send_to_instance(name, message)
273
+ self.session.add_user_message(message)
274
+
275
+ # Wait for and display response
276
+ if self.relay:
277
+ try:
278
+ output = await self.relay.get_latest_output(
279
+ name, inst.pane_target, context=message
280
+ )
281
+ self._print(f"\n[Response from {name}]:\n{output}")
282
+ self.session.add_assistant_message(output)
283
+ except Exception as e:
284
+ self._print(f"\n[Error getting response: {e}]")
285
+
286
+ def _get_prompt(self) -> str:
287
+ """Get prompt string based on connection state.
288
+
289
+ Returns:
290
+ Prompt string for input.
291
+ """
292
+ if self.session.context.is_connected:
293
+ return f"Commander ({self.session.context.connected_instance})> "
294
+ return "Commander> "
295
+
296
+ def _print(self, msg: str) -> None:
297
+ """Print message to console.
298
+
299
+ Args:
300
+ msg: Message to print.
301
+ """
302
+ print(msg)
303
+
304
+ def _print_welcome(self) -> None:
305
+ """Print welcome message."""
306
+ print("╔══════════════════════════════════════════╗")
307
+ print("║ MPM Commander - Interactive Mode ║")
308
+ print("╚══════════════════════════════════════════╝")
309
+ print("Type 'help' for commands, or natural language to chat.")
310
+ print()
@@ -0,0 +1,49 @@
1
+ """Daemon configuration for MPM Commander.
2
+
3
+ This module defines configuration structures for the Commander daemon,
4
+ including server settings, resource limits, and persistence options.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass
12
+ class DaemonConfig:
13
+ """Configuration for Commander daemon.
14
+
15
+ Attributes:
16
+ host: API server bind address
17
+ port: API server port
18
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
19
+ state_dir: Directory for state persistence
20
+ max_projects: Maximum concurrent projects
21
+ healthcheck_interval: Healthcheck interval in seconds
22
+ save_interval: State persistence interval in seconds
23
+ poll_interval: Event polling interval in seconds
24
+
25
+ Example:
26
+ >>> config = DaemonConfig(port=8765, log_level="DEBUG")
27
+ >>> config.state_dir
28
+ PosixPath('/Users/user/.claude-mpm/commander')
29
+ """
30
+
31
+ host: str = "127.0.0.1"
32
+ port: int = 8765
33
+ log_level: str = "INFO"
34
+ state_dir: Path = Path.home() / ".claude-mpm" / "commander"
35
+ max_projects: int = 10
36
+ healthcheck_interval: int = 30
37
+ save_interval: int = 30
38
+ poll_interval: float = 2.0
39
+
40
+ def __post_init__(self):
41
+ """Ensure state_dir is a Path object and create if needed."""
42
+ if isinstance(self.state_dir, str):
43
+ self.state_dir = Path(self.state_dir)
44
+
45
+ # Expand user home directory
46
+ self.state_dir = self.state_dir.expanduser()
47
+
48
+ # Create state directory if it doesn't exist
49
+ self.state_dir.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,115 @@
1
+ """Project configuration loading for MPM Commander.
2
+
3
+ This module handles loading configuration from a project's .claude-mpm/
4
+ directory, including YAML config files and directory structure validation.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ import yaml
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def load_project_config(project_path: str) -> Optional[Dict[str, Any]]:
17
+ """Load configuration from project's .claude-mpm/ directory.
18
+
19
+ Looks for:
20
+ - .claude-mpm/configuration.yaml (main config file)
21
+ - .claude-mpm/agents/ (directory exists check)
22
+ - .claude-mpm/skills/ (directory exists check)
23
+ - .claude-mpm/memories/ (directory exists check)
24
+
25
+ Args:
26
+ project_path: Absolute path to project directory
27
+
28
+ Returns:
29
+ Dict with config data and directory flags, or None if no config found.
30
+ Example structure:
31
+ {
32
+ 'configuration': {...}, # Parsed YAML config
33
+ 'has_agents': True,
34
+ 'has_skills': False,
35
+ 'has_memories': True,
36
+ }
37
+
38
+ Raises:
39
+ yaml.YAMLError: If configuration.yaml is malformed
40
+
41
+ Example:
42
+ >>> config = load_project_config("/Users/masa/Projects/my-app")
43
+ >>> if config:
44
+ ... print(f"Agents dir: {config['has_agents']}")
45
+ ... print(f"Config: {config.get('configuration', {})}")
46
+ Agents dir: True
47
+ Config: {'default_adapter': 'linear', ...}
48
+ """
49
+ proj_path = Path(project_path)
50
+ config_dir = proj_path / ".claude-mpm"
51
+
52
+ # Check if .claude-mpm/ directory exists
53
+ if not config_dir.exists() or not config_dir.is_dir():
54
+ logger.debug("No .claude-mpm/ directory found in project: %s", project_path)
55
+ return None
56
+
57
+ logger.info("Loading configuration from %s", config_dir)
58
+
59
+ result: Dict[str, Any] = {
60
+ "configuration": {},
61
+ "has_agents": False,
62
+ "has_skills": False,
63
+ "has_memories": False,
64
+ }
65
+
66
+ # Load configuration.yaml if present
67
+ config_file = config_dir / "configuration.yaml"
68
+ if config_file.exists() and config_file.is_file():
69
+ try:
70
+ with open(config_file, encoding="utf-8") as f:
71
+ config_data = yaml.safe_load(f)
72
+ result["configuration"] = config_data or {}
73
+ logger.info(
74
+ "Loaded configuration.yaml with %d top-level keys",
75
+ len(result["configuration"]),
76
+ )
77
+ except yaml.YAMLError as e:
78
+ logger.error(
79
+ "Failed to parse configuration.yaml in %s: %s",
80
+ config_dir,
81
+ e,
82
+ )
83
+ raise
84
+ except Exception as e:
85
+ logger.warning(
86
+ "Failed to read configuration.yaml in %s: %s",
87
+ config_dir,
88
+ e,
89
+ )
90
+ # Continue with empty config
91
+
92
+ # Check for subdirectories
93
+ agents_dir = config_dir / "agents"
94
+ result["has_agents"] = agents_dir.exists() and agents_dir.is_dir()
95
+ if result["has_agents"]:
96
+ logger.debug("Found agents directory: %s", agents_dir)
97
+
98
+ skills_dir = config_dir / "skills"
99
+ result["has_skills"] = skills_dir.exists() and skills_dir.is_dir()
100
+ if result["has_skills"]:
101
+ logger.debug("Found skills directory: %s", skills_dir)
102
+
103
+ memories_dir = config_dir / "memories"
104
+ result["has_memories"] = memories_dir.exists() and memories_dir.is_dir()
105
+ if result["has_memories"]:
106
+ logger.debug("Found memories directory: %s", memories_dir)
107
+
108
+ logger.info(
109
+ "Project config loaded: agents=%s, skills=%s, memories=%s",
110
+ result["has_agents"],
111
+ result["has_skills"],
112
+ result["has_memories"],
113
+ )
114
+
115
+ return result
@@ -0,0 +1,10 @@
1
+ """Core coordination components for MPM Commander.
2
+
3
+ This module provides core components that coordinate between different
4
+ subsystems like events, work execution, and session management.
5
+ """
6
+
7
+ from .block_manager import BlockManager
8
+ from .response_manager import ResponseManager, ResponseRoute
9
+
10
+ __all__ = ["BlockManager", "ResponseManager", "ResponseRoute"]