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.
Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {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}")
@@ -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, Optional
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
- global registry, tmux, event_manager, inbox, event_handler, session_manager
46
- registry = ProjectRegistry()
47
- tmux = TmuxOrchestrator()
48
- event_manager = EventManager()
49
- inbox = Inbox(event_manager, registry)
50
- session_manager = {} # Populated by daemon when sessions are created
51
- event_handler = EventHandler(inbox, session_manager)
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
+ )