claude-mpm 5.0.2__py3-none-any.whl ā 5.4.3__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/agents/CLAUDE_MPM_TEACHER_OUTPUT_STYLE.md +2002 -0
- claude_mpm/agents/PM_INSTRUCTIONS.md +1218 -905
- claude_mpm/agents/agent_loader.py +10 -17
- claude_mpm/agents/base_agent_loader.py +10 -35
- claude_mpm/agents/frontmatter_validator.py +68 -0
- claude_mpm/agents/templates/circuit-breakers.md +431 -45
- claude_mpm/cli/__init__.py +0 -1
- claude_mpm/cli/commands/__init__.py +2 -0
- claude_mpm/cli/commands/agent_state_manager.py +67 -23
- claude_mpm/cli/commands/agents.py +446 -25
- claude_mpm/cli/commands/auto_configure.py +535 -233
- claude_mpm/cli/commands/configure.py +1500 -147
- claude_mpm/cli/commands/configure_agent_display.py +13 -6
- claude_mpm/cli/commands/mpm_init/core.py +158 -1
- claude_mpm/cli/commands/mpm_init/knowledge_extractor.py +481 -0
- claude_mpm/cli/commands/mpm_init/prompts.py +280 -0
- claude_mpm/cli/commands/postmortem.py +401 -0
- claude_mpm/cli/commands/run.py +1 -39
- claude_mpm/cli/commands/skills.py +322 -19
- claude_mpm/cli/commands/summarize.py +413 -0
- claude_mpm/cli/executor.py +8 -0
- claude_mpm/cli/interactive/agent_wizard.py +302 -195
- claude_mpm/cli/parsers/agents_parser.py +137 -0
- claude_mpm/cli/parsers/auto_configure_parser.py +13 -0
- claude_mpm/cli/parsers/base_parser.py +9 -0
- claude_mpm/cli/parsers/skills_parser.py +7 -0
- claude_mpm/cli/startup.py +133 -85
- claude_mpm/commands/mpm-agents-auto-configure.md +2 -2
- claude_mpm/commands/mpm-agents-list.md +2 -2
- claude_mpm/commands/mpm-config-view.md +2 -2
- claude_mpm/commands/mpm-help.md +3 -0
- claude_mpm/commands/{mpm-ticket-organize.md ā mpm-organize.md} +4 -5
- claude_mpm/commands/mpm-postmortem.md +123 -0
- claude_mpm/commands/mpm-session-resume.md +2 -2
- claude_mpm/commands/mpm-ticket-view.md +2 -2
- claude_mpm/config/agent_presets.py +312 -82
- claude_mpm/config/agent_sources.py +27 -0
- claude_mpm/config/skill_presets.py +392 -0
- claude_mpm/constants.py +1 -0
- claude_mpm/core/claude_runner.py +2 -25
- claude_mpm/core/framework/loaders/agent_loader.py +8 -5
- claude_mpm/core/framework/loaders/file_loader.py +54 -101
- claude_mpm/core/interactive_session.py +19 -5
- claude_mpm/core/oneshot_session.py +16 -4
- claude_mpm/core/output_style_manager.py +173 -43
- claude_mpm/core/protocols/__init__.py +23 -0
- claude_mpm/core/protocols/runner_protocol.py +103 -0
- claude_mpm/core/protocols/session_protocol.py +131 -0
- claude_mpm/core/shared/singleton_manager.py +11 -4
- claude_mpm/core/socketio_pool.py +3 -3
- claude_mpm/core/system_context.py +38 -0
- claude_mpm/core/unified_agent_registry.py +134 -16
- claude_mpm/core/unified_config.py +22 -0
- claude_mpm/hooks/claude_hooks/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/correlation_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/hook_handler.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/memory_integration.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/response_tracking.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/__pycache__/tool_analysis.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/correlation_manager.py +60 -0
- claude_mpm/hooks/claude_hooks/event_handlers.py +35 -2
- claude_mpm/hooks/claude_hooks/hook_handler.py +4 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +12 -1
- claude_mpm/hooks/claude_hooks/services/__pycache__/__init__.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager_http.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/duplicate_detector.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/state_manager.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/__pycache__/subagent_processor.cpython-313.pyc +0 -0
- claude_mpm/hooks/claude_hooks/services/connection_manager.py +4 -0
- claude_mpm/models/agent_definition.py +7 -0
- claude_mpm/scripts/launch_monitor.py +93 -13
- claude_mpm/services/agents/agent_recommendation_service.py +279 -0
- claude_mpm/services/agents/cache_git_manager.py +621 -0
- claude_mpm/services/agents/deployment/agent_template_builder.py +3 -2
- claude_mpm/services/agents/deployment/multi_source_deployment_service.py +110 -3
- claude_mpm/services/agents/deployment/remote_agent_discovery_service.py +518 -55
- claude_mpm/services/agents/git_source_manager.py +20 -0
- claude_mpm/services/agents/sources/git_source_sync_service.py +45 -6
- claude_mpm/services/agents/toolchain_detector.py +6 -5
- claude_mpm/services/analysis/__init__.py +35 -0
- claude_mpm/services/analysis/clone_detector.py +1030 -0
- claude_mpm/services/analysis/postmortem_reporter.py +474 -0
- claude_mpm/services/analysis/postmortem_service.py +765 -0
- claude_mpm/services/command_deployment_service.py +106 -5
- claude_mpm/services/core/base.py +7 -2
- claude_mpm/services/diagnostics/checks/mcp_services_check.py +7 -15
- claude_mpm/services/event_bus/config.py +3 -1
- claude_mpm/services/git/git_operations_service.py +8 -8
- claude_mpm/services/mcp_config_manager.py +75 -145
- claude_mpm/services/mcp_service_verifier.py +6 -3
- claude_mpm/services/monitor/daemon.py +37 -10
- claude_mpm/services/monitor/daemon_manager.py +134 -21
- claude_mpm/services/monitor/server.py +225 -19
- claude_mpm/services/project/project_organizer.py +4 -0
- claude_mpm/services/runner_configuration_service.py +16 -3
- claude_mpm/services/session_management_service.py +16 -4
- claude_mpm/services/socketio/event_normalizer.py +15 -1
- claude_mpm/services/socketio/server/core.py +160 -21
- claude_mpm/services/version_control/git_operations.py +103 -0
- claude_mpm/utils/agent_filters.py +261 -0
- claude_mpm/utils/gitignore.py +3 -0
- claude_mpm/utils/migration.py +372 -0
- claude_mpm/utils/progress.py +5 -1
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/METADATA +69 -84
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/RECORD +112 -153
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/entry_points.txt +0 -2
- claude_mpm/dashboard/analysis_runner.py +0 -455
- claude_mpm/dashboard/index.html +0 -13
- claude_mpm/dashboard/open_dashboard.py +0 -66
- claude_mpm/dashboard/static/css/activity.css +0 -1958
- claude_mpm/dashboard/static/css/connection-status.css +0 -370
- claude_mpm/dashboard/static/css/dashboard.css +0 -4701
- claude_mpm/dashboard/static/js/components/activity-tree.js +0 -1871
- claude_mpm/dashboard/static/js/components/agent-hierarchy.js +0 -777
- claude_mpm/dashboard/static/js/components/agent-inference.js +0 -956
- claude_mpm/dashboard/static/js/components/build-tracker.js +0 -333
- claude_mpm/dashboard/static/js/components/code-simple.js +0 -857
- claude_mpm/dashboard/static/js/components/connection-debug.js +0 -654
- claude_mpm/dashboard/static/js/components/diff-viewer.js +0 -891
- claude_mpm/dashboard/static/js/components/event-processor.js +0 -542
- claude_mpm/dashboard/static/js/components/event-viewer.js +0 -1155
- claude_mpm/dashboard/static/js/components/export-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +0 -443
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +0 -690
- claude_mpm/dashboard/static/js/components/file-tool-tracker.js +0 -724
- claude_mpm/dashboard/static/js/components/file-viewer.js +0 -580
- claude_mpm/dashboard/static/js/components/hud-library-loader.js +0 -211
- claude_mpm/dashboard/static/js/components/hud-manager.js +0 -671
- claude_mpm/dashboard/static/js/components/hud-visualizer.js +0 -1718
- claude_mpm/dashboard/static/js/components/module-viewer.js +0 -2764
- claude_mpm/dashboard/static/js/components/session-manager.js +0 -579
- claude_mpm/dashboard/static/js/components/socket-manager.js +0 -368
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +0 -749
- claude_mpm/dashboard/static/js/components/unified-data-viewer.js +0 -1824
- claude_mpm/dashboard/static/js/components/working-directory.js +0 -920
- claude_mpm/dashboard/static/js/connection-manager.js +0 -536
- claude_mpm/dashboard/static/js/dashboard.js +0 -1914
- claude_mpm/dashboard/static/js/extension-error-handler.js +0 -164
- claude_mpm/dashboard/static/js/socket-client.js +0 -1474
- claude_mpm/dashboard/static/js/tab-isolation-fix.js +0 -185
- claude_mpm/dashboard/static/socket.io.min.js +0 -7
- claude_mpm/dashboard/static/socket.io.v4.8.1.backup.js +0 -7
- claude_mpm/dashboard/templates/code_simple.html +0 -153
- claude_mpm/dashboard/templates/index.html +0 -606
- claude_mpm/dashboard/test_dashboard.html +0 -372
- claude_mpm/scripts/mcp_server.py +0 -75
- claude_mpm/scripts/mcp_wrapper.py +0 -39
- claude_mpm/services/mcp_gateway/__init__.py +0 -159
- claude_mpm/services/mcp_gateway/auto_configure.py +0 -369
- claude_mpm/services/mcp_gateway/config/__init__.py +0 -17
- claude_mpm/services/mcp_gateway/config/config_loader.py +0 -296
- claude_mpm/services/mcp_gateway/config/config_schema.py +0 -243
- claude_mpm/services/mcp_gateway/config/configuration.py +0 -429
- claude_mpm/services/mcp_gateway/core/__init__.py +0 -43
- claude_mpm/services/mcp_gateway/core/base.py +0 -312
- claude_mpm/services/mcp_gateway/core/exceptions.py +0 -253
- claude_mpm/services/mcp_gateway/core/interfaces.py +0 -443
- claude_mpm/services/mcp_gateway/core/process_pool.py +0 -971
- claude_mpm/services/mcp_gateway/core/singleton_manager.py +0 -315
- claude_mpm/services/mcp_gateway/core/startup_verification.py +0 -316
- claude_mpm/services/mcp_gateway/main.py +0 -589
- claude_mpm/services/mcp_gateway/registry/__init__.py +0 -12
- claude_mpm/services/mcp_gateway/registry/service_registry.py +0 -412
- claude_mpm/services/mcp_gateway/registry/tool_registry.py +0 -489
- claude_mpm/services/mcp_gateway/server/__init__.py +0 -15
- claude_mpm/services/mcp_gateway/server/mcp_gateway.py +0 -414
- claude_mpm/services/mcp_gateway/server/stdio_handler.py +0 -372
- claude_mpm/services/mcp_gateway/server/stdio_server.py +0 -712
- claude_mpm/services/mcp_gateway/tools/__init__.py +0 -36
- claude_mpm/services/mcp_gateway/tools/base_adapter.py +0 -485
- claude_mpm/services/mcp_gateway/tools/document_summarizer.py +0 -789
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +0 -654
- claude_mpm/services/mcp_gateway/tools/health_check_tool.py +0 -456
- claude_mpm/services/mcp_gateway/tools/hello_world.py +0 -551
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +0 -555
- claude_mpm/services/mcp_gateway/utils/__init__.py +0 -14
- claude_mpm/services/mcp_gateway/utils/package_version_checker.py +0 -160
- claude_mpm/services/mcp_gateway/utils/update_preferences.py +0 -170
- /claude_mpm/agents/{OUTPUT_STYLE.md ā CLAUDE_MPM_OUTPUT_STYLE.md} +0 -0
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-5.0.2.dist-info ā claude_mpm-5.4.3.dist-info}/top_level.txt +0 -0
|
@@ -1,971 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP Process Pool Manager
|
|
3
|
-
========================
|
|
4
|
-
|
|
5
|
-
Manages a pool of MCP server processes to prevent multiple instances
|
|
6
|
-
and reduce startup overhead through connection reuse.
|
|
7
|
-
|
|
8
|
-
WHY: MCP vector search servers load 400MB+ indexes on startup causing 11.9s delays.
|
|
9
|
-
By maintaining a process pool and reusing connections, we eliminate this overhead.
|
|
10
|
-
|
|
11
|
-
DESIGN DECISIONS:
|
|
12
|
-
- Singleton process pool shared across all agent invocations
|
|
13
|
-
- Pre-warm processes during framework initialization
|
|
14
|
-
- Health checks and automatic restart of failed processes
|
|
15
|
-
- Graceful shutdown and resource cleanup
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
import asyncio
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import signal
|
|
22
|
-
import subprocess
|
|
23
|
-
import sys
|
|
24
|
-
import threading
|
|
25
|
-
import time
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
from typing import Any, Dict, Optional
|
|
28
|
-
|
|
29
|
-
from claude_mpm.config.paths import paths
|
|
30
|
-
from claude_mpm.core.logger import get_logger
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class MCPProcessPool:
|
|
34
|
-
"""
|
|
35
|
-
Manages a pool of MCP server processes for efficient resource utilization.
|
|
36
|
-
|
|
37
|
-
WHY: Prevent multiple MCP server instances from being spawned and
|
|
38
|
-
reduce startup overhead by reusing existing processes.
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
_instance: Optional["MCPProcessPool"] = None
|
|
42
|
-
_lock = threading.Lock()
|
|
43
|
-
|
|
44
|
-
def __new__(cls):
|
|
45
|
-
"""Singleton pattern implementation."""
|
|
46
|
-
with cls._lock:
|
|
47
|
-
if cls._instance is None:
|
|
48
|
-
cls._instance = super().__new__(cls)
|
|
49
|
-
cls._instance._initialized = False
|
|
50
|
-
return cls._instance
|
|
51
|
-
|
|
52
|
-
def __init__(self):
|
|
53
|
-
"""Initialize the process pool manager."""
|
|
54
|
-
if self._initialized:
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
self.logger = get_logger("MCPProcessPool")
|
|
58
|
-
self._initialized = True
|
|
59
|
-
|
|
60
|
-
# Process tracking
|
|
61
|
-
self._processes: Dict[str, subprocess.Popen] = {}
|
|
62
|
-
self._process_info: Dict[str, Dict] = {}
|
|
63
|
-
self._startup_times: Dict[str, float] = {}
|
|
64
|
-
|
|
65
|
-
# Configuration
|
|
66
|
-
self.max_processes = 3 # Maximum number of pooled processes
|
|
67
|
-
self.process_timeout = 300 # 5 minutes idle timeout
|
|
68
|
-
self.health_check_interval = 30 # Check process health every 30s
|
|
69
|
-
|
|
70
|
-
# Paths
|
|
71
|
-
self.pool_dir = paths.claude_mpm_dir_hidden / "mcp" / "pool"
|
|
72
|
-
self.pool_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
|
|
74
|
-
# Pre-warming flag
|
|
75
|
-
self._pre_warmed = False
|
|
76
|
-
|
|
77
|
-
# Background health check task
|
|
78
|
-
self._health_check_task: Optional[asyncio.Task] = None
|
|
79
|
-
|
|
80
|
-
# Setup cleanup handlers
|
|
81
|
-
self._setup_cleanup_handlers()
|
|
82
|
-
|
|
83
|
-
self.logger.info("MCP Process Pool initialized")
|
|
84
|
-
|
|
85
|
-
def _setup_cleanup_handlers(self):
|
|
86
|
-
"""Setup signal handlers for cleanup on termination."""
|
|
87
|
-
|
|
88
|
-
def cleanup_handler(signum, frame):
|
|
89
|
-
self.logger.info(f"Received signal {signum}, cleaning up process pool")
|
|
90
|
-
self.cleanup_all()
|
|
91
|
-
|
|
92
|
-
signal.signal(signal.SIGTERM, cleanup_handler)
|
|
93
|
-
signal.signal(signal.SIGINT, cleanup_handler)
|
|
94
|
-
|
|
95
|
-
def get_or_create_process(
|
|
96
|
-
self, server_name: str, config: Dict
|
|
97
|
-
) -> Optional[subprocess.Popen]:
|
|
98
|
-
"""
|
|
99
|
-
Get an existing process or create a new one for the given server.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
server_name: Name of the MCP server
|
|
103
|
-
config: Server configuration including command and args
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
Process handle or None if failed
|
|
107
|
-
"""
|
|
108
|
-
start_time = time.time()
|
|
109
|
-
|
|
110
|
-
# Check if we have a healthy existing process
|
|
111
|
-
if server_name in self._processes:
|
|
112
|
-
process = self._processes[server_name]
|
|
113
|
-
if self._is_process_healthy(process):
|
|
114
|
-
self.logger.info(
|
|
115
|
-
f"Reusing existing process for {server_name} (PID: {process.pid})"
|
|
116
|
-
)
|
|
117
|
-
return process
|
|
118
|
-
# Process is dead, clean it up
|
|
119
|
-
self.logger.warning(f"Process for {server_name} is dead, cleaning up")
|
|
120
|
-
self._cleanup_process(server_name)
|
|
121
|
-
|
|
122
|
-
# Check if we've hit the process limit
|
|
123
|
-
if len(self._processes) >= self.max_processes:
|
|
124
|
-
# Find and clean up the oldest idle process
|
|
125
|
-
self._cleanup_oldest_idle_process()
|
|
126
|
-
|
|
127
|
-
# Create new process
|
|
128
|
-
self.logger.info(f"Creating new process for {server_name}")
|
|
129
|
-
process = self._create_process(server_name, config)
|
|
130
|
-
|
|
131
|
-
if process:
|
|
132
|
-
create_time = time.time() - start_time
|
|
133
|
-
self.logger.info(
|
|
134
|
-
f"Process created for {server_name} in {create_time:.2f}s (PID: {process.pid})"
|
|
135
|
-
)
|
|
136
|
-
self._startup_times[server_name] = create_time
|
|
137
|
-
|
|
138
|
-
return process
|
|
139
|
-
|
|
140
|
-
def _create_process(
|
|
141
|
-
self, server_name: str, config: Dict
|
|
142
|
-
) -> Optional[subprocess.Popen]:
|
|
143
|
-
"""
|
|
144
|
-
Create a new MCP server process.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
server_name: Name of the MCP server
|
|
148
|
-
config: Server configuration
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Process handle or None if failed
|
|
152
|
-
"""
|
|
153
|
-
try:
|
|
154
|
-
# Extract command and args from config
|
|
155
|
-
command = config.get("command", "")
|
|
156
|
-
args = config.get("args", [])
|
|
157
|
-
env = config.get("env", {})
|
|
158
|
-
cwd = config.get("cwd")
|
|
159
|
-
|
|
160
|
-
# Build full command
|
|
161
|
-
full_command = [command, *args]
|
|
162
|
-
|
|
163
|
-
# Merge environment variables
|
|
164
|
-
process_env = os.environ.copy()
|
|
165
|
-
process_env.update(env)
|
|
166
|
-
|
|
167
|
-
# Add timing instrumentation
|
|
168
|
-
process_env["MCP_STARTUP_TRACKING"] = "1"
|
|
169
|
-
process_env["MCP_SERVER_NAME"] = server_name
|
|
170
|
-
|
|
171
|
-
# Start the process
|
|
172
|
-
process = subprocess.Popen(
|
|
173
|
-
full_command,
|
|
174
|
-
stdin=subprocess.PIPE,
|
|
175
|
-
stdout=subprocess.PIPE,
|
|
176
|
-
stderr=subprocess.PIPE,
|
|
177
|
-
env=process_env,
|
|
178
|
-
cwd=cwd,
|
|
179
|
-
bufsize=0, # Unbuffered for real-time communication
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Store process info
|
|
183
|
-
self._processes[server_name] = process
|
|
184
|
-
self._process_info[server_name] = {
|
|
185
|
-
"pid": process.pid,
|
|
186
|
-
"started_at": time.time(),
|
|
187
|
-
"last_used": time.time(),
|
|
188
|
-
"config": config,
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
# Write process info to file for debugging
|
|
192
|
-
info_file = self.pool_dir / f"{server_name}_{process.pid}.json"
|
|
193
|
-
with info_file.open("w") as f:
|
|
194
|
-
json.dump(self._process_info[server_name], f, indent=2)
|
|
195
|
-
|
|
196
|
-
return process
|
|
197
|
-
|
|
198
|
-
except Exception as e:
|
|
199
|
-
self.logger.error(f"Failed to create process for {server_name}: {e}")
|
|
200
|
-
return None
|
|
201
|
-
|
|
202
|
-
def _is_process_healthy(self, process: subprocess.Popen) -> bool:
|
|
203
|
-
"""Check if a process is still running and healthy."""
|
|
204
|
-
if process.poll() is not None:
|
|
205
|
-
# Process has terminated
|
|
206
|
-
return False
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
# Send signal 0 to check if process is alive
|
|
210
|
-
os.kill(process.pid, 0)
|
|
211
|
-
return True
|
|
212
|
-
except (OSError, ProcessLookupError):
|
|
213
|
-
return False
|
|
214
|
-
|
|
215
|
-
def _cleanup_process(self, server_name: str):
|
|
216
|
-
"""Clean up a specific process."""
|
|
217
|
-
if server_name not in self._processes:
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
process = self._processes[server_name]
|
|
221
|
-
|
|
222
|
-
try:
|
|
223
|
-
# Try graceful shutdown first
|
|
224
|
-
if self._is_process_healthy(process):
|
|
225
|
-
process.terminate()
|
|
226
|
-
try:
|
|
227
|
-
process.wait(timeout=5)
|
|
228
|
-
except subprocess.TimeoutExpired:
|
|
229
|
-
# Force kill if graceful shutdown fails
|
|
230
|
-
process.kill()
|
|
231
|
-
process.wait()
|
|
232
|
-
|
|
233
|
-
# Remove from tracking
|
|
234
|
-
del self._processes[server_name]
|
|
235
|
-
del self._process_info[server_name]
|
|
236
|
-
|
|
237
|
-
# Clean up info file
|
|
238
|
-
for info_file in self.pool_dir.glob(f"{server_name}_*.json"):
|
|
239
|
-
info_file.unlink()
|
|
240
|
-
|
|
241
|
-
self.logger.info(f"Cleaned up process for {server_name}")
|
|
242
|
-
|
|
243
|
-
except Exception as e:
|
|
244
|
-
self.logger.warning(f"Error cleaning up process for {server_name}: {e}")
|
|
245
|
-
|
|
246
|
-
def _cleanup_oldest_idle_process(self):
|
|
247
|
-
"""Find and clean up the oldest idle process."""
|
|
248
|
-
if not self._process_info:
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
# Find process with oldest last_used time
|
|
252
|
-
oldest_server = min(
|
|
253
|
-
self._process_info.keys(),
|
|
254
|
-
key=lambda k: self._process_info[k].get("last_used", 0),
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
self.logger.info(f"Cleaning up oldest idle process: {oldest_server}")
|
|
258
|
-
self._cleanup_process(oldest_server)
|
|
259
|
-
|
|
260
|
-
async def pre_warm_servers(self, configs: Dict[str, Dict]):
|
|
261
|
-
"""
|
|
262
|
-
Pre-warm MCP servers during framework initialization.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
configs: Dictionary of server configurations
|
|
266
|
-
"""
|
|
267
|
-
if self._pre_warmed:
|
|
268
|
-
self.logger.info("Servers already pre-warmed")
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
self.logger.info(f"Pre-warming {len(configs)} MCP servers")
|
|
272
|
-
start_time = time.time()
|
|
273
|
-
|
|
274
|
-
# Start all servers in parallel
|
|
275
|
-
for server_name, config in configs.items():
|
|
276
|
-
# Only pre-warm critical servers (like vector search)
|
|
277
|
-
if "vector" in server_name.lower() or config.get("pre_warm", False):
|
|
278
|
-
self.logger.info(f"Pre-warming {server_name}")
|
|
279
|
-
process = self.get_or_create_process(server_name, config)
|
|
280
|
-
if process:
|
|
281
|
-
self.logger.info(f"Pre-warmed {server_name} (PID: {process.pid})")
|
|
282
|
-
|
|
283
|
-
self._pre_warmed = True
|
|
284
|
-
total_time = time.time() - start_time
|
|
285
|
-
self.logger.info(f"Pre-warming completed in {total_time:.2f}s")
|
|
286
|
-
|
|
287
|
-
async def start_health_monitoring(self):
|
|
288
|
-
"""Start background health monitoring of processes."""
|
|
289
|
-
if self._health_check_task and not self._health_check_task.done():
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
self._health_check_task = asyncio.create_task(self._health_check_loop())
|
|
293
|
-
self.logger.info("Started health monitoring")
|
|
294
|
-
|
|
295
|
-
async def _health_check_loop(self):
|
|
296
|
-
"""Background loop to check process health."""
|
|
297
|
-
while True:
|
|
298
|
-
try:
|
|
299
|
-
await asyncio.sleep(self.health_check_interval)
|
|
300
|
-
|
|
301
|
-
# Check each process
|
|
302
|
-
dead_processes = []
|
|
303
|
-
for server_name, process in self._processes.items():
|
|
304
|
-
if not self._is_process_healthy(process):
|
|
305
|
-
dead_processes.append(server_name)
|
|
306
|
-
|
|
307
|
-
# Clean up dead processes
|
|
308
|
-
for server_name in dead_processes:
|
|
309
|
-
self.logger.warning(f"Process {server_name} is dead, cleaning up")
|
|
310
|
-
self._cleanup_process(server_name)
|
|
311
|
-
|
|
312
|
-
# Check for idle timeout
|
|
313
|
-
current_time = time.time()
|
|
314
|
-
idle_processes = []
|
|
315
|
-
for server_name, info in self._process_info.items():
|
|
316
|
-
last_used = info.get("last_used", current_time)
|
|
317
|
-
if current_time - last_used > self.process_timeout:
|
|
318
|
-
idle_processes.append(server_name)
|
|
319
|
-
|
|
320
|
-
# Clean up idle processes
|
|
321
|
-
for server_name in idle_processes:
|
|
322
|
-
self.logger.info(f"Process {server_name} idle timeout, cleaning up")
|
|
323
|
-
self._cleanup_process(server_name)
|
|
324
|
-
|
|
325
|
-
except Exception as e:
|
|
326
|
-
self.logger.error(f"Error in health check loop: {e}")
|
|
327
|
-
|
|
328
|
-
def mark_process_used(self, server_name: str):
|
|
329
|
-
"""Mark a process as recently used."""
|
|
330
|
-
if server_name in self._process_info:
|
|
331
|
-
self._process_info[server_name]["last_used"] = time.time()
|
|
332
|
-
|
|
333
|
-
def get_startup_metrics(self) -> Dict[str, float]:
|
|
334
|
-
"""Get startup time metrics for all servers."""
|
|
335
|
-
return self._startup_times.copy()
|
|
336
|
-
|
|
337
|
-
def get_pool_status(self) -> Dict[str, Any]:
|
|
338
|
-
"""Get current status of the process pool."""
|
|
339
|
-
return {
|
|
340
|
-
"active_processes": len(self._processes),
|
|
341
|
-
"max_processes": self.max_processes,
|
|
342
|
-
"pre_warmed": self._pre_warmed,
|
|
343
|
-
"processes": {
|
|
344
|
-
name: {
|
|
345
|
-
"pid": info.get("pid"),
|
|
346
|
-
"uptime": time.time() - info.get("started_at", time.time()),
|
|
347
|
-
"idle_time": time.time() - info.get("last_used", time.time()),
|
|
348
|
-
}
|
|
349
|
-
for name, info in self._process_info.items()
|
|
350
|
-
},
|
|
351
|
-
"startup_metrics": self._startup_times,
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
def cleanup_all(self):
|
|
355
|
-
"""Clean up all processes in the pool."""
|
|
356
|
-
self.logger.info("Cleaning up all processes in pool")
|
|
357
|
-
|
|
358
|
-
# Stop health monitoring
|
|
359
|
-
if self._health_check_task:
|
|
360
|
-
self._health_check_task.cancel()
|
|
361
|
-
|
|
362
|
-
# Clean up all processes
|
|
363
|
-
for server_name in list(self._processes.keys()):
|
|
364
|
-
self._cleanup_process(server_name)
|
|
365
|
-
|
|
366
|
-
self.logger.info("Process pool cleanup completed")
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
# Global instance
|
|
370
|
-
_pool: Optional[MCPProcessPool] = None
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def get_process_pool() -> MCPProcessPool:
|
|
374
|
-
"""Get the global MCP process pool instance."""
|
|
375
|
-
global _pool
|
|
376
|
-
if _pool is None:
|
|
377
|
-
_pool = MCPProcessPool()
|
|
378
|
-
return _pool
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
async def auto_initialize_vector_search():
|
|
382
|
-
"""
|
|
383
|
-
Auto-initialize mcp-vector-search for the current project.
|
|
384
|
-
|
|
385
|
-
WHY: Vector search requires project initialization before it can be used.
|
|
386
|
-
This function ensures the current project is automatically initialized
|
|
387
|
-
for vector search when the system starts up.
|
|
388
|
-
|
|
389
|
-
DESIGN DECISION:
|
|
390
|
-
- Automatically install mcp-vector-search if not present
|
|
391
|
-
- Run in background with timeout to avoid blocking startup
|
|
392
|
-
- Failures are logged but don't prevent the system from starting
|
|
393
|
-
"""
|
|
394
|
-
logger = get_logger("vector_search_init")
|
|
395
|
-
|
|
396
|
-
try:
|
|
397
|
-
# Import MCPConfigManager to handle installation
|
|
398
|
-
from claude_mpm.services.mcp_config_manager import MCPConfigManager
|
|
399
|
-
|
|
400
|
-
config_manager = MCPConfigManager()
|
|
401
|
-
|
|
402
|
-
# Check if mcp-vector-search is already installed
|
|
403
|
-
vector_search_path = config_manager.detect_service_path("mcp-vector-search")
|
|
404
|
-
|
|
405
|
-
if vector_search_path:
|
|
406
|
-
logger.debug(f"mcp-vector-search found at: {vector_search_path}")
|
|
407
|
-
else:
|
|
408
|
-
# Not installed - attempt installation
|
|
409
|
-
logger.info("š mcp-vector-search not found. Installing via pipx...")
|
|
410
|
-
|
|
411
|
-
# First check if pipx is available
|
|
412
|
-
import shutil
|
|
413
|
-
import subprocess
|
|
414
|
-
|
|
415
|
-
if not shutil.which("pipx"):
|
|
416
|
-
logger.warning(
|
|
417
|
-
"ā ļø pipx not found. Please install pipx to enable automatic mcp-vector-search installation"
|
|
418
|
-
)
|
|
419
|
-
logger.info(" Install pipx with: python -m pip install --user pipx")
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
try:
|
|
423
|
-
result = subprocess.run(
|
|
424
|
-
["pipx", "install", "mcp-vector-search"],
|
|
425
|
-
capture_output=True,
|
|
426
|
-
text=True,
|
|
427
|
-
timeout=60,
|
|
428
|
-
check=False, # 1 minute timeout for installation
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
if result.returncode == 0:
|
|
432
|
-
logger.info("ā
mcp-vector-search installed successfully")
|
|
433
|
-
# Detect the newly installed path
|
|
434
|
-
vector_search_path = config_manager.detect_service_path(
|
|
435
|
-
"mcp-vector-search"
|
|
436
|
-
)
|
|
437
|
-
if not vector_search_path:
|
|
438
|
-
logger.warning(
|
|
439
|
-
"mcp-vector-search installed but command not found in PATH"
|
|
440
|
-
)
|
|
441
|
-
return
|
|
442
|
-
|
|
443
|
-
# Update the Claude configuration to include the newly installed service
|
|
444
|
-
logger.info("š Updating Claude configuration...")
|
|
445
|
-
config_success, config_msg = (
|
|
446
|
-
config_manager.ensure_mcp_services_configured()
|
|
447
|
-
)
|
|
448
|
-
if config_success:
|
|
449
|
-
logger.info(f"ā
{config_msg}")
|
|
450
|
-
else:
|
|
451
|
-
logger.warning(f"ā ļø Configuration update issue: {config_msg}")
|
|
452
|
-
else:
|
|
453
|
-
logger.warning(
|
|
454
|
-
f"Failed to install mcp-vector-search: {result.stderr}"
|
|
455
|
-
)
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
except subprocess.TimeoutExpired:
|
|
459
|
-
logger.warning("Installation of mcp-vector-search timed out")
|
|
460
|
-
return
|
|
461
|
-
except Exception as e:
|
|
462
|
-
logger.warning(f"Error installing mcp-vector-search: {e}")
|
|
463
|
-
return
|
|
464
|
-
|
|
465
|
-
# At this point, mcp-vector-search should be available
|
|
466
|
-
# Get the actual command to use
|
|
467
|
-
import shutil
|
|
468
|
-
|
|
469
|
-
vector_search_cmd = shutil.which("mcp-vector-search")
|
|
470
|
-
if not vector_search_cmd:
|
|
471
|
-
# Try pipx installation path as fallback
|
|
472
|
-
pipx_path = (
|
|
473
|
-
Path.home()
|
|
474
|
-
/ ".local/pipx/venvs/mcp-vector-search/bin/mcp-vector-search"
|
|
475
|
-
)
|
|
476
|
-
if pipx_path.exists():
|
|
477
|
-
vector_search_cmd = str(pipx_path)
|
|
478
|
-
else:
|
|
479
|
-
logger.debug("mcp-vector-search command not found after installation")
|
|
480
|
-
return
|
|
481
|
-
|
|
482
|
-
# Check if current project is already initialized
|
|
483
|
-
current_dir = Path.cwd()
|
|
484
|
-
vector_config = current_dir / ".mcp-vector-search/config.json"
|
|
485
|
-
|
|
486
|
-
if vector_config.exists():
|
|
487
|
-
logger.debug(f"Vector search already initialized for {current_dir}")
|
|
488
|
-
|
|
489
|
-
# Ensure .mcp-vector-search is in gitignore even if already initialized
|
|
490
|
-
try:
|
|
491
|
-
from ....services.project.project_organizer import ProjectOrganizer
|
|
492
|
-
|
|
493
|
-
if (current_dir / ".claude-mpm").exists() or (
|
|
494
|
-
current_dir / ".git"
|
|
495
|
-
).exists():
|
|
496
|
-
organizer = ProjectOrganizer(current_dir)
|
|
497
|
-
organizer.update_gitignore(
|
|
498
|
-
additional_patterns=[".mcp-vector-search/"]
|
|
499
|
-
)
|
|
500
|
-
logger.debug("Ensured .mcp-vector-search is in gitignore")
|
|
501
|
-
except Exception as e:
|
|
502
|
-
logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
|
|
503
|
-
# Check if index needs rebuilding (corrupted database)
|
|
504
|
-
chroma_db = current_dir / ".mcp-vector-search/chroma.sqlite3"
|
|
505
|
-
if chroma_db.exists():
|
|
506
|
-
# Quick health check - verify database file exists and is accessible
|
|
507
|
-
try:
|
|
508
|
-
# Check if database file exists and has reasonable size
|
|
509
|
-
if chroma_db.exists() and chroma_db.stat().st_size > 0:
|
|
510
|
-
logger.info("ā Vector search index is healthy and ready")
|
|
511
|
-
return
|
|
512
|
-
logger.info("ā ļø Vector search index may be corrupted, rebuilding...")
|
|
513
|
-
except Exception as e:
|
|
514
|
-
logger.debug(
|
|
515
|
-
f"Vector search health check failed: {e}, will attempt to rebuild"
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
# Initialize or reinitialize the project
|
|
519
|
-
logger.info(f"šÆ Initializing vector search for project: {current_dir}")
|
|
520
|
-
|
|
521
|
-
# Initialize the project (this creates the config)
|
|
522
|
-
# Note: mcp-vector-search operates on the current directory
|
|
523
|
-
import subprocess
|
|
524
|
-
|
|
525
|
-
proc = subprocess.run(
|
|
526
|
-
[vector_search_cmd, "init"],
|
|
527
|
-
capture_output=True,
|
|
528
|
-
text=True,
|
|
529
|
-
timeout=30,
|
|
530
|
-
cwd=str(current_dir),
|
|
531
|
-
check=False, # Run in the project directory
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
if proc.returncode == 0:
|
|
535
|
-
logger.info("ā
Vector search initialization completed")
|
|
536
|
-
|
|
537
|
-
# Ensure .mcp-vector-search is in gitignore
|
|
538
|
-
try:
|
|
539
|
-
from ....services.project.project_organizer import ProjectOrganizer
|
|
540
|
-
|
|
541
|
-
# Check if we're in a git repository (parent of .claude-mpm)
|
|
542
|
-
if (current_dir / ".claude-mpm").exists() or (
|
|
543
|
-
current_dir / ".git"
|
|
544
|
-
).exists():
|
|
545
|
-
organizer = ProjectOrganizer(current_dir)
|
|
546
|
-
organizer.update_gitignore(
|
|
547
|
-
additional_patterns=[".mcp-vector-search/"]
|
|
548
|
-
)
|
|
549
|
-
logger.debug("Ensured .mcp-vector-search is in gitignore")
|
|
550
|
-
except Exception as e:
|
|
551
|
-
logger.debug(f"Could not update gitignore for .mcp-vector-search: {e}")
|
|
552
|
-
# Non-critical, don't fail initialization
|
|
553
|
-
|
|
554
|
-
# Start background indexing (non-blocking)
|
|
555
|
-
def background_index():
|
|
556
|
-
try:
|
|
557
|
-
logger.info("š Starting project indexing in background...")
|
|
558
|
-
index_proc = subprocess.run(
|
|
559
|
-
[vector_search_cmd, "index", "main"],
|
|
560
|
-
capture_output=True,
|
|
561
|
-
text=True,
|
|
562
|
-
timeout=300, # 5 minute timeout for indexing
|
|
563
|
-
cwd=str(current_dir),
|
|
564
|
-
check=False, # Run in the project directory
|
|
565
|
-
)
|
|
566
|
-
if index_proc.returncode == 0:
|
|
567
|
-
logger.info("ā
Project indexing completed successfully")
|
|
568
|
-
# Parse output to show statistics if available
|
|
569
|
-
if "indexed" in index_proc.stdout.lower():
|
|
570
|
-
# Extract and log indexing statistics
|
|
571
|
-
lines = index_proc.stdout.strip().split("\n")
|
|
572
|
-
for line in lines:
|
|
573
|
-
if "indexed" in line.lower() or "files" in line.lower():
|
|
574
|
-
logger.info(f" {line.strip()}")
|
|
575
|
-
else:
|
|
576
|
-
logger.warning(
|
|
577
|
-
f"ā ļø Project indexing failed: {index_proc.stderr}"
|
|
578
|
-
)
|
|
579
|
-
except subprocess.TimeoutExpired:
|
|
580
|
-
logger.warning(
|
|
581
|
-
"ā ļø Project indexing timed out (will continue in background)"
|
|
582
|
-
)
|
|
583
|
-
except Exception as e:
|
|
584
|
-
logger.debug(f"Background indexing error (non-critical): {e}")
|
|
585
|
-
|
|
586
|
-
# Run indexing in background thread
|
|
587
|
-
import threading
|
|
588
|
-
|
|
589
|
-
index_thread = threading.Thread(target=background_index, daemon=True)
|
|
590
|
-
index_thread.start()
|
|
591
|
-
logger.info(
|
|
592
|
-
"š Background indexing started - vector search will be available shortly"
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
else:
|
|
596
|
-
logger.warning(f"ā ļø Vector search initialization failed: {proc.stderr}")
|
|
597
|
-
|
|
598
|
-
except Exception as e:
|
|
599
|
-
logger.debug(f"Vector search auto-initialization error (non-critical): {e}")
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
async def auto_initialize_kuzu_memory():
|
|
603
|
-
"""
|
|
604
|
-
Auto-initialize kuzu-memory for persistent knowledge storage.
|
|
605
|
-
|
|
606
|
-
WHY: Kuzu-memory provides a graph database for structured memory storage
|
|
607
|
-
with semantic search capabilities, enabling persistent context across sessions.
|
|
608
|
-
|
|
609
|
-
DESIGN DECISION:
|
|
610
|
-
- Automatically install kuzu-memory if not present via pipx
|
|
611
|
-
- Initialize database in background to avoid blocking startup
|
|
612
|
-
- Failures are logged but don't prevent the system from starting
|
|
613
|
-
"""
|
|
614
|
-
logger = get_logger("kuzu_memory_init")
|
|
615
|
-
|
|
616
|
-
try:
|
|
617
|
-
# Import MCPConfigManager to handle installation
|
|
618
|
-
from claude_mpm.services.mcp_config_manager import MCPConfigManager
|
|
619
|
-
|
|
620
|
-
config_manager = MCPConfigManager()
|
|
621
|
-
|
|
622
|
-
# Check if kuzu-memory is already installed
|
|
623
|
-
kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
|
|
624
|
-
|
|
625
|
-
if kuzu_memory_path:
|
|
626
|
-
logger.debug(f"kuzu-memory found at: {kuzu_memory_path}")
|
|
627
|
-
else:
|
|
628
|
-
# Not installed - attempt installation
|
|
629
|
-
logger.info("š§ kuzu-memory not found. Installing via pipx...")
|
|
630
|
-
|
|
631
|
-
# First check if pipx is available
|
|
632
|
-
import shutil
|
|
633
|
-
import subprocess
|
|
634
|
-
|
|
635
|
-
if not shutil.which("pipx"):
|
|
636
|
-
logger.warning(
|
|
637
|
-
"ā ļø pipx not found. Please install pipx to enable automatic kuzu-memory installation"
|
|
638
|
-
)
|
|
639
|
-
logger.info(" Install pipx with: python -m pip install --user pipx")
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
try:
|
|
643
|
-
result = subprocess.run(
|
|
644
|
-
["pipx", "install", "kuzu-memory"],
|
|
645
|
-
capture_output=True,
|
|
646
|
-
text=True,
|
|
647
|
-
timeout=60,
|
|
648
|
-
check=False, # 1 minute timeout for installation
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
if result.returncode == 0:
|
|
652
|
-
logger.info("ā
kuzu-memory installed successfully")
|
|
653
|
-
# Detect the newly installed path
|
|
654
|
-
kuzu_memory_path = config_manager.detect_service_path("kuzu-memory")
|
|
655
|
-
if not kuzu_memory_path:
|
|
656
|
-
logger.warning(
|
|
657
|
-
"kuzu-memory installed but command not found in PATH"
|
|
658
|
-
)
|
|
659
|
-
return
|
|
660
|
-
|
|
661
|
-
# Update the Claude configuration to include the newly installed service
|
|
662
|
-
logger.info("š Updating Claude configuration...")
|
|
663
|
-
config_success, config_msg = (
|
|
664
|
-
config_manager.ensure_mcp_services_configured()
|
|
665
|
-
)
|
|
666
|
-
if config_success:
|
|
667
|
-
logger.info(f"ā
{config_msg}")
|
|
668
|
-
else:
|
|
669
|
-
logger.warning(f"ā ļø Configuration update issue: {config_msg}")
|
|
670
|
-
else:
|
|
671
|
-
logger.warning(f"Failed to install kuzu-memory: {result.stderr}")
|
|
672
|
-
return
|
|
673
|
-
|
|
674
|
-
except subprocess.TimeoutExpired:
|
|
675
|
-
logger.warning("Installation of kuzu-memory timed out")
|
|
676
|
-
return
|
|
677
|
-
except Exception as e:
|
|
678
|
-
logger.warning(f"Error installing kuzu-memory: {e}")
|
|
679
|
-
return
|
|
680
|
-
|
|
681
|
-
# At this point, kuzu-memory should be available
|
|
682
|
-
# Get the actual command to use
|
|
683
|
-
import shutil
|
|
684
|
-
|
|
685
|
-
kuzu_memory_cmd = shutil.which("kuzu-memory")
|
|
686
|
-
if not kuzu_memory_cmd:
|
|
687
|
-
# Try pipx installation path as fallback
|
|
688
|
-
pipx_path = Path.home() / ".local/pipx/venvs/kuzu-memory/bin/kuzu-memory"
|
|
689
|
-
if pipx_path.exists():
|
|
690
|
-
kuzu_memory_cmd = str(pipx_path)
|
|
691
|
-
else:
|
|
692
|
-
logger.debug("kuzu-memory command not found after installation")
|
|
693
|
-
return
|
|
694
|
-
|
|
695
|
-
# Check for kuzu-memory updates (non-blocking)
|
|
696
|
-
try:
|
|
697
|
-
await _check_kuzu_memory_updates(kuzu_memory_cmd)
|
|
698
|
-
except Exception as e:
|
|
699
|
-
logger.debug(f"Update check failed (non-critical): {e}")
|
|
700
|
-
|
|
701
|
-
# Initialize kuzu-memory database in current project
|
|
702
|
-
current_dir = Path.cwd()
|
|
703
|
-
kuzu_memories_dir = current_dir / "kuzu-memories"
|
|
704
|
-
|
|
705
|
-
# Check if database is already initialized
|
|
706
|
-
if kuzu_memories_dir.exists():
|
|
707
|
-
logger.debug(
|
|
708
|
-
f"Kuzu-memory database already initialized at {kuzu_memories_dir}"
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
# Ensure kuzu-memories is in gitignore even if already initialized
|
|
712
|
-
try:
|
|
713
|
-
from ....services.project.project_organizer import ProjectOrganizer
|
|
714
|
-
|
|
715
|
-
if (current_dir / ".claude-mpm").exists() or (
|
|
716
|
-
current_dir / ".git"
|
|
717
|
-
).exists():
|
|
718
|
-
organizer = ProjectOrganizer(current_dir)
|
|
719
|
-
organizer.update_gitignore(additional_patterns=["kuzu-memories/"])
|
|
720
|
-
logger.debug("Ensured kuzu-memories is in gitignore")
|
|
721
|
-
except Exception as e:
|
|
722
|
-
logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
|
|
723
|
-
else:
|
|
724
|
-
logger.info(
|
|
725
|
-
f"šÆ Initializing kuzu-memory database for project: {current_dir}"
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
# Initialize the database in current project directory
|
|
729
|
-
import subprocess
|
|
730
|
-
|
|
731
|
-
proc = subprocess.run(
|
|
732
|
-
[kuzu_memory_cmd, "init"],
|
|
733
|
-
capture_output=True,
|
|
734
|
-
text=True,
|
|
735
|
-
timeout=30,
|
|
736
|
-
cwd=str(current_dir),
|
|
737
|
-
check=False,
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
if proc.returncode == 0:
|
|
741
|
-
logger.info("ā
Kuzu-memory database initialized successfully")
|
|
742
|
-
|
|
743
|
-
# Ensure kuzu-memories is in gitignore
|
|
744
|
-
try:
|
|
745
|
-
from ....services.project.project_organizer import ProjectOrganizer
|
|
746
|
-
|
|
747
|
-
if (current_dir / ".claude-mpm").exists() or (
|
|
748
|
-
current_dir / ".git"
|
|
749
|
-
).exists():
|
|
750
|
-
organizer = ProjectOrganizer(current_dir)
|
|
751
|
-
organizer.update_gitignore(
|
|
752
|
-
additional_patterns=["kuzu-memories/"]
|
|
753
|
-
)
|
|
754
|
-
logger.debug("Ensured kuzu-memories is in gitignore")
|
|
755
|
-
except Exception as e:
|
|
756
|
-
logger.debug(f"Could not update gitignore for kuzu-memories: {e}")
|
|
757
|
-
# Non-critical, don't fail initialization
|
|
758
|
-
else:
|
|
759
|
-
logger.warning(f"ā ļø Kuzu-memory initialization failed: {proc.stderr}")
|
|
760
|
-
|
|
761
|
-
except Exception as e:
|
|
762
|
-
logger.debug(f"Kuzu-memory auto-initialization error (non-critical): {e}")
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
async def _check_kuzu_memory_updates(kuzu_cmd: Path) -> None:
|
|
766
|
-
"""
|
|
767
|
-
Check for kuzu-memory updates and prompt user.
|
|
768
|
-
|
|
769
|
-
Args:
|
|
770
|
-
kuzu_cmd: Path to kuzu-memory command
|
|
771
|
-
|
|
772
|
-
WHY: Keep users informed about important updates that may fix bugs
|
|
773
|
-
or add features they need.
|
|
774
|
-
|
|
775
|
-
DESIGN DECISIONS:
|
|
776
|
-
- Non-blocking with timeout to prevent startup delays
|
|
777
|
-
- Respects user preferences and environment variables
|
|
778
|
-
- Only prompts in interactive TTY sessions
|
|
779
|
-
"""
|
|
780
|
-
logger = get_logger("kuzu_memory_update")
|
|
781
|
-
|
|
782
|
-
# Skip if environment variable set
|
|
783
|
-
if os.environ.get("CLAUDE_MPM_SKIP_UPDATE_CHECK"):
|
|
784
|
-
return
|
|
785
|
-
|
|
786
|
-
# Skip if not TTY (can't prompt)
|
|
787
|
-
if not sys.stdin.isatty():
|
|
788
|
-
return
|
|
789
|
-
|
|
790
|
-
# Import update utilities
|
|
791
|
-
from ..utils.package_version_checker import PackageVersionChecker
|
|
792
|
-
from ..utils.update_preferences import UpdatePreferences
|
|
793
|
-
|
|
794
|
-
# Check if updates are enabled for this package
|
|
795
|
-
if not UpdatePreferences.should_check_package("kuzu-memory"):
|
|
796
|
-
return
|
|
797
|
-
|
|
798
|
-
try:
|
|
799
|
-
# Get current version from pipx
|
|
800
|
-
result = subprocess.run(
|
|
801
|
-
["pipx", "list", "--json"],
|
|
802
|
-
capture_output=True,
|
|
803
|
-
text=True,
|
|
804
|
-
timeout=5,
|
|
805
|
-
check=False,
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
if result.returncode == 0:
|
|
809
|
-
pipx_data = json.loads(result.stdout)
|
|
810
|
-
venvs = pipx_data.get("venvs", {})
|
|
811
|
-
kuzu_info = venvs.get("kuzu-memory", {})
|
|
812
|
-
metadata = kuzu_info.get("metadata", {})
|
|
813
|
-
current_version = metadata.get("main_package", {}).get(
|
|
814
|
-
"package_version", "unknown"
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
if current_version != "unknown":
|
|
818
|
-
# Check for updates
|
|
819
|
-
checker = PackageVersionChecker()
|
|
820
|
-
update_info = await checker.check_for_update(
|
|
821
|
-
"kuzu-memory", current_version
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
if update_info and update_info.get("update_available"):
|
|
825
|
-
latest_version = update_info["latest"]
|
|
826
|
-
|
|
827
|
-
# Check if user wants to skip this version
|
|
828
|
-
if UpdatePreferences.should_skip_version(
|
|
829
|
-
"kuzu-memory", latest_version
|
|
830
|
-
):
|
|
831
|
-
logger.debug(
|
|
832
|
-
f"Skipping kuzu-memory update to {latest_version} per user preference"
|
|
833
|
-
)
|
|
834
|
-
return
|
|
835
|
-
|
|
836
|
-
# Prompt for update
|
|
837
|
-
_prompt_kuzu_update(update_info["current"], latest_version)
|
|
838
|
-
|
|
839
|
-
except Exception as e:
|
|
840
|
-
logger.debug(f"Update check error: {e}")
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
def _prompt_kuzu_update(current: str, latest: str) -> None:
|
|
844
|
-
"""
|
|
845
|
-
Prompt user to update kuzu-memory.
|
|
846
|
-
|
|
847
|
-
Args:
|
|
848
|
-
current: Current installed version
|
|
849
|
-
latest: Latest available version
|
|
850
|
-
"""
|
|
851
|
-
from ...cli.shared.error_handling import confirm_operation
|
|
852
|
-
from ..utils.update_preferences import UpdatePreferences
|
|
853
|
-
|
|
854
|
-
logger = get_logger("kuzu_memory_update")
|
|
855
|
-
|
|
856
|
-
message = (
|
|
857
|
-
f"\nš A new version of kuzu-memory is available!\n"
|
|
858
|
-
f" Current: v{current}\n"
|
|
859
|
-
f" Latest: v{latest}\n\n"
|
|
860
|
-
f" This update may include bug fixes and performance improvements.\n"
|
|
861
|
-
f" Update now?"
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
# Check if running in a non-interactive context
|
|
865
|
-
try:
|
|
866
|
-
if confirm_operation(message):
|
|
867
|
-
print("š Updating kuzu-memory...", file=sys.stderr)
|
|
868
|
-
try:
|
|
869
|
-
result = subprocess.run(
|
|
870
|
-
["pipx", "upgrade", "kuzu-memory"],
|
|
871
|
-
capture_output=True,
|
|
872
|
-
text=True,
|
|
873
|
-
timeout=30,
|
|
874
|
-
check=False,
|
|
875
|
-
)
|
|
876
|
-
if result.returncode == 0:
|
|
877
|
-
print("ā
Successfully updated kuzu-memory!", file=sys.stderr)
|
|
878
|
-
logger.info(f"Updated kuzu-memory from {current} to {latest}")
|
|
879
|
-
else:
|
|
880
|
-
print(f"ā ļø Update failed: {result.stderr}", file=sys.stderr)
|
|
881
|
-
logger.warning(f"kuzu-memory update failed: {result.stderr}")
|
|
882
|
-
except subprocess.TimeoutExpired:
|
|
883
|
-
print("ā ļø Update timed out. Please try again later.", file=sys.stderr)
|
|
884
|
-
logger.warning("kuzu-memory update timed out")
|
|
885
|
-
except Exception as e:
|
|
886
|
-
print(f"ā ļø Update failed: {e}", file=sys.stderr)
|
|
887
|
-
logger.warning(f"kuzu-memory update error: {e}")
|
|
888
|
-
else:
|
|
889
|
-
# User declined update
|
|
890
|
-
print("\n To skip this version permanently, run:", file=sys.stderr)
|
|
891
|
-
print(
|
|
892
|
-
f" claude-mpm config set-skip-version kuzu-memory {latest}",
|
|
893
|
-
file=sys.stderr,
|
|
894
|
-
)
|
|
895
|
-
print(" To disable update checks for kuzu-memory:", file=sys.stderr)
|
|
896
|
-
print(
|
|
897
|
-
" claude-mpm config disable-update-checks kuzu-memory",
|
|
898
|
-
file=sys.stderr,
|
|
899
|
-
)
|
|
900
|
-
|
|
901
|
-
# Ask if user wants to skip this version
|
|
902
|
-
if confirm_operation("\n Skip this version in future checks?"):
|
|
903
|
-
UpdatePreferences.set_skip_version("kuzu-memory", latest)
|
|
904
|
-
print(
|
|
905
|
-
f" Version {latest} will be skipped in future checks.",
|
|
906
|
-
file=sys.stderr,
|
|
907
|
-
)
|
|
908
|
-
except (KeyboardInterrupt, EOFError):
|
|
909
|
-
# User interrupted or input not available
|
|
910
|
-
pass
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
async def pre_warm_mcp_servers():
|
|
914
|
-
"""
|
|
915
|
-
Pre-warm MCP servers from configuration.
|
|
916
|
-
|
|
917
|
-
DISABLED: This function is currently disabled to avoid conflicts with
|
|
918
|
-
Claude Code's native MCP server management. When enabled, this can
|
|
919
|
-
cause issues with MCP server initialization and stderr/stdout handling.
|
|
920
|
-
|
|
921
|
-
TODO: Re-enable after ensuring compatibility with Claude Code's MCP handling.
|
|
922
|
-
"""
|
|
923
|
-
logger = get_logger("MCPProcessPool")
|
|
924
|
-
logger.debug("MCP server pre-warming is currently disabled")
|
|
925
|
-
|
|
926
|
-
# COMMENTED OUT: Auto-initialization that can interfere with Claude Code
|
|
927
|
-
# # Auto-initialize vector search for current project
|
|
928
|
-
# await auto_initialize_vector_search()
|
|
929
|
-
#
|
|
930
|
-
# # Auto-initialize kuzu-memory for persistent knowledge
|
|
931
|
-
# await auto_initialize_kuzu_memory()
|
|
932
|
-
#
|
|
933
|
-
# pool = get_process_pool()
|
|
934
|
-
#
|
|
935
|
-
# # Load MCP configurations
|
|
936
|
-
# configs = {}
|
|
937
|
-
#
|
|
938
|
-
# # Check .claude.json for MCP server configs
|
|
939
|
-
# claude_config_path = Path.home() / ".claude.json"
|
|
940
|
-
# if not claude_config_path.exists():
|
|
941
|
-
# # Try project-local config
|
|
942
|
-
# claude_config_path = Path.cwd() / ".claude.json"
|
|
943
|
-
#
|
|
944
|
-
# if claude_config_path.exists():
|
|
945
|
-
# try:
|
|
946
|
-
# with claude_config_path.open() as f:
|
|
947
|
-
# config_data = json.load(f)
|
|
948
|
-
# mcp_servers = config_data.get("mcpServers", {})
|
|
949
|
-
# configs.update(mcp_servers)
|
|
950
|
-
# except Exception as e:
|
|
951
|
-
# get_logger("MCPProcessPool").warning(f"Failed to load Claude config: {e}")
|
|
952
|
-
#
|
|
953
|
-
# # Check .mcp.json for additional configs
|
|
954
|
-
# mcp_config_path = Path.cwd() / ".mcp.json"
|
|
955
|
-
# if mcp_config_path.exists():
|
|
956
|
-
# try:
|
|
957
|
-
# with mcp_config_path.open() as f:
|
|
958
|
-
# config_data = json.load(f)
|
|
959
|
-
# mcp_servers = config_data.get("mcpServers", {})
|
|
960
|
-
# configs.update(mcp_servers)
|
|
961
|
-
# except Exception as e:
|
|
962
|
-
# get_logger("MCPProcessPool").warning(f"Failed to load MCP config: {e}")
|
|
963
|
-
#
|
|
964
|
-
# if configs:
|
|
965
|
-
# await pool.pre_warm_servers(configs)
|
|
966
|
-
# await pool.start_health_monitoring()
|
|
967
|
-
#
|
|
968
|
-
# return pool
|
|
969
|
-
|
|
970
|
-
# Return a basic pool instance without pre-warming
|
|
971
|
-
return get_process_pool()
|