claude-mpm 3.9.8__py3-none-any.whl → 3.9.9__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 (44) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/base_agent.json +1 -1
  3. claude_mpm/cli/__init__.py +3 -1
  4. claude_mpm/cli/commands/__init__.py +3 -1
  5. claude_mpm/cli/commands/cleanup.py +21 -1
  6. claude_mpm/cli/commands/mcp.py +821 -0
  7. claude_mpm/cli/parser.py +148 -1
  8. claude_mpm/config/memory_guardian_config.py +325 -0
  9. claude_mpm/constants.py +13 -0
  10. claude_mpm/hooks/claude_hooks/hook_handler.py +76 -19
  11. claude_mpm/models/state_models.py +433 -0
  12. claude_mpm/services/communication/__init__.py +2 -2
  13. claude_mpm/services/communication/socketio.py +18 -16
  14. claude_mpm/services/infrastructure/__init__.py +4 -1
  15. claude_mpm/services/infrastructure/logging.py +3 -3
  16. claude_mpm/services/infrastructure/memory_guardian.py +770 -0
  17. claude_mpm/services/mcp_gateway/__init__.py +28 -12
  18. claude_mpm/services/mcp_gateway/main.py +326 -0
  19. claude_mpm/services/mcp_gateway/registry/__init__.py +6 -3
  20. claude_mpm/services/mcp_gateway/registry/service_registry.py +397 -0
  21. claude_mpm/services/mcp_gateway/registry/tool_registry.py +477 -0
  22. claude_mpm/services/mcp_gateway/server/__init__.py +9 -3
  23. claude_mpm/services/mcp_gateway/server/mcp_server.py +430 -0
  24. claude_mpm/services/mcp_gateway/server/mcp_server_simple.py +444 -0
  25. claude_mpm/services/mcp_gateway/server/stdio_handler.py +373 -0
  26. claude_mpm/services/mcp_gateway/tools/__init__.py +16 -3
  27. claude_mpm/services/mcp_gateway/tools/base_adapter.py +497 -0
  28. claude_mpm/services/mcp_gateway/tools/document_summarizer.py +729 -0
  29. claude_mpm/services/mcp_gateway/tools/hello_world.py +551 -0
  30. claude_mpm/utils/file_utils.py +293 -0
  31. claude_mpm/utils/platform_memory.py +524 -0
  32. claude_mpm/utils/subprocess_utils.py +305 -0
  33. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/METADATA +3 -1
  34. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/RECORD +39 -28
  35. claude_mpm/agents/templates/.claude-mpm/memories/README.md +0 -36
  36. claude_mpm/agents/templates/.claude-mpm/memories/engineer_agent.md +0 -39
  37. claude_mpm/agents/templates/.claude-mpm/memories/qa_agent.md +0 -38
  38. claude_mpm/agents/templates/.claude-mpm/memories/research_agent.md +0 -39
  39. claude_mpm/agents/templates/.claude-mpm/memories/version_control_agent.md +0 -38
  40. /claude_mpm/agents/templates/{research_memory_efficient.json → backup/research_memory_efficient.json} +0 -0
  41. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/WHEEL +0 -0
  42. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/entry_points.txt +0 -0
  43. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/licenses/LICENSE +0 -0
  44. {claude_mpm-3.9.8.dist-info → claude_mpm-3.9.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,524 @@
1
+ """Platform-specific memory monitoring utilities.
2
+
3
+ This module provides fallback methods for monitoring process memory
4
+ when psutil is not available, using platform-specific approaches.
5
+
6
+ Design Principles:
7
+ - Graceful degradation when psutil is unavailable
8
+ - Platform-specific optimizations
9
+ - Consistent interface across platforms
10
+ - Error handling and logging
11
+ """
12
+
13
+ import os
14
+ import platform
15
+ import subprocess
16
+ import re
17
+ import logging
18
+ from typing import Optional, Dict, Any, Tuple
19
+ from pathlib import Path
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class MemoryInfo:
25
+ """Container for memory information."""
26
+
27
+ def __init__(self, rss: int, vms: int, percent: float = 0.0):
28
+ """Initialize memory info.
29
+
30
+ Args:
31
+ rss: Resident Set Size in bytes
32
+ vms: Virtual Memory Size in bytes
33
+ percent: Memory usage as percentage of total
34
+ """
35
+ self.rss = rss
36
+ self.vms = vms
37
+ self.percent = percent
38
+
39
+ @property
40
+ def rss_mb(self) -> float:
41
+ """Get RSS in megabytes."""
42
+ return self.rss / (1024 * 1024)
43
+
44
+ @property
45
+ def vms_mb(self) -> float:
46
+ """Get VMS in megabytes."""
47
+ return self.vms / (1024 * 1024)
48
+
49
+ def to_dict(self) -> Dict[str, Any]:
50
+ """Convert to dictionary."""
51
+ return {
52
+ 'rss_bytes': self.rss,
53
+ 'vms_bytes': self.vms,
54
+ 'rss_mb': self.rss_mb,
55
+ 'vms_mb': self.vms_mb,
56
+ 'percent': self.percent
57
+ }
58
+
59
+
60
+ def get_memory_info_psutil(pid: int) -> Optional[MemoryInfo]:
61
+ """Get memory info using psutil (preferred method).
62
+
63
+ Args:
64
+ pid: Process ID to monitor
65
+
66
+ Returns:
67
+ MemoryInfo object or None if psutil not available or process not found
68
+ """
69
+ try:
70
+ import psutil
71
+ process = psutil.Process(pid)
72
+ mem_info = process.memory_info()
73
+ mem_percent = process.memory_percent()
74
+ return MemoryInfo(
75
+ rss=mem_info.rss,
76
+ vms=mem_info.vms,
77
+ percent=mem_percent
78
+ )
79
+ except ImportError:
80
+ logger.debug("psutil not available")
81
+ return None
82
+ except psutil.NoSuchProcess:
83
+ logger.warning(f"Process {pid} not found")
84
+ return None
85
+ except Exception as e:
86
+ logger.error(f"Error getting memory info with psutil: {e}")
87
+ return None
88
+
89
+
90
+ def get_memory_info_macos(pid: int) -> Optional[MemoryInfo]:
91
+ """Get memory info on macOS using ps command.
92
+
93
+ Args:
94
+ pid: Process ID to monitor
95
+
96
+ Returns:
97
+ MemoryInfo object or None if unable to get info
98
+ """
99
+ try:
100
+ # Use ps to get memory info
101
+ # RSS is in KB, VSZ is in KB
102
+ result = subprocess.run(
103
+ ['ps', '-o', 'rss=,vsz=', '-p', str(pid)],
104
+ capture_output=True,
105
+ text=True,
106
+ timeout=5
107
+ )
108
+
109
+ if result.returncode != 0:
110
+ logger.debug(f"ps command failed for pid {pid}")
111
+ return None
112
+
113
+ output = result.stdout.strip()
114
+ if not output:
115
+ logger.debug(f"No output from ps for pid {pid}")
116
+ return None
117
+
118
+ parts = output.split()
119
+ if len(parts) >= 2:
120
+ rss_kb = int(parts[0])
121
+ vsz_kb = int(parts[1])
122
+
123
+ # Convert KB to bytes
124
+ rss_bytes = rss_kb * 1024
125
+ vms_bytes = vsz_kb * 1024
126
+
127
+ # Try to get total memory for percentage
128
+ percent = 0.0
129
+ try:
130
+ total_result = subprocess.run(
131
+ ['sysctl', '-n', 'hw.memsize'],
132
+ capture_output=True,
133
+ text=True,
134
+ timeout=5
135
+ )
136
+ if total_result.returncode == 0:
137
+ total_bytes = int(total_result.stdout.strip())
138
+ percent = (rss_bytes / total_bytes) * 100
139
+ except Exception:
140
+ pass
141
+
142
+ return MemoryInfo(rss=rss_bytes, vms=vms_bytes, percent=percent)
143
+
144
+ except subprocess.TimeoutExpired:
145
+ logger.warning(f"Timeout getting memory info for pid {pid}")
146
+ except ValueError as e:
147
+ logger.error(f"Error parsing ps output: {e}")
148
+ except Exception as e:
149
+ logger.error(f"Error getting macOS memory info: {e}")
150
+
151
+ return None
152
+
153
+
154
+ def get_memory_info_linux(pid: int) -> Optional[MemoryInfo]:
155
+ """Get memory info on Linux using /proc filesystem.
156
+
157
+ Args:
158
+ pid: Process ID to monitor
159
+
160
+ Returns:
161
+ MemoryInfo object or None if unable to get info
162
+ """
163
+ try:
164
+ status_path = Path(f'/proc/{pid}/status')
165
+ if not status_path.exists():
166
+ logger.debug(f"Process {pid} not found in /proc")
167
+ return None
168
+
169
+ rss_bytes = 0
170
+ vms_bytes = 0
171
+
172
+ with open(status_path, 'r') as f:
173
+ for line in f:
174
+ if line.startswith('VmRSS:'):
175
+ # VmRSS is in KB
176
+ rss_kb = int(line.split()[1])
177
+ rss_bytes = rss_kb * 1024
178
+ elif line.startswith('VmSize:'):
179
+ # VmSize is in KB
180
+ vms_kb = int(line.split()[1])
181
+ vms_bytes = vms_kb * 1024
182
+
183
+ if rss_bytes == 0 and vms_bytes == 0:
184
+ logger.debug(f"No memory info found for pid {pid}")
185
+ return None
186
+
187
+ # Try to get total memory for percentage
188
+ percent = 0.0
189
+ try:
190
+ meminfo_path = Path('/proc/meminfo')
191
+ if meminfo_path.exists():
192
+ with open(meminfo_path, 'r') as f:
193
+ for line in f:
194
+ if line.startswith('MemTotal:'):
195
+ total_kb = int(line.split()[1])
196
+ total_bytes = total_kb * 1024
197
+ percent = (rss_bytes / total_bytes) * 100
198
+ break
199
+ except Exception:
200
+ pass
201
+
202
+ return MemoryInfo(rss=rss_bytes, vms=vms_bytes, percent=percent)
203
+
204
+ except ValueError as e:
205
+ logger.error(f"Error parsing /proc data: {e}")
206
+ except Exception as e:
207
+ logger.error(f"Error getting Linux memory info: {e}")
208
+
209
+ return None
210
+
211
+
212
+ def get_memory_info_windows(pid: int) -> Optional[MemoryInfo]:
213
+ """Get memory info on Windows using wmic or tasklist.
214
+
215
+ Args:
216
+ pid: Process ID to monitor
217
+
218
+ Returns:
219
+ MemoryInfo object or None if unable to get info
220
+ """
221
+ try:
222
+ # Try wmic first (more detailed)
223
+ result = subprocess.run(
224
+ ['wmic', 'process', 'where', f'ProcessId={pid}', 'get',
225
+ 'WorkingSetSize,VirtualSize', '/format:list'],
226
+ capture_output=True,
227
+ text=True,
228
+ timeout=5,
229
+ shell=True
230
+ )
231
+
232
+ if result.returncode == 0:
233
+ output = result.stdout
234
+ rss_bytes = 0
235
+ vms_bytes = 0
236
+
237
+ for line in output.split('\n'):
238
+ line = line.strip()
239
+ if line.startswith('WorkingSetSize='):
240
+ try:
241
+ rss_bytes = int(line.split('=')[1])
242
+ except (IndexError, ValueError):
243
+ pass
244
+ elif line.startswith('VirtualSize='):
245
+ try:
246
+ vms_bytes = int(line.split('=')[1])
247
+ except (IndexError, ValueError):
248
+ pass
249
+
250
+ if rss_bytes > 0 or vms_bytes > 0:
251
+ return MemoryInfo(rss=rss_bytes, vms=vms_bytes)
252
+
253
+ # Fallback to tasklist
254
+ result = subprocess.run(
255
+ ['tasklist', '/FI', f'PID eq {pid}', '/FO', 'CSV'],
256
+ capture_output=True,
257
+ text=True,
258
+ timeout=5,
259
+ shell=True
260
+ )
261
+
262
+ if result.returncode == 0:
263
+ lines = result.stdout.strip().split('\n')
264
+ if len(lines) > 1:
265
+ # Parse CSV output
266
+ # Format: "Image Name","PID","Session Name","Session#","Mem Usage"
267
+ data = lines[1].split('","')
268
+ if len(data) >= 5:
269
+ mem_usage = data[4].rstrip('"')
270
+ # Remove 'K' suffix and convert to bytes
271
+ mem_usage = mem_usage.replace(',', '').replace('K', '').strip()
272
+ if mem_usage.isdigit():
273
+ rss_bytes = int(mem_usage) * 1024
274
+ return MemoryInfo(rss=rss_bytes, vms=rss_bytes) # tasklist only gives working set
275
+
276
+ except subprocess.TimeoutExpired:
277
+ logger.warning(f"Timeout getting memory info for pid {pid}")
278
+ except Exception as e:
279
+ logger.error(f"Error getting Windows memory info: {e}")
280
+
281
+ return None
282
+
283
+
284
+ def get_memory_info_resource(pid: int) -> Optional[MemoryInfo]:
285
+ """Get memory info using resource module (very limited).
286
+
287
+ This only works for the current process and its children,
288
+ not for arbitrary PIDs.
289
+
290
+ Args:
291
+ pid: Process ID to monitor (must be current process or child)
292
+
293
+ Returns:
294
+ MemoryInfo object or None if unable to get info
295
+ """
296
+ try:
297
+ import resource
298
+
299
+ # This only works if pid is the current process
300
+ if pid != os.getpid():
301
+ logger.debug("resource module only works for current process")
302
+ return None
303
+
304
+ usage = resource.getrusage(resource.RUSAGE_SELF)
305
+
306
+ # Convert to bytes (ru_maxrss is in KB on Linux, bytes on macOS)
307
+ if platform.system() == 'Darwin':
308
+ rss_bytes = usage.ru_maxrss
309
+ else:
310
+ rss_bytes = usage.ru_maxrss * 1024
311
+
312
+ # resource module doesn't provide VMS
313
+ return MemoryInfo(rss=rss_bytes, vms=rss_bytes)
314
+
315
+ except Exception as e:
316
+ logger.error(f"Error getting memory info with resource module: {e}")
317
+
318
+ return None
319
+
320
+
321
+ def get_process_memory(pid: int, method: Optional[str] = None) -> Optional[MemoryInfo]:
322
+ """Get memory information for a process using the best available method.
323
+
324
+ Args:
325
+ pid: Process ID to monitor
326
+ method: Specific method to use, or None for auto-detection
327
+
328
+ Returns:
329
+ MemoryInfo object or None if unable to get info
330
+ """
331
+ # If specific method requested, try it
332
+ if method:
333
+ if method == 'psutil':
334
+ return get_memory_info_psutil(pid)
335
+ elif method == 'macos':
336
+ return get_memory_info_macos(pid)
337
+ elif method == 'linux':
338
+ return get_memory_info_linux(pid)
339
+ elif method == 'windows':
340
+ return get_memory_info_windows(pid)
341
+ elif method == 'resource':
342
+ return get_memory_info_resource(pid)
343
+
344
+ # Auto-detect best method
345
+ # Try psutil first (most reliable and cross-platform)
346
+ info = get_memory_info_psutil(pid)
347
+ if info:
348
+ return info
349
+
350
+ # Fall back to platform-specific methods
351
+ system = platform.system()
352
+ if system == 'Darwin':
353
+ info = get_memory_info_macos(pid)
354
+ elif system == 'Linux':
355
+ info = get_memory_info_linux(pid)
356
+ elif system == 'Windows':
357
+ info = get_memory_info_windows(pid)
358
+
359
+ # Last resort: resource module (only for current process)
360
+ if not info and pid == os.getpid():
361
+ info = get_memory_info_resource(pid)
362
+
363
+ if not info:
364
+ logger.warning(f"Unable to get memory info for pid {pid} on {system}")
365
+
366
+ return info
367
+
368
+
369
+ def get_system_memory() -> Tuple[int, int]:
370
+ """Get total and available system memory in bytes.
371
+
372
+ Returns:
373
+ Tuple of (total_bytes, available_bytes)
374
+ """
375
+ try:
376
+ import psutil
377
+ mem = psutil.virtual_memory()
378
+ return mem.total, mem.available
379
+ except ImportError:
380
+ pass
381
+
382
+ # Fallback methods
383
+ system = platform.system()
384
+
385
+ if system == 'Darwin':
386
+ try:
387
+ # Get total memory
388
+ result = subprocess.run(
389
+ ['sysctl', '-n', 'hw.memsize'],
390
+ capture_output=True,
391
+ text=True,
392
+ timeout=5
393
+ )
394
+ total = int(result.stdout.strip()) if result.returncode == 0 else 0
395
+
396
+ # Get free pages and page size for available memory
397
+ vm_stat = subprocess.run(
398
+ ['vm_stat'],
399
+ capture_output=True,
400
+ text=True,
401
+ timeout=5
402
+ )
403
+ if vm_stat.returncode == 0:
404
+ free_pages = 0
405
+ for line in vm_stat.stdout.split('\n'):
406
+ if 'Pages free:' in line:
407
+ free_pages = int(line.split(':')[1].strip().rstrip('.'))
408
+ break
409
+ # Page size is typically 4096 bytes on macOS
410
+ available = free_pages * 4096
411
+ else:
412
+ available = 0
413
+
414
+ return total, available
415
+ except Exception as e:
416
+ logger.error(f"Error getting macOS system memory: {e}")
417
+
418
+ elif system == 'Linux':
419
+ try:
420
+ total = 0
421
+ available = 0
422
+ with open('/proc/meminfo', 'r') as f:
423
+ for line in f:
424
+ if line.startswith('MemTotal:'):
425
+ total = int(line.split()[1]) * 1024 # Convert KB to bytes
426
+ elif line.startswith('MemAvailable:'):
427
+ available = int(line.split()[1]) * 1024
428
+ return total, available
429
+ except Exception as e:
430
+ logger.error(f"Error getting Linux system memory: {e}")
431
+
432
+ elif system == 'Windows':
433
+ try:
434
+ result = subprocess.run(
435
+ ['wmic', 'OS', 'get', 'TotalVisibleMemorySize,FreePhysicalMemory', '/format:list'],
436
+ capture_output=True,
437
+ text=True,
438
+ timeout=5,
439
+ shell=True
440
+ )
441
+ if result.returncode == 0:
442
+ total = 0
443
+ free = 0
444
+ for line in result.stdout.split('\n'):
445
+ line = line.strip()
446
+ if line.startswith('TotalVisibleMemorySize='):
447
+ total = int(line.split('=')[1]) * 1024 # Convert KB to bytes
448
+ elif line.startswith('FreePhysicalMemory='):
449
+ free = int(line.split('=')[1]) * 1024
450
+ return total, free
451
+ except Exception as e:
452
+ logger.error(f"Error getting Windows system memory: {e}")
453
+
454
+ # Unable to determine
455
+ return 0, 0
456
+
457
+
458
+ def check_memory_pressure() -> str:
459
+ """Check system memory pressure status.
460
+
461
+ Returns:
462
+ One of: 'normal', 'warning', 'critical', 'unknown'
463
+ """
464
+ try:
465
+ import psutil
466
+ mem = psutil.virtual_memory()
467
+ percent_used = mem.percent
468
+
469
+ if percent_used > 90:
470
+ return 'critical'
471
+ elif percent_used > 75:
472
+ return 'warning'
473
+ else:
474
+ return 'normal'
475
+ except ImportError:
476
+ pass
477
+
478
+ # Platform-specific checks
479
+ system = platform.system()
480
+
481
+ if system == 'Darwin':
482
+ try:
483
+ # Check macOS memory pressure
484
+ result = subprocess.run(
485
+ ['memory_pressure'],
486
+ capture_output=True,
487
+ text=True,
488
+ timeout=5
489
+ )
490
+ if result.returncode == 0:
491
+ output = result.stdout.lower()
492
+ if 'critical' in output:
493
+ return 'critical'
494
+ elif 'warning' in output:
495
+ return 'warning'
496
+ else:
497
+ return 'normal'
498
+ except Exception:
499
+ pass
500
+
501
+ elif system == 'Linux':
502
+ try:
503
+ # Check if we're close to OOM
504
+ with open('/proc/meminfo', 'r') as f:
505
+ total = 0
506
+ available = 0
507
+ for line in f:
508
+ if line.startswith('MemTotal:'):
509
+ total = int(line.split()[1])
510
+ elif line.startswith('MemAvailable:'):
511
+ available = int(line.split()[1])
512
+
513
+ if total > 0:
514
+ percent_used = ((total - available) / total) * 100
515
+ if percent_used > 90:
516
+ return 'critical'
517
+ elif percent_used > 75:
518
+ return 'warning'
519
+ else:
520
+ return 'normal'
521
+ except Exception:
522
+ pass
523
+
524
+ return 'unknown'