claude-mpm 4.0.19__py3-none-any.whl → 4.0.22__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 (61) hide show
  1. claude_mpm/BUILD_NUMBER +1 -1
  2. claude_mpm/VERSION +1 -1
  3. claude_mpm/__main__.py +4 -0
  4. claude_mpm/agents/BASE_AGENT_TEMPLATE.md +38 -2
  5. claude_mpm/agents/INSTRUCTIONS.md +74 -0
  6. claude_mpm/agents/OUTPUT_STYLE.md +84 -0
  7. claude_mpm/agents/WORKFLOW.md +308 -4
  8. claude_mpm/agents/agents_metadata.py +52 -0
  9. claude_mpm/agents/base_agent_loader.py +75 -19
  10. claude_mpm/agents/templates/__init__.py +4 -0
  11. claude_mpm/agents/templates/api_qa.json +206 -0
  12. claude_mpm/agents/templates/qa.json +1 -1
  13. claude_mpm/agents/templates/research.json +24 -16
  14. claude_mpm/agents/templates/ticketing.json +18 -5
  15. claude_mpm/agents/templates/vercel_ops_agent.json +281 -0
  16. claude_mpm/agents/templates/vercel_ops_instructions.md +582 -0
  17. claude_mpm/cli/__init__.py +23 -1
  18. claude_mpm/cli/__main__.py +4 -0
  19. claude_mpm/cli/commands/mcp_command_router.py +87 -1
  20. claude_mpm/cli/commands/mcp_install_commands.py +207 -26
  21. claude_mpm/cli/commands/memory.py +32 -5
  22. claude_mpm/cli/commands/run.py +33 -6
  23. claude_mpm/cli/parsers/base_parser.py +5 -0
  24. claude_mpm/cli/parsers/mcp_parser.py +23 -0
  25. claude_mpm/cli/parsers/run_parser.py +5 -0
  26. claude_mpm/cli/utils.py +17 -4
  27. claude_mpm/constants.py +1 -0
  28. claude_mpm/core/base_service.py +8 -2
  29. claude_mpm/core/config.py +122 -32
  30. claude_mpm/core/framework_loader.py +385 -34
  31. claude_mpm/core/interactive_session.py +77 -12
  32. claude_mpm/core/oneshot_session.py +7 -1
  33. claude_mpm/core/output_style_manager.py +468 -0
  34. claude_mpm/core/unified_paths.py +190 -21
  35. claude_mpm/hooks/claude_hooks/hook_handler.py +91 -16
  36. claude_mpm/hooks/claude_hooks/hook_wrapper.sh +3 -0
  37. claude_mpm/init.py +1 -0
  38. claude_mpm/scripts/socketio_daemon.py +67 -7
  39. claude_mpm/scripts/socketio_daemon_hardened.py +897 -0
  40. claude_mpm/services/agents/deployment/agent_deployment.py +216 -10
  41. claude_mpm/services/agents/deployment/agent_template_builder.py +37 -1
  42. claude_mpm/services/agents/deployment/async_agent_deployment.py +65 -1
  43. claude_mpm/services/agents/deployment/multi_source_deployment_service.py +441 -0
  44. claude_mpm/services/agents/memory/__init__.py +0 -2
  45. claude_mpm/services/agents/memory/agent_memory_manager.py +577 -44
  46. claude_mpm/services/agents/memory/content_manager.py +144 -14
  47. claude_mpm/services/agents/memory/template_generator.py +7 -354
  48. claude_mpm/services/mcp_gateway/server/stdio_server.py +61 -169
  49. claude_mpm/services/memory_hook_service.py +62 -4
  50. claude_mpm/services/runner_configuration_service.py +5 -9
  51. claude_mpm/services/socketio/server/broadcaster.py +32 -1
  52. claude_mpm/services/socketio/server/core.py +4 -0
  53. claude_mpm/services/socketio/server/main.py +23 -4
  54. claude_mpm/services/subprocess_launcher_service.py +5 -0
  55. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/METADATA +1 -1
  56. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/RECORD +60 -54
  57. claude_mpm/services/agents/memory/analyzer.py +0 -430
  58. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/WHEEL +0 -0
  59. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/entry_points.txt +0 -0
  60. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/licenses/LICENSE +0 -0
  61. {claude_mpm-4.0.19.dist-info → claude_mpm-4.0.22.dist-info}/top_level.txt +0 -0
