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.
Files changed (131) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/PM_INSTRUCTIONS.md +8 -3
  3. claude_mpm/auth/__init__.py +35 -0
  4. claude_mpm/auth/callback_server.py +328 -0
  5. claude_mpm/auth/models.py +104 -0
  6. claude_mpm/auth/oauth_manager.py +266 -0
  7. claude_mpm/auth/providers/__init__.py +12 -0
  8. claude_mpm/auth/providers/base.py +165 -0
  9. claude_mpm/auth/providers/google.py +261 -0
  10. claude_mpm/auth/token_storage.py +252 -0
  11. claude_mpm/cli/commands/commander.py +174 -4
  12. claude_mpm/cli/commands/mcp.py +29 -17
  13. claude_mpm/cli/commands/mcp_command_router.py +39 -0
  14. claude_mpm/cli/commands/mcp_service_commands.py +304 -0
  15. claude_mpm/cli/commands/oauth.py +481 -0
  16. claude_mpm/cli/commands/skill_source.py +51 -2
  17. claude_mpm/cli/commands/skills.py +5 -3
  18. claude_mpm/cli/executor.py +9 -0
  19. claude_mpm/cli/helpers.py +1 -1
  20. claude_mpm/cli/parsers/base_parser.py +13 -0
  21. claude_mpm/cli/parsers/commander_parser.py +43 -10
  22. claude_mpm/cli/parsers/mcp_parser.py +79 -0
  23. claude_mpm/cli/parsers/oauth_parser.py +165 -0
  24. claude_mpm/cli/parsers/skill_source_parser.py +4 -0
  25. claude_mpm/cli/parsers/skills_parser.py +5 -0
  26. claude_mpm/cli/startup.py +300 -33
  27. claude_mpm/cli/startup_display.py +4 -2
  28. claude_mpm/cli/startup_migrations.py +236 -0
  29. claude_mpm/commander/__init__.py +6 -0
  30. claude_mpm/commander/adapters/__init__.py +32 -3
  31. claude_mpm/commander/adapters/auggie.py +260 -0
  32. claude_mpm/commander/adapters/base.py +98 -1
  33. claude_mpm/commander/adapters/claude_code.py +32 -1
  34. claude_mpm/commander/adapters/codex.py +237 -0
  35. claude_mpm/commander/adapters/example_usage.py +310 -0
  36. claude_mpm/commander/adapters/mpm.py +389 -0
  37. claude_mpm/commander/adapters/registry.py +204 -0
  38. claude_mpm/commander/api/app.py +32 -16
  39. claude_mpm/commander/api/errors.py +21 -0
  40. claude_mpm/commander/api/routes/messages.py +11 -11
  41. claude_mpm/commander/api/routes/projects.py +20 -20
  42. claude_mpm/commander/api/routes/sessions.py +37 -26
  43. claude_mpm/commander/api/routes/work.py +86 -50
  44. claude_mpm/commander/api/schemas.py +4 -0
  45. claude_mpm/commander/chat/cli.py +47 -5
  46. claude_mpm/commander/chat/commands.py +44 -16
  47. claude_mpm/commander/chat/repl.py +1729 -82
  48. claude_mpm/commander/config.py +5 -3
  49. claude_mpm/commander/core/__init__.py +10 -0
  50. claude_mpm/commander/core/block_manager.py +325 -0
  51. claude_mpm/commander/core/response_manager.py +323 -0
  52. claude_mpm/commander/daemon.py +215 -10
  53. claude_mpm/commander/env_loader.py +59 -0
  54. claude_mpm/commander/events/manager.py +61 -1
  55. claude_mpm/commander/frameworks/base.py +91 -1
  56. claude_mpm/commander/frameworks/mpm.py +9 -14
  57. claude_mpm/commander/git/__init__.py +5 -0
  58. claude_mpm/commander/git/worktree_manager.py +212 -0
  59. claude_mpm/commander/instance_manager.py +546 -15
  60. claude_mpm/commander/memory/__init__.py +45 -0
  61. claude_mpm/commander/memory/compression.py +347 -0
  62. claude_mpm/commander/memory/embeddings.py +230 -0
  63. claude_mpm/commander/memory/entities.py +310 -0
  64. claude_mpm/commander/memory/example_usage.py +290 -0
  65. claude_mpm/commander/memory/integration.py +325 -0
  66. claude_mpm/commander/memory/search.py +381 -0
  67. claude_mpm/commander/memory/store.py +657 -0
  68. claude_mpm/commander/models/events.py +6 -0
  69. claude_mpm/commander/persistence/state_store.py +95 -1
  70. claude_mpm/commander/registry.py +10 -4
  71. claude_mpm/commander/runtime/monitor.py +32 -2
  72. claude_mpm/commander/tmux_orchestrator.py +3 -2
  73. claude_mpm/commander/work/executor.py +38 -20
  74. claude_mpm/commander/workflow/event_handler.py +25 -3
  75. claude_mpm/config/skill_sources.py +16 -0
  76. claude_mpm/constants.py +5 -0
  77. claude_mpm/core/claude_runner.py +152 -0
  78. claude_mpm/core/config.py +30 -22
  79. claude_mpm/core/config_constants.py +74 -9
  80. claude_mpm/core/constants.py +56 -12
  81. claude_mpm/core/hook_manager.py +2 -1
  82. claude_mpm/core/interactive_session.py +5 -4
  83. claude_mpm/core/logger.py +16 -2
  84. claude_mpm/core/logging_utils.py +40 -16
  85. claude_mpm/core/network_config.py +148 -0
  86. claude_mpm/core/oneshot_session.py +7 -6
  87. claude_mpm/core/output_style_manager.py +37 -7
  88. claude_mpm/core/socketio_pool.py +47 -15
  89. claude_mpm/core/unified_paths.py +68 -80
  90. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +30 -31
  91. claude_mpm/hooks/claude_hooks/event_handlers.py +285 -194
  92. claude_mpm/hooks/claude_hooks/hook_handler.py +115 -32
  93. claude_mpm/hooks/claude_hooks/installer.py +222 -54
  94. claude_mpm/hooks/claude_hooks/memory_integration.py +52 -32
  95. claude_mpm/hooks/claude_hooks/response_tracking.py +40 -59
  96. claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
  97. claude_mpm/hooks/claude_hooks/services/connection_manager.py +25 -30
  98. claude_mpm/hooks/claude_hooks/services/connection_manager_http.py +24 -28
  99. claude_mpm/hooks/claude_hooks/services/container.py +326 -0
  100. claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
  101. claude_mpm/hooks/claude_hooks/services/state_manager.py +25 -38
  102. claude_mpm/hooks/claude_hooks/services/subagent_processor.py +49 -75
  103. claude_mpm/hooks/session_resume_hook.py +22 -18
  104. claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
  105. claude_mpm/hooks/templates/pre_tool_use_template.py +16 -8
  106. claude_mpm/init.py +21 -14
  107. claude_mpm/mcp/__init__.py +9 -0
  108. claude_mpm/mcp/google_workspace_server.py +610 -0
  109. claude_mpm/scripts/claude-hook-handler.sh +10 -9
  110. claude_mpm/services/agents/agent_selection_service.py +2 -2
  111. claude_mpm/services/agents/single_tier_deployment_service.py +4 -4
  112. claude_mpm/services/command_deployment_service.py +44 -26
  113. claude_mpm/services/hook_installer_service.py +77 -8
  114. claude_mpm/services/mcp_config_manager.py +99 -19
  115. claude_mpm/services/mcp_service_registry.py +294 -0
  116. claude_mpm/services/monitor/server.py +6 -1
  117. claude_mpm/services/pm_skills_deployer.py +5 -3
  118. claude_mpm/services/skills/git_skill_source_manager.py +79 -8
  119. claude_mpm/services/skills/selective_skill_deployer.py +28 -0
  120. claude_mpm/services/skills/skill_discovery_service.py +17 -1
  121. claude_mpm/services/skills_deployer.py +31 -5
  122. claude_mpm/skills/__init__.py +2 -1
  123. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  124. claude_mpm/skills/registry.py +295 -90
  125. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/METADATA +28 -3
  126. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/RECORD +131 -93
  127. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/WHEEL +1 -1
  128. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/entry_points.txt +2 -0
  129. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE +0 -0
  130. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  131. {claude_mpm-5.6.1.dist-info → claude_mpm-5.6.76.dist-info}/top_level.txt +0 -0
