claude-mpm 4.4.4__py3-none-any.whl → 4.4.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/mcp_external_commands.py +7 -7
  3. claude_mpm/cli/commands/mcp_install_commands.py +9 -9
  4. claude_mpm/cli/commands/mcp_setup_external.py +6 -6
  5. claude_mpm/hooks/kuzu_memory_hook.py +4 -2
  6. claude_mpm/services/diagnostics/checks/__init__.py +2 -2
  7. claude_mpm/services/diagnostics/checks/{claude_desktop_check.py → claude_code_check.py} +95 -112
  8. claude_mpm/services/diagnostics/checks/mcp_check.py +6 -6
  9. claude_mpm/services/diagnostics/checks/mcp_services_check.py +97 -22
  10. claude_mpm/services/diagnostics/diagnostic_runner.py +5 -5
  11. claude_mpm/services/diagnostics/doctor_reporter.py +4 -4
  12. claude_mpm/services/mcp_config_manager.py +314 -47
  13. claude_mpm/services/mcp_gateway/core/process_pool.py +11 -8
  14. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +4 -4
  15. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +8 -4
  16. claude_mpm/services/project/project_organizer.py +8 -1
  17. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +1 -2
  18. claude_mpm/services/unified/config_strategies/context_strategy.py +1 -3
  19. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +3 -1
  20. claude_mpm/validation/frontmatter_validator.py +1 -1
  21. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/METADATA +35 -18
  22. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/RECORD +26 -26
  23. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/WHEEL +0 -0
  24. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/entry_points.txt +0 -0
  25. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/licenses/LICENSE +0 -0
  26. {claude_mpm-4.4.4.dist-info → claude_mpm-4.4.6.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,7 @@
2
2
  Check MCP external services installation and health.
3
3
 
4
4
  WHY: Verify that MCP services (mcp-vector-search, mcp-browser, mcp-ticketer, kuzu-memory)
5
- are properly installed and accessible for enhanced Claude Desktop capabilities.
5
+ are properly installed and accessible for enhanced Claude Code capabilities.
6
6
  """
7
7
 
8
8
  import json
@@ -21,30 +21,38 @@ class MCPServicesCheck(BaseDiagnosticCheck):
21
21
  MCP_SERVICES = {
22
22
  "mcp-vector-search": {
23
23
  "package": "mcp-vector-search",
24
- "command": ["mcp-vector-search", "--help"],
24
+ "command": [
25
+ "mcp-vector-search",
26
+ "--version",
27
+ ], # Use --version for proper check
25
28
  "description": "Vector search for semantic code navigation",
26
29
  "check_health": True,
27
30
  "health_command": ["mcp-vector-search", "--version"],
31
+ "pipx_run_command": ["pipx", "run", "mcp-vector-search", "--version"],
28
32
  },
29
33
  "mcp-browser": {
30
34
  "package": "mcp-browser",
31
- "command": ["mcp-browser", "--help"],
35
+ "command": ["mcp-browser", "--version"], # Use --version for proper check
32
36
  "description": "Browser automation and web interaction",
33
37
  "check_health": True,
34
38
  "health_command": ["mcp-browser", "--version"],
39
+ "pipx_run_command": ["pipx", "run", "mcp-browser", "--version"],
35
40
  },
36
41
  "mcp-ticketer": {
37
42
  "package": "mcp-ticketer",
38
- "command": ["mcp-ticketer", "--help"],
43
+ "command": ["mcp-ticketer", "--version"], # Use --version for proper check
39
44
  "description": "Ticket and task management",
40
45
  "check_health": True,
41
46
  "health_command": ["mcp-ticketer", "--version"],
47
+ "pipx_run_command": ["pipx", "run", "mcp-ticketer", "--version"],
42
48
  },
43
49
  "kuzu-memory": {
44
50
  "package": "kuzu-memory",
45
- "command": ["kuzu-memory", "--help"],
51
+ "command": ["kuzu-memory", "--version"], # Use --version for proper check
46
52
  "description": "Graph-based memory system",
47
- "check_health": False, # May not have version command
53
+ "check_health": True, # v1.1.0+ has version command
54
+ "health_command": ["kuzu-memory", "--version"],
55
+ "pipx_run_command": ["pipx", "run", "kuzu-memory", "--version"],
48
56
  },
49
57
  }
50
58
 
@@ -143,6 +151,13 @@ class MCPServicesCheck(BaseDiagnosticCheck):
143
151
  if command_path:
144
152
  details["command_path"] = command_path
145
153
 
154
+ # If not directly accessible, try pipx run command
155
+ if not accessible and "pipx_run_command" in config:
156
+ if self._verify_command_works(config["pipx_run_command"]):
157
+ accessible = True
158
+ details["accessible_via_pipx_run"] = True
159
+ details["pipx_run_available"] = True
160
+
146
161
  # Check for installation in various locations
147
162
  if not pipx_installed and not accessible:
148
163
  # Try common installation locations
@@ -157,11 +172,19 @@ class MCPServicesCheck(BaseDiagnosticCheck):
157
172
 
158
173
  # Check service health/version if accessible
159
174
  if accessible and config.get("check_health"):
160
- version = self._get_service_version(
161
- config.get("health_command", config["command"])
162
- )
163
- if version:
164
- details["version"] = version
175
+ # Try different version commands in order of preference
176
+ version_commands = []
177
+ if details.get("accessible_via_pipx_run") and "pipx_run_command" in config:
178
+ version_commands.append(config["pipx_run_command"])
179
+ if "health_command" in config:
180
+ version_commands.append(config["health_command"])
181
+ version_commands.append(config["command"])
182
+
183
+ for cmd in version_commands:
184
+ version = self._get_service_version(cmd)
185
+ if version:
186
+ details["version"] = version
187
+ break
165
188
 
166
189
  # Determine status
167
190
  if not (pipx_installed or accessible):
@@ -175,6 +198,14 @@ class MCPServicesCheck(BaseDiagnosticCheck):
175
198
  )
176
199
 
177
200
  if pipx_installed and not accessible:
201
+ # Check if pipx run works
202
+ if details.get("pipx_run_available"):
203
+ return DiagnosticResult(
204
+ category=f"MCP Service: {service_name}",
205
+ status=DiagnosticStatus.OK,
206
+ message="Installed via pipx (use 'pipx run' to execute)",
207
+ details=details,
208
+ )
178
209
  return DiagnosticResult(
179
210
  category=f"MCP Service: {service_name}",
180
211
  status=DiagnosticStatus.WARNING,
@@ -247,29 +278,73 @@ class MCPServicesCheck(BaseDiagnosticCheck):
247
278
 
248
279
  if result.returncode == 0:
249
280
  path = result.stdout.strip()
250
- return True, path
281
+ # Verify the command actually works with --version
282
+ if self._verify_command_works(command):
283
+ return True, path
284
+ return False, path
251
285
  except (subprocess.SubprocessError, FileNotFoundError):
252
286
  pass
253
287
 
254
- # Try direct execution
288
+ # Try direct execution with --version
289
+ if self._verify_command_works(command):
290
+ return True, None
291
+
292
+ return False, None
293
+
294
+ def _verify_command_works(self, command: List[str]) -> bool:
295
+ """Verify a command actually works by checking its --version output."""
255
296
  try:
256
297
  result = subprocess.run(
257
298
  command,
258
299
  capture_output=True,
259
300
  text=True,
260
- timeout=2,
301
+ timeout=5,
261
302
  check=False,
262
303
  )
263
- if (
264
- result.returncode == 0
265
- or "help" in result.stdout.lower()
266
- or "usage" in result.stdout.lower()
267
- ):
268
- return True, None
269
- except (subprocess.SubprocessError, FileNotFoundError):
304
+
305
+ # Check for successful execution or version output
306
+ # Don't accept error messages containing "help" or "usage" as success
307
+ if result.returncode == 0:
308
+ # Look for actual version information
309
+ output = (result.stdout + result.stderr).lower()
310
+ # Check for version indicators
311
+ if any(
312
+ keyword in output
313
+ for keyword in ["version", "v1.", "v0.", "1.", "0."]
314
+ ):
315
+ # But reject if it's an error message
316
+ if not any(
317
+ error in output
318
+ for error in [
319
+ "error",
320
+ "not found",
321
+ "no such",
322
+ "command not found",
323
+ ]
324
+ ):
325
+ return True
326
+
327
+ # For some tools, non-zero return code is OK if version is shown
328
+ elif "--version" in command or "--help" in command:
329
+ output = (result.stdout + result.stderr).lower()
330
+ # Must have version info and no error indicators
331
+ if "version" in output or "v1." in output or "v0." in output:
332
+ if not any(
333
+ error in output
334
+ for error in [
335
+ "error",
336
+ "not found",
337
+ "no such",
338
+ "command not found",
339
+ "traceback",
340
+ ]
341
+ ):
342
+ return True
343
+
344
+ except (subprocess.SubprocessError, FileNotFoundError, OSError):
270
345
  pass
271
346
 
272
- return False, None
347
+ return False
273
348
 
274
349
  def _check_alternative_installations(
275
350
  self, service_name: str
@@ -14,7 +14,7 @@ from claude_mpm.core.logging_utils import get_logger
14
14
  from .checks import (
15
15
  AgentCheck,
16
16
  BaseDiagnosticCheck,
17
- ClaudeDesktopCheck,
17
+ ClaudeCodeCheck,
18
18
  CommonIssuesCheck,
19
19
  ConfigurationCheck,
20
20
  FilesystemCheck,
@@ -53,7 +53,7 @@ class DiagnosticRunner:
53
53
  ConfigurationCheck,
54
54
  FilesystemCheck,
55
55
  InstructionsCheck, # Check instruction files early
56
- ClaudeDesktopCheck,
56
+ ClaudeCodeCheck,
57
57
  AgentCheck,
58
58
  MCPCheck,
59
59
  MCPServicesCheck, # Check external MCP services
@@ -121,7 +121,7 @@ class DiagnosticRunner:
121
121
  ]
122
122
  # Level 2: May depend on level 1
123
123
  level2 = [
124
- ClaudeDesktopCheck,
124
+ ClaudeCodeCheck,
125
125
  AgentCheck,
126
126
  MCPCheck,
127
127
  MCPServicesCheck,
@@ -209,8 +209,8 @@ class DiagnosticRunner:
209
209
  "config": ConfigurationCheck,
210
210
  "filesystem": FilesystemCheck,
211
211
  "fs": FilesystemCheck,
212
- "claude": ClaudeDesktopCheck,
213
- "claude_desktop": ClaudeDesktopCheck,
212
+ "claude": ClaudeCodeCheck,
213
+ "claude_code": ClaudeCodeCheck,
214
214
  "agents": AgentCheck,
215
215
  "agent": AgentCheck,
216
216
  "mcp": MCPCheck,
@@ -385,9 +385,9 @@ class DoctorReporter:
385
385
  print(
386
386
  f"| Installation Method | {result.details['installation_method']} |"
387
387
  )
388
- elif result.category == "Claude Desktop":
388
+ elif result.category == "Claude Code":
389
389
  if result.details.get("version"):
390
- print(f"| Claude Desktop | {result.details['version']} |")
390
+ print(f"| Claude Code (CLI) | {result.details['version']} |")
391
391
 
392
392
  def _print_mcp_services_markdown(self, summary: DiagnosticSummary):
393
393
  """Print MCP services status table in markdown."""
@@ -533,11 +533,11 @@ class DoctorReporter:
533
533
  )
534
534
 
535
535
  if (
536
- result.category == "Claude Desktop"
536
+ result.category == "Claude Code"
537
537
  and result.status == DiagnosticStatus.WARNING
538
538
  ):
539
539
  recommendations.append(
540
- "Update Claude Desktop to the latest version for best compatibility"
540
+ "Update Claude Code (CLI) to the latest version for best compatibility"
541
541
  )
542
542
 
543
543
  if recommendations:
@@ -11,6 +11,7 @@ MCP service installations.
11
11
 
12
12
  import json
13
13
  import subprocess
14
+ import sys
14
15
  from datetime import datetime, timezone
15
16
  from enum import Enum
16
17
  from pathlib import Path
@@ -85,7 +86,11 @@ class MCPConfigManager:
85
86
  for path in candidates:
86
87
  try:
87
88
  result = subprocess.run(
88
- [path, "--help"], capture_output=True, text=True, timeout=5, check=False
89
+ [path, "--help"],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=5,
93
+ check=False,
89
94
  )
90
95
  # Check if this version has MCP support
91
96
  if "claude" in result.stdout or "mcp" in result.stdout:
@@ -194,55 +199,172 @@ class MCPConfigManager:
194
199
  """
195
200
  Generate configuration for a specific MCP service.
196
201
 
202
+ Prefers 'pipx run' or 'uvx' commands over direct execution for better isolation.
203
+
197
204
  Args:
198
205
  service_name: Name of the MCP service
199
206
 
200
207
  Returns:
201
208
  Service configuration dict or None if service not found
202
209
  """
203
- service_path = self.detect_service_path(service_name)
204
- if not service_path:
205
- return None
210
+ import shutil
206
211
 
207
- config = {
208
- "type": "stdio",
209
- "command": service_path,
210
- }
212
+ # Check for pipx run first (preferred for isolation)
213
+ use_pipx_run = False
214
+ use_uvx = False
215
+
216
+ # Try pipx run test
217
+ if shutil.which("pipx"):
218
+ try:
219
+ result = subprocess.run(
220
+ ["pipx", "run", service_name, "--version"],
221
+ capture_output=True,
222
+ text=True,
223
+ timeout=5,
224
+ check=False,
225
+ )
226
+ if result.returncode == 0 or "version" in result.stdout.lower():
227
+ use_pipx_run = True
228
+ self.logger.debug(f"Will use 'pipx run' for {service_name}")
229
+ except:
230
+ pass
231
+
232
+ # Try uvx if pipx run not available
233
+ if not use_pipx_run and shutil.which("uvx"):
234
+ try:
235
+ result = subprocess.run(
236
+ ["uvx", service_name, "--version"],
237
+ capture_output=True,
238
+ text=True,
239
+ timeout=5,
240
+ check=False,
241
+ )
242
+ if result.returncode == 0 or "version" in result.stdout.lower():
243
+ use_uvx = True
244
+ self.logger.debug(f"Will use 'uvx' for {service_name}")
245
+ except:
246
+ pass
247
+
248
+ # If neither work, try to find direct path
249
+ service_path = None
250
+ if not use_pipx_run and not use_uvx:
251
+ service_path = self.detect_service_path(service_name)
252
+ if not service_path:
253
+ return None
254
+
255
+ # Build configuration
256
+ config = {"type": "stdio"}
211
257
 
212
258
  # Service-specific configurations
213
259
  if service_name == "mcp-vector-search":
214
- config["args"] = [
215
- "-m",
216
- "mcp_vector_search.mcp.server",
217
- str(self.project_root),
218
- ]
260
+ if use_pipx_run:
261
+ config["command"] = "pipx"
262
+ config["args"] = [
263
+ "run",
264
+ "mcp-vector-search",
265
+ "-m",
266
+ "mcp_vector_search.mcp.server",
267
+ str(self.project_root),
268
+ ]
269
+ elif use_uvx:
270
+ config["command"] = "uvx"
271
+ config["args"] = [
272
+ "mcp-vector-search",
273
+ "-m",
274
+ "mcp_vector_search.mcp.server",
275
+ str(self.project_root),
276
+ ]
277
+ else:
278
+ config["command"] = service_path
279
+ config["args"] = [
280
+ "-m",
281
+ "mcp_vector_search.mcp.server",
282
+ str(self.project_root),
283
+ ]
219
284
  config["env"] = {}
285
+
220
286
  elif service_name == "mcp-browser":
221
- config["args"] = ["mcp"]
287
+ if use_pipx_run:
288
+ config["command"] = "pipx"
289
+ config["args"] = ["run", "mcp-browser", "mcp"]
290
+ elif use_uvx:
291
+ config["command"] = "uvx"
292
+ config["args"] = ["mcp-browser", "mcp"]
293
+ else:
294
+ config["command"] = service_path
295
+ config["args"] = ["mcp"]
222
296
  config["env"] = {"MCP_BROWSER_HOME": str(Path.home() / ".mcp-browser")}
297
+
223
298
  elif service_name == "mcp-ticketer":
224
- config["args"] = ["mcp"]
225
- elif service_name == "kuzu-memory":
226
- # Check kuzu-memory version to determine correct command
227
- # v1.1.0+ has "claude mcp-server", v1.0.0 has "serve"
228
- import subprocess
299
+ if use_pipx_run:
300
+ config["command"] = "pipx"
301
+ config["args"] = ["run", "mcp-ticketer", "mcp"]
302
+ elif use_uvx:
303
+ config["command"] = "uvx"
304
+ config["args"] = ["mcp-ticketer", "mcp"]
305
+ else:
306
+ config["command"] = service_path
307
+ config["args"] = ["mcp"]
229
308
 
230
- try:
231
- result = subprocess.run(
232
- [service_path, "--help"], capture_output=True, text=True, timeout=10, check=False
233
- )
234
- if "claude" in result.stdout:
235
- # v1.1.0+ with claude command
236
- config["args"] = ["claude", "mcp-server"]
237
- else:
238
- # v1.0.0 with serve command
239
- config["args"] = ["serve"]
240
- except:
241
- # Default to older version command
242
- config["args"] = ["serve"]
243
- # kuzu-memory works with project-specific databases, no custom path needed
309
+ elif service_name == "kuzu-memory":
310
+ # Determine kuzu-memory command version
311
+ kuzu_args = ["mcp", "serve"] # Default to the correct modern format
312
+ test_cmd = None
313
+
314
+ if use_pipx_run:
315
+ test_cmd = ["pipx", "run", "kuzu-memory", "--help"]
316
+ elif use_uvx:
317
+ test_cmd = ["uvx", "kuzu-memory", "--help"]
318
+ elif service_path:
319
+ test_cmd = [service_path, "--help"]
320
+
321
+ if test_cmd:
322
+ try:
323
+ result = subprocess.run(
324
+ test_cmd,
325
+ capture_output=True,
326
+ text=True,
327
+ timeout=10,
328
+ check=False,
329
+ )
330
+ # Check for MCP support in help output
331
+ help_output = result.stdout.lower() + result.stderr.lower()
332
+
333
+ # Modern version detection - look for "mcp serve" command
334
+ if "mcp serve" in help_output or ("mcp" in help_output and "serve" in help_output):
335
+ # Modern version with mcp serve command
336
+ kuzu_args = ["mcp", "serve"]
337
+ # Legacy version detection - only "serve" without "mcp"
338
+ elif "serve" in help_output and "mcp" not in help_output:
339
+ # Very old version that only has serve command
340
+ kuzu_args = ["serve"]
341
+ # Note: "claude mcp-server" format is deprecated and not used
342
+ else:
343
+ # Default to the correct modern format
344
+ kuzu_args = ["mcp", "serve"]
345
+ except Exception:
346
+ # Default to the correct mcp serve command on any error
347
+ kuzu_args = ["mcp", "serve"]
348
+
349
+ if use_pipx_run:
350
+ config["command"] = "pipx"
351
+ config["args"] = ["run", "kuzu-memory"] + kuzu_args
352
+ elif use_uvx:
353
+ config["command"] = "uvx"
354
+ config["args"] = ["kuzu-memory"] + kuzu_args
355
+ else:
356
+ config["command"] = service_path
357
+ config["args"] = kuzu_args
358
+
359
+ # Generic config for unknown services
360
+ elif use_pipx_run:
361
+ config["command"] = "pipx"
362
+ config["args"] = ["run", service_name]
363
+ elif use_uvx:
364
+ config["command"] = "uvx"
365
+ config["args"] = [service_name]
244
366
  else:
245
- # Generic config for unknown services
367
+ config["command"] = service_path
246
368
  config["args"] = []
247
369
 
248
370
  return config
@@ -464,7 +586,7 @@ class MCPConfigManager:
464
586
 
465
587
  def install_missing_services(self) -> Tuple[bool, str]:
466
588
  """
467
- Install missing MCP services via pipx.
589
+ Install missing MCP services via pipx with verification and fallbacks.
468
590
 
469
591
  Returns:
470
592
  Tuple of (success, message)
@@ -481,22 +603,167 @@ class MCPConfigManager:
481
603
  failed = []
482
604
 
483
605
  for service_name in missing:
484
- try:
485
- self.logger.info(f"Installing {service_name} via pipx...")
486
- subprocess.run(
487
- ["pipx", "install", service_name],
488
- capture_output=True,
489
- text=True,
490
- check=True,
491
- )
492
- installed.append(service_name)
493
- self.logger.info(f"Successfully installed {service_name}")
494
- except subprocess.CalledProcessError as e:
606
+ # Try pipx install first
607
+ success, method = self._install_service_with_fallback(service_name)
608
+ if success:
609
+ installed.append(f"{service_name} ({method})")
610
+ self.logger.info(f"Successfully installed {service_name} via {method}")
611
+ else:
495
612
  failed.append(service_name)
496
- self.logger.error(f"Failed to install {service_name}: {e.stderr}")
613
+ self.logger.error(f"Failed to install {service_name}")
497
614
 
498
615
  if failed:
499
616
  return False, f"Failed to install: {', '.join(failed)}"
500
617
  if installed:
501
618
  return True, f"Successfully installed: {', '.join(installed)}"
502
619
  return True, "No services needed installation"
620
+
621
+ def _install_service_with_fallback(self, service_name: str) -> Tuple[bool, str]:
622
+ """
623
+ Install a service with multiple fallback methods.
624
+
625
+ Returns:
626
+ Tuple of (success, installation_method)
627
+ """
628
+ import shutil
629
+
630
+ # Method 1: Try pipx install
631
+ if shutil.which("pipx"):
632
+ try:
633
+ self.logger.debug(f"Attempting to install {service_name} via pipx...")
634
+ result = subprocess.run(
635
+ ["pipx", "install", service_name],
636
+ capture_output=True,
637
+ text=True,
638
+ timeout=120, # 2 minute timeout
639
+ check=False,
640
+ )
641
+
642
+ if result.returncode == 0:
643
+ # Verify installation worked
644
+ if self._verify_service_installed(service_name, "pipx"):
645
+ return True, "pipx"
646
+
647
+ self.logger.warning(
648
+ f"pipx install succeeded but verification failed for {service_name}"
649
+ )
650
+ else:
651
+ self.logger.debug(f"pipx install failed: {result.stderr}")
652
+ except subprocess.TimeoutExpired:
653
+ self.logger.warning(f"pipx install timed out for {service_name}")
654
+ except Exception as e:
655
+ self.logger.debug(f"pipx install error: {e}")
656
+
657
+ # Method 2: Try uvx (if available)
658
+ if shutil.which("uvx"):
659
+ try:
660
+ self.logger.debug(f"Attempting to install {service_name} via uvx...")
661
+ result = subprocess.run(
662
+ ["uvx", "install", service_name],
663
+ capture_output=True,
664
+ text=True,
665
+ timeout=120,
666
+ check=False,
667
+ )
668
+
669
+ if result.returncode == 0:
670
+ if self._verify_service_installed(service_name, "uvx"):
671
+ return True, "uvx"
672
+ except Exception as e:
673
+ self.logger.debug(f"uvx install error: {e}")
674
+
675
+ # Method 3: Try pip install --user
676
+ try:
677
+ self.logger.debug(f"Attempting to install {service_name} via pip --user...")
678
+ result = subprocess.run(
679
+ [sys.executable, "-m", "pip", "install", "--user", service_name],
680
+ capture_output=True,
681
+ text=True,
682
+ timeout=120,
683
+ check=False,
684
+ )
685
+
686
+ if result.returncode == 0:
687
+ if self._verify_service_installed(service_name, "pip"):
688
+ return True, "pip --user"
689
+
690
+ self.logger.warning(
691
+ f"pip install succeeded but verification failed for {service_name}"
692
+ )
693
+ except Exception as e:
694
+ self.logger.debug(f"pip install error: {e}")
695
+
696
+ return False, "none"
697
+
698
+ def _verify_service_installed(self, service_name: str, method: str) -> bool:
699
+ """
700
+ Verify that a service was successfully installed and is functional.
701
+
702
+ Args:
703
+ service_name: Name of the service
704
+ method: Installation method used
705
+
706
+ Returns:
707
+ True if service is installed and functional
708
+ """
709
+ import time
710
+
711
+ # Give the installation a moment to settle
712
+ time.sleep(1)
713
+
714
+ # Check if we can find the service
715
+ service_path = self.detect_service_path(service_name)
716
+ if not service_path:
717
+ # Try pipx run as fallback for pipx installations
718
+ if method == "pipx":
719
+ try:
720
+ result = subprocess.run(
721
+ ["pipx", "run", service_name, "--version"],
722
+ capture_output=True,
723
+ text=True,
724
+ timeout=10,
725
+ check=False,
726
+ )
727
+ if result.returncode == 0 or "version" in result.stdout.lower():
728
+ self.logger.debug(f"{service_name} accessible via 'pipx run'")
729
+ return True
730
+ except:
731
+ pass
732
+ return False
733
+
734
+ # Try to verify it works
735
+ try:
736
+ # Different services may need different verification
737
+ test_commands = [
738
+ [service_path, "--version"],
739
+ [service_path, "--help"],
740
+ ]
741
+
742
+ for cmd in test_commands:
743
+ result = subprocess.run(
744
+ cmd,
745
+ capture_output=True,
746
+ text=True,
747
+ timeout=10,
748
+ check=False,
749
+ )
750
+
751
+ output = (result.stdout + result.stderr).lower()
752
+ # Check for signs of success
753
+ if result.returncode == 0:
754
+ return True
755
+ # Some tools return non-zero but still work
756
+ if any(
757
+ indicator in output
758
+ for indicator in ["version", "usage", "help", service_name.lower()]
759
+ ):
760
+ # Make sure it's not an error message
761
+ if not any(
762
+ error in output
763
+ for error in ["error", "not found", "traceback", "no such"]
764
+ ):
765
+ return True
766
+ except Exception as e:
767
+ self.logger.debug(f"Verification error for {service_name}: {e}")
768
+
769
+ return False