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.

Files changed (82) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/auth/__init__.py +35 -0
  3. claude_mpm/auth/callback_server.py +328 -0
  4. claude_mpm/auth/models.py +104 -0
  5. claude_mpm/auth/oauth_manager.py +266 -0
  6. claude_mpm/auth/providers/__init__.py +12 -0
  7. claude_mpm/auth/providers/base.py +165 -0
  8. claude_mpm/auth/providers/google.py +261 -0
  9. claude_mpm/auth/token_storage.py +252 -0
  10. claude_mpm/cli/commands/commander.py +6 -6
  11. claude_mpm/cli/commands/mcp.py +29 -17
  12. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  13. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  14. claude_mpm/cli/commands/oauth.py +481 -0
  15. claude_mpm/cli/executor.py +9 -0
  16. claude_mpm/cli/helpers.py +1 -1
  17. claude_mpm/cli/parsers/base_parser.py +13 -0
  18. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  19. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  20. claude_mpm/cli/startup.py +150 -33
  21. claude_mpm/cli/startup_display.py +3 -2
  22. claude_mpm/commander/chat/cli.py +5 -2
  23. claude_mpm/commander/chat/commands.py +42 -16
  24. claude_mpm/commander/chat/repl.py +1581 -70
  25. claude_mpm/commander/events/manager.py +61 -1
  26. claude_mpm/commander/frameworks/base.py +87 -0
  27. claude_mpm/commander/frameworks/mpm.py +9 -14
  28. claude_mpm/commander/git/__init__.py +5 -0
  29. claude_mpm/commander/git/worktree_manager.py +212 -0
  30. claude_mpm/commander/instance_manager.py +428 -13
  31. claude_mpm/commander/models/events.py +6 -0
  32. claude_mpm/commander/persistence/state_store.py +95 -1
  33. claude_mpm/commander/tmux_orchestrator.py +3 -2
  34. claude_mpm/constants.py +5 -0
  35. claude_mpm/core/hook_manager.py +2 -1
  36. claude_mpm/core/logging_utils.py +4 -2
  37. claude_mpm/core/output_style_manager.py +5 -2
  38. claude_mpm/core/socketio_pool.py +34 -10
  39. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
  40. claude_mpm/hooks/claude_hooks/event_handlers.py +206 -94
  41. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  42. claude_mpm/hooks/claude_hooks/installer.py +175 -51
  43. claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
  44. claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
  45. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  46. claude_mpm/hooks/claude_hooks/services/connection_manager.py +2 -2
  47. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +2 -2
  48. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  49. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  50. claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
  51. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
  52. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  53. claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
  54. claude_mpm/init.py +21 -14
  55. claude_mpm/mcp/__init__.py +9 -0
  56. claude_mpm/mcp/google_workspace_server.py +610 -0
  57. claude_mpm/scripts/claude-hook-handler.sh +3 -3
  58. claude_mpm/services/command_deployment_service.py +44 -26
  59. claude_mpm/services/hook_installer_service.py +77 -8
  60. claude_mpm/services/mcp_config_manager.py +99 -19
  61. claude_mpm/services/mcp_service_registry.py +294 -0
  62. claude_mpm/services/monitor/server.py +6 -1
  63. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/METADATA +24 -1
  64. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/RECORD +69 -64
  65. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/WHEEL +1 -1
  66. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/entry_points.txt +2 -0
  67. claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  68. claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-311.pyc +0 -0
  69. claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-311.pyc +0 -0
  70. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  71. claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
  72. claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
  73. claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
  74. claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-311.pyc +0 -0
  75. claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
  76. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
  77. claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-311.pyc +0 -0
  78. claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-311.pyc +0 -0
  79. claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-311.pyc +0 -0
  80. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE +0 -0
  81. {claude_mpm-5.6.23.dist-info → claude_mpm-5.6.73.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  82. {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 MPM agent loading via CLAUDE.md.
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 --dangerously-skip-permissions"
22
+ "cd '/Users/user/myapp' && claude-mpm"
25
23
  """
26
24
 
27
25
  name = "mpm"
28
26
  display_name = "Claude MPM"
29
- command = "claude" # Uses claude with MPM agent loading
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 with MPM
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 --dangerously-skip-permissions"
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 --dangerously-skip-permissions"
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,5 @@
1
+ """Git operations for Commander."""
2
+
3
+ from .worktree_manager import WorktreeInfo, WorktreeManager
4
+
5
+ __all__ = ["WorktreeInfo", "WorktreeManager"]
@@ -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}"