@@ -10,8 +10,9 @@ import logging
10
10
  import tempfile
11
11
  from datetime import datetime, timezone
12
12
  from pathlib import Path
13
- from typing import Any, Dict, List
13
+ from typing import Any, Dict, List, Optional
14
14
 
15
+ from ..frameworks.base import RegisteredInstance
15
16
  from ..models import Project, ProjectState, ToolSession
16
17
  from ..registry import ProjectRegistry
17
18
 
@@ -48,6 +49,7 @@ class StateStore:
48
49
 
49
50
  self.projects_path = self.state_dir / "projects.json"
50
51
  self.sessions_path = self.state_dir / "sessions.json"
52
+ self.instances_path = self.state_dir / "instances.json"
51
53
 
52
54
  logger.info(f"Initialized StateStore at {self.state_dir}")
53
55
 
@@ -307,3 +309,95 @@ class StateStore:
307
309
  else None
308
310
  ),
309
311
  )
312
+
313
+ # Instance persistence methods
314
+
315
+ def save_instances(self, instances: Dict[str, RegisteredInstance]) -> None:
316
+ """Save registered instances to disk.
317
+
318
+ Args:
319
+ instances: Dict mapping instance name to RegisteredInstance
320
+
321
+ Raises:
322
+ IOError: If write fails
323
+ """
324
+ data = {
325
+ "version": self.VERSION,
326
+ "saved_at": datetime.now(timezone.utc).isoformat(),
327
+ "instances": {name: inst.to_dict() for name, inst in instances.items()},
328
+ }
329
+ self._atomic_write(self.instances_path, data)
330
+ logger.info(f"Saved {len(instances)} instances to {self.instances_path}")
331
+
332
+ def load_instances(self) -> Dict[str, RegisteredInstance]:
333
+ """Load registered instances from disk.
334
+
335
+ Returns:
336
+ Dict mapping instance name to RegisteredInstance
337
+ (empty if file missing or corrupt)
338
+ """
339
+ if not self.instances_path.exists():
340
+ logger.info("No instances file found, returning empty dict")
341
+ return {}
342
+
343
+ try:
344
+ data = self._read_json(self.instances_path)
345
+
346
+ if data.get("version") != self.VERSION:
347
+ logger.warning(
348
+ f"Version mismatch: expected {self.VERSION}, "
349
+ f"got {data.get('version')}"
350
+ )
351
+
352
+ instances = {
353
+ name: RegisteredInstance.from_dict(inst_data)
354
+ for name, inst_data in data.get("instances", {}).items()
355
+ }
356
+
357
+ logger.info(f"Loaded {len(instances)} instances from {self.instances_path}")
358
+ return instances
359
+
360
+ except Exception as e:
361
+ logger.error(f"Failed to load instances: {e}", exc_info=True)
362
+ return {}
363
+
364
+ def register_instance(self, instance: RegisteredInstance) -> None:
365
+ """Register a single instance (add to existing).
366
+
367
+ Args:
368
+ instance: RegisteredInstance to add
369
+ """
370
+ instances = self.load_instances()
371
+ instances[instance.name] = instance
372
+ self.save_instances(instances)
373
+ logger.info(f"Registered instance '{instance.name}'")
374
+
375
+ def unregister_instance(self, name: str) -> bool:
376
+ """Remove an instance registration.
377
+
378
+ Args:
379
+ name: Instance name to remove
380
+
381
+ Returns:
382
+ True if instance was found and removed, False if not found
383
+ """
384
+ instances = self.load_instances()
385
+ if name in instances:
386
+ del instances[name]
387
+ self.save_instances(instances)
388
+ logger.info(f"Unregistered instance '{name}'")
389
+ return True
390
+ logger.warning(f"Instance '{name}' not found for unregistration")
391
+ return False
392
+
393
+ def get_registered_instance(self, name: str) -> Optional[RegisteredInstance]:
394
+ """Get a single registered instance by name.
395
+
396
+ Args:
397
+ name: Instance name to look up
398
+
399
+ Returns:
400
+ RegisteredInstance if found, None otherwise
401
+ """
402
+ instances = self.load_instances()
403
+ return instances.get(name)
@@ -42,15 +42,18 @@ class ProjectRegistry:
42
42
  self._lock = threading.RLock()
