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
@@ -1,19 +1,32 @@
1
1
  """Manages running Claude Code/MPM instances."""
2
2
 
3
+ import asyncio
3
4
  import logging
5
+ import re
6
+ from datetime import datetime, timezone
4
7
  from pathlib import Path
5
- from typing import Optional
8
+ from typing import TYPE_CHECKING, Optional
6
9
 
7
10
  from claude_mpm.commander.adapters import (
8
11
  AdapterResponse,
9
12
  ClaudeCodeAdapter,
10
13
  ClaudeCodeCommunicationAdapter,
11
14
  )
12
- from claude_mpm.commander.frameworks.base import BaseFramework, InstanceInfo
15
+ from claude_mpm.commander.frameworks.base import (
16
+ BaseFramework,
17
+ InstanceInfo,
18
+ RegisteredInstance,
19
+ )
13
20
  from claude_mpm.commander.frameworks.claude_code import ClaudeCodeFramework
14
21
  from claude_mpm.commander.frameworks.mpm import MPMFramework
22
+ from claude_mpm.commander.git.worktree_manager import WorktreeManager
23
+ from claude_mpm.commander.models.events import EventType
15
24
  from claude_mpm.commander.tmux_orchestrator import TmuxOrchestrator
16
25
 
26
+ if TYPE_CHECKING:
27
+ from claude_mpm.commander.events.manager import EventManager
28
+ from claude_mpm.commander.persistence.state_store import StateStore
29
+
17
30
  logger = logging.getLogger(__name__)
18
31
 
19
32
 
@@ -75,6 +88,32 @@ class InstanceManager:
75
88
  self._instances: dict[str, InstanceInfo] = {}
76
89
  self._frameworks = self._load_frameworks()
77
90
  self._adapters: dict[str, ClaudeCodeCommunicationAdapter] = {}
91
+ self._event_manager: Optional[EventManager] = None
92
+ self._state_store: Optional[StateStore] = None
93
+
94
+ def set_event_manager(self, event_manager: "EventManager") -> None:
95
+ """Set the event manager for emitting instance events.
96
+
97
+ Args:
98
+ event_manager: EventManager instance for event emission
99
+
100
+ Example:
101
+ >>> manager = InstanceManager(orchestrator)
102
+ >>> manager.set_event_manager(event_manager)
103
+ """
104
+ self._event_manager = event_manager
105
+
106
+ def set_state_store(self, state_store: "StateStore") -> None:
107
+ """Set state store for persistence.
108
+
109
+ Args:
110
+ state_store: StateStore instance for persisting registered instances
111
+
112
+ Example:
113
+ >>> manager = InstanceManager(orchestrator)
114
+ >>> manager.set_state_store(state_store)
115
+ """
116
+ self._state_store = state_store
78
117
 
79
118
  def _load_frameworks(self) -> dict[str, BaseFramework]:
80
119
  """Load available frameworks.
@@ -167,6 +206,20 @@ class InstanceManager:
167
206
  startup_cmd = framework_obj.get_startup_command(project_path)
168
207
  self.orchestrator.send_keys(pane_target, startup_cmd)
169
208
 
209
+ # Create communication adapter for the instance (only for Claude Code for now)
210
+ # Do this BEFORE creating InstanceInfo so we can set connected=True
211
+ has_adapter = False
212
+ if framework == "cc":
213
+ runtime_adapter = ClaudeCodeAdapter()
214
+ comm_adapter = ClaudeCodeCommunicationAdapter(
215
+ orchestrator=self.orchestrator,
216
+ pane_target=pane_target,
217
+ runtime_adapter=runtime_adapter,
218
+ )
219
+ self._adapters[name] = comm_adapter
220
+ has_adapter = True
221
+ logger.debug(f"Created communication adapter for instance '{name}'")
222
+
170
223
  # Create instance info
171
224
  instance = InstanceInfo(
172
225
  name=name,
@@ -176,28 +229,197 @@ class InstanceManager:
176
229
  pane_target=pane_target,
177
230
  git_branch=git_branch,
178
231
  git_status=git_status,
232
+ connected=has_adapter,
179
233
  )
180
234
 
181
235
  # Track instance
182
236
  self._instances[name] = instance
183
237
 
184
- # Create communication adapter for the instance (only for Claude Code for now)
185
- if framework == "cc":
186
- runtime_adapter = ClaudeCodeAdapter()
187
- comm_adapter = ClaudeCodeCommunicationAdapter(
188
- orchestrator=self.orchestrator,
189
- pane_target=pane_target,
190
- runtime_adapter=runtime_adapter,
191
- )
192
- self._adapters[name] = comm_adapter
193
- logger.debug(f"Created communication adapter for instance '{name}'")
194
-
195
238
  logger.info(
196
239
  f"Started instance '{name}' with framework '{framework}' at {project_path}"
197
240
  )
198
241
 
242
+ # Emit starting event and start background ready detection
243
+ if self._event_manager:
244
+ event = self._event_manager.create(
245
+ project_id=name,
246
+ event_type=EventType.INSTANCE_STARTING,
247
+ title=f"Starting instance '{name}'",
248
+ content=f"Instance {name} is starting at {project_path}",
249
+ context={"instance_name": name, "working_dir": str(project_path)},
250
+ )
251
+ await self._event_manager.emit(event)
252
+
253
+ # Start background ready detection (always, not just with event_manager)
254
+ asyncio.create_task(self._detect_ready(name, instance))
255
+
199
256
  return instance
200
257
 
258
+ async def _detect_ready(
259
+ self, name: str, instance_info: InstanceInfo, timeout: int = 30
260
+ ) -> None:
261
+ """Background task to detect when instance is ready.
262
+
263
+ Monitors the pane output for patterns indicating the instance
264
+ is ready to accept commands.
265
+
266
+ Args:
267
+ name: Instance name
268
+ instance_info: InstanceInfo with pane details
269
+ timeout: Maximum seconds to wait for ready state
270
+
271
+ Example:
272
+ >>> # Called internally by start_instance
273
+ >>> asyncio.create_task(self._detect_ready(name, instance))
274
+ """
275
+ # TIER 1: Startup markers (most reliable, appear at T+0-2s)
276
+ startup_patterns = [
277
+ r"Claude Code v[\d.]+", # Version marker
278
+ r"Claude Code", # Product identification
279
+ r"Opus 4\.5", # Model identifier
280
+ r"Claude Max|Claude Pro", # Tier indicator
281
+ ]
282
+
283
+ # TIER 2: Ready state indicators (appear at T+5-10s)
284
+ ready_patterns = [
285
+ r">\s*$", # CLI prompt (end of line)
286
+ r"^>\s*", # CLI prompt (start of line)
287
+ r"What can I.*help", # Specific greeting
288
+ r"How can I.*assist", # Specific greeting
289
+ r"(Human|User):\s*$", # Anthropic format
290
+ ]
291
+
292
+ # TIER 3: Secondary indicators (confirmation)
293
+ secondary_patterns = [
294
+ r"Ready for input", # Explicit ready
295
+ r"Tips for getting", # Claude CLI tips
296
+ r"Use /help", # Help hint
297
+ r"claudeMd", # CLAUDE.md loaded
298
+ r"Agent Memory", # Agent initialized
299
+ r"PM_INSTRUCTIONS", # MPM initialized
300
+ ]
301
+
302
+ # Combine all patterns (TIER 1 most reliable)
303
+ all_patterns = startup_patterns + ready_patterns + secondary_patterns
304
+
305
+ start_time = asyncio.get_event_loop().time()
306
+ check_count = 0
307
+
308
+ while asyncio.get_event_loop().time() - start_time < timeout:
309
+ elapsed = asyncio.get_event_loop().time() - start_time
310
+ check_count += 1
311
+
312
+ # Progress logging at key milestones
313
+ if elapsed > 10 and check_count == 11:
314
+ logger.info(
315
+ f"Ready detection for '{name}' still waiting after 10s... "
316
+ f"(Claude Code startup may be slow)"
317
+ )
318
+ elif elapsed > 20 and check_count == 21:
319
+ logger.info(
320
+ f"Ready detection for '{name}' still waiting after 20s... "
321
+ f"(system may be overloaded)"
322
+ )
323
+
324
+ await asyncio.sleep(1)
325
+ try:
326
+ # IMPROVED: Increased buffer from 50 to 100 lines
327
+ output = self.orchestrator.capture_output(
328
+ instance_info.pane_target, lines=100
329
+ )
330
+
331
+ if output:
332
+ # Check all patterns (added re.IGNORECASE flag)
333
+ # IMPROVED: Use MULTILINE | IGNORECASE flags
334
+ for pattern in all_patterns:
335
+ if re.search(pattern, output, re.MULTILINE | re.IGNORECASE):
336
+ # Instance is ready!
337
+ if name in self._instances:
338
+ self._instances[name].ready = True
339
+
340
+ # Emit ready event
341
+ if self._event_manager:
342
+ event = self._event_manager.create(
343
+ project_id=name,
344
+ event_type=EventType.INSTANCE_READY,
345
+ title=f"Instance '{name}' ready",
346
+ content=(
347
+ f"Instance {name} is ready for commands "
348
+ f"(detected in {elapsed:.1f}s)"
349
+ ),
350
+ context={
351
+ "instance_name": name,
352
+ "detection_time": elapsed,
353
+ "pattern_matched": pattern,
354
+ },
355
+ )
356
+ await self._event_manager.emit(event)
357
+
358
+ logger.info(
359
+ f"Instance '{name}' is ready "
360
+ f"(detected in {elapsed:.1f}s via pattern: {pattern})"
361
+ )
362
+ return
363
+
364
+ except Exception as e:
365
+ logger.debug(
366
+ f"Error checking ready state for '{name}': {e}. "
367
+ f"Elapsed: {elapsed:.1f}s"
368
+ )
369
+
370
+ # Timeout - mark as ready anyway since instance might still work
371
+ logger.warning(
372
+ f"Instance '{name}' ready detection timed out after {timeout}s. "
373
+ f"Instance may still be functional. Check logs for issues."
374
+ )
375
+
376
+ if name in self._instances:
377
+ self._instances[name].ready = True
378
+
379
+ if self._event_manager:
380
+ event = self._event_manager.create(
381
+ project_id=name,
382
+ event_type=EventType.INSTANCE_READY,
383
+ title=f"Instance '{name}' started",
384
+ content=(
385
+ f"Instance {name} startup timeout. "
386
+ f"May be ready, or startup may be slow/blocked. "
387
+ f"Check instance manually if issues occur."
388
+ ),
389
+ context={
390
+ "instance_name": name,
391
+ "timeout": True,
392
+ "timeout_seconds": timeout,
393
+ },
394
+ )
395
+ await self._event_manager.emit(event)
396
+
397
+ async def wait_for_ready(self, name: str, timeout: int = 30) -> bool:
398
+ """Wait for an instance to be ready.
399
+
400
+ Polls the instance's ready flag until it becomes True or timeout.
401
+
402
+ Args:
403
+ name: Instance name
404
+ timeout: Maximum seconds to wait
405
+
406
+ Returns:
407
+ True if instance is ready, False if timeout
408
+
409
+ Example:
410
+ >>> manager = InstanceManager(orchestrator)
411
+ >>> await manager.start_instance("myapp", "/path/to/app", "mpm")
412
+ >>> if await manager.wait_for_ready("myapp", timeout=30):
413
+ ... print("Ready!")
414
+ """
415
+ start_time = asyncio.get_event_loop().time()
416
+ while asyncio.get_event_loop().time() - start_time < timeout:
417
+ inst = self._instances.get(name)
418
+ if inst and inst.ready:
419
+ return True
420
+ await asyncio.sleep(0.5)
421
+ return False
422
+
201
423
  async def stop_instance(self, name: str) -> bool:
202
424
  """Stop an instance.
