claude-mpm 5.6.23__py3-none-any.whl → 5.6.73__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/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 +6 -6
- 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/executor.py +9 -0
- claude_mpm/cli/helpers.py +1 -1
- claude_mpm/cli/parsers/base_parser.py +13 -0
- claude_mpm/cli/parsers/mcp_parser.py +79 -0
- claude_mpm/cli/parsers/oauth_parser.py +165 -0
- claude_mpm/cli/startup.py +150 -33
- claude_mpm/cli/startup_display.py +3 -2
- claude_mpm/commander/chat/cli.py +5 -2
- claude_mpm/commander/chat/commands.py +42 -16
- claude_mpm/commander/chat/repl.py +1581 -70
- claude_mpm/commander/events/manager.py +61 -1
- claude_mpm/commander/frameworks/base.py +87 -0
- 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 +428 -13
- claude_mpm/commander/models/events.py +6 -0
- claude_mpm/commander/persistence/state_store.py +95 -1
- claude_mpm/commander/tmux_orchestrator.py +3 -2
- claude_mpm/constants.py +5 -0
- claude_mpm/core/hook_manager.py +2 -1
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +34 -10
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
- claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
- claude_mpm/hooks/claude_hooks/installer.py +175 -51
- 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/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 +326 -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/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 +3 -3
- 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-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/METADATA +24 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/RECORD +69 -64
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/WHEEL +1 -1
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/entry_points.txt +2 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.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/__pycache__/tool_analysis.cpython-311.pyc +0 -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__/duplicate_detector.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-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/top_level.txt +0 -0
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
Manages event lifecycle, inbox queries, and project event tracking.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import asyncio
|
|
6
7
|
import logging
|
|
7
8
|
import threading
|
|
8
9
|
import uuid
|
|
10
|
+
from asyncio import Queue
|
|
9
11
|
from datetime import datetime, timezone
|
|
10
|
-
from typing import Any, Dict, List, Optional
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
11
13
|
|
|
12
14
|
from ..models.events import (
|
|
13
15
|
DEFAULT_PRIORITIES,
|
|
@@ -48,6 +50,8 @@ class EventManager:
|
|
|
48
50
|
self._events: Dict[str, Event] = {}
|
|
49
51
|
self._project_index: Dict[str, List[str]] = {} # project_id -> event_ids
|
|
50
52
|
self._lock = threading.RLock()
|
|
53
|
+
self._subscribers: Dict[EventType, List[Callable]] = {}
|
|
54
|
+
self._event_queue: Queue = Queue()
|
|
51
55
|
|
|
52
56
|
def create(
|
|
53
57
|
self,
|
|
@@ -330,3 +334,59 @@ class EventManager:
|
|
|
330
334
|
for eid in event_ids:
|
|
331
335
|
self._events.pop(eid, None)
|
|
332
336
|
return len(event_ids)
|
|
337
|
+
|
|
338
|
+
def subscribe(self, event_type: EventType, callback: Callable) -> None:
|
|
339
|
+
"""Subscribe callback to event type.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
event_type: Type of event to subscribe to
|
|
343
|
+
callback: Function to call when event occurs (sync or async)
|
|
344
|
+
|
|
345
|
+
Example:
|
|
346
|
+
def on_error(event):
|
|
347
|
+
print(f"Error: {event.title}")
|
|
348
|
+
|
|
349
|
+
manager.subscribe(EventType.ERROR, on_error)
|
|
350
|
+
"""
|
|
351
|
+
if event_type not in self._subscribers:
|
|
352
|
+
self._subscribers[event_type] = []
|
|
353
|
+
self._subscribers[event_type].append(callback)
|
|
354
|
+
|
|
355
|
+
def unsubscribe(self, event_type: EventType, callback: Callable) -> None:
|
|
356
|
+
"""Unsubscribe callback from event type.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
event_type: Type of event to unsubscribe from
|
|
360
|
+
callback: Function to remove from subscribers
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
manager.unsubscribe(EventType.ERROR, on_error)
|
|
364
|
+
"""
|
|
365
|
+
if (
|
|
366
|
+
event_type in self._subscribers
|
|
367
|
+
and callback in self._subscribers[event_type]
|
|
368
|
+
):
|
|
369
|
+
self._subscribers[event_type].remove(callback)
|
|
370
|
+
|
|
371
|
+
async def emit(self, event: Event) -> None:
|
|
372
|
+
"""Emit event to all subscribers.
|
|
373
|
+
|
|
374
|
+
Queues the event and notifies all subscribed callbacks.
|
|
375
|
+
Supports both sync and async callbacks.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
event: Event to emit
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
await manager.emit(event)
|
|
382
|
+
"""
|
|
383
|
+
await self._event_queue.put(event)
|
|
384
|
+
if event.type in self._subscribers:
|
|
385
|
+
for callback in self._subscribers[event.type]:
|
|
386
|
+
try:
|
|
387
|
+
if asyncio.iscoroutinefunction(callback):
|
|
388
|
+
await callback(event)
|
|
389
|
+
else:
|
|
390
|
+
callback(event)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Subscriber callback error: {e}")
|
|
@@ -42,6 +42,93 @@ class InstanceInfo:
|
|
|
42
42
|
git_branch: Optional[str] = None
|
|
43
43
|
git_status: Optional[str] = None
|
|
44
44
|
connected: bool = False
|
|
45
|
+
ready: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class RegisteredInstance:
|
|
50
|
+
"""Persistent instance configuration (survives daemon restart).
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
name: Instance identifier
|
|
54
|
+
path: Original project directory path (stored as string for JSON)
|
|
55
|
+
framework: Framework identifier ("cc" or "mpm")
|
|
56
|
+
registered_at: ISO timestamp when instance was registered
|
|
57
|
+
worktree_path: Path to git worktree (if using worktree isolation)
|
|
58
|
+
worktree_branch: Branch name in the worktree
|
|
59
|
+
use_worktree: Whether worktree isolation is enabled
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> instance = RegisteredInstance(
|
|
63
|
+
... name="myapp",
|
|
64
|
+
... path="/Users/user/myapp",
|
|
65
|
+
... framework="cc",
|
|
66
|
+
... registered_at="2024-01-15T10:30:00"
|
|
67
|
+
... )
|
|
68
|
+
>>> instance.to_dict()
|
|
69
|
+
{'name': 'myapp', 'path': '/Users/user/myapp', 'framework': 'cc', ...}
|
|
70
|
+
>>> instance.working_path
|
|
71
|
+
'/Users/user/myapp'
|
|
72
|
+
|
|
73
|
+
>>> # With worktree enabled
|
|
74
|
+
>>> instance = RegisteredInstance(
|
|
75
|
+
... name="myapp",
|
|
76
|
+
... path="/Users/user/myapp",
|
|
77
|
+
... framework="cc",
|
|
78
|
+
... registered_at="2024-01-15T10:30:00",
|
|
79
|
+
... worktree_path="/Users/user/.mpm/worktrees/myapp",
|
|
80
|
+
... worktree_branch="feature/new-feature",
|
|
81
|
+
... use_worktree=True
|
|
82
|
+
... )
|
|
83
|
+
>>> instance.working_path
|
|
84
|
+
'/Users/user/.mpm/worktrees/myapp'
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
path: str # Original project path
|
|
89
|
+
framework: str
|
|
90
|
+
registered_at: str
|
|
91
|
+
# Worktree fields
|
|
92
|
+
worktree_path: Optional[str] = None # Path to worktree (if using)
|
|
93
|
+
worktree_branch: Optional[str] = None # Branch in worktree
|
|
94
|
+
use_worktree: bool = False # Whether worktree is enabled
|
|
95
|
+
|
|
96
|
+
def to_dict(self) -> dict:
|
|
97
|
+
"""Serialize for JSON storage."""
|
|
98
|
+
return {
|
|
99
|
+
"name": self.name,
|
|
100
|
+
"path": self.path,
|
|
101
|
+
"framework": self.framework,
|
|
102
|
+
"registered_at": self.registered_at,
|
|
103
|
+
"worktree_path": self.worktree_path,
|
|
104
|
+
"worktree_branch": self.worktree_branch,
|
|
105
|
+
"use_worktree": self.use_worktree,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, data: dict) -> "RegisteredInstance":
|
|
110
|
+
"""Deserialize from JSON."""
|
|
111
|
+
return cls(
|
|
112
|
+
name=data["name"],
|
|
113
|
+
path=data["path"],
|
|
114
|
+
framework=data["framework"],
|
|
115
|
+
registered_at=data.get("registered_at", ""),
|
|
116
|
+
worktree_path=data.get("worktree_path"),
|
|
117
|
+
worktree_branch=data.get("worktree_branch"),
|
|
118
|
+
use_worktree=data.get("use_worktree", False),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def working_path(self) -> str:
|
|
123
|
+
"""Get the actual working path (worktree or original).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
The worktree path if worktree is enabled and configured,
|
|
127
|
+
otherwise the original project path.
|
|
128
|
+
"""
|
|
129
|
+
if self.use_worktree and self.worktree_path:
|
|
130
|
+
return self.worktree_path
|
|
131
|
+
return self.path
|
|
45
132
|
|
|
46
133
|
|
|
47
134
|
class BaseFramework(ABC):
|
|
@@ -10,9 +10,7 @@ from .base import BaseFramework
|
|
|
10
10
|
class MPMFramework(BaseFramework):
|
|
11
11
|
"""Claude MPM framework.
|
|
12
12
|
|
|
13
|
-
This framework launches Claude with
|
|
14
|
-
It uses the same 'claude' command as Claude Code, but relies on CLAUDE.md
|
|
15
|
-
in the project to load the MPM agent system.
|
|
13
|
+
This framework launches Claude MPM with full agent orchestration.
|
|
16
14
|
|
|
17
15
|
Example:
|
|
18
16
|
>>> framework = MPMFramework()
|
|
@@ -21,42 +19,39 @@ class MPMFramework(BaseFramework):
|
|
|
21
19
|
>>> framework.is_available()
|
|
22
20
|
True
|
|
23
21
|
>>> framework.get_startup_command(Path("/Users/user/myapp"))
|
|
24
|
-
"cd '/Users/user/myapp' && claude
|
|
22
|
+
"cd '/Users/user/myapp' && claude-mpm"
|
|
25
23
|
"""
|
|
26
24
|
|
|
27
25
|
name = "mpm"
|
|
28
26
|
display_name = "Claude MPM"
|
|
29
|
-
command = "claude"
|
|
27
|
+
command = "claude-mpm"
|
|
30
28
|
|
|
31
29
|
def get_startup_command(self, project_path: Path) -> str:
|
|
32
30
|
"""Get the command to start Claude MPM in a project.
|
|
33
31
|
|
|
34
|
-
The MPM framework uses the standard 'claude' command, but expects
|
|
35
|
-
a CLAUDE.md file in the project to load the MPM agent system.
|
|
36
|
-
|
|
37
32
|
Args:
|
|
38
33
|
project_path: Path to the project directory
|
|
39
34
|
|
|
40
35
|
Returns:
|
|
41
|
-
Shell command string to start Claude
|
|
36
|
+
Shell command string to start Claude MPM
|
|
42
37
|
|
|
43
38
|
Example:
|
|
44
39
|
>>> framework = MPMFramework()
|
|
45
40
|
>>> framework.get_startup_command(Path("/Users/user/myapp"))
|
|
46
|
-
"cd '/Users/user/myapp' && claude
|
|
41
|
+
"cd '/Users/user/myapp' && claude-mpm"
|
|
47
42
|
"""
|
|
48
43
|
quoted_path = shlex.quote(str(project_path))
|
|
49
|
-
return f"cd {quoted_path} && claude
|
|
44
|
+
return f"cd {quoted_path} && claude-mpm"
|
|
50
45
|
|
|
51
46
|
def is_available(self) -> bool:
|
|
52
|
-
"""Check if 'claude' command is available.
|
|
47
|
+
"""Check if 'claude-mpm' command is available.
|
|
53
48
|
|
|
54
49
|
Returns:
|
|
55
|
-
True if 'claude' command exists in PATH
|
|
50
|
+
True if 'claude-mpm' command exists in PATH
|
|
56
51
|
|
|
57
52
|
Example:
|
|
58
53
|
>>> framework = MPMFramework()
|
|
59
54
|
>>> framework.is_available()
|
|
60
55
|
True
|
|
61
56
|
"""
|
|
62
|
-
return shutil.which("claude") is not None
|
|
57
|
+
return shutil.which("claude-mpm") is not None
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Git worktree manager for session isolation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess # nosec B404 - subprocess needed for git commands
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class WorktreeInfo:
|
|
14
|
+
"""Info about a git worktree."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
path: Path
|
|
18
|
+
branch: str
|
|
19
|
+
base_path: Path # Original repo path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorktreeManager:
|
|
23
|
+
"""Manages git worktrees for session isolation."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, base_path: Path):
|
|
26
|
+
"""Initialize with base project path."""
|
|
27
|
+
self.base_path = Path(base_path)
|
|
28
|
+
self.worktrees_dir = self.base_path.parent / f".worktrees-{self.base_path.name}"
|
|
29
|
+
|
|
30
|
+
def create(self, name: str, branch: Optional[str] = None) -> WorktreeInfo:
|
|
31
|
+
"""Create a worktree for a session.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Session/worktree name (e.g., "izzie")
|
|
35
|
+
branch: Branch name, defaults to session-{name}
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
WorktreeInfo with path and branch details
|
|
39
|
+
"""
|
|
40
|
+
branch = branch or f"session-{name}"
|
|
41
|
+
worktree_path = self.worktrees_dir / name
|
|
42
|
+
|
|
43
|
+
# Ensure worktrees directory exists
|
|
44
|
+
self.worktrees_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# Check if worktree already exists
|
|
47
|
+
if worktree_path.exists():
|
|
48
|
+
logger.info(f"Worktree '{name}' already exists at {worktree_path}")
|
|
49
|
+
return self._get_worktree_info(name, worktree_path)
|
|
50
|
+
|
|
51
|
+
# Create new branch if it doesn't exist
|
|
52
|
+
try:
|
|
53
|
+
subprocess.run( # nosec B603 B607 - git command with safe args
|
|
54
|
+
["git", "branch", branch],
|
|
55
|
+
cwd=self.base_path,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
check=False, # Branch may already exist
|
|
58
|
+
)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.warning(f"Could not create branch {branch}: {e}")
|
|
61
|
+
|
|
62
|
+
# Create worktree
|
|
63
|
+
result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
64
|
+
["git", "worktree", "add", str(worktree_path), branch],
|
|
65
|
+
check=False,
|
|
66
|
+
cwd=self.base_path,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
raise RuntimeError(f"Failed to create worktree: {result.stderr}")
|
|
73
|
+
|
|
74
|
+
logger.info(f"Created worktree '{name}' at {worktree_path} on branch {branch}")
|
|
75
|
+
return WorktreeInfo(
|
|
76
|
+
name=name, path=worktree_path, branch=branch, base_path=self.base_path
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _get_worktree_info(self, name: str, path: Path) -> WorktreeInfo:
|
|
80
|
+
"""Get info for existing worktree."""
|
|
81
|
+
result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
82
|
+
["git", "branch", "--show-current"],
|
|
83
|
+
check=False,
|
|
84
|
+
cwd=path,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
)
|
|
88
|
+
branch = result.stdout.strip() or f"session-{name}"
|
|
89
|
+
return WorktreeInfo(
|
|
90
|
+
name=name, path=path, branch=branch, base_path=self.base_path
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def list(self) -> list[WorktreeInfo]:
|
|
94
|
+
"""List all worktrees for this project."""
|
|
95
|
+
result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
96
|
+
["git", "worktree", "list", "--porcelain"],
|
|
97
|
+
check=False,
|
|
98
|
+
cwd=self.base_path,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
worktrees = []
|
|
104
|
+
current_path = None
|
|
105
|
+
current_branch = None
|
|
106
|
+
|
|
107
|
+
for line in result.stdout.strip().split("\n"):
|
|
108
|
+
if line.startswith("worktree "):
|
|
109
|
+
current_path = Path(line.split(" ", 1)[1])
|
|
110
|
+
elif line.startswith("branch "):
|
|
111
|
+
current_branch = line.split("/")[-1] # refs/heads/branch -> branch
|
|
112
|
+
elif line == "":
|
|
113
|
+
if current_path and str(self.worktrees_dir) in str(current_path):
|
|
114
|
+
name = current_path.name
|
|
115
|
+
worktrees.append(
|
|
116
|
+
WorktreeInfo(
|
|
117
|
+
name=name,
|
|
118
|
+
path=current_path,
|
|
119
|
+
branch=current_branch or f"session-{name}",
|
|
120
|
+
base_path=self.base_path,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
current_path = None
|
|
124
|
+
current_branch = None
|
|
125
|
+
|
|
126
|
+
return worktrees
|
|
127
|
+
|
|
128
|
+
def remove(self, name: str, force: bool = False) -> bool:
|
|
129
|
+
"""Remove a worktree."""
|
|
130
|
+
worktree_path = self.worktrees_dir / name
|
|
131
|
+
|
|
132
|
+
if not worktree_path.exists():
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
args = ["git", "worktree", "remove", str(worktree_path)]
|
|
136
|
+
if force:
|
|
137
|
+
args.insert(3, "--force")
|
|
138
|
+
|
|
139
|
+
result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
140
|
+
args, check=False, cwd=self.base_path, capture_output=True, text=True
|
|
141
|
+
)
|
|
142
|
+
return result.returncode == 0
|
|
143
|
+
|
|
144
|
+
def get(self, name: str) -> Optional[WorktreeInfo]:
|
|
145
|
+
"""Get worktree by name."""
|
|
146
|
+
worktree_path = self.worktrees_dir / name
|
|
147
|
+
if worktree_path.exists():
|
|
148
|
+
return self._get_worktree_info(name, worktree_path)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def merge_to_main(self, name: str, delete_after: bool = True) -> tuple[bool, str]:
|
|
152
|
+
"""Merge worktree branch back to main and optionally delete worktree.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: Worktree name to merge
|
|
156
|
+
delete_after: Remove worktree after merge (default True)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (success, message)
|
|
160
|
+
"""
|
|
161
|
+
worktree = self.get(name)
|
|
162
|
+
if not worktree:
|
|
163
|
+
return False, f"Worktree '{name}' not found"
|
|
164
|
+
|
|
165
|
+
# Get main branch name
|
|
166
|
+
result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
167
|
+
["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
168
|
+
check=False,
|
|
169
|
+
cwd=self.base_path,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
)
|
|
173
|
+
main_branch = (
|
|
174
|
+
result.stdout.strip().split("/")[-1] if result.returncode == 0 else "main"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Checkout main in base repo
|
|
178
|
+
checkout_result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
179
|
+
["git", "checkout", main_branch],
|
|
180
|
+
check=False,
|
|
181
|
+
cwd=self.base_path,
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
)
|
|
185
|
+
if checkout_result.returncode != 0:
|
|
186
|
+
return False, f"Failed to checkout {main_branch}: {checkout_result.stderr}"
|
|
187
|
+
|
|
188
|
+
# Merge worktree branch
|
|
189
|
+
merge_result = subprocess.run( # nosec B603 B607 - git command with safe args
|
|
190
|
+
["git", "merge", worktree.branch, "--no-edit"],
|
|
191
|
+
check=False,
|
|
192
|
+
cwd=self.base_path,
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if merge_result.returncode != 0:
|
|
198
|
+
return False, f"Merge failed: {merge_result.stderr}"
|
|
199
|
+
|
|
200
|
+
# Delete worktree if requested
|
|
201
|
+
if delete_after:
|
|
202
|
+
self.remove(name, force=True)
|
|
203
|
+
# Delete branch
|
|
204
|
+
subprocess.run( # nosec B603 B607 - git command with safe args
|
|
205
|
+
["git", "branch", "-d", worktree.branch],
|
|
206
|
+
check=False,
|
|
207
|
+
cwd=self.base_path,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
logger.info(f"Merged '{worktree.branch}' to {main_branch}")
|
|
212
|
+
return True, f"Merged '{worktree.branch}' to {main_branch}"
|