claude-mpm 5.6.16__py3-none-any.whl → 5.6.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +7 -7
- claude_mpm/cli/parsers/commander_parser.py +2 -2
- claude_mpm/cli/startup.py +130 -19
- claude_mpm/commander/chat/cli.py +38 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/daemon.py +9 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/core/claude_runner.py +22 -13
- claude_mpm/core/config.py +3 -3
- claude_mpm/core/config_constants.py +74 -9
- claude_mpm/core/constants.py +56 -12
- claude_mpm/core/interactive_session.py +5 -4
- claude_mpm/core/logging_utils.py +4 -2
- claude_mpm/core/network_config.py +148 -0
- claude_mpm/core/oneshot_session.py +7 -6
- claude_mpm/core/output_style_manager.py +5 -2
- claude_mpm/core/socketio_pool.py +13 -5
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.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/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +0 -0
- claude_mpm/hooks/claude_hooks/hook_handler.py +6 -6
- claude_mpm/hooks/claude_hooks/installer.py +2 -2
- 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/__pycache__/connection_manager_http.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/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/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/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/scripts/start_activity_logging.py +0 -0
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/pm_skills_deployer.py +3 -2
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/METADATA +1 -1
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/RECORD +47 -73
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/auto_pause_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-314.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-312.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-314.pyc +0 -0
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.16.dist-info → claude_mpm-5.6.30.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.6.
|
|
1
|
+
5.6.30
|
|
@@ -43,12 +43,12 @@ def display_commander_banner():
|
|
|
43
43
|
|
|
44
44
|
# Commander ASCII art banner
|
|
45
45
|
banner = f"""
|
|
46
|
-
{CYAN}╭{
|
|
47
|
-
{CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{
|
|
48
|
-
{CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{
|
|
49
|
-
{CYAN}├{
|
|
50
|
-
{CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {
|
|
51
|
-
{CYAN}╰{
|
|
46
|
+
{CYAN}╭{"─" * (width - 2)}╮{RESET}
|
|
47
|
+
{CYAN}│{RESET}{BOLD} ⚡ MPM Commander {RESET}{DIM}v{version}{RESET}{" " * (width - 24 - len(version))}│
|
|
48
|
+
{CYAN}│{RESET}{DIM} Multi-Project AI Orchestration{RESET}{" " * (width - 36)}│
|
|
49
|
+
{CYAN}├{"─" * (width - 2)}┤{RESET}
|
|
50
|
+
{CYAN}│{RESET} {YELLOW}ALPHA{RESET} - APIs may change {" " * (width - 55)}│
|
|
51
|
+
{CYAN}╰{"─" * (width - 2)}╯{RESET}
|
|
52
52
|
"""
|
|
53
53
|
print(banner)
|
|
54
54
|
|
|
@@ -142,7 +142,7 @@ def handle_commander_command(args) -> int:
|
|
|
142
142
|
print() # Blank line after loading
|
|
143
143
|
|
|
144
144
|
# Get arguments
|
|
145
|
-
port = getattr(args, "port",
|
|
145
|
+
port = getattr(args, "port", 8766) # NetworkPorts.COMMANDER_DEFAULT
|
|
146
146
|
host = getattr(args, "host", "127.0.0.1")
|
|
147
147
|
state_dir = getattr(args, "state_dir", None)
|
|
148
148
|
no_chat = getattr(args, "no_chat", False) or getattr(args, "daemon_only", False)
|
|
@@ -77,8 +77,8 @@ Examples:
|
|
|
77
77
|
commander_parser.add_argument(
|
|
78
78
|
"--port",
|
|
79
79
|
type=int,
|
|
80
|
-
default=
|
|
81
|
-
help="Port for internal services (default:
|
|
80
|
+
default=8766, # NetworkPorts.COMMANDER_DEFAULT
|
|
81
|
+
help="Port for internal services (default: 8766)",
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
# Optional: State directory
|
claude_mpm/cli/startup.py
CHANGED
|
@@ -34,13 +34,13 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
|
|
|
34
34
|
installer = HookInstaller()
|
|
35
35
|
|
|
36
36
|
# Show brief status (hooks sync is fast)
|
|
37
|
-
if not quiet:
|
|
37
|
+
if not quiet and sys.stdout.isatty():
|
|
38
38
|
print("Syncing Claude Code hooks...", end=" ", flush=True)
|
|
39
39
|
|
|
40
40
|
# Reinstall hooks (force=True ensures update)
|
|
41
41
|
success = installer.install_hooks(force=True)
|
|
42
42
|
|
|
43
|
-
if not quiet:
|
|
43
|
+
if not quiet and sys.stdout.isatty():
|
|
44
44
|
if success:
|
|
45
45
|
print("✓")
|
|
46
46
|
else:
|
|
@@ -49,7 +49,7 @@ def sync_hooks_on_startup(quiet: bool = False) -> bool:
|
|
|
49
49
|
return success
|
|
50
50
|
|
|
51
51
|
except Exception as e:
|
|
52
|
-
if not quiet:
|
|
52
|
+
if not quiet and sys.stdout.isatty():
|
|
53
53
|
print("(error)")
|
|
54
54
|
# Log but don't fail startup
|
|
55
55
|
from ..core.logger import get_logger
|
|
@@ -280,11 +280,13 @@ def deploy_bundled_skills():
|
|
|
280
280
|
if deployment_result.get("deployed"):
|
|
281
281
|
# Show simple feedback for deployed skills
|
|
282
282
|
deployed_count = len(deployment_result["deployed"])
|
|
283
|
-
|
|
283
|
+
if sys.stdout.isatty():
|
|
284
|
+
print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
|
|
284
285
|
logger.info(f"Skills: Deployed {deployed_count} skill(s)")
|
|
285
286
|
elif not deployment_result.get("errors"):
|
|
286
287
|
# No deployment needed, skills already present
|
|
287
|
-
|
|
288
|
+
if sys.stdout.isatty():
|
|
289
|
+
print("✓ Bundled skills ready", flush=True)
|
|
288
290
|
|
|
289
291
|
if deployment_result.get("errors"):
|
|
290
292
|
logger.warning(
|
|
@@ -318,7 +320,8 @@ def discover_and_link_runtime_skills():
|
|
|
318
320
|
|
|
319
321
|
discover_skills()
|
|
320
322
|
# Show simple success feedback
|
|
321
|
-
|
|
323
|
+
if sys.stdout.isatty():
|
|
324
|
+
print("✓ Runtime skills linked", flush=True)
|
|
322
325
|
except Exception as e:
|
|
323
326
|
# Import logger here to avoid circular imports
|
|
324
327
|
from ..core.logger import get_logger
|
|
@@ -373,7 +376,8 @@ def deploy_output_style_on_startup():
|
|
|
373
376
|
|
|
374
377
|
if all_up_to_date:
|
|
375
378
|
# Show feedback that output styles are ready
|
|
376
|
-
|
|
379
|
+
if sys.stdout.isatty():
|
|
380
|
+
print("✓ Output styles ready", flush=True)
|
|
377
381
|
return
|
|
378
382
|
|
|
379
383
|
# Deploy all styles using the manager
|
|
@@ -383,7 +387,8 @@ def deploy_output_style_on_startup():
|
|
|
383
387
|
deployed_count = sum(1 for success in results.values() if success)
|
|
384
388
|
|
|
385
389
|
if deployed_count > 0:
|
|
386
|
-
|
|
390
|
+
if sys.stdout.isatty():
|
|
391
|
+
print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
|
|
387
392
|
else:
|
|
388
393
|
# Deployment failed - log but don't fail startup
|
|
389
394
|
from ..core.logger import get_logger
|
|
@@ -478,6 +483,94 @@ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) ->
|
|
|
478
483
|
return removed_count
|
|
479
484
|
|
|
480
485
|
|
|
486
|
+
def _save_deployment_state_after_reconciliation(
|
|
487
|
+
agent_result, project_path: Path
|
|
488
|
+
) -> None:
|
|
489
|
+
"""Save deployment state after reconciliation to prevent duplicate deployment.
|
|
490
|
+
|
|
491
|
+
WHY: After perform_startup_reconciliation() deploys agents to .claude/agents/,
|
|
492
|
+
we need to save a deployment state file so that ClaudeRunner.setup_agents()
|
|
493
|
+
can detect agents are already deployed and skip redundant deployment.
|
|
494
|
+
|
|
495
|
+
This prevents the "✓ Deployed 31 native agents" duplicate deployment that
|
|
496
|
+
occurs when setup_agents() doesn't know reconciliation already ran.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
agent_result: DeploymentResult from perform_startup_reconciliation()
|
|
500
|
+
project_path: Project root directory
|
|
501
|
+
|
|
502
|
+
DESIGN DECISION: Use same state file format as ClaudeRunner._save_deployment_state()
|
|
503
|
+
Located at: .claude-mpm/cache/deployment_state.json
|
|
504
|
+
|
|
505
|
+
State file format:
|
|
506
|
+
{
|
|
507
|
+
"version": "5.6.13",
|
|
508
|
+
"agent_count": 15,
|
|
509
|
+
"deployment_hash": "sha256:...",
|
|
510
|
+
"deployed_at": 1234567890.123
|
|
511
|
+
}
|
|
512
|
+
"""
|
|
513
|
+
import hashlib
|
|
514
|
+
import json
|
|
515
|
+
import time
|
|
516
|
+
|
|
517
|
+
from ..core.logger import get_logger
|
|
518
|
+
|
|
519
|
+
logger = get_logger("cli")
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
# Get version from package
|
|
523
|
+
from claude_mpm import __version__
|
|
524
|
+
|
|
525
|
+
# Path to state file (matches ClaudeRunner._get_deployment_state_path())
|
|
526
|
+
state_file = project_path / ".claude-mpm" / "cache" / "deployment_state.json"
|
|
527
|
+
agents_dir = project_path / ".claude" / "agents"
|
|
528
|
+
|
|
529
|
+
# Count deployed agents
|
|
530
|
+
if agents_dir.exists():
|
|
531
|
+
agent_count = len(list(agents_dir.glob("*.md")))
|
|
532
|
+
else:
|
|
533
|
+
agent_count = 0
|
|
534
|
+
|
|
535
|
+
# Calculate deployment hash (matches ClaudeRunner._calculate_deployment_hash())
|
|
536
|
+
# CRITICAL: Must match exact hash algorithm used in ClaudeRunner
|
|
537
|
+
# Hashes filename + file content (not mtime) for consistency
|
|
538
|
+
deployment_hash = ""
|
|
539
|
+
if agents_dir.exists():
|
|
540
|
+
agent_files = sorted(agents_dir.glob("*.md"))
|
|
541
|
+
hash_obj = hashlib.sha256()
|
|
542
|
+
for agent_file in agent_files:
|
|
543
|
+
# Include filename and content in hash (matches ClaudeRunner)
|
|
544
|
+
hash_obj.update(agent_file.name.encode())
|
|
545
|
+
try:
|
|
546
|
+
hash_obj.update(agent_file.read_bytes())
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.debug(f"Error reading {agent_file} for hash: {e}")
|
|
549
|
+
|
|
550
|
+
deployment_hash = hash_obj.hexdigest()
|
|
551
|
+
|
|
552
|
+
# Create state data
|
|
553
|
+
state_data = {
|
|
554
|
+
"version": __version__,
|
|
555
|
+
"agent_count": agent_count,
|
|
556
|
+
"deployment_hash": deployment_hash,
|
|
557
|
+
"deployed_at": time.time(),
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# Ensure directory exists
|
|
561
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
562
|
+
|
|
563
|
+
# Write state file
|
|
564
|
+
state_file.write_text(json.dumps(state_data, indent=2))
|
|
565
|
+
logger.debug(
|
|
566
|
+
f"Saved deployment state after reconciliation: {agent_count} agents"
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
# Non-critical error - log but don't fail startup
|
|
571
|
+
logger.debug(f"Failed to save deployment state: {e}")
|
|
572
|
+
|
|
573
|
+
|
|
481
574
|
def sync_remote_agents_on_startup(force_sync: bool = False):
|
|
482
575
|
"""
|
|
483
576
|
Synchronize agent templates from remote sources on startup.
|
|
@@ -640,6 +733,12 @@ def sync_remote_agents_on_startup(force_sync: bool = False):
|
|
|
640
733
|
)
|
|
641
734
|
print(" Run with --verbose for detailed error information.\n")
|
|
642
735
|
|
|
736
|
+
# Save deployment state to prevent duplicate deployment in ClaudeRunner
|
|
737
|
+
# This ensures setup_agents() skips deployment since we already reconciled
|
|
738
|
+
_save_deployment_state_after_reconciliation(
|
|
739
|
+
agent_result=agent_result, project_path=project_path
|
|
740
|
+
)
|
|
741
|
+
|
|
643
742
|
except Exception as e:
|
|
644
743
|
# Deployment failure shouldn't block startup
|
|
645
744
|
from ..core.logger import get_logger
|
|
@@ -1196,9 +1295,11 @@ def verify_and_show_pm_skills():
|
|
|
1196
1295
|
if result.verified:
|
|
1197
1296
|
# Show verified status with count
|
|
1198
1297
|
total_required = len(REQUIRED_PM_SKILLS)
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1298
|
+
if sys.stdout.isatty():
|
|
1299
|
+
print(
|
|
1300
|
+
f"✓ PM skills: {total_required}/{total_required} verified",
|
|
1301
|
+
flush=True,
|
|
1302
|
+
)
|
|
1202
1303
|
else:
|
|
1203
1304
|
# Show warning with details
|
|
1204
1305
|
missing_count = len(result.missing_skills)
|
|
@@ -1217,13 +1318,15 @@ def verify_and_show_pm_skills():
|
|
|
1217
1318
|
if "Auto-repaired" in result.message:
|
|
1218
1319
|
# Auto-repair succeeded
|
|
1219
1320
|
total_required = len(REQUIRED_PM_SKILLS)
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1321
|
+
if sys.stdout.isatty():
|
|
1322
|
+
print(
|
|
1323
|
+
f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
|
|
1324
|
+
flush=True,
|
|
1325
|
+
)
|
|
1224
1326
|
else:
|
|
1225
1327
|
# Auto-repair failed or not attempted
|
|
1226
|
-
|
|
1328
|
+
if sys.stdout.isatty():
|
|
1329
|
+
print(f"⚠ PM skills: {status}", flush=True)
|
|
1227
1330
|
|
|
1228
1331
|
# Log warnings for debugging
|
|
1229
1332
|
from ..core.logger import get_logger
|
|
@@ -1422,7 +1525,9 @@ def check_mcp_auto_configuration():
|
|
|
1422
1525
|
from ..services.mcp_gateway.auto_configure import check_and_configure_mcp
|
|
1423
1526
|
|
|
1424
1527
|
# Show progress feedback - this operation can take 10+ seconds
|
|
1425
|
-
|
|
1528
|
+
# Only show progress message in TTY mode to avoid interfering with Claude Code's status display
|
|
1529
|
+
if sys.stdout.isatty():
|
|
1530
|
+
print("Checking MCP configuration...", end="", flush=True)
|
|
1426
1531
|
|
|
1427
1532
|
# This function handles all the logic:
|
|
1428
1533
|
# - Checks if already configured
|
|
@@ -1433,11 +1538,17 @@ def check_mcp_auto_configuration():
|
|
|
1433
1538
|
check_and_configure_mcp()
|
|
1434
1539
|
|
|
1435
1540
|
# Clear the "Checking..." message by overwriting with spaces
|
|
1436
|
-
|
|
1541
|
+
# Only use carriage return clearing if stdout is a real TTY
|
|
1542
|
+
if sys.stdout.isatty():
|
|
1543
|
+
print("\r" + " " * 30 + "\r", end="", flush=True)
|
|
1544
|
+
# In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
|
|
1437
1545
|
|
|
1438
1546
|
except Exception as e:
|
|
1439
1547
|
# Clear progress message on error
|
|
1440
|
-
|
|
1548
|
+
# Only use carriage return clearing if stdout is a real TTY
|
|
1549
|
+
if sys.stdout.isatty():
|
|
1550
|
+
print("\r" + " " * 30 + "\r", end="", flush=True)
|
|
1551
|
+
# In non-TTY mode, don't print anything - the "Checking..." message will just remain on its line
|
|
1441
1552
|
|
|
1442
1553
|
# Non-critical - log but don't fail
|
|
1443
1554
|
from ..core.logger import get_logger
|
claude_mpm/commander/chat/cli.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
@@ -26,20 +27,47 @@ load_env()
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
@dataclass
|
|
31
|
+
class CommanderCLIConfig:
|
|
32
|
+
"""Configuration for Commander CLI mode.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
summarize_responses: Whether to use LLM to summarize instance responses
|
|
36
|
+
port: Port for internal services (reserved for future use)
|
|
37
|
+
state_dir: Directory for state persistence (optional)
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> config = CommanderCLIConfig(summarize_responses=False)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
summarize_responses: bool = True
|
|
44
|
+
port: int = 8765
|
|
45
|
+
state_dir: Optional[Path] = None
|
|
46
|
+
|
|
47
|
+
|
|
29
48
|
async def run_commander(
|
|
30
49
|
port: int = 8765,
|
|
31
50
|
state_dir: Optional[Path] = None,
|
|
51
|
+
config: Optional[CommanderCLIConfig] = None,
|
|
32
52
|
) -> None:
|
|
33
53
|
"""Run Commander in interactive mode.
|
|
34
54
|
|
|
35
55
|
Args:
|
|
36
56
|
port: Port for internal services (unused currently).
|
|
37
57
|
state_dir: Directory for state persistence (optional).
|
|
58
|
+
config: Commander CLI configuration (optional, uses defaults if None).
|
|
38
59
|
|
|
39
60
|
Example:
|
|
40
61
|
>>> asyncio.run(run_commander())
|
|
41
62
|
# Starts interactive Commander REPL
|
|
63
|
+
>>> config = CommanderCLIConfig(summarize_responses=False)
|
|
64
|
+
>>> asyncio.run(run_commander(config=config))
|
|
65
|
+
# Starts Commander without response summarization
|
|
42
66
|
"""
|
|
67
|
+
# Use default config if not provided
|
|
68
|
+
if config is None:
|
|
69
|
+
config = CommanderCLIConfig(port=port, state_dir=state_dir)
|
|
70
|
+
|
|
43
71
|
# Setup logging
|
|
44
72
|
logging.basicConfig(
|
|
45
73
|
level=logging.INFO,
|
|
@@ -61,8 +89,8 @@ async def run_commander(
|
|
|
61
89
|
# Try to initialize LLM client (optional)
|
|
62
90
|
llm_client: Optional[OpenRouterClient] = None
|
|
63
91
|
try:
|
|
64
|
-
|
|
65
|
-
llm_client = OpenRouterClient(
|
|
92
|
+
llm_config = OpenRouterConfig()
|
|
93
|
+
llm_client = OpenRouterClient(llm_config)
|
|
66
94
|
logger.info("LLM client initialized")
|
|
67
95
|
except ValueError as e:
|
|
68
96
|
logger.warning(f"LLM client not available: {e}")
|
|
@@ -72,7 +100,14 @@ async def run_commander(
|
|
|
72
100
|
output_relay: Optional[OutputRelay] = None
|
|
73
101
|
if llm_client:
|
|
74
102
|
try:
|
|
75
|
-
summarizer
|
|
103
|
+
# Only create summarizer if summarize_responses is enabled
|
|
104
|
+
summarizer = None
|
|
105
|
+
if config.summarize_responses:
|
|
106
|
+
summarizer = OutputSummarizer(llm_client)
|
|
107
|
+
logger.info("Response summarization enabled")
|
|
108
|
+
else:
|
|
109
|
+
logger.info("Response summarization disabled")
|
|
110
|
+
|
|
76
111
|
handler = OutputHandler(orchestrator, summarizer)
|
|
77
112
|
formatter = OutputFormatter()
|
|
78
113
|
output_relay = OutputRelay(handler, formatter)
|
claude_mpm/commander/config.py
CHANGED
|
@@ -14,28 +14,30 @@ class DaemonConfig:
|
|
|
14
14
|
|
|
15
15
|
Attributes:
|
|
16
16
|
host: API server bind address
|
|
17
|
-
port: API server port
|
|
17
|
+
port: API server port (default: 8766 from NetworkPorts.COMMANDER_DEFAULT)
|
|
18
18
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
19
19
|
state_dir: Directory for state persistence
|
|
20
20
|
max_projects: Maximum concurrent projects
|
|
21
21
|
healthcheck_interval: Healthcheck interval in seconds
|
|
22
22
|
save_interval: State persistence interval in seconds
|
|
23
23
|
poll_interval: Event polling interval in seconds
|
|
24
|
+
summarize_responses: Whether to use LLM to summarize instance responses
|
|
24
25
|
|
|
25
26
|
Example:
|
|
26
|
-
>>> config = DaemonConfig(port=
|
|
27
|
+
>>> config = DaemonConfig(port=8766, log_level="DEBUG")
|
|
27
28
|
>>> config.state_dir
|
|
28
29
|
PosixPath('/Users/user/.claude-mpm/commander')
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
32
|
host: str = "127.0.0.1"
|
|
32
|
-
port: int =
|
|
33
|
+
port: int = 8766 # Default commander port (from network_config.NetworkPorts.COMMANDER_DEFAULT)
|
|
33
34
|
log_level: str = "INFO"
|
|
34
35
|
state_dir: Path = Path.home() / ".claude-mpm" / "commander"
|
|
35
36
|
max_projects: int = 10
|
|
36
37
|
healthcheck_interval: int = 30
|
|
37
38
|
save_interval: int = 30
|
|
38
39
|
poll_interval: float = 2.0
|
|
40
|
+
summarize_responses: bool = True
|
|
39
41
|
|
|
40
42
|
def __post_init__(self):
|
|
41
43
|
"""Ensure state_dir is a Path object and create if needed."""
|
claude_mpm/commander/daemon.py
CHANGED
|
@@ -307,7 +307,16 @@ class CommanderDaemon:
|
|
|
307
307
|
|
|
308
308
|
Registers handlers for SIGINT and SIGTERM that trigger
|
|
309
309
|
daemon shutdown via asyncio event loop.
|
|
310
|
+
|
|
311
|
+
Note: Signal handlers can only be registered from the main thread.
|
|
312
|
+
If called from a background thread, registration is skipped.
|
|
310
313
|
"""
|
|
314
|
+
import threading
|
|
315
|
+
|
|
316
|
+
# Signal handlers can only be registered from the main thread
|
|
317
|
+
if threading.current_thread() is not threading.main_thread():
|
|
318
|
+
logger.info("Running in background thread - signal handlers skipped")
|
|
319
|
+
return
|
|
311
320
|
|
|
312
321
|
def handle_signal(signum: int, frame) -> None:
|
|
313
322
|
"""Handle shutdown signal.
|
|
@@ -19,6 +19,7 @@ class InstanceInfo:
|
|
|
19
19
|
pane_target: Tmux pane target (e.g., "%1")
|
|
20
20
|
git_branch: Current git branch if project is a git repo
|
|
21
21
|
git_status: Git status summary if project is a git repo
|
|
22
|
+
connected: Whether instance has an active adapter connection
|
|
22
23
|
|
|
23
24
|
Example:
|
|
24
25
|
>>> info = InstanceInfo(
|
|
@@ -28,7 +29,8 @@ class InstanceInfo:
|
|
|
28
29
|
... tmux_session="mpm-commander",
|
|
29
30
|
... pane_target="%1",
|
|
30
31
|
... git_branch="main",
|
|
31
|
-
... git_status="clean"
|
|
32
|
+
... git_status="clean",
|
|
33
|
+
... connected=True
|
|
32
34
|
... )
|
|
33
35
|
"""
|
|
34
36
|
|
|
@@ -39,6 +41,7 @@ class InstanceInfo:
|
|
|
39
41
|
pane_target: str
|
|
40
42
|
git_branch: Optional[str] = None
|
|
41
43
|
git_status: Optional[str] = None
|
|
44
|
+
connected: bool = False
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
class BaseFramework(ABC):
|
|
@@ -167,6 +167,20 @@ class InstanceManager:
|
|
|
167
167
|
startup_cmd = framework_obj.get_startup_command(project_path)
|
|
168
168
|
self.orchestrator.send_keys(pane_target, startup_cmd)
|
|
169
169
|
|
|
170
|
+
# Create communication adapter for the instance (only for Claude Code for now)
|
|
171
|
+
# Do this BEFORE creating InstanceInfo so we can set connected=True
|
|
172
|
+
has_adapter = False
|
|
173
|
+
if framework == "cc":
|
|
174
|
+
runtime_adapter = ClaudeCodeAdapter()
|
|
175
|
+
comm_adapter = ClaudeCodeCommunicationAdapter(
|
|
176
|
+
orchestrator=self.orchestrator,
|
|
177
|
+
pane_target=pane_target,
|
|
178
|
+
runtime_adapter=runtime_adapter,
|
|
179
|
+
)
|
|
180
|
+
self._adapters[name] = comm_adapter
|
|
181
|
+
has_adapter = True
|
|
182
|
+
logger.debug(f"Created communication adapter for instance '{name}'")
|
|
183
|
+
|
|
170
184
|
# Create instance info
|
|
171
185
|
instance = InstanceInfo(
|
|
172
186
|
name=name,
|
|
@@ -176,22 +190,12 @@ class InstanceManager:
|
|
|
176
190
|
pane_target=pane_target,
|
|
177
191
|
git_branch=git_branch,
|
|
178
192
|
git_status=git_status,
|
|
193
|
+
connected=has_adapter,
|
|
179
194
|
)
|
|
180
195
|
|
|
181
196
|
# Track instance
|
|
182
197
|
self._instances[name] = instance
|
|
183
198
|
|
|
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
199
|
logger.info(
|
|
196
200
|
f"Started instance '{name}' with framework '{framework}' at {project_path}"
|
|
197
201
|
)
|
|
@@ -226,6 +230,7 @@ class InstanceManager:
|
|
|
226
230
|
# Remove adapter if exists
|
|
227
231
|
if name in self._adapters:
|
|
228
232
|
del self._adapters[name]
|
|
233
|
+
instance.connected = False
|
|
229
234
|
logger.debug(f"Removed adapter for instance '{name}'")
|
|
230
235
|
|
|
231
236
|
# Remove from tracking
|
|
@@ -335,3 +340,111 @@ class InstanceManager:
|
|
|
335
340
|
... print(chunk, end='')
|
|
336
341
|
"""
|
|
337
342
|
return self._adapters.get(name)
|
|
343
|
+
|
|
344
|
+
async def rename_instance(self, old_name: str, new_name: str) -> bool:
|
|
345
|
+
"""Rename an instance.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
old_name: Current instance name
|
|
349
|
+
new_name: New instance name
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if renamed successfully
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
InstanceNotFoundError: If old_name doesn't exist
|
|
356
|
+
InstanceAlreadyExistsError: If new_name already exists
|
|
357
|
+
|
|
358
|
+
Example:
|
|
359
|
+
>>> manager = InstanceManager(orchestrator)
|
|
360
|
+
>>> await manager.rename_instance("myapp", "myapp-v2")
|
|
361
|
+
True
|
|
362
|
+
"""
|
|
363
|
+
# Validate old_name exists
|
|
364
|
+
if old_name not in self._instances:
|
|
365
|
+
raise InstanceNotFoundError(old_name)
|
|
366
|
+
|
|
367
|
+
# Validate new_name doesn't exist
|
|
368
|
+
if new_name in self._instances:
|
|
369
|
+
raise InstanceAlreadyExistsError(new_name)
|
|
370
|
+
|
|
371
|
+
# Get instance and update name
|
|
372
|
+
instance = self._instances[old_name]
|
|
373
|
+
instance.name = new_name
|
|
374
|
+
|
|
375
|
+
# Update _instances dict (remove old key, add new)
|
|
376
|
+
del self._instances[old_name]
|
|
377
|
+
self._instances[new_name] = instance
|
|
378
|
+
|
|
379
|
+
# Update _adapters dict if exists
|
|
380
|
+
if old_name in self._adapters:
|
|
381
|
+
adapter = self._adapters[old_name]
|
|
382
|
+
del self._adapters[old_name]
|
|
383
|
+
self._adapters[new_name] = adapter
|
|
384
|
+
logger.debug(f"Moved adapter from '{old_name}' to '{new_name}'")
|
|
385
|
+
|
|
386
|
+
logger.info(f"Renamed instance from '{old_name}' to '{new_name}'")
|
|
387
|
+
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
async def close_instance(self, name: str) -> bool:
|
|
391
|
+
"""Close and remove an instance.
|
|
392
|
+
|
|
393
|
+
Alias for stop_instance that provides clearer semantics for closing.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
name: Instance name to close
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
True if closed successfully
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
InstanceNotFoundError: If instance not found
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
>>> manager = InstanceManager(orchestrator)
|
|
406
|
+
>>> await manager.close_instance("myapp")
|
|
407
|
+
True
|
|
408
|
+
"""
|
|
409
|
+
return await self.stop_instance(name)
|
|
410
|
+
|
|
411
|
+
async def disconnect_instance(self, name: str) -> bool:
|
|
412
|
+
"""Disconnect from an instance without closing it.
|
|
413
|
+
|
|
414
|
+
The instance keeps running but we stop communication.
|
|
415
|
+
Removes the adapter while keeping the instance tracked.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
name: Instance name to disconnect from
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
True if disconnected successfully
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
InstanceNotFoundError: If instance not found
|
|
425
|
+
|
|
426
|
+
Example:
|
|
427
|
+
>>> manager = InstanceManager(orchestrator)
|
|
428
|
+
>>> await manager.disconnect_instance("myapp")
|
|
429
|
+
True
|
|
430
|
+
>>> # Instance still running, but no adapter connection
|
|
431
|
+
>>> adapter = manager.get_adapter("myapp")
|
|
432
|
+
>>> print(adapter)
|
|
433
|
+
None
|
|
434
|
+
"""
|
|
435
|
+
# Validate instance exists
|
|
436
|
+
if name not in self._instances:
|
|
437
|
+
raise InstanceNotFoundError(name)
|
|
438
|
+
|
|
439
|
+
instance = self._instances[name]
|
|
440
|
+
|
|
441
|
+
# Remove adapter if exists (but keep instance)
|
|
442
|
+
if name in self._adapters:
|
|
443
|
+
# Could add cleanup here if adapter has resources to close
|
|
444
|
+
del self._adapters[name]
|
|
445
|
+
instance.connected = False
|
|
446
|
+
logger.info(f"Disconnected from instance '{name}' (instance still running)")
|
|
447
|
+
else:
|
|
448
|
+
logger.debug(f"No adapter to disconnect for instance '{name}'")
|
|
449
|
+
|
|
450
|
+
return True
|
claude_mpm/core/claude_runner.py
CHANGED
|
@@ -214,8 +214,12 @@ class ClaudeRunner:
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
def _get_deployment_state_path(self) -> Path:
|
|
217
|
-
"""Get path to deployment state file.
|
|
218
|
-
|
|
217
|
+
"""Get path to deployment state file.
|
|
218
|
+
|
|
219
|
+
CRITICAL: Must match path used by startup.py::_save_deployment_state_after_reconciliation()
|
|
220
|
+
Located at: .claude-mpm/cache/deployment_state.json
|
|
221
|
+
"""
|
|
222
|
+
return Path.cwd() / ".claude-mpm" / "cache" / "deployment_state.json"
|
|
219
223
|
|
|
220
224
|
def _calculate_deployment_hash(self, agents_dir: Path) -> str:
|
|
221
225
|
"""Calculate hash of all agent files for change detection.
|
|
@@ -331,18 +335,23 @@ class ClaudeRunner:
|
|
|
331
335
|
def setup_agents(self) -> bool:
|
|
332
336
|
"""Deploy native agents to .claude/agents/."""
|
|
333
337
|
try:
|
|
334
|
-
#
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
component="deployment",
|
|
338
|
+
# SIMPLE CHECK: If agents already exist from reconciliation, skip deployment
|
|
339
|
+
# This ensures reconciliation's user-configured agents are never overwritten
|
|
340
|
+
agents_dir = Path.cwd() / ".claude" / "agents"
|
|
341
|
+
if agents_dir.exists():
|
|
342
|
+
existing_agents = list(agents_dir.glob("*.md"))
|
|
343
|
+
if len(existing_agents) > 0:
|
|
344
|
+
# Reconciliation already deployed agents - skip
|
|
345
|
+
self.logger.debug(
|
|
346
|
+
f"Skipping setup_agents: {len(existing_agents)} agents already deployed by reconciliation"
|
|
344
347
|
)
|
|
345
|
-
|
|
348
|
+
if self.project_logger:
|
|
349
|
+
self.project_logger.log_system(
|
|
350
|
+
f"Agents already deployed via reconciliation: {len(existing_agents)} agents",
|
|
351
|
+
level="DEBUG",
|
|
352
|
+
component="deployment",
|
|
353
|
+
)
|
|
354
|
+
return True
|
|
346
355
|
|
|
347
356
|
if self.project_logger:
|
|
348
357
|
self.project_logger.log_system(
|