203
425
 
@@ -226,6 +448,7 @@ class InstanceManager:
226
448
  # Remove adapter if exists
227
449
  if name in self._adapters:
228
450
  del self._adapters[name]
451
+ instance.connected = False
229
452
  logger.debug(f"Removed adapter for instance '{name}'")
230
453
 
231
454
  # Remove from tracking
@@ -313,8 +536,11 @@ class InstanceManager:
313
536
  return None
314
537
 
315
538
  # Fallback to direct tmux if no adapter
316
- self.orchestrator.send_keys(instance.pane_target, message)
317
- logger.info(f"Sent message to instance '{name}': {message[:50]}...")
539
+ success = self.orchestrator.send_keys(instance.pane_target, message)
540
+ if success:
541
+ logger.info(f"Sent message to instance '{name}': {message[:50]}...")
542
+ else:
543
+ logger.error(f"Failed to send message to instance '{name}'")
318
544
  return None
319
545
 
320
546
  def get_adapter(self, name: str) -> Optional[ClaudeCodeCommunicationAdapter]:
@@ -335,3 +561,308 @@ class InstanceManager:
335
561
  ... print(chunk, end='')
336
562
  """
337
563
  return self._adapters.get(name)
564
+
565
+ async def rename_instance(self, old_name: str, new_name: str) -> bool:
566
+ """Rename an instance.
567
+
568
+ Args:
569
+ old_name: Current instance name
570
+ new_name: New instance name
571
+
572
+ Returns:
573
+ True if renamed successfully
574
+
575
+ Raises:
576
+ InstanceNotFoundError: If old_name doesn't exist
577
+ InstanceAlreadyExistsError: If new_name already exists
578
+
579
+ Example:
580
+ >>> manager = InstanceManager(orchestrator)
581
+ >>> await manager.rename_instance("myapp", "myapp-v2")
582
+ True
583
+ """
584
+ # Validate old_name exists
585
+ if old_name not in self._instances:
586
+ raise InstanceNotFoundError(old_name)
587
+
588
+ # Validate new_name doesn't exist
589
+ if new_name in self._instances:
590
+ raise InstanceAlreadyExistsError(new_name)
591
+
592
+ # Get instance and update name
593
+ instance = self._instances[old_name]
594
+ instance.name = new_name
595
+
596
+ # Update _instances dict (remove old key, add new)
597
+ del self._instances[old_name]
598
+ self._instances[new_name] = instance
599
+
600
+ # Update _adapters dict if exists
601
+ if old_name in self._adapters:
602
+ adapter = self._adapters[old_name]
603
+ del self._adapters[old_name]
604
+ self._adapters[new_name] = adapter
605
+ logger.debug(f"Moved adapter from '{old_name}' to '{new_name}'")
606
+
607
+ logger.info(f"Renamed instance from '{old_name}' to '{new_name}'")
608
+
609
+ return True
610
+
611
+ async def close_instance(self, name: str, merge: bool = True) -> tuple[bool, str]:
612
+ """Close instance: stop tmux, optionally merge worktree, cleanup.
613
+
614
+ Args:
615
+ name: Instance name to close
616
+ merge: Whether to merge worktree to main (default True)
617
+
618
+ Returns:
619
+ Tuple of (success, message)
620
+
621
+ Example:
622
+ >>> manager = InstanceManager(orchestrator)
623
+ >>> success, msg = await manager.close_instance("myapp")
624
+ >>> print(success, msg)
625
+ True Closed 'myapp'
626
+ """
627
+ registered = (
628
+ self._state_store.get_registered_instance(name)
629
+ if self._state_store
630
+ else None
631
+ )
632
+
633
+ # Stop the tmux session
634
+ try:
635
+ await self.stop_instance(name)
636
+ except InstanceNotFoundError:
637
+ # Instance not running, but may still have worktree to clean up
638
+ pass
639
+
640
+ # Merge and cleanup worktree if enabled
641
+ if registered and registered.use_worktree and registered.worktree_path:
642
+ wt_manager = WorktreeManager(Path(registered.path))
643
+ if merge:
644
+ success, msg = wt_manager.merge_to_main(name, delete_after=True)
645
+ if not success:
646
+ return False, msg
647
+ else:
648
+ wt_manager.remove(name, force=True)
649
+
650
+ # Remove from registry
651
+ if self._state_store:
652
+ self._state_store.unregister_instance(name)
653
+
654
+ return True, f"Closed '{name}'"
655
+
656
+ async def disconnect_instance(self, name: str) -> bool:
657
+ """Disconnect from an instance without closing it.
658
+
659
+ The instance keeps running but we stop communication.
660
+ Removes the adapter while keeping the instance tracked.
661
+
662
+ Args:
663
+ name: Instance name to disconnect from
664
+
665
+ Returns:
666
+ True if disconnected successfully
667
+
668
+ Raises:
669
+ InstanceNotFoundError: If instance not found
670
+
671
+ Example:
672
+ >>> manager = InstanceManager(orchestrator)
673
+ >>> await manager.disconnect_instance("myapp")
674
+ True
675
+ >>> # Instance still running, but no adapter connection
676
+ >>> adapter = manager.get_adapter("myapp")
677
+ >>> print(adapter)
678
+ None
679
+ """
680
+ # Validate instance exists
681
+ if name not in self._instances:
682
+ raise InstanceNotFoundError(name)
683
+
684
+ instance = self._instances[name]
685
+
686
+ # Remove adapter if exists (but keep instance)
687
+ if name in self._adapters:
688
+ # Could add cleanup here if adapter has resources to close
689
+ del self._adapters[name]
690
+ instance.connected = False
691
+ logger.info(f"Disconnected from instance '{name}' (instance still running)")
692
+ else:
693
+ logger.debug(f"No adapter to disconnect for instance '{name}'")
694
+
695
+ return True
696
+
697
+ async def register_instance(
698
+ self,
699
+ path: str,
700
+ framework: str,
701
+ name: str,
702
+ use_worktree: bool = True,
703
+ branch: Optional[str] = None,
704
+ ) -> InstanceInfo:
705
+ """Register an instance and start it.
706
+
707
+ Registers the instance in persistent storage so it can be started
708
+ by name in future sessions, then starts the instance.
709
+
710
+ Args:
711
+ path: Project directory path
712
+ framework: Framework to use ("cc" or "mpm")
713
+ name: Instance name (also worktree name if enabled)
714
+ use_worktree: Create isolated git worktree (default True)
715
+ branch: Branch for worktree (default: session-{name})
716
+
717
+ Returns:
718
+ InstanceInfo with tmux session details
719
+
720
+ Raises:
721
+ FrameworkNotFoundError: If framework is not available
722
+ InstanceAlreadyExistsError: If instance already exists
723
+
724
+ Example:
725
+ >>> manager = InstanceManager(orchestrator)
726
+ >>> manager.set_state_store(state_store)
727
+ >>> instance = await manager.register_instance(
728
+ ... "/Users/user/myapp", "cc", "myapp"
729
+ ... )
730
+ >>> print(instance.name, instance.framework)
731
+ myapp cc
732
+ """
733
+ project_path = Path(path).expanduser().resolve()
734
+
735
+ worktree_path = None
736
+ worktree_branch = None
737
+ working_path = project_path
738
+
739
+ # Create worktree if enabled and it's a git repo
740
+ if use_worktree and (project_path / ".git").exists():
741
+ try:
742
+ wt_manager = WorktreeManager(project_path)
743
+ wt_info = wt_manager.create(name, branch)
744
+ worktree_path = str(wt_info.path)
745
+ worktree_branch = wt_info.branch
746
+ working_path = wt_info.path
747
+ logger.info(
748
+ f"Created worktree for '{name}' at {worktree_path} "
749
+ f"on branch {worktree_branch}"
750
+ )
751
+ except Exception as e:
752
+ logger.warning(f"Could not create worktree: {e}. Using original path.")
753
+
754
+ # Create registered instance with worktree info
755
+ registered = RegisteredInstance(
756
+ name=name,
757
+ path=str(project_path),
758
+ framework=framework,
759
+ registered_at=datetime.now(timezone.utc).isoformat(),
760
+ worktree_path=worktree_path,
761
+ worktree_branch=worktree_branch,
762
+ use_worktree=use_worktree and worktree_path is not None,
763
+ )
764
+
765
+ # Save to persistent storage
766
+ if self._state_store:
767
+ self._state_store.register_instance(registered)
768
+
769
+ # Start the instance in worktree path
770
+ return await self.start_instance(name, working_path, framework)
771
+
772
+ async def start_by_name(self, name: str) -> Optional[InstanceInfo]:
773
+ """Start a previously registered instance by name.
774
+
775
+ Looks up the instance registration and starts it with the
776
+ stored path and framework. Uses the worktree path if configured.
777
+
778
+ Args:
779
+ name: Instance name (must have been previously registered)
780
+
781
+ Returns:
782
+ InstanceInfo if instance was found and started, None if not registered
783
+
784
+ Example:
785
+ >>> manager = InstanceManager(orchestrator)
786
+ >>> manager.set_state_store(state_store)
787
+ >>> # After previous registration
788
+ >>> instance = await manager.start_by_name("myapp")
789
+ >>> if instance:
790
+ ... print(instance.name, instance.project_path)
791
+ myapp /Users/user/myapp
792
+ """
793
+ if not self._state_store:
794
+ return None
795
+
796
+ registered = self._state_store.get_registered_instance(name)
797
+ if not registered:
798
+ return None
799
+
800
+ # Use working_path which respects worktree setting
801
+ return await self.start_instance(
802
+ registered.name,
803
+ Path(registered.working_path),
804
+ registered.framework,
805
+ )
806
+
807
+ def list_registered(self) -> dict[str, RegisteredInstance]:
808
+ """List all registered instances.
809
+
810
+ Returns:
811
+ Dict mapping instance name to RegisteredInstance
812
+
813
+ Example:
814
+ >>> manager = InstanceManager(orchestrator)
815
+ >>> manager.set_state_store(state_store)
816
+ >>> registered = manager.list_registered()
817
+ >>> for name, instance in registered.items():
818
+ ... print(f"{name}: {instance.path} ({instance.framework})")
819
+ myapp: /Users/user/myapp (cc)
820
+ """
821
+ if not self._state_store:
822
+ return {}
823
+ return self._state_store.load_instances()
824
+
825
+ def unregister(self, name: str) -> bool:
826
+ """Unregister an instance.
827
+
828
+ Removes the instance from persistent storage. Does not stop
829
+ any running instance with this name.
830
+
831
+ Args:
832
+ name: Instance name to unregister
833
+
834
+ Returns:
835
+ True if instance was found and unregistered, False if not found
836
+
837
+ Example:
838
+ >>> manager = InstanceManager(orchestrator)
839
+ >>> manager.set_state_store(state_store)
840
+ >>> success = manager.unregister("myapp")
841
+ >>> print(success)
842
+ True
843
+ """
844
+ if not self._state_store:
845
+ return False
846
+ return self._state_store.unregister_instance(name)
847
+
848
+ def list_worktrees(self, path: str) -> list:
849
+ """List worktrees for a project.
850
+
851
+ Args:
852
+ path: Project directory path
853
+
854
+ Returns:
855
+ List of WorktreeInfo for all worktrees associated with the project
856
+
857
+ Example:
858
+ >>> manager = InstanceManager(orchestrator)
859
+ >>> worktrees = manager.list_worktrees("/Users/user/myapp")
860
+ >>> for wt in worktrees:
861
+ ... print(f"{wt.name}: {wt.path} ({wt.branch})")
862
+ myapp: /Users/user/.worktrees-myapp/myapp (session-myapp)
863
+ """
864
+ try:
865
+ wt_manager = WorktreeManager(Path(path))
866
+ return wt_manager.list()
867
+ except Exception:
868
+ return []
@@ -0,0 +1,45 @@
1
+ """Conversation memory system for Commander.
2
+
3
+ This module provides semantic search, storage, and context compression
4
+ for all Claude Code instance conversations.
5
+
6
+ Key Components:
7
+ - ConversationStore: CRUD operations for conversations
8
+ - EmbeddingService: Generate vector embeddings
9
+ - SemanticSearch: Query conversations semantically
10
+ - ContextCompressor: Summarize conversations for context
11
+ - EntityExtractor: Extract files, functions, errors
12
+
13
+ Example:
14
+ >>> from claude_mpm.commander.memory import (
15
+ ... ConversationStore,
16
+ ... EmbeddingService,
17
+ ... SemanticSearch,
18
+ ... ContextCompressor,
19
+ ... )
20
+ >>> store = ConversationStore()
21
+ >>> embeddings = EmbeddingService()
22
+ >>> search = SemanticSearch(store, embeddings)
23
+ >>> results = await search.search("how did we fix the login bug?")
24
+ """
25
+
26
+ from .compression import ContextCompressor
27
+ from .embeddings import EmbeddingService
28
+ from .entities import Entity, EntityExtractor, EntityType
29
+ from .integration import MemoryIntegration
30
+ from .search import SearchResult, SemanticSearch
31
+ from .store import Conversation, ConversationMessage, ConversationStore
32
+
33
+ __all__ = [
34
+ "ContextCompressor",
35
+ "Conversation",
36
+ "ConversationMessage",
37
+ "ConversationStore",
38
+ "EmbeddingService",
39
+ "Entity",
40
+ "EntityExtractor",
41
+ "EntityType",
42
+ "MemoryIntegration",
43
+ "SearchResult",
44
+ "SemanticSearch",
45
+ ]