43
43
  logger.info("Initialized ProjectRegistry")
44
44
 
45
- def register(self, path: str, name: Optional[str] = None) -> Project:
45
+ def register(
46
+ self, path: str, name: Optional[str] = None, project_id: Optional[str] = None
47
+ ) -> Project:
46
48
  """Register a new project.
47
49
 
48
- Creates a new project with unique UUID and adds it to the registry.
50
+ Creates a new project with unique UUID (or user-provided ID) and adds it to registry.
49
51
  Path must be a valid directory and cannot already be registered.
50
52
 
51
53
  Args:
52
54
  path: Absolute filesystem path to project directory
53
55
  name: Optional human-readable name (defaults to directory name)
56
+ project_id: Optional project identifier (UUID generated if omitted)
54
57
 
55
58
  Returns:
56
59
  Newly created Project instance
@@ -92,8 +95,11 @@ class ProjectRegistry:
92
95
  if name is None:
93
96
  name = path_obj.name
94
97
 
95
- # Generate unique project ID
96
- project_id = str(uuid.uuid4())
98
+ # Generate unique project ID if not provided
99
+ if project_id is None:
100
+ project_id = str(uuid.uuid4())
101
+ elif project_id in self._projects:
102
+ raise ValueError(f"Project ID already exists: {project_id}")
97
103
 
98
104
  # Create project instance
99
105
  project = Project(
@@ -6,13 +6,16 @@ and detects events using OutputParser.
6
6
 
7
7
  import asyncio
8
8
  import logging
9
- from typing import Dict, List, Optional
9
+ from typing import TYPE_CHECKING, Dict, List, Optional
10
10
 
11
11
  from ..events.manager import EventManager
12
12
  from ..models.events import Event
13
13
  from ..parsing.output_parser import OutputParser
14
14
  from ..tmux_orchestrator import TmuxOrchestrator
15
15
 
16
+ if TYPE_CHECKING:
17
+ from ..core.block_manager import BlockManager
18
+
16
19
  logger = logging.getLogger(__name__)
17
20
 
18
21
 
@@ -44,6 +47,7 @@ class RuntimeMonitor:
44
47
  event_manager: EventManager,
45
48
  poll_interval: float = 2.0,
46
49
  capture_lines: int = 1000,
50
+ block_manager: Optional["BlockManager"] = None,
47
51
  ):
48
52
  """Initialize runtime monitor.