@@ -64,42 +64,179 @@ class DeploymentContext(Enum):
64
64
  class PathContext:
65
65
  """Handles deployment context detection and path resolution."""
66
66
 
67
+ @staticmethod
68
+ def _is_editable_install() -> bool:
69
+ """Check if the current installation is editable (development mode).
70
+
71
+ This checks for various indicators of an editable/development installation:
72
+ - Presence of pyproject.toml in parent directories
73
+ - .pth files pointing to the source directory
74
+ - Direct source installation (src/ directory structure)
75
+ - Current working directory is within a development project
76
+ """
77
+ try:
78
+ import claude_mpm
79
+ module_path = Path(claude_mpm.__file__).parent
80
+
81
+ # Check if we're in a src/ directory structure with pyproject.toml
82
+ current = module_path
83
+ for _ in range(5): # Check up to 5 levels up
84
+ if (current / "pyproject.toml").exists():
85
+ # Found pyproject.toml, check if this looks like a development setup
86
+ if (current / "src" / "claude_mpm").exists():
87
+ logger.debug(f"Found development installation at {current}")
88
+ return True
89
+ if current == current.parent:
90
+ break
91
+ current = current.parent
92
+
93
+ # Additional check: If we're running from within a claude-mpm development directory
94
+ # This handles the case where pipx claude-mpm is invoked from within the dev directory
95
+ cwd = Path.cwd()
96
+ current = cwd
97
+ for _ in range(5): # Check up to 5 levels up from current directory
98
+ if (current / "pyproject.toml").exists() and (current / "src" / "claude_mpm").exists():
99
+ # Check if this is the claude-mpm project
100
+ try:
101
+ pyproject_content = (current / "pyproject.toml").read_text()
102
+ if "claude-mpm" in pyproject_content and "claude_mpm" in pyproject_content:
103
+ logger.debug(f"Running from within claude-mpm development directory: {current}")
104
+ # Verify this is a development setup by checking for key files
105
+ if (current / "scripts" / "claude-mpm").exists():
106
+ return True
107
+ except Exception:
108
+ pass
109
+ if current == current.parent:
110
+ break
111
+ current = current.parent
112
+
113
+ # Check for .pth files indicating editable install
114
+ try:
115
+ import site
116
+ for site_dir in site.getsitepackages():
117
+ site_path = Path(site_dir)
118
+ if site_path.exists():
119
+ # Check for .pth files
120
+ for pth_file in site_path.glob("*.pth"):
121
+ try:
122
+ content = pth_file.read_text()
123
+ # Check if the .pth file points to our module's parent
124
+ if str(module_path.parent) in content or str(module_path) in content:
125
+ logger.debug(f"Found editable install via .pth file: {pth_file}")
126
+ return True
127
+ except Exception:
128
+ continue
129
+
130
+ # Check for egg-link files
131
+ for egg_link in site_path.glob("*egg-link"):
132
+ if "claude" in egg_link.name.lower():
133
+ try:
134
+ content = egg_link.read_text()
135
+ if str(module_path.parent) in content or str(module_path) in content:
136
+ logger.debug(f"Found editable install via egg-link: {egg_link}")
137
+ return True
138
+ except Exception:
139
+ continue
140
+ except ImportError:
141
+ pass
142
+
143
+ except Exception as e:
144
+ logger.debug(f"Error checking for editable install: {e}")
145
+
146
+ return False
147
+
67
148
  @staticmethod
68
149
  @lru_cache(maxsize=1)
69
150
  def detect_deployment_context() -> DeploymentContext:
70
- """Detect the current deployment context."""
151
+ """Detect the current deployment context.
152
+
153
+ Priority order:
154
+ 1. Environment variable override (CLAUDE_MPM_DEV_MODE)
155
+ 2. Current working directory is a claude-mpm development project
156
+ 3. Editable installation detection
157
+ 4. Path-based detection (development, pipx, system, pip)
158
+ """
159
+ # Check for environment variable override
160
+ if os.environ.get("CLAUDE_MPM_DEV_MODE", "").lower() in ("1", "true", "yes"):
161
+ logger.info("Development mode forced via CLAUDE_MPM_DEV_MODE environment variable")
162
+ return DeploymentContext.DEVELOPMENT
163
+
164
+ # Check if current working directory is a claude-mpm development project
165
+ # This handles the case where pipx claude-mpm is run from within the dev directory
166
+ cwd = Path.cwd()
167
+ current = cwd
168
+ for _ in range(5): # Check up to 5 levels up from current directory
169
+ if (current / "pyproject.toml").exists() and (current / "src" / "claude_mpm").exists():
170
+ # Check if this is the claude-mpm project
171
+ try:
172
+ pyproject_content = (current / "pyproject.toml").read_text()
173
+ if 'name = "claude-mpm"' in pyproject_content or '"claude-mpm"' in pyproject_content:
174
+ logger.info(f"Detected claude-mpm development directory at {current}")
175
+ logger.info("Using development mode for local source preference")
176
+ return DeploymentContext.DEVELOPMENT
177
+ except Exception:
178
+ pass
179
+ if current == current.parent:
180
+ break
181
+ current = current.parent
182
+
71
183
  try:
72
184
  import claude_mpm
73
-
74
185
  module_path = Path(claude_mpm.__file__).parent
75
-
76
- # Check for development mode
186
+
187
+ # First check if this is an editable install, regardless of path
188
+ # This is important for cases where pipx points to a development installation
189
+ if PathContext._is_editable_install():
190
+ logger.info(f"Detected editable/development installation")
191
+ # Check if we should use development paths
192
+ # This could be because we're in a src/ directory or running from dev directory
193
+ if module_path.parent.name == "src":
194
+ return DeploymentContext.DEVELOPMENT
195
+ elif "pipx" in str(module_path):
196
+ # Running via pipx but from within a development directory
197
+ # Use development mode to prefer local source over pipx installation
198
+ cwd = Path.cwd()
199
+ current = cwd
200
+ for _ in range(5):
201
+ if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
202
+ logger.info(f"Running pipx from development directory, using development mode")
203
+ return DeploymentContext.DEVELOPMENT
204
+ if current == current.parent:
205
+ break
206
+ current = current.parent
207
+ return DeploymentContext.EDITABLE_INSTALL
208
+ else:
209
+ return DeploymentContext.EDITABLE_INSTALL
210
+
211
+ # Check for development mode based on directory structure
77
212
  # module_path is typically /path/to/project/src/claude_mpm
78
- # So we need to check if /path/to/project/src exists (module_path.parent)
79
- # and if /path/to/project/src/claude_mpm exists (module_path itself)
80
213
  if (module_path.parent.name == "src" and
81
214
  (module_path.parent.parent / "src" / "claude_mpm").exists()):
215
+ logger.info(f"Detected development mode via directory structure at {module_path}")
82
216
  return DeploymentContext.DEVELOPMENT
83
217
 
84
- # Check for editable install
85
- if (
86
- "site-packages" in str(module_path)
87
- and (module_path.parent.parent / "src").exists()
88
- ):
89
- return DeploymentContext.EDITABLE_INSTALL
90
-
91
218
  # Check for pipx install
92
219
  if "pipx" in str(module_path):
220
+ logger.info(f"Detected pipx installation at {module_path}")
93
221
  return DeploymentContext.PIPX_INSTALL
94
222
 
95
223
  # Check for system package
96
224
  if "dist-packages" in str(module_path):
225
+ logger.info(f"Detected system package installation at {module_path}")
97
226
  return DeploymentContext.SYSTEM_PACKAGE
227
+
228
+ # Check for site-packages (could be pip or editable)
229
+ if "site-packages" in str(module_path):
230
+ # Already checked for editable above, so this is a regular pip install
231
+ logger.info(f"Detected pip installation at {module_path}")
232
+ return DeploymentContext.PIP_INSTALL
98
233
 
99
234
  # Default to pip install
235
+ logger.info(f"Defaulting to pip installation for {module_path}")
100
236
  return DeploymentContext.PIP_INSTALL
101
237
 
102
238
  except ImportError:
239
+ logger.info("ImportError during context detection, defaulting to development")
103
240
  return DeploymentContext.DEVELOPMENT
104
241
 
105
242
 
