claude-mpm 5.6.10__py3-none-any.whl → 5.6.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (61) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +173 -3
  3. claude_mpm/cli/parsers/commander_parser.py +41 -8
  4. claude_mpm/cli/startup.py +104 -1
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +4 -0
  22. claude_mpm/commander/core/__init__.py +10 -0
  23. claude_mpm/commander/core/block_manager.py +325 -0
  24. claude_mpm/commander/core/response_manager.py +323 -0
  25. claude_mpm/commander/daemon.py +206 -10
  26. claude_mpm/commander/env_loader.py +59 -0
  27. claude_mpm/commander/memory/__init__.py +45 -0
  28. claude_mpm/commander/memory/compression.py +347 -0
  29. claude_mpm/commander/memory/embeddings.py +230 -0
  30. claude_mpm/commander/memory/entities.py +310 -0
  31. claude_mpm/commander/memory/example_usage.py +290 -0
  32. claude_mpm/commander/memory/integration.py +325 -0
  33. claude_mpm/commander/memory/search.py +381 -0
  34. claude_mpm/commander/memory/store.py +657 -0
  35. claude_mpm/commander/registry.py +10 -4
  36. claude_mpm/commander/runtime/monitor.py +32 -2
  37. claude_mpm/commander/work/executor.py +38 -20
  38. claude_mpm/commander/workflow/event_handler.py +25 -3
  39. claude_mpm/core/claude_runner.py +143 -0
  40. claude_mpm/core/output_style_manager.py +34 -7
  41. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +22 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
  47. claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
  48. claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
  49. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  50. claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/skills/__init__.py +2 -1
  53. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  54. claude_mpm/skills/registry.py +295 -90
  55. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +5 -3
  56. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +55 -36
  57. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  58. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  61. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.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}")
@@ -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
 
@@ -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 global."""
21
- from ..app import registry
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(project_id: str, req: SendMessageRequest) -> MessageResponse:
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 global."""
21
- from ..app import registry
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(req: RegisterProjectRequest) -> ProjectResponse:
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 global."""
32
- from ..app import registry
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
- if tmux is None:
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(project_id: str, req: CreateSessionRequest) -> SessionResponse:
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