49
53
 
@@ -53,6 +57,7 @@ class RuntimeMonitor:
53
57
  event_manager: EventManager for emitting events
54
58
  poll_interval: Seconds between polls (default: 2.0)
55
59
  capture_lines: Number of lines to capture (default: 1000)
60
+ block_manager: Optional BlockManager for automatic work blocking
56
61
 
57
62
  Raises:
58
63
  ValueError: If any required parameter is None
@@ -69,15 +74,17 @@ class RuntimeMonitor:
69
74
  self.event_manager = event_manager
70
75
  self.poll_interval = poll_interval
71
76
  self.capture_lines = capture_lines
77
+ self.block_manager = block_manager
72
78
 
73
79
  # Track active monitors: pane_target -> (project_id, task, last_output_hash)
74
80
  self._monitors: Dict[str, tuple[str, Optional[asyncio.Task], int]] = {}
75
81
  self._running = False
76
82
 
77
83
  logger.debug(
78
- "RuntimeMonitor initialized (interval: %.2fs, lines: %d)",
84
+ "RuntimeMonitor initialized (interval: %.2fs, lines: %d, block_manager: %s)",
79
85
  poll_interval,
80
86
  capture_lines,
87
+ "enabled" if block_manager else "disabled",
81
88
  )
82
89
 
83
90
  async def start_monitoring(self, pane_target: str, project_id: str) -> None:
@@ -284,6 +291,29 @@ class RuntimeMonitor:
284
291
  pane_target,
285
292
  )
286
293
 
