claude-mpm 5.6.10__py3-none-any.whl → 5.6.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of claude-mpm might be problematic. Click here for more details.
- claude_mpm/VERSION +1 -1
- claude_mpm/cli/commands/commander.py +174 -4
- claude_mpm/cli/parsers/commander_parser.py +43 -10
- claude_mpm/cli/startup.py +140 -20
- claude_mpm/cli/startup_display.py +2 -1
- claude_mpm/commander/__init__.py +6 -0
- claude_mpm/commander/adapters/__init__.py +32 -3
- claude_mpm/commander/adapters/auggie.py +260 -0
- claude_mpm/commander/adapters/base.py +98 -1
- claude_mpm/commander/adapters/claude_code.py +32 -1
- claude_mpm/commander/adapters/codex.py +237 -0
- claude_mpm/commander/adapters/example_usage.py +310 -0
- claude_mpm/commander/adapters/mpm.py +389 -0
- claude_mpm/commander/adapters/registry.py +204 -0
- claude_mpm/commander/api/app.py +32 -16
- claude_mpm/commander/api/routes/messages.py +11 -11
- claude_mpm/commander/api/routes/projects.py +20 -20
- claude_mpm/commander/api/routes/sessions.py +19 -21
- claude_mpm/commander/api/routes/work.py +86 -50
- claude_mpm/commander/api/schemas.py +4 -0
- claude_mpm/commander/chat/cli.py +42 -3
- claude_mpm/commander/config.py +5 -3
- claude_mpm/commander/core/__init__.py +10 -0
- claude_mpm/commander/core/block_manager.py +325 -0
- claude_mpm/commander/core/response_manager.py +323 -0
- claude_mpm/commander/daemon.py +215 -10
- claude_mpm/commander/env_loader.py +59 -0
- claude_mpm/commander/frameworks/base.py +4 -1
- claude_mpm/commander/instance_manager.py +124 -11
- claude_mpm/commander/memory/__init__.py +45 -0
- claude_mpm/commander/memory/compression.py +347 -0
- claude_mpm/commander/memory/embeddings.py +230 -0
- claude_mpm/commander/memory/entities.py +310 -0
- claude_mpm/commander/memory/example_usage.py +290 -0
- claude_mpm/commander/memory/integration.py +325 -0
- claude_mpm/commander/memory/search.py +381 -0
- claude_mpm/commander/memory/store.py +657 -0
- claude_mpm/commander/registry.py +10 -4
- claude_mpm/commander/runtime/monitor.py +32 -2
- claude_mpm/commander/work/executor.py +38 -20
- claude_mpm/commander/workflow/event_handler.py +25 -3
- claude_mpm/core/claude_runner.py +152 -0
- 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 +37 -7
- 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__/event_handlers.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/auto_pause_handler.py +1 -1
- claude_mpm/hooks/claude_hooks/event_handlers.py +284 -89
- claude_mpm/hooks/claude_hooks/hook_handler.py +81 -32
- claude_mpm/hooks/claude_hooks/installer.py +90 -28
- claude_mpm/hooks/claude_hooks/memory_integration.py +1 -1
- claude_mpm/hooks/claude_hooks/response_tracking.py +1 -1
- claude_mpm/hooks/claude_hooks/services/__init__.py +21 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/container.cpython-311.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/protocols.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/container.py +310 -0
- claude_mpm/hooks/claude_hooks/services/protocols.py +328 -0
- claude_mpm/hooks/claude_hooks/services/state_manager.py +2 -2
- claude_mpm/hooks/claude_hooks/services/subagent_processor.py +2 -2
- claude_mpm/hooks/templates/pre_tool_use_simple.py +6 -6
- claude_mpm/hooks/templates/pre_tool_use_template.py +6 -6
- claude_mpm/scripts/claude-hook-handler.sh +3 -3
- claude_mpm/services/command_deployment_service.py +44 -26
- claude_mpm/services/hook_installer_service.py +77 -8
- claude_mpm/services/pm_skills_deployer.py +3 -2
- claude_mpm/skills/__init__.py +2 -1
- claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
- claude_mpm/skills/registry.py +295 -90
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/METADATA +5 -3
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/RECORD +91 -94
- 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_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.10.dist-info → claude_mpm-5.6.33.dist-info}/WHEEL +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/entry_points.txt +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/licenses/LICENSE-FAQ.md +0 -0
- {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.33.dist-info}/top_level.txt +0 -0
claude_mpm/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.6.
|
|
1
|
+
5.6.33
|
|
@@ -2,25 +2,129 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
5
9
|
|
|
6
10
|
logger = logging.getLogger(__name__)
|
|
7
11
|
|
|
12
|
+
# ANSI colors
|
|
13
|
+
CYAN = "\033[36m"
|
|
14
|
+
DIM = "\033[2m"
|
|
15
|
+
BOLD = "\033[1m"
|
|
16
|
+
YELLOW = "\033[33m"
|
|
17
|
+
GREEN = "\033[32m"
|
|
18
|
+
RED = "\033[31m"
|
|
19
|
+
RESET = "\033[0m"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_terminal_width() -> int:
|
|
23
|
+
"""Get terminal width with reasonable bounds."""
|
|
24
|
+
try:
|
|
25
|
+
width = shutil.get_terminal_size().columns
|
|
26
|
+
return max(80, min(width, 120))
|
|
27
|
+
except Exception:
|
|
28
|
+
return 100
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_version() -> str:
|
|
32
|
+
"""Get Commander version."""
|
|
33
|
+
version_file = Path(__file__).parent.parent.parent / "VERSION"
|
|
34
|
+
if version_file.exists():
|
|
35
|
+
return version_file.read_text().strip()
|
|
36
|
+
return "unknown"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def display_commander_banner():
|
|
40
|
+
"""Display Commander-specific startup banner."""
|
|
41
|
+
width = _get_terminal_width()
|
|
42
|
+
version = _get_version()
|
|
43
|
+
|
|
44
|
+
# Commander ASCII art banner
|
|
45
|
+
banner = f"""
|
|
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
|
+
"""
|
|
53
|
+
print(banner)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _count_cached_agents() -> int:
|
|
57
|
+
"""Count cached agents from ~/.claude-mpm/cache/agents/."""
|
|
58
|
+
try:
|
|
59
|
+
cache_agents_dir = Path.home() / ".claude-mpm" / "cache" / "agents"
|
|
60
|
+
if not cache_agents_dir.exists():
|
|
61
|
+
return 0
|
|
62
|
+
# Recursively find all .md files excluding base/README files
|
|
63
|
+
agent_files = [
|
|
64
|
+
f
|
|
65
|
+
for f in cache_agents_dir.rglob("*.md")
|
|
66
|
+
if f.is_file()
|
|
67
|
+
and not f.name.startswith(".")
|
|
68
|
+
and f.name not in ("README.md", "BASE-AGENT.md", "INSTRUCTIONS.md")
|
|
69
|
+
]
|
|
70
|
+
return len(agent_files)
|
|
71
|
+
except Exception:
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _count_cached_skills() -> int:
|
|
76
|
+
"""Count cached skills from ~/.claude-mpm/cache/skills/."""
|
|
77
|
+
try:
|
|
78
|
+
cache_skills_dir = Path.home() / ".claude-mpm" / "cache" / "skills"
|
|
79
|
+
if not cache_skills_dir.exists():
|
|
80
|
+
return 0
|
|
81
|
+
# Recursively find all directories containing SKILL.md
|
|
82
|
+
skill_files = list(cache_skills_dir.rglob("SKILL.md"))
|
|
83
|
+
return len(skill_files)
|
|
84
|
+
except Exception:
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_agents_and_skills():
|
|
89
|
+
"""Load agents and skills for Commander sessions."""
|
|
90
|
+
try:
|
|
91
|
+
print(f"{DIM}Loading agents...{RESET}", end=" ", flush=True)
|
|
92
|
+
agent_count = _count_cached_agents()
|
|
93
|
+
print(f"{GREEN}✓{RESET} {agent_count} agents")
|
|
94
|
+
|
|
95
|
+
print(f"{DIM}Loading skills...{RESET}", end=" ", flush=True)
|
|
96
|
+
skill_count = _count_cached_skills()
|
|
97
|
+
print(f"{GREEN}✓{RESET} {skill_count} skills")
|
|
98
|
+
|
|
99
|
+
return agent_count, skill_count
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.warning(f"Could not load agents/skills: {e}")
|
|
102
|
+
print(f"{YELLOW}⚠{RESET} Could not load agents/skills")
|
|
103
|
+
return 0, 0
|
|
104
|
+
|
|
8
105
|
|
|
9
106
|
def handle_commander_command(args) -> int:
|
|
10
|
-
"""Handle the commander command.
|
|
107
|
+
"""Handle the commander command with auto-starting daemon.
|
|
11
108
|
|
|
12
109
|
Args:
|
|
13
110
|
args: Parsed command line arguments with:
|
|
14
|
-
- port: Port for
|
|
111
|
+
- port: Port for daemon (default: 8765)
|
|
112
|
+
- host: Host for daemon (default: 127.0.0.1)
|
|
15
113
|
- state_dir: Optional state directory path
|
|
16
114
|
- debug: Enable debug logging
|
|
115
|
+
- no_chat: Start daemon only without interactive chat
|
|
116
|
+
- daemon_only: Alias for no_chat
|
|
17
117
|
|
|
18
118
|
Returns:
|
|
19
119
|
Exit code (0 for success, 1 for error)
|
|
20
120
|
"""
|
|
21
121
|
try:
|
|
22
122
|
# Import here to avoid circular dependencies
|
|
123
|
+
import requests
|
|
124
|
+
|
|
23
125
|
from claude_mpm.commander.chat.cli import run_commander
|
|
126
|
+
from claude_mpm.commander.config import DaemonConfig
|
|
127
|
+
from claude_mpm.commander.daemon import main as daemon_main
|
|
24
128
|
|
|
25
129
|
# Setup debug logging if requested
|
|
26
130
|
if getattr(args, "debug", False):
|
|
@@ -29,11 +133,76 @@ def handle_commander_command(args) -> int:
|
|
|
29
133
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
30
134
|
)
|
|
31
135
|
|
|
136
|
+
# Display Commander banner
|
|
137
|
+
display_commander_banner()
|
|
138
|
+
|
|
139
|
+
# Load agents and skills
|
|
140
|
+
load_agents_and_skills()
|
|
141
|
+
|
|
142
|
+
print() # Blank line after loading
|
|
143
|
+
|
|
32
144
|
# Get arguments
|
|
33
|
-
port = getattr(args, "port",
|
|
145
|
+
port = getattr(args, "port", 8766) # NetworkPorts.COMMANDER_DEFAULT
|
|
146
|
+
host = getattr(args, "host", "127.0.0.1")
|
|
34
147
|
state_dir = getattr(args, "state_dir", None)
|
|
148
|
+
no_chat = getattr(args, "no_chat", False) or getattr(args, "daemon_only", False)
|
|
149
|
+
|
|
150
|
+
# Check if daemon already running
|
|
151
|
+
daemon_running = False
|
|
152
|
+
try:
|
|
153
|
+
resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
|
|
154
|
+
if resp.status_code == 200:
|
|
155
|
+
print(f"{GREEN}✓{RESET} Daemon already running on {host}:{port}")
|
|
156
|
+
daemon_running = True
|
|
157
|
+
except (requests.RequestException, requests.ConnectionError):
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
# Start daemon if not running
|
|
161
|
+
if not daemon_running:
|
|
162
|
+
print(
|
|
163
|
+
f"{DIM}Starting daemon on {host}:{port}...{RESET}", end=" ", flush=True
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Create daemon config
|
|
167
|
+
config_kwargs = {"host": host, "port": port}
|
|
168
|
+
if state_dir:
|
|
169
|
+
config_kwargs["state_dir"] = state_dir
|
|
170
|
+
config = DaemonConfig(**config_kwargs)
|
|
171
|
+
|
|
172
|
+
# Start daemon in background thread
|
|
173
|
+
daemon_thread = threading.Thread(
|
|
174
|
+
target=lambda: asyncio.run(daemon_main(config)), daemon=True
|
|
175
|
+
)
|
|
176
|
+
daemon_thread.start()
|
|
177
|
+
|
|
178
|
+
# Wait for daemon to be ready (max 3 seconds)
|
|
179
|
+
for _ in range(30):
|
|
180
|
+
time.sleep(0.1)
|
|
181
|
+
try:
|
|
182
|
+
resp = requests.get(f"http://{host}:{port}/api/health", timeout=1)
|
|
183
|
+
if resp.status_code == 200:
|
|
184
|
+
print(f"{GREEN}✓{RESET}")
|
|
185
|
+
daemon_running = True
|
|
186
|
+
break
|
|
187
|
+
except (requests.RequestException, requests.ConnectionError):
|
|
188
|
+
pass
|
|
189
|
+
else:
|
|
190
|
+
print(f"{RED}✗{RESET} Failed (timeout)")
|
|
191
|
+
return 1
|
|
192
|
+
|
|
193
|
+
# If daemon-only mode, keep running until interrupted
|
|
194
|
+
if no_chat:
|
|
195
|
+
print(f"\n{CYAN}Daemon running.{RESET} API at http://{host}:{port}")
|
|
196
|
+
print(f"{DIM}Press Ctrl+C to stop{RESET}\n")
|
|
197
|
+
try:
|
|
198
|
+
while True:
|
|
199
|
+
time.sleep(1)
|
|
200
|
+
except KeyboardInterrupt:
|
|
201
|
+
print(f"\n{DIM}Shutting down...{RESET}")
|
|
202
|
+
return 0
|
|
35
203
|
|
|
36
|
-
#
|
|
204
|
+
# Launch interactive chat
|
|
205
|
+
print(f"\n{CYAN}Entering Commander chat...{RESET}\n")
|
|
37
206
|
asyncio.run(run_commander(port=port, state_dir=state_dir))
|
|
38
207
|
|
|
39
208
|
return 0
|
|
@@ -43,4 +212,5 @@ def handle_commander_command(args) -> int:
|
|
|
43
212
|
return 0
|
|
44
213
|
except Exception as e:
|
|
45
214
|
logger.error(f"Commander error: {e}", exc_info=True)
|
|
215
|
+
print(f"{RED}Error:{RESET} {e}")
|
|
46
216
|
return 1
|
|
@@ -23,17 +23,21 @@ def add_commander_subparser(subparsers: argparse._SubParsersAction) -> None:
|
|
|
23
23
|
"""
|
|
24
24
|
commander_parser = subparsers.add_parser(
|
|
25
25
|
"commander",
|
|
26
|
-
help="
|
|
26
|
+
help="Launch Commander multi-project orchestration (ALPHA)",
|
|
27
27
|
description="""
|
|
28
|
-
Commander Mode -
|
|
28
|
+
Commander Mode - Multi-Project Orchestration (ALPHA)
|
|
29
29
|
|
|
30
|
-
Commander
|
|
31
|
-
|
|
32
|
-
- Connecting to instances and sending natural language commands
|
|
33
|
-
- Managing multiple concurrent projects
|
|
34
|
-
- Viewing instance status and output
|
|
30
|
+
The commander subcommand auto-starts the Commander daemon (if not already running)
|
|
31
|
+
and launches an interactive REPL for managing multiple Claude Code instances.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Commander provides:
|
|
34
|
+
- Auto-starting daemon that manages project lifecycles
|
|
35
|
+
- Interactive REPL for controlling instances
|
|
36
|
+
- Tmux-based session management
|
|
37
|
+
- Real-time output monitoring
|
|
38
|
+
- REST API for external control (http://127.0.0.1:8765)
|
|
39
|
+
|
|
40
|
+
REPL Commands:
|
|
37
41
|
list, ls, instances List active instances
|
|
38
42
|
start <path> Start new instance at path
|
|
39
43
|
--framework <cc|mpm> Specify framework (default: cc)
|
|
@@ -50,7 +54,16 @@ Natural Language:
|
|
|
50
54
|
command will be sent to the connected instance as a message.
|
|
51
55
|
|
|
52
56
|
Examples:
|
|
57
|
+
# Start daemon and launch interactive chat
|
|
53
58
|
claude-mpm commander
|
|
59
|
+
|
|
60
|
+
# Start daemon only (no chat interface)
|
|
61
|
+
claude-mpm commander --daemon-only
|
|
62
|
+
|
|
63
|
+
# Use custom port
|
|
64
|
+
claude-mpm commander --port 9000
|
|
65
|
+
|
|
66
|
+
# In REPL:
|
|
54
67
|
> start ~/myproject --framework cc --name myapp
|
|
55
68
|
> connect myapp
|
|
56
69
|
> Fix the authentication bug in login.py
|
|
@@ -64,8 +77,8 @@ Examples:
|
|
|
64
77
|
commander_parser.add_argument(
|
|
65
78
|
"--port",
|
|
66
79
|
type=int,
|
|
67
|
-
default=
|
|
68
|
-
help="Port for internal services (default:
|
|
80
|
+
default=8766, # NetworkPorts.COMMANDER_DEFAULT
|
|
81
|
+
help="Port for internal services (default: 8766)",
|
|
69
82
|
)
|
|
70
83
|
|
|
71
84
|
# Optional: State directory
|
|
@@ -81,3 +94,23 @@ Examples:
|
|
|
81
94
|
action="store_true",
|
|
82
95
|
help="Enable debug logging",
|
|
83
96
|
)
|
|
97
|
+
|
|
98
|
+
# Daemon auto-start options
|
|
99
|
+
commander_parser.add_argument(
|
|
100
|
+
"--host",
|
|
101
|
+
type=str,
|
|
102
|
+
default="127.0.0.1",
|
|
103
|
+
help="Daemon host (default: 127.0.0.1)",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
commander_parser.add_argument(
|
|
107
|
+
"--no-chat",
|
|
108
|
+
action="store_true",
|
|
109
|
+
help="Start daemon only without interactive chat",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
commander_parser.add_argument(
|
|
113
|
+
"--daemon-only",
|
|
114
|
+
action="store_true",
|
|
115
|
+
help="Alias for --no-chat (start daemon only)",
|
|
116
|
+
)
|
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
|
|
@@ -210,7 +210,16 @@ def should_skip_background_services(args, processed_argv):
|
|
|
210
210
|
return any(cmd in (processed_argv or sys.argv[1:]) for cmd in skip_commands) or (
|
|
211
211
|
hasattr(args, "command")
|
|
212
212
|
and args.command
|
|
213
|
-
in [
|
|
213
|
+
in [
|
|
214
|
+
"info",
|
|
215
|
+
"doctor",
|
|
216
|
+
"config",
|
|
217
|
+
"mcp",
|
|
218
|
+
"configure",
|
|
219
|
+
"hook-errors",
|
|
220
|
+
"autotodos",
|
|
221
|
+
"commander",
|
|
222
|
+
]
|
|
214
223
|
)
|
|
215
224
|
|
|
216
225
|
|
|
@@ -271,11 +280,13 @@ def deploy_bundled_skills():
|
|
|
271
280
|
if deployment_result.get("deployed"):
|
|
272
281
|
# Show simple feedback for deployed skills
|
|
273
282
|
deployed_count = len(deployment_result["deployed"])
|
|
274
|
-
|
|
283
|
+
if sys.stdout.isatty():
|
|
284
|
+
print(f"✓ Bundled skills ready ({deployed_count} deployed)", flush=True)
|
|
275
285
|
logger.info(f"Skills: Deployed {deployed_count} skill(s)")
|
|
276
286
|
elif not deployment_result.get("errors"):
|
|
277
287
|
# No deployment needed, skills already present
|
|
278
|
-
|
|
288
|
+
if sys.stdout.isatty():
|
|
289
|
+
print("✓ Bundled skills ready", flush=True)
|
|
279
290
|
|
|
280
291
|
if deployment_result.get("errors"):
|
|
281
292
|
logger.warning(
|
|
@@ -309,7 +320,8 @@ def discover_and_link_runtime_skills():
|
|
|
309
320
|
|
|
310
321
|
discover_skills()
|
|
311
322
|
# Show simple success feedback
|
|
312
|
-
|
|
323
|
+
if sys.stdout.isatty():
|
|
324
|
+
print("✓ Runtime skills linked", flush=True)
|
|
313
325
|
except Exception as e:
|
|
314
326
|
# Import logger here to avoid circular imports
|
|
315
327
|
from ..core.logger import get_logger
|
|
@@ -364,7 +376,8 @@ def deploy_output_style_on_startup():
|
|
|
364
376
|
|
|
365
377
|
if all_up_to_date:
|
|
366
378
|
# Show feedback that output styles are ready
|
|
367
|
-
|
|
379
|
+
if sys.stdout.isatty():
|
|
380
|
+
print("✓ Output styles ready", flush=True)
|
|
368
381
|
return
|
|
369
382
|
|
|
370
383
|
# Deploy all styles using the manager
|
|
@@ -374,7 +387,8 @@ def deploy_output_style_on_startup():
|
|
|
374
387
|
deployed_count = sum(1 for success in results.values() if success)
|
|
375
388
|
|
|
376
389
|
if deployed_count > 0:
|
|
377
|
-
|
|
390
|
+
if sys.stdout.isatty():
|
|
391
|
+
print(f"✓ Output styles deployed ({deployed_count} styles)", flush=True)
|
|
378
392
|
else:
|
|
379
393
|
# Deployment failed - log but don't fail startup
|
|
380
394
|
from ..core.logger import get_logger
|
|
@@ -469,6 +483,94 @@ def _cleanup_orphaned_agents(deploy_target: Path, deployed_agents: list[str]) ->
|
|
|
469
483
|
return removed_count
|
|
470
484
|
|
|
471
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
|
+
|
|
472
574
|
def sync_remote_agents_on_startup(force_sync: bool = False):
|
|
473
575
|
"""
|
|
474
576
|
Synchronize agent templates from remote sources on startup.
|
|
@@ -631,6 +733,12 @@ def sync_remote_agents_on_startup(force_sync: bool = False):
|
|
|
631
733
|
)
|
|
632
734
|
print(" Run with --verbose for detailed error information.\n")
|
|
633
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
|
+
|
|
634
742
|
except Exception as e:
|
|
635
743
|
# Deployment failure shouldn't block startup
|
|
636
744
|
from ..core.logger import get_logger
|
|
@@ -1187,9 +1295,11 @@ def verify_and_show_pm_skills():
|
|
|
1187
1295
|
if result.verified:
|
|
1188
1296
|
# Show verified status with count
|
|
1189
1297
|
total_required = len(REQUIRED_PM_SKILLS)
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1298
|
+
if sys.stdout.isatty():
|
|
1299
|
+
print(
|
|
1300
|
+
f"✓ PM skills: {total_required}/{total_required} verified",
|
|
1301
|
+
flush=True,
|
|
1302
|
+
)
|
|
1193
1303
|
else:
|
|
1194
1304
|
# Show warning with details
|
|
1195
1305
|
missing_count = len(result.missing_skills)
|
|
@@ -1208,13 +1318,15 @@ def verify_and_show_pm_skills():
|
|
|
1208
1318
|
if "Auto-repaired" in result.message:
|
|
1209
1319
|
# Auto-repair succeeded
|
|
1210
1320
|
total_required = len(REQUIRED_PM_SKILLS)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1321
|
+
if sys.stdout.isatty():
|
|
1322
|
+
print(
|
|
1323
|
+
f"✓ PM skills: {total_required}/{total_required} verified (auto-repaired)",
|
|
1324
|
+
flush=True,
|
|
1325
|
+
)
|
|
1215
1326
|
else:
|
|
1216
1327
|
# Auto-repair failed or not attempted
|
|
1217
|
-
|
|
1328
|
+
if sys.stdout.isatty():
|
|
1329
|
+
print(f"⚠ PM skills: {status}", flush=True)
|
|
1218
1330
|
|
|
1219
1331
|
# Log warnings for debugging
|
|
1220
1332
|
from ..core.logger import get_logger
|
|
@@ -1413,7 +1525,9 @@ def check_mcp_auto_configuration():
|
|
|
1413
1525
|
from ..services.mcp_gateway.auto_configure import check_and_configure_mcp
|
|
1414
1526
|
|
|
1415
1527
|
# Show progress feedback - this operation can take 10+ seconds
|
|
1416
|
-
|
|
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)
|
|
1417
1531
|
|
|
1418
1532
|
# This function handles all the logic:
|
|
1419
1533
|
# - Checks if already configured
|
|
@@ -1424,11 +1538,17 @@ def check_mcp_auto_configuration():
|
|
|
1424
1538
|
check_and_configure_mcp()
|
|
1425
1539
|
|
|
1426
1540
|
# Clear the "Checking..." message by overwriting with spaces
|
|
1427
|
-
|
|
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
|
|
1428
1545
|
|
|
1429
1546
|
except Exception as e:
|
|
1430
1547
|
# Clear progress message on error
|
|
1431
|
-
|
|
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
|
|
1432
1552
|
|
|
1433
1553
|
# Non-critical - log but don't fail
|
|
1434
1554
|
from ..core.logger import get_logger
|
|
@@ -540,7 +540,8 @@ def should_show_banner(args) -> bool:
|
|
|
540
540
|
return False
|
|
541
541
|
|
|
542
542
|
# Check for commands that should skip banner
|
|
543
|
-
|
|
543
|
+
# Commander has its own banner, so skip the main MPM banner
|
|
544
|
+
skip_commands = {"info", "doctor", "config", "configure", "commander"}
|
|
544
545
|
if hasattr(args, "command") and args.command in skip_commands:
|
|
545
546
|
return False
|
|
546
547
|
|
claude_mpm/commander/__init__.py
CHANGED
|
@@ -12,12 +12,18 @@ Key Components:
|
|
|
12
12
|
- ProjectSession: Per-project lifecycle management
|
|
13
13
|
- InstanceManager: Framework selection and instance lifecycle
|
|
14
14
|
- Frameworks: Claude Code, MPM framework abstractions
|
|
15
|
+
- Memory: Conversation storage, semantic search, context compression
|
|
15
16
|
|
|
16
17
|
Example:
|
|
17
18
|
>>> from claude_mpm.commander import ProjectRegistry
|
|
18
19
|
>>> registry = ProjectRegistry()
|
|
19
20
|
>>> project = registry.register("/path/to/project")
|
|
20
21
|
>>> registry.update_state(project.id, ProjectState.WORKING)
|
|
22
|
+
|
|
23
|
+
>>> # Memory integration
|
|
24
|
+
>>> from claude_mpm.commander.memory import MemoryIntegration
|
|
25
|
+
>>> memory = MemoryIntegration.create()
|
|
26
|
+
>>> await memory.capture_project_conversation(project)
|
|
21
27
|
"""
|
|
22
28
|
|
|
23
29
|
from claude_mpm.commander.config import DaemonConfig
|
|
@@ -6,26 +6,55 @@ the TmuxOrchestrator to interface with various runtimes in a uniform way.
|
|
|
6
6
|
Two types of adapters:
|
|
7
7
|
- RuntimeAdapter: Synchronous parsing and state detection
|
|
8
8
|
- CommunicationAdapter: Async I/O and state management
|
|
9
|
+
|
|
10
|
+
Available Runtime Adapters:
|
|
11
|
+
- ClaudeCodeAdapter: Vanilla Claude Code CLI
|
|
12
|
+
- AuggieAdapter: Auggie with MCP support
|
|
13
|
+
- CodexAdapter: Codex (limited features)
|
|
14
|
+
- MPMAdapter: Full MPM with agents, hooks, skills, monitoring
|
|
15
|
+
|
|
16
|
+
Registry:
|
|
17
|
+
- AdapterRegistry: Centralized adapter management with auto-detection
|
|
9
18
|
"""
|
|
10
19
|
|
|
11
|
-
from .
|
|
20
|
+
from .auggie import AuggieAdapter
|
|
21
|
+
from .base import (
|
|
22
|
+
Capability,
|
|
23
|
+
ParsedResponse,
|
|
24
|
+
RuntimeAdapter,
|
|
25
|
+
RuntimeCapability,
|
|
26
|
+
RuntimeInfo,
|
|
27
|
+
)
|
|
12
28
|
from .claude_code import ClaudeCodeAdapter
|
|
29
|
+
from .codex import CodexAdapter
|
|
13
30
|
from .communication import (
|
|
14
31
|
AdapterResponse,
|
|
15
32
|
AdapterState,
|
|
16
33
|
BaseCommunicationAdapter,
|
|
17
34
|
ClaudeCodeCommunicationAdapter,
|
|
18
35
|
)
|
|
36
|
+
from .mpm import MPMAdapter
|
|
37
|
+
from .registry import AdapterRegistry
|
|
38
|
+
|
|
39
|
+
# Auto-register all adapters
|
|
40
|
+
AdapterRegistry.register("claude-code", ClaudeCodeAdapter)
|
|
41
|
+
AdapterRegistry.register("auggie", AuggieAdapter)
|
|
42
|
+
AdapterRegistry.register("codex", CodexAdapter)
|
|
43
|
+
AdapterRegistry.register("mpm", MPMAdapter)
|
|
19
44
|
|
|
20
45
|
__all__ = [
|
|
21
|
-
|
|
46
|
+
"AdapterRegistry",
|
|
22
47
|
"AdapterResponse",
|
|
23
48
|
"AdapterState",
|
|
49
|
+
"AuggieAdapter",
|
|
24
50
|
"BaseCommunicationAdapter",
|
|
25
|
-
# Runtime adapters (parsing)
|
|
26
51
|
"Capability",
|
|
27
52
|
"ClaudeCodeAdapter",
|
|
28
53
|
"ClaudeCodeCommunicationAdapter",
|
|
54
|
+
"CodexAdapter",
|
|
55
|
+
"MPMAdapter",
|
|
29
56
|
"ParsedResponse",
|
|
30
57
|
"RuntimeAdapter",
|
|
58
|
+
"RuntimeCapability",
|
|
59
|
+
"RuntimeInfo",
|
|
31
60
|
]
|