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
|
@@ -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:
|
|
@@ -5,13 +5,19 @@ This module implements REST endpoints for creating and managing tool sessions
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
|
+
import subprocess # nosec B404 - needed for tmux error handling
|
|
8
9
|
import uuid
|
|
9
10
|
from typing import List
|
|
10
11
|
|
|
11
|
-
from fastapi import APIRouter, Response
|
|
12
|
+
from fastapi import APIRouter, Request, Response
|
|
12
13
|
|
|
13
14
|
from ...models import ToolSession
|
|
14
|
-
from ..errors import
|
|
15
|
+
from ..errors import (
|
|
16
|
+
InvalidRuntimeError,
|
|
17
|
+
ProjectNotFoundError,
|
|
18
|
+
SessionNotFoundError,
|
|
19
|
+
TmuxNoSpaceError,
|
|
20
|
+
)
|
|
15
21
|
from ..schemas import CreateSessionRequest, SessionResponse
|
|
16
22
|
|
|
17
23
|
router = APIRouter()
|
|
@@ -21,22 +27,18 @@ logger = logging.getLogger(__name__)
|
|
|
21
27
|
VALID_RUNTIMES = {"claude-code"}
|
|
22
28
|
|
|
23
29
|
|
|
24
|
-
def _get_registry():
|
|
25
|
-
"""Get registry instance from app
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
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:
|
|
29
33
|
raise RuntimeError("Registry not initialized")
|
|
30
|
-
return registry
|
|
31
|
-
|
|
34
|
+
return request.app.state.registry
|
|
32
35
|
|
|
33
|
-
def _get_tmux():
|
|
34
|
-
"""Get tmux orchestrator instance from app global."""
|
|
35
|
-
from ..app import tmux
|
|
36
36
|
|
|
37
|
-
|
|
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:
|
|
38
40
|
raise RuntimeError("Tmux orchestrator not initialized")
|
|
39
|
-
return tmux
|
|
41
|
+
return request.app.state.tmux
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def _session_to_response(session: ToolSession) -> SessionResponse:
|
|
@@ -59,7 +61,7 @@ def _session_to_response(session: ToolSession) -> SessionResponse:
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
@router.get("/projects/{project_id}/sessions", response_model=List[SessionResponse])
|
|
62
|
-
async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
64
|
+
async def list_sessions(request: Request, project_id: str) -> List[SessionResponse]:
|
|
63
65
|
"""List all sessions for a project.
|
|
64
66
|
|
|
65
67
|
Args:
|
|
@@ -84,7 +86,7 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
|
84
86
|
}
|
|
85
87
|
]
|
|
86
88
|
"""
|
|
87
|
-
registry = _get_registry()
|
|
89
|
+
registry = _get_registry(request)
|
|
88
90
|
project = registry.get(project_id)
|
|
89
91
|
|
|
90
92
|
if project is None:
|
|
@@ -97,7 +99,9 @@ async def list_sessions(project_id: str) -> List[SessionResponse]:
|
|
|
97
99
|
@router.post(
|
|
98
100
|
"/projects/{project_id}/sessions", response_model=SessionResponse, status_code=201
|
|
99
101
|
)
|
|
100
|
-
async def create_session(
|
|
102
|
+
async def create_session(
|
|
103
|
+
request: Request, project_id: str, req: CreateSessionRequest
|
|
104
|
+
) -> SessionResponse:
|
|
101
105
|
"""Create a new session for a project.
|
|
102
106
|
|
|
103
107
|
Creates a new tmux pane and initializes the specified runtime adapter.
|
|
@@ -112,6 +116,7 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
112
116
|
Raises:
|
|
113
117
|
ProjectNotFoundError: If project_id doesn't exist
|
|
114
118
|
InvalidRuntimeError: If runtime is not supported
|
|
119
|
+
TmuxNoSpaceError: If tmux has no space for new pane
|
|
115
120
|
|
|
116
121
|
Example:
|
|
117
122
|
POST /api/projects/abc-123/sessions
|
|
@@ -128,8 +133,8 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
128
133
|
"created_at": "2025-01-12T10:00:00Z"
|
|
129
134
|
}
|
|
130
135
|
"""
|
|
131
|
-
registry = _get_registry()
|
|
132
|
-
tmux_orch = _get_tmux()
|
|
136
|
+
registry = _get_registry(request)
|
|
137
|
+
tmux_orch = _get_tmux(request)
|
|
133
138
|
|
|
134
139
|
# Validate project exists
|
|
135
140
|
project = registry.get(project_id)
|
|
@@ -144,10 +149,16 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
144
149
|
session_id = str(uuid.uuid4())
|
|
145
150
|
|
|
146
151
|
# Create tmux pane for session
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
try:
|
|
153
|
+
tmux_target = tmux_orch.create_pane(
|
|
154
|
+
pane_id=f"{project.name}-{req.runtime}",
|
|
155
|
+
working_dir=project.path,
|
|
156
|
+
)
|
|
157
|
+
except subprocess.CalledProcessError as e:
|
|
158
|
+
stderr = e.stderr.decode() if e.stderr else ""
|
|
159
|
+
if "no space for new pane" in stderr.lower():
|
|
160
|
+
raise TmuxNoSpaceError() from None
|
|
161
|
+
raise # Re-raise other subprocess errors
|
|
151
162
|
|
|
152
163
|
# Create session object
|
|
153
164
|
session = ToolSession(
|
|
@@ -168,7 +179,7 @@ async def create_session(project_id: str, req: CreateSessionRequest) -> SessionR
|
|
|
168
179
|
|
|
169
180
|
|
|
170
181
|
@router.delete("/sessions/{session_id}", status_code=204)
|
|
171
|
-
async def stop_session(session_id: str) -> Response:
|
|
182
|
+
async def stop_session(request: Request, session_id: str) -> Response:
|
|
172
183
|
"""Stop and remove a session.
|
|
173
184
|
|
|
174
185
|
Kills the tmux pane and removes the session from its project.
|
|
@@ -186,8 +197,8 @@ async def stop_session(session_id: str) -> Response:
|
|
|
186
197
|
DELETE /api/sessions/sess-456
|
|
187
198
|
Response: 204 No Content
|
|
188
199
|
"""
|
|
189
|
-
registry = _get_registry()
|
|
190
|
-
tmux_orch = _get_tmux()
|
|
200
|
+
registry = _get_registry(request)
|
|
201
|
+
tmux_orch = _get_tmux(request)
|
|
191
202
|
|
|
192
203
|
# Find session across all projects
|
|
193
204
|
session = None
|
|
@@ -4,25 +4,39 @@ This module implements REST endpoints for managing work items
|
|
|
4
4
|
in project work queues.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import List, Optional
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
8
|
|
|
9
|
-
from fastapi import APIRouter, HTTPException, Query
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
10
10
|
|
|
11
11
|
from ...models.work import WorkPriority, WorkState
|
|
12
12
|
from ...work import WorkQueue
|
|
13
|
-
from ..errors import ProjectNotFoundError
|
|
14
13
|
from ..schemas import CreateWorkRequest, WorkItemResponse
|
|
15
14
|
|
|
16
15
|
router = APIRouter()
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
def _get_registry():
|
|
20
|
-
"""Get registry instance from app
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if registry is None:
|
|
18
|
+
def _get_registry(request: Request):
|
|
19
|
+
"""Get registry instance from app.state."""
|
|
20
|
+
if not hasattr(request.app.state, "registry") or request.app.state.registry is None:
|
|
24
21
|
raise RuntimeError("Registry not initialized")
|
|
25
|
-
return registry
|
|
22
|
+
return request.app.state.registry
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_work_queues(request: Request) -> Dict:
|
|
26
|
+
"""Get work queues dict from app.state (shared with daemon)."""
|
|
27
|
+
if (
|
|
28
|
+
not hasattr(request.app.state, "work_queues")
|
|
29
|
+
or request.app.state.work_queues is None
|
|
30
|
+
):
|
|
31
|
+
raise RuntimeError("Work queues not initialized")
|
|
32
|
+
return request.app.state.work_queues
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_daemon(request: Request):
|
|
36
|
+
"""Get daemon instance from app.state."""
|
|
37
|
+
if not hasattr(request.app.state, "daemon_instance"):
|
|
38
|
+
return None
|
|
39
|
+
return request.app.state.daemon_instance
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
def _work_item_to_response(work_item) -> WorkItemResponse:
|
|
@@ -51,10 +65,13 @@ def _work_item_to_response(work_item) -> WorkItemResponse:
|
|
|
51
65
|
|
|
52
66
|
|
|
53
67
|
@router.post("/projects/{project_id}/work", response_model=WorkItemResponse)
|
|
54
|
-
async def add_work(
|
|
68
|
+
async def add_work(
|
|
69
|
+
request: Request, project_id: str, work: CreateWorkRequest
|
|
70
|
+
) -> WorkItemResponse:
|
|
55
71
|
"""Add work item to project queue.
|
|
56
72
|
|
|
57
73
|
Args:
|
|
74
|
+
request: FastAPI request (for accessing app.state)
|
|
58
75
|
project_id: Project identifier
|
|
59
76
|
work: Work item creation request
|
|
60
77
|
|
|
@@ -80,22 +97,29 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
|
|
|
80
97
|
...
|
|
81
98
|
}
|
|
82
99
|
"""
|
|
83
|
-
registry = _get_registry()
|
|
100
|
+
registry = _get_registry(request)
|
|
101
|
+
work_queues = _get_work_queues(request)
|
|
102
|
+
daemon = _get_daemon(request)
|
|
84
103
|
|
|
85
104
|
# Get project
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
105
|
+
project = registry.get(project_id)
|
|
106
|
+
if project is None:
|
|
107
|
+
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
90
108
|
|
|
91
|
-
# Get or create work queue
|
|
92
|
-
|
|
93
|
-
# For now, we'll need to integrate with project's work queue
|
|
94
|
-
# Access or create work queue
|
|
95
|
-
if not hasattr(project, "_work_queue"):
|
|
96
|
-
project._work_queue = WorkQueue(project_id)
|
|
109
|
+
# Get or create work queue (shared with daemon)
|
|
110
|
+
import logging
|
|
97
111
|
|
|
98
|
-
|
|
112
|
+
logger = logging.getLogger(__name__)
|
|
113
|
+
logger.info(
|
|
114
|
+
f"work_queues dict id: {id(work_queues)}, keys: {list(work_queues.keys())}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if project_id not in work_queues:
|
|
118
|
+
logger.info(f"Creating new work queue for {project_id}")
|
|
119
|
+
work_queues[project_id] = WorkQueue(project_id)
|
|
120
|
+
logger.info(f"After creation, work_queues keys: {list(work_queues.keys())}")
|
|
121
|
+
|
|
122
|
+
queue = work_queues[project_id]
|
|
99
123
|
|
|
100
124
|
# Convert priority int to enum
|
|
101
125
|
priority = WorkPriority(work.priority)
|
|
@@ -105,16 +129,25 @@ async def add_work(project_id: str, work: CreateWorkRequest) -> WorkItemResponse
|
|
|
105
129
|
content=work.content, priority=priority, depends_on=work.depends_on
|
|
106
130
|
)
|
|
107
131
|
|
|
132
|
+
# Ensure daemon has a session for this project (creates if needed)
|
|
133
|
+
if daemon and not daemon.sessions.get(project_id):
|
|
134
|
+
# Session creation will be handled by daemon's main loop
|
|
135
|
+
# when it detects work in the queue
|
|
136
|
+
pass
|
|
137
|
+
|
|
108
138
|
return _work_item_to_response(work_item)
|
|
109
139
|
|
|
110
140
|
|
|
111
141
|
@router.get("/projects/{project_id}/work", response_model=List[WorkItemResponse])
|
|
112
142
|
async def list_work(
|
|
113
|
-
|
|
143
|
+
request: Request,
|
|
144
|
+
project_id: str,
|
|
145
|
+
state: Optional[str] = Query(None, description="Filter by state"),
|
|
114
146
|
) -> List[WorkItemResponse]:
|
|
115
147
|
"""List work items for project.
|
|
116
148
|
|
|
117
149
|
Args:
|
|
150
|
+
request: FastAPI request (for accessing app.state)
|
|
118
151
|
project_id: Project identifier
|
|
119
152
|
state: Optional state filter (pending, queued, in_progress, etc.)
|
|
120
153
|
|
|
@@ -136,19 +169,20 @@ async def list_work(
|
|
|
136
169
|
{"id": "work-1", "state": "queued", ...}
|
|
137
170
|
]
|
|
138
171
|
"""
|
|
139
|
-
registry = _get_registry()
|
|
172
|
+
registry = _get_registry(request)
|
|
173
|
+
work_queues = _get_work_queues(request)
|
|
140
174
|
|
|
141
175
|
# Get project
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
176
|
+
project = registry.get(project_id)
|
|
177
|
+
if project is None:
|
|
178
|
+
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
146
179
|
|
|
147
|
-
# Get work queue
|
|
148
|
-
if not
|
|
149
|
-
|
|
180
|
+
# Get work queue (shared with daemon)
|
|
181
|
+
if project_id not in work_queues:
|
|
182
|
+
# Return empty list if no work queue exists yet
|
|
183
|
+
return []
|
|
150
184
|
|
|
151
|
-
queue =
|
|
185
|
+
queue = work_queues[project_id]
|
|
152
186
|
|
|
153
187
|
# Parse state filter
|
|
154
188
|
state_filter = None
|
|
@@ -169,10 +203,11 @@ async def list_work(
|
|
|
169
203
|
|
|
170
204
|
|
|
171
205
|
@router.get("/projects/{project_id}/work/{work_id}", response_model=WorkItemResponse)
|
|
172
|
-
async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
|
|
206
|
+
async def get_work(request: Request, project_id: str, work_id: str) -> WorkItemResponse:
|
|
173
207
|
"""Get work item details.
|
|
174
208
|
|
|
175
209
|
Args:
|
|
210
|
+
request: FastAPI request (for accessing app.state)
|
|
176
211
|
project_id: Project identifier
|
|
177
212
|
work_id: Work item identifier
|
|
178
213
|
|
|
@@ -191,19 +226,19 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
|
|
|
191
226
|
...
|
|
192
227
|
}
|
|
193
228
|
"""
|
|
194
|
-
registry = _get_registry()
|
|
229
|
+
registry = _get_registry(request)
|
|
230
|
+
work_queues = _get_work_queues(request)
|
|
195
231
|
|
|
196
232
|
# Get project
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
233
|
+
project = registry.get(project_id)
|
|
234
|
+
if project is None:
|
|
235
|
+
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
201
236
|
|
|
202
|
-
# Get work queue
|
|
203
|
-
if not
|
|
237
|
+
# Get work queue (shared with daemon)
|
|
238
|
+
if project_id not in work_queues:
|
|
204
239
|
raise HTTPException(status_code=404, detail="Work queue not found")
|
|
205
240
|
|
|
206
|
-
queue =
|
|
241
|
+
queue = work_queues[project_id]
|
|
207
242
|
|
|
208
243
|
# Get work item
|
|
209
244
|
work_item = queue.get(work_id)
|
|
@@ -214,10 +249,11 @@ async def get_work(project_id: str, work_id: str) -> WorkItemResponse:
|
|
|
214
249
|
|
|
215
250
|
|
|
216
251
|
@router.post("/projects/{project_id}/work/{work_id}/cancel")
|
|
217
|
-
async def cancel_work(project_id: str, work_id: str) -> dict:
|
|
252
|
+
async def cancel_work(request: Request, project_id: str, work_id: str) -> dict:
|
|
218
253
|
"""Cancel pending work item.
|
|
219
254
|
|
|
220
255
|
Args:
|
|
256
|
+
request: FastAPI request (for accessing app.state)
|
|
221
257
|
project_id: Project identifier
|
|
222
258
|
work_id: Work item identifier
|
|
223
259
|
|
|
@@ -231,19 +267,19 @@ async def cancel_work(project_id: str, work_id: str) -> dict:
|
|
|
231
267
|
POST /api/projects/proj-123/work/work-xyz/cancel
|
|
232
268
|
Response: {"status": "cancelled", "id": "work-xyz"}
|
|
233
269
|
"""
|
|
234
|
-
registry = _get_registry()
|
|
270
|
+
registry = _get_registry(request)
|
|
271
|
+
work_queues = _get_work_queues(request)
|
|
235
272
|
|
|
236
273
|
# Get project
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
274
|
+
project = registry.get(project_id)
|
|
275
|
+
if project is None:
|
|
276
|
+
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
|
|
241
277
|
|
|
242
|
-
# Get work queue
|
|
243
|
-
if not
|
|
278
|
+
# Get work queue (shared with daemon)
|
|
279
|
+
if project_id not in work_queues:
|
|
244
280
|
raise HTTPException(status_code=404, detail="Work queue not found")
|
|
245
281
|
|
|
246
|
-
queue =
|
|
282
|
+
queue = work_queues[project_id]
|
|
247
283
|
|
|
248
284
|
# Cancel work item
|
|
249
285
|
if not queue.cancel(work_id):
|
|
@@ -17,10 +17,14 @@ class RegisterProjectRequest(BaseModel):
|
|
|
17
17
|
|
|
18
18
|
Attributes:
|
|
19
19
|
path: Filesystem path to project directory
|
|
20
|
+
project_id: Optional project identifier (UUID generated if omitted)
|
|
20
21
|
name: Optional display name (derived from path if omitted)
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
path: str = Field(..., description="Filesystem path to project")
|
|
25
|
+
project_id: Optional[str] = Field(
|
|
26
|
+
None, description="Project identifier (UUID generated if omitted)"
|
|
27
|
+
)
|
|
24
28
|
name: Optional[str] = Field(
|
|
25
29
|
None, description="Display name (derived from path if omitted)"
|
|
26
30
|
)
|