294
+ # Automatically block work for blocking events
295
+ if self.block_manager:
296
+ for parse_result in parse_results:
297
+ # Get the created event from EventManager
298
+ # Events are created with matching titles, so find by title
299
+ pending_events = self.event_manager.get_pending(project_id)
300
+ for event in pending_events:
301
+ if (
302
+ event.title == parse_result.title
303
+ and event.is_blocking
304
+ ):
305
+ blocked_work = (
306
+ await self.block_manager.check_and_block(event)
307
+ )
308
+ if blocked_work:
309
+ logger.info(
310
+ "Event %s blocked %d work items: %s",
311
+ event.id,
312
+ len(blocked_work),
313
+ blocked_work,
314
+ )
315
+ break
316
+
287
317
  except Exception as e:
288
318
  logger.error(
289
319
  "Error in monitor loop for pane %s: %s",
@@ -158,10 +158,11 @@ class TmuxOrchestrator:
158
158
  """
159
159
  logger.info(f"Creating pane '{pane_id}' in {working_dir}")
160
160
 
161
- # Split window to create new pane
161
+ # Create new window instead of splitting pane to avoid "no space for new pane" error
162
+ # when tmux window is too small to split
162
163
  self._run_tmux(
163
164
  [
164
- "split-window",
165
+ "new-window",
165
166
  "-t",
166
167
  self.session_name,
167
168
  "-c",
@@ -51,16 +51,19 @@ class WorkExecutor:
51
51
 
52
52
  logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
53
53
 
54
- async def execute_next(self) -> bool:
54
+ async def execute_next(self, pane_target: Optional[str] = None) -> bool:
55
55
  """Execute next available work item.
56
56
 
57
57
  Gets next work from queue, starts it, and executes via RuntimeExecutor.
58
58
 
59
+ Args:
60
+ pane_target: Optional tmux pane target for execution
61
+
59
62
  Returns:
60
63
  True if work was executed, False if queue empty/blocked
61
64
 
62
65
  Example:
63
- >>> executed = await executor.execute_next()
66
+ >>> executed = await executor.execute_next("%5")
64
67
  >>> if not executed:
65
68
  ... print("No work available")
66
69
  """
@@ -71,10 +74,12 @@ class WorkExecutor:
71
74
  return False
72
75
 
73
76
  # Execute the work item
74
- await self.execute(work_item)
77
+ await self.execute(work_item, pane_target)
75
78
  return True
76
79
 
77
- async def execute(self, work_item: WorkItem) -> None:
80
+ async def execute(
81
+ self, work_item: WorkItem, pane_target: Optional[str] = None
82
+ ) -> None:
78
83
  """Execute a specific work item.
79
84
 
80
85
  Marks work as IN_PROGRESS and sends to RuntimeExecutor.
@@ -83,12 +88,13 @@ class WorkExecutor:
83
88
 
84
89
  Args:
85
90
  work_item: WorkItem to execute
91
+ pane_target: Optional tmux pane target for execution
86
92
 
87
93
  Raises:
88
94
  RuntimeError: If execution fails
89
95
 
90
96
  Example:
91
- >>> await executor.execute(work_item)
97
+ >>> await executor.execute(work_item, "%5")
92
98
  """
93
99
  # Mark as in progress
94
100
  if not self.queue.start(work_item.id):
@@ -103,17 +109,21 @@ class WorkExecutor:
103
109
  )
104
110
 
105
111
  try:
106
- # Send work content to runtime
107
- # Note: In actual implementation, this would integrate with
108
- # ProjectSession which manages the pane_target
109
- # For now, we assume runtime has active session
110
- # This will be properly integrated when wiring with ProjectSession
112
+ # Send work content to runtime if pane target provided
113
+ if pane_target:
114
+ await self.runtime.send_message(pane_target, work_item.content)
115
+ logger.info(
116
+ f"Work item {work_item.id} sent to pane {pane_target} for execution"
117
+ )
118
+ else:
119
+ logger.warning(
120
+ f"No pane target provided for work item {work_item.id}, "
121
+ f"work marked as in-progress but not sent to runtime"
122
+ )
111
123
 
112
124
  # Store work item ID in metadata for callback tracking
113
125
  work_item.metadata["execution_started"] = True
114
126
 
115
- logger.info(f"Work item {work_item.id} sent to runtime for execution")
116
-
117
127
  except Exception as e:
118
128
  logger.error(f"Failed to execute work item {work_item.id}: {e}")
119
129
  await self.handle_failure(work_item.id, str(e))
@@ -155,7 +165,7 @@ class WorkExecutor:
155
165
  else:
156
166
  logger.warning(f"Failed to mark work item {work_id} as failed")
157
167
 
158
- async def handle_block(self, work_id: str, reason: str) -> None:
168
+ async def handle_block(self, work_id: str, reason: str) -> bool:
159
169
  """Handle work being blocked by an event.
160
170
 
161
171
  Called when RuntimeMonitor detects a blocking event.
@@ -164,15 +174,19 @@ class WorkExecutor:
164
174
  work_id: Work item ID that is blocked
165
175
  reason: Reason for blocking (e.g., "Waiting for approval")
166
176
 
177
+ Returns:
178
+ True if work was successfully blocked, False otherwise
179
+
167
180
  Example:
168
- >>> await executor.handle_block("work-123", "Decision needed")
181
+ >>> success = await executor.handle_block("work-123", "Decision needed")
169
182
  """
170
183
  if self.queue.block(work_id, reason):
171
184
  logger.info(f"Work item {work_id} blocked: {reason}")
172
- else:
173
- logger.warning(f"Failed to mark work item {work_id} as blocked")
185
+ return True
186
+ logger.warning(f"Failed to mark work item {work_id} as blocked")
187
+ return False
174
188
 
175
- async def handle_unblock(self, work_id: str) -> None:
189
+ async def handle_unblock(self, work_id: str) -> bool:
176
190
  """Handle work being unblocked after event resolution.
177
191
 
178
192
  Called when EventHandler resolves a blocking event.
@@ -180,10 +194,14 @@ class WorkExecutor:
180
194
  Args:
181
195
  work_id: Work item ID to unblock
182
196
 
197
+ Returns:
198
+ True if work was successfully unblocked, False otherwise
199
+
183
200
  Example:
184
- >>> await executor.handle_unblock("work-123")
201
+ >>> success = await executor.handle_unblock("work-123")
185
202
  """
186
203
  if self.queue.unblock(work_id):
187
204
  logger.info(f"Work item {work_id} unblocked, resuming execution")
188
- else:
189
- logger.warning(f"Failed to unblock work item {work_id}")
205
+ return True
206
+ logger.warning(f"Failed to unblock work item {work_id}")
207
+ return False
@@ -5,12 +5,15 @@ user input and coordinates session pause/resume.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Dict, List, Optional
8
+ from typing import TYPE_CHECKING, Dict, List, Optional
9
9
 
10
10
  from ..inbox import Inbox
11
11
  from ..models.events import BLOCKING_EVENTS, Event, EventStatus
12
12
  from ..project_session import ProjectSession
13
13
 
14
+ if TYPE_CHECKING:
15
+ from ..core.block_manager import BlockManager
16
+
14
17
  logger = logging.getLogger(__name__)
15
18
 
16
19
 
@@ -32,13 +35,17 @@ class EventHandler:
32
35
  """
33
36
 
34
37
  def __init__(
35
- self, inbox: Inbox, session_manager: Dict[str, ProjectSession]
38
+ self,
39
+ inbox: Inbox,
40
+ session_manager: Dict[str, ProjectSession],
41
+ block_manager: Optional["BlockManager"] = None,
36
42
  ) -> None:
37
43
  """Initialize event handler.
38
44
 
39
45
  Args:
40
46
  inbox: Inbox instance for event access
41
47
  session_manager: Dict mapping project_id -> ProjectSession
48
+ block_manager: Optional BlockManager for automatic work unblocking
42
49
 
43
50
  Raises:
44
51
  ValueError: If inbox or session_manager is None
@@ -51,8 +58,12 @@ class EventHandler:
51
58
  self.inbox = inbox
52
59
  self.session_manager = session_manager
53
60
  self._event_manager = inbox.events
61
+ self.block_manager = block_manager
54
62
 
55
- logger.debug("EventHandler initialized")
63
+ logger.debug(
64
+ "EventHandler initialized (block_manager: %s)",
65
+ "enabled" if block_manager else "disabled",
66
+ )
56
67
 
57
68
  async def process_event(self, event: Event) -> None:
58
69
  """Process an event - pause session if blocking.
@@ -137,6 +148,17 @@ class EventHandler:
137
148
  # Mark event as resolved
138
149
  self._event_manager.respond(event_id, response)
139
150
 
151
+ # Automatically unblock work items if BlockManager is available
152
+ if self.block_manager and was_blocking:
153
+ unblocked_work = await self.block_manager.check_and_unblock(event_id)
154
+ if unblocked_work:
155
+ logger.info(
156
+ "Event %s resolution unblocked %d work items: %s",
157
+ event_id,
158
+ len(unblocked_work),
159
+ unblocked_work,
160
+ )
161
+
140
162
  # If event was NOT blocking, no need to resume
141
163
  if not was_blocking:
142
164
  logger.debug("Event %s was non-blocking, no resume needed", event_id)
@@ -54,6 +54,7 @@ class SkillSource:
54
54
  branch: Git branch to use (default: "main")
55
55
  priority: Priority for skill resolution (lower = higher precedence)
56
56
  enabled: Whether this source should be synced
57
+ token: Optional GitHub token or env var reference (e.g., "$MY_TOKEN")
57
58
 
58
59
  Priority System:
59
60
  - 0: Reserved for system repository (highest precedence)
@@ -61,6 +62,12 @@ class SkillSource:
61
62
  - 100-999: Normal priority custom sources
62
63
  - 1000+: Low priority custom sources
63
64
 
65
+ Token Authentication:
66
+ - Direct token: "ghp_xxxxx" (stored in config, not recommended)
67
+ - Env var reference: "$PRIVATE_REPO_TOKEN" (resolved at runtime)
68
+ - If None, falls back to GITHUB_TOKEN or GH_TOKEN env vars
69
+ - Priority: source.token > GITHUB_TOKEN > GH_TOKEN
70
+
64
71
  Example:
65
72
  >>> source = SkillSource(
66
73
  ... id="system",
@@ -70,6 +77,12 @@ class SkillSource:
70
77
  ... )
71
78
  >>> source.validate()
72
79
  []
80
+ >>> private_source = SkillSource(
81
+ ... id="private",
82
+ ... type="git",
83
+ ... url="https://github.com/myorg/private-skills",
84
+ ... token="$PRIVATE_REPO_TOKEN"
85
+ ... )
73
86
  """
74
87
 
75
88
  id: str
@@ -78,6 +91,7 @@ class SkillSource:
78
91
  branch: str = "main"
79
92
  priority: int = 100
80
93
  enabled: bool = True
94
+ token: Optional[str] = None
81
95
 
82
96
  def __post_init__(self):
83
97
  """Validate skill source configuration after initialization.
@@ -262,6 +276,7 @@ class SkillSourceConfiguration:
262
276
  branch=source_data.get("branch", "main"),
263
277
  priority=source_data.get("priority", 100),
264
278
  enabled=source_data.get("enabled", True),
279
+ token=source_data.get("token"),
265
280
  )
266
281
  sources.append(source)
267
282
  except (KeyError, ValueError) as e:
@@ -326,6 +341,7 @@ class SkillSourceConfiguration:
326
341
  "branch": source.branch,
327
342
  "priority": source.priority,
328
343
  "enabled": source.enabled,
344
+ **({"token": source.token} if source.token else {}),
329
345
  }
330
346
  for source in sources
331
347
  ]
claude_mpm/constants.py CHANGED
@@ -46,6 +46,7 @@ class CLICommands(str, Enum):
46
46
  DASHBOARD = "dashboard"
47
47
  UPGRADE = "upgrade"
48
48
  SKILLS = "skills"
49
+ OAUTH = "oauth"
49
50
 
50
51
  def with_prefix(self, prefix: CLIPrefix = CLIPrefix.MPM) -> str:
51
52
  """Get command with prefix."""
@@ -143,6 +144,10 @@ class MCPCommands(str, Enum):
143
144
  CONFIG = "config"
144
145
  SERVER = "server"
145
146
  EXTERNAL = "external"
147
+ # Service management commands
148
+ ENABLE = "enable"
149
+ DISABLE = "disable"
150
+ LIST = "list"
146
151
 
147
152
 
148
153
  class TicketCommands(str, Enum):