@@ -143,8 +280,8 @@ class UnifiedPathManager:
143
280
  ]
144
281
  self._initialized = True
145
282
 
146
- logger.debug(
147
- f"UnifiedPathManager initialized with context: {self._deployment_context}"
283
+ logger.info(
284
+ f"UnifiedPathManager initialized with context: {self._deployment_context.value}"
148
285
  )
149
286
 
150
287
  # ========================================================================
@@ -160,11 +297,38 @@ class UnifiedPathManager:
160
297
 
161
298
  module_path = Path(claude_mpm.__file__).parent
162
299
 
163
- if self._deployment_context == DeploymentContext.DEVELOPMENT:
164
- # Development: go up to project root
300
+ if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
301
+ # For development mode, first check if we're running from within a dev directory
302
+ # This handles the case where pipx is invoked from a development directory
303
+ cwd = Path.cwd()
304
+ current = cwd
305
+ for _ in range(5):
306
+ if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
307
+ # Verify this is the claude-mpm project
308
+ try:
309
+ pyproject_content = (current / "pyproject.toml").read_text()
310
+ if "claude-mpm" in pyproject_content:
311
+ logger.debug(f"Found framework root via cwd at {current}")
312
+ return current
313
+ except Exception:
314
+ pass
315
+ if current == current.parent:
316
+ break
317
+ current = current.parent
318
+
319
+ # Development or editable install: go up to project root from module
165
320
  current = module_path
166
321
  while current != current.parent:
167
- if (current / "src" / "claude_mpm").exists():
322
+ if (current / "src" / "claude_mpm").exists() and (current / "pyproject.toml").exists():
323
+ logger.debug(f"Found framework root at {current}")
324
+ return current
325
+ current = current.parent
326
+
327
+ # Secondary check: Look for pyproject.toml without src structure
328
+ current = module_path
329
+ while current != current.parent:
330
+ if (current / "pyproject.toml").exists():
331
+ logger.debug(f"Found framework root (no src) at {current}")
168
332
  return current
169
333
  current = current.parent
170
334
 
@@ -202,9 +366,14 @@ class UnifiedPathManager:
202
366
  @property
203
367
  def package_root(self) -> Path:
204
368
  """Get the claude_mpm package root directory."""
369
+ if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
370
+ # In development mode, always use the source directory
371
+ package_path = self.framework_root / "src" / "claude_mpm"
372
+ if package_path.exists():
373
+ return package_path
374
+
205
375
  try:
206
376
  import claude_mpm
207
-
208
377
  return Path(claude_mpm.__file__).parent
209
378
  except ImportError:
210
379
  return self.framework_root / "src" / "claude_mpm"
@@ -246,7 +415,7 @@ class UnifiedPathManager:
246
415
  elif scope == "project":
247
416
  return self.get_project_config_dir() / "agents"
248
417
  elif scope == "framework":
249
- if self._deployment_context == DeploymentContext.DEVELOPMENT:
418
+ if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
250
419
  return self.framework_root / "src" / "claude_mpm" / "agents"
251
420
  else:
252
421
  return self.package_root / "agents"
@@ -277,7 +446,7 @@ class UnifiedPathManager:
277
446
 
278
447
  def get_scripts_dir(self) -> Path:
279
448
  """Get the scripts directory."""
280
- if self._deployment_context == DeploymentContext.DEVELOPMENT:
449
+ if self._deployment_context in (DeploymentContext.DEVELOPMENT, DeploymentContext.EDITABLE_INSTALL):
281
450
  return self.framework_root / "scripts"
282
451
  else:
283
452
  return self.package_root / "scripts"
@@ -65,6 +65,10 @@ except ImportError:
65
65
  _global_handler = None
66
66
  _handler_lock = threading.Lock()
67
67
 
68
+ # Track recent events to detect duplicates
69
+ _recent_events = deque(maxlen=10)
70
+ _events_lock = threading.Lock()
71
+
68
72
 
69
73
  class ClaudeHookHandler:
70
74
  """Optimized hook handler with direct Socket.IO client.
@@ -288,12 +292,16 @@ class ClaudeHookHandler:
288
292
  - Always continues regardless of event status
289
293
  - Process exits after handling to prevent accumulation
290
294
  """
295
+ _continue_sent = False # Track if continue has been sent
291
296
 
292
297
  def timeout_handler(signum, frame):
293
298
  """Handle timeout by forcing exit."""
299
+ nonlocal _continue_sent
294
300
  if DEBUG:
295
301
  print(f"Hook handler timeout (pid: {os.getpid()})", file=sys.stderr)
296
- self._continue_execution()
302
+ if not _continue_sent:
303
+ self._continue_execution()
304
+ _continue_sent = True
297
305
  sys.exit(0)
298
306
 
299
307
  try:
@@ -304,8 +312,35 @@ class ClaudeHookHandler:
304
312
  # Read and parse event
305
313
  event = self._read_hook_event()
306
314
  if not event:
307
- self._continue_execution()
315
+ if not _continue_sent:
316
+ self._continue_execution()
317
+ _continue_sent = True
308
318
  return
319
+
320
+ # Check for duplicate events (same event within 100ms)
321
+ global _recent_events, _events_lock
322
+ event_key = self._get_event_key(event)
323
+ current_time = time.time()
324
+
325
+ with _events_lock:
326
+ # Check if we've seen this event recently
327
+ for recent_key, recent_time in _recent_events:
328
+ if recent_key == event_key and (current_time - recent_time) < 0.1:
329
+ if DEBUG:
330
+ print(f"[{datetime.now().isoformat()}] Skipping duplicate event: {event.get('hook_event_name', 'unknown')} (PID: {os.getpid()})", file=sys.stderr)
331
+ # Still need to output continue for this invocation
332
+ if not _continue_sent:
333
+ self._continue_execution()
334
+ _continue_sent = True
335
+ return
336
+
337
+ # Not a duplicate, record it
338
+ _recent_events.append((event_key, current_time))
339
+
340
+ # Debug: Log that we're processing an event
341
+ if DEBUG:
342
+ hook_type = event.get("hook_event_name", "unknown")
343
+ print(f"\n[{datetime.now().isoformat()}] Processing hook event: {hook_type} (PID: {os.getpid()})", file=sys.stderr)
309
344
 
310
345
  # Increment event counter and perform periodic cleanup
311
346
  self.events_processed += 1
@@ -320,12 +355,16 @@ class ClaudeHookHandler:
320
355
  # Route event to appropriate handler
321
356
  self._route_event(event)
322
357
 
323
- # Always continue execution
324
- self._continue_execution()
358
+ # Always continue execution (only if not already sent)
359
+ if not _continue_sent:
360
+ self._continue_execution()
361
+ _continue_sent = True
325
362
 
326
- except:
327
- # Fail fast and silent
328
- self._continue_execution()
363
+ except Exception:
364
+ # Fail fast and silent (only send continue if not already sent)
365
+ if not _continue_sent:
366
+ self._continue_execution()
367
+ _continue_sent = True
329
368
  finally:
330
369
  # Cancel the alarm
331
370
  signal.alarm(0)
@@ -402,6 +441,35 @@ class ClaudeHookHandler:
402
441
  if DEBUG:
403
442
  print(f"Error handling {hook_type}: {e}", file=sys.stderr)
404
443
 
444
+ def _get_event_key(self, event: dict) -> str:
445
+ """Generate a unique key for an event to detect duplicates.
446
+
447
+ WHY: Claude Code may call the hook multiple times for the same event
448
+ because the hook is registered for multiple event types. We need to
449
+ detect and skip duplicate processing while still returning continue.
450
+ """
451
+ # Create a key from event type, session_id, and key data
452
+ hook_type = event.get("hook_event_name", "unknown")
453
+ session_id = event.get("session_id", "")
454
+
455
+ # Add type-specific data to make the key unique
456
+ if hook_type == "PreToolUse":
457
+ tool_name = event.get("tool_name", "")
458
+ # For some tools, include parameters to distinguish calls
459
+ if tool_name == "Task":
460
+ tool_input = event.get("tool_input", {})
461
+ agent = tool_input.get("subagent_type", "")
462
+ prompt_preview = (tool_input.get("prompt", "") or tool_input.get("description", ""))[:50]
463
+ return f"{hook_type}:{session_id}:{tool_name}:{agent}:{prompt_preview}"
464
+ else:
465
+ return f"{hook_type}:{session_id}:{tool_name}"
466
+ elif hook_type == "UserPromptSubmit":
467
+ prompt_preview = event.get("prompt", "")[:50]
468
+ return f"{hook_type}:{session_id}:{prompt_preview}"
469
+ else:
470
+ # For other events, just use type and session
471
+ return f"{hook_type}:{session_id}"
472
+
405
473
  def _continue_execution(self) -> None:
406
474
  """
407
475
  Send continue action to Claude.
@@ -893,24 +961,26 @@ class ClaudeHookHandler:
893
961
  def main():
894
962
  """Entry point with singleton pattern and proper cleanup."""
895
963
  global _global_handler
964
+ _continue_printed = False # Track if we've already printed continue
896
965
 
897
966
  def cleanup_handler(signum=None, frame=None):
898
967
  """Cleanup handler for signals and exit."""
968
+ nonlocal _continue_printed
899
969
  if DEBUG:
900
970
  print(
901
971
  f"Hook handler cleanup (pid: {os.getpid()}, signal: {signum})",
902
972
  file=sys.stderr,
903
973
  )
904
- # Always output continue action to not block Claude
905
- print(json.dumps({"action": "continue"}))
906
- # Only exit if this is a signal handler call, not atexit
907
- if signum is not None:
974
+ # Only output continue if we haven't already (i.e., if interrupted by signal)
975
+ if signum is not None and not _continue_printed:
976
+ print(json.dumps({"action": "continue"}))
977
+ _continue_printed = True
908
978
  sys.exit(0)
909
979
 
910
980
  # Register cleanup handlers
911
981
  signal.signal(signal.SIGTERM, cleanup_handler)
912
982
  signal.signal(signal.SIGINT, cleanup_handler)
913
- atexit.register(cleanup_handler)
983
+ # Don't register atexit handler since we're handling exit properly in main
914
984
 
915
985
  try:
916
986
  # Use singleton pattern to prevent creating multiple instances
@@ -931,14 +1001,19 @@ def main():
931
1001
 
932
1002
  handler = _global_handler
933
1003
 
1004
+ # Mark that handle() will print continue
934
1005
  handler.handle()
1006
+ _continue_printed = True # Mark as printed since handle() always prints it
935
1007
 
936
- # Ensure we exit after handling
937
- cleanup_handler()
1008
+ # handler.handle() already calls _continue_execution(), so we don't need to do it again
1009
+ # Just exit cleanly
1010
+ sys.exit(0)
938
1011
 
939
1012
  except Exception as e:
940
- # Always output continue action to not block Claude
941
- print(json.dumps({"action": "continue"}))
1013
+ # Only output continue if not already printed
1014
+ if not _continue_printed:
1015
+ print(json.dumps({"action": "continue"}))
1016
+ _continue_printed = True
942
1017
  # Log error for debugging
943
1018
  if DEBUG:
944
1019
  print(f"Hook handler error: {e}", file=sys.stderr)
@@ -57,3 +57,6 @@ if ! "$PYTHON_CMD" -m claude_mpm.hooks.claude_hooks.hook_handler "$@" 2>/tmp/hoo
57
57
  echo "[$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)] Hook handler failed, see /tmp/hook-error.log" >> /tmp/hook-wrapper.log
58
58
  exit 0
59
59
  fi
60
+
61
+ # Success - Python handler already printed continue, just exit
62
+ exit 0
claude_mpm/init.py CHANGED
@@ -47,6 +47,7 @@ class ProjectInitializer:
47
47
  self.user_dir / "logs",
48
48
  self.user_dir / "templates",
49
49
  self.user_dir / "registry",
50
+ self.user_dir / "memories", # Add user-level memories directory
50
51
  ]
51
52
 
52
53
  for directory in directories:
@@ -1,5 +1,3 @@
1
- from pathlib import Path
2
-
3
1
  #!/usr/bin/env python3
4
2
  """
5
3
  Pure Python daemon management for Socket.IO server.
@@ -12,6 +10,64 @@ import signal
12
10
  import subprocess
13
11
  import sys
14
12
  import time
13
+ from pathlib import Path
14
+
15
+ # Detect and use virtual environment Python if available
16
+ def get_python_executable():
17
+ """
18
+ Get the appropriate Python executable, preferring virtual environment.
19
+
20
+ WHY: The daemon must use the same Python environment as the parent process
21
+ to ensure all dependencies are available. System Python won't have the
22
+ required packages installed.
23
+ """
24
+ # First, check if we're already in a virtual environment
25
+ if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
26
+ # We're in a virtual environment, use its Python
27
+ return sys.executable
28
+
29
+ # Check for common virtual environment indicators
30
+ # 1. VIRTUAL_ENV environment variable (most common)
31
+ venv_path = os.environ.get('VIRTUAL_ENV')
32
+ if venv_path:
33
+ venv_python = Path(venv_path) / 'bin' / 'python'
34
+ if venv_python.exists():
35
+ return str(venv_python)
36
+
37
+ # 2. Check if current executable is in a venv directory structure
38
+ exe_path = Path(sys.executable).resolve()
39
+ for parent in exe_path.parents:
40
+ # Check for common venv directory names
41
+ if parent.name in ('venv', '.venv', 'env', '.env'):
42
+ # This looks like a virtual environment
43
+ return sys.executable
44
+
45
+ # Check for typical venv structure (bin/python or Scripts/python.exe)
46
+ if parent.name == 'bin' and (parent.parent / 'pyvenv.cfg').exists():
47
+ return sys.executable
48
+ if parent.name == 'Scripts' and (parent.parent / 'pyvenv.cfg').exists():
49
+ return sys.executable
50
+
51
+ # 3. Try to detect project-specific venv
52
+ # Look for venv in the project root (going up from script location)
53
+ script_path = Path(__file__).resolve()
54
+ for parent in script_path.parents:
55
+ # Stop at src or when we've gone too far up
56
+ if parent.name == 'src' or not (parent / 'src').exists():
57
+ # Check for venv directories
58
+ for venv_name in ('venv', '.venv', 'env', '.env'):
59
+ venv_dir = parent / venv_name
60
+ if venv_dir.exists():
61
+ venv_python = venv_dir / 'bin' / 'python'
62
+ if venv_python.exists():
63
+ return str(venv_python)
64
+ break
65
+
66
+ # Fall back to current Python executable
67
+ return sys.executable
68
+
69
+ # Store the detected Python executable for daemon usage
70
+ PYTHON_EXECUTABLE = get_python_executable()
15
71
 
16
72
  import psutil
17
73
 
@@ -145,11 +201,12 @@ def start_server():
145
201
 
146
202
  ensure_dirs()
147
203
 
148
- # Fork to create daemon
204
+ # Fork to create daemon using the correct Python environment
149
205
  pid = os.fork()
150
206
  if pid > 0:
151
207
  # Parent process
152
208
  print(f"Starting Socket.IO server on port {selected_port} (PID: {pid})...")
209
+ print(f"Using Python: {PYTHON_EXECUTABLE}")
153
210
 
154
211
  # Register the instance
155
212
  instance_id = port_manager.register_instance(selected_port, pid)
@@ -179,10 +236,13 @@ def start_server():
179
236
  os.dup2(log.fileno(), sys.stdout.fileno())
180
237
  os.dup2(log.fileno(), sys.stderr.fileno())
181
238
 
182
- # Start server
239
+ # Log environment information for debugging
183
240
  print(
184
241
  f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Starting Socket.IO server on port {selected_port}..."
185
242
  )
243
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python executable: {sys.executable}")
244
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python version: {sys.version}")
245
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Python path: {sys.path[:3]}...") # Show first 3 entries
186
246
  server = SocketIOServer(host="localhost", port=selected_port)
187
247
 
188
248
  # Handle signals
@@ -384,12 +444,12 @@ def main():
384
444
 
385
445
 
386
446
  if __name__ == "__main__":
387
- # Install psutil if not available
447
+ # Install psutil if not available (using correct Python)
388
448
  try:
389
449
  import psutil
390
450
  except ImportError:
391
- print("Installing psutil...")
392
- subprocess.check_call([sys.executable, "-m", "pip", "install", "psutil"])
451
+ print(f"Installing psutil using {PYTHON_EXECUTABLE}...")
452
+ subprocess.check_call([PYTHON_EXECUTABLE, "-m", "pip", "install", "psutil"])
393
453
  import psutil
394
454
 
395
455
  main()