claude-mpm 5.6.1__py3-none-any.whl → 5.6.76__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
- claude_mpm/auth/__init__.py +35 -0
- claude_mpm/auth/callback_server.py +328 -0
- claude_mpm/auth/models.py +104 -0
- claude_mpm/auth/oauth_manager.py +266 -0
- claude_mpm/auth/providers/__init__.py +12 -0
- claude_mpm/auth/providers/base.py +165 -0
- claude_mpm/auth/providers/google.py +261 -0
- claude_mpm/auth/token_storage.py +252 -0
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/commands/mcp.py +29 -17
- claude_mpm/cli/commands/mcp_command_router.py +39 -0
- claude_mpm/cli/commands/mcp_service_commands.py +304 -0
- claude_mpm/cli/commands/oauth.py +481 -0
- claude_mpm/cli/commands/skill_source.py +51 -2
- claude_mpm/cli/commands/skills.py +5 -3
- claude_mpm/cli/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/parsers/skill_source_parser.py +4 -0
- claude_mpm/cli/parsers/skills_parser.py +5 -0
- claude_mpm/cli/startup.py +300 -33
- claude_mpm/cli/startup_display.py +4 -2
- claude_mpm/cli/startup_migrations.py +236 -0
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/errors.py +21 -0
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +37 -26
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +47 -5
- claude_mpm/commander/chat/commands.py +44 -16
- claude_mpm/commander/chat/repl.py +1729 -82
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +91 -1
- claude_mpm/commander/frameworks/mpm.py +9 -14
- claude_mpm/commander/git/__init__.py +5 -0
- claude_mpm/commander/git/worktree_manager.py +212 -0
- claude_mpm/commander/instance_manager.py +546 -15
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/config/skill_sources.py +16 -0
- claude_mpm/constants.py +5 -0
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +30 -22
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logger.py +16 -2
- claude_mpm/core/logging_utils.py +40 -16
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +37 -7
- claude_mpm/core/socketio_pool.py +47 -15
- claude_mpm/core/unified_paths.py +68 -80
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
- claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +222 -54
- claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
- claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
- claude_mpm/hooks/claude_hooks/services/container.py +326 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
- claude_mpm/hooks/session_resume_hook.py +22 -18
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
- claude_mpm/init.py +21 -14
- claude_mpm/mcp/__init__.py +9 -0
- claude_mpm/mcp/google_workspace_server.py +610 -0
- claude_mpm/scripts/claude-hook-handler.sh +10 -9
- claude_mpm/services/agents/agent_selection_service.py +2 -2
- claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/mcp_config_manager.py +99 -19
- claude_mpm/services/mcp_service_registry.py +294 -0
- claude_mpm/services/monitor/server.py +6 -1
- claude_mpm/services/pm_skills_deployer.py +5 -3
- claude_mpm/services/skills/git_skill_source_manager.py +79 -8
- claude_mpm/services/skills/selective_skill_deployer.py +28 -0
- claude_mpm/services/skills/skill_discovery_service.py +17 -1
- claude_mpm/services/skills_deployer.py +31 -5
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""MPM (Multi-agent Project Manager) runtime adapter.
|
|
2
|
+
|
|
3
|
+
This module implements the RuntimeAdapter interface for MPM,
|
|
4
|
+
providing full support for agent delegation, hooks, skills, and monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import shlex
|
|
11
|
+
from typing import List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from .base import (
|
|
14
|
+
Capability,
|
|
15
|
+
ParsedResponse,
|
|
16
|
+
RuntimeAdapter,
|
|
17
|
+
RuntimeCapability,
|
|
18
|
+
RuntimeInfo,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MPMAdapter(RuntimeAdapter):
|
|
25
|
+
"""Adapter for MPM (Multi-agent Project Manager).
|
|
26
|
+
|
|
27
|
+
MPM extends Claude Code with multi-agent orchestration, lifecycle hooks,
|
|
28
|
+
skills, real-time monitoring, and advanced project management capabilities.
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
- Agent delegation and sub-agent spawning
|
|
32
|
+
- Lifecycle hooks (pre/post task, pre/post commit, etc.)
|
|
33
|
+
- Loadable skills for specialized tasks
|
|
34
|
+
- Real-time monitoring dashboard
|
|
35
|
+
- Custom instructions via CLAUDE.md
|
|
36
|
+
- MCP server integration
|
|
37
|
+
- Git workflow automation
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> adapter = MPMAdapter()
|
|
41
|
+
>>> cmd = adapter.build_launch_command("/home/user/project")
|
|
42
|
+
>>> # Inject agent context
|
|
43
|
+
>>> ctx_cmd = adapter.inject_agent_context("eng-001", {"role": "Engineer"})
|
|
44
|
+
>>> # Check capabilities
|
|
45
|
+
>>> info = adapter.runtime_info
|
|
46
|
+
>>> if RuntimeCapability.AGENT_DELEGATION in info.capabilities:
|
|
47
|
+
... print("Supports agent delegation")
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Idle detection patterns (inherits from Claude Code)
|
|
51
|
+
IDLE_PATTERNS = [
|
|
52
|
+
r"^>\s*$", # Simple prompt
|
|
53
|
+
r"claude>\s*$", # Named prompt
|
|
54
|
+
r"╭─+╮", # Box drawing
|
|
55
|
+
r"What would you like",
|
|
56
|
+
r"How can I help",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# MPM-specific patterns
|
|
60
|
+
MPM_PATTERNS = [
|
|
61
|
+
r"\[MPM\]", # MPM prefix
|
|
62
|
+
r"Agent spawned:",
|
|
63
|
+
r"Delegating to agent:",
|
|
64
|
+
r"Hook triggered:",
|
|
65
|
+
r"Skill loaded:",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Error patterns
|
|
69
|
+
ERROR_PATTERNS = [
|
|
70
|
+
r"Error:",
|
|
71
|
+
r"Failed:",
|
|
72
|
+
r"Exception:",
|
|
73
|
+
r"Permission denied",
|
|
74
|
+
r"not found",
|
|
75
|
+
r"Traceback \(most recent call last\)",
|
|
76
|
+
r"FATAL:",
|
|
77
|
+
r"✗",
|
|
78
|
+
r"command not found",
|
|
79
|
+
r"cannot access",
|
|
80
|
+
r"Agent error:",
|
|
81
|
+
r"Hook failed:",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# Question patterns
|
|
85
|
+
QUESTION_PATTERNS = [
|
|
86
|
+
r"Which option",
|
|
87
|
+
r"Should I proceed",
|
|
88
|
+
r"Please choose",
|
|
89
|
+
r"\(y/n\)\?",
|
|
90
|
+
r"Are you sure",
|
|
91
|
+
r"Do you want",
|
|
92
|
+
r"\[Y/n\]",
|
|
93
|
+
r"\[yes/no\]",
|
|
94
|
+
r"Select an option",
|
|
95
|
+
r"Choose from",
|
|
96
|
+
r"Delegate to which agent",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# ANSI escape code pattern
|
|
100
|
+
ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
"""Return the runtime identifier."""
|
|
105
|
+
return "mpm"
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def capabilities(self) -> Set[Capability]:
|
|
109
|
+
"""Return the set of capabilities supported by MPM."""
|
|
110
|
+
return {
|
|
111
|
+
Capability.TOOL_USE,
|
|
112
|
+
Capability.FILE_EDIT,
|
|
113
|
+
Capability.FILE_CREATE,
|
|
114
|
+
Capability.GIT_OPERATIONS,
|
|
115
|
+
Capability.SHELL_COMMANDS,
|
|
116
|
+
Capability.WEB_SEARCH,
|
|
117
|
+
Capability.COMPLEX_REASONING,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def runtime_info(self) -> RuntimeInfo:
|
|
122
|
+
"""Return detailed runtime information.
|
|
123
|
+
|
|
124
|
+
MPM has the most comprehensive capabilities of all runtimes.
|
|
125
|
+
"""
|
|
126
|
+
return RuntimeInfo(
|
|
127
|
+
name="mpm",
|
|
128
|
+
version=None, # Could parse from mpm --version
|
|
129
|
+
capabilities={
|
|
130
|
+
RuntimeCapability.FILE_READ,
|
|
131
|
+
RuntimeCapability.FILE_EDIT,
|
|
132
|
+
RuntimeCapability.FILE_CREATE,
|
|
133
|
+
RuntimeCapability.BASH_EXECUTION,
|
|
134
|
+
RuntimeCapability.GIT_OPERATIONS,
|
|
135
|
+
RuntimeCapability.TOOL_USE,
|
|
136
|
+
RuntimeCapability.AGENT_DELEGATION, # Key MPM feature
|
|
137
|
+
RuntimeCapability.HOOKS, # Lifecycle hooks
|
|
138
|
+
RuntimeCapability.INSTRUCTIONS, # CLAUDE.md
|
|
139
|
+
RuntimeCapability.MCP_TOOLS, # MCP integration
|
|
140
|
+
RuntimeCapability.SKILLS, # Loadable skills
|
|
141
|
+
RuntimeCapability.MONITOR, # Real-time monitoring
|
|
142
|
+
RuntimeCapability.WEB_SEARCH,
|
|
143
|
+
RuntimeCapability.COMPLEX_REASONING,
|
|
144
|
+
},
|
|
145
|
+
command="claude", # MPM uses claude CLI with MPM config
|
|
146
|
+
supports_agents=True, # Full agent support
|
|
147
|
+
instruction_file="CLAUDE.md",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def build_launch_command(
|
|
151
|
+
self, project_path: str, agent_prompt: Optional[str] = None
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Generate shell command to start MPM.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
project_path: Absolute path to the project directory
|
|
157
|
+
agent_prompt: Optional system prompt to configure agent
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Shell command string ready to execute
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> adapter = MPMAdapter()
|
|
164
|
+
>>> adapter.build_launch_command("/home/user/project")
|
|
165
|
+
"cd '/home/user/project' && claude --dangerously-skip-permissions"
|
|
166
|
+
"""
|
|
167
|
+
quoted_path = shlex.quote(project_path)
|
|
168
|
+
cmd = f"cd {quoted_path} && claude"
|
|
169
|
+
|
|
170
|
+
if agent_prompt:
|
|
171
|
+
quoted_prompt = shlex.quote(agent_prompt)
|
|
172
|
+
cmd += f" --system-prompt {quoted_prompt}"
|
|
173
|
+
|
|
174
|
+
# Skip permissions for automated operation
|
|
175
|
+
cmd += " --dangerously-skip-permissions"
|
|
176
|
+
|
|
177
|
+
logger.debug(f"Built MPM launch command: {cmd}")
|
|
178
|
+
return cmd
|
|
179
|
+
|
|
180
|
+
def format_input(self, message: str) -> str:
|
|
181
|
+
"""Prepare message for MPM's input format."""
|
|
182
|
+
formatted = message.strip()
|
|
183
|
+
logger.debug(f"Formatted input: {formatted[:100]}...")
|
|
184
|
+
return formatted
|
|
185
|
+
|
|
186
|
+
def strip_ansi(self, text: str) -> str:
|
|
187
|
+
"""Remove ANSI escape codes from text."""
|
|
188
|
+
return self.ANSI_ESCAPE.sub("", text)
|
|
189
|
+
|
|
190
|
+
def detect_idle(self, output: str) -> bool:
|
|
191
|
+
"""Recognize when MPM is waiting for input."""
|
|
192
|
+
if not output:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
clean = self.strip_ansi(output)
|
|
196
|
+
lines = clean.strip().split("\n")
|
|
197
|
+
|
|
198
|
+
if not lines:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
last_line = lines[-1].strip()
|
|
202
|
+
|
|
203
|
+
for pattern in self.IDLE_PATTERNS:
|
|
204
|
+
if re.search(pattern, last_line):
|
|
205
|
+
logger.debug(f"Detected idle state with pattern: {pattern}")
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def detect_error(self, output: str) -> Optional[str]:
|
|
211
|
+
"""Recognize error states and extract error message."""
|
|
212
|
+
clean = self.strip_ansi(output)
|
|
213
|
+
|
|
214
|
+
for pattern in self.ERROR_PATTERNS:
|
|
215
|
+
match = re.search(pattern, clean, re.IGNORECASE)
|
|
216
|
+
if match:
|
|
217
|
+
for line in clean.split("\n"):
|
|
218
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
219
|
+
error_msg = line.strip()
|
|
220
|
+
logger.warning(f"Detected error: {error_msg}")
|
|
221
|
+
return error_msg
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def detect_question(
|
|
226
|
+
self, output: str
|
|
227
|
+
) -> tuple[bool, Optional[str], Optional[List[str]]]:
|
|
228
|
+
"""Detect if MPM is asking a question."""
|
|
229
|
+
clean = self.strip_ansi(output)
|
|
230
|
+
|
|
231
|
+
for pattern in self.QUESTION_PATTERNS:
|
|
232
|
+
if re.search(pattern, clean, re.IGNORECASE):
|
|
233
|
+
lines = clean.strip().split("\n")
|
|
234
|
+
question = None
|
|
235
|
+
options = []
|
|
236
|
+
|
|
237
|
+
for line in lines:
|
|
238
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
239
|
+
question = line.strip()
|
|
240
|
+
|
|
241
|
+
# Look for numbered options
|
|
242
|
+
opt_match = re.match(r"^\s*(\d+)[.):]\s*(.+)$", line)
|
|
243
|
+
if opt_match:
|
|
244
|
+
options.append(opt_match.group(2).strip())
|
|
245
|
+
|
|
246
|
+
logger.debug(
|
|
247
|
+
f"Detected question: {question}, options: {options if options else 'none'}"
|
|
248
|
+
)
|
|
249
|
+
return True, question, options if options else None
|
|
250
|
+
|
|
251
|
+
return False, None, None
|
|
252
|
+
|
|
253
|
+
def parse_response(self, output: str) -> ParsedResponse:
|
|
254
|
+
"""Extract meaningful content from MPM output."""
|
|
255
|
+
if not output:
|
|
256
|
+
return ParsedResponse(
|
|
257
|
+
content="",
|
|
258
|
+
is_complete=False,
|
|
259
|
+
is_error=False,
|
|
260
|
+
is_question=False,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
clean = self.strip_ansi(output)
|
|
264
|
+
error_msg = self.detect_error(output)
|
|
265
|
+
is_question, question_text, options = self.detect_question(output)
|
|
266
|
+
is_complete = self.detect_idle(output)
|
|
267
|
+
|
|
268
|
+
response = ParsedResponse(
|
|
269
|
+
content=clean,
|
|
270
|
+
is_complete=is_complete,
|
|
271
|
+
is_error=error_msg is not None,
|
|
272
|
+
error_message=error_msg,
|
|
273
|
+
is_question=is_question,
|
|
274
|
+
question_text=question_text,
|
|
275
|
+
options=options,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
logger.debug(
|
|
279
|
+
f"Parsed response: complete={is_complete}, error={error_msg is not None}, "
|
|
280
|
+
f"question={is_question}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return response
|
|
284
|
+
|
|
285
|
+
def inject_instructions(self, instructions: str) -> Optional[str]:
|
|
286
|
+
"""Return command to inject custom instructions.
|
|
287
|
+
|
|
288
|
+
MPM uses CLAUDE.md for custom instructions.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
instructions: Instructions text to inject
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Command to write CLAUDE.md file
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> adapter = MPMAdapter()
|
|
298
|
+
>>> cmd = adapter.inject_instructions("You are a Python expert")
|
|
299
|
+
>>> print(cmd)
|
|
300
|
+
echo '...' > CLAUDE.md
|
|
301
|
+
"""
|
|
302
|
+
# Write to CLAUDE.md
|
|
303
|
+
escaped = instructions.replace("'", "'\\''")
|
|
304
|
+
return f"echo '{escaped}' > CLAUDE.md"
|
|
305
|
+
|
|
306
|
+
def inject_agent_context(self, agent_id: str, context: dict) -> Optional[str]:
|
|
307
|
+
"""Return command to inject agent context.
|
|
308
|
+
|
|
309
|
+
MPM supports agent context injection via special command.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
agent_id: Unique identifier for agent
|
|
313
|
+
context: Context dictionary with agent metadata
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Command to inject agent context
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
>>> adapter = MPMAdapter()
|
|
320
|
+
>>> cmd = adapter.inject_agent_context("eng-001", {"role": "Engineer"})
|
|
321
|
+
>>> # Command would set MPM_AGENT_ID and MPM_AGENT_CONTEXT env vars
|
|
322
|
+
"""
|
|
323
|
+
# Serialize context to JSON
|
|
324
|
+
context_json = json.dumps(context)
|
|
325
|
+
escaped_json = context_json.replace("'", "'\\''")
|
|
326
|
+
|
|
327
|
+
# Set environment variables for MPM agent context
|
|
328
|
+
# MPM runtime can read these to understand agent identity
|
|
329
|
+
cmd = f"export MPM_AGENT_ID='{agent_id}' && export MPM_AGENT_CONTEXT='{escaped_json}'"
|
|
330
|
+
|
|
331
|
+
logger.debug(f"Built agent context injection command for {agent_id}")
|
|
332
|
+
return cmd
|
|
333
|
+
|
|
334
|
+
def detect_agent_spawn(self, output: str) -> Optional[dict]:
|
|
335
|
+
"""Detect if MPM has spawned a new agent.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
output: Raw output from MPM
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dict with agent info if spawn detected, None otherwise
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
>>> adapter = MPMAdapter()
|
|
345
|
+
>>> info = adapter.detect_agent_spawn("[MPM] Agent spawned: eng-001 (Engineer)")
|
|
346
|
+
>>> if info:
|
|
347
|
+
... print(info['agent_id'])
|
|
348
|
+
'eng-001'
|
|
349
|
+
"""
|
|
350
|
+
clean = self.strip_ansi(output)
|
|
351
|
+
|
|
352
|
+
# Pattern: [MPM] Agent spawned: <agent_id> (<role>)
|
|
353
|
+
match = re.search(r"\[MPM\] Agent spawned: (\S+) \(([^)]+)\)", clean)
|
|
354
|
+
if match:
|
|
355
|
+
agent_id = match.group(1)
|
|
356
|
+
role = match.group(2)
|
|
357
|
+
|
|
358
|
+
logger.info(f"Detected agent spawn: {agent_id} ({role})")
|
|
359
|
+
return {"agent_id": agent_id, "role": role}
|
|
360
|
+
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def detect_hook_trigger(self, output: str) -> Optional[dict]:
|
|
364
|
+
"""Detect if a lifecycle hook was triggered.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
output: Raw output from MPM
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Dict with hook info if trigger detected, None otherwise
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
>>> adapter = MPMAdapter()
|
|
374
|
+
>>> info = adapter.detect_hook_trigger("[MPM] Hook triggered: pre-commit")
|
|
375
|
+
>>> if info:
|
|
376
|
+
... print(info['hook_name'])
|
|
377
|
+
'pre-commit'
|
|
378
|
+
"""
|
|
379
|
+
clean = self.strip_ansi(output)
|
|
380
|
+
|
|
381
|
+
# Pattern: [MPM] Hook triggered: <hook_name>
|
|
382
|
+
match = re.search(r"\[MPM\] Hook triggered: (\S+)", clean)
|
|
383
|
+
if match:
|
|
384
|
+
hook_name = match.group(1)
|
|
385
|
+
|
|
386
|
+
logger.info(f"Detected hook trigger: {hook_name}")
|
|
387
|
+
return {"hook_name": hook_name}
|
|
388
|
+
|
|
389
|
+
return None
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Adapter registry for runtime detection and selection.
|
|
2
|
+
|
|
3
|
+
This module provides a registry for managing runtime adapters, with
|
|
4
|
+
automatic detection of available runtimes on the system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import Dict, List, Optional, Type
|
|
10
|
+
|
|
11
|
+
from .base import RuntimeAdapter
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AdapterRegistry:
|
|
17
|
+
"""Registry for managing runtime adapters.
|
|
18
|
+
|
|
19
|
+
Provides centralized registration, retrieval, and auto-detection
|
|
20
|
+
of available runtime adapters.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> # Register adapter
|
|
24
|
+
>>> AdapterRegistry.register('claude-code', ClaudeCodeAdapter)
|
|
25
|
+
>>> # Get adapter instance
|
|
26
|
+
>>> adapter = AdapterRegistry.get('claude-code')
|
|
27
|
+
>>> # Detect available runtimes
|
|
28
|
+
>>> available = AdapterRegistry.detect_available()
|
|
29
|
+
>>> print(available)
|
|
30
|
+
['claude-code', 'mpm']
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_adapters: Dict[str, Type[RuntimeAdapter]] = {}
|
|
34
|
+
_runtime_commands: Dict[str, str] = {
|
|
35
|
+
"claude-code": "claude",
|
|
36
|
+
"auggie": "auggie",
|
|
37
|
+
"codex": "codex",
|
|
38
|
+
"mpm": "claude", # MPM uses claude with extra config
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def register(cls, name: str, adapter_class: Type[RuntimeAdapter]) -> None:
|
|
43
|
+
"""Register a runtime adapter.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Unique identifier for the adapter
|
|
47
|
+
adapter_class: RuntimeAdapter subclass to register
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> AdapterRegistry.register('my-runtime', MyAdapter)
|
|
51
|
+
"""
|
|
52
|
+
cls._adapters[name] = adapter_class
|
|
53
|
+
logger.debug(f"Registered adapter: {name}")
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def unregister(cls, name: str) -> None:
|
|
57
|
+
"""Unregister a runtime adapter.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name: Identifier of adapter to unregister
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> AdapterRegistry.unregister('my-runtime')
|
|
64
|
+
"""
|
|
65
|
+
if name in cls._adapters:
|
|
66
|
+
del cls._adapters[name]
|
|
67
|
+
logger.debug(f"Unregistered adapter: {name}")
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get(cls, name: str) -> Optional[RuntimeAdapter]:
|
|
71
|
+
"""Get adapter instance by name.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Identifier of adapter to retrieve
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
RuntimeAdapter instance if found, None otherwise
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> adapter = AdapterRegistry.get('claude-code')
|
|
81
|
+
>>> if adapter:
|
|
82
|
+
... print(adapter.name)
|
|
83
|
+
'claude-code'
|
|
84
|
+
"""
|
|
85
|
+
if name in cls._adapters:
|
|
86
|
+
adapter = cls._adapters[name]()
|
|
87
|
+
logger.debug(f"Retrieved adapter: {name}")
|
|
88
|
+
return adapter
|
|
89
|
+
logger.warning(f"Adapter not found: {name}")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def list_registered(cls) -> List[str]:
|
|
94
|
+
"""List all registered adapter names.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of registered adapter identifiers
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> registered = AdapterRegistry.list_registered()
|
|
101
|
+
>>> print(registered)
|
|
102
|
+
['claude-code', 'auggie', 'codex', 'mpm']
|
|
103
|
+
"""
|
|
104
|
+
return list(cls._adapters.keys())
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def detect_available(cls) -> List[str]:
|
|
108
|
+
"""Detect which runtimes are available on this system.
|
|
109
|
+
|
|
110
|
+
Checks for CLI commands in PATH to determine which runtimes
|
|
111
|
+
are installed and accessible.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of available runtime identifiers
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> available = AdapterRegistry.detect_available()
|
|
118
|
+
>>> if 'claude-code' in available:
|
|
119
|
+
... print("Claude Code is available")
|
|
120
|
+
"""
|
|
121
|
+
available = []
|
|
122
|
+
|
|
123
|
+
for name, command in cls._runtime_commands.items():
|
|
124
|
+
if name in cls._adapters and shutil.which(command):
|
|
125
|
+
available.append(name)
|
|
126
|
+
logger.debug(f"Detected available runtime: {name} (command: {command})")
|
|
127
|
+
|
|
128
|
+
logger.info(f"Available runtimes: {available}")
|
|
129
|
+
return available
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def get_default(cls) -> Optional[RuntimeAdapter]:
|
|
133
|
+
"""Get the best available adapter.
|
|
134
|
+
|
|
135
|
+
Selection priority: mpm > claude-code > auggie > codex
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
RuntimeAdapter instance for best available runtime, or None
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> adapter = AdapterRegistry.get_default()
|
|
142
|
+
>>> if adapter:
|
|
143
|
+
... print(f"Using {adapter.name}")
|
|
144
|
+
"""
|
|
145
|
+
# Priority order: MPM has most features, then Claude Code, etc.
|
|
146
|
+
priority = ["mpm", "claude-code", "auggie", "codex"]
|
|
147
|
+
|
|
148
|
+
available = cls.detect_available()
|
|
149
|
+
|
|
150
|
+
for name in priority:
|
|
151
|
+
if name in available:
|
|
152
|
+
adapter = cls.get(name)
|
|
153
|
+
logger.info(f"Selected default adapter: {name}")
|
|
154
|
+
return adapter
|
|
155
|
+
|
|
156
|
+
logger.warning("No adapters available")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def is_available(cls, name: str) -> bool:
|
|
161
|
+
"""Check if a specific runtime is available.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Runtime identifier to check
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if runtime is registered and command is in PATH
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> if AdapterRegistry.is_available('claude-code'):
|
|
171
|
+
... adapter = AdapterRegistry.get('claude-code')
|
|
172
|
+
"""
|
|
173
|
+
return name in cls.detect_available()
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def get_command(cls, name: str) -> Optional[str]:
|
|
177
|
+
"""Get CLI command for a runtime.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Runtime identifier
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
CLI command string if found, None otherwise
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> cmd = AdapterRegistry.get_command('claude-code')
|
|
187
|
+
>>> print(cmd)
|
|
188
|
+
'claude'
|
|
189
|
+
"""
|
|
190
|
+
return cls._runtime_commands.get(name)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def register_command(cls, name: str, command: str) -> None:
|
|
194
|
+
"""Register CLI command for a runtime.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
name: Runtime identifier
|
|
198
|
+
command: CLI command to invoke runtime
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> AdapterRegistry.register_command('my-runtime', 'my-cli')
|
|
202
|
+
"""
|
|
203
|
+
cls._runtime_commands[name] = command
|
|
204
|
+
logger.debug(f"Registered command for {name}: {command}")
|
claude_mpm/commander/api/app.py
CHANGED
|
@@ -6,7 +6,7 @@ lifecycle management, and route registration.
|
|
|
6
6
|
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import AsyncGenerator
|
|
9
|
+
from typing import AsyncGenerator
|
|
10
10
|
|
|
11
11
|
from fastapi import FastAPI
|
|
12
12
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -20,14 +20,6 @@ from ..tmux_orchestrator import TmuxOrchestrator
|
|
|
20
20
|
from ..workflow import EventHandler
|
|
21
21
|
from .routes import events, inbox as inbox_routes, messages, projects, sessions, work
|
|
22
22
|
|
|
23
|
-
# Global instances (injected at startup via lifespan)
|
|
24
|
-
registry: Optional[ProjectRegistry] = None
|
|
25
|
-
tmux: Optional[TmuxOrchestrator] = None
|
|
26
|
-
event_manager: Optional[EventManager] = None
|
|
27
|
-
inbox: Optional[Inbox] = None
|
|
28
|
-
event_handler: Optional[EventHandler] = None
|
|
29
|
-
session_manager: dict = {} # project_id -> ProjectSession
|
|
30
|
-
|
|
31
23
|
|
|
32
24
|
@asynccontextmanager
|
|
33
25
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
@@ -42,13 +34,37 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
42
34
|
None during application runtime
|
|
43
35
|
"""
|
|
44
36
|
# Startup
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
37
|
+
import logging
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
logger.info("Lifespan starting. Initializing app.state resources...")
|
|
41
|
+
|
|
42
|
+
# Initialize app.state resources (daemon will inject its instances later)
|
|
43
|
+
if not hasattr(app.state, "registry"):
|
|
44
|
+
app.state.registry = ProjectRegistry()
|
|
45
|
+
if not hasattr(app.state, "tmux"):
|
|
46
|
+
app.state.tmux = TmuxOrchestrator()
|
|
47
|
+
if not hasattr(app.state, "event_manager"):
|
|
48
|
+
app.state.event_manager = EventManager()
|
|
49
|
+
if not hasattr(app.state, "inbox"):
|
|
50
|
+
app.state.inbox = Inbox(app.state.event_manager, app.state.registry)
|
|
51
|
+
if not hasattr(app.state, "session_manager"):
|
|
52
|
+
app.state.session_manager = {}
|
|
53
|
+
if not hasattr(app.state, "work_queues"):
|
|
54
|
+
logger.info("work_queues not set, creating new dict")
|
|
55
|
+
app.state.work_queues = {}
|
|
56
|
+
else:
|
|
57
|
+
logger.info(
|
|
58
|
+
f"work_queues already set, preserving id: {id(app.state.work_queues)}"
|
|
59
|
+
)
|
|
60
|
+
if not hasattr(app.state, "daemon_instance"):
|
|
61
|
+
app.state.daemon_instance = None
|
|
62
|
+
if not hasattr(app.state, "event_handler"):
|
|
63
|
+
app.state.event_handler = EventHandler(
|
|
64
|
+
app.state.inbox, app.state.session_manager
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
logger.info(f"Lifespan complete. work_queues id: {id(app.state.work_queues)}")
|
|
52
68
|
|
|
53
69
|
yield
|
|
54
70
|
|
|
@@ -110,3 +110,24 @@ class InvalidRuntimeError(CommanderAPIError):
|
|
|
110
110
|
f"Invalid runtime: {runtime}",
|
|
111
111
|
400,
|
|
112
112
|
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TmuxNoSpaceError(CommanderAPIError):
|
|
116
|
+
"""Raised when tmux has no space for a new pane."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, message: str | None = None):
|
|
119
|
+
"""Initialize tmux no space error.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
message: Custom error message (optional)
|
|
123
|
+
"""
|
|
124
|
+
default_msg = (
|
|
125
|
+
"Unable to create session: tmux has no space for new pane. "
|
|
126
|
+
"Try closing some sessions or resize your terminal window. "
|
|
127
|
+
"You can also create a new tmux window with `tmux new-window`."
|
|
128
|
+
)
|
|
129
|
+
super().__init__(
|
|
130
|
+
"TMUX_NO_SPACE",
|
|
131
|
+
message or default_msg,
|
|
132
|
+
409,
|
|
133
|
+
)
|