claude-mpm 5.6.10__py3-none-any.whl → 5.6.33__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/startup.py +140 -20
- claude_mpm/cli/startup_display.py +2 -1
- 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/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +42 -3
- 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/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- 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/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/core/claude_runner.py +152 -0
- claude_mpm/core/config.py +3 -3
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logging_utils.py +4 -2
- 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 +13 -5
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +284 -89
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
- claude_mpm/hooks/claude_hooks/installer.py +90 -28
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
- claude_mpm/hooks/claude_hooks/services/container.py +310 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/pm_skills_deployer.py +3 -2
- 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.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@ conversation threads for projects.
|
|
|
7
7
|
import uuid
|
|
8
8
|
from typing import List
|
|
9
9
|
|
|
10
|
-
from fastapi import APIRouter
|
|
10
|
+
from fastapi import APIRouter, Request
|
|
11
11
|
|
|
12
12
|
from ...models import ThreadMessage
|
|
13
13
|
from ..errors import ProjectNotFoundError
|
|
@@ -16,13 +16,11 @@ from ..schemas import MessageResponse, SendMessageRequest
|
|
|
16
16
|
router = APIRouter()
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _get_registry():
|
|
20
|
-
"""Get registry instance from app
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if registry is None:
|
|
19
|
+
def _get_registry(request: Request):
|
|
20
|
+
"""Get registry instance from app.state."""
|
|
21
|
+
if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
|
|
24
22
|
raise RuntimeError("Registry not initialized")
|
|
25
|
-
return registry
|
|
23
|
+
return request.app.state.registry
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def _message_to_response(message: ThreadMessage) -> MessageResponse:
|
|
@@ -44,7 +42,7 @@ def _message_to_response(message: ThreadMessage) -> MessageResponse:
|
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
@router.get("/projects/{project_id}/thread", response_model=List[MessageResponse])
|
|
47
|
-
async def get_thread(project_id: str) -> List[MessageResponse]:
|
|
45
|
+
async def get_thread(request: Request, project_id: str) -> List[MessageResponse]:
|
|
48
46
|
"""Get conversation thread for a project.
|
|
49
47
|
|
|
50
48
|
Returns all messages in chronological order.
|
|
@@ -77,7 +75,7 @@ async def get_thread(project_id: str) -> List[MessageResponse]:
|
|
|
77
75
|
}
|
|
78
76
|
]
|
|
79
77
|
"""
|
|
80
|
-
registry = _get_registry()
|
|
78
|
+
registry = _get_registry(request)
|
|
81
79
|
project = registry.get(project_id)
|
|
82
80
|
|
|
83
81
|
if project is None:
|
|
@@ -90,7 +88,9 @@ async def get_thread(project_id: str) -> List[MessageResponse]:
|
|
|
90
88
|
@router.post(
|
|
91
89
|
"/projects/{project_id}/messages", response_model=MessageResponse, status_code=201
|
|
92
90
|
)
|
|
93
|
-
async def send_message(
|
|
91
|
+
async def send_message(
|
|
92
|
+
request: Request, project_id: str, req: SendMessageRequest
|
|
93
|
+
) -> MessageResponse:
|
|
94
94
|
"""Send a message to a project's active session.
|
|
95
95
|
|
|
96
96
|
Adds message to conversation thread and sends to specified or active session.
|
|
@@ -119,7 +119,7 @@ async def send_message(project_id: str, req: SendMessageRequest) -> MessageRespo
|
|
|
119
119
|
"timestamp": "2025-01-12T10:00:00Z"
|
|
120
120
|
}
|
|
121
121
|
"""
|
|
122
|
-
registry = _get_registry()
|
|
122
|
+
registry = _get_registry(request)
|
|
123
123
|
project = registry.get(project_id)
|
|
124
124
|
|
|
125
125
|
if project is None:
|
|
@@ -7,7 +7,7 @@ projects in the MPM Commander.
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import List
|
|
9
9
|
|
|
10
|
-
from fastapi import APIRouter, Response
|
|
10
|
+
from fastapi import APIRouter, Request, Response
|
|
11
11
|
|
|
12
12
|
from ...models import ProjectState
|
|
13
13
|
from ..errors import InvalidPathError, ProjectAlreadyExistsError, ProjectNotFoundError
|
|
@@ -16,13 +16,11 @@ from ..schemas import ProjectResponse, RegisterProjectRequest, SessionResponse
|
|
|
16
16
|
router = APIRouter()
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _get_registry():
|
|
20
|
-
"""Get registry instance from app
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if registry is None:
|
|
19
|
+
def _get_registry(request: Request):
|
|
20
|
+
"""Get registry instance from app.state."""
|
|
21
|
+
if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
|
|
24
22
|
raise RuntimeError("Registry not initialized")
|
|
25
|
-
return registry
|
|
23
|
+
return request.app.state.registry
|
|
26
24
|
|
|
27
25
|
|
|
28
26
|
def _project_to_response(project) -> ProjectResponse:
|
|
@@ -61,7 +59,7 @@ def _project_to_response(project) -> ProjectResponse:
|
|
|
61
59
|
|
|
62
60
|
|
|
63
61
|
@router.get("/projects", response_model=List[ProjectResponse])
|
|
64
|
-
async def list_projects() -> List[ProjectResponse]:
|
|
62
|
+
async def list_projects(request: Request) -> List[ProjectResponse]:
|
|
65
63
|
"""List all registered projects.
|
|
66
64
|
|
|
67
65
|
Returns:
|
|
@@ -83,13 +81,13 @@ async def list_projects() -> List[ProjectResponse]:
|
|
|
83
81
|
}
|
|
84
82
|
]
|
|
85
83
|
"""
|
|
86
|
-
registry = _get_registry()
|
|
84
|
+
registry = _get_registry(request)
|
|
87
85
|
projects = registry.list_all()
|
|
88
86
|
return [_project_to_response(p) for p in projects]
|
|
89
87
|
|
|
90
88
|
|
|
91
89
|
@router.get("/projects/{project_id}", response_model=ProjectResponse)
|
|
92
|
-
async def get_project(project_id: str) -> ProjectResponse:
|
|
90
|
+
async def get_project(request: Request, project_id: str) -> ProjectResponse:
|
|
93
91
|
"""Get project details by ID.
|
|
94
92
|
|
|
95
93
|
Args:
|
|
@@ -111,7 +109,7 @@ async def get_project(project_id: str) -> ProjectResponse:
|
|
|
111
109
|
...
|
|
112
110
|
}
|
|
113
111
|
"""
|
|
114
|
-
registry = _get_registry()
|
|
112
|
+
registry = _get_registry(request)
|
|
115
113
|
project = registry.get(project_id)
|
|
116
114
|
|
|
117
115
|
if project is None:
|
|
@@ -121,7 +119,9 @@ async def get_project(project_id: str) -> ProjectResponse:
|
|
|
121
119
|
|
|
122
120
|
|
|
123
121
|
@router.post("/projects", response_model=ProjectResponse, status_code=201)
|
|
124
|
-
async def register_project(
|
|
122
|
+
async def register_project(
|
|
123
|
+
request: Request, req: RegisterProjectRequest
|
|
124
|
+
) -> ProjectResponse:
|
|
125
125
|
"""Register a new project.
|
|
126
126
|
|
|
127
127
|
Args:
|
|
@@ -148,7 +148,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
|
|
|
148
148
|
...
|
|
149
149
|
}
|
|
150
150
|
"""
|
|
151
|
-
registry = _get_registry()
|
|
151
|
+
registry = _get_registry(request)
|
|
152
152
|
|
|
153
153
|
# Validate path exists and is directory
|
|
154
154
|
path_obj = Path(req.path)
|
|
@@ -156,7 +156,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
|
|
|
156
156
|
raise InvalidPathError(req.path)
|
|
157
157
|
|
|
158
158
|
try:
|
|
159
|
-
project = registry.register(req.path, req.name)
|
|
159
|
+
project = registry.register(req.path, req.name, req.project_id)
|
|
160
160
|
return _project_to_response(project)
|
|
161
161
|
except ValueError as e:
|
|
162
162
|
# Registry raises ValueError for duplicate registration
|
|
@@ -168,7 +168,7 @@ async def register_project(req: RegisterProjectRequest) -> ProjectResponse:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
@router.delete("/projects/{project_id}", status_code=204)
|
|
171
|
-
async def unregister_project(project_id: str) -> Response:
|
|
171
|
+
async def unregister_project(request: Request, project_id: str) -> Response:
|
|
172
172
|
"""Unregister a project.
|
|
173
173
|
|
|
174
174
|
Args:
|
|
@@ -184,7 +184,7 @@ async def unregister_project(project_id: str) -> Response:
|
|
|
184
184
|
DELETE /api/projects/abc-123
|
|
185
185
|
Response: 204 No Content
|
|
186
186
|
"""
|
|
187
|
-
registry = _get_registry()
|
|
187
|
+
registry = _get_registry(request)
|
|
188
188
|
|
|
189
189
|
try:
|
|
190
190
|
registry.unregister(project_id)
|
|
@@ -194,7 +194,7 @@ async def unregister_project(project_id: str) -> Response:
|
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
@router.post("/projects/{project_id}/pause", response_model=ProjectResponse)
|
|
197
|
-
async def pause_project(project_id: str) -> ProjectResponse:
|
|
197
|
+
async def pause_project(request: Request, project_id: str) -> ProjectResponse:
|
|
198
198
|
"""Pause a project.
|
|
199
199
|
|
|
200
200
|
Sets project state to PAUSED to prevent automatic work processing.
|
|
@@ -217,7 +217,7 @@ async def pause_project(project_id: str) -> ProjectResponse:
|
|
|
217
217
|
...
|
|
218
218
|
}
|
|
219
219
|
"""
|
|
220
|
-
registry = _get_registry()
|
|
220
|
+
registry = _get_registry(request)
|
|
221
221
|
project = registry.get(project_id)
|
|
222
222
|
|
|
223
223
|
if project is None:
|
|
@@ -233,7 +233,7 @@ async def pause_project(project_id: str) -> ProjectResponse:
|
|
|
233
233
|
|
|
234
234
|
|
|
235
235
|
@router.post("/projects/{project_id}/resume", response_model=ProjectResponse)
|
|
236
|
-
async def resume_project(project_id: str) -> ProjectResponse:
|
|
236
|
+
async def resume_project(request: Request, project_id: str) -> ProjectResponse:
|
|
237
237
|
"""Resume a paused project.
|
|
238
238
|
|
|
239
239
|
Sets project state back to IDLE to allow work processing.
|
|
@@ -256,7 +256,7 @@ async def resume_project(project_id: str) -> ProjectResponse:
|
|
|
256
256
|
...
|
|
257
257
|
}
|
|
258
258
|
"""
|
|
259
|
-
registry = _get_registry()
|
|
259
|
+
registry = _get_registry(request)
|
|
260
260
|
project = registry.get(project_id)
|
|
261
261
|
|
|
262
262
|
if project is None:
|
|
@@ -9,7 +9,7 @@ import subprocess # nosec B404 - needed for tmux error handling
|
|
|
9
9
|
import uuid
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
12
|
-
from fastapi import APIRouter, Response
|
|
12
|
+
from fastapi import APIRouter, Request, Response
|
|
13
13
|
|
|
14
14
|
from ...models import ToolSession
|
|
15
15
|
from ..errors import (
|
|
@@ -27,22 +27,18 @@ logger = logging.getLogger(__name__)
|
|
|
27
27
|
VALID_RUNTIMES = {"claude-code"}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def _get_registry():
|
|
31
|
-
"""Get registry instance from app
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if registry is None:
|
|
30
|
+
def _get_registry(request: Request):
|
|
31
|
+
"""Get registry instance from app.state."""
|
|
32
|
+
if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
|
|
35
33
|
raise RuntimeError("Registry not initialized")
|
|
36
|
-
return registry
|
|
37
|
-
|
|
34
|
+
return request.app.state.registry
|
|
38
35
|
|
|
39
|
-
def _get_tmux():
|
|
40
|
-
"""Get tmux orchestrator instance from app global."""
|
|
41
|
-
from ..app import tmux
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
def _get_tmux(request: Request):
|
|
38
|
+
"""Get tmux orchestrator instance from app.state."""
|
|
39
|
+
if not hasattr(request.app.state, "tmux") or request.app.state.tmux is None:
|
|
44
40
|
raise RuntimeError("Tmux orchestrator not initialized")
|
|
45
|
-
return tmux
|
|
41
|
+
return request.app.state.tmux
|
|
46
42
|
|
|
47
43
|
|
|
48
44
|
def _session_to_response(session: ToolSession) -> SessionResponse:
|
|
@@ -65,7 +61,7 @@ def _session_to_response(session: ToolSession) -> SessionResponse:
|
|
|
65
61
|
|
|
66
62
|
|
|
67
63
|
@router.get("/projects/{project_id}/sessions", response_model=List[SessionResponse])
|
|
68
|
-
async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
64
|
+
async def list_sessions(request: Request, project_id: str) -> List[SessionResponse]:
|
|
69
65
|
"""List all sessions for a project.
|
|
70
66
|
|
|
71
67
|
Args:
|
|
@@ -90,7 +86,7 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
|
90
86
|
}
|
|
91
87
|
]
|
|
92
88
|
"""
|
|
93
|
-
registry = _get_registry()
|
|
89
|
+
registry = _get_registry(request)
|
|
94
90
|
project = registry.get(project_id)
|
|
95
91
|
|
|
96
92
|
if project is None:
|
|
@@ -103,7 +99,9 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
|
103
99
|
@router.post(
|
|
104
100
|
"/projects/{project_id}/sessions", response_model=SessionResponse, status_code=201
|
|
105
101
|
)
|
|
106
|
-
async def create_session(
|
|
102
|
+
async def create_session(
|
|
103
|
+
request: Request, project_id: str, req: CreateSessionRequest
|
|
104
|
+
) -> SessionResponse:
|
|
107
105
|
"""Create a new session for a project.
|
|
108
106
|
|
|
109
107
|
Creates a new tmux pane and initializes the specified runtime adapter.
|
|
@@ -135,8 +133,8 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
135
133
|
"created_at": "2025-01-12T10:00:00Z"
|
|
136
134
|
}
|
|
137
135
|
"""
|
|
138
|
-
registry = _get_registry()
|
|
139
|
-
tmux_orch = _get_tmux()
|
|
136
|
+
registry = _get_registry(request)
|
|
137
|
+
tmux_orch = _get_tmux(request)
|
|
140
138
|
|
|
141
139
|
# Validate project exists
|
|
142
140
|
project = registry.get(project_id)
|
|
@@ -181,7 +179,7 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
181
179
|
|
|
182
180
|
|
|
183
181
|
@router.delete("/sessions/{session_id}", status_code=204)
|
|
184
|
-
async def stop_session(session_id: str) -> Response:
|
|
182
|
+
async def stop_session(request: Request, session_id: str) -> Response:
|
|
185
183
|
"""Stop and remove a session.
|
|
186
184
|
|
|
187
185
|
Kills the tmux pane and removes the session from its project.
|
|
@@ -199,8 +197,8 @@ async def stop_session(session_id: str) -> Response:
|
|
|
199
197
|
DELETE /api/sessions/sess-456
|
|
200
198
|
Response: 204 No Content
|
|
201
199
|
"""
|
|
202
|
-
registry = _get_registry()
|
|
203
|
-
tmux_orch = _get_tmux()
|
|
200
|
+
registry = _get_registry(request)
|
|
201
|
+
tmux_orch = _get_tmux(request)
|
|
204
202
|
|
|
205
203
|
# Find session across all projects
|
|
206
204
|
session = None
|